Files
Jyotisha/docs/tasks/TASK-consultation-external-evidence-cache-20260915.md
T
Jesse_ChenandClaude Opus 5 e4788dfc00 docs(tasks): 换掉两条增长冻结口径 + page.tsx 状态下沉第一簇 + C1 提前
产品 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
2026-09-15 23:15:37 +00:00

174 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 授权本单,并明确以下口径:
1. **不许「干脆不调 VedAstro」。** 那正是 BUG-301 修掉的问题(前台跳过后技法表里 VedAstro 云状态永远 blocked)。本单的目标是**留住证据、去掉等待**,不是二选一。
2. **缓存键沿用引擎内部已有的那一个**:出生数据 + 岁差 + 交点 + **UTC 日期**。产品明确否决了「自己定一个 N 小时 TTL」——那会和 `_official_snapshot_reference_date` 的日期键错位,跨 UTC 零点时以为还新鲜、内容其实已经该换。
3. **新鲜度分档**(产品定):
- 同一天命中 → 直接用,**0 等待**。
- 跨天了 → **先用旧的**(上限 7 天)立刻返回,同时后台刷新,下一轮就是新的。
- `entrypoint = daily_starlanguage`(「深入看今日」)→ **要求当天**,没有当天的不吃旧的,走下一档。
- 完全没有缓存 → 才走现在那套 1.5 秒有界等待。
4. **后台任务必须可取消、线程池必须可排队。** 现在「超时不 cancel + 只有 2 个 worker」的组合是本单必须解决的部分,不是顺带。
5. **`western_evidence_packet` 按需返回,不删计算。** 西洋盘本身仍是 must-use 层之一(被读的是别的字段),本单只是不再把这个整包塞进每一轮的响应。
6. **不动并发闸门(默认 2)、不动 `AGENT_TIMEOUT_MS`、不动 `maxDuration`。**
7. **本单排在 API server 拆解之前**2026-09-15 产品拍板,推翻本任务书首版的排序)。依据:本单要改的 `execute_consultation_workflow``_join_foreground_vedastro`、那三个常量**全是模块级函数**,而拆解单的核心是把业务逻辑搬出 `JyotishAPIHandler` **类**,两边动的是文件的不同部分;且本单的用户价值(每一轮省掉一次外网等待)远高于一次纯搬运。代价是拆解单将来要吸收本单的 diff,已在那一单里写明。
## 4. 硬红线
1. 缓存键必须与 `_api_chart_cache` 的键**分开**(BUG-161 防复发原文:「快速排盘与完整证据排盘必须使用不同缓存键」)。不得复用同一个目录或同一个 key 函数。
2. 赶不上外网时,不得把未交付的官方层标成 `executed`(BUG-301 防复发)。用旧缓存时,证据里必须能看出它是哪一天的。
3. 缓存内容里不得落盘姓名、邮箱、用户 ID;出生资料派生值只能以哈希进键,不得明文写进缓存文件名(AGENTS §8)。
4. 前台不得再同步串联 overview + snapshot + range scanBUG-301 防复发,现状已满足,不得回退)。
5. `scripts/jyotish_api_server.py` 不得增长(AGENTS §6):新逻辑进 `scripts/` 下的独立模块,主文件只做薄注册。**具体到数字**:开工时实测 11,334 行、上限 11,363**余量只有 29 行**`JyotishAPIHandler` 有 225 个方法,**本单不得新增任何类方法**。
`TASK-freeze-metric-change-20260915` 已经合入,主门改为「类方法数不得增长」,行数粗护栏放宽——**以开工当时生效的合同测试为准**,但「不新增类方法、新逻辑进独立模块」这条无论口径怎么换都成立。
6. 不得顺手升级依赖、不得顺手修不在本单里的 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. 让步顺序
1. 5.1(缓存)必须做,它是本单的全部意义。
2. 5.2(可取消 + 可排队)必须做——只加缓存不修线程池,冷启动和跨天刷新仍会把池子占死。
3. 5.3 可以砍到下一轮,砍了在进度记录里写明。
4. 5.4 **不得砍**,但允许写成环境缺口(没有 staging 访问权时,把清单交出来即可)。
5. 5.5 不得砍。
## 7. 开工前置命令
```bash
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 的运行期真相,与本单同一外部服务)。
验收命令:
```bash
.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 校正四单已预占 **721726**。本单预占 **BUG-727 / 728**。开工时核对当时的实际最大号,冲突顺延并在进度记录写明。
## 9. 不在本单范围
- `JyotishAPIHandler` 的拆解本身(本单只做薄注册,拆解排在本单之后)
- 三域上限 3 → N 的放宽(5.4 只负责量数据,放宽是产品决策)
- 并发闸门、`AGENT_TIMEOUT_MS``maxDuration`
- `reference_date` 缺省用 UTC 当天、而用户的「今天」是 UTC+8,早上八点前后会错开一天——**记为观察项,本单不修**
- 对话记忆与上限(见另外两单)