Files
cc-web/.planning/fix-title-anchor/findings.md
2026-08-07 14:22:52 +08:00

111 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 调研记录
## 2026-08-07 初始现象
- 用户截图中,标题条位于编号 2 的消息之后、编号 3 的消息之前。
- 预期是标题条属于编号 2 的消息,应显示在编号 2 的消息上方。
- 本轮 `ccweb_set_title` 返回的标题事件同时包含 `messageIndex: 1` 和
`anchorMessageIndex: 0`。两个字段相差 1,说明系统已经表达了“事件发生位置”
与“标题归属锚点”的区别。
- 初步假设:事件生成端已恢复 `anchorMessageIndex`,但某个持久化或渲染路径
仍读取 `messageIndex`,从而产生一位偏移。
## 仓库状态
- Git 工作区开始时干净。
- `codebase-memory-mcp` 项目 `home-cc-web` 索引状态为 `ready`,
共 5036 个节点、10706 条边。
## 代码索引初查
- 服务端 `setCurrentConversationTitle()` 已将当前消息总数写入 `messageIndex`,
并通过 `findLatestUserMessageIndex()` 写入 `anchorMessageIndex`。
- 服务端与前端 normalizer 都会保留满足
`0 <= anchorMessageIndex < messageIndex` 的显式锚点。
- `buildUserOutlineTimelineItems()` 已优先查找显式锚点,并把标题的
`sortPosition` 设为 `anchorMessageIndex * 2 - 1`,从位置值看应排在锚点消息前。
- 现有 `scripts/regression.js` 已断言时间线类型顺序为
`date,title,message,date,title,message`,并断言显式锚点为 0。
- 因此问题不在标题事件生成或基础时间线构造的明显缺失;下一步需要重点核对
最终排序比较器、实时事件处理、DOM 更新是否绕开或覆盖了该结果。
## 历史修复与当前源码
- 隔离计划 `.planning/title-history-locator/` 记录:2026-07-29 已针对同一现象
新增 `anchorMessageIndex`,专项与全量回归当时均通过。
- Git blame 显示该实现来自提交 `33c9783`,当前 `main` 已包含此提交。
- 当前排序比较器首先比较 `sortPosition`;显式锚点 0 的标题位置为 -1,
锚点消息位置为 0,纯函数上确实会得到“标题 → 消息”。
- 当前 `updateUserOutlinePanel()` 直接按该纯函数返回顺序生成 DOM,没有二次排序。
- 由此可排除“当前源码根本没有实现”这一可能;需要验证浏览器实际加载的静态资源
是否仍是旧版本,或实时标题事件在某条路径被前端 normalizer 丢掉锚点。
## 运行态与静态资源
- PM2 的 `ccweb` 进程在线,启动于 2026-08-06 19:47(本地时区),未启用 watch。
- 页面引用 `app.js?v=20260805-usage-loading-inline`,该缓存版本晚于
2026-07-29 的标题锚点修复;正常重新加载过页面的浏览器应取得含修复的脚本。
- `session_renamed` 实时处理会把 `msg.titleEvent` 同时写入 snapshot 和
`currentOutlineTitleHistory`,两处都经过保留锚点的 normalizer,然后立即重绘。
- 精确标题文本只在会话 `904bcc12-...` 的运行态 Codex App 文件中命中;
该文件本身不是 cc-web 的会话元数据,需继续定位真正的持久化会话文档。
## 当前会话字段实证
- 当前会话文档 `904bcc12-...json` 已持久化本轮标题事件:
`messageIndex: 1`、`anchorMessageIndex: 0`,且消息数组只有索引 0 的用户消息。
- 这证明正在运行的服务端会正确生成和保存锚点;回归现象更可能只发生在客户端
展示版本或目标历史会话的数据形状,而非当前服务端生成逻辑。
## 客户端版本风险
- 前端没有应用版本握手,也没有检测新静态资源后自动刷新页面的逻辑。
- `app.js` 的查询参数只在用户重新请求 `index.html` 后生效;已经长期打开的标签页
会一直执行旧内存中的 JavaScript,即使 PM2 重启或磁盘源码已更新。
- 这与“源码和回归正确、运行中 UI 仍呈现旧排序”的现象吻合,需进一步用运行态
静态响应和端到端 DOM 验证确认,而不是再次改写已正确的排序算法。
## 静态服务与目标数据补查
- 服务端对所有静态资源返回 `Cache-Control: no-store, max-age=0`,因此一旦页面刷新,
浏览器缓存不会继续提供旧脚本;风险只剩“标签页从未刷新”的内存旧版本。
- 以截图可见关键词宽泛搜索到的两个旧会话都没有 `titleHistory`,命中来自长文本
内容而非截图对应的标题事件,不能据此认定是目标会话。
- 下一步应直接枚举标题历史的结构分布,确认是否还存在缺少锚点的新事件或旧格式
事件在兼容回退中排序错误。
## 标题历史结构审计
- 当前 18 条持久化标题事件中,15 条新事件包含合法锚点;3 条旧事件没有锚点。
- 所有带锚点事件都指向事件发生位置之前的用户消息,例如当前会话为
`messageIndex: 1 -> anchorMessageIndex: 0`,多轮会话也保持相同契约。
- 3 条无锚点旧事件的 `messageIndex` 之前都存在可见用户消息,现有
`precedingAnchor` 回退能够找到正确候选。
- 没有发现磁盘数据层面的非法锚点、字符串索引或事件位置异常;数据本身不足以
复现截图顺序,继续核对 CSS 与真实浏览器渲染。
## CSS 与浏览器验证条件
- 标题、消息和日期节点都按普通文档流渲染;标题样式没有 `order`、定位或变换,
CSS 不会把正确的 DOM 顺序重新排错。
- 当前环境没有 Chromium、Playwright、Puppeteer 或 jsdom,不能直接启动现成的
浏览器端到端测试;可继续使用项目现有的函数提取式前端回归。
- 尚需确认 PM2 是否通过 `CC_WEB_PUBLIC_DIR` 指向另一份静态目录;如果存在,
就能解释“源码正确但页面仍是旧实现”。
## 最终实现
- 服务端读取当前 `PUBLIC_DIR/app.js` 字节,计算 SHA-256 并取前 16 位十六进制
作为资源指纹;入口页脚本 URL 与 `auth_result.frontendAssetVersion` 使用同一算法。
- `index.html` 使用占位符,服务端仅在返回入口页时替换;实际运行态测试确认占位符
全部消失,页面全局版本、脚本 URL 和 WebSocket 鉴权版本三者一致。
- 客户端只接受 16–64 位十六进制版本;缺失、非法或一致时保持正常鉴权流程,
不一致时设置内存 guard 后刷新,重复握手不会重复触发。
- 独立质量复核代理虽未回传结论,但在被中断前落盘了两项合理加固:
`PUBLIC_DIR` 统一 `path.resolve()`,静态文件边界从字符串前缀改为复用
`isPathInside()`,同时让运行态专项显式覆盖 `CC_WEB_PUBLIC_DIR`。
- 该加固修复了类似 `/public-evil` 通过 `startsWith('/public')` 的潜在路径边界误判,
与本次动态入口注入的自定义静态目录兼容性直接相关,予以保留。
- 加固后的全量回归最终以 exit 0 完成;`PUBLIC_DIR` 自定义目录、入口页动态注入、
WebSocket 版本下发及原有标题历史回归均保持通过。