Skip to content

v0.1.1 Hotfix — Implementation Plan

Status: ✅ Phase 1 (hotfix) shipped 2026-05-07 · ✅ Phase 2 Tasks 7-10 已合主线(PR #13)· Tasks 11-12 待操作 · 完整状态见 /status/Source spec: docs/planning/06-v0.1.1-hotfix-and-p1.mdLinked roadmap: docs/planning/superpowers/plans/2026-05-07-twilight-drive-phase1.md (Phase 2 here pulls Tasks 1-4 + 8 from there)

Goal: Ship v0.1.1 to fix the get_price stale-price bug reported on the live stock-research_Agent Hermes profile, then start the P1.0 backend skeleton in the same sprint to validate the Direct → Service migration story.

Architecture: Add an akshare adapter (Xueqiu single-code endpoint) + market-clock helper to core, route get_price automatically, extend the cite envelope with served_by. Backend (P1.0) reuses the new schemas.get_price routing behind a FastAPI + DuckDB cache + Bearer auth shell.

Tech Stack:

  • Python 3.11+
  • akshare>=1.13 (Xueqiu spot endpoint)
  • existing: pydantic, httpx, structlog

Repository surface affected:

core/
├── core/data/
│   ├── _market.py                    # NEW
│   ├── akshare.py                    # NEW
│   ├── schemas.py                    # MODIFY get_price routing
│   ├── tushare.py                    # unchanged
│   └── citation.py                   # MODIFY (served_by field)
├── tests/unit/
│   ├── test_data_market.py           # NEW
│   ├── test_data_akshare.py          # NEW
│   └── test_data_schemas.py          # EXTEND (3 routing branches)
├── tests/integration/
│   └── test_skill_scripts.py         # EXTEND (served_by + akshare e2e + Keychain leak fix)
└── pyproject.toml                    # 0.1.0 → 0.1.1; +akshare dep

skill/stock-research/
└── SKILL.md                          # MODIFY (price freshness section)

docs/                                 # site refresh tracked separately
profile/template-stock-research-pro/  # NEW (Week 1 D5 wrap)

Phase 1: v0.1.1 Hotfix (Week 1, ✅ shipped)

Task 1: Market clock + trade_cal helper

Files:

  • Create: core/core/data/_market.py

  • Create: core/tests/unit/test_data_market.py

  • [x] Step 1.1: is_trading_day(date_str, token) — Tushare trade_cal (exchange=SSE) with per-date in-process cache; weekday fallback when no token; conservative False on empty response

  • [x] Step 1.2: is_market_open(now, token) — Asia/Shanghai TZ + 09:30–15:00 window + is_trading_day (with try/except → weekday fallback on Tushare outage)

  • [x] Step 1.3: Unit tests — 11 cases covering weekday/weekend, holidays, before/after market, lunch break (coarse-open), naive datetime, cache, Tushare-fallback

python
# core/core/data/_market.py — public surface
def is_trading_day(date_str: str, *, token: str | None = None) -> bool: ...
def is_market_open(*, now: datetime | None = None, token: str | None = None) -> bool: ...

Task 2: akshare adapter

Files:

  • Create: core/core/data/akshare.py

  • Create: core/tests/unit/test_data_akshare.py

  • [x] Step 2.1: _to_xueqiu_symbol(code) — translate "600519.SH""SH600519"; reject bare 6-digit codes

  • [x] Step 2.2: get_spot_price(code) — call ak.stock_individual_spot_xq(symbol=...); extract 现价 row from the two-column item/value frame; wrap upstream exceptions as AkshareError

  • [x] Step 2.3: cite envelope — same shape as schemas._claim plus served_by: "akshare"; metric: "current_price"; as_of = today in Shanghai TZ

  • [x] Step 2.4: Lazy importimport akshare as ak inside get_spot_price, not at module top, so the Tushare-only path doesn't pay the pandas/HTTP import tax

Performance note: First implementation used stock_zh_a_spot_em which paginates the full ~5,000-stock universe (~75s). E2E test caught it; switched to per-code Xueqiu (~1.4s).

Task 3: cite envelope served_by

Files:

  • Modify: core/core/citation.py

  • Modify: core/core/data/schemas.py

  • [x] Step 3.1: ToolCite.served_by: str | None = None — additive, optional, so legacy cites parse fine

  • [x] Step 3.2: _claim(..., served_by=None) — only writes served_by to dict when not None

  • [x] Step 3.3: Backfill served_by="tushare" in the existing get_price Tushare branch

Task 4: get_price routing

Files:

  • Modify: core/core/data/schemas.py

  • [x] Step 4.1: Add early return — when trade_date is None and _market.is_market_open(token=token), call akshare.get_spot_price(code)

  • [x] Step 4.2: Fallbacktry/except AkshareError: pass so a missing-row in akshare degrades to Tushare daily rather than raising

  • [x] Step 4.3: Unit tests — 4 routing branches: explicit trade_date → tushare; market open → akshare; akshare error → tushare fallback; market closed → tushare

Task 5: Skill-side updates

Files:

  • Modify: skill/stock-research/SKILL.md

  • Modify: core/tests/integration/test_skill_scripts.py

  • [x] Step 5.1: SKILL.md "价格新鲜度语义" — table mapping (盘中 / 盘后 / 显式 trade_date) → (metric, served_by); narration discipline ("当前价" vs "最近收盘")

  • [x] Step 5.2: Integration test — extend test_fetch_price_direct_mode_emits_cited_envelope to assert served_by; add test_fetch_price_routes_to_akshare_during_market_hours (mocks is_market_open + get_spot_price)

  • [x] Step 5.3: Fix Keychain leak in test_no_token_configured_emits_clear_error — monkeypatch _resolve_secret so the dev-machine Keychain doesn't satisfy the test's "no token" precondition

Task 6: Bump + ship

  • [x] Step 6.1: core/pyproject.toml0.1.00.1.1; add akshare>=1.13,<2
  • [x] Step 6.2: pytest 60/60 green, ruff clean
  • [x] Step 6.3: install-skill.sh dev mode — refresh skill in ~/.hermes/profiles/stock-research-agent/; verify Hermes venv picks up new core + akshare
  • [x] Step 6.4: E2E verifyfetch_price.py 600519 returns served_by: "tushare" (post-market), direct akshare call returns served_by: "akshare" (~1.4s)
  • [ ] Step 6.5: tag v0.1.1 + push — Week 1 D5 PM
  • [ ] Step 6.6: Replace AGENTS.md/SOUL.md in stock-research_Agent profile with A-share template; commit template to profile/template-stock-research-pro/ — Week 1 D5 wrap

Phase 2: P1.0 Backend Skeleton (Week 2, ✅ Tasks 7-10 in PR #13 · ⏳ Tasks 11-12 待操作)

Pulls from docs/planning/superpowers/plans/2026-05-07-twilight-drive-phase1.md Tasks 1-4 + 8. The full task breakdown lives there; this section captures Week 2's slice.

Task 7: FastAPI scaffold + Settings + Bearer auth

Files:

  • Create: src/service/main.py, src/service/config.py, src/service/auth.py

  • Create: core/tests/test_service_auth.py

  • [ ] Step 7.1: app skeletonFastAPI(title="twilight-drive", version="0.2.0"); CORS off; health route placeholder

  • [ ] Step 7.2: Settings (Pydantic v2) — env-driven: TUSHARE_TOKEN, DUCKDB_PATH, BEARER_DB_PATH, LISTEN_HOST/PORT, LOG_LEVEL

  • [ ] Step 7.3: Bearer auth middleware — pull Authorization: Bearer <token>, SHA256 hash, look up in SQLite (P1.0) or Postgres (P1.1+); attach user_id + scope to request state; 401 on missing, 403 on revoked/expired

  • [ ] Step 7.4: Tests — auth happy path / missing header / invalid hash / revoked / expired

Task 8: DuckDB cache + /price

Files:

  • Create: src/service/cache.py

  • Create: src/service/routes/price.py

  • Create: core/tests/test_service_cache.py, core/tests/test_service_routes.py

  • [ ] Step 8.1: DuckDB schemadaily_cache(code TEXT, trade_date TEXT, close DOUBLE, fetched_at TIMESTAMP, PRIMARY KEY (code, trade_date)); 24h TTL via fetched_at filter

  • [ ] Step 8.2: cache layerget_cached(code, trade_date) / put_cached(code, trade_date, close, fetched_at); backend serializes writes (single writer)

  • [ ] Step 8.3: /price route — query: code, optional trade_date; cache hit → return; miss → call core.data.schemas.get_price (auto-inherits v0.1.1 routing) → cache → return

  • [ ] Step 8.4: Verifier integration — re-validate cite envelope before returning to client (defense in depth)

  • [ ] Step 8.5: Tests — cache hit < 50ms; 100 same-code requests = 1 Tushare call; 401/403; akshare path bypasses cache (live data)

Task 9: /fundamentals

Files:

  • Create: src/service/routes/fundamentals.py

  • [ ] Step 9.1: routecode + period query params; thin wrapper over core.data.schemas.get_fundamentals

  • [ ] Step 9.2: Cache strategy — fundamentals are quarterly; (code, period) cache key; long TTL (30 days)

Task 10: admin CLI scripts/issue-token.py

Files:

  • Create: scripts/issue-token.py

  • [ ] Step 10.1: CLI surfaceissue {--user-id, --plan, --paid-until} / revoke {--user-id} / list

  • [ ] Step 10.2: Token generationsecrets.token_urlsafe(32); print plaintext once, store SHA256 hash + metadata in same SQLite/Postgres the auth middleware reads

  • [ ] Step 10.3: Smoke test — issue → curl /healthz with bearer → 200; revoke → curl → 403

Task 11: Deploy to Vultr

Files:

  • Create: scripts/deploy-backend.sh, twilight-service.service (systemd unit), Caddyfile, docker-compose.yml

  • [ ] Step 11.1: systemd unitType=notify, EnvironmentFile=/etc/twilight-drive/secrets.env, Restart=on-failure, User=twilight

  • [ ] Step 11.2: Caddy — auto-LE TLS; reverse proxy 443 → 127.0.0.1:8080; Authorization header passthrough

  • [ ] Step 11.3: deploy script — rsync repo to Vultr, uv pip install -e ./core, systemctl daemon-reload && systemctl restart twilight-service

  • [ ] Step 11.4: smoke testcurl https://api.twilight-drive.example/healthz returns 200; curl https://.../price?code=600519.SH (with bearer) returns cite envelope

Task 12: Migrate stock-research_Agent profile to Service mode

  • [ ] Step 12.1: Issue user bearerscripts/issue-token.py issue --user-id liang --plan internal --paid-until 2027-12-31
  • [ ] Step 12.2: Update profile env — set TWILIGHT_SERVICE_URL=https://... + TWILIGHT_API_TOKEN=...; remove TUSHARE_TOKEN from profile (Tushare token now lives only in backend EnvironmentFile)
  • [ ] Step 12.3: Verifyfetch_price.py 600519 returns identical envelope shape, but cite shows backend-side tool_call_id

Verification

Phase 1 (already verified)

$ pytest core/tests/ -q
60 passed
$ ruff check core/ skill/
All checks passed
$ ~/.hermes/profiles/stock-research-agent/skills/research/stock-research/scripts/fetch_price.py 600519
{"value": 1371.05, "metric": "close", "cite": {"served_by": "tushare", ...}}

Phase 2 (success criteria — pulled from Phase 1 spec §1.5)

  • /healthz returns 200
  • /price?code=600519.SH returns cite-wrapped value with proper served_by
  • 100 same-code requests during one minute → 1 Tushare call (cache works)
  • 401 without bearer; 403 with revoked bearer
  • Cache hit < 50 ms (p99)
  • Verifier re-validation passes on all responses

风险与缓解

风险缓解
akshare 镜像延迟 5–15 mincite 记录 fetched_at;P1.2 加 staleness_max_seconds 配置
akshare 包破坏性升级pin akshare==X.Y.Z;pip-audit 加到 supply chain
is_market_open() 边界(午休、ST 停牌)v0.1.1 粗粒度(is_trading_day && 09:30 ≤ now ≤ 15:00),P1.2 精细化
Tushare token 在 backend 集中持有 = 单点故障systemd EnvironmentFile + secondary Tushare token + rate-limit 不超 paid 配额
Profile stale gateway.lock(Hermes 崩溃但锁未释放)scripts/lock-status.sh(P1.1 后)+ 启动脚本兜底清理

进一步阅读

团队内部文档