Skip to content

Twilight Drive Phase 2 — Infrastructure Hardening

📤 内部参考

本 spec 引用了 P1 阶段的对外定价(¥198/月、WeChat Pay)作为上下文。对外内容由独立项目承接,不展示于 dev.fsagent.cc 的对外路径;本文呈现仅限技术决策(Token Gateway / 数据仓库 / Profile Manager)讨论。

Date: 2026-05-07 Status: Draft (pending user review) Source: /Users/syone/prd/MASTER_PLAN.md §3 (Initiatives A, B, C)

Background

Phase 1 ships a paid A-share research bot on WeChat (¥198/mo). After P1.0–P1.3, we have:

  • FastAPI on Vultr with DuckDB cache serving price/fundamentals/search
  • WeChat Pay → provisioning flow → per-user Hermes gateway → WeChat bot
  • Multi-source data (Tushare + pytdx3 + akshare + Yahoo) behind the same API
  • Web search via DashScope → Google fallback

Phase 2 addresses three infrastructure gaps that block scale and reliability:

  1. Initiative A: Centralized data warehouse — eliminate duplicate API calls, add data quality checks, 4-layer data model
  2. Initiative B: Profile replication + ownership — git-backed profiles, claim/push/pull/sync CLI, template system
  3. Initiative C: Token gateway — SiliconFlow key pooling with New-API, SearXNG web search, guardrails

Goal

Ship three infrastructure foundations that let Twilight Drive scale from "a few alpha users" to "dozens of paying users with reliable, auditable data":

  1. A: twilight-data hub — scheduled batch ingestion → data quality checks → 4-layer DuckDB warehouse → redistribution API
  2. B: hermes-profile-manager — git-backed profile registry with ownership, templates, multi-host sync
  3. C: New-API + SearXNG — LLM key pooling with auto-fallback + self-hosted web search (no per-engine API keys)

Non-Goals

  • Real-time data streaming (P3)
  • Multi-tenant Kubernetes orchestration (overkill for ≤ 100 users)
  • Stripe integration (Phase 2 payment scope, not infra)
  • Research report PDF ingestion (Phase 2 content scope, not infra)

Initiative A: Centralized Data Warehouse

A.1 — Problem

Currently, each Hermes profile and the FastAPI backend independently fetch data from upstream sources (Tushare, akshare, Alpha Vantage, etc.):

  • Duplicate API calls: Profile #1 and Profile #2 both query 600519 daily — wasting Tushare points
  • No data quality control: No cross-source verification, no anomaly detection
  • No historical persistence: Every query is fresh from upstream; no backfill
  • No unified access layer: Downstream consumers (Hermes profiles, external APIs) each wire their own adapters

A.2 — Architecture

┌─────────────────────────────────────────────────────────────┐
│                    twilight-data Hub                        │
│                    (Vultr + Mac Mini)                       │
│                                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────────┐ │
│  │ Tushare  │  │  akshare │  │  pytdx3  │  │  Yahoo/AV  │ │
│  │  (A股)   │  │  (宏观)  │  │  (历史)  │  │  (全球)    │ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └─────┬──────┘ │
│       └──────────────┴──────────────┴──────────────┘        │
│                          ▼                                  │
│              ┌───────────────────────┐                      │
│              │   Data Quality Layer  │                      │
│              │  - Cross-source check │                      │
│              │  - Anomaly detection  │                      │
│              │  - Schema validation  │                      │
│              │  - Freshness monitor  │                      │
│              └───────────┬───────────┘                      │
│                          ▼                                  │
│              ┌───────────────────────┐                      │
│              │    DuckDB Warehouse   │                      │
│              │                       │                      │
│              │  Raw ──► Normalized   │                      │
│              │   ──► Curated ──►     │                      │
│              │   Feature             │                      │
│              └───────────┬───────────┘                      │
│                          ▼                                  │
│              ┌───────────────────────┐                      │
│              │   Redistribution API  │                      │
│              │   FastAPI (unified)   │                      │
│              │   extends P1.0 API    │                      │
│              └───────────────────────┘                      │
└─────────────────────────────────────────────────────────────┘


          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
    ┌──────────┐  ┌──────────────┐  ┌──────────┐
    │ Hermes #1│  │ Hermes #2..N │  │  External│
    │ (crypto) │  │  (stocks)    │  │  Users   │
    └──────────┘  └──────────────┘  └──────────┘

