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