Files
Jyotisha/docs/tasks/TASK-people-archive-p1-20260924.md
T

14 KiB
Raw Blame History

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)

  1. 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 连带删除。

开工前置命令

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,开工时核对。