From 62be52dacf05aa75bc225fb60081cde824fcd475 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Wed, 16 Sep 2026 00:22:00 +0800 Subject: [PATCH] fix(chart): skip VedAstro on chart-page first paint The natal /api/chart request from the chart page now sends skip_vedastro_main_entry_overview, matching the ephemeris page. Mapper and contract never read that evidence. BUG-718 recurs from BUG-161. Tests lock both foreground natal paths. --- CHANGELOG.md | 4 ++ docs/BUG_HISTORY.md | 16 +++++++ ...OGRESS-chart-vedastro-decouple-20260915.md | 48 +++++++++++++++++++ docs/tasks/README.md | 2 +- docs/testing/chart-page-20260915.md | 1 + frontend/src/lib/chart-view-load.ts | 1 + frontend/tests/chart-view-route.test.ts | 20 +++++++- frontend/tests/ephemeris-route.test.ts | 10 ++++ 8 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 docs/tasks/PROGRESS-chart-vedastro-decouple-20260915.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b410a44d..82dc36e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 印度占星 Skill 更新日志 +## 2026-09-15 — 打开星盘不再等外部占星服务,本地直接出盘 + +登录后打开星盘页,主盘按本站计算直接出来,不再先等外部服务。外部服务异常或断网时,星盘页仍然能看盘。Skill 版本不变。 + ## 2026-09-15 — 星盘页先出外壳,主盘到了再填;分盘和大运按需才算 点侧栏「星盘」会先看到页面,不再对着白屏等五个计算一起回来。主盘先算;分盘、Chara 大运、西洋盘、七政四余点到那一栏才算,不消耗点数。服务忙时会说正忙、稍等再开;其它算不出来会记下,不再写「过一会儿再打开」。成功页眉标改为「主盘直接算 · 分盘按需 · 不消耗点数」。Skill 版本不变。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 05652a6f..a0982f3b 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -11157,3 +11157,19 @@ - 相关记录:BUG-707、BUG-715、BUG-716、TASK-chart-page-blocking-open-20260915 - 复发自:无 - 修复版本:待发布 + +## BUG-718 | 星盘页首屏 `/api/chart` 同步挂上 VedAstro 外部证据,页面却不读它 + +- 状态:resolved +- 首次发现:2026-09-15 +- 最近更新:2026-09-15 +- 影响面:`frontend/src/lib/chart-view-load.ts`、`GET /api/chart-view`、`/chart` +- 用户现象:打开星盘要等零点几秒到数秒;生产上缓存未命中时会同步等外部占星服务。盘面本身用的是本站 Swiss Ephemeris。 +- 触发条件:登录后打开 `/chart`,出生资料齐全,引擎缓存未命中。 +- 根因:`/api/chart` 默认会 `_attach_vedastro_main_entry_overview`(完整 snapshot + 三领域扫描)。跳过要显式传 `skip_vedastro_main_entry_overview`。BUG-161 只给 `/api/consult` 装了这个标志。星历页传了,2026-09-15 新建的星盘页没传。`chart-view-mapper` / `chart-view-contract` 不读这份证据。 +- 修复:`birthPayload()` 带上 `skip_vedastro_main_entry_overview: true`,与星历页同一写法。不改引擎默认行为,解读链仍取外部证据。 +- 验证:合同测试锁住星盘 BFF 与星历两处 natal/transit 请求都带该标志;删掉 `birthPayload` 那一行测试必须红。 +- 防复发:任何前台页面路由调 `/api/chart` 必须显式声明外部证据策略,并有合同测试覆盖;新增页面路由时按此检查。 +- 相关记录:BUG-161、BUG-065、BUG-715、BUG-716、BUG-717、ERR-107、ERR-108、TASK-chart-page-blocking-open-20260915 +- 复发自:BUG-161 +- 修复版本:待发布 diff --git a/docs/tasks/PROGRESS-chart-vedastro-decouple-20260915.md b/docs/tasks/PROGRESS-chart-vedastro-decouple-20260915.md new file mode 100644 index 00000000..f82f13b9 --- /dev/null +++ b/docs/tasks/PROGRESS-chart-vedastro-decouple-20260915.md @@ -0,0 +1,48 @@ +# PROGRESS · 星盘页与 VedAstro 外网调用脱钩(2026-09-15) + +工作树:`.worktrees/chart-vedastro-decouple-20260915` +分支:`codex/chart-vedastro-decouple-20260915` +基线:`origin/staging` @ `ce1939b0`(任务书;代码基线 `69ede436`) +本机 Windows。 + +未改 `scripts/**`、`page.tsx`、超时、限流、`HEAVY_COMPUTE_PATHS`。未回退 BUG-715/716/717。Skill 未 bump。 + +## 任务状态 + +| 任务 | 状态 | 说明 | +| --- | --- | --- | +| 1 首屏 skip VedAstro | 完成 | `birthPayload()` 增加 `skip_vedastro_main_entry_overview: true`。`followUp` 展开同一 payload,四个后续 layer 自动带上该字段 | +| 2 合同测试 | 完成 | 星盘 BFF 请求体 + 星历两处源码/桩 | +| 3 BUG-718 | 完成 | 复发自 BUG-161 | +| 4 文档 | 完成 | CHANGELOG、真人走查第 16 条 | + +## 反向验证 + +把 `birthPayload()` 里 `skip_vedastro_main_entry_overview: true` 删掉后,`foreground chart-view natal requests skip VedAstro overview` 失败(`undefined !== true`);恢复后通过。 + +## 耗时对比 + +本机未起引擎(无 Docker / 未跑 `jyotish_api_server.py`)。任务书基线:不带 skip 0.40–0.66 s(其中 VedAstro 空转 0.36 s),带 skip 的星历路径 5 ms / 58 KB,不带 skip 163 KB。本单与星历页同一标志,mapper 不读 `modules`,五个 Tab 合同仍走原 golden。真人走查见 `docs/testing/chart-page-20260915.md` 第 16 条。 + +## 既有断言改动 + +| 文件 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| `chart-view-route.test.ts` 的 `engine()` | 只记 `path` | 同时记 `path` + `body` | 要锁住请求体里的 skip 标志;既有路径断言未改 | + +未弱化其它既有断言。 + +## 测试 + +| 命令 | 结果 | +| --- | --- | +| `npx tsx --test tests/chart-view-route.test.ts tests/ephemeris-route.test.ts tests/chart-page-view.test.tsx` | **29 pass / 0 fail**(上一单同三文件 27;本单 +2:星盘 skip、星历两处 skip) | +| `node node_modules/typescript/bin/tsc --noEmit` | **0 错** | +| `npm run lint` | **0 error** / 119 条既有 warning;本单未新增 | +| `npx next build` | 本机未重跑。上一单 compile + tsc 过后死在既有 `SkillPackageRegistryError`(EPERM symlink `/api/birth-time-guide`)。源码未改 `page.tsx` / 路由类型。 | + +## 环境缺口 + +- 无登录态、无 Chrome:真人走查见 `docs/testing/chart-page-20260915.md` 第 16 条,不得写成通过。 +- 无本机引擎:耗时/字节对比沿用任务书实测,未在本轮复测。 +- `next build` 收集页面数据时 symlink EPERM,无法在本机核对路由表。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 9d28e5b7..f2bfefc6 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -232,7 +232,7 @@ | `TASK-staging-dispatch-autofill-sha-20260915.md` | `PROGRESS-staging-dispatch-autofill-sha-20260915.md` | `Migrate Staging Database` 每次都要手抄 40 位 SHA,而那个值恰恰是「最新一个过门禁的 staging 提交」——机器能自己算,查询代码那一步里就有。改成留空自动解析、填了仍走原路径(回滚用),三条安全属性一条不丢。**产品 2026-09-15 明确授权修改该 workflow,执行方不得以 AGENTS.md §2.7 拒改**;生产两个按钮保持手填,那是护栏不是麻烦 | 待验收 | `codex/staging-dispatch-autofill-sha-20260915` | | `TASK-staging-auto-migrate-on-deploy-20260915.md` | `PROGRESS-staging-auto-migrate-on-deploy-20260915.md` | 门禁通过后自动先跑 staging 迁移再部署,不再手点(迁移幂等、无挂起时是 no-op,`db-migrate.mjs --check` 挂起返 3 可用于日志)。今天 `deploy-staging.yml` 完全不提迁移,忘点就让新代码跑在旧 schema 上且无人拦。**产品再次授权改 workflow,范围限 `backend-quality-gate.yml` 的 dispatch 段**;迁移失败必须阻断部署;回滚不自动迁移;生产完全不动。⚠️ 同轮必须把「迁移须对已部署代码向后兼容、破坏性变更拆两轮」写进 AGENTS.md §7.6 | 待验收 | `codex/staging-auto-migrate-on-deploy-20260915` | | `TASK-api-server-decomposition-20260916.md` | `PROGRESS-api-server-decomposition-20260916.md` | **重构单(串行在 qizheng 单之后)**:把业务逻辑搬出 `JyotishAPIHandler`。核心不是行数,是全仓 3 处靠 `JyotishAPIHandler.__new__` 伪造空壳 handler 借方法(`consultation_workflow_service` ×2、`capture_report_blocked_repairs_golden`、`local_accuracy_report`,MCP 也走这条),依赖方向反了、handler 没有 `headers`/`wfile` 随时可炸。四阶段:拆 `__new__` 后门 → 抽 ≥150 行业务方法 → `do_POST`/`do_GET` 改路由表 → 重新冻结行数 baseline(余量 300→50)。纯搬运不改行为,`test_api_server_security.py` 3841 行断言一条不许改。预计 11,314 → 约 9,230 行。BUG 段 710+ | 待领取 | — | -| `TASK-chart-vedastro-decouple-20260915.md` | `PROGRESS-chart-vedastro-decouple-20260915.md` | **P0**:星盘页首屏那一发 `/api/chart` 没传 `skip_vedastro_main_entry_overview`,实测冷算 0.40–0.66 秒里约 0.36 秒是 VedAstro 空转(本机连 endpoint 都没配);生产 env 开着 network + fanout,等于首屏同步等 24 个外部请求 + 3 次领域扫描,而 `chart-view-mapper.ts` / `chart-view-contract.ts` 根本不读这份证据。星历页同端点传了标志,两页策略相反。BUG-718,**复发自 BUG-161**(前台请求不得同步串联可选外部证据)。串行在 chart-page-blocking-open 之后 | 待领取 | — | +| `TASK-chart-vedastro-decouple-20260915.md` | `PROGRESS-chart-vedastro-decouple-20260915.md` | **P0**:星盘页首屏那一发 `/api/chart` 没传 `skip_vedastro_main_entry_overview`,实测冷算 0.40–0.66 秒里约 0.36 秒是 VedAstro 空转(本机连 endpoint 都没配);生产 env 开着 network + fanout,等于首屏同步等 24 个外部请求 + 3 次领域扫描,而 `chart-view-mapper.ts` / `chart-view-contract.ts` 根本不读这份证据。星历页同端点传了标志,两页策略相反。BUG-718,**复发自 BUG-161**(前台请求不得同步串联可选外部证据)。串行在 chart-page-blocking-open 之后 | 待验收 | `codex/chart-vedastro-decouple-20260915` | | `TASK-vedastro-runtime-ops-20260915.md` | `PROGRESS-vedastro-runtime-ops-20260915.md` | 运行期真相单(与上单并行,文件不重叠;**不得改 `jyotish_api_server.py`**):官方 `vedastro==1.23.25` 其实是 REST 客户端(46 KB,全打 `api.vedastro.org`),且 import 时请求 pypi 并 `pip install --upgrade` 自升级——本机实测 pin 装完一 import 就变 1.23.26,`requirements.txt` 的锁在运行期是假的(BUG-719);无 key 时免费层排队是同步 sleep + 全局锁,24 个请求 ≈ 4.8 分钟堵住前台线程(BUG-720,定级依赖生产 key 是否配置)。生产 env 核对清单在 `docs/testing/vedastro-runtime-20260915.md`,**只能由产品负责人执行**。台账 ERR-107 / ERR-108 | 待领取 | — | ## 命名与归档 diff --git a/docs/testing/chart-page-20260915.md b/docs/testing/chart-page-20260915.md index 9448e16a..35e70ab4 100644 --- a/docs/testing/chart-page-20260915.md +++ b/docs/testing/chart-page-20260915.md @@ -28,3 +28,4 @@ ## 资料不全 15. 没有出生日期/时间/地点时,页面用人话提示去填星盘资料,不是 500。 +16. 登录后打开 `/chart`,主盘应几乎即时出现。断网或外部占星服务异常时,星盘页仍应正常出盘。 diff --git a/frontend/src/lib/chart-view-load.ts b/frontend/src/lib/chart-view-load.ts index 6f4c1bf7..c6af321e 100644 --- a/frontend/src/lib/chart-view-load.ts +++ b/frontend/src/lib/chart-view-load.ts @@ -56,6 +56,7 @@ function birthPayload(profile: ChartViewProfileInput): Record { ayanamsa: resolveAyanamsa({ ayanamsa: profile.ayanamsa }), node_mode: "mean", house_system: "P", + skip_vedastro_main_entry_overview: true, }; } diff --git a/frontend/tests/chart-view-route.test.ts b/frontend/tests/chart-view-route.test.ts index b42a5a9e..3533885e 100644 --- a/frontend/tests/chart-view-route.test.ts +++ b/frontend/tests/chart-view-route.test.ts @@ -52,8 +52,10 @@ function asResult(path: string, value: EngineCallResult | Record | null> = {}) { const calls: string[] = []; - const postEngine: ChartViewEnginePost = async (path) => { + const bodies: Array<{ path: string; body: Record }> = []; + const postEngine: ChartViewEnginePost = async (path, body) => { calls.push(path); + bodies.push({ path, body }); if (path in overrides) return asResult(path, overrides[path]!); if (path === "/api/chart") return asResult(path, golden.chart); if (path === "/api/varga_full") return asResult(path, golden.varga_full); @@ -62,7 +64,7 @@ function engine(overrides: Record(run: () => Promise): Promise<{ result: T; warnings: unknown[] }> { @@ -128,6 +130,20 @@ test("an incomplete birth profile returns a structured message, not 500", async assert.equal(calls.length, 0); }); +test("foreground chart-view natal requests skip VedAstro overview", async () => { + const { bodies, postEngine } = engine(); + await assembleChartView({ + userId: "user-1", + profile, + postEngine, + asOf: "2026-09-15", + }); + const natal = bodies.filter((item) => item.path === "/api/chart"); + assert.equal(natal.length, 1); + assert.equal(natal[0]?.body.skip_vedastro_main_entry_overview, true); + assert.match(loadSource, /skip_vedastro_main_entry_overview:\s*true/); +}); + test("opening the natal chart does not call follow-up engine paths", async () => { const { calls, postEngine } = engine(); const result = await assembleChartView({ diff --git a/frontend/tests/ephemeris-route.test.ts b/frontend/tests/ephemeris-route.test.ts index 834520c4..467e9417 100644 --- a/frontend/tests/ephemeris-route.test.ts +++ b/frontend/tests/ephemeris-route.test.ts @@ -147,6 +147,16 @@ test("the ephemeris BFF does not import billing or mastra", () => { assert.doesNotMatch(viewSource, /consultation-billing|@\/mastra/); }); +test("ephemeris natal and transit chart requests skip VedAstro overview", () => { + assert.equal((routeSource.match(/skip_vedastro_main_entry_overview:\s*true/g) ?? []).length, 2); + const result = executeRoute({ user: true, row: completeRow }); + const chartCalls = result.upstream.filter((item) => item.url.endsWith("/api/chart")); + assert.ok(chartCalls.length >= 2); + for (const call of chartCalls) { + assert.equal(call.body.skip_vedastro_main_entry_overview, true); + } +}); + test("unauthenticated ephemeris requests are 401", () => { const result = executeRoute({ user: false, row: null }); assert.equal(result.status, 401);