# 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": "...", "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 │ └────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ 解析响应 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
``` --- ## 十二、快速部署参考(历史版本) > 本节保留早期简洁部署方案,生产环境请参考「八、部署与运维架构」。 ### 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 直接触发)