Renumbered from BUG-599: staging took 599 and 600 from other sessions while this branch was in review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016P5RoqzmUQEbeC2qjAkeGr
6.6 KiB
6.6 KiB
PROGRESS · 报告生成分章进度(2026-09-09)
- 分支:
codex/report-progress-20260909,基线origin/staging=a43a6db8 - 模式:直接执行模式(用户指定 subagent 执行),无独立任务书;设计口径来自会话内定稿
- BUG 编号:BUG-601(开工核对当时最大号 598,原写 599;交付前 rebase 发现 staging 已有别的会话占用 599「回访登录自动打开生时校正」与 600「新用户保存账户资料 500」,故顺延到 601)
一、做了什么
把 /reports/[reportId] 的生成等待屏从「spinner + 秒表」改成分章进度。
后端(本轮唯一的服务端改动)
app/api/reports/[reportId]/route.ts:listSections原先只返回{status, lastErrorCode},现补上sectionId与attemptCount。两者都只在服务端消费。lib/personal-report-route-core.ts:- 分章行原先只在
row.status === "failed"时才读,reportView也不含任何分章字段——也就是说生成中根本没有章节数据离开服务端。现改为failed与generating两种状态都读;ready路径不变,不增加查询。 - 新增
sectionProgress推导,随reportView第四参数下发,只带{id, state}。
- 分章行原先只在
新增共享层
lib/personal-report-progress.ts(新):REPORT_THEME_LABELS/reportThemeLabel:从personal-report-document-view.tsx提取,原处改为 import,不留第二份副本。deriveSectionProgressState(status, attemptCount):行状态 →done / failed / writing / waiting。这是全仓唯一读attemptCount的地方,且只在服务端跑。classifyReportProgressStage(phase):preparing / writing / finishing。describeReportProgress():纯函数,产出标题句与章节清单。reportProgressSignature():进度指纹,用于停滞判定。REPORT_PROGRESS_STALL_MS = 90_000。
前端
components/personal-report/personal-report-progress-panel.tsx(新):按章分格的进度条 + 章节清单。无 percent、无定时器、无 transition。components/personal-report/personal-report-page.tsx:解析sections(带白名单校验,形状不对就丢弃而不是渲染成谜之行);用progressMark记录进度指纹与时刻,停滞判定复用既有的一秒 tick(waitStartedAt + waitedMs即"现在"),未新增定时器、未新增 effect;写作阶段隐藏 spinner 换成面板。app/globals.css:新增等待屏进度样式。frontend/DESIGN.md§9 新增「报告生成等待态」小节。
二、三条实现事实(做错任一条功能就是错的)
section:<id>命名的是刚写完的那一章。emitProgress在complete()/block()之后调用(personal-report-generation.ts:3459 / 3485 / 3496)。- "已完成数 + 1"同样会指错。
sectionService.list()按section_id字典序返回(personal-report-section-service-core.ts:113),不是写作顺序。所以顺推出来的"下一章"与真正在写的那一章无关。 → 因此本轮改用行状态判定:start_personal_report_section只attempt_count + 1并保持status='pending'(迁移20260830020000第 102–111 行),所以"正在写"精确等于pending && attemptCount > 0,不是推断。测试里专门构造了字典序会指错的用例(正确答案marriage,顺推会得到timing)。 - blocked 计入完成数,进度条照常前进;收尾走既有
summarizePersonalReportFailure()。
三、测试数字
| 项 | 基线(a43a6db8) | 改后 | 结论 |
|---|---|---|---|
tsc --noEmit |
0 错 | 0 错 | 通过 |
npm run lint |
0 error / 108 warning | 0 error / 108 warning | 通过;warning 数不变,且无一条在本轮改动的文件里 |
npm test tests |
2929 | 2945 | +16 |
npm test pass |
2887 | 2903 | +16 |
npm test fail |
28 | 28 | 清单 diff 逐条一致 |
npm test skipped |
14 | 14 | 不变 |
next build / |
○ Static |
○ Static |
通过 |
globals.css gzip |
33,324 B | 33,641 B | +317 B / +0.95%,在 ±2% 内 |
28 条失败全部是无 Docker 的数据库/部署/迁移套件,基线即红。失败清单已逐条 diff 比对,完全一致,无新增、无消失。
新增测试:frontend/tests/personal-report-progress.test.ts 16 项,覆盖任务要求的六类——off-by-one(含字典序陷阱)、blocked 计入进度、全 ready 与部分 blocked 两种收尾、90 秒停滞文案切换且不出现「第 N 次尝试」、准备/写作/收尾三分支渲染、以及后台字段不出服务端。
未改动任何既有断言,因此无「原值 / 新值 / 原因」三栏。特别说明一条:tests/report-polling-contract.test.ts:105 有 assert.doesNotMatch(pageSource, /章节/),本轮把章节渲染整体放进独立组件 personal-report-progress-panel.tsx,pageSource 里确实不出现「章节」,该断言原样通过,未被弱化或删除。
四、偏离与判断
- 任务交代「
sections[]目前被剥掉了sectionId,把它加回去」,实际情况更严重一层。 分章数组根本不在generating的响应里——resolveReportRead只在failed时读分章行,reportView也没有该字段。所以只补sectionId不够,必须同时让生成中也读分章行并下发。这是比交代范围略大的服务端改动,但不做则整个功能无数据可用。ready路径未加查询。 - 停滞判定没有用
progressPhase的持续时长,而是用完整进度指纹(phase + percent + 各章状态)。理由:重试时 phase 与 percent 都不动,但若同期别的信号变了,仍不该报"停滞"。指纹变化才重置计时,更严格。 - 面板不重复渲染标题句。 标题句(「已完成 N / M 章」)只出现在页面的
<p role="status">里,面板只画条与清单。这样读屏软件只播报一次,也不会出现同一句话上下各一遍。 --type-body-xs这个 token 不存在,写样式时误用后已改掉,改用继承字号。未新增 token。
五、没做的
- 真实报告生成一次都没跑过(无登录态、无 Chrome、无模型凭据)。写作阶段是否真的出现、章节名是否错位、慢章节文案是否真的切换,全部无运行时证据。已写成
docs/testing/report-progress-20260909.md八节清单,并记入BLOCKED.md。 npm run test:db未跑(无 Docker,基线同样红)。本轮未动数据库结构、未加迁移。- 未 push(按会话纪律,代码分支不自行推送)。