A.3 — 4-Layer Data Model

LayerPurposeMutabilityRetention
RawExact upstream response, immutableAppend-onlyForever
NormalizedCleaned, typed, deduplicated, unified schemaRewrite on re-ingestForever
CuratedBusiness logic applied (splits adjusted, corporate actions)Rewrite on logic changeForever
FeatureDerived metrics (PE bands, momentum, factor scores)Rewrite on recomputation90 days (regenerate on demand)

A.4 — DuckDB Schemas

sql
-- Layer 0: Raw (append-only, JSON blobs)
CREATE TABLE raw_tushare_daily (
  id           BIGINT AUTO_INCREMENT PRIMARY KEY,
  code         VARCHAR,
  fetched_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  response_json  JSON,              -- exact Tushare response
  source       VARCHAR DEFAULT 'tushare'
);

-- Layer 1: Normalized (unified schema)
CREATE TABLE normalized_daily (
  code         VARCHAR NOT NULL,
  trade_date   DATE NOT NULL,
  open         DOUBLE,
  high         DOUBLE,
  low          DOUBLE,
  close        DOUBLE,
  volume       BIGINT,
  amount       DOUBLE,
  source       VARCHAR,              -- 'tushare', 'pytdx3', 'yahoo'
  ingested_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (code, trade_date, source)
);

-- Layer 2: Curated (business logic)
CREATE TABLE curated_daily (
  code         VARCHAR NOT NULL,
  trade_date   DATE NOT NULL,
  open         DOUBLE,
  high         DOUBLE,
  low          DOUBLE,
  close        DOUBLE,              -- split-adjusted
  volume       BIGINT,
  amount       DOUBLE,
  adj_factor   DOUBLE,              -- adjustment factor for backtesting
  PRIMARY KEY (code, trade_date)
);

-- Layer 3: Feature (derived metrics)
CREATE TABLE feature_daily (
  code         VARCHAR NOT NULL,
  trade_date   DATE NOT NULL,
  ma_5         DOUBLE,
  ma_10        DOUBLE,
  ma_20        DOUBLE,
  ma_60        DOUBLE,
  pe_ttm       DOUBLE,
  pb           DOUBLE,
  momentum_5d  DOUBLE,
  momentum_20d DOUBLE,
  volatility_20d DOUBLE,
  PRIMARY KEY (code, trade_date)
);

