# TASK · 星盘页首屏与 VedAstro 外网调用脱钩(2026-09-15) ## 基线 - 基线 commit:`origin/staging` = `69ede436`(`fix(ci): migrate staging automatically before deploy`) - 分支:`codex/chart-vedastro-decouple-20260915` - 串行依赖:本单改 `frontend/src/lib/chart-view-load.ts`,与 `TASK-chart-page-blocking-open-20260915`(BUG-715/716/717,代码已在 staging)同文件。该单的代码已合入基线,本单在其之上改;**不得回退它的 layer / 缓存 / 日志分档三项修复**。 - BUG 编号起点:**BUG-718**(开工时复核 `docs/BUG_HISTORY.md` 当前最大号 717,`BUG-905` 是他单误称,本仓无该条目) ## 事故实证 以下数字是 2026-09-15 在本机对基线代码实测得到的(引擎跑在 `.venv`,swisseph 可用,`JYOTISH_API_CHART_CACHE_TTL_SECONDS=0` 关缓存冷算)。 ### 一、星盘页首屏那一发 `/api/chart` 有 99% 的时间花在外网 | 调用 | 页面 | 冷算耗时 | 响应体积 | | --- | --- | --- | --- | | `/api/chart`(星盘页原样,不带 skip) | `/chart` | **0.40–0.66 s** | 163 KB | | `/api/chart`(带 `skip_vedastro_main_entry_overview`) | `/ephemeris` | **5 ms** | 58 KB | | `/api/varga_full`(17 张分盘) | `/chart` | 5 ms | 22 KB | | `/api/dasha/chara` | `/chart` | 3 ms | 177 KB | | `/api/western` | `/chart` | 2 ms | 8 KB | | `/api/qizheng` | `/chart` | 160 ms | 15 KB | | 任意一条缓存命中 | — | 5 ms | — | 同一台机器上单独计时 `vedastro_evidence_orchestrator.orchestrate_vedastro_evidence(route='overview')`:**0.36 s / 次,连调两次都是 0.36 s(无进程内记忆)**,返回 `status=service_endpoint_not_configured`——**这还是本机没有配 endpoint、一个外部请求都没发出去的情况下的空转成本**。 `TASK-chart-page-blocking-open-20260915` 把这 0.6 秒记成「引擎五个调用合计 0.75 秒」并据此设了 10 秒超时。那份实测没错,但 0.6 秒里几乎全部不是 Swiss Ephemeris,是 VedAstro 分支。 ### 二、生产配置下这一发会变成外网请求乘法 - `frontend/src/lib/chart-view-load.ts` 的 `birthPayload()`(基线第 40–60 行)构造 `/api/chart` 请求体,**没有 `skip_vedastro_main_entry_overview`**;同文件 `assembleChartView()` 用它发第一发引擎调用。 - `frontend/src/app/api/ephemeris/route.ts` 两处(基线第 93、185 行)都显式传了 `skip_vedastro_main_entry_overview: true`。**同一个引擎端点,两个页面策略相反。** - `scripts/jyotish_api_server.py` `_compute_chart_sync()` 里 `if not body.get('skip_vedastro_main_entry_overview'):` → `_attach_vedastro_main_entry_overview(...)`(基线第 7090–7091 行)。 - 该 attach 走 `vedastro_evidence_orchestrator.orchestrate_vedastro_evidence(route='overview')`:先 `run_official_full_snapshot_for_case`,再对 `career / marriage / wealth` 三个领域各跑一次 `run_range_scan_for_case`。 - full snapshot 的请求清单(实跑 `_official_full_snapshot_manifest` 数出来的):`events_overview` 1 + `dasha_all` 1 + `chart_core` 10 + `house_core` 12 = **24 个外部 HTTP 请求**,fanout 开关 `VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED` 默认值就是 `"1"`(`scripts/vedastro_service_adapter.py:2520`)。 - `deploy/README.md` 的生产 env 段写着 `VEDASTRO_API_ENDPOINT=https://api.vedastro.org/api`、`VEDASTRO_ENABLE_NETWORK=1`、`VEDASTRO_RANGE_SCAN_NETWORK_ENABLED=1`、`VEDASTRO_TIMEOUT_SECONDS=20`,**没有关 fanout**。 也就是说:生产上每一次缓存未命中的星盘页打开,首屏那一发要等 24 个外网请求加 3 次领域扫描。前端这条链路只等 10 秒(`chart-view-engine.ts` 的 `CHART_VIEW_ENGINE_TIMEOUT_MS`),VedAstro 侧超时 20 秒。 ### 三、这份外部证据星盘页根本不用 - `frontend/src/lib/chart-view-mapper.ts` 全文不出现 `vedastro`、也不读 `modules`。 - `frontend/src/lib/chart-view-contract.ts` 全文不出现 `vedastro`。 `/api/chart` 响应里那份 `modules.vedastro_range_scan_result` 从 Python 传到 Next,再被 mapper 整段丢弃。它不展示、不进合同、不给任何下游消费。 ### 四、`docs/engine/vedastro-gateway.md` 自己不推荐这种用法 > 完整模式会增加外部请求数量和首包等待时间,更适合后台任务、预计算或非实时专业解盘,**不建议直接放在聊天首轮的同步关键路径**。 ## 根因 `/api/chart` 的默认行为是「顺带取一份 VedAstro 外部证据」,跳过它要显式传标志。这个标志是 BUG-161 修复时引入的(`defer_optional_external_evidence`,`scripts/jyotish_api_server.py:2183/2292/2334`),当时只给 `/api/consult` 这一条前台路径装上了。 **BUG-161 的防复发原文:「用户前台请求不得同步串联多个可选外部证据调用;快速排盘与完整证据排盘必须使用不同缓存键。」** 2026-09-15 新建的 `/chart` 是一条新的前台路径。星历页作者按老规矩传了标志,星盘页作者没传。防复发条款只写在 Bug 历史里,代码里没有任何东西拦住新页面漏传——这是 BUG-161 的复发面,不是新问题。 顺带说明:缓存键那一半是好的。`_build_api_chart_cache_payload` 已经把 `skip_vedastro_main_entry_overview` 放进 key(基线第 2634 行附近),快慢两种排盘天然不共享缓存,本单不需要动它。 ## 决策记录 - 2026-09-15,产品负责人在会话中看过上述实证后授权本单,口径是:**星盘页首屏不挂 VedAstro**。 - **不做「按需加载的 VedAstro 层」**。理由是上面第三条:当前没有任何消费方。按产品既有偏好(多余入口宁可删除也不修),先把它从这条路径上摘干净;将来星盘页真要展示外部交叉验证,另立任务书连同 UI 一起设计。 - 本单**不推翻**任何既有红线。它是把 BUG-161 的防复发从「聊天入口」扩大到「所有前台页面路径」,并第一次用测试把它钉住。 - 本单**不动** `HEAVY_COMPUTE_PATHS`、不动 `JYOTISH_HEAVY_COMPUTE_CONCURRENCY`、不动前端 10 秒超时、不放宽任何限流——BUG-717 的防复发继续有效。 - 生产 env 与镜像层面的两件事(API key 是否配置、SDK 自动联网升级)**不在本单**,见 `TASK-vedastro-runtime-ops-20260915.md`。两单可并行,文件不重叠。 ## 硬红线 1. 只改前台请求体与测试。**不得修改** `scripts/jyotish_api_server.py`、`scripts/vedastro_service_adapter.py`、`scripts/vedastro_evidence_orchestrator.py`。 2. 不得把 `_attach_vedastro_main_entry_overview` 的默认行为改成「默认不取」——`/api/consult`、`/api/professional_reading`、高严谨工作流、报告链仍然依赖它,改默认值会静默削掉解读链的外部证据(AGENTS Part B / MEVG 是硬约束)。本单只在调用方显式传标志。 3. 不得回退 BUG-715/716/717 的三项修复(layer 按需、5 分钟引擎结果缓存、失败原因分档日志与文案)。 4. 不得给星盘页加 spinner / 骨架 / 「正在加载」(AGENTS §6)。 5. 测试总数不得低于开工时 `origin/staging` 实测;改任何既有断言要写「原值 / 新值 / 原因」三栏。 6. 不得顺手升级依赖、不得顺手修不在本单里的 warning。 ## 任务分解 ### 1. 星盘页首屏请求显式跳过 VedAstro(P0) 在 `frontend/src/lib/chart-view-load.ts` 的 `birthPayload()` 返回体里加 `skip_vedastro_main_entry_overview: true`,与 `frontend/src/app/api/ephemeris/route.ts` 的既有写法一致。 注意 `assembleChartView()` 里 `followUp` 是 `{...payload, planets, ascendant, houses}`,所以四个后续 layer 调用会自动带上这个字段——`/api/varga_full`、`/api/dasha/chara` 不读它,`/api/western`、`/api/qizheng` 也不读,无副作用。确认一遍即可,不要为此拆两个 payload。 **验收标准** - `assembleChartView` 发往 `/api/chart` 的 body 里 `skip_vedastro_main_entry_overview === true`。 - 进度记录里给出改前 / 改后同一份出生资料的实测对比:冷算耗时、响应字节数。改后主盘应在 10 ms 量级(本机基线 5 ms)。 - 星盘页五个 Tab 的渲染结果与改前逐项一致(mapper 不读 `modules`,预期零差异)。任一 Tab 出现差异即停手上报,不要自行调 mapper。 ### 2. 用合同测试钉住「前台页面必须显式声明外部证据策略」(P0) 新增合同测试(建议放 `frontend/tests/chart-view-route.test.ts` 或同目录新文件),至少锁住两条: - 无 layers 时,`assembleChartView` 对 `/api/chart` 的请求体带 `skip_vedastro_main_entry_overview: true`; - `/api/ephemeris` 路由的两处 natal / transit 请求体同样带该字段(现状已满足,本轮只是补上锁)。 第二条可以用源码合同的方式写(读文件断言),与既有 `chart/page.tsx` 禁止 `force-dynamic` 的源码合同同一路数;能用真实桩跑通就优先跑桩。 **验收标准** - 把 `birthPayload()` 里那一行删掉,测试必须红;恢复后绿。执行方要在进度记录里写明自己做过这个反向验证(防止写出恒真断言)。 - `npx tsx --test frontend/tests/chart-view-route.test.ts`(或新文件)通过。 ### 3. Bug 历史(P0) 新增 **BUG-718**,字段按 `docs/BUG_HISTORY.md` 既有格式写全,其中: - `复发自`:**BUG-161**(必填,不得写「无」)。说明旧防复发为什么没拦住:它以 `/api/consult` 的 `defer_optional_external_evidence` 形式落在一条路径上,没有任何代码级约束能作用于 2026-09-15 新建的 `/chart`。 - `相关记录`:BUG-161、BUG-065、BUG-715、BUG-716、BUG-717、ERR-107、ERR-108、TASK-chart-page-blocking-open-20260915。 - `验证`:贴任务 1 的耗时 / 字节数对比与任务 2 的反向验证结论。 - `防复发`:写成可执行的一句——任何前台页面路由调 `/api/chart` 必须显式声明外部证据策略,并有合同测试覆盖;新增页面路由时按此检查。 BUG-065(VedAstro 事件扫描请求乘法导致校正数分钟无响应,288 次同步请求)是同族根因的更早一次,在 `相关记录` 里带上即可,不必标成复发自它。 ### 4. 文档(P1) - `CHANGELOG.md`:一句话——打开星盘页不再等外部占星服务,本地直接出盘。用户可感知的是「快」,不要写内部端点名。 - 不改 `frontend/DESIGN.md`(本单不动视觉)。 - `docs/testing/chart-page-20260915.md` 追加一条真人走查项:登录后打开 `/chart`,主盘应几乎即时出现;断网或外部服务异常时,星盘页仍应正常出盘(因为它已经不依赖外网)。 ## 让步顺序 资源不够时按此顺序保:任务 1 → 任务 3 → 任务 2 → 任务 4。任务 1 单独交付也是净收益;任务 2 缺席则必须在进度记录里明确写「防复发未落测试」,不得沉默。 ## 开工前置命令 ```bash cd /workspace/Jyotisha git fetch origin --prune git worktree add -b codex/chart-vedastro-decouple-20260915 \ .worktrees/chart-vedastro-decouple-20260915 origin/staging cd .worktrees/chart-vedastro-decouple-20260915/frontend && npm ci ``` 开工前必读:`docs/BUG_HISTORY.md` 的 BUG-161、BUG-065、BUG-715/716/717;`docs/research/pre_work_error_ledger.md` 的 ERR-107、ERR-108。 本单只改前端请求体与测试,不碰引擎与适配器,`scripts/pre_work_check.py` 可不跑(AGENTS §9 纯前端例外)。 ## 验收口径 - `./node_modules/.bin/tsc --noEmit` 0 错;`npm run lint` 0 error。 - 前端相关测试套件 fail=0,总数不低于开工基线。 - `next build` 后 `/` 仍 `○ Static`;本单不碰首屏包,gzip 应无变化,有变化要解释。 - 部署后 `https://staging.jyotisha.chat/api/health` 的 `deployment.gitCommit` 等于本轮 staging 提交。 - 真人走查(无登录态 / 无 Chrome 的环境缺口)写进 `docs/testing/chart-page-20260915.md`,不得写成「通过」。