docs(consult): BUG-1074/1075 records, changelog, device checklist, progress and BLOCKED for first-frame and pacing (T4)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
Jesse_Chen
2026-09-28 09:36:21 +08:00
co-authored by Claude Fable 5.1
parent 891e9c62da
commit 20a54d176c
8 changed files with 178 additions and 3 deletions
@@ -0,0 +1,104 @@
# PROGRESS · 普通对话发出即有反馈,正文按打字节奏(2026-09-28)
任务书:`TASK-consult-first-frame-and-pacing-20260928.md`(产品已确认 D1、D2、D3;D4 160 字阈值不动;D5 工具 / 扣点 / 分类模型不动)。执行方:Claude fork 子代理(直接执行模式)。
## 基线
- `origin/staging` = `584ad05c`(2026-09-28);分支 `codex/consult-first-frame-and-pacing-20260928`,工作树 `.worktrees/consult-first-frame-and-pacing-20260928`。
- 开工实测(Node 22.14,`npm test`):4197 条 / pass 4143 / fail 24 / skipped 30 / cancelled 0;24 条失败均为无 Docker 的 DB / 部署套件(清单存 scratchpad `baseline2-fails.txt`)。
- 首屏 rootMainFiles gzip(9) 基线:130,933 B(`PROGRESS-consult-answer-the-question-20260927` 口径,4 个文件)。
- 分类耗时的真实分位数:无法从本机取得(staging 观测日志需登录后台)。代码上限是 `SMALLTALK_TIMEOUT_MS = 3_000`(fail-open);在 R2 的开流前序列里它前面还有会话读取(1 次查询)、`prepareConsultationRoute`(资料 / 档案 / 区间,2~3 次查询)、`reserve_consultation_usage`(1 次 RPC + 1 次计价查询)、`append_consultation_question`(1 次 RPC)。部署后用 `classification.<outcome>` 与本单新增的 `first_byte` 阶段 `durationMs` 复核。
## 做了什么
### T1 客户端首帧(BUG-1074,提交 `86d99115`)
- `chat-message-row.tsx`:删 `awaitingClassification`,`quiet` 只由 `responseKind === "smalltalk"` 决定。
- `consultation-run-timeline.tsx`:`QUEUED_TIMELINE_ROW.label` 改 `CONSULTATION_RECEIVED_LABEL`(「收到,正在看你的问题…」,新增在 `consultation-activity-labels.ts`,服务端首帧共用)。
- `DESIGN.md` §9、`docs/VOICE.md` 新段。
### T2 服务端先建流(BUG-1074,提交 `0bc66590`,补修 `891e9c62`)
- 新 `frontend/src/lib/stream-first-response.ts`:`streamFirstResponse({ requestId, run, onFirstFrame, onUnhandled })` 先返回 NDJSON Response,`start()` 里同步写首帧 `activity`(phase `received`),再 `await run()`:内层 NDJSON Response 逐字节透传,内层 JSON 拒绝转成一条 `run.failed` `request_rejected`(`consultationRejectionEvent(status, payload)`:句子按 recovery → message → error 取,与客户端 `payloadMessage` 同一优先级;原 `code` 作 `reason`),`run` 抛错则写通用 `run.failed` 并回调日志;外层 `cancel()` 把断连传给内层 reader(内层 `continueAfterDisconnect` 照旧结算)。
- `route.ts`:`const runTurn = async (): Promise<Response> => { … }` 包住从 `prepareConsultationRoute` 到最后 `catch` 的整段(**正文缩进未动**,保持 diff 最小、源码断言不变);`shouldUseAgenticRuntime(user)` 为假走原顺序(legacy 的 text/plain 回复不能走 NDJSON),为真 `return streamFirstResponse({...})`;`onFirstFrame` 记 `consultation-stream-first-v1` / `first_byte`(毫秒,不含原文)。寒暄分支、`streamSmalltalkResponse`、免费结算 RPC 与参数一行未改(任务书写「改成可写 controller 的函数」,实际用逐字节中继达到同一效果、少改一处结算代码;见偏离)。
- `consultation-agent-events.ts`:`publicActivityPhaseSchema` 加 `received`;`run.failed` 的 `code` 加 `request_rejected`,可选 `status`(400–599)与 `reason`(≤ 80),union 级 `superRefine`:`status` / `reason` 只允许 `request_rejected`,且 `request_rejected` 必须带 `status`。
- `use-consultation-run.ts`:删 `x-jyotish-response-kind` 响应头读取(只认 `run.completed.responseKind`);`request_rejected` 事件与 HTTP 非 2xx 走同一个 `rejectRun(status, message, code)`(401 跳登录、402 开充值、抛 `ConsultationResponseError`),后续按 `status` / `code` 的回滚、`session_full`、`session_not_consultation`、`request_conflict` 处理不变;首帧 `activity(received)` 不带已完成轨迹。`chat-message-row.tsx` 的 phase → 状态映射加 `received: "working"`。
#### 开流前 / 流内对照表(agentic 运行时)
| 失败 | 以前 | 现在 | 客户端 |
| --- | --- | --- | --- |
| Supabase 未配置 | JSON 503 | **仍开流前** JSON 503 | 不变 |
| 未登录 | JSON 401 | **仍开流前** 401 | 跳 `/login` |
| 请求体不合法 | JSON 400 | **仍开流前** 400 | 不变 |
| 会话读取失败 / 不存在 / 是校正会话 / 模型已变化 | JSON 503 / 404 / 409(`session_not_consultation`) / 409 | **仍开流前** | 不变(`session_not_consultation` 仍切到校正会话) |
| 会话模型不可用;旧版校正入口;提示词提取 | JSON 503 / 409 / 400 | **仍开流前** | 不变 |
| 计费配置不可用(`pricing_configuration_unavailable`) | JSON 503 | 流内 `request_rejected` status 503 reason 同 | 同句、同状态 |
| 咨询计划不可用(上限 / 模式不一致) | JSON 409 | 流内 status 409 | 409 → 回滚问题到输入框 |
| 主体 / 资料失败(`consultationSubjectFailureResponse`、`ConsultationProfileTruthError`) | JSON 4xx/503 | 流内,status 原值 | 不变 |
| 预留失败 / 模型不可用 | JSON 503 / 409 | 流内 | 不变 |
| 点数不足 | JSON 402 | 流内 status 402 | 开充值 + 回滚 |
| 保存问题失败 / 对话已写满 | JSON 503 / 409(`session_full`) | 流内,`reason: "session_full"` | 「开新对话」提示不变 |
| 生成失败(最后的 catch) | JSON 503(recovery 句) | 流内,句子取 recovery | 不变 |
| 寒暄 | NDJSON + 响应头 | 流内 NDJSON 透传,无响应头 | 只认 `run.completed.responseKind` |
### T3 客户端节奏(BUG-1075,提交 `3f8b8172`,补修 `891e9c62`)
- `stream-frame-buffer.ts`:`STREAM_RELEASE_MAX_CHARS = 4`(普通积压每帧上限)、`STREAM_RELEASE_CATCHUP_CHARS = 600`(超过才按 1/12 追赶)、`STREAM_SETTLE_MAX_FRAMES = 90`;`settle({ paced: true })` 按 `max(4, 剩余/90)` 每帧写完剩余、最后一帧 `settled: true`、返回 Promise;无参 `settle()` 保持一帧全放;页面隐藏一帧全放;写出期间 `dispose()` 延到写完;`reset()` 结束写出。
- `use-consultation-run.ts`:流正常结束 → `settleFrames(false)`:先给 streaming reply 打 `settling: requestId`,`frames.settle({ paced: true })`;停止 / 截断 / 失败 → 无参 settle 立即全放。数据层(入库、`persistSession`、结算、`completeConsultationInterface`)不等动画;`completeConsultationInterface` 不再清掉正在写出的回复(函数式更新);每个写出帧只在 `current.settling === requestId` 时更新,最后一帧清空;新一轮发送覆盖 streaming reply 后旧帧自然落空。
- `chat-message-view.ts`:`settlingChatMessageView(last, loading, streamingText)`,`latestAssistantView` / `chatMessageViews` 在 `loading` 为假、释放文本是已存回复严格前缀时用同一 `renderKey` 渲染前缀(`settling: true`);`chat-message-row.tsx` 写出期间时间线不 live。`home-types.ts` `StreamingReply.settling`。
- 滚动跟随不新写:`useConversationScrollAnchor` 观察内容增长,写出期间与流式期间同一路径。
### T4 记录(本提交)
- `docs/BUG_HISTORY.md`:BUG-1074、BUG-1075(`fixed_pending_acceptance`)。
- `CHANGELOG.md` 一条;`docs/testing/consult-first-frame-and-pacing-20260928.md` 7 条;`BLOCKED.md` 新段;`docs/tasks/README.md` 行状态「已实现待验收」。
## 测试
### 新增
- `frontend/tests/consult-first-frame-and-pacing-20260928.test.tsx`(13 条):T3 前缀覆盖 view / 行渲染、非前缀 / loading / 全文不覆盖、hook 源码(完成走节奏、停止 / 截断 / 失败立即、`completeConsultationInterface` 不清 settling、只一处 `settle({ paced: true })`);T2 ① 分类挂起首帧已出(`onFirstFrame` 40 ms)② 真实 `streamSmalltalkResponse` 流内完成、免费结算 1 次、无响应头 ③ 402 / 409 / 503 三种 JSON → `request_rejected` 字段逐一对照 + schema 约束 ④ NDJSON 首帧后逐字节一致、断连传内层、`run` 抛错通用失败;route 源码开流前 / 流内两张清单、分流、`first_byte`;客户端源码事件判寒暄、`rejectRun` 两处调用、计数不变。
- `stream-frame-buffer.test.ts` +3:160 字 40 帧;600 以上仍 12 帧追赶;剩余 300 字 paced settle ≤ 90 帧、单调、最后一帧 settled、无参 settle 一帧;dispose 延后。
### 改既有断言(原值 / 新值 / 原因,注释也写在测试里)
| 文件 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `chat-stream-settle-contract.test.ts` | live 时间线含「正在处理…」 | 含「收到,正在看你的问题…」、不含旧句 | D1 / BUG-1074 |
| `consultation-smalltalk-ui.test.tsx` | 分类回来前不渲染时间线、不出现「正在处理 / 正在分析」 | 渲染排队行(唯一一行、无 think / write 步);只有 smalltalk 行静默 | D1 / BUG-1074 |
| `consultation-smalltalk.test.ts` | `/const quiet = smalltalk \|\| awaitingClassification/` | `/const quiet = smalltalk;/` 且无 `awaitingClassification` | D1 / BUG-1074 |
| `consultation-agentic-runtime.test.ts` | `writerToSend` 的 phase 手写四值联合 | `PublicActivityPhase`(多 `received`) | D2 / BUG-1074 |
| `chat-navigation-a11y-contract.test.ts` | `/if \(response\.status === 402\) openAccountDialog(...)/` | `/if \(status === 402\) openAccountDialog(...)/`(`rejectRun` 里) | D2;调用点计数 3 / 2 不变 |
| `consultation-recovery.test.ts` | `/!response\.ok[\s\S]*throw new ConsultationResponseError\([\s\S]*response\.status/` | `rejectRun(status, message, code): never` 抛 `ConsultationResponseError(status, …)`,`!response.ok` 调 `rejectRun(response.status, …)` | D2 / BUG-1074:HTTP 体与流内事件共用一处 |
| `stream-frame-buffer.test.ts` | `settle()` 同步放完 | `settle({ paced: true })` 在 90 帧内写完、最后一帧 settled;无参 settle 仍一帧 | D3 / BUG-1075 |
`chat-stream-layout.test.ts` 的 `/正在处理…/` 仍成立(活动面板兜底文案未改,那是无时间线行的 fallback)。
## 门禁数字(完工实测,Node 22.14,本工作树,顺序执行)
| 项 | 结果 |
| --- | --- |
| `tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error(128 条既有 warning 未动) |
| `npm test` | 4212 条(基线 4197,+15)/ pass 4158 / fail 24 / skipped 30 / cancelled 0;退出码 1 = 那 24 条 |
| 失败清单比对 | 24 条失败按名称与基线逐条一致(`diff` 为空,均为无 Docker 的 DB / 部署套件)。全部测试名对比(基线取自 `9d01757f` 同代码的验收全量 TAP):新增 17 个名字;消失 2 个 = 本单按三栏改名的两条(`classification wait does not invent a thinking step before any activity` → `the classification wait shows the queued row from the send frame; only a smalltalk reply is quiet`;`many events collapse into one flush per frame and settle releases everything synchronously` → `… and a paced settle writes the rest out within the budget`),原名在测试注释的原值里 |
| `next build --webpack` | exit 0;`/`、`/chart`、`/ephemeris`、`/people` 均 ○ Static |
| 首屏 rootMainFiles gzip(9) | 130,950 B(4 个文件),基线 130,933 B,+17 B(+0.013%,±2% 内);量于 `891e9c62`,其后只改了测试与文档 |
| `page.tsx` | 未动 |
| `python3 -m pytest tests/test_repo_privacy_markers.py -q` | 通过(写了实测数字的文档推送前必跑) |
## 提交
| SHA | 内容 |
| --- | --- |
| `86d99115` | T1 客户端首帧 |
| `3f8b8172` | T3 放字节奏与 paced settle(首版默认 paced) |
| `0bc66590` | T2 先建流、事件、客户端 |
| `891e9c62` | T2/T3 补修:paced 改 opt-in、`rejectRun` 合并两条拒绝路径(全量跑出 7 条新失败后改) |
| (本提交) | T4 记录 + `consultation-recovery.test.ts` 三栏(第二次全量跑出的最后 1 条源码断言) |
## 偏离与说明
1. **寒暄流没改成「写 controller 的函数」**(任务书 T2 第 1 条):外层流把内层 `streamSmalltalkResponse` 的 Response 逐字节中继,效果相同(先免费结算再发 answer.delta + run.completed),且不动 BUG-976 那组结算代码;本机无 Docker 跑不了它的真实 PG 测试,少改一处更稳。假模型层的断言补在新测试 ②。
2. **paced settle 改成 opt-in**:首版把 `settle()` 默认改成按节奏,全量跑出 7 条新失败——校正面 `rectification-chat-turn-run.ts` 两处 `settle()` 之后同步合并快照(`rectification-dup-question`、`rectification-opening-plain`、`rectification-activity-receipt` 5 条),以及 Node 无 `window.requestAnimationFrame` 的未捕获异常。改成 `settle({ paced: true })` 只在普通对话完成路径用,校正面契约不动;校正面要同样节奏另立单(BUG-1075 防复发写明)。另 2 条是 `chat-navigation-a11y-contract` 数 `window.location.assign("/login")` / `openAccountDialog("billing", …)` 的调用点,首版在事件分支复制了两句,改成 `rejectRun` 共用后计数回到 3 / 2。
3. **legacy 运行时不先建流**:`CONSULTATION_AGENTIC_RUNTIME=legacy` / canary 非成员仍是原顺序(text/plain 回复无法走 NDJSON)。staging / 生产 compose 都是 `enabled`。
4. **`route.ts` 的 `runTurn` 正文没有重新缩进**:包一层箭头函数但保留原缩进,避免上千行的纯缩进 diff 打破十几个按源码文本断言的合同测试;lint 无缩进规则报错。
5. 首帧的 `activity(received)` 在时间线 reducer 里不加行(返回原状态),客户端从发送那一帧起已经显示同一句排队行;这样那一帧到达时字不变、行不跳。
6. `next build` 默认 Turbopack 对软链 `node_modules` 报错(同前几单),用 `--webpack` 量 `/` 与 gzip。
7. 真机、真实耗时、真实 PG 结算测试:见 `BLOCKED.md`。
+1 -1
View File
@@ -245,7 +245,7 @@
| `TASK-billing-pricing-20260830.md` | `PROGRESS-billing-pricing-20260830.md` | 计费闭环与功能级定价 | 已合入 | `aaaa5fb7`~`a499c344` |
| `TASK-cost-accounting-gaps-20260927.md` | — | **成本记账补漏与缓存分价**:七处模型调用不入账(标题 / 摘要 / 校正意图 / 采用旁白 / 校正开场 / 生时引导 / 每日星语);缓存命中按全价算、单价表无缓存列;多事件账本 `metadata.cache` 被覆盖;失败运行无成本记录;测算页不显示 token 分位数;**后台模型表单没有单价输入框,单价只能是 0**。只加「记」不加「扣」,不动售价与会员参数 | 待领取 | BUG-1064~1069 预留;部署后由产品按 `docs/testing/cost-accounting-20260927.md` 采真实成本 |
| `TASK-consult-answer-the-question-20260927.md` | `PROGRESS-consult-answer-the-question-20260927.md` | **普通对话回答问的那句话**:反差只写盘上、删「按我的理解答」、禁写动机句;追问轮 ≤ 200 字直接回答不重开四标题骨架(服务端按「本会话已有解读」切指令,不用正则);清单是核对项不是段落大纲;步骤栏「先抓住你真正在问的事」改「先回答你问的这件事」 | 已实现待验收(Claude 子代理直接执行) | 分支 `codex/consult-answer-the-question-20260927`(未推送);基线 `2ce3c216`;BUG-1070~1073;Skill 不 bump;真机清单 `docs/testing/consult-answer-the-question-20260927.md` |
| `TASK-consult-first-frame-and-pacing-20260928.md` | — | **普通对话发出即有反馈、正文按打字节奏放出**:客户端分类前静默(BUG-976 副作用)+ 服务端分类后才开流 → 发出后一两秒空白;服务端 160 字截留 + 客户端 settle 一帧全放 → 追问轮正文一下全出。D1 首帧「收到,正在看你的问题…」、D2 先建流、D3 每帧上限 + 限时收尾、D4 160 字阈值不动 | 待产品确认 D1–D3 | 基线 `56ca8b26`;BUG 从 1074 起 |
| `TASK-consult-first-frame-and-pacing-20260928.md` | `PROGRESS-consult-first-frame-and-pacing-20260928.md` | **普通对话发出即有反馈、正文按打字节奏放出**:客户端分类前静默(BUG-976 副作用)+ 服务端分类后才开流 → 发出后一两秒空白;服务端 160 字截留 + 客户端 settle 一帧全放 → 追问轮正文一下全出。D1 首帧「收到,正在看你的问题…」、D2 先建流、D3 每帧上限 + 限时收尾、D4 160 字阈值不动 | 已实现待验收 | 分支 `codex/consult-first-frame-and-pacing-20260928`(未推送);进度 `PROGRESS-consult-first-frame-and-pacing-20260928.md`;BUG-1074 / BUG-1075 |
| `TASK-cend-ui-claude-alignment-20260916.md` | `PROGRESS-cend-ui-r1/r2/r3-20260916.md` | **C 端界面向 claude.ai 产品界面对齐(三轮串行 R1→R2→R3,都动 `globals.css`,不得并行)**:根因是 `frontend/CLAUDE_DESIGN.md` 扒的是 **claude.com 营销官网**,它自己在 Known Gaps 里写明 claude.ai 产品界面不在范围内,而 `DESIGN.md:3` 把它当成了产品界面的实现契约。**R1**:`--font-display` 里 Tiempos Headline / StyreneB **从未加载**(无 `@font-face`、`public/` 无字体、`layout.tsx` 只 vendor 了 Inter),中文标题全站落到 **宋体 / SimSun**,波及 20 处含助手回答的 h2/h3(BUG-737);亮色强调色 `#85432f` 与暗色 `#d78064` 不同源,产品拍板亮色换 **Claude coral `#cc785c`**,**易漏点**是 `globals.css:16` 的 `--color-ring` 硬编码在 `@theme inline` 里不跟随 `:root`,另有第四个 `:root` 亮色块(`:4358`)必须同步(BUG-738);`.composer-footer` 常驻 44px + 顶栏 68px + `--composer-reserve` 148px,每屏固定吃掉 216px,模型选择器移进输入框内部、删掉底栏、顶栏收到 46px 并删「分析对象」副标题。**R2**:空状态是营销落地页(hero 卡 + 两张 132px 入口大卡 + 3 列 156px 主题卡),输入框被压在 **800px 以上**内容之下,重排成「问候 + 居中输入框 + 两枚入口 pill + 一排 chip」。**R3**:侧栏两个 `<details>` 拍平成一条「最近」、星盘的两个入口(侧栏分组 + 账户菜单)收敛到一处、删掉逐条助手头像。**决策记录 D3 推翻 DESIGN.md「报告强调色与应用同源」一句**(报告刻意保留深棕)。原型图 https://claude.ai/code/artifact/da275da6-2954-4f50-99aa-32bb8694d38b(三套画面 + 明暗,页面标题就是建议字体栈的实际渲染)。环境缺口:无登录态无 Chrome,四项真机观感留 `docs/testing/`。BUG 段 737–738 | 待领取 | — |
| `TASK-cend-surfaces-claude-alignment-20260916.md` | `PROGRESS-cend-shell-20260916.md`、`PROGRESS-cend-report-20260916.md`、`PROGRESS-cend-rectification-20260916.md`、`PROGRESS-cend-chart-eph-20260916.md` | **次级页面对齐(上一单的续篇,R4→R5/R6,R7、R8 可并行)**:星盘 `/chart`、星历 `/ephemeris`、报告 `/reports` **各是脱离 app 外壳的独立全屏页**,顶部只有一个「返回对话」链接、侧栏整个消失,且三家各写了一套一模一样的 `*-shell`/`*-topbar`/`*-hero` 骨架——与上一单 E5 同根因(营销站 band 结构被套到产品界面)。**R4** 抽只读导航外壳 `AppNavRail`(只用现成的 `GET /api/sessions` + `GET /api/account`,会话行走 `sessionHref` 跳 `/?c=<uuid>`;**刻意不带**重命名/删除/收藏/归档——那套连着 `Home()` 的乐观更新与回滚,搬过来会撞 useState 增长门禁)。**R5** 星盘五 tab 下划线化 + 参数合表 + 行星表横向滚动;星历日期导航改 `‹ 日期 ›`。**R6** 报告中心卡片网格改行式列表;阅读页加常驻目录。**R7** 生时校正把可信区间从盘面板标题行提成常驻条(窄屏 `.is-compact` 下盘面板是 overlay,现在默认看不到区间),五个 `technique-audit` 折叠块收成两段。**R8** 设置内容区收窄(880px 弹窗里表单铺了 690px)、套餐卡三修饰符收敛成两态。**已解锁**:原挡路的设置单已于 `111b4a84`(BUG-698)合入。**两条不得回退**:BUG-698 的 `@supports (height: 1dvh)` 写法(重复声明回退会被 Lightning CSS 折叠)、BUG-616/617 的报告盘面 grid 实现。默认不占 BUG 号 | **R4–R8 全部已实现并验收合入** | R4:抽出 `AppNavRail`(只读,两个 GET,零写操作)+ `SecondaryShell`,三个次级页并入 app 外壳并删掉各自的 shell/topbar/hero;四个路由渲染标记**完全不变**(`/` `/chart` `/ephemeris` 仍 Static);CSS gzip −0.25%。`/reports/[reportId]` 留给 R6 与目录一起做。两处自身健壮性问题被测试抓到:`usePathname()` 可为 null、`fetch` 可能不存在。差点弄丢 BUG-717 的 eyebrow 文案(已放回)。R8:表单分区收窄到 440px(列表分区不变)、套餐卡三修饰符收敛成互斥的 `is-current` / `is-recommended`,`--highlighted` 删除改为滚动定位;手机端 `order:-1` 改挂 `[data-plan-alias]`(版位不是状态)。测试 3350→3354(净增 4),失败清单与基线逐条一致;`/` 仍 Static;我的干净构建实测 CSS gzip −3 字节。**遗留待产品拍板**:`?plan=` 深链现在完全没有视觉指向,只有滚动位置。R6:报告中心卡片网格改行式列表、阅读页并入外壳并把目录挪到右侧常驻。**任务书 E10 过期**——目录在 `cfcd369d` 就已存在,本轮是挪位置定稿而非从零加。挂外壳带出一个真实打印风险已处理:`.chat-app`/`.chat-panel` 是 `height:100%;overflow:hidden`,裸 `window.print()` 会把九节报告裁成一页,阅读页因此多挂一条只在挂载期生效的 print 样式解锁外壳。「生成中的分节进度」做不了——`REPORT_LIST_COLUMNS` 不返回节数,按 VOICE.md 不许前端编。R7:区间常驻条与盘面折叠收敛。**任务书 E9 也不准确**——对话区顶部早有常驻条 `RectificationTimeline` 且窄屏可见,真正只在盘面标题行的是**代表分钟**;因此没另造第二条,在既有条上补齐代表分钟与已答题数(与盘面同一次 `workingRectificationTime()` 调用)。折叠块实际是 **8 个**不是 5 个。**触发让步顺序第 5 条**:收窄进度未做——服务端无该字段,且 `candidate_range` 会放宽(BUG-572),前端相减会把一次放宽报成收窄,已写进 `BLOCKED.md`。顺带修掉一个**静默失效的旧断言**(`slice(indexOf(A), indexOf(B))` 在 B 改名后变成几乎整份文件,四条 `doesNotMatch` 假通过)|
| — (产品口头拍板,无任务书) | `PROGRESS-chart-glyphs-20260918.md` | **星盘改用行星符号,度数进行星表(非 Bug,无 BUG 号)**:北印星盘(星盘页 / 校正右栏 / 我的报告三处共用 `VedicChartSvg`)宫内不再画「水 19°」「罗逆 29°」这类文本——一宫三颗星就叠三行、字号压到 8。改成九个彩色符号(☉☽☿♀♂♃♄☊☋),每行最多 3 个、逆行改符号下同色横线、第 1 宫加 `As`、盘下两行图例。颜色只认星不表吉凶,罗计共用中性灰,`--color-planet-*` 九个 token 在 `:root` 与两个深色块各一份;颜色挂 `is-*` 修饰类而非行内 `style`(报告标记合同禁 `style=`)。符号字体是 vendored 的 9 字形 Noto Sans Symbols 子集(OFL,1.5 KB),`--font-planet-glyphs`,♀♂ 带 U+FE0E 防 iOS 画成表情。星盘页行星表四列扩到八列(行星 / 黄道经度 / 速度 / 宫位 / 星宿 / 宿主 / pada / 状态),度数精确到分,首行是上升(`vedic.ascendant` 新字段)。数据来源:`toNorthIndianChart()` 加结构化 `occupantGlyphs`;校正与报告两条路线的模型只有引擎/golden 显示文本,按首字解析并有测试锁住。**星宿未译中文**——仓库无对照表,印度 27 宿与中国二十八宿不同源,不自造 | 已实现,待验收 | `codex/chart-glyphs-20260918`:tsc 0 / lint 0 error(118 warning 不变)/ npm test 3471→3484 条、36 红与基线 `84b293fb` 逐条相同、0 新红 / `○ /` Static / 首屏 JS gzip 1,450,774→1,455,017(+0.29%),CSS gzip +2.23%(构成见 PROGRESS §2)。真机走查欠(iOS 符号呈现、深浅两主题配色、375px 横滚、打印) |
@@ -31,7 +31,7 @@
- 网络与模型:分类耗时有埋点(`classification.<outcome>` 的 `durationMs`),执行方开工时从 staging 观测日志取一周分位数写进 PROGRESS;即便分类只要 300 ms,R1 也会让这 300 ms 是空白。
- `createVisibleTextTransformer`(`stream-text-response.ts`)只处理隐藏块,不按句截留。
## 决策记录(产品 2026-09-28,待确认后执行;Claude 建议如下)
## 决策记录(产品 2026-09-28 已确认 D1–D3,Claude 直接执行)
- D1 **发出那一帧就有步骤栏**:去掉 `awaitingClassification` 的静默,每轮从发送起显示时间线,首行是确定性文案「收到,正在看你的问题…」(改 `QUEUED_TIMELINE_ROW` 的 label;文案对照 VOICE)。寒暄轮:服务端一旦判定寒暄,客户端收到 `responseKind: "smalltalk"` 即按现有逻辑转 `quiet`,步骤栏消失、只剩一句回复。代价是寒暄前会闪约一秒「收到…」,Claude 认为可接受;若产品不接受,退而求其次是 D1':首行不进时间线,只在头衔旁放一个 `InlineSpinner`(DESIGN §9 已有的唯一 live 标记),寒暄回来一样消失。
- D2 **服务端先建流**(同 BUG-1047 D3 的做法):`route.ts` 在鉴权与请求体校验之后就返回流;会话读取、准备、预留扣点、分类、以及最终的 `streamAgentResponse` / 寒暄流全部在 `start()` 里跑,首帧推一条确定性 `activity` 事件(label 同 D1 文案)。分类为寒暄时,在同一条流里写寒暄回复与免费结算(现在 `streamSmalltalkResponse` 自建 Response,需改成可写入外层 controller 的形式)。`x-jyotish-response-kind` 响应头在先建流后无法再设,客户端改为只认 `run.completed` 事件里的 `responseKind`(`use-consultation-run.ts` 第 838 / 924 行已有事件路径,删头部路径)。错误路径(预留失败、会话不存在等原本返回 4xx/5xx JSON 的分支)改为流内 `run.failed` 事件,文案不变;客户端对这些 code 的处理保持一致,写进测试。