From ce1939b0749f6b9e67afb7419989854fc1357bcf Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Tue, 15 Sep 2026 15:47:32 +0000 Subject: [PATCH] =?UTF-8?q?docs(tasks):=20=E6=98=9F=E7=9B=98=E9=A1=B5?= =?UTF-8?q?=E4=B8=8E=20VedAstro=20=E5=A4=96=E7=BD=91=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E8=84=B1=E9=92=A9=20+=20=E8=BF=90=E8=A1=8C=E6=9C=9F=E7=9C=9F?= =?UTF-8?q?=E7=9B=B8=E4=B8=A4=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 星盘页首屏那一发 /api/chart 没传 skip_vedastro_main_entry_overview, 本机实测冷算 0.40–0.66 秒里约 0.36 秒是 VedAstro 空转(连 endpoint 都 没配的情况下);带标志的同一调用是 5 毫秒。生产 env 开着 network 与 fanout,等于首屏同步等 24 个外部请求加 3 次领域扫描,而前端 mapper 与 contract 根本不读这份证据。BUG-718,复发自 BUG-161。 运行期单记录两条新发现:官方 vedastro==1.23.25 其实是 REST 客户端, 且 import 时请求 pypi 并 pip install --upgrade 自升级(实测 pin 装完 一 import 即变 1.23.26);无 key 时免费层排队是同步 sleep 加进程级 全局锁,24 个请求约 4.8 分钟堵住前台线程。台账补 ERR-107 / ERR-108, 生产 env 核对清单交产品负责人执行。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu --- docs/research/pre_work_error_ledger.md | 16 ++ docs/tasks/README.md | 2 + .../TASK-chart-vedastro-decouple-20260915.md | 146 +++++++++++++++ .../TASK-vedastro-runtime-ops-20260915.md | 176 ++++++++++++++++++ docs/testing/vedastro-runtime-20260915.md | 89 +++++++++ 5 files changed, 429 insertions(+) create mode 100644 docs/tasks/TASK-chart-vedastro-decouple-20260915.md create mode 100644 docs/tasks/TASK-vedastro-runtime-ops-20260915.md create mode 100644 docs/testing/vedastro-runtime-20260915.md diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index 5268b66c..a7bff475 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -285,3 +285,19 @@ Prevention: 构建与测试 runner 不得与其他服务共用宿主文件系统 `docker compose --project-name jyotisha-postgres-* up -d --wait postgres` 报 `failed to create network …_app: all predefined address pools have been fully subnetted`。Docker daemon 正常,本机同时存在着十余个遗留 `jyotisha-postgres-*-postgres-1` 与 `jyotisha-local-preview-postgres-1`。`npm run test:db --test-concurrency=1` 仍每测新建 compose 项目网络。咨询上下文单未做 `docker network prune`(会清共享宿主资源)。 Prevention: 需要真实库测时先清本任务自己的 fixture 网络,或在默认 `bridge` 上起一次性 Postgres 做迁移语法检查;不得把地址池耗尽写成「本机无 Docker」。新迁移至少用 `psql --set ON_ERROR_STOP=1 -f` 在可写实例上跑通 ALTER/GRANT。 + +## ERR-107 | 官方 `vedastro` Python 包是 REST 客户端,且 import 时联网自升级,运行期版本与 `requirements.txt` 的 pin 不一致 | observed 2026-09-15 + +`requirements.txt:9` 固定 `vedastro==1.23.25`,`deploy/railway-api.Dockerfile` 在镜像里 `pip install -r requirements.txt`。把该版本装进本机 `.venv` 拆开后确认:整包 46 KB,`vedastro/calculate.py` 的每个方法都汇到 `requests.post("https://api.vedastro.org/api/Calculate/", timeout=120)`,`vedastro/__init__.py` 的自述是 `# VedAstro Python Library - REST API Mode`,**包内没有任何本地天文计算**。因此 `vedastro_python_bridge.py` / `vedastro_official_capability_runner.py` 这条「官方 SDK 路径」与 `vedastro_service_adapter.py` 的 `urlopen` 路径打的是同一个外部 API,不是本地算力。 + +更要紧的是 `vedastro/update_check.py`:`__init__.py` 在导出任何符号之前调用 `check_for_update("vedastro")`,它请求 `https://pypi.org/pypi/vedastro/json`(5 秒超时),发现有新版就直接 `subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "vedastro", "--quiet"])`。本机实测:装 1.23.25 后第一次 import 即被升级为 1.23.26。于是每次 spawn bridge 子进程都要付一次 pypi 往返;容器文件系统可写时 pin 会在运行期漂移,而这个包的内容直接决定发给外部 API 的请求形状。 + +Prevention: 不得把 `vedastro` 当作本地计算能力来源,也不得用它的存在证明「外部引擎已本地闭环」——它不可用时只等于外部 API 不可达。镜像必须中和 `check_for_update`(patch 失败即构建失败),并有测试断言运行期 `importlib.metadata.version("vedastro")` 等于 `requirements.txt` 的 pin。评估任何第三方 SDK 前先看它 import 期做了什么;import 期联网或改写自身环境的包一律按外部依赖审。关联 BUG-089(CI 自动升级 MCP 打破固定版本,同族)。 + +## ERR-108 | VedAstro 免费公共模式的排队是同步 sleep + 进程级全局锁,24 个请求的取证会把前台线程堵住数分钟 | observed 2026-09-15 + +`scripts/vedastro_service_adapter.py` 的 `_acquire_free_tier_slot()`(第 1877 行)在命中官方公共 endpoint 且 `VEDASTRO_API_KEY` 为空时,持 `_FREE_TIER_REQUEST_LOCK`(第 358 行)`time.sleep()` 等名额;配额默认 5 个 / 60 秒(第 354–355 行)。有 key 才走 `api_key_present` 分支不排队。 + +一次 official full snapshot 的请求清单实跑 `_official_full_snapshot_manifest` 数出来是 24 个(`events_overview` 1 + `dasha_all` 1 + `chart_core` 10 + `house_core` 12),fanout 开关 `VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED` 的代码默认值就是 `"1"`(第 2520 行)。24 ÷ 5 个每分钟 ≈ 4.8 分钟,期间该进程所有 VedAstro 调用被同一把锁串起来;生产是 2 vCPU 单进程。参照延迟:本机单次 `api.vedastro.org` 往返 0.38 秒,快速模式(fanout 关)一次完整 snapshot 3.65 秒 `status=ok`。生产在国内 VPS,真实 RTT 未测,上述数字只是下限。 + +Prevention: 前台同步路径不得进入无上限排队,超预算即 fail-fast 降级并让调用方看出是限流而非「服务不可用」;排队总预算必须可配且默认不超过 `VEDASTRO_TIMEOUT_SECONDS`。不得靠调大免费层配额绕过——那是第三方的额度。判断「VedAstro 慢」之前先确认当前是不是无 key 的免费公共模式,以及 fanout 是否开着;两者都属于运行期 env 真相,不能从代码默认值推定。关联 BUG-065、BUG-161、BUG-718、BUG-720。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index eebb08b1..9d28e5b7 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -232,6 +232,8 @@ | `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-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/tasks/TASK-chart-vedastro-decouple-20260915.md b/docs/tasks/TASK-chart-vedastro-decouple-20260915.md new file mode 100644 index 00000000..55fe0933 --- /dev/null +++ b/docs/tasks/TASK-chart-vedastro-decouple-20260915.md @@ -0,0 +1,146 @@ +# 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`,不得写成「通过」。 diff --git a/docs/tasks/TASK-vedastro-runtime-ops-20260915.md b/docs/tasks/TASK-vedastro-runtime-ops-20260915.md new file mode 100644 index 00000000..a02bf5bf --- /dev/null +++ b/docs/tasks/TASK-vedastro-runtime-ops-20260915.md @@ -0,0 +1,176 @@ +# TASK · VedAstro 运行期真相:SDK 自更新、免费层排队与生产模式确认(2026-09-15) + +## 基线 + +- 基线 commit:`origin/staging` = `69ede436` +- 分支:`codex/vedastro-runtime-ops-20260915` +- 并行关系:与 `TASK-chart-vedastro-decouple-20260915`(只改 `frontend/**`)文件不重叠,可并行。与 `TASK-api-server-decomposition-20260916`(独占 `scripts/jyotish_api_server.py`)也不重叠——**本单不得改 `scripts/jyotish_api_server.py`**。 +- BUG 编号起点:**BUG-719**(718 已由 chart-vedastro-decouple 单占用;开工时复核 `docs/BUG_HISTORY.md` 最大号) +- ERR 台账:本单相关的 **ERR-107 / ERR-108** 已写入 `docs/research/pre_work_error_ledger.md`,开工前必读。 + +## 事故实证 + +2026-09-15 在本机把 `requirements.txt` 固定的官方 SDK 装进 `.venv` 拆开看过(验完已卸载,`.venv` 已恢复原状)。 + +### 一、`vedastro==1.23.25` 不是计算库,它本身就是 REST 客户端 + +包总共 46 KB。`vedastro/calculate.py` 里每个方法最终都汇到同一处: + +```python +base_url = "https://api.vedastro.org/api/Calculate" +response = requests.post(url, json=params, timeout=120) +``` + +`vedastro/__init__.py` 第一行注释:`# VedAstro Python Library - REST API Mode`。包内没有任何本地天文计算。 + +**结论:所谓「官方 Python SDK 路径」和我们自己的 HTTP adapter 路径,打的是同一个 `api.vedastro.org`。现在是两套客户端接同一个外部 API**,一套经 `scripts/vedastro_python_bridge.py` / `scripts/vedastro_official_capability_runner.py` 起子进程,一套是 `scripts/vedastro_service_adapter.py` 自己 `urlopen`。 + +### 二、SDK 在 import 时联网检查版本并自动升级自己 + +`vedastro/update_check.py`: + +```python +response = requests.get('https://pypi.org/pypi/vedastro/json', timeout=5) +... +subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", package_name, "--quiet"]) +``` + +`__init__.py` 在导出任何符号之前就调用了它。本机实测:`pip install vedastro==1.23.25` 之后第一次 `import vedastro`,它自己把版本升到了 **1.23.26** 并打印「Please restart your script」。 + +生产镜像 `deploy/railway-api.Dockerfile` 走 `python -m pip install -r requirements.txt`,`requirements.txt:9` 是 `vedastro==1.23.25`。于是: + +- 每次 spawn 一个 bridge 子进程 → 一次 `import vedastro` → 一次 `pypi.org` 请求(5 秒超时)。容器出网受限时,这是每次调用白付的等待。 +- 容器文件系统可写时,pin 的 1.23.25 会在运行期漂成 pypi 上的最新版。**`requirements.txt` 的版本锁在运行期是假的**,而这个包的内容直接决定我们发给外部 API 的请求形状。 + +### 三、没有 API key 时,免费层排队是同步 sleep + 进程级全局锁 + +`scripts/vedastro_service_adapter.py`: + +- `DEFAULT_FREE_TIER_MAX_REQUESTS = 5`、窗口 60 秒(第 354–355 行) +- `_FREE_TIER_REQUEST_LOCK = threading.Lock()`(第 358 行) +- `_acquire_free_tier_slot()`(第 1877 行):命中官方公共 endpoint 且 `VEDASTRO_API_KEY` 为空时,**持锁 `time.sleep()` 等到窗口让出名额**;有 key 直接返回 `api_key_present` 不排队。 + +一次 official full snapshot 的请求清单是 24 个(实跑 `_official_full_snapshot_manifest` 数出来:`events_overview` 1 + `dasha_all` 1 + `chart_core` 10 + `house_core` 12),fanout 默认开(第 2520 行默认值 `"1"`)。 + +**无 key 时的算术:24 ÷ 5 个/分钟 ≈ 4.8 分钟**,且这段时间里该进程所有 VedAstro 调用被那把全局锁串成一条队。生产是 2 vCPU 单进程。 + +### 四、本机实测的两个延迟参照 + +- 单次 `api.vedastro.org` 往返:**0.38 s**(本机可直连公网)。 +- 快速模式(配 endpoint + `VEDASTRO_ENABLE_NETWORK=1` + `VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED=0`)跑一次 `run_official_full_snapshot_for_case`:**3.65 s,`status=ok`**,拿到 `ashtakavarga / chara_dasha_now / dasha_all / shadbala / vimshottari_now` 五个 section 全部 `ok`。 + +**环境缺口**:生产在国内 VPS(`118.194.235.34`),到 `vedastro.org` 的真实 RTT 与可达性我测不到。上面两个数字是「能直连公网的机器」的下限,不得当作生产表现。 + +### 五、生产 env 的已知与未知 + +`deploy/README.md` 生产段写明的:`VEDASTRO_GATEWAY_MODE=official_first`、`VEDASTRO_API_ENDPOINT=https://api.vedastro.org/api`、`VEDASTRO_ENABLE_NETWORK=1`、`VEDASTRO_RANGE_SCAN_NETWORK_ENABLED=1`、`VEDASTRO_TIMEOUT_SECONDS=20`、`VEDASTRO_API_KEY=`。 + +未知的(会话内无法查证,必须由产品负责人在服务器上确认): + +1. `VEDASTRO_API_KEY` 是否真的有值。**这一项决定第三条是不是正在生产上发生。** +2. `VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED` 是否被显式设为 `0`。README 没写,代码默认是开。 +3. `VEDASTRO_CACHE_TTL_SECONDS` / `VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS` 的实际值。 + +`docs/engine/vedastro-gateway.md` 推荐的「聊天快速模式」正是 fanout 关 + range scan 关,并明确写着完整模式「不建议直接放在聊天首轮的同步关键路径」。 + +## 根因 + +三件事叠在一起: + +1. 我们把一个第三方 REST 客户端当成「本地 SDK」在用,因此没人把它的行为当外部依赖审。 +2. 那个客户端在 import 期做了两件超出预期的事:联网、以及改写自己所在的运行环境。 +3. 我们自己的限流兜底(免费层排队)是为「偶尔调一次」设计的,而调用量是 24 起步。 + +## 决策记录 + +- 2026-09-15 产品负责人授权本单。范围是运行期真相:SDK 自更新隔离、免费层排队不得阻塞前台、生产模式确认。 +- **不在本单做的**:不砍掉两套客户端中的任何一套。虽然 SDK 只是 REST 包装、理论上 adapter 一套就够,但 SDK 那条目前现成提供了 `ashtakavarga / shadbala / vimshottari_now / chara_dasha_now / dasha_all` 五个 section 的包装,砍掉需要在 adapter 里补等价请求并重新对账,属于独立一轮。本单只把它的副作用关掉。 +- **不得改** `_attach_vedastro_main_entry_overview` 的默认行为。解读链(`/api/consult`、`/api/professional_reading`、高严谨工作流)依赖它,AGENTS Part B 的 MEVG 是硬约束。前台页面用显式标志跳过,见 chart-vedastro-decouple 单。 +- 生产 `.env.production` 的修改与服务重建**只由产品负责人触发**,执行方不得代劳、不得 SSH 改生产。 + +## 硬红线 + +1. 任何提交、任务书、进度记录、Bug 历史、测试 fixture 里**不得出现 key 值**(AGENTS §8、ERR-077 的教训)。只写变量名与「已配置 / 未配置」。 +2. 不得把任何 VedAstro 凭据放进前端或前端可读的响应。 +3. 不得改 `.gitea/workflows/**`、不改 DNS、不提升 `main`。 +4. 不得改 `scripts/jyotish_api_server.py`(由 api-server-decomposition 单独占)。 +5. 不得为了让调用变快而放宽 SSRF 防护、超时下限或限流。 +6. 不得声称生产行为已验证——除非拿到产品负责人回填的真实环境证据。 + +## 任务分解 + +### 1. 隔离 SDK 的自动升级与 pypi 探测(P0,BUG-719) + +目标:子进程 `import vedastro` 不得请求 `pypi.org`,不得 `pip install --upgrade` 改写运行环境;运行期版本必须等于 `requirements.txt` 固定的版本。 + +实现方式由执行方选,建议按此优先级评估: + +- 镜像层 post-install 把 `vedastro/update_check.py` 的 `check_for_update` 改写成 no-op(`Dockerfile` 内一条确定性的 patch 命令,patch 失败必须让构建失败,不得静默跳过); +- 或在 spawn 子进程时注入使 pip 网络安装不可能的环境(例如 `PIP_NO_INDEX=1`),但这只挡住升级动作,挡不住那次 5 秒超时的 pypi 请求,**单独使用不算达标**; +- 不接受的做法:删 `requirements.txt` 里的 pin、改成不装 SDK、或把 bridge 整条砍掉(那是另一轮的事)。 + +**验收标准** +- 新增测试:断言运行期 `importlib.metadata.version("vedastro")` 等于 `requirements.txt` 里 pin 的版本。 +- 新增测试或构建期断言:`check_for_update` 已被中和(调用它不产生网络请求)。用桩验证,不要真连 pypi。 +- 进度记录里给出改前 / 改后一次 bridge 子进程调用的耗时对比。 +- 反向验证:去掉 patch,断言必须红。 + +### 2. 免费层排队不得阻塞前台请求(P1,BUG-720) + +现状:无 key 时前台请求线程会持全局锁 sleep 数分钟。 + +**定级依赖任务 4 的回填结果**: + +- 若生产**有** key → 这条在生产不触发,降为 P2,但仍要做,因为 staging 与本地开发经常无 key,并且 key 失效时会静默退化成这个行为。 +- 若生产**无** key → 升为 P0,与任务 1 同批交付。 + +要求: + +- 前台同步路径(任何会被 HTTP 请求线程等待的调用)不得进入无上限排队。超出预算即 fail-fast 返回明确的降级状态,由调用方按既有降级路径处理。 +- 排队等待的总预算必须可配置且有默认上限,默认值不得大于 `VEDASTRO_TIMEOUT_SECONDS`。 +- 不得用「提高免费层配额」的方式绕过——那是别人家的服务,不是我们的开关。 + +**验收标准** +- 新增回归:模拟名额耗尽时,前台路径在预算内返回降级状态而不是 sleep 到超时;断言返回体里能看出是限流降级(不是「外部服务不可用」这种混淆文案)。 +- 断言全局锁不会被单个请求持有超过预算。 +- 既有 VedAstro adapter 测试全绿,总数不降。 + +### 3. 让当前模式可观测(P2) + +`/api/vedastro_gateway/status` 应能一眼看出本进程当前处在哪种模式,至少包含:是否配置 endpoint、是否有 key(**布尔值,不是 key 本身**)、fanout 是否开、range scan 是否开、免费层排队是否 active、运行期 SDK 版本、两个缓存 TTL。 + +**验收标准**:新增测试断言响应里这些字段存在且不泄露凭据;至少覆盖「有 key」「无 key」两种桩。 + +### 4. 生产模式确认(P0,产品负责人执行) + +清单写在 `docs/testing/vedastro-runtime-20260915.md`,执行方**不得代劳**。执行方的责任是:清单回填后,把结论写进 BUG-720 的触发条件与定级,并据此决定任务 2 的优先级。 + +### 5. Bug 历史与台账(P0) + +- **BUG-719**(SDK 自更新):`相关记录` 带上 ERR-107、BUG-089(Staging CI 自动升级 MCP 2.0 导致导入失败——同族:自动升级打破固定版本)。 +- **BUG-720**(免费层同步排队):`相关记录` 带上 ERR-108、BUG-065(VedAstro 请求乘法导致数分钟无响应)、BUG-161(前台不得同步串联可选外部证据)、BUG-718。若任务 4 尚未回填,状态写 `investigating`,把已确认的代码事实写清楚,**不得预先标 `resolved`**,也不得编造生产表现。 +- 两条都不得写入任何凭据、真实出生资料或用户标识。 + +## 让步顺序 + +任务 1 → 任务 4(本来就是产品侧)→ 任务 2 → 任务 5 → 任务 3。任务 1 可单独交付。 + +## 开工前置命令 + +```bash +cd /workspace/Jyotisha +git fetch origin --prune +git worktree add -b codex/vedastro-runtime-ops-20260915 \ + .worktrees/vedastro-runtime-ops-20260915 origin/staging +cd .worktrees/vedastro-runtime-ops-20260915 +python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45 +``` + +本单涉及外部 oracle 与镜像边界,AGENTS §9 预检必跑。开工前必读:`docs/research/pre_work_error_ledger.md`(尤其 ERR-077、ERR-079、ERR-107、ERR-108)、`docs/engine/vedastro-gateway.md`、`docs/BUG_HISTORY.md` 的 BUG-065 / BUG-089 / BUG-161。 + +## 验收口径 + +- `.venv/bin/python scripts/run_quality_gate.py --profile quick` 通过。 +- `.venv/bin/python -m pytest tests/` fail=0,总数不低于开工基线。 +- 镜像层改动:必须给出构建证据(patch 生效、版本一致),不得只凭代码推断。 +- 生产表现一律写成环境缺口,附 `docs/testing/vedastro-runtime-20260915.md`,回填前不得写「通过」。 diff --git a/docs/testing/vedastro-runtime-20260915.md b/docs/testing/vedastro-runtime-20260915.md new file mode 100644 index 00000000..598623ef --- /dev/null +++ b/docs/testing/vedastro-runtime-20260915.md @@ -0,0 +1,89 @@ +# 真人核对 · 生产 VedAstro 运行模式(2026-09-15) + +这份清单只能由产品负责人在生产主机上执行(会话内没有 SSH 凭据,也不应该有)。目的是确认三件事:**有没有 API key、fanout 是不是开着、缓存 TTL 是多少**。 + +下面每条命令都刻意只输出「有 / 没有」或数字,**不会打印 key 值**。请不要改写成 `cat .env.production` 或 `grep VEDASTRO_API_KEY` 直接看值,也不要把命令输出之外的内容贴回对话。 + +## 准备 + +```bash +ssh -p deploy@118.194.235.34 +cd /opt/jyotisha-production +``` + +## 1. key 是否配置(最关键) + +```bash +grep -qE '^VEDASTRO_API_KEY=.+$' .env.production && echo "API_KEY: set" || echo "API_KEY: EMPTY" +``` + +回填:`API_KEY: ____` + +- `EMPTY` 意味着生产正在用官方免费公共模式。一次未命中缓存的完整取证要发 24 个外部请求,而免费层是 5 个/60 秒且我们的排队实现是同步 sleep + 全局锁——见 `TASK-vedastro-runtime-ops-20260915.md` 任务 2,该任务会因此升为 P0。 + +## 2. fanout 与 range scan 开关 + +```bash +grep -E '^VEDASTRO_(FULL_SNAPSHOT_FANOUT_ENABLED|RANGE_SCAN_NETWORK_ENABLED|ENABLE_NETWORK|TIMEOUT_SECONDS|GATEWAY_MODE)=' .env.production +``` + +这几个不是 secret,可以直接看值。回填: + +| 变量 | 实际值 | 代码默认 | +| --- | --- | --- | +| `VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED` | ____ | `1`(开,24 个请求) | +| `VEDASTRO_RANGE_SCAN_NETWORK_ENABLED` | ____ | 跟随 `ENABLE_NETWORK` | +| `VEDASTRO_ENABLE_NETWORK` | ____ | 空(关) | +| `VEDASTRO_TIMEOUT_SECONDS` | ____ | 见适配器默认 | +| `VEDASTRO_GATEWAY_MODE` | ____ | — | + +**没有这一行**就等于取代码默认值 `1`(开)。 + +## 3. 缓存 TTL + +```bash +grep -E '^VEDASTRO_(CACHE_TTL_SECONDS|OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS)=' .env.production +grep -E '^JYOTISH_API_CHART_CACHE_TTL_SECONDS=' .env.production +``` + +回填:`VEDASTRO_CACHE_TTL_SECONDS: ____`、`OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS: ____`、`JYOTISH_API_CHART_CACHE_TTL_SECONDS: ____`(后者没配则是 900 秒) + +## 4. 网关自报的运行状态 + +```bash +COMPOSE='docker compose -p jyotisha-production --env-file .env.production -f deploy/docker-compose.server.yml -f deploy/docker-compose.postgres.yml -f deploy/docker-compose.production.yml' +$COMPOSE exec -T api sh -c 'curl -s -m 20 http://127.0.0.1:5200/api/vedastro_gateway/status' | head -c 2000; echo +``` + +回填整段输出(它不含 key,只含布尔与状态名)。如果里面出现任何看起来像凭据的长串,**不要贴回来**,只说「有疑似凭据字段」。 + +## 5. 运行期 SDK 实际版本(验证 pin 是否漂移) + +```bash +$COMPOSE exec -T api python -c "from importlib.metadata import version; print('vedastro runtime version:', version('vedastro'))" +``` + +回填:`____` + +`requirements.txt` 固定的是 `1.23.25`。**如果这里显示的不是 1.23.25**,说明 SDK 在容器里自己把自己升级了(它 import 时会请求 pypi 并 `pip install --upgrade`),这就是 BUG-719 的生产实证。 + +## 6. 星盘页真实首屏(改动上线后再做) + +`TASK-chart-vedastro-decouple-20260915` 部署到 staging 之后: + +```bash +# 在你自己的电脑上,登录 staging 后用浏览器打开 +https://staging.jyotisha.chat/chart +``` + +- 主盘应该几乎立刻出现(本机实测本地计算 5 毫秒)。 +- 记录一次「冷」打开(先等 15 分钟以上让引擎缓存过期,或换一个还没算过的出生资料)的主观等待时间。 +- 五个 Tab 逐个点开,内容与改动前一致即可。 + +回填:冷打开等待 `____` 秒;Tab 有无异常 `____` + +## 回填后怎么处理 + +把 1–5 的回填结果交给执行方,写进 `BUG-720` 的触发条件与定级。第 6 条写进 `docs/testing/chart-page-20260915.md` 的走查记录。 + +**在这份清单回填之前,任何人不得声称「生产 VedAstro 行为已验证」。**