# TASK · 星盘档案(多人物)P1:人物统一 + 标题人名切换 + 功能跟人走(2026-09-24) 原型(形态参考,**以本文决策为准**,原型里的侧栏顶部切换、关系、「全部 / 只看当前人」筛选、首页小字均已被产品否掉):https://claude.ai/artifact/8EFiEZBbD8JnToHfdwZej2 ## 基线 - `origin/staging = b9756176`。分支 `codex/people-archive-p1-20260924`。 - **串行(本单最后做)**:同日待领取单与本单大面积重叠——`TASK-home-slow-network-20260924`(`page.tsx`)、`TASK-chart-ephemeris-fixes-20260924`(`/api/ephemeris`、星盘空状态 `?settings=chart`)、`TASK-report-reader-actions-20260924`(报告列表)、`TASK-chart-dasha-western-redesign-20260924`(星盘页)。**这四单全部合入后再开工**,基线以开工时 `origin/staging` 为准。 - 动表:必须真跑 `npm run test:db --prefix frontend`(需 Docker);数据库迁移部署前要先执行 Migrate Staging Database。 ## 事故实证(按 `origin/staging` e0db23f4 调查) **数据** 1. 户主资料在 `public.profiles`(带 `reported_birth_time` 不可改、`active_birth_*` 只由校正 RPC 写入等触发器守卫)。 2. 其他人在 `public.chart_profiles`(`20260718100000_repair_missing_chart_profiles.sql`):`id, user_id, role('self'|'other'), profile jsonb`。**出生资料全在无校验的 jsonb 里**,没有 reported / active 区分和守卫;`/api/chart-profiles`(GET / POST、`[id]` PUT / DELETE)只校验"是对象 + 属于本人";**无数量上限**。关系 `chartRelationship` 只在 JSON 里。 3. 户主还有一份 `chart_profiles role='self'` 镜像:`use-profile-onboarding.ts` `persistProfile` 写入,但读取时被 `chartLibraryFromCloudOthers` 丢掉、由 `buildSelfChartRecord` 从 `profile` 重建——写而不读。 **各功能对"他人"的支持** | 功能 | 现状 | 证据 | | --- | --- | --- | | 普通对话 | ✅ 真按所选人排盘(BUG-997/1000)| `consultation-subject-resolver.ts` `resolveConsultationSubject`;`/api/consult` `loadOwnedChartProfile`;首问后 `subject_locked` | | 生时校正 | ❌ 只读 / 只写 `profiles` | `rectification-agentic/v9/case-service.ts` `loadV9RectificationProfile`;RPC `accept_/confirm_agentic_rectification_candidate_for_case_v2` | | 个人报告 | ❌ `chartProfileId` 收了、校验了、存了,**不用于取资料** | `api/reports/route.ts`、`personal-report-route-core.ts`;客户端 `buildPersonalReportCreateRequest` 不发 | | 星盘页 | ❌ 只 `profiles` | `chart-view-service.ts` `loadChartView` → `prepareChartViewProfile` → `resolveServerOwnedChartBirth` | | 星历 | ❌ 只 `profiles` | `api/ephemeris/route.ts` `globalBirthProfileFromAccountRow` | | 今日星语 | ❌ 服务端丢弃请求体读 `profiles` | `api/daily-starlanguage/route.ts` | | 合盘 | ⚠️ 只能「我 × 他人」 | `api/synastry/route.ts` | **遗留「当前星盘 / 设为默认」造成的错位(Bug)** 4. `page.tsx` `activeChartId`(localStorage `jyotisha_active_chart:${accountId}`,`home-cloud-sync.ts` `activeChartStorageKey`)+ 一个 effect **把页面级 `profile` 换成别人的**;`makeDefaultChart` 同样 `setProfile(record.profile)`。服务端从不读它,但它仍喂给:`use-synastry.ts` `buildSynastryQuestion(profile, …)`(合盘问题里"我的资料"写成别人)、首页今日星语卡的可用判断、`use-consultation-run.ts` 的咨询模式 / 完整度门、`use-rectification-surface.ts` `chartSnapshotForSession(activeChartId, …)`(**校正会话标签显示别人名字、实际算户主**)、个人资料昵称。设置里「当前默认」徽章又写死在户主行。 ## 根因 - 多人物只在对话一条链上打通(BUG-997/1000 明确"报告、每日星语、生时校正另立任务");其余功能都直接读 `profiles`。 - 他人资料没有与户主同级的服务端真值与守卫,不能直接接到报告 / 星盘这些"结论型"功能上。 - 「设为默认」是旧模型残留,只改客户端全局 `profile`,与服务端真值分叉。 ## 决策记录(产品 2026-09-24) - **D1 每个星盘档案 = 一个人,户主本人也是其中一个**,统一列表、统一操作;户主不可删。 - **D2 最多 5 人(含本人)**:他人最多 4 个;数据库与接口双重限制,第 6 个时添加按钮禁用并提示「最多保存 5 个人的星盘」。 - **D3 不要「关系」字段**:档案只有称呼 + 出生资料(日期、时间与精度、地点 / 时区、岁差)。UI 删关系选择;存量 JSON 里的 `chartRelationship` 忽略、不迁移。 - **D4 切换入口 = 标题旁的人名**:复用现有对话标题旁的人名组件(`ChatProfilePicker`),扩成全局「当前人物」切换器 `SubjectSwitcher`,同一组件放在:对话标题旁、星盘 / 星历 / 我的报告 / 星盘档案页标题旁。**侧栏顶部不放切换器。**切换后全局生效。 - 在已发过消息的对话里点人名切到别人 = 切换当前人物 **并新建一个空对话**(首问锁定规则不变);空对话里点 = 直接改绑。 - **D5 全功能跟随当前人物**:星盘页、星历(本命叠加)、生成个人报告、今日星语 / 今日节奏、新建对话默认人物 = 当前人物(**推翻 BUG-1000「新建对话固定本人」**)。 - **D6 历史只显示当前人物的**:侧栏会话列表、报告列表都只列当前人物的记录;切人,列表跟着换。列表项不标人名,不做"全部"筛选。 - **D7 计费**:给任何人对话 / 报告 / 校正都按正常规则扣点,不区分本人与他人。 - **D8 档案管理 = 侧栏独立页「星盘档案」** `/people`(与星盘 / 星历 / 我的报告并列,`○ Static`),列表 + 详情;详情里有「和 TA 对话 / 看星盘 / 生成报告」快捷入口和「和我合盘」(沿用现有合盘能力)。**设置弹窗删除「星盘资料」面板**;`?settings=chart`(若 chart-ephemeris-fixes T3 已加)改为跳 `/people`。 - **D9 删除一个人 = 连同他的对话与报告一起删除**,删除前显示「将同时删除 N 个对话和 M 份报告,无法恢复。」;服务端事务内删除,删除后服务器不保留其出生资料。当前人物被删 → 回到本人。 - **D10 首页开场不加任何"在看某某"提示**,问候保持原样(今日星语内容随当前人物变)。 - **D11 取消「设为默认」与 `activeChartId`**:删除 localStorage 键与"替换页面 `profile`"的 effect;当前人物只作为请求参数传给服务端,**任何客户端代码不得再用他人资料覆盖页面级 `profile`**。 - **D12 生时校正本单不跟人**(P2):当前人物不是本人时,生时校正入口禁用,文案「生时校正暂时只支持本人」。任意两人合盘也在 P2。 - **D13(架构,Claude 决定)**:P1 **不把户主迁出 `profiles`**——校正 RPC、`resolveServerOwnedChartBirth`、首页引导都依赖它,迁移风险与本单收益不成比例。产品层面"户主也是其中一个人"通过统一的服务端解析实现:新增唯一入口 `resolveSubjectBirth({ userId, subjectId })`:`self` → 现有 `profiles` 真值链;其他 → `chart_profiles` 新增的**类型化列**(见 T1)。户主的 `chart_profiles role='self'` 镜像停止写入并在迁移里删除。 ## 硬红线 1. 服务端永不信任客户端出生资料;所有功能的出生资料只来自 `resolveSubjectBirth`,按 `user_id` 校验所有权,失败 fail-closed(不回落本人、不扣点),沿用 `consultation-subject-resolver` 的失败码。 2. 他人类型化列具备与户主同级的守卫:`reported_*` 写入后不可改;`active_*` 只能由(P2 的)校正流程写,P1 客户端与 `/api/chart-profiles` 不得写 `active_*` / `birth_time_status = accepted|confirmed`。 3. 解析函数只一处实现;星盘 / 星历 / 报告 / 今日星语 / 对话都调用它,不得各写一份读取。 4. 缓存键(星盘页 `secondary-page-data` 身份、服务端引擎缓存、今日星语指纹)必须包含 `subjectId`,切人不得闪现上一个人的盘。 5. `page.tsx` 不得净增行、`Home()` 不新增 useState / useRef;`jyotish_api_server.py` 不增长。 6. `/`、`/chart`、`/ephemeris`、`/people` 为 `○ Static`;首屏 gzip ±2%。 7. 隐私:fixture / 测试 / 文档只用虚构人物;日志不记姓名、生日、坐标。 8. 既有断言三栏;测试总数不低于基线;不改 workflow / DNS。 ## 任务分解 ### T1 · 数据层(迁移 + 服务端) - 迁移:`chart_profiles` 增类型化列(`name, birth_date, reported_birth_time, birth_time_source, birth_time_period, declared_window_start/end, uncertainty_before/after_minutes, latitude, longitude, timezone_id, timezone_offset, birth_place_label/type/provider/provider_id, ayanamsa, active_birth_time, active_birth_date, active_birth_timezone_offset, birth_time_status`)+ 守卫触发器(仿 `guard_birth_time_journey` / `zz_guard_adopted_birth_date`);从现有 `profile` jsonb **回填**;删 `role='self'` 行;每用户 `role='other'` 行数 ≤ 4 的约束(触发器)。jsonb 列保留只读一版以便回滚,下一轮再删。 - 删除级联:`delete_chart_subject(id)` RPC(事务内删 `chat_sessions where chart_profile_id = id`、`personal_reports where chart_profile_id = id`、该档案),与计数查询 `chart_subject_usage(id)`。 - `frontend/src/lib/subject-birth.ts`(新)`resolveSubjectBirth`;`/api/chart-profiles` 改读写类型化列并做字段校验(日期 / 时间 / 坐标 / 时区合法、长度上限),POST 超限返回 409 `subject_limit_reached`。 验收:`npm run test:db` 新增用例——回填后类型化列与原 JSON 一致;第 5 个他人插入被拒;`reported_birth_time` 更新被拒;客户端写 `active_*` 被拒;删除 RPC 连带删会话与报告且只删本人的;跨用户读写被 RLS 拒绝。 ### T2 · 功能跟人走(服务端 + 请求参数) - `/api/chart-view`、`/api/ephemeris`、`/api/reports`(POST 用 `chartProfileId` 取资料)、`/api/daily-starlanguage`、`/api/consult`(改为经 `resolveSubjectBirth`)都接受 `subjectId`(缺省 `self`),经 `resolveSubjectBirth` 取资料。 - 列表:`GET /api/sessions?subject=`、`GET /api/reports?subject=` 服务端过滤(`self` 匹配 `chart_profile_id is null or = 'self'` 的存量行)。 - 生时校正入口在非本人时禁用(D12)。 验收:路由测试——各接口 `subjectId=他人` 时引擎收到的是该人的出生资料(fixture 用虚构人物);他人不存在 / 不属于本人 → fail-closed 且不扣点;列表按人过滤正确;存量本人会话在 `subject=self` 下可见。 ### T3 · 当前人物与切换器(前端) - `frontend/src/lib/current-subject.ts`(新):当前人物 id(localStorage 按账户存,读到无效 id 回落 `self`),订阅式 store,不进 `Home()` 状态。 - `SubjectSwitcher`(由 `ChatProfilePicker` 抽出):对话标题旁、星盘 / 星历 / 我的报告 / 星盘档案页标题旁;D4 的锁定对话行为;列表最多 5 人。 - 删除 `activeChartId`、`activeChartStorageKey`、`makeDefaultChart`、替换 `profile` 的 effect、「当前默认」徽章;`use-synastry.ts` 改为服务端取"我"的资料;`use-rectification-surface.ts` 会话绑定固定 `self`。 - 新建对话默认人物 = 当前人物(`NEW_CHAT_SUBJECT_ID` 改为读 store)。 验收:切人后星盘页 / 星历 / 报告列表 / 侧栏会话同时换人且不闪旧盘;锁定对话里切人 → 新空对话;`grep -rn "activeChartId\|activeChartStorageKey\|makeDefaultChart" frontend/src` = 0;合盘问题文本中"我"为本人。 ### T4 · 星盘档案页 `/people`(前端) - 侧栏新增入口;列表(本人第一)+ 详情(称呼、出生时间两格:填报时间 / 排盘时间、地点与岁差、快捷入口、和我合盘、删除);添加 / 编辑表单复用 `ChartProfileForm`(删关系字段);第 5 人上限提示;删除确认显示计数(D9)。 - 设置弹窗删除「星盘资料」面板与相关导航;`?settings=chart` → `/people`。 验收:`/people` `○ Static`;添加第 5 个他人(总数第 6)被禁用;删除确认计数来自 `chart_subject_usage`;删除当前人物后回到本人;设置弹窗不再有星盘资料。 ### T5 · 记录 - `docs/BUG_HISTORY.md`:**BUG-1030** 「设为默认」/`activeChartId` 覆盖页面 `profile` 致合盘问题与校正会话标签错人(关联 BUG-466、997、1000)。其余为功能,进 CHANGELOG。 - `CONTEXT.md` 术语:新增「人物 / 星盘档案 / 当前人物」,替换「当前星盘」。 - DESIGN(星盘档案页、SubjectSwitcher、设置弹窗面板变更)、VOICE(「最多保存 5 个人的星盘」「生时校正暂时只支持本人」「将同时删除 N 个对话和 M 份报告,无法恢复。」)、CHANGELOG、PROGRESS、`docs/testing/people-archive-p1-20260924.md`(加 4 个虚构人物 → 第 5 个被拦;切人后四个页面同步;锁定对话切人开新对话;删人连带删除;本人不可删;校正入口禁用;旧账号升级后资料不丢)。 ## 让步顺序 1. 类型化列回填遇到脏 JSON(缺字段)时,允许该行保持"资料不完整"状态并在档案页提示补全,不得猜值。 2. 若 `/api/consult` 改走 `resolveSubjectBirth` 牵动过大,允许 consult 暂保留现有 resolver,但其读他人资料必须改读类型化列。 3. 列表服务端过滤若与 `SessionListProvider` 分页冲突,允许按人物分开缓存键重新拉取。 4. 不得让步:红线 1、2、3;D11(清除 activeChartId);D9 连带删除。 ## 开工前置命令 ```bash git fetch origin --prune git log --oneline origin/staging | head -20 # 确认四个前置单已合入 git worktree add -b codex/people-archive-p1-20260924 .worktrees/people-archive-p1-20260924 origin/staging cd .worktrees/people-archive-p1-20260924/frontend ./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -5 npm run test:db # 需 Docker;无 Docker 写 BLOCKED 并停在 T1 之前 ``` ## BUG 编号 写单时最大 BUG-1020,1021–1028 已预留,1029 留给大运 / 西洋单备用;本单 **BUG-1030**,开工时核对。