forked from niuniu/llm-intelligence
2224 lines
76 KiB
Markdown
2224 lines
76 KiB
Markdown
# LLM Intelligence Hub — 技术设计文档 v1.1
|
||
|
||
> 文档版本:v1.1
|
||
> 日期:2026-05-09
|
||
> 负责人:宰相(AI 辅助)
|
||
> 状态:Phase 1 执行中,技术栈已修正为 Go+PostgreSQL
|
||
|
||
---
|
||
|
||
## 一、系统架构概览
|
||
|
||
### 1.1 整体架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────┐
|
||
│ LLM Intelligence Hub │
|
||
├──────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
|
||
│ │ 报告 │ │ Web UI │ │ AI Agent / MCP Client │ │
|
||
│ │ Phase 2才推送│ │ (Explorer+报告) │ │ (REST API / MCP) │ │
|
||
│ └──────┬──────┘ └──────┬──────┘ └────────────┬────────────┘ │
|
||
│ │ │ │ │
|
||
│ ┌──────▼──────────────────▼────────────────────────▼────────────┐ │
|
||
│ │ Service Layer (Go) │ │
|
||
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │ │
|
||
│ │ │ Report │ │ API │ │ Scheduler │ │ Notifier │ │ │
|
||
│ │ │ Generator │ │ Server │ │ (cron) │ │ (告警) │ │ │
|
||
│ │ └────────────┘ └────────────┘ └────────────┘ └──────────┘ │ │
|
||
│ └───────────────────────────┬────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌───────────────────────────▼────────────────────────────────────┐ │
|
||
│ │ Data Access Layer (database/sql + pq) │ │
|
||
│ └───────────────────────────┬────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌───────────────────────────▼────────────────────────────────────┐ │
|
||
│ │ Storage Layer (PostgreSQL) │ │
|
||
│ └────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Data Collection Layer │ │
|
||
│ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │
|
||
│ │ │ OpenRouter │ │ Phase 2才扩充厂商/中转平台 │ │ 中转平台采集器 │ │ │
|
||
│ │ │ Collector │ │ (10家厂商) │ │ (硅基流动等) │ │ │
|
||
│ │ └─────────────┘ └──────────────┘ └──────────────────────┘ │ │
|
||
│ └────────────────────────────────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.2 各层职责
|
||
|
||
| 层级 | 职责 | 技术选型 |
|
||
| **数据采集层** | 从 OpenRouter 抓取模型元数据、定价 | **Go** + `net/http` + `encoding/json` |
|
||
| **存储层** | 结构化数据持久化 | PostgreSQL(与立交桥技术栈统一) |
|
||
| **服务层** | 报告生成、API 服务、调度 | **Go 1.22.2**(采集器+报告生成 CLI);API Server Phase 2 评估 |
|
||
| **前端层** | 静态 Web 页面展示(Explorer / 报告) | React/Vite(TypeScript)或纯静态 HTML |
|
||
|
||
### 1.3 技术架构决策(与立交桥技术栈统一)
|
||
|
||
**核心约束**:与立交桥技术栈保持一致,Phase 1 直接使用 PostgreSQL。
|
||
|
||
| 决策 | 选型 | 理由 |
|
||
| 数据库 | **PostgreSQL** | 与立交桥统一;支持 JSONB/数组类型 |
|
||
| 采集语言 | **Go** | 与立交桥统一;静态编译、单二进制部署、并发性能优 |
|
||
| API 框架 | **Phase 2 评估** | Phase 1 暂无 API 服务需求(直写 DB + 静态页面) |
|
||
| 前端 | **React/Vite 或纯静态 HTML** | 视部署方式定;Phase 1 最小可用 |
|
||
| HTTP 客户端 | `net/http`(标准库) | Go 原生,无需第三方依赖 |
|
||
| 数据库驱动 | `github.com/lib/pq` | 最成熟的 PostgreSQL Go 驱动 |
|
||
| 调度 | **系统 cron + Go binary** | `go build` 产出单二进制,cron 直接调用 |
|
||
| 日志/监控 | **标准库 log + 文件输出** | Go 标准库够用,Phase 2 再接入结构化日志 |
|
||
| 告警 | **Phase 2** | 钉钉/飞书 Webhook 推送 |
|
||
|
||
**Phase 1 单机部署拓扑**:
|
||
```
|
||
Phase 1 单机部署
|
||
├── PostgreSQL DB
|
||
├── fetch_openrouter(Go binary,cron 触发,直写 DB)
|
||
├── generate_daily_report(Go binary,Markdown 输出到 reports/daily/)
|
||
└── frontend/(静态页面,CDN 或本地 nginx 托管)
|
||
```
|
||
|
||
---
|
||
|
||
## 二、技术选型详解
|
||
|
||
### 2.1 语言与运行时
|
||
|
||
| 组件 | 选型 | 版本 | 依据 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| 主力语言 | **Go** | 1.22.2 | 与立交桥统一;静态编译单二进制部署;并发采集性能优;标准库完备 |
|
||
| 前端 | **Vanilla JS / React** | — | Phase 1 最小可用;React 用于 Explorer 复杂交互 |
|
||
|
||
### 2.2 框架与工具库
|
||
|
||
| 用途 | 库/工具 | 用途说明 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| HTTP 请求 | `net/http`(标准库) | 数据采集主库,无需第三方依赖 |
|
||
| JSON 解析 | `encoding/json`(标准库) | OpenRouter API 响应反序列化 |
|
||
| 数据库 | `database/sql` + `github.com/lib/pq` | PostgreSQL 连接与查询 |
|
||
| 报告生成 | Go `text/template` / `html/template` | Markdown/HTML 报告模板渲染 |
|
||
| 图表 | **ECharts**(前端) | 浏览器端可视化(价格趋势/排行榜) |
|
||
| 调度 | **系统 cron** | Go binary 直接由 cron 调用 |
|
||
| 日志 | `log` 标准库 | 简单文件输出,Phase 2 评估 zap/slog |
|
||
| 日期处理 | `time` 标准库 | Go 原生日期解析与格式化 |
|
||
| 货币换算 | **Phase 2** | USD/CNY/EUR 汇率获取(当前仅记录原始币种) |
|
||
| 测试 | `testing` + `net/http/httptest` | Go 标准库测试框架 |
|
||
|
||
### 2.3 数据库
|
||
|
||
**Phase 1:PostgreSQL**
|
||
- 与立交桥技术栈统一
|
||
- 利用 PostgreSQL JSONB 存储灵活字段(如 capabilities 数组)
|
||
- Schema 设计直接以 PostgreSQL 语法编写(见 `db/migrations/`)
|
||
- Phase 2 评估是否需要数据库内任务队列(当前用 cron 直接触发 binary)
|
||
|
||
### 2.4 为什么不用这些
|
||
|
||
| 未选方案 | 原因 |
|
||
|
||
|
||
|
||
|
||
| SQLite | 不符合与立交桥技术栈统一的要求 |
|
||
| FastAPI | 技术栈已统一为 Go;Python 框架需要运行时和依赖管理,部署复杂度高于 Go 单二进制 |
|
||
| Scrapy | 重量级框架,Phase 1 采集规模不需要分布式 |
|
||
| Celery + RabbitMQ | 增加运维复杂度,当前用 cron + Go binary 替代 |
|
||
| React/Vue | Phase 1 使用静态页面或最小 React 构建;部署复杂度可控 |
|
||
| Deno/Bun | 生态不如 Go 成熟,与立交桥技术栈不一致 |
|
||
|
||
---
|
||
|
||
## 三、数据库设计(DDL)
|
||
|
||
> 以下 DDL 以 PostgreSQL 语法编写,与立交桥技术栈统一。
|
||
|
||
### 3.1 model_provider(模型商)
|
||
|
||
```sql
|
||
CREATE TABLE model_provider (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
name TEXT NOT NULL UNIQUE, -- "OpenAI", "百度", "DeepSeek"
|
||
name_cn TEXT, -- 中文名:"百度智能云"
|
||
country TEXT NOT NULL, -- "US" / "CN" / "EU"
|
||
website TEXT, -- 官网 URL
|
||
founded_year INTEGER, -- 成立年份
|
||
description TEXT, -- 简介
|
||
logo_url TEXT, -- 厂商 Logo
|
||
status TEXT NOT NULL DEFAULT 'active', -- active / deprecated
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
created_by TEXT DEFAULT 'system', -- 创建者审计
|
||
updated_by TEXT DEFAULT 'system', -- 更新者审计
|
||
deleted_at TIMESTAMP -- 软删除标记(NULL=未删除)
|
||
);
|
||
|
||
CREATE INDEX idx_provider_country ON model_provider(country);
|
||
CREATE INDEX idx_provider_status ON model_provider(status);
|
||
CREATE INDEX idx_provider_deleted ON model_provider(deleted_at);
|
||
```
|
||
|
||
### 3.2 model(模型)
|
||
|
||
```sql
|
||
CREATE TABLE model (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
provider_id INTEGER NOT NULL,
|
||
name TEXT NOT NULL, -- "GPT-4o", "ERNIE-4.0"
|
||
version TEXT, -- "2025-12", "V3.2"
|
||
modality TEXT NOT NULL, -- text / vision / audio / video / code
|
||
context_length INTEGER NOT NULL DEFAULT 0, -- 上下文窗口,0=未知
|
||
capabilities TEXT, -- JSON数组: ["function_calling","vision"]
|
||
release_date DATE, -- 发布日期
|
||
status TEXT NOT NULL DEFAULT 'active', -- active / deprecated / discontinued
|
||
parent_model_id INTEGER, -- 父模型ID(区分 Turbo/Lite 变体)
|
||
elo_score REAL, -- ELO 分数(OpenRouter)
|
||
benchmark_scores TEXT, -- JSON: {"mmlu": 88.5, "humaneval": 90.2}
|
||
source_url TEXT, -- 来源 URL
|
||
data_confidence TEXT DEFAULT 'official', -- official / inferred / unverified / stale
|
||
retrieved_at TIMESTAMP, -- 数据抓取时间(数据新鲜度)
|
||
batch_id TEXT, -- 采集批次号(血缘追踪)
|
||
collector_version TEXT DEFAULT 'v1.0', -- 采集器版本
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
created_by TEXT DEFAULT 'system',
|
||
updated_by TEXT DEFAULT 'system',
|
||
deleted_at TIMESTAMP,
|
||
FOREIGN KEY (provider_id) REFERENCES model_provider(id) ON DELETE CASCADE,
|
||
UNIQUE(provider_id, name, version)
|
||
);
|
||
|
||
CREATE INDEX idx_model_provider ON model(provider_id);
|
||
CREATE INDEX idx_model_modality ON model(modality);
|
||
CREATE INDEX idx_model_status ON model(status);
|
||
CREATE INDEX idx_model_name ON model(name);
|
||
CREATE INDEX idx_model_deleted ON model(deleted_at);
|
||
CREATE INDEX idx_model_retrieved ON model(retrieved_at);
|
||
CREATE INDEX idx_model_confidence ON model(data_confidence);
|
||
```
|
||
|
||
### 3.3 operator(运营商/云平台)
|
||
|
||
```sql
|
||
CREATE TABLE operator (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
name TEXT NOT NULL UNIQUE, -- "AWS Bedrock", "硅基流动"
|
||
name_cn TEXT, -- 中文名
|
||
type TEXT NOT NULL, -- cloud / reseller / official
|
||
country TEXT NOT NULL, -- 运营主体国籍
|
||
website TEXT, -- 控制台地址
|
||
api_endpoint TEXT, -- API 基础 URL
|
||
auth_type TEXT NOT NULL, -- api_key / oauth / sts
|
||
is_cn_accessible BOOLEAN DEFAULT 1, -- 国内是否可访问
|
||
stability_grade TEXT DEFAULT 'B', -- A/B/C/D 稳定性评级
|
||
status TEXT NOT NULL DEFAULT 'active', -- active / deprecated
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
created_by TEXT DEFAULT 'system',
|
||
updated_by TEXT DEFAULT 'system',
|
||
deleted_at TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX idx_operator_type ON operator(type);
|
||
CREATE INDEX idx_operator_country ON operator(country);
|
||
CREATE INDEX idx_operator_deleted ON operator(deleted_at);
|
||
```
|
||
|
||
### 3.4 region_pricing(区域定价)
|
||
|
||
```sql
|
||
CREATE TABLE region_pricing (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
operator_id INTEGER NOT NULL,
|
||
model_id INTEGER NOT NULL,
|
||
region TEXT NOT NULL DEFAULT 'GLOBAL', -- CN / US / EU / GLOBAL
|
||
currency TEXT NOT NULL, -- CNY / USD / EUR
|
||
input_price_per_mtok REAL NOT NULL, -- 元/百万Token
|
||
output_price_per_mtok REAL NOT NULL,
|
||
unit TEXT DEFAULT 'per_mtok', -- per_mtok / per_1k / per_token
|
||
free_tier_id INTEGER, -- 关联 free_tier 表
|
||
rate_limit TEXT, -- JSON: {"rpm": 60, "tpm": 100000}
|
||
free_limitations TEXT, -- JSON数组: ["仅限国内IP","新用户专享"]
|
||
last_updated DATE NOT NULL,
|
||
source_url TEXT,
|
||
data_confidence TEXT DEFAULT 'official', -- official / inferred / expired
|
||
retrieved_at TIMESTAMP, -- 数据抓取时间
|
||
batch_id TEXT, -- 采集批次号
|
||
collector_version TEXT DEFAULT 'v1.0',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
created_by TEXT DEFAULT 'system',
|
||
updated_by TEXT DEFAULT 'system',
|
||
deleted_at TIMESTAMP,
|
||
FOREIGN KEY (operator_id) REFERENCES operator(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (model_id) REFERENCES model(id) ON DELETE CASCADE,
|
||
UNIQUE(operator_id, model_id, region, currency)
|
||
);
|
||
|
||
CREATE INDEX idx_pricing_operator ON region_pricing(operator_id);
|
||
CREATE INDEX idx_pricing_model ON region_pricing(model_id);
|
||
CREATE INDEX idx_pricing_region ON region_pricing(region);
|
||
CREATE INDEX idx_pricing_currency ON region_pricing(currency);
|
||
CREATE INDEX idx_pricing_input_cost ON region_pricing(input_price_per_mtok);
|
||
CREATE INDEX idx_pricing_deleted ON region_pricing(deleted_at);
|
||
CREATE INDEX idx_pricing_retrieved ON region_pricing(retrieved_at);
|
||
```
|
||
|
||
### 3.5 pricing_history(价格历史)
|
||
|
||
```sql
|
||
CREATE TABLE pricing_history (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
region_pricing_id INTEGER NOT NULL,
|
||
model_id INTEGER NOT NULL,
|
||
operator_id INTEGER NOT NULL,
|
||
region TEXT NOT NULL,
|
||
currency TEXT NOT NULL,
|
||
old_input_price REAL,
|
||
new_input_price REAL NOT NULL,
|
||
old_output_price REAL,
|
||
new_output_price REAL NOT NULL,
|
||
change_pct REAL, -- 变动百分比(自动计算)
|
||
change_type TEXT NOT NULL, -- increase / decrease / new_model / discontinued
|
||
recorded_at DATE NOT NULL, -- 记录日期
|
||
source_url TEXT,
|
||
batch_id TEXT, -- 采集批次号
|
||
collector_version TEXT DEFAULT 'v1.0',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (region_pricing_id) REFERENCES region_pricing(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (model_id) REFERENCES model(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (operator_id) REFERENCES operator(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX idx_history_model ON pricing_history(model_id);
|
||
CREATE INDEX idx_history_operator ON pricing_history(operator_id);
|
||
CREATE INDEX idx_history_recorded ON pricing_history(recorded_at);
|
||
CREATE INDEX idx_history_change_type ON pricing_history(change_type);
|
||
|
||
-- Phase 2: 按 recorded_at 分区,提升历史查询性能
|
||
-- CREATE TABLE pricing_history_partitioned (...) PARTITION BY RANGE (recorded_at);
|
||
```
|
||
|
||
### 3.6 free_tier(免费政策)
|
||
|
||
```sql
|
||
CREATE TABLE free_tier (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
operator_id INTEGER NOT NULL,
|
||
model_id INTEGER, -- NULL表示该平台全部免费额度
|
||
free_model_name TEXT, -- 免费模型名称(展示用)
|
||
quota_type TEXT NOT NULL, -- daily / monthly / one_time / unlimited
|
||
quota_amount REAL, -- 配额数量
|
||
quota_unit TEXT, -- requests / tokens / minutes
|
||
tpm_limit INTEGER, -- tokens per minute 限制
|
||
rpm_limit INTEGER, -- requests per minute 限制
|
||
daily_req_limit INTEGER, -- 每日请求上限
|
||
monthly_req_limit INTEGER, -- 每月请求上限
|
||
token_limit_per_req INTEGER, -- 单次请求Token上限
|
||
requires_credit_card BOOLEAN DEFAULT 0, -- 是否需要绑定信用卡
|
||
requires_verification BOOLEAN DEFAULT 0, -- 是否需要实名认证
|
||
region_restrictions TEXT, -- JSON: ["仅限部分地区"]
|
||
applicable_scenarios TEXT, -- JSON: ["仅限新用户"]
|
||
special_notes TEXT, -- 特殊说明
|
||
effective_from DATE,
|
||
effective_until DATE, -- NULL表示长期有效
|
||
last_updated DATE,
|
||
source_url TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
created_by TEXT DEFAULT 'system',
|
||
updated_by TEXT DEFAULT 'system',
|
||
deleted_at TIMESTAMP,
|
||
FOREIGN KEY (operator_id) REFERENCES operator(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (model_id) REFERENCES model(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_free_operator ON free_tier(operator_id);
|
||
CREATE INDEX idx_free_model ON free_tier(model_id);
|
||
CREATE INDEX idx_free_quota_type ON free_tier(quota_type);
|
||
CREATE INDEX idx_free_deleted ON free_tier(deleted_at);
|
||
```
|
||
|
||
### 3.7 daily_report(每日报告)
|
||
|
||
```sql
|
||
CREATE TABLE daily_report (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
report_date DATE NOT NULL UNIQUE,
|
||
new_models TEXT, -- JSON数组:新上线模型
|
||
price_changes TEXT, -- JSON数组:价格变动
|
||
free_changes TEXT, -- JSON数组:免费政策变更
|
||
top_recommendations TEXT, -- JSON对象:场景推荐
|
||
cost_alerts TEXT, -- JSON数组:成本告警
|
||
html_content TEXT, -- 完整HTML报告内容
|
||
summary_md TEXT, -- Markdown摘要
|
||
status TEXT NOT NULL DEFAULT 'generated', -- generated / failed / partial
|
||
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
error_message TEXT,
|
||
created_by TEXT DEFAULT 'system',
|
||
updated_by TEXT DEFAULT 'system'
|
||
);
|
||
|
||
CREATE INDEX idx_report_date ON daily_report(report_date);
|
||
CREATE INDEX idx_report_status ON daily_report(status);
|
||
```
|
||
|
||
### 3.8 user_subscription(用户订阅)
|
||
|
||
```sql
|
||
CREATE TABLE user_subscription (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
user_id TEXT NOT NULL, -- 统一用户ID
|
||
email TEXT,
|
||
phone TEXT,
|
||
subscription_tier TEXT NOT NULL DEFAULT 'free', -- free / pro / team / enterprise
|
||
subscription_start DATE,
|
||
subscription_end DATE,
|
||
notify_channels TEXT, -- JSON: ["feishu","email","dingtalk"]
|
||
feishu_webhook TEXT,
|
||
dingtalk_webhook TEXT,
|
||
email_webhook TEXT,
|
||
model_watchlist TEXT, -- JSON数组:关注模型
|
||
operator_watchlist TEXT, -- JSON数组:关注平台
|
||
price_alert_threshold REAL DEFAULT 10.0, -- 告警阈值(%)
|
||
monthly_token_limit INTEGER, -- 月度Token限制
|
||
monthly_token_used INTEGER DEFAULT 0,
|
||
stripe_customer_id TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
created_by TEXT DEFAULT 'system',
|
||
updated_by TEXT DEFAULT 'system',
|
||
deleted_at TIMESTAMP,
|
||
UNIQUE(email)
|
||
);
|
||
|
||
CREATE INDEX idx_sub_user ON user_subscription(user_id);
|
||
CREATE INDEX idx_sub_tier ON user_subscription(subscription_tier);
|
||
CREATE INDEX idx_sub_deleted ON user_subscription(deleted_at);
|
||
```
|
||
|
||
---
|
||
|
||
## 四、API 设计
|
||
|
||
### 4.1 内部采集 API(Collector → Server)
|
||
|
||
#### POST /api/v1/collect/push
|
||
采集器推送采集结果(Phase 2 分布式采集节点使用)
|
||
|
||
**Request:**
|
||
```json
|
||
{
|
||
"batch": [
|
||
{
|
||
"provider_name": "OpenAI",
|
||
"model_name": "GPT-4o",
|
||
"version": "2025-01",
|
||
"operator_name": "OpenRouter",
|
||
"region": "GLOBAL",
|
||
"currency": "USD",
|
||
"input_price": 2.50,
|
||
"output_price": 10.0,
|
||
"context_length": 128000,
|
||
"capabilities": ["vision", "function_calling", "json_mode"],
|
||
"free_tier": null,
|
||
"source_url": "https://openrouter.ai/api/v1/models"
|
||
}
|
||
],
|
||
"collected_at": "2026-05-04T08:00:00+08:00"
|
||
}
|
||
```
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"inserted": 365,
|
||
"updated": 12,
|
||
"errors": 0
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.1.5 API 安全与生产规范
|
||
|
||
#### 认证与鉴权
|
||
|
||
| 场景 | 机制 | 说明 |
|
||
|------|------|------|
|
||
| **内部采集器 → DB** | 无 API 认证 | 采集器直连 PostgreSQL,通过 DB 用户权限隔离 |
|
||
| **Phase 2 对外 API** | API Key(X-API-Key Header) | 按订阅等级分配不同 Key,DB 存储 bcrypt 哈希 |
|
||
| **Admin 运维接口** | JWT + RBAC | 仅内部使用,Token 有效期 1h,Refresh Token 7d |
|
||
|
||
#### 限流策略
|
||
|
||
| 订阅等级 | QPS | 日调用上限 | 并发连接 |
|
||
|----------|-----|-----------|---------|
|
||
| Free | 2 | 100 | 1 |
|
||
| Pro | 10 | 5,000 | 3 |
|
||
| Team | 50 | 50,000 | 10 |
|
||
| Enterprise | 协商 | 无限 | 协商 |
|
||
|
||
- 限流实现:Token Bucket(内存)+ Redis(分布式,Phase 2)
|
||
- 超限响应:`429 Too Many Requests`,`Retry-After` 头部
|
||
|
||
#### 错误码规范
|
||
|
||
| HTTP Status | 业务码 | 含义 | 示例 |
|
||
|-------------|--------|------|------|
|
||
| 200 | — | 成功 | — |
|
||
| 400 | `BAD_REQUEST` | 参数非法 | `page_size > 100` |
|
||
| 401 | `UNAUTHORIZED` | 认证失败 | API Key 无效/过期 |
|
||
| 403 | `FORBIDDEN` | 权限不足 | Free 用户访问 Team 功能 |
|
||
| 404 | `NOT_FOUND` | 资源不存在 | 模型 ID 不存在 |
|
||
| 429 | `RATE_LIMITED` | 限流触发 | 超出 QPS/日配额 |
|
||
| 500 | `INTERNAL_ERROR` | 服务端错误 | DB 连接失败 |
|
||
| 503 | `SERVICE_UNAVAILABLE` | 维护模式 | 系统升级中 |
|
||
|
||
**错误响应格式:**
|
||
```json
|
||
{
|
||
"error": {
|
||
"code": "RATE_LIMITED",
|
||
"message": "Quota exceeded: 100/100 daily calls used",
|
||
"retry_after": 3600,
|
||
"request_id": "req_abc123"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### CORS 策略
|
||
|
||
```
|
||
Access-Control-Allow-Origin: https://llm-hub.example.com
|
||
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||
Access-Control-Allow-Headers: Content-Type, X-API-Key
|
||
Access-Control-Max-Age: 86400
|
||
```
|
||
|
||
- 生产环境:只允许特定域名
|
||
- 开发环境:`*`(仅本地)
|
||
|
||
#### 请求/响应约束
|
||
|
||
| 约束 | 值 | 说明 |
|
||
|------|-----|------|
|
||
| 请求体大小 | ≤ 1MB | 防止超大 JSON 攻击 |
|
||
| 响应体大小 | ≤ 5MB | 分页控制 |
|
||
| 超时 | 30s | API 请求硬超时 |
|
||
| 参数校验 | 严格 | 未知参数返回 400 |
|
||
|
||
---
|
||
|
||
### 4.2 对外 REST API
|
||
|
||
#### GET /api/v1/models
|
||
查询模型列表
|
||
|
||
**Query Parameters:**
|
||
| 参数 | 类型 | 默认值 | 说明 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| `provider` | string | — | 模型商名称过滤 |
|
||
| `modality` | string | — | text/vision/audio/video/code |
|
||
| `min_context` | int | — | 最小上下文长度 |
|
||
| `max_input_price` | float | — | 最大输入价格(/MTok) |
|
||
| `has_free` | bool | false | 仅显示有免费额的模型 |
|
||
| `search` | string | — | 关键词搜索(模型名/capabilities) |
|
||
| `sort` | string | `input_price` | 排序字段 |
|
||
| `order` | string | `asc` | asc/desc |
|
||
| `page` | int | 1 | 页码 |
|
||
| `page_size` | int | 20 | 每页数量(max 100) |
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"total": 523,
|
||
"page": 1,
|
||
"page_size": 20,
|
||
"models": [
|
||
{
|
||
"id": 42,
|
||
"name": "DeepSeek V4-Flash",
|
||
"provider": "DeepSeek",
|
||
"provider_cn": "深度求索",
|
||
"modality": "text",
|
||
"context_length": 1048576,
|
||
"capabilities": ["function_calling", "json_mode"],
|
||
"status": "active",
|
||
"lowest_price": {
|
||
"operator": "硅基流动",
|
||
"currency": "CNY",
|
||
"input": 0.14,
|
||
"output": 0.028,
|
||
"region": "CN"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### GET /api/v1/models/{id}
|
||
查询单个模型详情
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"id": 42,
|
||
"name": "DeepSeek V4-Flash",
|
||
"provider": {
|
||
"id": 5,
|
||
"name": "DeepSeek",
|
||
"country": "CN"
|
||
},
|
||
"version": "V4-Flash",
|
||
"modality": "text",
|
||
"context_length": 1048576,
|
||
"capabilities": ["function_calling", "json_mode"],
|
||
"release_date": "2026-04-15",
|
||
"status": "active",
|
||
"elo_score": 1382.5,
|
||
"pricing": [
|
||
{
|
||
"operator": "硅基流动",
|
||
"region": "CN",
|
||
"currency": "CNY",
|
||
"input": 0.14,
|
||
"output": 0.028,
|
||
"source_url": "https://siliconflow.cn"
|
||
},
|
||
{
|
||
"operator": "OpenRouter",
|
||
"region": "GLOBAL",
|
||
"currency": "USD",
|
||
"input": 0.02,
|
||
"output": 0.004
|
||
}
|
||
],
|
||
"free_tier": {
|
||
"quota_type": "monthly",
|
||
"quota_amount": 5000000,
|
||
"quota_unit": "tokens",
|
||
"requires_credit_card": false
|
||
}
|
||
}
|
||
```
|
||
|
||
#### GET /api/v1/cost
|
||
成本计算器
|
||
|
||
**Query Parameters:**
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| `input_tokens` | int | 是 | 输入 Token 数 |
|
||
| `output_tokens` | int | 否 | 输出 Token 数(默认=input_tokens×0.3) |
|
||
| `modality` | string | 否 | 模态过滤 |
|
||
| `region` | string | 否 | 区域(CN/US/GLOBAL) |
|
||
| `currency` | string | CNY | 显示货币 |
|
||
| `top_n` | int | 10 | 返回前N个最低价 |
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"input_tokens": 1000000,
|
||
"output_tokens": 300000,
|
||
"currency": "CNY",
|
||
"results": [
|
||
{
|
||
"rank": 1,
|
||
"model": "DeepSeek V4-Flash",
|
||
"provider": "DeepSeek",
|
||
"operator": "硅基流动",
|
||
"input_cost": 0.14,
|
||
"output_cost": 0.0084,
|
||
"total_cost": 0.1484,
|
||
"total_cost_usd": 0.020
|
||
},
|
||
{
|
||
"rank": 2,
|
||
"model": "Kimi K2.5",
|
||
"provider": "Moonshot",
|
||
"operator": "硅基流动",
|
||
"input_cost": 0.23,
|
||
"output_cost": 0.021,
|
||
"total_cost": 0.251,
|
||
"total_cost_usd": 0.034
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### GET /api/v1/recommend
|
||
模型推荐
|
||
|
||
**Query Parameters:**
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| `use_case` | string | 是 | 场景:coding/writing/reasoning/free/vision |
|
||
| `min_context` | int | — | 最小上下文需求 |
|
||
| `budget` | float | — | 预算上限(/MTok input) |
|
||
| `region` | string | CN | 区域偏好 |
|
||
| `limit` | int | 5 | 返回数量 |
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"use_case": "coding",
|
||
"recommendations": [
|
||
{
|
||
"rank": 1,
|
||
"model": "Kimi K2.6",
|
||
"provider": "Moonshot",
|
||
"reason": "SWE-Bench Pro 超越 GPT-5.4,编码能力最强",
|
||
"input_price": 0.95,
|
||
"currency": "CNY",
|
||
"free_option": null
|
||
},
|
||
{
|
||
"rank": 2,
|
||
"model": "GLM-5.1",
|
||
"provider": "智谱",
|
||
"reason": "编码能力接近 Opus 4.6,性价比高",
|
||
"input_price": 1.40,
|
||
"currency": "CNY",
|
||
"free_option": null
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### GET /api/v1/reports
|
||
每日报告列表
|
||
|
||
**Query Parameters:**
|
||
| 参数 | 类型 | 默认值 | 说明 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| `from` | date | 30天前 | 开始日期 |
|
||
| `to` | date | 今天 | 结束日期 |
|
||
| `page` | int | 1 | 页码 |
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"total": 30,
|
||
"reports": [
|
||
{
|
||
"id": 30,
|
||
"report_date": "2026-05-04",
|
||
"status": "generated",
|
||
"summary": "新上线3个模型,价格变动2项,免费政策更新1项",
|
||
"generated_at": "2026-05-04T08:00:45+08:00"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### GET /api/v1/reports/{date}
|
||
获取指定日期报告内容
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"id": 30,
|
||
"report_date": "2026-05-04",
|
||
"html_content": "<html>...</html>",
|
||
"new_models": [
|
||
{"name": "xAI Grok 4.1 Fast", "provider": "xAI", "input_price": 0.20, "currency": "USD"}
|
||
],
|
||
"price_changes": [
|
||
{
|
||
"model": "Claude Opus 4.6",
|
||
"operator": "Anthropic",
|
||
"old_price": 15.0,
|
||
"new_price": 5.0,
|
||
"change_pct": -66.7,
|
||
"currency": "USD"
|
||
}
|
||
],
|
||
"free_changes": [
|
||
{
|
||
"model": "Gemini 2.5 Pro",
|
||
"operator": "Google",
|
||
"change": "免费层下线,需付费使用"
|
||
}
|
||
],
|
||
"top_recommendations": {
|
||
"coding": {"model": "Kimi K2.6", "provider": "Moonshot"},
|
||
"writing": {"model": "GLM-5.1", "provider": "智谱"},
|
||
"free": {"model": "DeepSeek R1", "provider": "DeepSeek"},
|
||
"cheapest": {"model": "Step 3.5 Flash", "provider": "字节"}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### GET /api/v1/health
|
||
健康检查
|
||
|
||
**Response:**
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"version": "1.0.0",
|
||
"db_record_count": {
|
||
"models": 523,
|
||
"providers": 22,
|
||
"operators": 31,
|
||
"pricing_records": 1847
|
||
},
|
||
"last_collect_time": "2026-05-04T08:00:12+08:00",
|
||
"last_report_time": "2026-05-04T08:00:45+08:00"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、数据采集 Pipeline
|
||
|
||
### 5.1 OpenRouter 采集流程
|
||
|
||
```
|
||
┌─────────────────┐
|
||
│ 每日 08:00 │
|
||
│ cron 触发 │
|
||
└────────┬────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ GET https://openrouter.ai/api/v1/models │
|
||
│ Headers: Authorization: Bearer <OPENROUTER_KEY>│
|
||
└────────┬────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ 解析响应 JSON │
|
||
│ 字段映射: │
|
||
│ id → model.name (如 "anthropic/claude-3.5-sonnet")│
|
||
│ name → display_name │
|
||
│ pricing.input * 1e6 → input_price_per_mtok │
|
||
│ pricing.output * 1e6 → output_price_per_mtok│
|
||
│ context_length → context_length │
|
||
│ supported_parameters → capabilities │
|
||
│ opensource → modality (text/vision/etc) │
|
||
└────────┬────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ 识别 provider_name (从 id 前缀提取) │
|
||
│ 示例: "anthropic/claude-3.5-sonnet" → │
|
||
│ provider="Anthropic", model="Claude 3.5 Sonnet"│
|
||
└────────┬────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ Upsert: │
|
||
│ INSERT OR REPLACE INTO model_provider (...) │
|
||
│ INSERT OR REPLACE INTO model (...) │
|
||
│ INSERT OR REPLACE INTO region_pricing (...) │
|
||
└────────┬────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ 检测价格变动: │
|
||
│ SELECT old_price FROM pricing_history │
|
||
│ WHERE model_id = x AND operator_id = y │
|
||
│ IF new_price != old_price: │
|
||
│ INSERT INTO pricing_history (...) │
|
||
│ IF abs(change_pct) > 5%: 标记为高亮变动 │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 5.2 国内厂商采集流程 — Phase 2
|
||
|
||
每个国内厂商独立采集器(`collectors/` 目录),统一接口输出(Go):
|
||
|
||
```go
|
||
// collectors/collector.go
|
||
package collectors
|
||
|
||
import "time"
|
||
|
||
// Collector 所有采集器必须实现的接口
|
||
type Collector interface {
|
||
Name() string
|
||
Collect() ([]CollectedRecord, error)
|
||
Schedule() string // cron 表达式,如 "0 8 * * *"
|
||
Timeout() time.Duration
|
||
RetryCount() int
|
||
}
|
||
```
|
||
|
||
#### 采集器清单(Phase 1)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
#### 统一字段映射
|
||
|
||
每个采集器输出标准化 `CollectedRecord`(Go struct):
|
||
|
||
```go
|
||
type CollectedRecord struct {
|
||
ProviderName string `json:"provider_name"`
|
||
ProviderNameCN string `json:"provider_name_cn"`
|
||
ModelName string `json:"model_name"`
|
||
ModelVersion string `json:"model_version"`
|
||
Modality string `json:"modality"`
|
||
ContextLength int `json:"context_length"`
|
||
Capabilities []string `json:"capabilities"`
|
||
OperatorName string `json:"operator_name"`
|
||
OperatorType string `json:"operator_type"`
|
||
Region string `json:"region"`
|
||
Currency string `json:"currency"`
|
||
InputPricePerMTok float64 `json:"input_price_per_mtok"`
|
||
OutputPricePerMTok float64 `json:"output_price_per_mtok"`
|
||
FreeTier *FreeTierRecord `json:"free_tier,omitempty"`
|
||
SourceURL string `json:"source_url"`
|
||
CollectedAt time.Time `json:"collected_at"`
|
||
}
|
||
```
|
||
|
||
统一 `ProviderMapper`(Go map):
|
||
|
||
```go
|
||
var ProviderNameMap = map[string]struct {
|
||
Provider string
|
||
Model string
|
||
Version string
|
||
}{
|
||
// DeepSeek
|
||
"deepseek-ai/DeepSeek-V3": {Provider: "DeepSeek", Model: "V3.2", Version: "2026-03"},
|
||
"deepseek-ai/DeepSeek-V4": {Provider: "DeepSeek", Model: "V4", Version: "2026-04"},
|
||
"deepseek-ai/DeepSeek-R1": {Provider: "DeepSeek", Model: "R1", Version: "2026-01"},
|
||
// 阿里
|
||
"qwen/Qwen3-VL-32B": {Provider: "阿里云", Model: "Qwen3-VL-32B", Version: "2026-03"},
|
||
"qwen/Qwen3-VL-8B": {Provider: "阿里云", Model: "Qwen3-VL-8B", Version: "2026-03"},
|
||
// Moonshot
|
||
"moonshotai/Kimi-K2.6": {Provider: "Moonshot", Model: "K2.6", Version: "2026-04"},
|
||
"moonshotai/Kimi-K2.5": {Provider: "Moonshot", Model: "K2.5", Version: "2026-03"},
|
||
// 智谱
|
||
"zhipuai/GLM-5.1": {Provider: "智谱", Model: "GLM-5.1", Version: "2026-03"},
|
||
"zhipuai/GLM-4.7": {Provider: "智谱", Model: "GLM-4.7", Version: "2025-12"},
|
||
// ... 其他厂商
|
||
}
|
||
```
|
||
|
||
### 5.3 每日调度设计
|
||
|
||
**调度策略**:系统 cron 统一调度,无外部消息队列依赖。
|
||
|
||
```crontab
|
||
# /etc/crontab
|
||
|
||
# 每日 08:00 触发全量采集 + 报告生成
|
||
0 8 * * * root /opt/llm-hub/scripts/run_daily.sh >> /var/log/llm-hub/daily.log 2>&1
|
||
|
||
# 每日 09:00 触发数据备份
|
||
0 9 * * * root /opt/llm-hub/scripts/backup.sh >> /var/log/llm-hub/backup.log 2>&1
|
||
```
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# run_daily.sh
|
||
|
||
set -e
|
||
|
||
LOG_FILE="/var/log/llm-hub/daily.log"
|
||
HUB_DIR="/opt/llm-hub"
|
||
|
||
log() {
|
||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
|
||
}
|
||
|
||
log "开始每日采集任务"
|
||
|
||
# 1. 采集 OpenRouter(海外模型,优先级最高)
|
||
cd $HUB_DIR
|
||
./scripts/fetch_openrouter >> $LOG_FILE 2>&1
|
||
log "OpenRouter 采集完成"
|
||
|
||
# 2. Phase 2 才并行采集国内厂商(DeepSeek/阿里/Kimi/智谱等)
|
||
|
||
# 3. 生成每日报告
|
||
./scripts/generate_daily_report >> $LOG_FILE 2>&1
|
||
log "日报生成完成"
|
||
|
||
# 4. Phase 2 才检测价格变动并告警
|
||
log "每日任务完成"
|
||
```
|
||
|
||
### 5.4 失败重试 + 告警机制(Go 实现)
|
||
|
||
```go
|
||
// pkg/retry/retry.go
|
||
package retry
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"time"
|
||
)
|
||
|
||
// Do 指数退避重试
|
||
func Do(maxAttempts int, delay time.Duration, backoff float64, fn func() error) error {
|
||
var err error
|
||
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||
if err = fn(); err == nil {
|
||
return nil
|
||
}
|
||
if attempt == maxAttempts {
|
||
return fmt.Errorf("all %d attempts failed: %w", maxAttempts, err)
|
||
}
|
||
wait := time.Duration(float64(delay) * pow(backoff, float64(attempt-1)))
|
||
log.Printf("Attempt %d/%d failed: %v. Retrying in %v...", attempt, maxAttempts, err, wait)
|
||
time.Sleep(wait)
|
||
}
|
||
return err
|
||
}
|
||
|
||
func pow(x, y float64) float64 {
|
||
result := 1.0
|
||
for i := 0; i < int(y); i++ {
|
||
result *= x
|
||
}
|
||
return result
|
||
}
|
||
```
|
||
|
||
**采集器调用示例(Go):**
|
||
```go
|
||
func collectWithRetry(collector collectors.Collector) error {
|
||
return retry.Do(3, 10*time.Second, 2.0, func() error {
|
||
records, err := collector.Collect()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return saveToDB(records)
|
||
})
|
||
}
|
||
```
|
||
|
||
**告警触发逻辑(Go):**
|
||
```go
|
||
func checkAndAlertPriceChange(modelID, operatorID int, newPrice float64) {
|
||
oldPrice := getLastPrice(modelID, operatorID)
|
||
if oldPrice == 0 {
|
||
return // 首次录入,不告警
|
||
}
|
||
|
||
changePct := (newPrice - oldPrice) / oldPrice * 100
|
||
|
||
if math.Abs(changePct) > 10 {
|
||
alertMsg := fmt.Sprintf(
|
||
"⚠️ 价格变动告警\n模型: %s\n平台: %s\n原价: %.4f\n新价: %.4f\n变动: %+.1f%%",
|
||
getModelName(modelID), getOperatorName(operatorID), oldPrice, newPrice, changePct,
|
||
)
|
||
sendFeishuAlert(alertMsg)
|
||
log.Printf("[ALERT] %s", alertMsg)
|
||
}
|
||
}
|
||
```
|
||
|
||
**告警规则:**
|
||
|
||
| 条件 | 动作 |
|
||
|
||
|
||
|
||
|
||
| 单个采集器失败 | 记录日志,保留旧数据,发送低优先级告警 |
|
||
| 连续 3 天同一采集器失败 | 发送高优先级告警(钉钉/飞书) |
|
||
| 价格变动 > 10% | 立即触发告警 |
|
||
| 价格变动 > 20% | 立即触发告警 + 暂停该平台数据(人工确认) |
|
||
| 报告生成失败 | 发送告警,保留前一天报告 |
|
||
| 数据库写入失败 | 立即告警,回滚事务 |
|
||
|
||
---
|
||
|
||
## 六、前端架构
|
||
|
||
### 6.1 技术栈
|
||
|
||
| 组件 | 选型 | 理由 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| **页面框架** | React 18 + Vite + TypeScript | 组件化、类型安全、构建优化 |
|
||
| **图表库** | ECharts 5 + echarts-for-react | 功能全面、中文支持好 |
|
||
| **图标** | Lucide React | 现代化图标、Tree-shaking |
|
||
| **搜索** | 前端 Fuse.js | 轻量模糊搜索、< 100KB |
|
||
| **布局** | Tailwind CSS | 原子化 CSS、响应式、定制灵活 |
|
||
| **构建** | Vite | 快速 HMR、Rollup 打包、生产优化 |
|
||
|
||
### 6.2 页面清单
|
||
|
||
| 页面 | 路径 | 功能说明 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| **首页 / 报告列表** | `/` | 展示最新每日报告入口,显示近期报告摘要 |
|
||
| **报告详情** | `/reports/{date}.html` | 单日报告完整内容(新模型/价格变动/推荐) |
|
||
| **模型浏览器** | `/explorer.html` | 组合筛选 + 卡片/表格视图 + 搜索 |
|
||
| **模型详情** | `/model/{id}.html` | 模型完整信息 + 全平台定价对比 |
|
||
Phase 2|Phase 2 Phase 2~Phase 2~Phase 2*Phase 2*Phase 2成Phase 2本Phase 2计Phase 2算Phase 2器Phase 2*Phase 2*Phase 2~Phase 2~Phase 2 Phase 2|Phase 2 Phase 2`Phase 2/Phase 2cPhase 2aPhase 2lPhase 2cPhase 2uPhase 2lPhase 2aPhase 2tPhase 2oPhase 2rPhase 2.Phase 2hPhase 2tPhase 2mPhase 2lPhase 2`Phase 2 Phase 2|Phase 2 Phase 2TPhase 2oPhase 2kPhase 2ePhase 2nPhase 2 Phase 2用Phase 2量Phase 2 Phase 2→Phase 2 Phase 2多Phase 2平Phase 2台Phase 2成Phase 2本Phase 2对Phase 2比Phase 2排Phase 2行Phase 2 Phase 2|Phase 2
|
||
Phase 2Phase 2|Phase 2 Phase 2~Phase 2~Phase 2*Phase 2*Phase 2趋Phase 2势Phase 2图Phase 2*Phase 2*Phase 2~Phase 2~Phase 2 Phase 2|Phase 2 Phase 2`Phase 2/Phase 2tPhase 2rPhase 2ePhase 2nPhase 2dPhase 2sPhase 2.Phase 2hPhase 2tPhase 2mPhase 2lPhase 2`Phase 2 Phase 2|Phase 2 Phase 2价Phase 2格Phase 2/Phase 2模Phase 2型Phase 2能Phase 2力Phase 2历Phase 2史Phase 2趋Phase 2势Phase 2(Phase 2EPhase 2CPhase 2hPhase 2aPhase 2rPhase 2tPhase 2sPhase 2)Phase 2 Phase 2|Phase 2
|
||
Phase 2| **关于我们** | `/about.html` | 项目介绍、数据来源说明 |
|
||
|
||
### 6.3 与后端的数据交互
|
||
|
||
**模式**:纯前端 SPA(Single Page Application),通过 Fetch API 调用后端 REST API。
|
||
|
||
```
|
||
前端静态文件(Phase 2才 Nginx 托管)
|
||
│
|
||
├── GET /api/v1/models → Go API 返回 JSON
|
||
├── GET /api/v1/models/{id} → 模型详情 JSON
|
||
├── GET /api/v1/cost → 成本计算 JSON
|
||
├── GET /api/v1/recommend → 推荐结果 JSON
|
||
└── GET /api/v1/reports/{date} → 报告 JSON
|
||
```
|
||
|
||
**前端数据层(dataService.js)**:
|
||
|
||
```javascript
|
||
// 统一 API 调用封装
|
||
const API_BASE = '/api/v1';
|
||
|
||
async function apiGet(endpoint, params = {}) {
|
||
const url = new URL(`${API_BASE}${endpoint}`, window.location.origin);
|
||
Object.entries(params).forEach(([k, v]) => v != null && url.searchParams.set(k, v));
|
||
const resp = await fetch(url);
|
||
if (!resp.ok) throw new Error(`API error: ${resp.status}`);
|
||
return resp.json();
|
||
}
|
||
|
||
// 主要接口封装
|
||
const api = {
|
||
models: {
|
||
list: (params) => apiGet('/models', params),
|
||
detail: (id) => apiGet(`/models/${id}`)
|
||
},
|
||
cost: {
|
||
calculate: (params) => apiGet('/cost', params)
|
||
},
|
||
recommend: (params) => apiGet('/recommend', params),
|
||
reports: {
|
||
list: (params) => apiGet('/reports', params),
|
||
get: (date) => apiGet(`/reports/${date}`)
|
||
}
|
||
};
|
||
```
|
||
|
||
### 6.4 模型浏览器页面结构
|
||
|
||
```html
|
||
<!-- explorer.html -->
|
||
|
||
<!-- 筛选栏 -->
|
||
<div class="row mb-3">
|
||
<div class="col-md-2">
|
||
<select id="filter-provider" class="form-select">
|
||
<option value="">全部厂商</option>
|
||
<option value="DeepSeek">DeepSeek</option>
|
||
<option value="阿里云">阿里云</option>
|
||
<!-- ... -->
|
||
</select>
|
||
</div>
|
||
<div class="col-md-2">
|
||
<select id="filter-modality" class="form-select">
|
||
<option value="">全部模态</option>
|
||
<option value="text">文字</option>
|
||
<option value="vision">视觉</option>
|
||
<option value="code">代码</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-2">
|
||
<input type="number" id="filter-max-price" class="form-control"
|
||
placeholder="最大输入价(¥/MT)">
|
||
</div>
|
||
<div class="col-md-3">
|
||
<input type="text" id="search-keyword" class="form-control"
|
||
placeholder="搜索模型名称...">
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="btn-group" role="group">
|
||
<button class="btn btn-outline-primary active" data-view="card">卡片</button>
|
||
<button class="btn btn-outline-primary" data-view="table">表格</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 结果区域 -->
|
||
<div id="results" class="row">
|
||
<!-- 动态渲染卡片或表格 -->
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<nav><ul class="pagination" id="pagination"></ul></nav>
|
||
```
|
||
|
||
---
|
||
|
||
## 十二、快速部署参考(历史版本)
|
||
|
||
> 本节保留早期简洁部署方案,生产环境请参考「八、部署与运维架构」。
|
||
|
||
### 7.1 Docker 配置
|
||
|
||
```yaml
|
||
# docker-compose.yml
|
||
version: '3.8'
|
||
|
||
services:
|
||
# --- Phase 1 核心服务 ---
|
||
|
||
collector:
|
||
build:
|
||
context: .
|
||
dockerfile: Dockerfile.collector
|
||
volumes:
|
||
- ./data:/opt/llm-hub/data # PostgreSQL 数据持久化
|
||
- ./logs:/var/log/llm-hub # 日志持久化
|
||
- ./reports:/opt/llm-hub/reports # 报告输出
|
||
env_file:
|
||
- .env
|
||
restart: unless-stopped
|
||
networks:
|
||
- llm-hub-net
|
||
|
||
api:
|
||
build:
|
||
context: .
|
||
dockerfile: Dockerfile.api
|
||
ports:
|
||
- "5000:5000"
|
||
volumes:
|
||
- ./data:/opt/llm-hub/data
|
||
- ./reports:/opt/llm-hub/reports
|
||
env_file:
|
||
- .env
|
||
restart: unless-stopped
|
||
depends_on:
|
||
- collector
|
||
networks:
|
||
- llm-hub-net
|
||
|
||
# --- Phase 2 才引入 Nginx(内网访问 + 静态文件服务)---
|
||
|
||
networks:
|
||
llm-hub-net:
|
||
driver: bridge
|
||
```
|
||
|
||
### 7.2 内网部署要求
|
||
|
||
**部署前提**:
|
||
- 一台可访问外网的服务器(境外更好,便于访问 OpenRouter)
|
||
- 域名(可选,用于 HTTPS + 钉钉/飞书 Webhook 回调)
|
||
- Docker + Docker Compose
|
||
|
||
**网络访问需求**:
|
||
|
||
| 目的地 | 用途 | 协议 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| `openrouter.ai` | 采集海外模型数据 | HTTPS |
|
||
| `api.deepseek.com` | 采集 DeepSeek 定价 | HTTPS |
|
||
| `dashscope.aliyuncs.com` | 采集阿里云定价 | HTTPS |
|
||
| `api.moonshot.cn` | 采集 Kimi 定价 | HTTPS |
|
||
| `open.bigmodel.cn` | 采集智谱定价 | HTTPS |
|
||
| `api.siliconflow.cn` | 采集硅基流动定价 | HTTPS |
|
||
| `oapi.dingtalk.com` | Phase 2 钉钉告警 | HTTPS |
|
||
| `open.feishu.cn` | Phase 2 飞书告警 | HTTPS |
|
||
| **无需访问** | 国内云厂商定价页(如阿里云控制台) | — |
|
||
|
||
### 7.3 环境变量清单
|
||
|
||
```bash
|
||
# .env 文件(Phase 1 最小配置)
|
||
|
||
# === 数据库 ===
|
||
DATABASE_URL=postgresql://user:pass@localhost:5432/llmhub
|
||
|
||
# === OpenRouter ===
|
||
OPENROUTER_API_KEY=sk-or-v1-xxxxx
|
||
|
||
# === 国内厂商 API Keys ===
|
||
DEEPSEEK_API_KEY=sk-xxxxx
|
||
DASHSCOPE_API_KEY=sk-xxxxx
|
||
MOONSHOT_API_KEY=sk-xxxxx
|
||
ZHIPU_API_KEY=xxxxx
|
||
MINIMAX_API_KEY=xxxxx
|
||
VOLCENGINE_API_KEY=xxxxx
|
||
VOLCENGINE_SECRET_KEY=xxxxx
|
||
TENCENT_SECRET_ID=xxxxx
|
||
TENCENT_SECRET_KEY=xxxxx
|
||
BAIDU_QIANFAN_API_KEY=xxxxx
|
||
BAIDU_QIANFAN_SECRET_KEY=xxxxx
|
||
SILICONFLOW_API_KEY=sk-xxxxx
|
||
|
||
# === 告警配置(Phase 2 才启用)===
|
||
# DINGTALK_WEBHOOK=https://oapi.dingtalk.com/robot/send?access_token=xxxxx
|
||
# FEISHU_WEBHOOK=https://open.feishu.cn/open-apis/bot/v2/hook/xxxxx
|
||
ALERT_THRESHOLD_PCT=10
|
||
ALERT_THRESHOLD_CRITICAL_PCT=20
|
||
|
||
# === 邮件配置(可选)===
|
||
SMTP_HOST=smtp.example.com
|
||
SMTP_PORT=587
|
||
SMTP_USER=noreply@example.com
|
||
SMTP_PASS=xxxxx
|
||
|
||
# === 备份配置 ===
|
||
BACKUP_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
||
BACKUP_OSS_BUCKET=llm-hub-backup
|
||
BACKUP_OSS_KEY=xxxxx
|
||
BACKUP_OSS_SECRET=xxxxx
|
||
|
||
# === 系统 ===
|
||
LOG_LEVEL=INFO
|
||
TZ=Asia/Shanghai
|
||
```
|
||
|
||
### 7.4 Nginx 配置
|
||
|
||
```nginx
|
||
# nginx.conf
|
||
worker_processes auto;
|
||
error_log /var/log/nginx/error.log warn;
|
||
pid /var/run/nginx.pid;
|
||
|
||
events {
|
||
worker_connections 1024;
|
||
}
|
||
|
||
http {
|
||
include /etc/nginx/mime.types;
|
||
default_type application/octet-stream;
|
||
|
||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||
'$status $body_bytes_sent "$http_referer" '
|
||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||
|
||
access_log /var/log/nginx/access.log main;
|
||
|
||
sendfile on;
|
||
keepalive_timeout 65;
|
||
|
||
# --- 静态文件服务(前端)---
|
||
server {
|
||
listen 80;
|
||
server_name _;
|
||
|
||
root /usr/share/nginx/html;
|
||
index index.html;
|
||
|
||
# 前端静态页面
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
|
||
# 每日报告 HTML
|
||
location /reports/ {
|
||
alias /usr/share/nginx/html/reports/;
|
||
expires 7d;
|
||
add_header Cache-Control "public, immutable";
|
||
}
|
||
|
||
# --- API 反向代理 ---
|
||
location /api/ {
|
||
proxy_pass http://api:5000/api/;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_read_timeout 60s;
|
||
}
|
||
|
||
# 健康检查(无需认证)
|
||
location /health {
|
||
proxy_pass http://api:5000/api/v1/health;
|
||
proxy_set_header Host $host;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 十三、Phase 1 技术路线(3个月)
|
||
|
||
### 8.1 Sprint 划分
|
||
|
||
| Sprint | 周期 | 目标 | 交付物 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| **Sprint 0** | Week 1 | 技术方案确认 + 环境搭建 | `TECHNICAL_DESIGN.md` 终稿;开发环境就绪 |
|
||
| **Sprint 1** | Week 2-3 | OpenRouter 采集器 + 数据库 Schema | 371 海外模型入库;数据库 DDL 可执行 |
|
||
| **Sprint 2** | Week 4-5 | PostgreSQL migration + 日报生成器 | 三张表落地;Markdown 报告输出到 reports/daily/ |
|
||
| **Sprint 2** | Week 4-5 | 每日报告生成 + Explorer 页面 | Markdown 报告生成;Explorer 页面上线;Markdown 报告可输出到 reports/daily/ |
|
||
| **Sprint 4** | Week 8-9 | 模型浏览器 + 搜索筛选 | `/explorer.html` 上线;卡片/表格视图 |
|
||
| **Sprint 5** | Week 10-11 | Explorer 页面完善 + Dashboard 占位图 | 表格/筛选排序;价格趋势占位图 |
|
||
| **Sprint 6** | Week 12 | 收尾 + 部署 + 验证脚本 | Docker Compose 部署文档;验证脚本;备份策略 |
|
||
|
||
### 8.2 Sprint 1 详细任务(OpenRouter 采集器)
|
||
|
||
```
|
||
Sprint 1 目标:从 OpenRouter API 采集 371 模型,建立基础数据库
|
||
|
||
任务分解:
|
||
├── T1.1 数据库 Schema 部署
|
||
│ ├── [ ] 创建所有 DDL 表(model_provider/model/operator/region_pricing/...)
|
||
│ ├── [ ] 编写 PostgreSQL Schema 部署脚本(deploy.sh)
|
||
│ └── [ ] 验证:查询所有表,返回空表,数量正确
|
||
│
|
||
├── T1.2 OpenRouter 采集器实现
|
||
│ ├── [ ] 实现 `scripts/fetch_openrouter.go`(Go)
|
||
│ │ ├── 调用 GET https://openrouter.ai/api/v1/models
|
||
│ │ ├── 解析 id/name/pricing/context_length/capabilities
|
||
│ │ ├── 从 id 前缀提取 provider(anthropic/claude-3.5-sonnet → Anthropic)
|
||
│ │ ├── 处理免费模型(id 包含 :free 后缀)
|
||
│ │ └── 错误处理(401/429/500)
|
||
│ ├── [ ] 实现 base collector 抽象类
|
||
│ ├── [ ] 实现数据清洗逻辑(去除异常价格、统一单位)
|
||
│ └── [ ] 验证:371 模型全部入库,无重复,数据正确
|
||
│
|
||
├── T1.3 数据映射 + Provider 标准化
|
||
│ ├── [ ] 建立 PROVIDER_NAME_MAP(OpenRouter id → 标准厂商名)
|
||
│ ├── [ ] 验证:所有 provider 名称统一(无别名)
|
||
│ └── [ ] 补充 provider logo_url / description
|
||
│
|
||
├── T1.4 初始数据导入
|
||
│ ├── [ ] 运行 OpenRouter 采集器,导入 371 模型
|
||
│ ├── [ ] 质量检查:随机抽 10 条数据,验证价格/上下文长度
|
||
│ └── [ ] 导出数据字典文档
|
||
│
|
||
└── T1.5 采集脚本 + cron 配置
|
||
├── [ ] 编写 scripts/run_openrouter_collect.sh
|
||
├── [ ] 配置 crontab(08:00 每日执行)
|
||
├── [ ] 编写失败重试逻辑
|
||
└── [ ] 验证:手动运行脚本成功,数据入库
|
||
```
|
||
|
||
### 8.3 Phase 1 关键技术决策记录
|
||
|
||
| 决策 | 选型 | 记录时间 | 理由 |
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
| Phase 1 数据库用 PostgreSQL | ✅ 确认 | Sprint 0 | 与立交桥技术栈统一;支持 JSONB/数组类型;数据库内队列 |
|
||
| 数据采集用 Go net/http | ✅ 确认 | Sprint 0 | 标准库,无需第三方依赖,静态编译 |
|
||
| 报告生成用 Go html/template | ✅ 确认 | Sprint 0 | 标准库模板,无需第三方依赖 |
|
||
| 告警用 Webhook 直推 | ✅ 确认 | Sprint 0 | 无需消息队列,降低复杂度 |
|
||
| OpenRouter ELO 数据暂不采集 | ⚠️ 延期 | Sprint 1 | ELO API 可能收费,Phase 1 跳过 |
|
||
| 国内厂商优先级:DeepSeek > 阿里 > Kimi > 智谱 > MiniMax > 火山 > 腾讯 > 百度 | ✅ 确认 | Sprint 2 | 按市场热度排序 |
|
||
|
||
### 8.4 质量检查清单(Phase 1 上线前)
|
||
|
||
#### 功能验证
|
||
- [ ] OpenRouter 371 模型全部入库,覆盖率 100%
|
||
# (Phase 2 才采集国内厂商)
|
||
- [ ] 每日 08:00 cron 触发采集,报告自动生成
|
||
- [ ] 报告内容包含:新模型、价格变动(>5% 高亮)、场景推荐
|
||
- [ ] `/explorer.html` 搜索响应 < 500ms
|
||
# (Phase 2 才实现告警推送)
|
||
|
||
#### 数据质量验证
|
||
- [ ] 每条数据有 `source_url` 来源标注
|
||
- [ ] 置信度分级标注(official / inferred / expired)
|
||
- [ ] 价格单位统一为 ¥/MTok 或 $/MTok
|
||
- [ ] 同模型多源价格差异 > 20% 时标注"待核实"
|
||
- [ ] 采集失败写入日志,保留旧数据
|
||
|
||
#### 部署验证
|
||
- [ ] `docker-compose up` 可正常启动所有服务
|
||
- [ ] PostgreSQL 数据库持久化到 `data/` 目录
|
||
- [ ] 报告 HTML 生成到 `reports/` 目录
|
||
# Phase 2 才引入 Nginx
|
||
- [ ] API `/api/v1/health` 返回 200
|
||
- [ ] 备份脚本每日推送至 OSS 成功
|
||
|
||
#### 性能验证
|
||
- [ ] 371 模型采集完成 < 5 分钟
|
||
- [ ] 报告生成 < 30 秒
|
||
- [ ] API 查询响应 < 500ms(/models, 20 条)
|
||
- [ ] 并发 10 个采集器同时运行,内存 < 2GB
|
||
|
||
---
|
||
|
||
## 附录:目录结构
|
||
|
||
```
|
||
llm-intelligence/
|
||
├── TECHNICAL_DESIGN.md # 本文档
|
||
├── PRD.md # 产品需求文档
|
||
├── FEATURE_LIST.md # 功能清单
|
||
├── BUSINESS_MODEL.md # 商业模式
|
||
├── MARKET_ANALYSIS.md # 市场调研
|
||
│
|
||
├── Dockerfile.collector # 采集器镜像
|
||
├── Dockerfile.api # API 服务镜像
|
||
├── docker-compose.yml # 容器编排
|
||
├── .env.example # 环境变量模板
|
||
├── nginx.conf # Nginx 配置
|
||
│
|
||
├── collectors/ # 数据采集器
|
||
│ ├── collector.go # 采集器接口定义
|
||
│ ├── openrouter.go # OpenRouter 采集器(Go)
|
||
│ └── [deepseek.go] # Phase 2: DeepSeek 采集器
|
||
│
|
||
├── services/ # 服务层
|
||
│ ├── db.go # 数据库连接池封装(Go database/sql + pq)
|
||
│ ├── queries.go # 预编译 SQL 查询
|
||
│ └── models.go # Go struct 模型定义(对应 DB schema)
|
||
│
|
||
├── api/ # REST API
|
||
│ ├── main.go # HTTP 服务入口(Go net/http 或 Phase 2 框架)
|
||
│ ├── routes.go # 路由注册
|
||
│ └── middleware.go # 认证/限流/日志中间件
|
||
│
|
||
├── static/ # 前端静态文件
|
||
│ ├── index.html # 首页/报告列表
|
||
│ ├── explorer.html # 模型浏览器
|
||
│ ├── calculator.html # 成本计算器
|
||
│ ├── trends.html # 趋势分析
|
||
│ ├── css/
|
||
│ │ └── style.css
|
||
│ └── js/
|
||
│ ├── dataService.js # API 调用封装
|
||
│ ├── explorer.js # 模型浏览器逻辑
|
||
│ ├── calculator.js # 计算器逻辑
|
||
│ └── charts.js # ECharts 封装
|
||
│
|
||
├── templates/ # Go html/template 模板
|
||
│ └── report.html # 每日报告 HTML 模板
|
||
│
|
||
├── reports/ # 生成的报告 HTML 输出
|
||
│ └── 2026-05-04.html
|
||
│
|
||
├── scripts/ # 运维脚本
|
||
│ ├── run_daily.sh # 每日采集 + 报告脚本
|
||
│ ├── backup.sh # 数据库备份脚本
|
||
│ ├── migrate.sh # PostgreSQL Schema 部署脚本
|
||
│ └── init_db.sql # 数据库初始化(权限/扩展)
|
||
│
|
||
├── internal/ # Go 内部包
|
||
│ ├── collectors/ # 采集器接口 + 实现
|
||
│ │ ├── collector.go # Collector 接口定义
|
||
│ │ └── openrouter.go # OpenRouter 采集器
|
||
│ ├── db/ # 数据库连接 + 查询封装
|
||
│ │ ├── db.go # 连接池管理
|
||
│ │ └── queries.go # 预编译 SQL
|
||
│ ├── retry/ # 重试工具包
|
||
│ │ └── retry.go # 指数退避重试
|
||
│ └── report/ # 报告生成
|
||
│ └── generator.go # Markdown/HTML 生成
|
||
│
|
||
├── db/migrations/ # PostgreSQL 迁移
|
||
│ └── 001_phase1_core_tables.sql
|
||
│
|
||
├── frontend/ # React 前端
|
||
│ ├── src/
|
||
│ │ ├── pages/
|
||
│ │ │ └── Explorer.tsx # 模型浏览器
|
||
│ │ ├── data/
|
||
│ │ │ └── models.json # 静态数据(开发用)
|
||
│ │ └── App.tsx
|
||
│ ├── package.json
|
||
│ └── vite.config.ts
|
||
│
|
||
├── reports/daily/ # Markdown 日报输出
|
||
│ └── daily_report_YYYY-MM-DD.md
|
||
│
|
||
├── tests/ # 测试
|
||
│ ├── integration/ # 集成测试(testcontainers-go)
|
||
│ └── e2e/ # E2E 测试
|
||
│
|
||
├── logs/ # 日志文件(运行时生成,logrotate)
|
||
│ ├── collector.log
|
||
│ ├── api.log
|
||
│ └── backup.log
|
||
│
|
||
├── Dockerfile # 多阶段构建
|
||
├── docker-compose.yml # 生产编排
|
||
├── Makefile # 常用命令
|
||
├── go.mod # Go 依赖
|
||
└── .env.example # 环境变量模板
|
||
|
||
```
|
||
|
||
---
|
||
|
||
**文档状态:** 生产级设计 v1.1 完成 ✅
|
||
|
||
**修订内容(2026-05-09):**
|
||
- 技术栈统一为 Go 1.22.2 + PostgreSQL
|
||
- DDL 补充审计字段(created_by/updated_by/deleted_at)+ 数据血缘字段(retrieved_at/batch_id/collector_version)
|
||
- API 安全章节:认证/限流/错误码/CORS
|
||
- 新增 5 个生产级章节:安全/部署运维/可观测性/测试策略/容量规划
|
||
|
||
**下一步行动:**
|
||
- [ ] T-3.2 Dashboard 组件完善(Explorer 页面数据对接)
|
||
- [ ] T-4.3 cron 每日自动采集 + 日报生成
|
||
|
||
---
|
||
|
||
_文档编制:宰相(AI 辅助)_
|
||
_基于 PRD.md(v0.3)、FEATURE_LIST.md(v1.1)、BUSINESS_MODEL.md(v1.0)、MARKET_ANALYSIS.md(v3.0)_
|
||
|
||
---
|
||
|
||
## 七、安全设计
|
||
|
||
### 7.1 安全原则
|
||
|
||
本项目遵循**最小权限原则**和**纵深防御**策略:
|
||
|
||
| 层级 | 防御措施 |
|
||
|------|----------|
|
||
| 网络层 | TLS 1.3 全链路加密、防火墙白名单 |
|
||
| 应用层 | 输入校验、SQL 参数化查询、CSRF 防护 |
|
||
| 数据层 | 敏感字段加密、DB 用户权限最小化 |
|
||
| 运维层 | 密钥外部化、日志脱敏、定期轮换 |
|
||
|
||
### 7.2 API 密钥管理
|
||
|
||
**OpenRouter API Key**:
|
||
|
||
| 存储方式 | 优先级 | 说明 |
|
||
|----------|--------|------|
|
||
| **环境变量** `OPENROUTER_API_KEY` | ✅ 推荐 | 系统级配置,不进入代码仓库 |
|
||
| **Docker Secrets** | ✅ 容器环境 | `docker secret` 或 K8s secret |
|
||
| **.env 文件** | ⚠️ 开发环境 | 必须加入 `.gitignore` |
|
||
| ❌ **硬编码** | 禁止 | 任何提交含密钥 = 安全事件 |
|
||
|
||
**Go 读取示例:**
|
||
```go
|
||
apiKey := os.Getenv("OPENROUTER_API_KEY")
|
||
if apiKey == "" {
|
||
log.Fatal("OPENROUTER_API_KEY not set")
|
||
}
|
||
```
|
||
|
||
### 7.3 数据库凭证管理
|
||
|
||
**连接字符串**:
|
||
```
|
||
postgresql://llm_hub:${DB_PASSWORD}@db:5432/llm_intelligence?sslmode=require
|
||
```
|
||
|
||
| 环境 | 密码来源 |
|
||
|------|----------|
|
||
| 开发 | `.env` 文件(不提交 git) |
|
||
| CI/CD | GitHub Actions Secrets |
|
||
| 生产 | Docker Secret / 云 KMS |
|
||
|
||
**`.env.example` 模板(无真实值):**
|
||
```bash
|
||
# 数据库
|
||
DB_HOST=localhost
|
||
DB_PORT=5432
|
||
DB_NAME=llm_intelligence
|
||
DB_USER=llm_hub
|
||
DB_PASSWORD= # 必填,留空示例
|
||
DB_SSLMODE=require
|
||
|
||
# API 密钥
|
||
OPENROUTER_API_KEY= # 必填
|
||
|
||
# 日志级别
|
||
LOG_LEVEL=info
|
||
```
|
||
|
||
### 7.4 传输安全
|
||
|
||
- **DB ↔ App**:`sslmode=require`,禁止明文连接
|
||
- **App ↔ 外部 API**:`tls.Config{MinVersion: tls.VersionTLS13}`
|
||
- **前端 ↔ 后端**:HTTPS 强制(HSTS max-age=31536000)
|
||
- **内部通信**:若 Phase 2 引入微服务,mTLS 双向认证
|
||
|
||
### 7.5 输入验证与防注入
|
||
|
||
**JSON Schema 校验**(采集器):
|
||
```go
|
||
type OpenRouterModel struct {
|
||
ID string `json:"id" validate:"required"`
|
||
Name string `json:"name"`
|
||
Pricing Pricing `json:"pricing" validate:"required"`
|
||
ContextLength int `json:"context_length" validate:"gte=0,lte=10000000"`
|
||
}
|
||
```
|
||
|
||
**SQL 防注入**:
|
||
- 全部使用 `database/sql` 参数化查询(`$1, $2`)
|
||
- 禁止字符串拼接 SQL
|
||
- ORM/查询 builder Phase 2 评估
|
||
|
||
### 7.6 敏感数据保护
|
||
|
||
**`user_subscription` 表敏感字段**:
|
||
|
||
| 字段 | 存储方式 | 说明 |
|
||
|------|----------|------|
|
||
| `email` | AES-256-GCM 加密 | 密钥由 KMS 管理 |
|
||
| `phone` | AES-256-GCM 加密 | 仅显示后 4 位 |
|
||
| `feishu_webhook` | AES-256-GCM 加密 | Webhook URL 含 token |
|
||
| `dingtalk_webhook` | AES-256-GCM 加密 | 同上 |
|
||
|
||
**日志脱敏**:
|
||
- 日志中 email 显示为 `l***@example.com`
|
||
- webhook URL 中 token 部分替换为 `***`
|
||
- DB 密码显示为 `[REDACTED]`
|
||
|
||
### 7.7 安全响应头
|
||
|
||
```go
|
||
// HTTP 中间件
|
||
func securityHeaders(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Security-Policy", "default-src 'self'")
|
||
w.Header().Set("X-Frame-Options", "DENY")
|
||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
```
|
||
|
||
### 7.8 数据库最小权限
|
||
|
||
| 用户 | 权限 | 用途 |
|
||
|------|------|------|
|
||
| `llm_hub_app` | SELECT, INSERT, UPDATE | 应用服务账号 |
|
||
| `llm_hub_collector` | SELECT, INSERT, UPDATE | 采集器账号 |
|
||
| `llm_hub_readonly` | SELECT | 只读查询(报表/审计) |
|
||
| `llm_hub_admin` | ALL | 迁移/运维(人工使用) |
|
||
|
||
---
|
||
|
||
## 八、部署与运维架构
|
||
|
||
### 8.1 CI/CD 流水线
|
||
|
||
**GitHub Actions 工作流**(`.github/workflows/ci.yml`):
|
||
|
||
```yaml
|
||
name: CI/CD
|
||
on: [push, pull_request]
|
||
jobs:
|
||
lint:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: actions/setup-go@v5
|
||
with: { go-version: '1.22' }
|
||
- run: go vet ./...
|
||
- run: gofmt -l . | tee /dev/stderr | wc -l | xargs test 0 -eq
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
services:
|
||
postgres:
|
||
image: postgres:16
|
||
env: { POSTGRES_PASSWORD: test }
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- run: go test -race -coverprofile=coverage.out ./...
|
||
- run: go tool cover -func=coverage.out | grep total
|
||
build:
|
||
needs: [lint, test]
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- run: go build -o bin/fetch_openrouter ./scripts/fetch_openrouter.go
|
||
- run: go build -o bin/generate_daily_report ./scripts/generate_daily_report.go
|
||
- uses: actions/upload-artifact@v4
|
||
with: { name: binaries, path: bin/ }
|
||
deploy:
|
||
needs: build
|
||
if: github.ref == 'refs/heads/main'
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/download-artifact@v4
|
||
with: { name: binaries }
|
||
# SSH/SCP 到生产服务器,或 docker push
|
||
```
|
||
|
||
### 8.2 容器化
|
||
|
||
**Dockerfile(多阶段构建)**:
|
||
```dockerfile
|
||
# 构建阶段
|
||
FROM golang:1.22-alpine AS builder
|
||
WORKDIR /app
|
||
COPY go.mod go.sum ./
|
||
RUN go mod download
|
||
COPY . .
|
||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o fetch_openrouter scripts/fetch_openrouter.go
|
||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o generate_daily_report scripts/generate_daily_report.go
|
||
|
||
# 运行阶段
|
||
FROM alpine:latest
|
||
RUN apk --no-cache add ca-certificates tzdata
|
||
WORKDIR /root/
|
||
COPY --from=builder /app/fetch_openrouter .
|
||
COPY --from=builder /app/generate_daily_report .
|
||
COPY --from=builder /app/db/migrations ./migrations
|
||
CMD ["./fetch_openrouter"]
|
||
```
|
||
|
||
**docker-compose.yml(生产版)**:
|
||
```yaml
|
||
version: '3.8'
|
||
services:
|
||
db:
|
||
image: postgres:16-alpine
|
||
environment:
|
||
POSTGRES_DB: llm_intelligence
|
||
POSTGRES_USER: llm_hub
|
||
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
- ./db/migrations:/docker-entrypoint-initdb.d
|
||
secrets:
|
||
- db_password
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U llm_hub"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
app:
|
||
build: .
|
||
environment:
|
||
DB_CONN: "host=db dbname=llm_intelligence sslmode=require"
|
||
OPENROUTER_API_KEY_FILE: /run/secrets/openrouter_key
|
||
secrets:
|
||
- openrouter_key
|
||
- db_password
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
restart: unless-stopped
|
||
|
||
nginx:
|
||
image: nginx:alpine
|
||
ports:
|
||
- "80:80"
|
||
- "443:443"
|
||
volumes:
|
||
- ./frontend/dist:/usr/share/nginx/html:ro
|
||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||
depends_on: [app]
|
||
|
||
volumes:
|
||
postgres_data:
|
||
|
||
secrets:
|
||
db_password:
|
||
file: ./secrets/db_password.txt
|
||
openrouter_key:
|
||
file: ./secrets/openrouter_key.txt
|
||
```
|
||
|
||
### 8.3 健康检查端点
|
||
|
||
```go
|
||
// GET /api/v1/health
|
||
func healthHandler(w http.ResponseWriter, r *http.Request) {
|
||
health := struct {
|
||
Status string `json:"status"`
|
||
Version string `json:"version"`
|
||
DB string `json:"db"`
|
||
DBRecordCount map[string]int `json:"db_record_count"`
|
||
LastCollectTime time.Time `json:"last_collect_time"`
|
||
LastReportTime time.Time `json:"last_report_time"`
|
||
Uptime string `json:"uptime"`
|
||
}{
|
||
Status: "ok",
|
||
Version: os.Getenv("APP_VERSION"),
|
||
}
|
||
|
||
// DB 连通性检查
|
||
if err := db.Ping(); err != nil {
|
||
health.Status = "degraded"
|
||
health.DB = "unreachable: " + err.Error()
|
||
w.WriteHeader(http.StatusServiceUnavailable)
|
||
} else {
|
||
health.DB = "ok"
|
||
health.DBRecordCount = getTableCounts()
|
||
}
|
||
|
||
// 数据新鲜度检查
|
||
if time.Since(lastCollectTime) > 25*time.Hour {
|
||
health.Status = "stale_data"
|
||
}
|
||
|
||
json.NewEncoder(w).Encode(health)
|
||
}
|
||
```
|
||
|
||
### 8.4 配置管理
|
||
|
||
**环境变量清单**:
|
||
|
||
| 变量 | 必填 | 默认值 | 说明 |
|
||
|------|------|--------|------|
|
||
| `DB_CONN` | ✅ | — | PostgreSQL 连接字符串 |
|
||
| `OPENROUTER_API_KEY` | ✅ | — | API 密钥 |
|
||
| `LOG_LEVEL` | ❌ | `info` | debug/info/warn/error |
|
||
| `APP_VERSION` | ❌ | `dev` | 应用版本号 |
|
||
| `COLLECT_TIMEOUT` | ❌ | `60s` | 采集超时 |
|
||
| `REPORT_OUTPUT_DIR` | ❌ | `./reports/daily` | 日报输出目录 |
|
||
| `FRONTEND_DATA_DIR` | ❌ | `./frontend/src/data` | 前端数据目录 |
|
||
| `ENABLE_METRICS` | ❌ | `false` | Prometheus 指标开关 |
|
||
|
||
**环境区分策略**:
|
||
```
|
||
.env.development # 本地开发
|
||
.env.staging # 预生产(连接 staging DB)
|
||
.env.production # 生产(仅服务器上存在,不提交 git)
|
||
```
|
||
|
||
### 8.5 滚动发布与回滚
|
||
|
||
**零停机部署**:
|
||
1. 构建新镜像 → `docker build -t llm-hub:v1.1`
|
||
2. 蓝绿部署:新容器启动 + 健康检查通过
|
||
3. 切换流量:nginx upstream 指向新容器
|
||
4. 保留旧容器 5 分钟(快速回滚)
|
||
|
||
**回滚策略**:
|
||
```bash
|
||
# 紧急回滚(30 秒内)
|
||
docker-compose stop app && docker-compose up -d app --no-deps --scale app=1
|
||
# 或切换到上一个镜像标签
|
||
docker tag llm-hub:v1.0 llm-hub:latest && docker-compose up -d app
|
||
```
|
||
|
||
---
|
||
|
||
## 九、可观测性体系
|
||
|
||
### 9.1 日志体系
|
||
|
||
**结构化日志(Go slog)**:
|
||
```go
|
||
import "log/slog"
|
||
|
||
var logger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||
Level: parseLogLevel(os.Getenv("LOG_LEVEL")),
|
||
AddSource: true,
|
||
}))
|
||
|
||
// 使用示例
|
||
logger.Info("collection completed",
|
||
slog.String("collector", "openrouter"),
|
||
slog.Int("records", 365),
|
||
slog.Duration("duration", 12*time.Second),
|
||
)
|
||
```
|
||
|
||
**日志分级**:
|
||
|
||
| 级别 | 场景 | 输出目标 |
|
||
|------|------|----------|
|
||
| DEBUG | 开发调试、详细 SQL | 本地 stdout(生产关闭) |
|
||
| INFO | 正常流程、采集完成 | stdout + 文件 |
|
||
| WARN | 降级处理、数据延迟 | stdout + 文件 + 告警通道 |
|
||
| ERROR | 失败、异常、DB 断开 | stdout + 文件 + 立即告警 |
|
||
|
||
**日志轮转**:
|
||
```yaml
|
||
# docker-compose 中的 logrotate 或 docker 原生
|
||
logging:
|
||
driver: "json-file"
|
||
options:
|
||
max-size: "10m"
|
||
max-file: "5"
|
||
```
|
||
|
||
### 9.2 Metrics(Prometheus)
|
||
|
||
**指标设计**:
|
||
|
||
| 指标名 | 类型 | 说明 |
|
||
|--------|------|------|
|
||
| `collector_duration_seconds` | Histogram | 采集耗时(按采集器标签) |
|
||
| `collector_records_total` | Counter | 采集记录数 |
|
||
| `collector_errors_total` | Counter | 采集失败次数(按错误类型标签) |
|
||
| `api_requests_total` | Counter | API 请求数(按 endpoint/method/status) |
|
||
| `api_request_duration_seconds` | Histogram | API 响应时间 P50/P95/P99 |
|
||
| `db_connection_active` | Gauge | 当前活跃 DB 连接数 |
|
||
| `db_connection_wait_duration` | Histogram | 等待连接池时间 |
|
||
| `data_freshness_hours` | Gauge | 数据新鲜度(距上次采集小时数) |
|
||
|
||
**Go 实现(prometheus/client_golang)**:
|
||
```go
|
||
import "github.com/prometheus/client_golang/prometheus"
|
||
|
||
var collectorDuration = prometheus.NewHistogramVec(
|
||
prometheus.HistogramOpts{
|
||
Name: "collector_duration_seconds",
|
||
Help: "Collector run duration",
|
||
Buckets: prometheus.DefBuckets,
|
||
},
|
||
[]string{"collector"},
|
||
)
|
||
|
||
func init() {
|
||
prometheus.MustRegister(collectorDuration)
|
||
}
|
||
```
|
||
|
||
### 9.3 告警分级
|
||
|
||
| 等级 | 触发条件 | 通知方式 | 响应时间 |
|
||
|------|----------|----------|----------|
|
||
| **P0** | 服务不可用、DB 断开、所有采集器失败 | 飞书/钉钉/短信 + 电话 | 15 分钟 |
|
||
| **P1** | 单个采集器失败 > 24h、API P99 > 2s | 飞书/钉钉 | 1 小时 |
|
||
| **P2** | 价格异常变动 > 20%、数据延迟 > 48h | 飞书群 | 4 小时 |
|
||
|
||
**告警升级策略**:
|
||
- P0 告警 15 分钟未恢复 → 升级至电话通知
|
||
- P1 告警 1 小时未恢复 → 升级至 P0 通道
|
||
- 同一问题 24h 内重复触发 → 合并为汇总告警(避免轰炸)
|
||
|
||
### 9.4 运行看板(Grafana)
|
||
|
||
**推荐面板**:
|
||
1. **采集健康**:成功率、耗时趋势、各采集器状态
|
||
2. **数据规模**:模型数、定价记录数、日增量
|
||
3. **API 性能**:QPS、P99 延迟、错误率
|
||
4. **资源使用**:CPU、内存、DB 连接池、磁盘
|
||
5. **业务看板**:价格变动 Top 10、新模型上线、免费政策变更
|
||
|
||
### 9.5 链路追踪(Phase 2)
|
||
|
||
评估 OpenTelemetry Go SDK:
|
||
```go
|
||
import "go.opentelemetry.io/otel"
|
||
|
||
// 采集链路:cron → collector → API → parser → DB
|
||
// 追踪维度:采集批次、模型处理耗时、DB 写入耗时
|
||
```
|
||
|
||
---
|
||
|
||
## 十、测试策略
|
||
|
||
### 10.1 单元测试
|
||
|
||
**覆盖率目标**:
|
||
- 整体 ≥ 80%
|
||
- 采集器核心逻辑 ≥ 90%
|
||
- 数据库查询 ≥ 85%
|
||
- 错误处理路径 100%
|
||
|
||
**Go 测试示例**:
|
||
```go
|
||
// scripts/fetch_openrouter_test.go
|
||
func TestParseModelID(t *testing.T) {
|
||
tests := []struct {
|
||
input string
|
||
wantProvider string
|
||
wantModel string
|
||
}{
|
||
{"anthropic/claude-3.5-sonnet", "Anthropic", "Claude 3.5 Sonnet"},
|
||
{"deepseek-ai/DeepSeek-V4", "DeepSeek", "V4"},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.input, func(t *testing.T) {
|
||
gotProvider, gotModel := parseModelID(tt.input)
|
||
assert.Equal(t, tt.wantProvider, gotProvider)
|
||
assert.Equal(t, tt.wantModel, gotModel)
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
**Mock 策略**:
|
||
- HTTP 外部调用:httptest 模拟 OpenRouter API
|
||
- DB 查询:sqlmock 模拟数据库响应
|
||
- 时间:手动注入 `time.Now()` 替代
|
||
|
||
### 10.2 集成测试
|
||
|
||
**testcontainers-go + PostgreSQL**:
|
||
```go
|
||
import "github.com/testcontainers/testcontainers-go"
|
||
import "github.com/testcontainers/testcontainers-go/modules/postgres"
|
||
|
||
func TestCollectorToDB(t *testing.T) {
|
||
ctx := context.Background()
|
||
pg, err := postgres.Run(ctx, "postgres:16-alpine",
|
||
postgres.WithDatabase("test_db"),
|
||
postgres.WithUsername("test"),
|
||
postgres.WithPassword("test"),
|
||
)
|
||
require.NoError(t, err)
|
||
defer pg.Terminate(ctx)
|
||
|
||
connStr, _ := pg.ConnectionString(ctx)
|
||
db := setupTestDB(connStr)
|
||
|
||
// 运行采集器
|
||
records, err := collectOpenRouter(ctx, db, mockAPIKey)
|
||
require.NoError(t, err)
|
||
require.Greater(t, len(records), 0)
|
||
|
||
// 验证 DB 写入
|
||
var count int
|
||
db.QueryRow("SELECT COUNT(*) FROM model").Scan(&count)
|
||
require.Greater(t, count, 0)
|
||
}
|
||
```
|
||
|
||
### 10.3 E2E 测试
|
||
|
||
**覆盖范围**:
|
||
- `fetch_openrouter` 二进制 → DB 有数据
|
||
- `generate_daily_report` 二进制 → `reports/daily/` 产出 Markdown
|
||
- API 端到端:启动服务 → curl 请求 → 验证响应
|
||
|
||
**前端 E2E(Phase 2)**:
|
||
- Cypress/Playwright:Explorer 页面加载、筛选交互、数据展示
|
||
|
||
### 10.4 性能测试
|
||
|
||
| 测试项 | 目标 | 工具 |
|
||
|--------|------|------|
|
||
| 采集器并发 | 371 模型 < 60s | Go benchmark + pprof |
|
||
| API 响应 | P99 < 200ms | k6 / vegeta |
|
||
| DB 查询 | 价格对比查询 < 100ms | EXPLAIN ANALYZE |
|
||
|
||
**Go Benchmark 示例**:
|
||
```go
|
||
func BenchmarkCollectOpenRouter(b *testing.B) {
|
||
for i := 0; i < b.N; i++ {
|
||
collectOpenRouter(ctx, mockClient)
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 十一、容量规划
|
||
|
||
### 11.1 数据增长模型
|
||
|
||
| 数据源 | 日增量 | 年增量 | 存储/条 |
|
||
|--------|--------|--------|---------|
|
||
| OpenRouter 模型 | ~371 条 model | ~135K | ~2KB |
|
||
| 定价记录 | ~371 条 pricing | ~135K | ~1KB |
|
||
| 价格历史 | 变动时插入,预估 ~50/天 | ~18K | ~0.5KB |
|
||
| 日报 | 1 条/天 | ~365 | ~50KB(HTML) |
|
||
|
||
**1 年总估算(含索引膨胀 ×1.5)**:
|
||
- 模型+定价表:~400MB
|
||
- 价格历史:~20MB
|
||
- 日报:~30MB
|
||
- 日志(90 天轮转):~500MB
|
||
- **总计:~1GB/年**(PostgreSQL 单机轻松承载)
|
||
|
||
### 11.2 QPS 与并发规划
|
||
|
||
| 阶段 | 预期并发 | QPS 目标 | 策略 |
|
||
|------|----------|----------|------|
|
||
| Phase 1 | 1-5 用户 | 10 QPS | 单机 + 连接池 10 |
|
||
| Phase 2 | 50-100 用户 | 100 QPS | nginx 反代 + 连接池 50 |
|
||
| Phase 3 | 1K+ 用户 | 500+ QPS | 读副本 + CDN + 缓存 |
|
||
|
||
### 11.3 性能基准
|
||
|
||
| 指标 | 目标 | 测量方式 |
|
||
|------|------|----------|
|
||
| 采集器单次运行 | < 60s | cron 日志 |
|
||
| 日报生成 | < 30s | 命令计时 |
|
||
| API 响应 P50 | < 50ms | Prometheus histogram |
|
||
| API 响应 P99 | < 200ms | Prometheus histogram |
|
||
| DB 查询(价格对比) | < 100ms | `EXPLAIN ANALYZE` |
|
||
| 前端首屏加载 | < 2s | Lighthouse |
|
||
|
||
### 11.4 扩展策略
|
||
|
||
**Phase 2 扩展点**:
|
||
1. **读副本**:PostgreSQL 流复制,分离报表查询和写入
|
||
2. **采集器并行**:按厂商拆分 goroutine,并发采集
|
||
3. **缓存层**:Redis 缓存热门查询(模型列表、价格对比)
|
||
4. **CDN**:前端静态资源 + 日报 HTML 缓存
|
||
|
||
**Phase 3 扩展点**:
|
||
1. **分库分表**:`pricing_history` 按时间分区(PostgreSQL 原生分区)
|
||
2. **对象存储**:日报 HTML 长期归档至 MinIO/S3
|
||
3. **消息队列**:采集任务入队,Worker 消费(替代 cron 直接触发)
|