主题
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.pyCreate:
core/tests/unit/test_data_market.py[x] Step 1.1:
is_trading_day(date_str, token)— Tusharetrade_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.pyCreate:
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)— callak.stock_individual_spot_xq(symbol=...); extract 现价 row from the two-column item/value frame; wrap upstream exceptions asAkshareError[x] Step 2.3: cite envelope — same shape as
schemas._claimplusserved_by: "akshare";metric: "current_price";as_of= today in Shanghai TZ[x] Step 2.4: Lazy import —
import akshare as akinsideget_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_emwhich 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.pyModify:
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 writesserved_byto dict when not None[x] Step 3.3: Backfill
served_by="tushare"in the existingget_priceTushare 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), callakshare.get_spot_price(code)[x] Step 4.2: Fallback —
try/except AkshareError: passso 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.mdModify:
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_envelopeto assertserved_by; addtest_fetch_price_routes_to_akshare_during_market_hours(mocksis_market_open+get_spot_price)[x] Step 5.3: Fix Keychain leak in
test_no_token_configured_emits_clear_error— monkeypatch_resolve_secretso the dev-machine Keychain doesn't satisfy the test's "no token" precondition
Task 6: Bump + ship
- [x] Step 6.1:
core/pyproject.toml—0.1.0→0.1.1; addakshare>=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 verify —
fetch_price.py 600519returnsserved_by: "tushare"(post-market), direct akshare call returnsserved_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.mdTasks 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.pyCreate:
core/tests/test_service_auth.py[ ] Step 7.1: app skeleton —
FastAPI(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+); attachuser_id+scopeto 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.pyCreate:
src/service/routes/price.pyCreate:
core/tests/test_service_cache.py,core/tests/test_service_routes.py[ ] Step 8.1: DuckDB schema —
daily_cache(code TEXT, trade_date TEXT, close DOUBLE, fetched_at TIMESTAMP, PRIMARY KEY (code, trade_date)); 24h TTL viafetched_atfilter[ ] Step 8.2: cache layer —
get_cached(code, trade_date)/put_cached(code, trade_date, close, fetched_at); backend serializes writes (single writer)[ ] Step 8.3:
/priceroute — query:code, optionaltrade_date; cache hit → return; miss → callcore.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: route —
code+periodquery params; thin wrapper overcore.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 surface —
issue {--user-id, --plan, --paid-until}/revoke {--user-id}/list[ ] Step 10.2: Token generation —
secrets.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
/healthzwith 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 unit —
Type=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;
Authorizationheader 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 test —
curl https://api.twilight-drive.example/healthzreturns 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 bearer —
scripts/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=...; removeTUSHARE_TOKENfrom profile (Tushare token now lives only in backend EnvironmentFile) - [ ] Step 12.3: Verify —
fetch_price.py 600519returns identical envelope shape, but cite shows backend-sidetool_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)
/healthzreturns 200/price?code=600519.SHreturns cite-wrapped value with properserved_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 min | cite 记录 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 后)+ 启动脚本兜底清理 |
进一步阅读
- 06 — v0.1.1 Hotfix + P1 起步:本 plan 的 spec 视角
- Phase 1 plan:P1.0–P1.3 完整任务清单
- 01 — 数据层:Tushare / akshare 数据决策
- 00 — 多租户架构:Phase 2 backend 怎么收 Tier 1 secrets