产品 2026-09-15 三项拍板,落成两份新单与两处既有单的修订: - 新增 TASK-freeze-metric-change-20260915(无 BUG 号,后面两单的前置): 两条冻结余量已用完(page.tsx 1951/1951 余 0;api server 11334/11363 余 29), 冻结从「逼新代码往外走」退化成拦路。实证:page.tsx 行数砍 59% 但 Home() 的 useState 从 56 涨到 66;api server 225 个类方法只有 12 处真碰 HTTP。 主门换成耦合指标,行数降为粗护栏;同时推翻 §6「参数式 hook 内部保持 0 个 React hook」——那正是状态搬不走的原因。 - 新增 TASK-home-state-lowering-20260915(无 BUG 号):先搬 rectification* 那 15 个 state 进已经是 dynamic 子树的校正面,Home() useState 66 → ≤53。 零行为变化;串行在 freeze-metric-change + C2 + R3 之后。 - 修订 TASK-consultation-external-evidence-cache-20260915:依赖反转,C1 排在 API server 拆解之前(它动模块级函数,拆解动类方法);补「不得新增类方法、 行数余量仅 29」的硬红线。 - 修订 TASK-api-server-decomposition-20260916:串行依赖加 C1 与 freeze-metric-change;__new__ 计数按 grep 的 4 计(原文 3 是文件数); 阶段 4 收尾口径改写;基线 11,314 → 11,334。 纯文档推送,不触发门禁、不发布镜像、不部署。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
14 KiB
TASK · 普通聊天每一轮都在同步等外网,本地计算只占 3%
- 日期:2026-09-15
- 基线 commit:
origin/staging@6b3248bf - 执行分支:
codex/consultation-external-evidence-cache-20260915 - 主要落点:
scripts/jyotish_api_server.py、scripts/vedastro_service_adapter.py、scripts/vedastro_user_entrypoint.py - 依赖已于 2026-09-15 反转:本单排在 API server 拆解单之前。 原文写的是「串行在
TASK-api-server-decomposition-20260916之后」,产品拍板改为本单先做——理由见 §3.7,拆解单已同步改为串行在本单之后 - 与 context-memory 单、session-capacity 单无文件重叠,可并行
1. 事故实证
普通聊天每发一条消息,每个问题域调一次 POST /api/consultation_workflow。本机(Python 3.13 + swisseph)热身后实测:
| 问题域 | 第 1 次 | 第 2 次(同参数) | 响应体积 |
|---|---|---|---|
| career | 928 ms | 865 ms | 523 k 字符 |
| wealth | 493 ms | 490 ms | 523 k 字符 |
| marriage | 493 ms | 492 ms | 518 k 字符 |
| general | 491 ms | 493 ms | 542 k 字符 |
| timing | 523 ms | 507 ms | 550 k 字符 |
cProfile 按 tottime 排前五名:
| 耗时 | |
|---|---|
select.poll(等外网 socket) |
0.801 s |
_ssl._SSLSocket.read |
0.786 s |
_ssl._SSLSocket.do_handshake |
0.206 s |
swisseph.calc_ut(本地排盘) |
0.022 s |
copy.deepcopy |
0.048 s |
前三名全部来自 _join_foreground_vedastro → _run_foreground_vedastro_gateway → vedastro_service_adapter 的 urlopen,目标是 api.vedastro.org,而且每次重新 TLS 握手。本地占星计算连 3% 都不到。
三个护栏数字凑不到一起,符号定位:
| 位置 | 值 | 含义 |
|---|---|---|
jyotish_api_server.py _foreground_vedastro_join_seconds |
1.5 s(上限 3 s) | 前台最多等多久 |
jyotish_api_server.py _foreground_vedastro_budget_seconds |
8 s(2–12 s) | 后台任务自己跑多久 |
jyotish_api_server.py _FOREGROUND_VEDASTRO_WORKERS |
2 | 整个进程的前台线程池 |
_join_foreground_vedastro 超时走 FuturesTimeoutError 分支返回 official_blocked,但不 cancel() 那个 future,任务继续跑满 8 秒预算。于是:只要平均每 4 秒有一轮聊天,2 个 worker 就永远被占着,之后每一轮都白等 1.5 秒再拿到 official_blocked——用户付出了等待,拿不到证据,技法表上那一层还是 blocked。
另有一条纯浪费:响应体 52 万字符里 western_evidence_packet 占 122 KB,全仓检索前端零读取点(grep -rn "western_evidence_packet" frontend/src 无命中)。算出来、序列化、走 HTTP、在 Node 里 JSON.parse 一遍,然后丢掉。三个域就是 366 KB 的无效解析,全压在 2 vCPU 上。其余键体积:chart 238 KB、consumer_context 43 KB、rectification 23 KB、vedastro_gateway 21 KB。
2. 根因
这份外网证据本来就是按「盘 + 日期」组织的,只是没有按这个键缓存过:
vedastro_service_adapter._official_snapshot_reference_date()在 case 里找不到reference_date/today/transit_date/current_date时,缺省取 UTC 当天。consultationInputSchema(frontend/src/mastra/consultation-workflow.ts)没有任何日期字段,所以普通聊天永远走缺省。_official_dasha_range_body()把大运区间取成该日期所在自然年的 1 月 1 日到 12 月 31 日。
也就是说:本命部分永不变,随时间变的部分粒度是天/年。同一个人一天里问十轮,十次拿到的是同一份东西,却打了十次外网。
3. 决策记录
产品 2026-09-15 授权本单,并明确以下口径:
- 不许「干脆不调 VedAstro」。 那正是 BUG-301 修掉的问题(前台跳过后技法表里 VedAstro 云状态永远 blocked)。本单的目标是留住证据、去掉等待,不是二选一。
- 缓存键沿用引擎内部已有的那一个:出生数据 + 岁差 + 交点 + UTC 日期。产品明确否决了「自己定一个 N 小时 TTL」——那会和
_official_snapshot_reference_date的日期键错位,跨 UTC 零点时以为还新鲜、内容其实已经该换。 - 新鲜度分档(产品定):
- 同一天命中 → 直接用,0 等待。
- 跨天了 → 先用旧的(上限 7 天)立刻返回,同时后台刷新,下一轮就是新的。
entrypoint = daily_starlanguage(「深入看今日」)→ 要求当天,没有当天的不吃旧的,走下一档。- 完全没有缓存 → 才走现在那套 1.5 秒有界等待。
- 后台任务必须可取消、线程池必须可排队。 现在「超时不 cancel + 只有 2 个 worker」的组合是本单必须解决的部分,不是顺带。
western_evidence_packet按需返回,不删计算。 西洋盘本身仍是 must-use 层之一(被读的是别的字段),本单只是不再把这个整包塞进每一轮的响应。- 不动并发闸门(默认 2)、不动
AGENT_TIMEOUT_MS、不动maxDuration。 - 本单排在 API server 拆解之前(2026-09-15 产品拍板,推翻本任务书首版的排序)。依据:本单要改的
execute_consultation_workflow、_join_foreground_vedastro、那三个常量全是模块级函数,而拆解单的核心是把业务逻辑搬出JyotishAPIHandler类,两边动的是文件的不同部分;且本单的用户价值(每一轮省掉一次外网等待)远高于一次纯搬运。代价是拆解单将来要吸收本单的 diff,已在那一单里写明。
4. 硬红线
- 缓存键必须与
_api_chart_cache的键分开(BUG-161 防复发原文:「快速排盘与完整证据排盘必须使用不同缓存键」)。不得复用同一个目录或同一个 key 函数。 - 赶不上外网时,不得把未交付的官方层标成
executed(BUG-301 防复发)。用旧缓存时,证据里必须能看出它是哪一天的。 - 缓存内容里不得落盘姓名、邮箱、用户 ID;出生资料派生值只能以哈希进键,不得明文写进缓存文件名(AGENTS §8)。
- 前台不得再同步串联 overview + snapshot + range scan(BUG-301 防复发,现状已满足,不得回退)。
scripts/jyotish_api_server.py不得增长(AGENTS §6):新逻辑进scripts/下的独立模块,主文件只做薄注册。具体到数字:开工时实测 11,334 行、上限 11,363,余量只有 29 行;JyotishAPIHandler有 225 个方法,本单不得新增任何类方法。 若TASK-freeze-metric-change-20260915已经合入,主门改为「类方法数不得增长」,行数粗护栏放宽——以开工当时生效的合同测试为准,但「不新增类方法、新逻辑进独立模块」这条无论口径怎么换都成立。- 不得顺手升级依赖、不得顺手修不在本单里的 warning。
5. 任务分解
5.1 外网证据按「盘 + UTC 日期」缓存
新增一个独立模块(例如 scripts/vedastro_snapshot_cache.py),提供读/写/新鲜度判定:
-
键:
sha256(出生年月日时分秒 + lat + lon + tz + ayanamsa_policy + node_policy + reference_date)。 -
落盘位置与
_api_chart_cache_dir()不同目录,TTL 由reference_date与「最多 7 天」共同决定,不再引入第二个 TTL 环境变量。 -
execute_consultation_workflow里:命中当天 → 直接用,完全不 submit 后台任务;命中 1–7 天前 → 直接用旧的并 submit 一次后台刷新(刷新结果只写缓存,不参与本轮);entrypoint == "daily_starlanguage"时只接受当天;全未命中 → 现有 1.5 秒有界路径,拿到结果后写缓存。 -
验收:新增 pytest——同一份 body 连调两次,第二次零外网调用(用 stub/monkeypatch 计数
urlopen或适配器入口),且两次返回的vedastro_gateway逐字相等。 -
验收:把
reference_date推到第二天,断言缓存不再命中当天档、走「先用旧的 + 后台刷新」,且本轮不等待。 -
验收:
daily_starlanguage入口在只有昨天缓存时不得使用它。 -
验收:缓存目录与
_api_chart_cache_dir()不同,且有一条断言钉死这一点。
5.2 后台任务可取消 + 线程池可排队
_join_foreground_vedastro 超时时必须 future.cancel();已经在跑的任务要能被 temporary_timeout_seconds 之外的显式取消标记打断,或者把预算从 8 秒收到不超过 join 的两倍并说明理由。线程池大小与 join/budget 三个数必须在同一处成组声明,并写明它们的关系(照 consultation-tools.ts 里那段预算注释的写法)。
- 验收:新增测试——连续发起超过
_FOREGROUND_VEDASTRO_WORKERS个前台请求,断言第 N+1 个的等待时间仍不超过 join 上限,且不会因为前面的任务没结束而排队变长。 - 验收:源码合同断言三个数在同一个声明块里,且
budget ≤ k × join(k 取实现选定的常量)。
5.3 western_evidence_packet 按需返回
默认不放进 /api/consultation_workflow 的响应;需要它的调用方(个人报告、高严谨工作流,若确有读取点)显式请求。开工第一步先把真实读取点查清楚,查到的写进进度记录,没查到的按「无人读」处理。
- 验收:
grep -rn "western_evidence_packet" frontend/src scripts/ tests/的结果写进进度记录,逐个说明保留或去掉。 - 验收:改后响应体积实测下降幅度写进进度记录(改前 523 k 字符是本单的基线数)。
- 验收:
consultationWorkflowResponseSchema仍能解析(它是.passthrough(),去掉一个键不应报错——但必须有测试证明,不能靠推断)。
5.4 在 staging 量一次真实的单域耗时
frontend/src/mastra/consultation-tools.ts 的预算注释写明:域上限 3 是按「staging 实测三域共 62.9 秒」反推的,即一域约 21 秒。本机同样的调用只要 0.5 秒,差 40 倍。这单交付后必须在 staging 上重新量一次:
-
交付一份可照做的清单进
docs/testing/,内容是:在 staging 上对同一张盘连发两轮同域提问,记录两轮的consultationToolDurationMs(第一轮冷、第二轮应命中缓存)。 -
结论写进进度记录:如果单域耗时已显著低于 21 秒,明确写出「三域上限可以放宽到 N」的依据,但本单不改那个上限——放宽是产品决策,另开单。
-
验收:
docs/testing/下有这份清单;进度记录里有改前/改后的实测两组数字,或明确写成环境缺口(无 staging 访问权时)。
5.5 两条 Bug 历史
同一变更内写进 docs/BUG_HISTORY.md:
- BUG-727:普通聊天每轮每域同步等外网,本地计算只占 3%;超时不取消 + 2 个 worker 让线程池长期饱和。关联 BUG-161、BUG-301、BUG-718,并写清楚:BUG-301 是故意把 VedAstro 放回前台的(否则技法表永远 blocked),所以本单不是推翻它,而是用缓存同时满足 161 和 301。防复发写成:前台外部证据必须有「盘 + 日期」级缓存;任何有界等待都必须同时取消它等待的后台任务。
- BUG-728:
western_evidence_packet无人读却每轮每域传 122 KB。防复发写成:工作流响应新增大字段前必须有读取点;没有读取点的字段不得进入前台响应。
6. 让步顺序
- 5.1(缓存)必须做,它是本单的全部意义。
- 5.2(可取消 + 可排队)必须做——只加缓存不修线程池,冷启动和跨天刷新仍会把池子占死。
- 5.3 可以砍到下一轮,砍了在进度记录里写明。
- 5.4 不得砍,但允许写成环境缺口(没有 staging 访问权时,把清单交出来即可)。
- 5.5 不得砍。
7. 开工前置命令
git fetch origin --prune
# 本单不再等 api-server-decomposition;直接以 origin/staging 为基线
git log --oneline origin/staging | head -5
git worktree add -b codex/consultation-external-evidence-cache-20260915 \
.worktrees/consultation-external-evidence-cache-20260915 origin/staging
cd .worktrees/consultation-external-evidence-cache-20260915
git status -sb | head -1
python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45 # AGENTS §9:本单涉及外部 oracle
开工前必读:docs/research/pre_work_error_ledger.md(AGENTS §9),以及 docs/testing/vedastro-runtime-20260915.md(BUG-719/720 的运行期真相,与本单同一外部服务)。
验收命令:
.venv/bin/python -m pytest tests/test_api_server_security.py \
tests/test_consultation_consumer_context.py \
tests/test_vedastro_runtime_ops.py
.venv/bin/python scripts/run_quality_gate.py --profile quick
cd frontend && npx tsx --test tests/consultation-*.test.ts
8. BUG 编号起点
基线 6b3248bf 上最大号 BUG-720;2026-09-15 校正四单已预占 721–726。本单预占 BUG-727 / 728。开工时核对当时的实际最大号,冲突顺延并在进度记录写明。
9. 不在本单范围
JyotishAPIHandler的拆解本身(本单只做薄注册,拆解排在本单之后)- 三域上限 3 → N 的放宽(5.4 只负责量数据,放宽是产品决策)
- 并发闸门、
AGENT_TIMEOUT_MS、maxDuration reference_date缺省用 UTC 当天、而用户的「今天」是 UTC+8,早上八点前后会错开一天——记为观察项,本单不修- 对话记忆与上限(见另外两单)