-- Data quality log
CREATE TABLE quality_log (
  id           BIGINT AUTO_INCREMENT PRIMARY KEY,
  check_name   VARCHAR,              -- 'cross_source', 'anomaly', 'freshness'
  code         VARCHAR,
  trade_date   DATE,
  severity     VARCHAR,              -- 'info', 'warning', 'error'
  message      VARCHAR,
  created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Source freshness tracker
CREATE TABLE source_freshness (
  source       VARCHAR PRIMARY KEY,
  last_success TIMESTAMP,
  last_failure TIMESTAMP,
  error_message VARCHAR,
  row_count_today INT DEFAULT 0
);

A.5 — Data Quality Rules

RuleCheckAction
Cross-source agreementTushare close vs pytdx3 close for same (code, date)Log warning if diff > 0.01%
FreshnessLast successful fetch per sourceAlert if > 24h stale
Schema validationAll rows match expected types, no nulls in key fieldsReject + quarantine bad rows
Anomaly detectionPrice change > 20% single dayFlag for manual review
CompletenessExpected columns presentReject incomplete rows

A.6 — Ingestion Scheduler

APScheduler in-process, daily batch:

python
# src/data/ingest/scheduler.py
from apscheduler.schedulers.background import BackgroundScheduler

scheduler = BackgroundScheduler()

# Daily: pull all stocks from Tushare (1 call → all 5000 stocks)
scheduler.add_job(
    ingest_tushare_daily,
    'cron',
    hour=18, minute=30,  # after market close
    id='tushare_daily',
    replace_existing=True,
)

# Historical backfill (run once, then disable)
# scheduler.add_job(
#     backfill_pytdx3,
#     'cron',
#     hour=2, minute=0,
#     id='pytdx3_backfill',
# )

# Akshare macro data (weekly)
scheduler.add_job(
    ingest_akshare_macro,
    'cron',
    hour=3, minute=0,
    day_of_week='mon',
    id='akshare_macro',
)

# Quality checks (after each ingest)
scheduler.add_job(
    run_quality_checks,
    'cron',
    hour=19, minute=0,
    id='quality_checks',
)

# Freshness check (every 6 hours)
scheduler.add_job(
    check_source_freshness,
    'interval',
    hours=6,
    id='freshness_check',
)

A.7 — Redistribution API

Extends the P1.0 FastAPI. New endpoints:

# Warehouse reads (authoritative, not cached upstream calls)
GET  /warehouse/daily?code=600519.SH&start=2026-01-01&end=2026-04-30
                                       → {rows: [...], source: "curated"}
GET  /warehouse/fundamentals?code=600519.SH&period=2025Q4
                                       → {code, period, claims: [...], source: "curated"}
GET  /warehouse/features?code=600519.SH&date=2026-04-30
                                       → {code, date, features: {...}, source: "feature"}

# Data quality
GET  /quality/status                     → {sources: {...}, last_checks: [...]}
GET  /quality/logs?severity=warning      → [{check_name, code, message, ...}]

# Admin (internal)
POST /admin/ingest/run?source=tushare    → trigger manual ingest
POST /admin/ingest/backfill              → trigger pytdx3 backfill

All warehouse reads go through the Curated layer by default. Callers can request ?layer=raw or ?layer=normalized for debugging.

A.8 — Mac Mini ↔ Vultr Split

HostRole
Mac Mini (home, 48GB RAM)Warehouse PRIMARY — writes + nightly batch (pytdx3 backfill, daily fills). Heavy batch ops, no user-facing latency requirement.
Vultr (cloud, 4GB RAM)Warehouse REPLICA (rsync'd from Mac Mini nightly) + FastAPI serving + all Hermes gateways. Read-only DuckDB replica for low-latency serving.
Mac Mini (PRIMARY - writes)
  └── DuckDB warehouse (full)

         └── nightly rsync ──► Vultr (REPLICA - reads)
                                   └── FastAPI reads from replica

A.9 — Files Created / Modified (Initiative A)

FileAction
twilight-drive/src/data/ingest/scheduler.pyNEW
twilight-drive/src/data/ingest/tushare.pyNEW
twilight-drive/src/data/ingest/pytdx3.pyNEW
twilight-drive/src/data/ingest/akshare.pyNEW
twilight-drive/src/data/ingest/yahoo.pyNEW
twilight-drive/src/data/quality/checks.pyNEW
twilight-drive/src/data/quality/rules.pyNEW
twilight-drive/src/data/warehouse/schemas.pyNEW
twilight-drive/src/data/warehouse/loader.pyNEW
twilight-drive/src/service/routes/warehouse.pyNEW
twilight-drive/src/service/routes/quality.pyNEW
twilight-drive/scripts/backfill_pytdx3.pyNEW
twilight-drive/scripts/admin.pyMODIFY (add warehouse admin commands)
twilight-drive/tests/test_ingest_tushare.pyNEW
twilight-drive/tests/test_quality_checks.pyNEW
twilight-drive/tests/test_warehouse_routes.pyNEW

A.10 — Success Criteria (Initiative A)

  • [ ] Daily Tushare ingest runs at 18:30 → 5,000 stocks in DuckDB within 5 min
  • [ ] GET /warehouse/daily?code=600519.SH returns curated data in < 10ms
  • [ ] Cross-source check: Tushare vs pytdx3 for 600519 close → diff < 0.01% → no warning
  • [ ] Anomaly: inject a row with close = 0 → quality check flags it as error
  • [ ] Freshness: disable Tushare ingest for 24h → freshness check reports stale
  • [ ] Mac Mini → Vultr rsync completes nightly, replica readable by FastAPI

Initiative B: Profile Replication & Ownership

B.1 — Problem

Hermes profiles live in ~/.hermes/profiles/<name>/. Current issues:

  • No version control: Profile configs change, no history of who changed what
  • No push/pull sync: Mac Mini ↔ VPS profiles diverge; manual copy required
  • No ownership model: Anyone with filesystem access can modify any profile
  • No template system: Provisioner clones from a hardcoded directory, no formal templates

B.2 — Architecture

┌──────────────────────────────────────────────────┐
│              Profile Registry                     │
│              (bare git repo)                      │
│              /var/lib/hermes-profiles/            │
│                                                   │
│  origin/                                          │
│  ├── profiles/                                    │
│  │   ├── template-stock-research-pro/             │
│  │   ├── template-stock-research-lite/            │
│  │   ├── template-crypto/                         │
│  │   ├── user-abc123/                             │
│  │   └── user-def456/                             │
│  ├── OWNERS                                       │
│  └── README.md                                    │
│                                                   │
│  Every change = git commit (versioned, auditable) │
└──────────────────────────────────────────────────┘
         │                      │
    git pull              git push
         │                      │
    ┌────┴────┐          ┌─────┴──────┐
    │ Mac Mini │          │   Vultr    │
    │ (local)  │          │  (local)   │
    └─────────┘          └────────────┘

B.3 — Git-Backed Storage

Bare repo setup:

bash
# On Vultr (central registry)
mkdir -p /var/lib/hermes-profiles
cd /var/lib/hermes-profiles
git init --bare

# On Mac Mini (working copy)
git clone vultr:/var/lib/hermes-profiles ~/hermes-profiles-registry
cd ~/hermes-profiles-registry

# Migrate existing profiles
cp -r ~/.hermes/profiles/stock-research-agent profiles/stock-research-agent
cp -r ~/.hermes/profiles/main profiles/main
git add -A
git commit -m "migrate existing profiles to git-backed storage"
git push origin main

B.4 — Ownership Model

OWNERS file at repo root:

# Profile ownership registry
# Format: profile_name | owner_id | verified_by | claimed_at
# ---
stock-research-agent | liang | telegram:5838121981 | 2026-04-30
main | liang | telegram:5838121981 | 2026-04-10
user-abc123 | user-abc123 | payment:pay_xxxxx | 2026-05-15

Claim flow:

hermes profile claim stock-research-agent
  → verifies identity (Telegram ID or payment proof)
  → writes to OWNERS file
  → git commit + push
  → "Profile stock-research-agent claimed by liang"

B.5 — CLI: hermes-profile-manager

hermes-profile-manager
  ├── claim    <profile-name>     Claim ownership of a profile
  ├── push                        Push local profile changes to registry
  ├── pull                        Pull latest profile configs from registry
  ├── sync                        Pull + resolve conflicts + push
  ├── list                        List all profiles + ownership status
  ├── show     <profile-name>     Show profile config diff vs last commit
  ├── template
  │   ├── list                    List available templates
  │   ├── create <name>           Create template from current profile
  │   └── apply  <name> <target>  Clone template into new profile
  └── ownership
      ├── list                    Show ownership registry
      └── transfer <from> <to>    Transfer ownership (requires auth from both)

B.6 — Template System

Template structure:

profiles/template-stock-research-pro/
├── config.yaml.template           # config with {{PLACEHOLDER}} variables
├── SOUL.md                        # identity (shared across all stock profiles)
├── AGENTS.md                      # research methodology (shared)
├── CLAUDE.md                      # project instructions (shared)
├── secrets.schema.json            # required secrets schema
│   {
│     "TWILIGHT_API_TOKEN": {"type": "string", "generated": true},
│     "TUSHARE_TOKEN": {"type": "string", "from_pool": "tushare"},
│     "WEIXIN_HOME_CHANNEL": {"type": "string", "generated": true},
│   }
└── crontab.json                   # default cron jobs

Provisioner clone flow:

python
async def clone_profile(template: str, profile_name: str, user_id: UUID) -> str:
    """Clone a template into a new user profile."""
    
    # 1. Copy template directory
    template_dir = REGISTRY / "profiles" / template
    target_dir = REGISTRY / "profiles" / profile_name
    shutil.copytree(template_dir, target_dir)
    
    # 2. Fill placeholders in config.yaml.template
    config = load_template(target_dir / "config.yaml.template")
    config["model"]["api_key"] = await get_key_from_pool("siliconflow")
    config["platforms"]["weixin"]["home_channel"] = generate_ilink_channel()
    save_config(target_dir / "config.yaml", config)
    
    # 3. Generate secrets
    secrets = load_secrets_schema(target_dir / "secrets.schema.json")
    for key, schema in secrets.items():
        if schema.get("generated"):
            plant_secret(target_dir, key, secrets.token_urlsafe(32))
        elif schema.get("from_pool"):
            plant_secret(target_dir, key, await get_key_from_pool(schema["from_pool"]))
    
    # 4. Git commit
    subprocess.run(["git", "add", profile_name])
    subprocess.run(["git", "commit", "-m", f"provision: {profile_name} from {template}"])
    subprocess.run(["git", "push"])
    
    # 5. Register ownership
    append_owner(profile_name, user_id)
    
    return profile_name

B.7 — Multi-Host Sync

┌─────────────────────────────────────────────────────────────┐
│                     Sync Flow                               │
│                                                             │
│  Mac Mini (working copy)          Vultr (bare repo)        │
│                                                             │
│  hermes-profile-manager push ──────────────► git receive   │
│                                                             │
│  hermes-profile-manager pull ◄────────────── git fetch     │
│                                                             │
│  hermes-profile-manager sync (pull → merge → push)         │
│                                                             │
│  On conflict:                                               │
│    - 3-way merge for YAML files (automerge safe)           │
│    - SOUL.md / AGENTS.md: ours (local) wins, theirs logged  │
│    - config.yaml: manual resolution required, abort push    │
└─────────────────────────────────────────────────────────────┘

B.8 — Files Created / Modified (Initiative B)

FileAction
hermes-profile-manager/cli.pyNEW
hermes-profile-manager/claim.pyNEW
heriles-profile-manager/push_pull.pyNEW
hermes-profile-manager/ownership.pyNEW
hermes-profile-manager/templates.pyNEW
hermes-profile-manager/secrets.pyNEW
hermes-profile-manager/sync.pyNEW
hermes-profile-manager/pyproject.tomlNEW
profiles/template-stock-research-pro/NEW dir
profiles/template-stock-research-pro/config.yaml.templateNEW
profiles/template-stock-research-pro/secrets.schema.jsonNEW
profiles/template-stock-research-pro/crontab.jsonNEW
profiles/template-crypto/NEW dir
profiles/OWNERSNEW
/var/lib/hermes-profiles/ (bare repo)NEW
twilight-drive/src/provisioner/hermes.pyMODIFY (use profile-manager clone)

B.9 — Success Criteria (Initiative B)

  • [ ] hermes-profile-manager list shows all profiles + ownership
  • [ ] hermes-profile-manager claim stock-research-agent → ownership recorded in OWNERS
  • [ ] hermes-profile-manager push → git commit + push to bare repo
  • [ ] hermes-profile-manager pull → git fetch + merge, local profiles updated
  • [ ] Provisioner clones from template → new profile with filled placeholders → pushed to registry
  • [ ] Mac Mini ↔ Vultr sync resolves YAML merges automatically, flags config.yaml conflicts
  • [ ] Second webhook for same user → idempotent (no duplicate profile)

Initiative C: Token Gateway — New-API + SearXNG

C.1 — Problem

Both Hermes profiles currently share API keys, causing:

  • HTTP 429 rate limits (profiles competing for same SiliconFlow key)
  • No usage tracking or guardrails
  • No automatic fallback when a key is exhausted
  • API keys exposed in config files (not managed centrally)
  • Web search depends on DashScope (1000/day cap) + Google CSE ($5/1000 queries)

C.2 — LLM Gateway: New-API

Recommended: New-API — active fork of One-API, web UI, channel-based key pooling.

┌──────────────────────────────────────────────────────────┐
│                    New-API Gateway                       │
│                    (Docker on Vultr)                     │
│                                                          │
│  Channels:                                               │
│  ┌────────────────────────────────────────────────────┐  │
│  │ #1 SiliconFlow key-abc  [active]   45% used  ¥225  │  │
│  │ #2 SiliconFlow key-def  [active]   12% used  ¥60   │  │
│  │ #3 SiliconFlow key-ghi  [active]   78% used  ¥390  │  │
│  │ #4 SiliconFlow key-jkl  [disabled] 100% used ¥500  │  │
│  │ #5 DashScope key-mno    [active]   30% used  ¥150  │  │
│  └────────────────────────────────────────────────────┘  │
│                                                          │
│  Routing: Round-robin among healthy channels             │
│  Fallback: Auto-switch on 429/5xx to next healthy        │
│  Guardrails: RPM, TPM, daily budget per channel          │
│                                                          │
│  Output: OpenAI-compatible endpoint                      │
│  http://localhost:3000/v1                                │
└──────────────────────────────────────────────────────────┘


          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
    ┌──────────┐  ┌──────────────┐  ┌──────────┐
    │ Hermes #1│  │ Hermes #2    │  │ Twilight │
    │ (crypto) │  │ (stocks)     │  │ Backend  │
    └──────────┘  └──────────────┘  └──────────┘

C.3 — Web Search: SearXNG

Recommended: SearXNG — privacy-respecting metasearch engine, 70+ engines, self-hosted.

┌──────────────────────────────────────────────────────────┐
│                   SearXNG (Web Search)                   │
│                    (Docker on Vultr)                     │
│                                                          │
│  Enabled engines:                                        │
│  ┌────────────────────────────────────────────────────┐  │
│  │ Chinese:  Baidu  │  Sogou  │  360 Search  │  Bing  │  │
│  │ Global:   Google │  DuckDuckGo  │  Brave  │  Yep   │  │
│  └────────────────────────────────────────────────────┘  │
│                                                          │
│  Features:                                               │
│  - No per-engine API keys needed                         │
│  - Rate limiting + Redis cache built-in                  │
│  - JSON API: /search?q=...&format=json&language=zh-CN   │
│  - Privacy: no query logging, no user tracking           │
│                                                          │
│  Output: http://localhost:8080/search                    │
└──────────────────────────────────────────────────────────┘


          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
    ┌──────────┐  ┌──────────────┐  ┌──────────┐
    │ Hermes #1│  │ Hermes #2    │  │ Twilight │
    │ (crypto) │  │ (stocks)     │  │ Backend  │
    └──────────┘  └──────────────┘  └──────────┘

C.4 — Why SearXNG over DashScope/Google

AspectDashScope + Google CSESearXNG
API keys needed2 (DashScope + Google)0 (self-hosted)
Chinese search qualityGood (DashScope) + limited (Google)Excellent (Baidu + Sogou + 360)
Cost$5/1000 queries above free tierFree (server cost only)
Rate limitsDashScope 1000/day, Google 100/day freeSelf-managed, no hard cap
PrivacyQueries logged by providersNo logging, privacy-first
MaintenanceDepends on upstream availabilityDocker compose, self-managed

C.5 — Docker Compose

yaml
# docker-compose.gateway.yml
version: "3.8"

services:
  new-api:
    image: calciumion/new-api:latest
    container_name: new-api
    restart: always
    ports:
      - "3000:3000"
    environment:
      - SQL_DSN=sqlite:///data/new-api.db
      - REDIS_CONN_STRING=redis://redis:6379
      - SYNC_FREQUENCY=60
    volumes:
      - /var/lib/twilight/new-api:/data
    depends_on:
      - redis

  searxng:
    image: searxng/searxng:latest
    container_name: searxng
    restart: always
    ports:
      - "8080:8080"
    environment:
      - SEARXNG_BASE_URL=http://localhost:8080/
      - SEARXNG_SECRET=$(openssl rand -hex 32)
      - UWSGI_WORKERS=4
      - UWSGI_THREADS=4
    volumes:
      - /var/lib/twilight/searxng:/etc/searxng:rw
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID

  redis:
    image: redis:7-alpine
    container_name: searxng-redis
    restart: always
    command: redis-server --save 30 5 --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - /var/lib/twilight/redis:/data

C.6 — Guardrails Configuration

GuardrailSettingAction
RPM limit (per channel)100 requests/minThrottle → queue → fallback
TPM limit (per channel)500K tokens/minThrottle → queue → fallback
Daily budget (per channel)¥500/dayAuto-disable channel when exhausted
Health checkEvery 5 minTest with simple request, mark unhealthy on failure
Auto-recoveryEvery 30 minRe-enable disabled channels, test if recovered
SearXNG rate limit10 requests/min per IPReturn 429, Twilight backend retries with backoff
SearXNG cache (Redis)256 MB, LRU evictionRepeated queries return cached results

C.7 — Hermes Profile Migration

Each Hermes profile's config.yaml updated:

yaml
# Before:
model:
  provider: siliconflow
  api_key: sk-xxxxx
  model: Qwen/Qwen3.5-27B

# After:
model:
  provider: openai_compatible
  base_url: http://localhost:3000/v1
  api_key: new-api-proxy-key    # single key for all profiles
  model: Qwen/Qwen3.5-27B       # New-API routes to SiliconFlow

SearXNG wired into Twilight backend:

python
# Before (P1.3):
async def search(query: str):
    return await dashscope_search(query)

# After (Phase 2 C):
async def search(query: str):
    return await searxng_search(
        url="http://localhost:8080/search",
        params={"q": query, "format": "json", "language": "zh-CN"}
    )

C.8 — Files Created / Modified (Initiative C)

FileAction
twilight-drive/docker-compose.gateway.ymlNEW
twilight-drive/searxng/settings.ymlNEW
twilight-drive/searxng/limiter.tomlNEW
twilight-drive/scripts/deploy-gateway.shNEW
twilight-drive/scripts/configure-searxng.pyNEW
twilight-drive/scripts/configure-new-api.pyNEW
twilight-drive/src/service/routes/search.pyMODIFY (use SearXNG)
~/.hermes/profiles/stock-research-agent/config.yamlMODIFY (point to New-API)
~/.hermes/config.yamlMODIFY (point to New-API)

C.9 — Success Criteria (Initiative C)

  • [ ] docker compose -f docker-compose.gateway.yml up -d → New-API + SearXNG running
  • [ ] New-API web UI at http://vultr-ip:3000 → 5 channels visible, health status green
  • [ ] curl http://localhost:3000/v1/chat/completions → response via SiliconFlow key #1
  • [ ] Kill key #1 → next request automatically uses key #2 (no error to caller)
  • [ ] Set key #1 daily budget to ¥0 → channel disabled → traffic routes to #2, #3
  • [ ] SearXNG at http://localhost:8080/search?q=600519+业绩&format=json&language=zh-CN → results from Baidu + Sogou
  • [ ] Hermes profiles point to New-API → no more 429 errors from SiliconFlow
  • [ ] Twilight /search uses SearXNG → no more DashScope quota consumption

Phase 2 — Combined Execution Plan

WeekInitiativeTasks
W6ADuckDB schemas, Tushare ingest scheduler, daily batch
W7Apytdx3 backfill, akshare/Yahoo wiring, quality checks, warehouse API
W8BBare git repo, migrate profiles, CLI (claim/push/pull/list), templates
W9CNew-API Docker deploy, add channels, guardrails; SearXNG Docker deploy, configure engines
W9CMigrate Hermes profiles to New-API, Twilight backend to SearXNG
W10AllIntegration testing, load testing, docs updated, monitoring setup

Test Plan (Initiative A)

TestWhat it verifies
test_tushare_daily_ingest5000 stocks ingested into Raw + Normalized within 5 min
test_normalized_schemaAll rows have required columns, correct types
test_curated_adjustmentSplit-adjusted close differs from raw close by adj_factor
test_feature_computationMA5/MA10/MA20/PE/momentum computed correctly
test_cross_source_agreementTushare vs pytdx3 close < 0.01% diff → no warning
test_cross_source_disagreementInject 1% diff → warning logged
test_anomaly_detectionPrice change > 20% → error flagged
test_freshness_checkSource stale > 24h → freshness check reports stale
test_warehouse_route_returns_curatedGET /warehouse/daily → curated layer data
test_warehouse_route_respects_layer_param?layer=raw → raw layer data

Test Plan (Initiative B)

TestWhat it verifies
test_claim_writes_ownerclaim command writes to OWNERS file + git commit
test_claim_idempotentSecond claim by same owner → no duplicate entry
test_claim_rejected_if_ownedClaim by different user → rejected with error
test_push_commits_and_pushespush creates commit + pushes to bare repo
test_pull_fetches_and_mergespull fetches + merges without conflicts
test_sync_resolves_yaml_automergesync auto-merges YAML files
test_sync_flags_config_conflictsync on conflicting config.yaml → aborts, asks for resolution
test_template_clone_fills_placeholdersClone from template → placeholders filled, secrets planted
test_template_clone_idempotentClone same template twice → different profiles, no collision

Test Plan (Initiative C)

TestWhat it verifies
test_new_api_channel_routingRequest → routed to healthy channel, response returned
test_new_api_fallback_on_429Channel returns 429 → next channel tried automatically
test_new_api_budget_exhaustionChannel budget exhausted → disabled, traffic rerouted
test_new_api_health_checkHealth check fails → channel marked unhealthy
test_searxng_search_returns_resultsSearch query → results from multiple engines
test_searxng_chinese_qualityChinese query → results from Baidu + Sogou + 360
test_searxng_cache_hitRepeat query → cached result, no upstream call
test_searxng_rate_limit> 10 req/min from same IP → 429

Risks & Mitigations

RiskImpactMitigation
New-API upstream breaking changesHighPin Docker image tag, test before upgrade
SearXNG engine changes (upstream API breaks)MediumMonitor SearXNG engine test results page, swap broken engines
DuckDB concurrent read/write locksMediumMac Mini is sole writer; Vultr reads from rsync replica
Git merge conflicts on profilesLowYAML files auto-merge; config.yaml conflicts flagged for manual resolution
SiliconFlow key pool exhaustionMediumAdd fallback channel (DashScope via New-API model routing)
SearXNG IP ban by search enginesMediumRotate user-agent, add delays, use proxy if needed

Out of Scope (Explicit)

  • Real-time data streaming (P3)
  • Multi-tenant Kubernetes orchestration
  • Per-user dashboards (P2 content scope)
  • Research report PDF ingestion (P2 content scope)
  • Stripe integration (P2 payment scope, separate spec)
  • Usage caps enforcement (soft logging only in Phase 2)

团队内部文档