# 调研记录 ## 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 版本下发及原有标题历史回归均保持通过。