fix: 修复标题归属并更新发布包

This commit is contained in:
shiyue
2026-08-07 14:22:52 +08:00
parent 4e1f987d62
commit 70be7eb279
14 changed files with 613 additions and 17 deletions

View File

@@ -0,0 +1,110 @@
# 调研记录
## 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 鉴权版本三者一致。
- 客户端只接受 1664 位十六进制版本;缺失、非法或一致时保持正常鉴权流程,
不一致时设置内存 guard 后刷新,重复握手不会重复触发。
- 独立质量复核代理虽未回传结论,但在被中断前落盘了两项合理加固:
`PUBLIC_DIR` 统一 `path.resolve()`,静态文件边界从字符串前缀改为复用
`isPathInside()`,同时让运行态专项显式覆盖 `CC_WEB_PUBLIC_DIR`
- 该加固修复了类似 `/public-evil` 通过 `startsWith('/public')` 的潜在路径边界误判,
与本次动态入口注入的自定义静态目录兼容性直接相关,予以保留。
- 加固后的全量回归最终以 exit 0 完成;`PUBLIC_DIR` 自定义目录、入口页动态注入、
WebSocket 版本下发及原有标题历史回归均保持通过。

View File

@@ -0,0 +1,38 @@
# 进度日志
## 2026-08-07
- 已读取 `planning-with-files``todo-list-csv` 和 Trellis 工作流。
- 已完成 Trellis 会话初始化,开发者身份为 `shiyue`
- 已创建并启动 Trellis 任务 `08-07-fix-title-anchor`
- 已记录截图现象、标题事件字段差异和初步回归假设。
- 独立计划审查第一次指出消息序号存在零基/一基歧义;修订验收标准后,
第二次审查已通过。
- 当前阶段:追踪标题事件生成、持久化与渲染链路。
- codebase-memory 已确认当前源码和回归都包含标题前置锚点;后续检索发生
`Transport closed`,已按三次失败规则停止重试并降级到本地只读检索。
- 已确认 PM2 无 `CC_WEB_PUBLIC_DIR` 覆盖8002 端口返回的 `app.js` 与仓库文件
SHA-256 完全一致并禁用缓存;标题专项回归通过。
- 根因确定为旧标签页长期运行修复前的内存脚本,而系统缺少前端资源版本握手。
- 当前阶段:编写失败回归测试锁定旧前端版本刷新。
- 调整后的计划首轮复审指出“机制上线前旧页面无法自动获得新代码”的不可达验收;
已限定为本次手动刷新一次、后续升级自动收敛,第二轮复审通过。
- 第二个实现代理在被中断后落盘了 `frontend-asset-version` 专项回归;
`node --check` 通过,专项测试以 6 类缺失契约、exit 1 按预期进入红灯。
- 已实现 `app.js` SHA-256 短指纹、入口页动态注入、鉴权下发,以及客户端
版本规范化和单次刷新 guard没有修改标题锚点排序。
- 首次绿灯因测试要求字面量替换而失败,调整为等价字面量后专项 exit 0。
- 增加轻量运行态集成:实际启动隔离服务,校验入口页和鉴权消息使用相同指纹。
- 修复占位符与全局变量属性名相同导致的误替换后,资源版本专项、标题专项及三项
`node --check` 均通过。
- 全量回归首次外层调用未保留退出码;按执行会话 ID 重新运行并等待完成,最终输出
`Regression checks passed.``REGRESSION_EXIT=0`
- `git diff --check` 和三项 `node --check` 通过。
- 当前阶段:审查差异并完成任务记录。
- Trellis 质量复核代理未及时回传,但中断前落盘了 `PUBLIC_DIR` 归一化、
`isPathInside()` 静态目录边界校验及自定义 public 目录回归覆盖;主线程逐行审计后
确认属于必要的安全与单文件发布兼容加固。
- 在上述加固落盘后重新运行全量回归,明确取得 `Regression checks passed.`
exit 0最终差异审计未发现功能偏差。
- 已将长驻页面的前端资源版本握手、单次刷新边界及对应集成测试要求写入 Trellis
前端质量规范,任务记录完成。

View File

@@ -0,0 +1,48 @@
# 修复标题显示归属
## 目标
确保会话内标题条显示在目标消息上方,并修复旧标签页继续运行过期前端脚本
导致修复看似失效的问题。
## 验收标准
- 标题显示在 `anchorMessageIndex` 指向的消息上方;示例事件中应显示在
数组索引 0 的目标消息上方,而不是按 `messageIndex = 1` 插在其后。
- 同一份标题事件在实时追加和会话重载后得到相同位置。
- 缺少新锚点字段的历史标题事件仍按兼容规则稳定显示。
- 已加载本次版本自检逻辑的页面,在后续服务升级后能检测资源版本不一致并
自动刷新一次;机制上线前已打开的旧页面需要手动刷新一次。
- 相关定向测试及项目质量检查通过。
## 阶段计划
1. **DONE** — 初始化任务并记录现象与验收条件
2. **DONE** — 追踪标题事件生成、持久化与渲染链路
3. **DONE** — 编写失败回归测试锁定旧前端版本刷新
4. **DONE** — 实现前端版本自检并保持标题锚点兼容
5. **DONE** — 运行标题与版本链路定向测试
6. **DONE** — 运行前端相关质量检查
7. **DONE** — 审查差异并完成任务记录
## 决策
- 不以 CSS 位移掩盖问题,优先统一标题事件的语义锚点。
- 标题锚点源码、数据与运行态静态文件均已正确,不重复改写排序算法。
- 以当前 `app.js` 内容指纹作为前端资源版本,由服务端动态注入页面并在
WebSocket 鉴权成功时下发;客户端版本不一致时只触发一次刷新。
- 先补失败测试再修改实现,锁定版本注入、握手与单次刷新契约。
## 已遇到错误
| 错误 | 尝试 | 处理 |
|---|---:|---|
| Trellis 新建任务后没有自动生成 `prd.md` | 1 | 已按工作流要求显式创建需求文档 |
| `codebase-memory-mcp search_code` 返回 `Transport closed` | 3 | 停止重复调用,使用已取得的函数级结果并降级到 `rg` 补查静态资源与事件路径 |
| 运行态静态文件比对命令因包含 `rm -f` 被安全策略拒绝 | 1 | 改用纯管道读取与哈希,不创建或删除临时文件 |
| 首个失败回归实现代理运行超过 6 分钟且未产生差异或测试进程 | 1 | 已中断,改派更窄的最小可执行专项测试任务 |
| 第二个窄化实现代理仍未进入编辑阶段且无差异 | 2 | 已中断Trellis 实现代理通道不可用,主线程加载同一上下文后降级实施 |
| 首次绿灯运行仅“服务端入口注入”静态契约失败 | 1 | 实现使用占位符常量而测试要求字面量;改为字面量替换,保持行为不变并对齐可审计契约 |
| 首次运行态专项未读到注入后的全局版本 | 1 | 发现占位符与全局属性名相同,`replaceAll` 同时改坏属性名;改用不含占位符的 `window.ccWebFrontendAssetVersion` |
| 首次全量回归的外层执行提前返回,无法取得子进程退出码 | 1 | 用直接 Node 调用并通过 `write_stdin` 等待会话,取得 `REGRESSION_EXIT=0` |
| Trellis 质量复核代理超过 5 分钟未回传、无新增差异或测试进程 | 1 | 已中断,主线程按同一 check 上下文完成逐行差异审计 |