docs(tasks): open consult first-frame and pacing brief (blank before classification, snap release)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
56ca8b266a
commit
584ad05c36
@@ -245,6 +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-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 横滚、打印) |
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# TASK · 普通对话:发出即有反馈,正文按打字节奏放出(2026-09-28)
|
||||
|
||||
## 基线
|
||||
|
||||
- `origin/staging` = `56ca8b26`(2026-09-28)。开工时 `git fetch origin --prune` 后以最新 `origin/staging` 为基线,写进 PROGRESS。
|
||||
- 分支 `codex/consult-first-frame-and-pacing-20260928`,工作树 `.worktrees/consult-first-frame-and-pacing-20260928`。
|
||||
- 与 `TASK-cost-accounting-gaps-20260927` 无文件交集。改 `route.ts` 的轮次要串行:本单动 `frontend/src/app/api/consult/route.ts` 的分类与开流段,若同期有别的单改这个文件,本单在后。
|
||||
|
||||
## 事故实证(2026-09-28 产品 staging 真机,`deepseek-v4-flash`)
|
||||
|
||||
产品原话:「用户发完信息后没有立马就有思考中或者步骤栏出现,会卡一两秒之后再出;而且答案也是一下全出来,没有流式输出的动画。」
|
||||
|
||||
两个现象各有确定的代码来源,都不是网络或模型慢。
|
||||
|
||||
## 根因(按符号定位,基线 `56ca8b26`)
|
||||
|
||||
### 现象一:发出后一两秒什么都没有
|
||||
|
||||
- **R1 客户端故意静默到分类结果回来**。`frontend/src/components/chat-message-row.tsx` 第 57 行起:`awaitingClassification = state === "thinking" && !text && !activity && !thinkingText && timeline.length === 0`,命中即 `quiet`:不渲染步骤栏、不渲染活动面板、头衔只写「Jyotisha」。这是 BUG-976 寒暄快路(`539d4dae`)加的,目的是「你好」的回复不要带步骤栏。副作用是**每一轮**在服务端第一个事件到达之前都是空白:`consultation-run-timeline.tsx` 里本来有 `QUEUED_TIMELINE_ROW`(「正在处理…」,BUG-475 为"首事件前无反馈"加的),被 `quiet` 挡住不显示。
|
||||
- **R2 服务端在开流之前做完分类**。`frontend/src/app/api/consult/route.ts`:请求进来先读会话(第 17 行附近)、解析模型、`prepareConsultationRoute`(资料 / 区间 / 时区,第 411 行)、`reserve_consultation_usage` 预留扣点(RPC)、`append_consultation_question`(RPC),然后第 636 行 `classifyConsultationTurn`——用**会话模型**做一次结构化分类(`consultation-smalltalk.ts`,3 秒 fail-open)。分类完才走到第 670 行(寒暄流)或第 1150 / 1252 / 1388 行 `streamAgentResponse` 建流。浏览器在这之前收不到任何字节。所以一两秒 = 几次数据库往返 + 一次模型分类。
|
||||
- 两条叠加:客户端等首事件,服务端首事件要等分类。校正面在 BUG-1047 已按「先建流、首帧确定性进度句」修过(`agent-route-agent-turn.ts` 第 149 行 `new ReadableStream` 内做分类,`turn.progress received`),普通对话没有同步。
|
||||
|
||||
### 现象二:正文一下全出来
|
||||
|
||||
- **R3 服务端按步截留**。`frontend/src/lib/stream-agent-response.ts` `ANSWER_RELEASE_CHARS = 160`、`readsAsAnswer()`:一步里的正文出现 Markdown 标题或满 160 字才放出,不到就扣到这一步结束(`settleStep`)再整段放出。这是 BUG-1053 取答边界(防止工具前的过程话进正文)。首轮开场 ≥ 160 字,所以前 160 字是一坨、之后才流;追问轮(本单之前的 `TASK-consult-answer-the-question` 后正文 ≤ 200 字、无标题)多数不满 160 字,**整段在步结束时才放出**,而步结束几乎就是流结束。
|
||||
- **R4 客户端收尾一帧吐完**。`frontend/src/lib/stream-frame-buffer.ts`:正常时每帧放 `max(2, backlog/12)` 字,一坨 160 字约 12 帧(0.2 秒)吐完;`settle()` 把 `releasedAnswer = targetAnswer` **一帧全放**。`use-consultation-run.ts` 第 952 行读完流立刻 `frames.settle()`。追问轮的正文在流结束前一刻才到,于是 R3 + R4 = 整段一帧出现,没有任何流动。
|
||||
- 首轮之所以也感觉「一下出来」:160 字一坨 + 之后模型快(deepseek-flash)每次到达几十字、12 帧清完,看起来是一块一块跳,不像打字。
|
||||
|
||||
### 不是根因的
|
||||
|
||||
- 网络与模型:分类耗时有埋点(`classification.<outcome>` 的 `durationMs`),执行方开工时从 staging 观测日志取一周分位数写进 PROGRESS;即便分类只要 300 ms,R1 也会让这 300 ms 是空白。
|
||||
- `createVisibleTextTransformer`(`stream-text-response.ts`)只处理隐藏块,不按句截留。
|
||||
|
||||
## 决策记录(产品 2026-09-28,待确认后执行;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 的处理保持一致,写进测试。
|
||||
- D3 **客户端按打字节奏放**:`stream-frame-buffer.ts` 加每帧上限 `STREAM_RELEASE_MAX_CHARS = 4`(60 fps ≈ 240 字/秒,高于模型平均产出,不会越积越多);只有积压超过 `STREAM_RELEASE_CATCHUP_CHARS = 600` 时才回到现在的 `backlog/12` 追赶。`settle()` 不再一帧全放:改为「按节奏放完剩余、但总时长不超过 1.5 秒」(剩余 / 1.5 秒 与 4 字/帧 取大),放完再 `emit(true)`;结算、扣点、消息入库不等这段动画(数据层照旧立即结算,只是显示层慢慢写完)。停止 / 断线 / 失败仍立即全放。
|
||||
- D4 **服务端 160 字阈值不动**(BUG-1053 的过程话防线)。追问轮正文短,靠 D3 的节奏放出即可有流动感;若真机后仍觉得首字太慢,再单独立单讨论按「首个句号」放出。
|
||||
- D5 排盘工具调用、扣点、结算、Bug-976 的分类模型与 3 秒 fail-open、`stepScopedAnswer` 的丢弃规则都不改。
|
||||
|
||||
## 硬红线
|
||||
|
||||
1. 不得用正则或关键词判寒暄(BUG-976 防复发);分类仍是模型。
|
||||
2. 不新增第二种 live 标记、不加 spinner / 骨架(DESIGN §9);首行文案走 `InlineSpinner` + shimmer 既有样式。
|
||||
3. `page.tsx` 不增长;`stream-agent-response.ts` 的取答规则不动(D4)。
|
||||
4. 前端红线全套;改既有断言三栏;测试总数不低于开工实测。`chat-stream-settle-contract.test.ts`、`stream-frame-buffer` 相关测试、`consultation-smalltalk.test.ts`、`consult-single-pass-answer-20260927.test.ts` 大概率命中。
|
||||
5. 先建流后,任何原来靠 HTTP 状态码表达的失败都必须在流内有等价事件并被客户端同样处理;不得让预留扣点失败变成静默空回复。用真实 PostgreSQL 的既有寒暄结算测试(BUG-976 那组)必须仍过。
|
||||
6. 禁止 `git stash`;只 `git add <具体路径>`;不推送,回报后由 Claude 验收再推 staging。
|
||||
|
||||
## 任务分解
|
||||
|
||||
### T1 客户端首帧(R1、D1)
|
||||
- `chat-message-row.tsx`:删 `awaitingClassification` 对时间线的屏蔽;`quiet` 只由 `responseKind === "smalltalk"` 决定。`consultation-run-timeline.tsx` `QUEUED_TIMELINE_ROW.label` 改「收到,正在看你的问题…」。`frontend/docs/VOICE.md`、`frontend/DESIGN.md` §9 同步。
|
||||
- **验收**:组件测试:loading 且无事件时渲染该行;收到 smalltalk 标记后行消失;`chat-stream-settle-contract.test.ts` 三栏。
|
||||
|
||||
### T2 服务端先建流(R2、D2)
|
||||
- `route.ts`:鉴权 + 请求体校验后即 `new Response(stream)`;其余搬进 `start()`;首帧 `activity` 事件;寒暄在流内完成(改 `stream-smalltalk-response.ts` 为写 controller 的函数);错误分支改流内 `run.failed`。
|
||||
- `use-consultation-run.ts`:删响应头判寒暄,只认事件;失败事件 code → 现有文案映射不变。
|
||||
- 观测:`logAgentObservability` 加 `first_byte` 阶段(从请求进入到首帧写出的毫秒数),不含原文。
|
||||
- **验收**:假模型 route 测试:① 首帧在分类完成前写出(用挂起的分类 Promise 证明首帧不等它);② 寒暄轮流内完成 + 免费结算调用一次;③ 预留失败 → `run.failed` 事件且客户端映射文案与原 JSON 一致;④ 咨询轮事件序列与现在相同(首帧多一条 activity)。真实 PG 寒暄结算测试仍过。
|
||||
|
||||
### T3 客户端节奏(R4、D3)
|
||||
- `stream-frame-buffer.ts`:每帧上限、追赶阈值、`settle()` 限时放完;停止 / 失败 / 断线走立即全放的旧路径(新参数 `immediate`)。
|
||||
- **验收**:纯函数测试:160 字一坨在 ≥ 40 帧内放完;600 字以上积压回到追赶;settle 剩余 300 字在 ≤ 1.5 秒内放完且最后一帧 `settled: true`;`immediate` 一帧全放。`use-consultation-run` 的结算测试确认消息入库时间不受动画影响。
|
||||
|
||||
### T4 记录
|
||||
- `docs/BUG_HISTORY.md` 从 **BUG-1074** 起(开工核对最大号):BUG-1074 发出后到分类完成前整行空白(R1+R2,关联 BUG-475、BUG-976、BUG-1047);BUG-1075 短正文在流结束时一帧全出(R3+R4,关联 BUG-1053)。状态 `fixed_pending_acceptance`。
|
||||
- `CHANGELOG.md` 一条;PROGRESS(含分类耗时分位数、首帧耗时前后对比);`docs/testing/consult-first-frame-and-pacing-20260928.md` 真机清单:① 发送瞬间步骤栏出现「收到,正在看你的问题…」;② 「你好」一秒内换成一句寒暄、无步骤栏;③ 首轮正文从第一坨开始按打字速度出现,不跳块;④ 追问轮 100~200 字的回答能看到逐字写出,不是一帧出现;⑤ 点停止立即定格。
|
||||
|
||||
## 让步顺序
|
||||
|
||||
1. D2 若 `streamSmalltalkResponse` 改造牵动 BUG-976 的真实 PG 结算测试超出一天工作量,先做 D1 + D3(客户端两项已能消掉大部分体感),D2 单独开单,PROGRESS 写明。
|
||||
2. D3 若 `settle()` 限时放完与 `useConversationScrollAnchor` / `JumpToLatestButton` 的结算滚动冲突(正文还在长、锚点已结算),把动画期间视为仍在流式(`settled` 延到放完那一帧),不得另写第二套滚动跟随。
|
||||
3. 任何一条做不到写 `BLOCKED.md`。
|
||||
|
||||
## 开工前置命令
|
||||
|
||||
```bash
|
||||
cd /workspace/Jyotisha && git status -sb | head -1
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/consult-first-frame-and-pacing-20260928 .worktrees/consult-first-frame-and-pacing-20260928 origin/staging
|
||||
cd .worktrees/consult-first-frame-and-pacing-20260928/frontend && export PATH=/exec-daemon:$PATH && node -v # v22
|
||||
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -5
|
||||
grep -rhoE "BUG-1[0-9]{3}" ../docs/BUG_HISTORY.md ../docs/tasks/*.md | sort -u | tail -1
|
||||
```
|
||||
|
||||
## BUG 编号起点
|
||||
|
||||
BUG-1074。
|
||||
Reference in New Issue
Block a user