Batch 1 (dispatch now): rect-scoring-perf, node-runtime-resources, home-sidebar-rerender, api-disk-cache-retention, report-reference-perf, consult-prep-parallel, frontend-dead-code. Batch 2 (queued on predecessors): api-chart-subprocess-gate, answer-effort-ab, consult-route-split, pl9-render-split, python-hygiene, dasha-unify, api-server-service-extract, data-layer-types, contracts-helpers-dedupe. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
18 KiB
18 KiB
TASK · 普通对话路由拆分(第一阶段:开流前加载 + 计费结算)(issue #待定)
- 日期:2026-10-13
- 来源:「2026-10-13 全仓屎山与性能排查」·
/api/consult路由过大 - 执行方:Grok(Windows + Docker),分支
codex/consult-route-split-20261013,只推这个分支 - 验收:Claude(Linux)
- BUG 编号:无 BUG 编号(重构,无缺陷;记录写 PROGRESS)(另:
TASK-upstream-sync7-20261014占 BUG-1318~1322,本批号段整体从 BUG-1323 起) - 开工前置:
consult-prep-parallel与answer-effort-ab都已合入origin/staging。前者在route.ts开流前段落新建了frontend/src/lib/consult/(prep-flags.ts、turn-prep.ts),本单在此基础上继续;后者改consultation-tools.ts与 prepareStep。三单串行,不得并行开工。
1. 基线
- 基线 commit:起草时
origin/staging77057a819;开工时以两单前置合入后的 staging 为准,行号会变,一律按符号定位。 frontend/src/app/api/consult/route.ts:1,840 行;export async function POST从第 296 行到文件末尾(约 1,545 行),内部闭包函数 17 个,鉴权、扣费、校验、编排、流输出混在一个函数作用域里。- 起草时
POST内的段落(77057a819行号,仅供定位):
| 段 | 行 | 内容 | 本单 |
|---|---|---|---|
| 开流前加载 | 296–443 | 建客户端、getUser、删号冻结、请求体、读会话、会话模型、历史窗口、提示词提取拦截、输入审核 |
搬(与前置单的 turn-prep.ts 合并) |
| 预留回调 | 488–503 | resolveFeaturePricing + reserve_consultation_usage |
搬 |
| 取消 / 追加问题 | 602–668 | cancel()、append_consultation_question 与 session_full |
搬 |
| 寒暄免费结算 | 709–737 | complete_consultation_free(smalltalk) |
搬(结算函数;streamSmalltalkResponse 调用留在 route) |
| 上下文摘要 | 748–782 | checkpointConsultationContext |
搬 |
| 用量与结算 | 783–962 | usagePayload、moderateAnswer、completeModeratedResponse、completeResponse、completeDirectRectificationTurn、settleResult / settle |
搬 |
| 标题副作用 | 970–996 | titleSideEvent |
留 |
| 运行编排 | 998–1630 | runAgenticConsultation(流选项、重试、续写、窗口预计算) |
留(第二阶段) |
| legacy 与流输出 | 1630–1839 | 旧运行时、streamTextResponse、streamFirstResponse |
留(第三阶段) |
2. 事故实证
- 一个函数作用域内约 1,545 行;计费 RPC(
reserve_/cancel_/append_/complete_consultation_*)散在 6 处闭包里,靠共享的let变量(classificationUsage、latencyState、outputModerated、outputBlocked、settlement)通信。 - 33 个前端测试文件读
route.ts源码做正则/indexOf断言(grep -rl "api/consult/route" frontend/tests),按读源码变量的出现次数估约 400 处;没有任何测试 importroute.ts的POST跑行为——计费顺序、冻结、审核先于预留这些红线今天全靠源码文本守。 - 后果:每次改动都要同步改源码断言(AGENTS.md §7.8 记录的 BUG-933 / 934 / 939 / 992 / 1014 都是这类),而真正的行为没有测试。
3. 根因
不是回归。BUG-1074(先建流)、BUG-976(寒暄免费结算)、BUG-1306(明说要校正免费结算)、09-30 内容审核等一轮轮加在同一个函数里,从未拆分。
关联红线(全部原样保留,本单只搬不改):
| 来源 | 红线 |
|---|---|
| BUG-1074 | 开流前只答鉴权 / 请求体 / 会话 / 会话模型;其余在 runTurn 内;streamFirstResponse 首帧 |
| BUG-976 | reserve 在分类前;寒暄走 complete_consultation_free |
| BUG-1306 / 1317 | 直达校正轮免费结算,不走 complete_consultation_response |
| BUG-1051 / 1142 | 截断不扣点;答题钟 |
| 09-30 审核 | 输入审核先于预留;输出审核命中走 complete_consultation_moderated;三条 agentic 流都带 moderateOutput: moderateAnswer |
| 账户注销 | 冻结检查 if (frozen) return frozen; 在任何写操作前 |
consult-prep-parallel |
§5.2 的状态码优先级;审核在 beforeReserve 内等结论 |
4. 决策记录
- 产品授权:2026-10-13 产品负责人同意按排查结论全部立单交 Grok 执行;重构只做第一阶段。
- 纯搬移,行为不变:不改文案、状态码、RPC 参数、调用顺序、日志字段;不顺手「优化」。
- 拆分方向(四层):鉴权与会话加载 / 计费预留与结算 / 运行编排 / 流输出。本单只做前两层;后两层见 §10。
- 测试迁移原则:源码断言跟着代码搬到新文件(读新文件源码),并为搬出去的计费函数补行为测试(新模块可注入假
accounting客户端,今天做不到)。不得把多个文件拼成一个字符串来让跨文件的indexOf顺序断言继续「通过」——那是伪造顺序。 - 不推翻任何既有红线。
5. 硬红线
- 行为不变:同一请求在改前改后的 HTTP 状态、NDJSON 事件序列、RPC 调用序列(名称 + 参数)、落库内容完全相同(T4 差分)。
- 测试总数不得低于开工时
origin/staging实测;每条被改的断言写「原值 / 新值 / 原因」三栏;不得删除断言而不替换;替换后的断言不得更弱(同一事实,换文件或换成行为断言)。 - 新文件只放在
frontend/src/lib/consult/;route.ts净减少;新模块import "server-only"(与现有服务端模块一致)。 - 改动规模:diff 合计 < 1,500 行(
git diff --stat origin/staging...HEAD的增删之和;纯搬移的增删都算),超了就按 §7 砍。 - 不改
consultation-route-service.ts::prepareConsultationRoute的签名与行为;不改stream-first-response.ts、stream-agent-response.ts。 - 不动表结构与迁移;不改
scripts/jyotish_api_server.py;不碰frontend/src/app/(app)/page.tsx;不改.gitea/workflows/**;不升级依赖;不顺手修 warning。 - 不改
frontend/tests/account-deletion.test.tsx的「付费路由清单」语义:route.ts若不再直接调用refuseWhenAccountDeletionPending,测试改为断言route.ts调用loadConsultTurnHead(或前置单定的名字)且该模块含refuseWhenAccountDeletionPending(与if (frozen) return frozen;(或等价的冻结即返回),三栏写明。
6. 任务分解
T1 · 开流前加载归入 lib/consult/turn-prep.ts(Grok 自跑 / Claude 验收)
- 把前置单留在
route.ts的开流前剩余部分(旧入口 409、历史窗口consultationHistoryWindow、blocksPromptExtraction、审核启动与moderationContext)搬进turn-prep.ts,POST开头变成一次调用:失败直接return response,成功拿到{ user, parsed, chatSession, sessionModel, historyWindow, storedHistory, moderation }。 - 搬移时保留每个
NextResponse.json(...)字面量原文。 - 验收:
consult-turn-prep-20261013.test.ts(前置单已建)补「旧入口 409」「提示词提取 400」两种返回;consult-first-frame-and-pacing-20260928.test.tsx第 256–289 行「开流前清单」改为:标记在turn-prep.ts源码中存在,且route.ts中对loadConsultTurnHead的调用位于const runTurn = async之前(三栏)。
T2 · 计费与结算归入 lib/consult/settlement.ts(Grok 自跑 / Claude 验收)
- 新建工厂
createConsultSettlement(ctx),ctx显式传入今天闭包捕获的值:accounting、supabase、userId、requestId、sessionId、visibleQuestion、moderationContext、sessionContextWindow、usageStartedAt,以及对可变状态的访问器(getSelectedModel()、getClassification()、getLatencyState())。返回:reserveCredit(model)(原 488–503 的回调体)、cancel、appendQuestion、completeSmalltalk(reply)、checkpointContext、usagePayload、moderateAnswer、completeModeratedResponse、completeResponse、completeDirectRectificationTurn、settleResult、settle。retryDetachedSettlement、first、consultationReservationSchema/consultationCompletionSchema/consultationQuestionAppendSchema一起搬。 outputModerated/outputBlocked/settlement变成工厂内部状态(每个请求一个实例),对外行为与今天一致(completeResponse里「未审核则先审、命中则走 moderated」顺序不变)。route.ts中所有调用点改成settlement.xxx(...),参数与顺序不变。cancel返回值、AgentSettlementResult语义不变。- 新增行为测试
frontend/tests/consult-settlement-20261013.test.ts(假accounting.rpc记录调用序列):completeResponse:正常 → 调complete_consultation_response一次、成功后触发上下文摘要;RPC 失败 → 抛错且已调cancel_consultation_credit;输出审核block→ 只调complete_consultation_moderated,不调complete_consultation_response;completeDirectRectificationTurn→complete_consultation_free,responseKind: "smalltalk";appendQuestion:session_full→ 返回给 route 的 409 不变、已调 cancel;cancel对request_completed视为成功;retryDetachedSettlement最多 3 次、退避 150/300 ms;usagePayload合并分类用量、费用按百万 token 单价计算(与今天逐字段相同);settleResult重复调用只执行一次。
- 受影响的源码断言(起草时 grep 得到,开工时按 AGENTS.md §7.8 重新 grep 为准),逐条迁到
settlement.ts源码或改为上面的行为断言,三栏记录:
| 文件 | 断言(约行号) | 迁移方式 |
|---|---|---|
consultation-stream-recovery.test.ts |
16(rpc("reserve_consultation_usage"…p_session_id)、96(await settleResult(action))、103–108(completeResponse 内 try 包 complete_consultation_response、失败后 await cancel())、123、146–147(cancel / completeResponse 经 retryDetachedSettlement) |
文本改读 settlement.ts;103–108 的顺序断言换成行为测试「RPC 失败 → cancel 被调」 |
consultation-stream-recovery.test.ts |
22、44、81(onComplete: … settle(() => completeResponse(、onError 不结算) |
留在 route.ts,只改成 settlement.settle(() => settlement.completeResponse( 的写法 |
content-moderation.test.ts |
243(输入审核先于预留)、244(三处 moderateOutput: moderateAnswer)、245(if (!outputModerated) await moderateAnswer…)、246 |
243 沿用前置单的新断言;244 留 route(写法改为 settlement.moderateAnswer);245–246 读 settlement.ts + 行为测试 |
application-billing-contract.test.ts |
160(resolveSessionLanguageModel(chatSession.model_id, …))、175–178、216(usagePayload 与 p_actual_usage) |
160 读 turn-prep.ts;其余读 settlement.ts |
chat-session-authority.test.ts |
66(session_not_consultation)、83(append_consultation_question) |
66 读 turn-prep.ts;83 读 settlement.ts |
rectification-session-composer-guard.test.ts |
56 | 读 turn-prep.ts |
rectification-explicit-entry-20261012.test.ts |
663–669(completeDirectRectificationTurn 只走 free) |
读 settlement.ts + 行为测试 |
consultation-smalltalk.test.ts / consult-first-frame-and-pacing-20260928.test.tsx |
229 / 288(寒暄分支 complete_consultation_free、不调 streamAgentResponse) |
route 中寒暄分支调用 settlement.completeSmalltalk;complete_consultation_free 文本读 settlement.ts |
consult-first-frame-and-pacing-20260928.test.tsx |
274–275(reserve_ / append_ 在流内) |
改为 route.ts 中 settlement.reserveCredit / settlement.appendQuestion 的调用位于 runTurn 内 |
session-title-persist-contract.test.ts |
55–56(async function usagePayload( 作为顺序锚点) |
锚点改为 route 中寒暄早返之后的标题逻辑,三栏写明 |
consultation-context-cache-contract.test.ts |
14(历史窗口)、37(void checkpointConsultationContext()) |
14 读 turn-prep.ts;37 读 settlement.ts |
consult-evidence-card-telemetry-20260927.test.ts |
96(settleRun(() => completeResponse() |
留 route,改写法 |
account-deletion.test.tsx |
111–122 | 见 §5.7 |
model-catalog.test.ts |
49(await resolveSessionLanguageModel(chatSession.model_id…) |
读 turn-prep.ts |
其余 20 个读 route.ts 的测试文件(编排、流输出相关)预计不受影响;开工 grep 后若命中,同样三栏。
T3 · route.ts 只编排(Grok 自跑 / Claude 验收)
- 完成 T1、T2 后
POST的结构:loadConsultTurnHead→ 定义runTurn(prepare +settlement.reserveCredit→ 拒绝分支 →settlement.appendQuestion→ 分类 → 寒暄/编排)→streamFirstResponse。编排与流输出原样留着。 - 验收:
route.ts行数净减(目标减少 ≥ 450 行);grep -c "accounting.rpc(" route.ts为 0。
T4 · 行为不变的差分验证(Grok 自跑 / Claude 验收)
- 写
frontend/scripts/research/consult-route-split-diff.mts:在同一进程里分别加载基线版(git show origin/staging:frontend/src/app/api/consult/route.ts等文件写到临时目录)与新版POST,用mock.module注入同一套假依赖(假createServerSupabaseClient/createAdminSupabaseClient记录每次.from()与.rpc()的名称和参数、假模型返回固定流、假审核、假引擎返回frontend/tests/fixtures/consult-evidence-card-golden.json的真实引擎响应),跑以下场景并逐字节比较「HTTP 状态 + 响应体(NDJSON 全文)+ RPC/查询调用序列」: 401;冻结 423;请求体 400;会话不存在 404;校正会话 409;模型变更 409;模型不可用 503;提示词提取 400;输入审核拦截;点数不足 402;session_full409;寒暄;普通咨询完成;输出审核命中;生成中失败(取消);直达校正。 - 若进程内同时加载两版因模块别名(
@/)冲突做不到,退一步:先在基线检出上跑一遍把结果写成 JSON 快照(fixtureProvenance写命令与基线 SHA),再在新分支上跑同一脚本比对。 - 验收:16 个场景全部一致,脚本输出贴进 PROGRESS(只贴「一致 / 不一致」与场景名,不贴响应全文)。
T5 · 记录(Grok)
docs/tasks/PROGRESS-consult-route-split-20261013.md:搬移清单(原位置符号 → 新位置符号)、三栏改断言总表、diff 行数、T4 结果、第二阶段建议。CHANGELOG.md:不写(用户不可感知)。frontend/DESIGN.md:不改。docs/BUG_HISTORY.md:不写(无缺陷)。
7. 让步顺序
时间或 diff 预算不够时:先只做 T2 的「预留 / 取消 / 追加 / 寒暄结算」与 usagePayload,completeResponse / completeDirectRectificationTurn / moderateAnswer 留到第二阶段 → 再砍 T1(开流前段落已由前置单部分抽出)。T4 差分与 T5 不可砍。
8. 验收命令分工
| 谁 | 命令 |
|---|---|
| Grok 自跑 | cd frontend && ./node_modules/.bin/tsc --noEmit && npm run lint;./node_modules/.bin/tsx --test tests/consult-settlement-20261013.test.ts tests/consult-turn-prep-20261013.test.ts + §6 T2 表中每个文件(# fail、# cancelled 都为 0,退出码 0);./node_modules/.bin/tsx scripts/research/consult-route-split-diff.mts。前端全量、整站构建不跑 |
| Claude 验收 | 上述全部 + 前端全量 npm test 与基线逐名比对(测试总数不降)+ next build(/ 仍 Static,首屏 gzip ±2%)+ 复跑差分脚本 + 抽查三栏表每一条新断言不弱于原断言 |
| 不需要 | test:db(不改 SQL、不改 RPC 参数)、Python 快速门 |
9. 开工前置命令
git fetch origin --prune
git log origin/staging --oneline | grep -i "consult-prep-parallel\|BUG-1337" | head -3 # 前置一
git log origin/staging --oneline | grep -i "answer-effort" | head -3 # 前置二
# 任一为空则停,回报「前置未合入」
git worktree add -b codex/consult-route-split-20261013 .worktrees/consult-route-split-20261013 origin/staging
cd .worktrees/consult-route-split-20261013
ls frontend/src/lib/consult/ # 应有 prep-flags.ts、turn-prep.ts
wc -l frontend/src/app/api/consult/route.ts # 记下开工基线行数
grep -rl "api/consult/route" frontend/tests | wc -l
grep -rn "reserve_consultation_usage\|cancel_consultation_credit\|append_consultation_question\|complete_consultation_\|usagePayload\|retryDetachedSettlement\|settleResult\|checkpointConsultationContext\|moderateAnswer\|completeResponse\|completeModeratedResponse\|completeDirectRectificationTurn\|session_not_consultation\|blocksPromptExtraction\|consultationHistoryWindow\|refuseWhenAccountDeletionPending\|resolveSessionLanguageModel" $(grep -rl "api/consult/route" frontend/tests)
10. BUG 编号起点
无 BUG 编号(重构,无缺陷)。若搬移中发现真实缺陷,不在本单修,写进 PROGRESS「发现」并回报,由 Claude 另立单。
11. 交付物清单
- 代码:
frontend/src/lib/consult/settlement.ts(新)、frontend/src/lib/consult/turn-prep.ts(扩充)、route.ts(净减) - 测试:
frontend/tests/consult-settlement-20261013.test.ts(新)、§6 T2 表中各文件的三栏迁移 - 脚本:
frontend/scripts/research/consult-route-split-diff.mts(新) - 文档:
docs/tasks/PROGRESS-consult-route-split-20261013.md - 不改:
CHANGELOG.md、frontend/DESIGN.md、docs/BUG_HISTORY.md、数据库迁移
后续阶段(本单不做)
- 第二阶段:运行编排
runAgenticConsultation(流选项、streamWithOverflowRetry、重试 / 续写 / 窗口预计算、logRun/settleRun)搬到lib/consult/run-agentic.ts。 - 第三阶段:legacy 运行时与流输出(
streamTextResponse包装、streamFirstResponse接线、响应头)搬到lib/consult/stream-output.ts;评估能否删除 legacy 运行时(需产品拍板,CONSULTATION_AGENTIC_RUNTIME=legacy仍是开关)。 - 第四阶段:把剩余约 400 处读
route.ts源码的断言逐步换成对服务函数的行为测试,并加一条route.ts行数 / 闭包数增长合同(参照home-shell-growth-contract.test.ts)。 - 标题副作用
titleSideEvent与上下文摘要的归属(会话服务)。