Files
Jyotisha/docs/tasks/TASK-session-list-single-source-20260917.md
T

18 KiB
Raw Blame History

任务书 · 会话列表一处数据源、服务端排序修正、空会话不入列、标题规则重定(2026-09-17)

0. 基线

  • 基线 commiteea90926origin/staging head)。串行在 TASK-rectification-session-composer-guard-20260917.md 之后:那一单改 use-session-management.ts 的回退路径,本单从它合入后的 staging 起分支,开工时以当时的 origin/staging 为准。
  • 分支:codex/session-list-single-source-20260917git worktree add -b codex/session-list-single-source-20260917 .worktrees/session-list-single-source-20260917 origin/staging
  • 范围:frontend/src/lib/db/local-postgres-client-core.ts(排序)、api/sessions 两个路由、frontend/src/app/ 路由组重排(page.tsx(secondary)/* 移入 (app)/)、新建 lib/session-list-context.tsx / hooks/use-session-list.tshooks/use-session-management.ts、删除 lib/sidebar-data-cache.ts / hooks/use-sidebar-data.tscomponents/app-sidebar.tsx / components/sidebar-session-row.tsxlib/agent-reply.ts 的标题函数、lib/home-profile.ts 的侧栏标题/副标题、lib/session-groups.ts,以及 page.tsx(只允许减行)。不动 Python、不动 Skill。T3 若选服务端过滤方案不动表结构;若产品另批清理迁移再动表并真跑 npm run test:db
  • BUG 段:BUG-926 起(上一单占 924–925;开工时核对 docs/BUG_HISTORY.md 最大号)。

1. 事故实证

产品负责人 2026-09-17 反馈:/(新建对话)、/chart/ephemeris/reports 四处的侧栏会话列表不是同一份,每换一页都重新读一次;列表内容还一直在变;排序和标题规则看不出哪个是哪个。

用产品负责人登录态只读核对 staging(GET /api/sessions?limit=50,不含身份信息):

项目 结果
返回顺序 严格按 id 降序updated_at 不单调
nextCursor 2026-09-03T07:09:23Z,<id>,按 updated_at 构造
首页 50 条里最新的 updated_at 2026-09-17 01:35Z;当天 11:12Z 新建的校正会话不在首页
首页 50 条标题分布 「新对话」28 条、「生时校正」4 条、「9月8日 · 生时校正」2 条、「9月14日 · 生时校正」2 条、其余各 1
各接口耗时 sessions 0.83 s、account 0.87 s

代码定位(按符号):

  • 服务端排序丢了第一键。 frontend/src/lib/db/local-postgres-client-core.tsorder(column, options)this.ordering = { column, ascending },只保存最后一次调用;SQL 生成处 if (this.ordering) sql += " order by …" 只有一个键。api/sessions/route.ts GET 链式调用 .order("updated_at", desc).order("id", desc),实际只按 id 排。游标 sessionCursorFilter 却按 updated_at, id 过滤,于是首页不是最近 40 条、翻页与首页重叠或漏掉、每次进入看到的集合都不同。同一缺陷还吃掉了 api/payment/packages/route.ts.order("sort_order").order("created_at"),套餐顺序变成按创建时间。
  • 两份数据源。 /page.tsx 启动 effect 自己拉 fetchSessions()(默认 40 条)+ 详情,存进 Home()sessions state,再经 visibleSessions(过滤掉空的普通会话)→ sidebarSessions 喂给侧栏;(secondary)/layout.tsxuseSidebarData() 另拉 /api/sessions?limit=40 + /api/account,进 sidebar-data-cache.ts 的 60 s 模块缓存,不做空会话过滤/app/ 里不在 (secondary) 路由组,离开 / 即卸载 Home(),回来重跑整个启动(账户、目录、列表、详情、entry-summary、今日星语);invalidateSidebarCache()/ 的每次写之后清空次级页缓存。BUG-745 修掉的是次级页之间的重挂,/ ↔ 次级页之间没有修。
  • 空会话堆积。 page.tsx 启动时 starterHomeLandingNeedsConsultation 为真(落点缺失或是校正会话)就 createSession + writeChatSession(create) 立即落库;startNewChat 也是点了就落库。用户从没发过问题的「新对话」因此在库里累积(首页 50 条里 28 条),/visibleSessions 把它们藏起来,次级页原样显示,两边列表自然不一样。
  • 标题规则。 resolveSessionTitle:普通会话在第一次回答前叫「新对话」,之后取模型标题或问题前 14 字;校正/今日节奏用 datedSessionTitle 生成「M月D日 · 生时校正」;同名靠 uniquifySessionTitle 追加「HH:MM」(新建时的墙钟,不是会话时间)。旧数据还有无日期的「生时校正」。侧栏行只有标题一行,/ 的副标题只在他人盘时显示,次级页根本没有副标题字段。结果是一列「新对话 / 9月14日 · 生时校正 / 9月14日 · 生时校正 02:14」。
  • 服务端 updated_at 口径(BUG-553:只由对话活动推进,元数据 PATCH 不写)仍在,不是本单问题;客户端 sortSessions 置顶 + updatedAt 倒序也正确,只是服务端给的集合本身不对。

Bug 历史检索:BUG-553(列表只按置顶排)、BUG-557(标题守卫 0 行)、BUG-699(打开旧校正会话改今天标题)、BUG-744~746(侧栏统一、次级页重挂、折叠状态)防复发措施均在,本单四条都是新问题;BUG-745 的「每跳一次重拉列表」在 / ↔ 次级页方向属未覆盖,关联但非复发。

2. 根因

  1. 本地 PostgreSQL 兼容层的 order() 不支持多键,服务端列表从来没有按最近活动排过;游标分页因此与排序不一致。
  2. 会话列表没有一个跨路由的所有者:Home()(secondary) 各拉各的,/ 又是每次进入从零启动。
  3. 会话在用户还没说一句话时就落库,而两处列表对空会话的处理不一致。
  4. 标题只承载「叫什么」,不承载「哪一类、哪一天、谁的盘」,同名去重又用墙钟时间。

3. 决策记录

  • 产品负责人 2026-09-17 授权:列表必须共用一份;排序与标题规则重构。以下具体口径由 Claude 定,产品负责人如不同意在验收时改:
    • 排序:置顶在前;组内按 updated_at 倒序;updated_at 仍只由对话活动推进(BUG-553 不变)。服务端与客户端同一口径。
    • 哪些会话入列:只列有内容的会话——普通会话至少一条消息;校正会话一律入列(它一打开就有开场轮)。当前正在用的空会话只在 / 本地显示,不入服务端列表。
    • 标题:普通会话 = 模型标题,否则第一问前 14 字;第一次回答落库前显示「新对话」但不入列。校正会话 = 「生时校正 · M月D日」;今日节奏 = 「今日节奏 · M月D日」——类别在前,日期在后,扫一眼能分类。同日多条不再加墙钟「HH:MM」,改为副标题区分。
    • 副标题(两处侧栏都显示一行小字):M月D日 HH:MM(会话创建时间,Asia/Shanghai+「 · <盘主名>」(仅他人盘)。校正会话再补状态词:进行中 / 已交付 / 已结束(从 entry-summary 或列表字段取;取不到就不显示,不得猜)。
    • 存量数据:旧标题不批量改(BUG-699 已做过一次校正会话标题修复,且手改标题不能动);只对未来新建生效。空会话不删,只是不入列;若产品负责人另批清理,另开迁移单。
  • 产品负责人 2026-09-17 拍板:侧栏外壳提到路由组 layout,首页也进去,侧栏在四个页面之间不卸载;不做模块级 store。这推翻 BUG-745 防复发里「layout 不传 controls」与 TASK-sidebar-unify-20260916 D1「次级页侧栏只读、管理操作留在首页」两条:管理操作里只依赖列表的部分(改名、置顶、归档、删除、翻页)上移到 provider,四页都能用;依赖首页状态的(新建、选会话、校正打开)仍由首页注册。BUG-553 / BUG-699 的防复发不动。
  • page.tsx 不得增长(本单应明显减少);Home() 的乐观更新与回滚层保留,只是列表 state 的所有者换成 provider。

4. 硬红线

  1. 兼容层 order() 改多键后,所有既有单键调用行为不变;有 SQL 文本级测试。
  2. 服务端列表排序键与 sessionCursorFilter 的键必须一致(updated_at desc, id desc),有测试锁住。
  3. 元数据 PATCH 仍不写 updated_atBUG-553);打开已有会话不改 title / updatedAtBUG-699session-open-preserves-identity.test.ts 必须保持绿)。
  4. app/SidebarProvider 只在 (app)/layout.tsx 出现一次(BUG-745 口径升级);侧栏源码不得出现 window.location/login/admin* 不得带侧栏。
  5. 既有测试断言不得静默弱化;改断言写「原值 / 新值 / 原因」三栏。测试总数不低于开工时。
  6. 无 Docker 时 npm run test:db 失败照基线记录,不得写成通过。

5. 任务分解

T1 兼容层多键排序 + 列表接口排序契约(BUG-926)

  • local-postgres-client-core.tsordering 改为数组,order() 追加;SQL 生成 order by a desc, b desc
  • api/sessions/route.ts 两处查询(分页与置顶)保持 .order("updated_at", desc).order("id", desc)api/payment/packages/route.ts 不改代码,验证顺序恢复为 sort_order, created_at
  • 验收:
    • frontend/tests/local-postgres-query-value.test.ts(或新建 local-postgres-order.test.ts):两次 order() 生成的 SQL 含 order by "updated_at" desc, "id" desc;单次 order() 输出不变。
    • frontend/tests/session-cursor.test.tschat-session-authority.test.ts 加断言:列表路由源码的排序键序列与 sessionCursorFilter 的键一致。
    • 有 Docker 时 npm run test:db 加一条:插入三条不同 updated_at 的会话,GET /api/sessions?limit=2 返回最新两条且 nextCursor 翻页拿到第三条、无重叠。无 Docker 写 BLOCKED.md

T2 应用外壳提到路由组 layout,首页也进去,侧栏在四个页面之间不卸载(BUG-927)

产品负责人 2026-09-17 拍板:不做 store,做常驻外壳。侧栏、会话列表、账户摘要只有一个所有者,就是 layout;四个页面切换只换中间内容。

  • 路由组:新建 frontend/src/app/(app)/app/page.tsx 移到 (app)/page.tsx(secondary)/chart|ephemeris|reports|reports/[reportId] 移到 (app)/ 下,(secondary) 目录删除。URL 不变;/login/admin*error / not-found 留在外面,不得带侧栏。每页自己的 metadata / dynamic 声明原样保留(reportsforce-dynamic 是页级的,不影响 / 的 Static)。
  • 外壳 layout(app)/layout.tsxclient= SessionListProvider + SidebarProvider + AppSidebar + SidebarInset。首页现在 page.tsx 里的 <SidebarProvider escapeBlocked=…><main className="chat-app">…<AppSidebar …/> 整段搬到这里;首页只剩 SidebarInset 里的内容。
  • SessionListProvider(新文件 frontend/src/lib/session-list-context.tsx + frontend/src/hooks/use-session-list.ts)拥有:sessions: ChatSession[]setSessions、分页游标、归档视图开关、账户摘要(侧栏页脚用)、signedOut。签入后每个标签页只拉一次列表;暴露一个 ready Promise 给首页启动用。只依赖 sessions + fetch 的元数据操作(renameSessiontogglePinnedSessiontoggleArchivedSessiondeleteSessionloadMoreSessionstoggleArchivedView)从 use-session-management.ts 搬到 provider401 时清空并标 signedOut
  • 首页改造const [sessions, setSessions] = useState<ChatSession[]>([]) 改为从 provider 取;启动 effect 不再自己 fetchSessions(),改 await ready,其余(账户、目录、详情、落点选择、entry-summary、今日星语)不变。use-session-management.ts 保留选会话、新建、换模型、校正会话延迟切换这些需要首页状态的部分,参数表相应缩短。
  • 侧栏控制注册:首页独有的控制(新建对话、选会话、正在打开 / 打开失败的行状态、账户菜单开关、escapeBlocked)通过 provider 的 registerShellControls() 在首页挂载时注册、卸载时清空;layout 把注册到的 controls 传给 AppSidebar。另外三页没注册:会话行仍是 Link/?c=<id>,但改名、置顶、归档、删除、翻页这些 provider 自带的操作在四页都可用;新建对话在三页上是 Link/。高亮只在 / 上有。
  • 删除hooks/use-sidebar-data.tslib/sidebar-data-cache.tstests/sidebar-data-cache.test.ts(改写成 provider 测试,不是删掉测试数量)。
  • 合同测试改写(每条写三栏「原值 / 新值 / 原因」):
    • sidebar-contract.test.ts 「the same component renders read-only when / is not the one mounting it」:原值断言次级 layout 不传 controls;新值断言 layout 只传 registerShellControls 注册到的 controls,未注册即只读。
    • sidebar-contract.test.ts 第 313 行附近对 page.tsx<SidebarProvider escapeBlocked={accountMenuOpen || modalOpen}> 正则:搬到对 (app)/layout.tsx 的断言,escapeBlocked 来自注册值。
    • BUG-745 的防复发「layout 里 <SidebarProvider> 恰好一次、且不传 controls」改为「(app)/layout.tsx 里恰好一次、app/ 其它地方零次;controls 只来自注册」。chart-page-viewephemeris-pagepersonal-report-view 三个测试里对 (secondary) 路径的引用改到 (app)
  • 验收:
    • next build/○ Static/chart/ephemeris/reports 的渲染方式与基线一致。
    • 新测试:provider 挂载后 /api/sessions 恰好一次;首页有 provider 列表时启动不再请求 /api/sessionsmock fetch 计数);在 / 改名后 provider 里的行立即更新;401 清空。
    • page.tsx 行数明显下降(外壳段 + 列表 state + 六个元数据操作的调用点都走了),进度记录写出前后行数。
    • 真机清单(docs/testing/):DevTools Network 观察 //chart/ephemeris/reports//api/sessions/api/account 全程各一次;侧栏 DOM 节点在四页之间不重建(Elements 面板选中侧栏节点,切页后仍是同一节点);改名、置顶、删除后另外三页立即一致;/login/admin 无侧栏。

T3 空会话不入列,且延迟落库(BUG-928)

  • 服务端:api/sessions GET 排除 session_type = 'consultation' AND messages = '[]'(保留校正会话);置顶查询同样处理。GET /api/sessions/<id> 不变(详情仍可读)。
  • 客户端:startNewChat 与启动时的落点会话改为本地创建、不立即落库ChatSessionpersisted: booleansend() 在撤回窗口结束、POST /api/consult 之前若 !persistedwriteChatSession(create)(失败则回滚并提示,沿用 startNewChat 现有回滚文案)。校正会话仍由 /cases/open 服务端创建,不受影响。
  • visibleSessions 的过滤搬进 provider 的派生值(当前空会话只在 / 激活时可见),四页读同一个派生结果。
  • 让步:若延迟落库牵动 ?c= 深链、刷新恢复(pendingConsultationStorageKey)过多,可只做服务端过滤 + 启动时复用已有空会话而不再新建,把延迟落库写进 BLOCKED.md
  • 验收:
    • chat-session-authority.test.ts:列表路由过滤条件存在;详情路由不过滤。
    • 新测试:连点三次「新建对话」不发 POST /api/sessions;发出第一问前恰好一次 POST /api/sessions?c=<本地未落库 id> 刷新后不报「会话不存在」而是回到落点。
    • 有 Dockertest:db 加一条空会话不出现在列表。

T4 标题与副标题规则重定(BUG-929)

  • agent-reply.tsdatedSessionTitle 改为「<类别> · M月D日」;uniquifySessionTitle 删除(同名允许,靠副标题区分);isGenericSessionTitle 的前缀正则同步改成类别在前,并兼容旧「M月D日 · 生时校正」格式(旧标题仍判为 generic,行为不变)。
  • 列表接口增加 created_at(表里已有;只是没选出来)。toSidebarSessionsidebarSessions 共用一个 lib/session-sidebar-row.tstitlesubtitle = 「M月D日 HH:MM」+「 · 盘主」、校正状态词。sidebar-session-row.tsx 副标题已支持,无需新组件;两处侧栏必须走同一个映射函数。
  • 分组(今天 / 昨天 / 最近 7 天 / 30 天 / 更早)保留,按 updated_at
  • frontend/DESIGN.md Sidebar shell 一节写明标题与副标题规则;frontend/docs/VOICE.md 对照「生时校正 · 」「今日节奏 · 」措辞。
  • 验收:agent-reply.test.ts 更新(写三栏);新测试锁映射函数:同一行输入在 / 与次级页产出相同 title/subtitlesession-open-preserves-identity.test.ts 保持绿。

T5 记录

  • docs/BUG_HISTORY.md 新增 BUG-926929926 关联 packages 路由;927 关联 BUG-745928 关联 BUG-553929 关联 BUG-699);CHANGELOG.mddocs/tasks/PROGRESS-session-list-single-source-20260917.mddocs/tasks/README.md 状态板行;BLOCKED.md(无 Docker 项)。

6. 让步顺序

T1 > T2 > T3(服务端过滤部分)> T4 > T3(延迟落库部分)。T1 一项独立可发。T2 内部若元数据操作上移牵动过大,允许第一步只做「路由组 + layout 常驻 + provider 拥有列表 + 首页注册全部控制」,元数据操作上移写进 BLOCKED.md 作第二步;但外壳常驻与列表单一所有者不得让步。

7. 开工前置命令

git fetch origin --prune
git log -1 --format='%h %s' origin/staging   # 确认上一单已合入
git worktree add -b codex/session-list-single-source-20260917 .worktrees/session-list-single-source-20260917 origin/staging
cd .worktrees/session-list-single-source-20260917/frontend
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -20   # 记下基线失败清单与测试总数
grep -n "^## BUG-" ../docs/BUG_HISTORY.md | tail -1

8. 验收口径

tsc --noEmit 0 错;npm run lint 0 errornpm test 失败清单与基线逐条一致、新增测试全绿、总数不降;next build/ 仍 Static;首屏 gzip ±2%page.tsx 行数 ≤ 开工时;有 Docker 则 npm run test:db 通过,无 Docker 写 BLOCKED.md。部署后用 DevTools 复核 T2 的「一次 /api/sessions」、侧栏节点不重建,与 GET /api/sessionsupdated_at 单调递减。