diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 5a3540eb..c1f5fb83 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -56,6 +56,8 @@ | `TASK-rectification-family-collect-continue-fix-20260905.md` | `PROGRESS-rectification-family-collect-continue-20260905.md` | `998406ce` 修复单:家人之后的 dated 补采集无条件抢在现成区分卡之前(choice-card A/B 卡用例回归),另两条顺序断言缺三栏说明 | 已验收通过(浏览器走查欠) | `cd704a92`(BUG-547) | | `TASK-rectification-probe-answer-covers-domain-20260905.md` | `PROGRESS-rectification-probe-answer-covers-domain-20260905.md` | 区分卡答 A/B 后紧接着又问同领域"哪年…"采集题:点选答案只进推断层不写账本,计划层领域覆盖只看账本;改为 yes/weak_yes 视为该领域采集已覆盖(不写账本、不动门) | 待验收 | `988d97ae`(BUG-549) | | `TASK-composer-live-input-and-stop-20260906.md` | `PROGRESS-composer-live-input-and-stop-20260906.md` | 生成中输入框整个禁用致焦点丢失、回车丢消息(改为永不禁用 + 排队发送);生时校正停止呈现为红色告警且选择题等待中停止无效(改为中性停止态、所有 fetch 挂 abort) | 待执行 | `codex/composer-live-input-and-stop-20260906`(BUG-551、552) | +| `TASK-session-list-title-and-order-20260906.md` | `PROGRESS-session-list-title-and-order-20260906.md` | 历史对话标题改为首轮模型总结(一次、不扣点数、校正/今日运势保持日期标题)、侧栏去资料前缀;排序只按置顶排且元数据 PATCH 推进 `updated_at`(改为置顶 + 活动时间,改名/收藏/换模型不动顺序);历史区加 今天/昨天/7天/30天/更早 分组 | 待执行(串行:在 composer 单之后) | `codex/session-list-title-and-order-20260906`(BUG-553) | +| `TASK-settings-dialog-and-billing-pane-20260906.md` | `PROGRESS-settings-dialog-and-billing-pane-20260906.md` | 设置弹窗尺寸随分区跳变、星盘资料格无条件铺开整张添加表单(改为固定尺寸四分区、列表→详情);「账户与点数」成为弹窗分区,删除 `/membership` 与 `/membership/orders` 页面并重定向,七处入口改回调 | 待执行(串行:在会话列表单之后) | `codex/settings-dialog-and-billing-pane-20260906`(BUG-554) | | `TASK-rectification-collect-vs-offer-consistency-20260905.md` | `PROGRESS-rectification-collect-vs-offer-consistency-20260905.md` | 带年份采集没问完就出采用卡 + 报告,同一轮又被搬家采集题把卡挤掉:决策层判 `adopt_representative` 而计划层仍有 dated 采集(BUG-546 只修了一半);改为剩余采集未完保持 `collect_evidence`,出牌轮才出卡写报告 | 已验收通过 `ca4e2408`(2026-09-05;staging 部署仍停在 `afd14948`,`deploy-staging` 自 `c295b853` 起连续失败,先解决 `bab07187` 的待迁移) | `codex/rectification-collect-vs-offer-consistency-20260905`(BUG-550) | | `TASK-api-not-configured-mislabel-20260904.md` | — | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 待执行 | `codex/api-not-configured-mislabel-20260904`(BUG-542 起) | | `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) | diff --git a/docs/tasks/TASK-session-list-title-and-order-20260906.md b/docs/tasks/TASK-session-list-title-and-order-20260906.md new file mode 100644 index 00000000..5445d217 --- /dev/null +++ b/docs/tasks/TASK-session-list-title-and-order-20260906.md @@ -0,0 +1,99 @@ +# TASK · 历史对话:模型总结式标题、按活动时间排序、日期分组(2026-09-06) + +- 基线:`origin/staging` @ `985c3258` +- 分支:`codex/session-list-title-and-order-20260906` +- 执行方:coding agent;验收:Claude +- 涉及文件:`frontend/src/app/api/consult/route.ts`(只加一次调用与一个事件,新逻辑进独立模块)、新建 `frontend/src/lib/session-title-agent.ts`、新建 `frontend/src/lib/session-groups.ts`、`frontend/src/hooks/use-consultation-run.ts`、`frontend/src/hooks/use-session-management.ts`、`frontend/src/app/api/sessions/[id]/route.ts`、`frontend/src/lib/home-profile.ts`、`frontend/src/components/app-sidebar.tsx`、`frontend/src/app/page.tsx`(只改 `visibleSessions` 的排序一行,**不得增行**)、`frontend/DESIGN.md`、`frontend/docs/VOICE.md` +- BUG 编号起点:**BUG-553**(开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核;BUG-551/552 已由 `TASK-composer-live-input-and-stop-20260906.md` 预留,BUG-542 由 `TASK-api-not-configured-mislabel-20260904.md` 预留) +- 串行:本单改 `page.tsx` 与 `use-consultation-run.ts`。同日三份单的顺序是 **composer(551/552)→ 本单 → 设置弹窗单**;本单开工前先把已合入 staging 的 composer 分支拉进来,不得并行改同一文件。 +- 不改数据库结构;不动迁移;不改 `/api/sessions` 列表 SQL。 + +## 1. 事故实证(staging,2026-09-06,产品实测) + +| # | 用户看到 | 期望(对标 Claude 的历史列表) | +| --- | --- | --- | +| 1 | 标题就是第一句话的前 14 个字加省略号(如「我想问一下最近半年换工…」),侧栏里再前缀一个资料名,一行根本读不出主题 | 一个 6–12 字的主题总结(「半年内换工作时机」),一眼能分辨 | +| 2 | 刚聊过的会话不在最上面;改个名、收藏一下、换个模型,刷新后旧会话反而跳到最顶 | 只有真正的对话活动改变顺序;置顶始终在前 | +| 3 | 历史列表是一根长条,没有今天 / 昨天 / 更早的分段 | 按时间分组 | + +## 2. 根因(按符号定位,行号以基线为准) + +**标题** + +1. 服务端 `append_consultation_question` RPC(迁移 `20260901010000_append_consultation_question.sql` L84–90)在标题为空或「新对话」时直接 `left(question, 14) || '…'`;`consultation-reply-metadata.ts::safeQuestionTitle` 是同一规则的 TS 版,作为 `parseAgentReply` 的兜底元数据。 +2. 客户端 `use-consultation-run.ts` L883–889:`reply.title && !isGenericSessionTitle(reply.title) ? resolveSessionTitle(question, reply.title, …) : userSession.title`。`reply.title` 只来自正文里的 `` 注释(`agent-reply.ts` L42),而当前没有任何提示词让模型输出它——这条「模型起名」链路早已断掉,只剩空壳。`agent-reply.ts::resolveSessionTitle` L87–110 的 `modelTitle` 分支、`clipTitle(14)`、`uniquifySessionTitle` 都还在,可直接复用。 +3. 侧栏 `home-profile.ts::sessionSidebarTitle` L106 把每条都拼成「资料名 · 标题」,而对话页头部 `chat-header-subtitle`(`page.tsx` L1806)已经显示「分析对象」,侧栏这一层前缀只是把标题挤没。 + +**排序** + +4. `/api/sessions` 按 `updated_at desc` 返回,但 `page.tsx` L380–392 的 `visibleSessions` 只 `.sort((l, r) => Number(r.pinned) - Number(l.pinned))`——不按 `updatedAt` 排。页面开着期间新活动只改 `updatedAt` 不改位置;`Array.prototype.sort` 稳定,顺序冻结到下次整页加载。 +5. `api/sessions/[id]/route.ts::metadataUpdateValues` L31 对任何元数据 PATCH(改名、收藏、归档、换模型、换资料)都写 `updated_at = now()`;`use-session-management.ts::renameSession` L188 也在客户端顺手 `updatedAt: timestamp()`。于是非对话操作会让会话在下次加载时跳到最顶。发消息真正的 bump 在 `append_consultation_question` 的 `updated_at = clock_timestamp()`,这条是对的。 +6. `app-sidebar.tsx` L113–114 只分「收藏 / 历史」两组,没有时间分组。 + +## 3. 决策记录(产品已授权,2026-09-06) + +1. **模型起名,产品同意增加模型开销**:一个会话只起一次名,用该会话当前选的模型(与 `rectification-adopt-narration-agent.ts` 一样走 `ResolvedLanguageModel`),最多 30 个输出 token,**不扣用户点数**、不进用户可见的用量;计量记录按 adopt-narration 既有做法处理,没有既有做法就不记。 +2. 起名只在**咨询会话的第一轮**(`storedHistory` 为空)且标题仍是自动派生值时触发;生时校正会话(「M月D日 · 生时校正」)和今日运势入口(「M月D日 · 今日节奏」)保持日期式标题,不调模型——它们是重复型会话,日期比总结更有用。 +3. 起名与主回答**并行**:在首轮开始时基于用户问题(加主题标签、分析对象是本人还是他人)就起,不等回答结束,避免拖慢回答;成功后通过流事件 `session.title` 推给客户端,并由服务端写库。写库带守卫:只在标题仍等于本轮开始时读到的值时更新,用户中途改名以用户为准。失败、超时(8 秒,计时器必须 ref,见 BUG-523)、输出不合格 → 静默保留现有标题,不重试、不报错。 +4. 标题合格标准(服务端清洗,纯函数):去引号、书名号、句末标点与空白;6–14 个字(Han 计数,`clipTitle(14)` 兜底);不含换行;不是 `isGenericSessionTitle`;**不含出生日期 / 时间 / 地点**(提示词禁止复述这些,清洗层再拦一次含 4 位年份+月日或 HH:MM 的输出)。不合格视同失败。 +5. **侧栏只显示标题**:删除 `sessionSidebarTitle` 的「资料名 · 」前缀。分析对象不是本人(`chartProfileRole !== "self"`)时在标题下加一行小字显示资料名;是本人不显示。产品偏好是删多余入口,不是再叠一层。 +6. **排序规则**:置顶在前;组内按 `updatedAt` 倒序;`updatedAt` 只由**对话活动**推进(发问、回答落库、校正会话的回合落库)。改名、收藏、归档、换模型、换资料一律不动 `updatedAt`(服务端 PATCH 不再写 `updated_at`;客户端相应操作不再 `timestamp()`)。 +7. **日期分组**:只对「历史」组分段:今天 / 昨天 / 最近 7 天 / 最近 30 天 / 更早,按浏览器本地时间、以 `updatedAt` 归组;空组不渲染;「收藏」组不分段。分组标题用现有 overline 字号,不加新的视觉元素。 +8. 不在本单:改名仍用 `window.prompt`(Claude 是行内改名)——产品若要,另开单;`` 注释解析留着当防御,不动。 + +## 4. 硬红线 + +- `page.tsx` 不得增行(现 2041 行);排序、分组、起名全部进 `lib/` / `hooks/`。 +- 起名调用不得阻塞主回答流的首个 `answer.delta`,不得共享主回答的 AbortController 之外的取消语义(用户停止回答时起名一并取消)。 +- 起名失败不得在 UI 出现任何提示;不得出现 spinner / 「正在生成标题」。 +- 不改 `append_consultation_question` / `complete_consultation_response`;不加迁移。 +- 不放宽 `chat-session-write-contract.ts` 的 PATCH 字段集合;`title` 仍由客户端在完成后 PATCH 持久化(L906 `persistSession(completedSession)` 不变)。 +- 既有测试总数不降;改动的断言写「原值 / 新值 / 原因」三栏。 +- `next build` 后 `/` 仍 Static;首屏 gzip ±2%。 +- 任务书 / 进度 / Bug 历史 / 测试 fixture 只用虚构问题文本,不得出现真实用户的问题、昵称、邮箱。 + +## 5. 任务分解 + +### 5.1 模型起名(CHANGELOG 项,不编 BUG) +- 新建 `frontend/src/lib/session-title-agent.ts`:导出 `generateSessionTitle({ model, question, theme, chartRole, signal })` 与纯函数 `sanitizeSessionTitle(raw): string | null`(决策 4)。Agent 结构照抄 `adopt-narration-agent.ts` 的 ref'd 超时包装。 +- `api/consult/route.ts`:首轮且标题为自动值时(判定放进 `session-title-agent.ts::shouldGenerateSessionTitle(session, storedHistory)`),在主回答启动的同一处并行发起;完成后 (a) 向流写事件 `{ type: "session.title", title }`;(b) `update chat_sessions set title = $1 where id = $2 and user_id = $3 and title = $4`(`$4` = 本轮开始读到的标题)。两步任一失败只 `console.warn`。事件 schema 加进 `consultation-agent-events.ts`(或既有事件联合类型所在处)。 +- `use-consultation-run.ts`:事件循环里收到 `session.title` 后记为 `streamedTitle`;L883 处改为 `resolveSessionTitle(question, streamedTitle ?? reply.title, …)`,其余不动(`existingTitles` 去重、`clipTitle` 保留)。 +- 恢复路径:`/api/consult/status` 响应里带当前标题;客户端恢复合并时若本地标题仍是自动值则采用(不在本单加新字段以外的逻辑)。 +- 验收:`tests/session-title-agent.test.ts` 新建——`sanitizeSessionTitle` 至少 8 例(去引号 / 去句末标点 / 超 14 字裁剪 / 少于 2 字返回 null / 含换行 null / 通用标题 null / 含 `1990年3月` 或 `04:50` null / 正常值原样);`shouldGenerateSessionTitle`:首轮+自动标题 true、非首轮 false、校正会话 false、用户已改名 false;带 fake `generateText` 的超时用例(超时后 promise 以 null 结束,且 `setTimeout` 计时器不 unref)。`tests/consultation-*` 或既有 consult 路由合同测试加一条:事件类型联合含 `session.title`。 + +### 5.2 排序与 `updated_at`(BUG-553) +- `page.tsx` L392 排序改为调用 `lib/session-groups.ts::sortSessions(sessions)`(置顶优先,其次 `updatedAt` 倒序,稳定)。 +- `api/sessions/[id]/route.ts::metadataUpdateValues`:去掉 `updated_at`。 +- `use-session-management.ts`:`renameSession`、收藏 / 取消收藏、归档 / 取消归档、换模型、换资料,凡是只改元数据的分支不再写 `updatedAt: timestamp()`;保留发问 / 回答 / 校正回合的 bump(`use-consultation-run.ts` L552、L902 等不动)。 +- 验收:`tests/session-groups.test.ts` 新建——`sortSessions` 至少 4 例(置顶胜时间;同置顶按时间;相等保持原序;归档过滤不在本函数);`tests/chat-session-write.test.ts`(或 `[id]` 路由合同测试)加一条:`metadataUpdateValues({ title })` 返回值不含 `updated_at`;`tests/sidebar-state.test.ts` 或 `use-session-management` 相关测试加一条:改名后 `updatedAt` 不变。 + +### 5.3 日期分组 +- `lib/session-groups.ts::groupSessionsByRecency(sessions, now = Date.now())` → `Array<{ key: "today"|"yesterday"|"week"|"month"|"older"; label: string; sessions }>`,空组剔除。 +- `app-sidebar.tsx`:「历史」组内按分组渲染,分组标题 `
`(复用 `.sidebar-section-summary` 的字号/颜色 token,不新造样式变量);焦点管理(`firstSessionRef` / `historyHeadingRef`)与序号(`favoriteSessions.length + index`)语义不变。 +- 验收:`tests/session-groups.test.ts` 加分组用例(跨天边界用固定 `now`,至少覆盖 23:59 → 00:01 的「昨天」判定、第 7 天与第 8 天、第 30 与第 31 天);`tests/sidebar-contract.test.ts` 加一条:历史区渲染分组标签且空组不渲染。 + +### 5.4 侧栏标题去前缀 +- `home-profile.ts::sessionSidebarTitle` 改为只返回标题;新增 `sessionSidebarSubtitle(session, library): string | null`(非本人时返回资料名,含「资料已删除 · 」前缀逻辑,本人返回 null)。`app-sidebar.tsx` 渲染副标题行。 +- 验收:`tests/chart-library-session.test.ts`、`tests/sidebar-contract.test.ts` 中断言前缀的用例按三栏表改;新增本人 / 他人 / 已删除资料三例。 + +### 5.5 记录 +- `docs/BUG_HISTORY.md`:BUG-553(列表只按置顶排、元数据 PATCH 推进 `updated_at`;关联 BUG-024 侧栏标题记录)。 +- `CHANGELOG.md`:一条写清「标题改为模型总结(首轮一次、不扣点数)、排序改活动时间、历史分组、侧栏去资料前缀」。 +- `frontend/DESIGN.md` Sidebar shell / Navigation item:分组标签与副标题行;`frontend/docs/VOICE.md`:分组标签文案「今天 / 昨天 / 最近 7 天 / 最近 30 天 / 更早」与起名提示词的措辞边界(不复述出生资料)。 +- `docs/tasks/PROGRESS-session-list-title-and-order-20260906.md`;`docs/testing/session-list-title-and-order-20260906.md`(真实环境:新会话首轮回答期间或结束后标题变成总结;改名 / 收藏 / 换模型后刷新不改变顺序;新发一条后该会话到组首;分组标签正确;停止回答后标题不出现错误提示)。 + +## 6. 让步顺序 + +5.2 → 5.4 → 5.3 → 5.1。5.1 若模型端 `generate` 在测试环境无法 mock,可先交付 5.1 的纯函数与事件契约并在进度记录写明;5.2 不可省。 + +## 7. 开工前置命令 + +```bash +git fetch origin --prune +git worktree add -b codex/session-list-title-and-order-20260906 .worktrees/session-list-title-and-order-20260906 origin/staging +cd .worktrees/session-list-title-and-order-20260906 +ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules +cd frontend && npx tsx --test tests/sidebar-*.test.ts tests/chart-library-session.test.ts tests/chat-session-*.test.ts tests/agent-reply.test.ts tests/consultation-*.test.ts 2>&1 | grep -E "^# (tests|pass|fail)|^not ok" +``` + +收尾跑同一条命令 fail=0,再 `./node_modules/.bin/tsc --noEmit`、`npm run lint`(0 error)、`npm test`、`npm run build`(`/` Static、gzip ±2%)。 diff --git a/docs/tasks/TASK-settings-dialog-and-billing-pane-20260906.md b/docs/tasks/TASK-settings-dialog-and-billing-pane-20260906.md new file mode 100644 index 00000000..8a59cb82 --- /dev/null +++ b/docs/tasks/TASK-settings-dialog-and-billing-pane-20260906.md @@ -0,0 +1,102 @@ +# TASK · 设置弹窗重排(固定尺寸、列表→详情)与「账户与点数」入弹窗、删除 `/membership` 整页(2026-09-06) + +- 基线:`origin/staging` @ `985c3258` +- 分支:`codex/settings-dialog-and-billing-pane-20260906` +- 执行方:coding agent;验收:Claude +- 涉及文件:`frontend/src/components/account-dialog-overlay.tsx`、`frontend/src/components/chart-library-panel.tsx`、新建 `frontend/src/components/profile-panel.tsx`、新建 `frontend/src/components/billing-panel.tsx`(+ `frontend/src/hooks/use-billing-panel.ts` 或 `frontend/src/lib/billing-order-polling.ts`)、新建 `frontend/src/lib/settings-url.ts`、`frontend/src/lib/home-types.ts`、`frontend/src/lib/membership.ts`、`frontend/src/app/page.tsx`(只改传参与调用,**必须净减行**:`renderProfile` 的 JSX 整段搬出)、`frontend/src/components/app-sidebar.tsx`、`frontend/src/components/onboarding-redeem-paywall.tsx`、`frontend/src/components/rectification-agentic-chat.tsx`、`frontend/src/hooks/use-consultation-run.ts`、`frontend/src/app/globals.css`、`frontend/next.config.ts`、**删除** `frontend/src/app/membership/page.tsx` 与 `frontend/src/app/membership/orders/page.tsx`、`tests/test_api_server_security.py`(路由集合)、`frontend/DESIGN.md`、`frontend/docs/VOICE.md` +- BUG 编号起点:**BUG-554**(BUG-553 由 `TASK-session-list-title-and-order-20260906.md` 预留;开工时复核最大号) +- 串行:同日顺序 **composer(551/552)→ 会话列表(553)→ 本单**。本单大改 `page.tsx`、`app-sidebar.tsx`、`use-consultation-run.ts`、`rectification-agentic-chat.tsx`,必须在前两单合入 staging 后再开分支。 +- 删除页面属于路由集合变更:本单必须在同一变更里跑 `.venv/bin/python -m pytest tests/test_api_server_security.py -k capability_audit`(BUG-143 的防复发要求),不得只跑前端矩阵。 + +## 1. 事故实证(staging,2026-09-06,桌面 Chrome,产品截图三张) + +| # | 用户看到 | 期望(对标 Claude 设置弹窗) | +| --- | --- | --- | +| 1 | 「星盘资料」格:本人资料条与「编辑本人资料」按钮之后,空态一句「还没有其他星盘,先添加一份资料」紧接**一整张**「添加其他人的星盘」表单(关系下拉、称呼、出生日期、「你对出生时间了解多少」单选组……)铺在同一个滚动区里,弹窗撑到接近整屏 | 列表是列表,表单是点了「添加」之后才出现的二级视图 | +| 2 | 三个分区弹窗尺寸各不相同:「通用设置」是一个只装得下主题三选的小盒子,「个人资料」「星盘资料」是宽而高的大盒子,切换分区整个弹窗跳变 | 一个固定尺寸的设置窗,左侧导航不动,右侧内容区自己滚 | +| 3 | 「个人资料」格:96px 头像预览 + 8 个配色块 + 「换一个形象」占了上半屏,账户信息两行,底部又一个「管理星盘资料」按钮(与左侧导航重复) | 头像一行搞定;重复入口删除 | +| 4 | 左侧导航第四项「账户与点数」点下去**离开首页**跳 `/membership` 整页,弹窗关闭,返回后要重新加载 | 它就是第四个分区 | +| 5 | 「星盘资料」里每一条其他人资料一行塞下 关系下拉 + 用于合盘 + 编辑 + 设为默认 + 删除;合盘结果卡和合盘历史平铺在列表下面 | 列表条只显示人;操作在这个人的详情里 | + +## 2. 根因(行号以基线为准) + +1. `chart-library-panel.tsx` L170–282:一个 `