Files
Jyotisha/docs/tasks/TASK-vedastro-runtime-ops-20260915.md
T
Jesse_ChenandClaude Fable 5 ce1939b074 docs(tasks): 星盘页与 VedAstro 外网调用脱钩 + 运行期真相两单
星盘页首屏那一发 /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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-15 15:47:32 +00:00

12 KiB
Raw Blame History

TASK · VedAstro 运行期真相:SDK 自更新、免费层排队与生产模式确认(2026-09-15)

基线

  • 基线 commitorigin/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-719718 已由 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 里每个方法最终都汇到同一处:

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

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.Dockerfilepython -m pip install -r requirements.txtrequirements.txt:9vedastro==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_case3.65 sstatus=ok,拿到 ashtakavarga / chara_dasha_now / dasha_all / shadbala / vimshottari_now 五个 section 全部 ok

环境缺口:生产在国内 VPS118.194.235.34),到 vedastro.org 的真实 RTT 与可达性我测不到。上面两个数字是「能直连公网的机器」的下限,不得当作生产表现。

五、生产 env 的已知与未知

deploy/README.md 生产段写明的:VEDASTRO_GATEWAY_MODE=official_firstVEDASTRO_API_ENDPOINT=https://api.vedastro.org/apiVEDASTRO_ENABLE_NETWORK=1VEDASTRO_RANGE_SCAN_NETWORK_ENABLED=1VEDASTRO_TIMEOUT_SECONDS=20VEDASTRO_API_KEY=<server-secret>

未知的(会话内无法查证,必须由产品负责人在服务器上确认):

  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 探测(P0BUG-719

目标:子进程 import vedastro 不得请求 pypi.org,不得 pip install --upgrade 改写运行环境;运行期版本必须等于 requirements.txt 固定的版本。

实现方式由执行方选,建议按此优先级评估:

  • 镜像层 post-install 把 vedastro/update_check.pycheck_for_update 改写成 no-opDockerfile 内一条确定性的 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-719SDK 自更新):相关记录 带上 ERR-107、BUG-089Staging CI 自动升级 MCP 2.0 导致导入失败——同族:自动升级打破固定版本)。
  • BUG-720(免费层同步排队):相关记录 带上 ERR-108、BUG-065VedAstro 请求乘法导致数分钟无响应)、BUG-161(前台不得同步串联可选外部证据)、BUG-718。若任务 4 尚未回填,状态写 investigating,把已确认的代码事实写清楚,不得预先标 resolved,也不得编造生产表现。
  • 两条都不得写入任何凭据、真实出生资料或用户标识。

让步顺序

任务 1 → 任务 4(本来就是产品侧)→ 任务 2 → 任务 5 → 任务 3。任务 1 可单独交付。

开工前置命令

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.mddocs/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/<vedastro 相关文件> fail=0,总数不低于开工基线。
  • 镜像层改动:必须给出构建证据(patch 生效、版本一致),不得只凭代码推断。
  • 生产表现一律写成环境缺口,附 docs/testing/vedastro-runtime-20260915.md,回填前不得写「通过」。