feat(frontend): show subscription plans on dashboard

This commit is contained in:
Your Name
2026-05-13 14:36:28 +08:00
parent ba054f04cf
commit 55e506b2b5
15 changed files with 7241 additions and 124 deletions

View File

@@ -1,110 +1,340 @@
# OpenClaw 执行诊断与修复 # OpenClaw 执行手册 — LLM Intelligence Hub
## 结论 > 本文档说明宰相AI Agent如何在本项目内执行、验证与回收任务。
> 版本v1.0
> 日期2026-05-11
> 状态:与当前代码状态对齐
`llm-intelligence` 当前的问题,**主因不是规划文档写得不够多,而是 OpenClaw 没有形成项目内执行闭环**。根因排序如下: ---
1. **协作问题最严重** ## 一、项目现状
- 项目没有本地 `GOALS.md` / `TASKS.md`
- 验证器默认读取的是全局 `~/.openclaw/workspace/TASKS.md`
- `openclaw.json` 中唯一明确绑定的 MCP `cwd` 指向 `ai-customer-service`,不是本项目
- 结果是:`llm-intelligence` 被塞进全局流程里,执行上下文被其他项目污染
2. **角色设计问题第二严重** **不再是"仅有规划文档"**
- 任务全部挂在“宰相”单角色上
- `subagents/runs.json` 为空,说明并没有真实发生多角色并行
- 文档、设计、采集器、前端、验收没有拆给不同责任面
3. **skills 问题是次要但真实存在** 当前已落地:
- 关键技能如 `code-analyzer``frontend-design``github``review-pr` 是可用的
- 但很多技能通过软链挂到 `~/.agents/skills`,被 OpenClaw 以 `symlink-escape` 拒绝加载
- 这会导致“看起来安装了,运行时却没真正可用”的错觉
## 现状误区 | 组件 | 状态 | 路径 |
|------|------|------|
| 规划文档 | ✅ | PRD.md / FEATURE_LIST.md / TECHNICAL_DESIGN.md / BUSINESS_MODEL.md |
| OpenRouter 采集器 | ✅ | `scripts/fetch_openrouter.go` — Go 实现,直写 PostgreSQL |
| 数据库迁移 | ✅ | `db/migrations/001_phase1_core_tables.sql` |
| 日报生成器 | ✅ | `scripts/generate_daily_report.go` |
| 日报产出 | ✅ | `reports/daily/` 已有 7 份 Markdown 日报 + HTML 产物 |
| 前端脚手架 | ✅ | `frontend/src/pages/Explorer.tsx` + 数据文件 |
| 项目内任务管理 | ✅ | `GOALS.md` / `TASKS.md` / `AGENTS.md` |
| 项目内记忆入口 | ✅ | `SESSION-STATE.md` / `MEMORY.md` / `memory/README.md` |
| 验证器 | ✅ | `scripts/verification_executor.go` + 4 个 verify 脚本 |
| OpenClaw Review | ✅ | `reports/openclaw/` 已有 13 份 review + backlog |
### 误区 1规划已完成执行自然会跟上 **技术栈确认**Go 1.22.2 + PostgreSQL + Vanilla JS/React前端
不是。现在仓库里主要是: ---
- `PRD.md`
- `FEATURE_LIST.md`
- `BUSINESS_MODEL.md`
- `TECHNICAL_DESIGN.md`
但没有: ## 二、角色定义
- 数据采集脚本
- `db/migrations`
- `frontend/`
- `reports/daily/`
说明执行没有从“文档阶段”切到“实现阶段”。 四个固定责任面,任务并行推进:
### 误区 2任务状态是可信的 ### 产品架构师
- **负责**PRD 维护、Phase 范围冻结、文档一致性审查
- **当前任务**:主线已完成,当前关注 Phase 2 范围定义与文档真实性维护
- **判定标准**PRD / FEATURE / TECH 三份文档无冲突描述
不是。全局 `TASKS.md` 中出现这种状态漂移: ### 数据后端
- `TECHNICAL_DESIGN.md` 已标记完成 - **负责**:采集器、数据库 schema、日报生成、数据质量
- 后续任务仍写着“等待技术设计完成后启动” - **当前任务**主线已完成当前关注数据质量、Phase 2 多数据源扩展、真实运行验证
- **判定标准**`fetch_openrouter` 可运行并写入 DB`generate_daily_report` 产出 Markdown
这是典型的任务依赖没有被回收更新。 ### 前端实现
- **负责**Explorer 页面、Dashboard 组件、数据可视化
- **当前任务**Explorer / Dashboard 最小可用已完成,腾讯云套餐订阅价已接入 Dashboard当前关注展示质量与后续增强
- **判定标准**:前端页面可展示模型表格 + 免费标记 + 筛选排序Dashboard 可独立展示腾讯云套餐订阅价
## 修复策略 ### 集成验收
- **负责**验证脚本、任务回收、日报推送、cron 集成
- **当前任务**主线已完成当前关注验证真实性、回写边界、review/cron/verifier 降噪
- **判定标准**`verification_executor --dry-run` 能读取本项目 TASKS.mdcron 每日触发采集+日报
## 一、项目内闭环 ---
本项目必须有自己的: ## 三、执行顺序(已更新)
- `GOALS.md`
- `TASKS.md`
- `scripts/verification_executor.go`
不要继续依赖全局 `~/.openclaw/workspace/TASKS.md` ```
执行基线2026-05-11
## 二、角色拆分 [✅] 1. Phase 1 范围冻结与文档冲突清理
[✅] 2. OpenRouter 采集器、数据库迁移、日报生成器落地
[✅] 3. Explorer / Dashboard 最小可用前端落地
[✅] 4. 项目内 TASKS / GOALS / verification / execution 闭环落地
[✅] 5. 自动采集 + 日报调度闭环落地
[✅] 6. Phase 6 综合验收通过(`verify_phase6.sh` PASS
[🟡] 7. OpenClaw review / cron / verifier 质量治理持续优化
[🟡] 8. Phase 2 多数据源扩展待规划
```
建议固定四个责任面 **下一步优先**
1. 提高 review / cron / verifier 的真实性与降噪质量
2. 推进 Phase 2 数据源扩展与真实验证入口
3. 收口工程纪律提交、CI、回写边界、报告一致性
- **产品架构师** ---
- 负责 PRD、Feature List、技术范围一致性
- **数据后端**
- 负责采集器、数据库、日报生成
- **前端实现**
- 负责 Explorer / Dashboard
- **集成验收**
- 负责验证器、任务回收、日报推送
角色不是为了“显得高级”,而是为了让任务能并行、状态能落地。 ## 四、验证规则
## 三、执行顺序 ### 项目内验证(优先)
按这个顺序推进: 宰相执行本项目任务时,**默认读取本项目 `TASKS.md`**,不是全局 `~/.openclaw/workspace/TASKS.md`
1. 冻结 Phase 1 范围 ```bash
2. 产出 OpenRouter 采集器 # 项目内验证(默认行为)
3. 产出 PostgreSQL migration cd /home/long/project/llm-intelligence && go run scripts/verification_executor.go --dry-run
4. 产出日报生成器
5. 搭 Explorer 最小页面
6. 接日报推送
7. 每一步通过项目内验证器回收
## 四、技能治理 # 指定任务验证
cd /home/long/project/llm-intelligence && go run scripts/verification_executor.go --task T-2.1
短期内不需要继续“装更多 skill”先把现有能力用好。 # 只验证已完成任务(推荐用于日常健康检查)
cd /home/long/project/llm-intelligence && go run scripts/verification_executor.go --completed-only
优先使用: # 按状态过滤
- `code-analyzer` cd /home/long/project/llm-intelligence && go run scripts/verification_executor.go --status planned
- `frontend-design` ```
- `github`
- `review-pr`
- `self-improving-agent`
后续要处理的是软链越界问题,不然技能表会继续出现“已安装但跳过加载”。 ### 验证 schema
## 推荐动作 每个 Task 必须包含:
### 立即做 ```yaml
- 使用本项目 `TASKS.md` verification:
- 只围绕 `llm-intelligence` 运行验证器 mode: artifact_present | test_pass | semantic
- 把任务从“写文档”切到“产出采集器 / migration / frontend skeleton” command: "精确命令"
expected_evidence: "预期输出"
evidence_grade: runtime-verified | artifact-present | doc-claimed
task_type: code | automation | documentation | configuration | data | analysis
timeout_seconds: 30
```
### 不要做 ### 验证真实性协议
- 不要继续往全局 `TASKS.md` 塞本项目任务
- 不要把所有任务都挂在单角色“宰相”名下 - 任何结论都要区分三种证据等级:
- 不要再新增一轮大而全设计文档,先把实现骨架跑起来 - `doc-claimed`:只有文档、任务表、说明文字这样写
- `artifact-present`:文件、脚本、模板、配置确实存在
- `runtime-verified`:构建、测试、接口、数据库、真实命令输出验证通过
- 对代码、脚本、自动化、调度、数据链路任务,默认**不能**只用 `doc-claimed` 或纯 `semantic` 判定完成。
- `artifact_present` 只适用于:
- 静态文档存在性
- 模板文件存在性
- 非执行型配置存在性
- 只要任务声称“可运行”“已打通”“已自动化”“已上线前可交付”,就必须至少补一条 `runtime-verified` 证据。
- review 报告里必须明确区分:
- 文档宣称完成
- 仓库产物存在
- 真实运行验证通过
- 未区分这三层时,不能把任务写成“完成”。
### 复杂任务执行协议
- 多文件、跨模块、跨角色或高风险任务,必须先拆成 checkpoints再执行。
- 每个 checkpoint 至少包含:
- 目标输出
- 预期证据
- 对应验证命令
- checkpoint 完成后:
1. 先更新 `SESSION-STATE.md`
2. 再做局部验证
3. 最后才考虑改 `TASKS.md`
- 在整个复杂任务链路中,只允许一个写者做最终任务状态回收,避免多个 agent 并发改状态。
- 如果某个 checkpoint 只完成文档或模板,而主链路运行证据尚未出现,任务状态最多更新到 `🟡`,不能直接标 `✅`
### 状态回收流程
1. 任务完成后 → 更新 `TASKS.md` 状态(🔴 → 🟡 → ✅)
2. 运行 `verification_executor` 确认通过
3. 按项目 daily memory 协议记录到 `memory/YYYY-MM-DD.md`
4. 每周复盘时 → 更新 `GOALS.md` 里程碑
### Project Daily Memory 回写协议
项目内的 `cron` / `review` / `verifier` / `main` / `worker` 在结束一个执行块后,如果需要留下可恢复归档,必须遵守以下规则:
#### 1. 写入目标
- 高频工作态只写 `SESSION-STATE.md`
- 单日归档只写 `memory/YYYY-MM-DD.md`
- 长期稳定知识只写 `MEMORY.md`
- 任务状态真相只写 `TASKS.md`
- 目标里程碑真相只写 `GOALS.md`
#### 2. 初始化规则
- 如果 `memory/YYYY-MM-DD.md` 不存在,必须先创建标准骨架:
```md
# llm-intelligence Daily Memory - YYYY-MM-DD
> 项目单日归档文件。
> 记录高价值摘要、证据、结论,不记录每条实时对话。
> 高频工作状态优先写 `SESSION-STATE.md`。
## Entries
```
- 不允许第一次写入就直接从裸 `## HH:MM ...` 开始。
- 项目内 daily memory 细则以 `memory/README.md` 为准。
#### 3. 追加格式
- 新条目只能追加在 `## Entries` 后面
- 标题格式固定为:
```md
## HH:MM - <actor> - <topic>
```
- `<actor>` 只允许:
- `main`
- `cron`
- `review`
- `verifier`
- `worker`
- 每个条目正文只允许使用四个小节:
```md
### Context
- ...
### Evidence
- ...
### Outcome
- ...
### Next
- ...
```
#### 4. 角色写法约束
- `cron`:只写调度结果、失败原因、是否需要人工介入
- `review`:只写关键发现、风险判断、建议动作
- `verifier`只写验证命令、证据、PASS/FAIL
- `main`:只写用户决策、任务切换、阶段结论
- `worker`:只写局部实现进展、阻塞、交接点
#### 5. 工具使用约束
- `memory/YYYY-MM-DD.md` 是日志型文件,默认不要优先使用脆弱的 `edit`
- 推荐流程固定为:
1. `read` 当前文件
2. 保留旧内容
3. 在末尾追加一个新时间块
4.`write` 做整文件重写
- 只有在锚点极小、唯一、且刚刚读取过的情况下,才允许对 daily memory 使用 `edit`
- 不允许把大段原始日志、整篇 review、整段命令输出直接粘进 daily memory只保留高价值摘要和可追溯证据路径
#### 6. 完成前核验
- 写完 `memory/YYYY-MM-DD.md` 后,必须立即重新读取并确认:
- 标题还在
- `## Entries` 还在
- 新条目已经落在末尾
- `<actor>` 与 section 标题格式正确
- 没有通过这一步,不能声称“已归档”
### Review 产物字段协议
- `reports/openclaw/YYYY-MM-DD-HHMM-review.md` 必须与项目 daily memory 保持同一组字段命名。
- 允许保留标题和 metadata block但除这两部分外顶层 section 只允许:
- `## Context`
- `## Evidence`
- `## Outcome`
- `## Next`
- 字段映射固定为:
- `Context`review 背景、阶段判断、时间窗口
- `Evidence`验证命令与结果、完成项、未完成项、不一致项、gap 证据
- `Outcome`:执行摘要、风险判断、阶段结论
- `Next`下一轮动作、owner、复核点
- review 模板以 `reports/openclaw/REVIEW_TEMPLATE.md` 为准;新报告默认基于该模板生成。
- 历史 review 报告允许保留旧格式,不要求批量回写;从本规则生效后生成的新报告必须遵守四段式字段协议。
- `Evidence` 段必须优先展示 `runtime-verified` 证据,其次才是 `artifact-present``doc-claimed`
### 任务写回边界
- `llm-intelligence` 的 review、cron、实施、验收任务**只允许写本项目**
- `/home/long/project/llm-intelligence/TASKS.md`
- `/home/long/project/llm-intelligence/GOALS.md`
- 明确禁止写:
- `~/.openclaw/workspace/TASKS.md`
- `~/.openclaw/workspace/GOALS.md`
- 如果只是 review不要顺手改任务状态只有当本轮真的完成了某项任务并拿到了验证证据才允许改本项目 `TASKS.md`
- 任何任务文件写回前,必须先跑预检守卫:
```bash
cd /home/long/project/llm-intelligence
bash scripts/review/preflight_task_write_guard.sh llm-intelligence-review /home/long/project/llm-intelligence/TASKS.md
bash scripts/review/preflight_task_write_guard.sh llm-intelligence-review /home/long/project/llm-intelligence/GOALS.md
```
- 如果是 cron 场景writer role 改成 `llm-intelligence-cron`;只要守卫返回非 0就必须立即停止不得继续写回。
---
## 五、文档改写规则
这条规则专门用于避免大 Markdown 文档在 Feishu 会话里出现“回复看起来完成、工具层实际失败”的假完成。
### 先读后改
- 修改 `TECHNICAL_DESIGN.md``IMPLEMENTATION_PLAN.md``TASKS.md` 这类大文件前,必须先重新读取目标文件的最新内容。
- 只有在**刚读取过且能精确定位** `oldText` 的情况下,才允许使用 `edit`
- 对共享或高频变动文件(如 `TASKS.md``OPENCLAW_CAPABILITY_BACKLOG.md`),默认假设存在并发写入风险。
### 大文件优先锚点,锚点不稳就整段重写
- 当单文件大于 50KB、变更跨越多个分散段落或旧文本包含表格/代码块/全角字符时,默认不要连续重试 `edit`
- `edit` 一旦出现 `Could not find the exact text`,必须停止复用旧的 `oldText`,改为:
- 重新读取更小范围的精确锚点后再改
- 或基于最新全文直接 `write` 重写目标文件
- **禁止**连续两次拿同一份 `oldText` 重试。
- 如果文件是共享面板或任务总表,第二次失败后直接放弃 `edit`,改为整段 `write`
### 单写者约束
- `~/.openclaw/workspace/TASKS.md``~/.openclaw/workspace/GOALS.md``main session` 独占写入。
- `llm-intelligence` agent 与它的 cron review 对全局任务面板一律只读。
- 本项目自己的 `TASKS.md` 允许本项目 agent 写入,但同一轮执行里只允许一个写者负责回收,避免多个 subagent 同时改任务表。
### 回写后必须二次核验
- 每次 `write``edit` 成功后,必须立刻重新读取目标文件。
- 至少核验三类内容:
- 目标标题是否更新
- 关键关键词是否已落盘
- 旧的冲突描述是否已消失
### 响应前提
- 没有通过“回写后二次核验”,不能在 Feishu 中声称“已完成”。
- 如果工具层失败,应明确报告“失败于 edit/write未落盘”不能用文字总结代替文件变更。
---
## 六、与立交桥其他项目的关系
| 项目 | 关系 | 注意 |
|------|------|------|
| `ai-customer-service` | 独立 | 技术栈相同Go+PostgreSQL但数据/目标完全独立 |
| `supply-intelligence` | 独立 | 小龙团队推进,宰相不直接参与 |
| `~/.openclaw/workspace/` | 全局配置 | 仅保留 GOALS/TASKS 双层管理框架,不塞本项目任务 |
---
## 七、不做清单
- ❌ 不再往全局 `~/.openclaw/workspace/TASKS.md` 塞本项目任务
- ❌ 不新增大而全的设计文档PRD/TECH 已冻结,进入执行期)
- ❌ 不引入 Python/Flask 技术栈(已统一为 Go
- ❌ Phase 1 不碰多租户、用户系统、邮件/飞书推送
---
*本文档由宰相维护,每次项目状态重大变更后更新。*

395
PHASE2_REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,395 @@
# LLM Intelligence Hub — Phase 2 需求文档 v0.1
> 文档版本v0.1
> 日期2026-05-11
> 负责人宰相AI 辅助)
> 状态Phase 2 需求收集中
> 前置依赖Phase 1 已完成并验收通过2026-05-10
---
## 一、Phase 2 目标
在 Phase 1OpenRouter 单数据源 + 基础日报)基础上,扩展为**多源聚合的 LLM 情报中心**
1. **数据源扩展**:从 1 家OpenRouter扩展到 10+ 家平台
2. **国内模型覆盖**:接入国内主流云厂商和官方 API
3. **国际模型精选**:限制 10 个最火爆模型,精准追踪
4. **来源区分**:明确标注模型来源(官方直销 / 中转 / 免费额度)
5. **日报升级**分类视频日报、CNY 统一定价、场景化推荐
---
## 二、Phase 1 已完成优化2026-05-11
### 2.1 日报生成器 v3.1 优化
| 优化项 | 之前 | 之后 |
|--------|------|------|
| **价格单位** | USD | CNY统一按 1 USD = 7.25 CNY 换算) |
| **免费模型展示** | 368 个全部列出 | 前 20 个代表性 + 国家分布统计 |
| **国际 TOP 5** | 无意义低价(全免费) | 国际推荐 TOP 5免费为主 |
| **国内 TOP 10** | 7 个模型 | 7 个模型(带场景标签) |
| **分类板块** | 无 | 代码/推理/视觉 3 大分类 |
| **HTML UI** | 简陋表格 | 现代化信息图(卡片、渐变、响应式) |
| **场景标签** | 无 | 自动识别:代码、推理、视觉、对话 |
**实现文件**`scripts/generate_daily_report.go` v3.1
### 2.2 健康检查优化
| 优化项 | 之前 | 之后 |
|--------|------|------|
| **CPU 告警** | 瞬时高负载即告警 | 持续 60s+ 或 30min 内 3 次 10s+ 才告警 |
| **OpenClaw 检测** | 仅基本状态 | 插件编译、sqlite-vec、数据库、会话堆积、日志扫描 |
| **Hermes 监控** | 无 | 进程检查、PID 校验、日志扫描、模型可用性、数据库状态 |
**实现文件**`scripts/HEALTH_CHECK_PROMPT.md`
---
## 三、Phase 2 数据源需求
### 3.1 国内模型平台(高优先级)
| 平台 | 类型 | 模型示例 | 接入方式 |
|------|------|----------|----------|
| **智谱 AI (Zhipu AI)** | 官方 | GLM-4/5 系列 | 官方 API / 定价页 |
| **百度千帆** | 云厂商中转 | ERNIE 4.0/4.5 | 官方 API / 定价页 |
| **阿里云百炼** | 云厂商中转 | Qwen 全系列 | 官方 API / 定价页 |
| **腾讯云** | 云厂商中转 | 混元、DeepSeek | Coding Plan / Token Plan |
| **华为云** | 云厂商中转 | 盘古系列 | 官方 API / 定价页 |
| **字节火山引擎** | 云厂商中转 | Doubao、Seed | 官方 API / 定价页 |
| **Moonshot AI** | 官方 | Kimi K2 系列 | 官方 API |
| **MiniMax** | 官方 | M2/M2.5 系列 | 官方 API |
| **硅基流动 (SiliconFlow)** | 聚合中转 | 多模型聚合 | API / 定价页 |
| **DeepSeek 官方** | 官方 | DeepSeek V3/R1 | 官方 API |
| **电信/移动/联通云** | 运营商中转 | 政企 Coding Plan | 官网定价页 |
### 3.2 国际模型平台(限制 10 个最火爆)
| 平台 | 类型 | 模型示例 | 优先级 |
|------|------|----------|--------|
| **OpenAI** | 官方 | GPT-5.5, GPT-5.4, o3, o4 | P0 |
| **Anthropic** | 官方 | Claude Opus 4.7, Sonnet 4.6 | P0 |
| **Google** | 官方 | Gemini 2.5 Pro, Lyria 3 | P0 |
| **xAI** | 官方 | Grok 4.1, Grok 4 | P0 |
| **Meta** | 官方 | Llama 4 Maverick, Llama 4 Scout | P1 |
| **Mistral AI** | 官方 | Mistral Large 3, Codestral | P1 |
| **Cohere** | 官方 | Command A, Command R+ | P2 |
| **AI21 Labs** | 官方 | Jamba Large | P2 |
| **Together AI** | 聚合中转 | 多模型聚合 | P2 |
| **Groq** | 聚合中转 | 极速推理 | P2 |
**原则**:国际不超过 10 个平台聚焦最火爆模型商。OpenRouter 作为兜底聚合源保留。
### 3.3 来源区分体系
```
模型来源标识:
├── official官方直销
│ ├── OpenAI API
│ ├── 阿里云百炼
│ ├── 腾讯云
│ └── ...
├── reseller中转/聚合)
│ ├── OpenRouter
│ ├── SiliconFlow
│ ├── Together AI
│ └── ...
└── free_tier免费额度
├── 免费额度说明
├── 限流规则
└── 有效期
```
**数据库字段扩展**
- `region_pricing.source_type`: official / reseller / free_tier
- `region_pricing.free_quota`: 免费额度描述
- `region_pricing.free_limitations`: 免费限制条件JSON 数组)
- `region_pricing.rate_limit`: 限流规则
---
## 四、日报升级需求
### 4.1 视频日报T-Video-1
**目标**:按分类生成短视频日报,每个分类 30 秒
| 分类 | 内容 | 时长 |
|------|------|------|
| 代码模型日报 | 今日代码模型动态、价格变动 | 30s |
| 推理模型日报 | o3/o4/R1 等推理模型更新 | 30s |
| 视觉模型日报 | 多模态模型新上线/降价 | 30s |
| 国内模型日报 | 智谱/百度/阿里等国内动态 | 30s |
| 国际热点日报 | Top 10 国际模型价格变动 | 30s |
**技术方案**
1. 复用日报分类数据
2. 文本转语音TTS生成配音
3. HTML 截图/录屏生成视频帧
4. 拼接为完整视频
### 4.2 日报内容增强
| 增强项 | 说明 |
|--------|------|
| **价格变动追踪** | 对比昨日价格,标注涨跌 |
| **新模型上线** | 今日新入库模型列表 |
| **免费政策变更** | 免费额度调整、新免费模型 |
| **场景推荐** | 按场景(代码/写作/推理/视觉)推荐最优模型 |
| **性价比排行** | 按 $/1M tokens 性价比排序 |
---
## 五、数据采集器规划
### 5.1 采集器清单(已就绪 / 开发中)
| 采集器 | 目标平台 | 优先级 | 状态 | 文件 |
|--------|----------|--------|------|------|
| `fetch_multi_source.go` | OpenRouter + Moonshot + DeepSeek + OpenAI | P0 | ✅ 已完成(支持 `--sources` / `--dry-run` | `scripts/fetch_multi_source.go` |
| `fetch_zhipu.go` | 智谱 AI | P0 | ⏸️ 待开发 | - |
| `fetch_baidu.go` | 百度千帆 | P0 | ⏸️ 待开发 | - |
| `fetch_aliyun.go` | 阿里云百炼 | P0 | ⏸️ 待开发 | - |
| `fetch_tencent_catalog.go` | 腾讯云公开目录 / Token Plan 公共页 | P0 | ✅ 已完成(支持真实 URL / `--fixture` dry-run | `scripts/fetch_tencent_catalog.go` |
| `tencent_pricing_mapping` | 腾讯云 Token Plan / Coding Plan 套餐映射设计 | P0 | ✅ 已完成(`subscription_plan` 方案已确定) | `subscription_plan` |
| `fetch_huawei.go` | 华为云 | P1 | ⏸️ 待开发 | - |
| `fetch_bytedance.go` | 火山引擎 | P1 | ⏸️ 待开发 | - |
| `fetch_siliconflow.go` | 硅基流动 | P1 | ⏸️ 待开发 | - |
| `fetch_anthropic.go` | Anthropic | P0 | ⏸️ 待开发 | - |
### 5.2 统一采集接口
```go
type DataSource interface {
Name() string // 来源名称
FetchModels() ([]ModelInfo, error) // 抓取模型列表
FetchPricing() ([]RegionPricing, error) // 抓取定价
SourceType() string // official / reseller
FreeTier() (*FreeTierInfo, error) // 免费额度信息
}
```
### 5.3 腾讯云拆分策略
腾讯云当前不再适合继续作为一个模糊的“待开发采集器”处理,而要拆成两个独立阶段:
1. **Tencent Public CatalogT-Data-5**
- 目标:采集腾讯云公开可见页面中的套餐名称、公开模型清单、上下文长度、适用产品、页面更新时间和来源 URL
- 边界:只解决“公开目录可自动采到”的问题,不强行把套餐价格折算成每模型输入/输出单价
- 产物:`scripts/fetch_tencent_catalog.go` 或等价入口,支持真实 URL 抓取和 `--fixture` dry-run
- 当前结果:已能解析 `2026-04-27` 公开页快照中的 `8` 个套餐和 `11` 个公开模型目录项,并可将套餐结果落入 `subscription_plan`
2. **Tencent Pricing MappingT-Data-6**
- 目标:明确 `Token Plan` / `Coding Plan` 的价格如何入库、如何展示、如何验收
- 约束:腾讯云公开页以套餐订阅价为主,不是现有 `region_pricing.input_price_per_mtok / output_price_per_mtok` 擅长承载的按量单价模型
- 设计结论:新增 `subscription_plan` 表,单独保存订阅型价格,而不是把套餐信息硬塞进 `region_pricing`
**为什么不能继续复用 `region_pricing`**
- `region_pricing` 的主语是“一个模型在一个区域/运营商下的按量价格”,核心字段是 `model_id + input_price_per_mtok + output_price_per_mtok`
- 腾讯云 `Token Plan` / `Coding Plan` 的主语是“一个可售套餐”,覆盖多个模型,共享月度额度,不存在稳定的一对一 `model_id`
- `request_price` 也不足以表达腾讯云套餐,因为它仍假设“单次请求价格”,而不是“月付 + 共享 token 配额”
- 当前日报和 API 都默认把 `region_pricing` 当作“单模型价格排行”数据源;如果把套餐硬塞进去,会制造虚假的单模型单价,污染排行榜和比价结果
**设计决策:**
- `region_pricing` 继续只承载按模型的按量价格、免费额度和限流信息
- 腾讯云 `Token Plan` / `Coding Plan` 进入新表 `subscription_plan`
- 后续日报/API 若要展示腾讯云套餐,走独立“套餐订阅价”区块,不进入按模型低价排行
### 5.4 `subscription_plan` DDL 草案
```sql
CREATE TABLE subscription_plan (
id BIGSERIAL PRIMARY KEY,
provider_id BIGINT NOT NULL REFERENCES model_provider(id),
operator_id BIGINT REFERENCES operator(id),
plan_family TEXT NOT NULL CHECK (plan_family IN ('token_plan', 'coding_plan')),
plan_code TEXT NOT NULL,
plan_name TEXT NOT NULL,
tier TEXT NOT NULL,
billing_cycle TEXT NOT NULL DEFAULT 'monthly',
currency TEXT NOT NULL DEFAULT 'CNY',
list_price REAL NOT NULL CHECK (list_price >= 0),
price_unit TEXT NOT NULL,
quota_value BIGINT,
quota_unit TEXT,
context_window INTEGER,
plan_scope TEXT,
model_scope TEXT NOT NULL DEFAULT '[]',
source_url TEXT NOT NULL,
published_at TIMESTAMP,
effective_date DATE,
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (provider_id, plan_code, effective_date)
);
```
**推荐的 `subscription_plan` 字段草案:**
- `id`
- `provider_id`
- `operator_id`
- `plan_family`
- `plan_code`
- `plan_name`
- `tier`
- `billing_cycle`
- `currency`
- `list_price`
- `price_unit`
- `quota_value`
- `quota_unit`
- `model_scope`
- `context_window`
- `plan_scope`
- `source_url`
- `published_at`
- `notes`
### 5.5 腾讯云套餐映射规则
1. **一行代表一个可售套餐**
- 例如 `通用 Token Plan / Lite`
- 例如 `Hy Token Plan / Max`
2. **不为套餐伪造模型单价**
- 不根据套餐价格反推 `input_price_per_mtok`
- 不把 `月费 / 套餐额度` 近似写成某个模型的输入/输出单价
3. **模型覆盖范围写入 `model_scope`**
- `model_scope` 保存当前套餐公开支持的模型清单
- 建议以 JSON 数组字符串形式保存,例如 `["glm-5","glm-5.1","hunyuan-t1"]`
4. **`context_window` 仅保存套餐页明确声明的上限**
- 如果页面只说明某个模型支持 `256K`,则写在对应套餐行的 `context_window`
- 页面未明确给出时允许为空
5. **公开目录采集与正式落库分离**
- `fetch_tencent_catalog.go` 负责提取公共页信息
- 后续导入脚本或迁移任务负责写入 `subscription_plan`
6. **日报/API 展示边界**
- 日报新增“腾讯云套餐订阅价”区块
- `/api/v1/models` 继续只返回模型级价格
- 套餐信息通过独立接口 `/api/v1/subscription-plans` 暴露
### 5.6 后续实施入口
- `T-Data-7`:新增 `subscription_plan` 迁移与导入链路
- `T-Data-8`:✅ 日报展示腾讯云套餐订阅价摘要
- `T-Data-9`:✅ API 暴露 `subscription_plan` 查询入口
- `T-3.3`:✅ Dashboard 已消费 `/api/v1/subscription-plans`,前端独立展示腾讯云套餐订阅价
---
## 六、验收标准
### Phase 2 完成条件
1. **数据源覆盖**:≥ 10 家平台接入(国内 7+,国际 3+
2. **模型总量**:≥ 500 个模型条目(当前 377
3. **国内模型**:≥ 50 个国内付费模型(当前 7
4. **来源区分**:所有模型标注 official/reseller/free_tier
5. **日报升级**
- CNY 统一定价 ✅
- 分类展示 ✅
- 场景标签 ✅
- 视频日报原型 ✅GIF + WAV 原型)
6. **更新频率**:每日 08:00 自动触发,覆盖所有数据源
---
## 七、任务清单(已导入 TASKS.md
### 数据源主线
- `T-Data-1`:✅ 规划基线完成
- `T-Data-2`:✅ 多源采集器入口落地
- `T-Data-3`:✅ 国内厂商种子与来源字段落库
- `T-Data-4`:✅ Phase 2 多源采集验收
- `T-Data-5`:✅ 腾讯云公开目录采集入口
- `T-Data-6`:✅ 腾讯云 Token Plan 套餐映射设计
- `T-Data-7`:✅ 腾讯云套餐表迁移与导入
- `T-Data-8`:✅ 日报接入腾讯云套餐订阅价
- `T-Data-9`:✅ 套餐订阅价独立 API
### 前端消费主线
- `T-3.3`:✅ Dashboard 接入套餐订阅价
### 视频日报主线
- `T-Video-1`:✅ 规划基线完成
- `T-Video-2`:✅ 视频日报生成 pipeline 落地GIF + WAV 原型)
- `T-Video-3`:✅ 视频日报端到端验收脚本
---
## 附录已抓取价格数据2026-05-11
### 抓取状态汇总
| 平台 | 类型 | 状态 | 已抓取模型数 | 说明 |
|------|------|------|-------------|------|
| **OpenRouter** | 国际聚合 | ✅ 完整 | 365 | 采集器已就绪 `fetch_multi_source.go` |
| **智谱 AI** | 国内 official | ✅ 完整 | 29 | 无头浏览器抓取 + 手动整理入库 |
| **百度千帆** | 国内 official | ✅ 完整 | 44 | 无头浏览器抓取 + 解析入库 |
| **Moonshot (Kimi)** | 国内 official | ✅ 完整 | 3 | 采集器已就绪 |
| **DeepSeek** | 国内 official | ✅ 完整 | 2 | 采集器已就绪 |
| **OpenAI** | 国际 official | ✅ 完整 | 3 | 采集器已就绪 |
| **阿里云百炼** | 国内 reseller | ⚠️ 部分 | 8+ | 模型列表已抓取,定价需登录 |
| **腾讯云** | 国内 reseller | ✅ 目录/套餐/API/前端 已接入 | 11公开目录 + 8套餐落库 | 公共页已可解析;`subscription_plan` 已落 8 条腾讯云套餐记录,已进入日报独立套餐区块,并可通过 `/api/v1/subscription-plans` 查询Dashboard 已独立展示套餐订阅价;模型级价格仍单独走 `region_pricing` |
| **华为云** | 国内 reseller | ❌ 受限 | 0 | 404 未找到定价页 |
| **字节火山引擎** | 国内 reseller | ✅ 完整 | 43 | 无头浏览器抓取 + 解析入库 |
| **硅基流动** | 国内聚合 | ❌ 受限 | 0 | 需要登录 |
| **Anthropic** | 国际 official | ❌ 受限 | 0 | 页面动态渲染 + 区域限制 |
### 已抓取完整数据
#### Moonshot (Kimi) - official
| 模型 | 输入(缓存命中) | 输入(缓存未命中) | 输出 | 上下文 |
|------|---------------|-----------------|------|--------|
| kimi-k2.6 | ¥1.10 | ¥6.50 | ¥27.00 | 262,144 |
| kimi-k2-0905-preview | ¥1.00 | ¥4.00 | ¥16.00 | 262,144 |
| kimi-k2-0711-preview | ¥1.00 | ¥4.00 | ¥16.00 | 131,072 |
| kimi-k2-turbo-preview | ¥1.00 | ¥8.00 | ¥58.00 | 262,144 |
| kimi-k2-thinking | ¥1.00 | ¥4.00 | ¥16.00 | 262,144 |
| moonshot-v1-8k | ¥2.00 | - | ¥10.00 | 8,192 |
| moonshot-v1-32k | ¥5.00 | - | ¥20.00 | 32,768 |
| moonshot-v1-128k | ¥10.00 | - | ¥30.00 | 131,072 |
#### DeepSeek - official
| 模型 | 输入(缓存命中) | 输入(缓存未命中) | 输出 | 上下文 |
|------|---------------|-----------------|------|--------|
| deepseek-v4-flash | $0.0028 | $0.14 | $0.28 | 1M |
| deepseek-v4-pro | $0.003625 | $0.435 | $0.87 | 1M |
**注意**deepseek-v4-pro 当前 75% 折扣(至 2026/05/31
#### OpenAI - official
| 模型 | 输入 | 缓存输入 | 输出 |
|------|------|----------|------|
| GPT-5.5 | $5.00 | $0.50 | $30.00 |
| GPT-5.4 | $2.50 | $0.25 | $15.00 |
| GPT-5.4 mini | $0.75 | $0.075 | $4.50 |
#### 阿里云百炼 - reseller/cloud模型列表价格待抓取
- qwen3.6-max-preview, qwen3.6-plus, qwen3.6-flash
- deepseek-v4-pro, deepseek-v4-flash, kimi-k2.6
- glm-5.1, MiniMax-M2.7
### 受限平台解决策略
| 平台 | 解决方式 | 优先级 |
|------|----------|--------|
| 智谱 AI | 尝试 API 接口 / 模拟浏览器请求 | P0 |
| 百度千帆 | 查找子页面 / 使用 API 文档 | P0 |
| 腾讯云 | 先做公开目录采集,再设计 Token Plan / Coding Plan 套餐映射;必要时单独新增 `subscription_plan` 表 | P0 |
| 华为云 | 查找正确的定价文档 URL | P1 |
| 字节火山引擎 | 使用 headless 浏览器 / API 接口 | P1 |
| 硅基流动 | 登录后抓取 / 使用 API 文档 | P1 |
| Anthropic | 使用 API 端点 / headless 浏览器 | P0 |
---
*本文档随需求变化持续更新。最后更新2026-05-13*

380
TASKS.md
View File

@@ -7,95 +7,385 @@
- **集成验收**:负责验证脚本、发布条件、日报推送 - **集成验收**:负责验证脚本、发布条件、日报推送
## T-1 范围收敛 ## T-1 范围收敛
### T-1.1 🔶 Phase 1 范围冻结 ### T-1.1 Phase 1 范围冻结基线
- **Task**:在 `PRD.md` 中补充 Phase 1 的明确范围、非目标、验收标准 - **Task**:在 `PRD.md` 中补充 Phase 1 的明确范围、非目标、验收标准
- **Owner**:产品架构师 - **Owner**:产品架构师
- **状态**:✅ 完成2026-05-09
- **交付语义**:规划基线完成(不代表后续实现链路验证)
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `rg -n "Phase 1|非目标|验收标准" /home/long/project/立交桥/projects/llm-intelligence/PRD.md` - command: `grep -q "Phase 1" /home/long/project/llm-intelligence/PRD.md && grep -q "非目标" /home/long/project/llm-intelligence/PRD.md && grep -q "验收标准" /home/long/project/llm-intelligence/PRD.md && echo verified`
- expected_evidence: `验收标准` - expected_evidence: `verified`
- evidence_grade: `artifact-present`
- task_type: `documentation`
- timeout_seconds: 10 - timeout_seconds: 10
### T-1.2 🔴 文档冲突清理 ### T-1.2 文档冲突清理基线
- **Task**:消除 `PRD.md``FEATURE_LIST.md``TECHNICAL_DESIGN.md` 中对阶段、技术栈、功能边界的冲突描述 - **Task**:消除 `PRD.md``FEATURE_LIST.md``TECHNICAL_DESIGN.md` 中对阶段、技术栈、功能边界的冲突描述
- **Owner**:产品架构师 - **Owner**:产品架构师
- **状态**:✅ 完成2026-05-09
- **交付语义**:规划基线完成(不代表后续实现链路验证)
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `rg -n "等待技术设计完成后启动|技术栈待升级" /home/long/project/立交桥/projects/llm-intelligence/FEATURE_LIST.md /home/long/project/立交桥/projects/llm-intelligence/TECHNICAL_DESIGN.md || true` - command: `if grep -qE "等待技术设计完成后启动|技术栈待升级" /home/long/project/llm-intelligence/FEATURE_LIST.md /home/long/project/llm-intelligence/TECHNICAL_DESIGN.md; then exit 1; fi; echo clean`
- expected_evidence: `` - expected_evidence: `clean`
- evidence_grade: `artifact-present`
- task_type: `documentation`
- timeout_seconds: 10 - timeout_seconds: 10
## T-2 数据后端 ## T-2 数据后端
### T-2.1 🔴 OpenRouter 采集器 ### T-2.1 OpenRouter 采集器
- **Task**:新增 `scripts/fetch_openrouter.go`,支持抓取模型基础信息与价格信息 - **Task**:新增 `scripts/fetch_openrouter.go`,支持抓取模型基础信息与价格信息
- **Owner**:数据后端 - **Owner**:数据后端
- **状态**:✅ 完成2026-05-08
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `test -f /home/long/project/立交桥/projects/llm-intelligence/scripts/fetch_openrouter.go && echo exists` - command: `cd /home/long/project/llm-intelligence && go test -tags llm_script scripts/fetch_openrouter.go scripts/fetch_openrouter_test.go -run "TestParseModels|TestRunNoAPIKey" >/tmp/llm_task_t21.out 2>&1 && echo runtime-ok`
- expected_evidence: `exists` - expected_evidence: `runtime-ok`
- timeout_seconds: 10 - evidence_grade: `runtime-verified`
- task_type: `code`
- timeout_seconds: 60
### T-2.2 🔴 PostgreSQL migration ### T-2.2 PostgreSQL migration
- **Task**:新增 `db/migrations`,落地 `models``model_prices``report_runs` - **Task**:新增 `db/migrations`,落地 `models``model_prices``report_runs`
- **Owner**:数据后端 - **Owner**:数据后端
- **状态**:✅ 完成2026-05-06
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `find /home/long/project/立交桥/projects/llm-intelligence/db/migrations -name "*.sql" | head -1` - command: `cd /home/long/project/llm-intelligence && bash scripts/verify_phase1.sh`
- expected_evidence: `.sql` - expected_evidence: `PHASE_RESULT: PASS`
- timeout_seconds: 10 - evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 60
### T-2.3 🔴 日报生成器 ### T-2.3 日报生成器
- **Task**:新增日报生成命令,输出 Markdown 报告到 `reports/daily/` - **Task**:新增日报生成命令,输出 Markdown 报告到 `reports/daily/`
- **Owner**:数据后端 - **Owner**:数据后端
- **状态**:✅ 完成2026-05-07
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `test -d /home/long/project/立交桥/projects/llm-intelligence/reports/daily && echo exists` - command: `cd /home/long/project/llm-intelligence && bash scripts/verify_phase3.sh`
- expected_evidence: `exists` - expected_evidence: `PHASE_RESULT: PASS`
- timeout_seconds: 10 - evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 60
## T-3 前台 ## T-3 前台
### T-3.1 🔴 Explorer 页面脚手架 ### T-3.1 Explorer 页面脚手架
- **Task**:新增 `frontend/src/pages/Explorer.tsx` - **Task**:新增 `frontend/src/pages/Explorer.tsx`
- **Owner**:前端实现 - **Owner**:前端实现
- **状态**:✅ 完成2026-05-07
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `test -f /home/long/project/立交桥/projects/llm-intelligence/frontend/src/pages/Explorer.tsx && echo exists` - command: `cd /home/long/project/llm-intelligence && bash scripts/verify_phase4.sh`
- expected_evidence: `exists` - expected_evidence: `PHASE_RESULT: PASS`
- timeout_seconds: 10 - evidence_grade: `runtime-verified`
- task_type: `code`
- timeout_seconds: 90
### T-3.2 🔴 Dashboard 最小组件 ### T-3.2 Dashboard 最小组件
- **Task**:提供模型表格、免费标签、价格趋势占位图 - **Task**:提供模型表格、免费标签、价格趋势占位图
- **Owner**:前端实现 - **Owner**:前端实现
- **状态**:✅ 完成2026-05-07脚手架就位组件待完善
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `rg -n "免费|trend|table|Explorer" /home/long/project/立交桥/projects/llm-intelligence/frontend/src 2>/dev/null` - command: `cd /home/long/project/llm-intelligence && bash scripts/verify_phase4.sh`
- expected_evidence: `Explorer` - expected_evidence: `PHASE_RESULT: PASS`
- timeout_seconds: 10 - evidence_grade: `runtime-verified`
- task_type: `code`
- timeout_seconds: 90
### T-3.3 ✅ Dashboard 接入套餐订阅价
- **Task**:让 Dashboard 读取 `/api/v1/subscription-plans`,把腾讯云套餐订阅价作为独立区块展示,并与模型价格排行分开展示
- **Owner**:前端实现
- **状态**:✅ 完成2026-05-13
- **依赖**`T-3.2``T-Data-9`
- **结果**Dashboard 已新增腾讯云套餐订阅价区块、套餐数量摘要和最低月费摘要;前端已支持 `subscription_plan` 归一化、额度格式化和接口异常降级
- **交付语义**:实现完成,代表前端已正式消费套餐订阅价 API用户无需再只从日报查看腾讯云套餐
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence/frontend && npm test -- --run >/tmp/llm_task_t33_test.log 2>&1 && npm run build >/tmp/llm_task_t33_build.log 2>&1 && echo runtime-ok`
- expected_evidence: `runtime-ok`
- evidence_grade: `runtime-verified`
- task_type: `code`
- timeout_seconds: 180
## T-4 OpenClaw 闭环 ## T-4 OpenClaw 闭环
### T-4.1 ✅ 项目本地任务清单 ### T-4.1 ✅ 项目本地任务清单基线
- **Task**:为 `llm-intelligence` 建立独立 `GOALS.md``TASKS.md` - **Task**:为 `llm-intelligence` 建立独立 `GOALS.md``TASKS.md`
- **Owner**:集成验收 - **Owner**:集成验收
- **状态**:✅ 完成2026-05-09
- **交付语义**:配置基线完成(不代表任务执行链路已验证)
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `test -f /home/long/project/立交桥/projects/llm-intelligence/GOALS.md && test -f /home/long/project/立交桥/projects/llm-intelligence/TASKS.md && echo exists` - command: `test -f /home/long/project/llm-intelligence/GOALS.md && test -f /home/long/project/llm-intelligence/TASKS.md && grep -q "## G-1" /home/long/project/llm-intelligence/GOALS.md && grep -q "## T-1" /home/long/project/llm-intelligence/TASKS.md && echo verified`
- expected_evidence: `exists` - expected_evidence: `verified`
- evidence_grade: `artifact-present`
- task_type: `configuration`
- timeout_seconds: 10 - timeout_seconds: 10
### T-4.2 ✅ 验证器项目本地化 ### T-4.2 ✅ 验证器项目本地化
- **Task**:让 `scripts/verification_executor.go` 默认优先读取本项目 `TASKS.md` - **Task**:让 `scripts/verification_executor.go` 默认优先读取本项目 `TASKS.md`
- **Owner**:集成验收 - **Owner**:集成验收
- **状态**:✅ 完成2026-05-10
- **交付语义**:实现完成
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `go run /home/long/project/立交桥/projects/llm-intelligence/scripts/verification_executor.go --dry-run | head -2` - command: `cd /home/long/project/llm-intelligence && go run -tags llm_script scripts/verification_executor.go --dry-run --tasks /home/long/project/llm-intelligence/TASKS.md | grep -q "/home/long/project/llm-intelligence/TASKS.md" && echo runtime-ok`
- expected_evidence: `/home/long/project/立交桥/projects/llm-intelligence/TASKS.md` - expected_evidence: `runtime-ok`
- timeout_seconds: 20 - evidence_grade: `runtime-verified`
- task_type: `code`
- timeout_seconds: 30
### T-4.3 🔴 项目执行说明 ### T-4.3 项目执行说明基线
- **Task**:沉淀 `OPENCLAW_EXECUTION.md`,说明本项目的角色、协作顺序、验证与回收规则 - **Task**:沉淀 `OPENCLAW_EXECUTION.md`,说明本项目的角色、协作顺序、验证与回收规则
- **Owner**:集成验收 - **Owner**:集成验收
- **状态**:✅ 完成2026-05-09
- **交付语义**:规划基线完成(不代表后续实现链路验证)
- **verification**: - **verification**:
- mode: `artifact_present` - mode: `test_pass`
- command: `test -f /home/long/project/立交桥/projects/llm-intelligence/OPENCLAW_EXECUTION.md && echo exists` - command: `grep -q "验证真实性协议" /home/long/project/llm-intelligence/OPENCLAW_EXECUTION.md && grep -q "复杂任务执行协议" /home/long/project/llm-intelligence/OPENCLAW_EXECUTION.md && grep -q "Review 产物字段协议" /home/long/project/llm-intelligence/OPENCLAW_EXECUTION.md && echo verified`
- expected_evidence: `exists` - expected_evidence: `verified`
- evidence_grade: `artifact-present`
- task_type: `documentation`
- timeout_seconds: 10 - timeout_seconds: 10
## T-5 生产级收口
### T-5.1 ✅ 生产级实施计划基线
- **Task**:将 `IMPLEMENTATION_PLAN.md` 升级为生产级实施计划,显式补充国内厂商覆盖、数据质量规则、容错降级、审计日志
- **Owner**:产品架构师
- **状态**:✅ 完成2026-05-10
- **交付语义**:规划基线完成(不代表各项实现已交付)
- **verification**:
- mode: `test_pass`
- command: `grep -q "国内厂商" /home/long/project/llm-intelligence/IMPLEMENTATION_PLAN.md && grep -q "数据质量" /home/long/project/llm-intelligence/IMPLEMENTATION_PLAN.md && grep -q "降级" /home/long/project/llm-intelligence/IMPLEMENTATION_PLAN.md && grep -q "审计日志" /home/long/project/llm-intelligence/IMPLEMENTATION_PLAN.md && echo verified`
- expected_evidence: `verified`
- evidence_grade: `artifact-present`
- task_type: `documentation`
- timeout_seconds: 10
### T-5.2 ✅ 任务清单与实施计划基线
- **Task**:补齐 `TASKS.md` 中缺失的生产级收口任务,避免只停留在早期 4 组骨架任务
- **Owner**:集成验收
- **状态**:✅ 完成2026-05-10
- **交付语义**:规划基线完成(不代表各项实现已交付)
- **verification**:
- mode: `test_pass`
- command: `grep -q "生产级收口" /home/long/project/llm-intelligence/TASKS.md && grep -q "环境变量与真实数据链路" /home/long/project/llm-intelligence/TASKS.md && grep -q "前端构建系统初始化" /home/long/project/llm-intelligence/TASKS.md && grep -q "自动采集与日报调度" /home/long/project/llm-intelligence/TASKS.md && echo verified`
- expected_evidence: `verified`
- evidence_grade: `artifact-present`
- task_type: `documentation`
- timeout_seconds: 10
### T-5.3 ✅ 环境变量与真实数据链路打通
- **Task**:配置 `OPENROUTER_API_KEY``DATABASE_URL`,验证真实采集、真实写库、真实日报链路
- **Owner**:数据后端
- **状态**:✅ 完成2026-05-10
- **结果**:已完成真实 OpenRouter 采集、PostgreSQL 写库和日报生成;`2026-05-10 21:22` 实测 API 拉取 `367` 条,当前库内 `models=377``report_runs=2`
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && bash scripts/run_real_pipeline.sh`
- expected_evidence: `采集完成`
- evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 120
### T-5.4 ✅ 前端构建系统初始化
- **Task**:补齐 `frontend/package.json``tsconfig.json`、构建脚本,确保 Explorer 不再只是孤立代码片段
- **Owner**:前端实现
- **状态**:✅ 完成2026-05-10
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence/frontend && npm run build >/tmp/llm_task_t54_build.log 2>&1 && echo runtime-ok`
- expected_evidence: `runtime-ok`
- evidence_grade: `runtime-verified`
- task_type: `code`
- timeout_seconds: 90
### T-5.5 ✅ 自动采集与日报调度
- **Task**:补齐 cron 或等价调度入口,形成真实的每日采集与日报生成闭环
- **Owner**:集成验收
- **状态**:✅ 完成2026-05-10
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && bash scripts/verify_phase3.sh`
- expected_evidence: `PHASE_RESULT: PASS`
- evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 30
### T-Video-1 ✅ 视频日报原型规划基线
- **Task**:在 `PHASE2_REQUIREMENTS.md` 中冻结视频日报的分类、时长、技术方案和前置依赖
- **Owner**:产品架构师
- **状态**:✅ 完成2026-05-11
- **交付语义**:规划基线完成
- **verification**:
- mode: `test_pass`
- command: `grep -q "### 4.1 视频日报T-Video-1" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && grep -q "代码模型日报" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && grep -q "文本转语音" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && grep -q "Phase 2 数据源接入" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && echo verified`
- expected_evidence: `verified`
- evidence_grade: `artifact-present`
- task_type: `documentation`
- timeout_seconds: 10
- **后续实现入口**:视频生成 pipeline、TTS 集成和视频验收脚本后续单独拆实施任务
### T-Video-2 ✅ 视频日报生成 pipeline 落地
- **Task**:实现 `scripts/generate_video_digest.go` 或等价入口,按分类生成视频脚本、配音文本和待渲染素材清单
- **Owner**:数据后端
- **状态**:✅ 完成2026-05-11
- **依赖**`T-Video-1``T-2.3`
- **结果**:已生成 5 组 digest cards、分镜脚本、PNG 帧、manifest、GIF 视频原型和 WAV 旁白音轨
- **交付语义**:实现完成,代表视频日报原型已具备可运行的素材与渲染入口(当前产物为 GIF + WAV 原型)
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && go run -tags llm_script scripts/generate_video_digest.go --report /home/long/project/llm-intelligence/reports/daily/daily_report_2026-05-11.md --output-dir /home/long/project/llm-intelligence/reports/daily/video/2026-05-11 | grep -q "cards=5" && echo runtime-ok`
- expected_evidence: `runtime-ok`
- evidence_grade: `runtime-verified`
- task_type: `code`
- timeout_seconds: 60
### T-Video-3 ✅ 视频日报端到端验收脚本
- **Task**:补齐视频日报真实验收入口,验证脚本生成、素材产出、音频生成和最终视频文件落盘
- **Owner**:集成验收
- **状态**:✅ 完成2026-05-11
- **结果**`verify_video_pipeline.sh` 已能端到端核验 manifest、5 组脚本、5 张 PNG 帧、GIF 视频原型和 WAV 音轨
- **交付语义**:实现完成,代表视频日报原型链路已通过端到端验收
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && bash scripts/verify_video_pipeline.sh`
- expected_evidence: `VIDEO_PIPELINE: PASS`
- evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 120
## Phase 2 数据源扩展需求
### T-Data-1 ✅ 国内云厂商价格采集规划基线
- **Task**:在 `PHASE2_REQUIREMENTS.md` 中冻结国内平台清单、来源区分体系、统一采集接口和 Phase 2 完成条件
- **Owner**:产品架构师
- **状态**:✅ 完成2026-05-11
- **交付语义**:规划基线完成
- **verification**:
- mode: `test_pass`
- command: `grep -q "### 3.1 国内模型平台" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && grep -q "智谱 AI" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && grep -q "### 3.3 来源区分体系" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && grep -q "type DataSource interface" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && grep -q "### Phase 2 完成条件" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && echo verified`
- expected_evidence: `verified`
- evidence_grade: `artifact-present`
- task_type: `documentation`
- timeout_seconds: 10
- **后续实现入口**:各平台采集器、真实 API 验证和 Phase 2 验收脚本后续单独拆实施任务
### T-Data-2 ✅ 多源采集器入口落地
- **Task**:完善 `scripts/fetch_multi_source.go`,让 Moonshot、DeepSeek、OpenAI 等多源采集可独立构建并具备统一入口
- **Owner**:数据后端
- **状态**:✅ 完成2026-05-11
- **结果**:已支持 `--sources` / `--dry-run` / `--list-sources`,可在不写库的情况下运行静态多源采集并输出摘要
- **交付语义**:实现完成,代表 Phase 2 已具备多源采集执行入口
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && go run -tags llm_script scripts/fetch_multi_source.go --dry-run --sources moonshot,deepseek,openai | grep -q "sources=3 successful_sources=3 models=8 domestic_models=5 currencies=CNY:3,USD:5" && echo runtime-ok`
- expected_evidence: `runtime-ok`
- evidence_grade: `runtime-verified`
- task_type: `code`
- timeout_seconds: 60
### T-Data-3 ✅ 国内厂商种子与来源字段落库
- **Task**:落地 `source_type/free_quota/free_limitations/rate_limit` 字段和国内厂商种子数据,确保 CNY 定价与来源标识可查询
- **Owner**:数据后端
- **状态**:✅ 完成2026-05-11
- **依赖**`T-Data-1``T-2.2`
- **交付语义**:实现完成后,才代表 Phase 2 数据模型能承载国内厂商和来源区分
- **verification**:
- mode: `test_pass`
- command: `test "$(psql -d llm_intelligence -Atqc "select count(*) from information_schema.columns where table_name='region_pricing' and column_name in ('source_type','free_quota','free_limitations','rate_limit');")" -ge 4 && test "$(psql -d llm_intelligence -Atqc "select count(*) from model_provider where country='CN';")" -ge 7 && test "$(psql -d llm_intelligence -Atqc "select count(*) from region_pricing where currency='CNY';")" -ge 10 && echo runtime-ok`
- expected_evidence: `runtime-ok`
- evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 60
### T-Data-4 ✅ Phase 2 多源采集验收
- **Task**:以 `scripts/verify_phase2.sh` 为主入口完成国内厂商覆盖、CNY 定价、采集成功统计和审计记录的真实验收
- **Owner**:集成验收
- **状态**:✅ 完成2026-05-11
- **结果**`verify_phase2.sh` 已通过国内厂商覆盖、CNY 定价、采集成功统计和模型审计记录均满足 Phase 2 门禁
- **交付语义**:实现完成,代表 Phase 2 数据源扩展已通过集成验收
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && bash scripts/verify_phase2.sh`
- expected_evidence: `PHASE_RESULT: PASS`
- evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 120
### T-Data-5 ✅ 腾讯云公开目录采集入口
- **Task**:新增 `scripts/fetch_tencent_catalog.go` 或等价入口,采集腾讯云公开可见的模型清单、套餐名称、套餐价格、上下文长度、适用范围和来源 URL
- **Owner**:数据后端
- **状态**:✅ 完成2026-05-13
- **依赖**`T-Data-1``T-Data-3`
- **结果**:已新增 `scripts/fetch_tencent_catalog.go`,支持真实 URL 抓取和 `--fixture` 离线 dry-run当前可解析 `2026-04-27` 公开页快照中的 `8` 个套餐和 `11` 个模型目录项
- **交付语义**:实现完成,代表腾讯云公开目录信息已进入自动采集链路;不代表 Token Plan 套餐已完成现有价格模型映射或正式落库
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && go run -tags llm_script scripts/tencent_catalog_lib.go scripts/fetch_tencent_catalog.go --dry-run --fixture /home/long/project/llm-intelligence/scripts/testdata/tencent_token_plan_sample.txt | grep -q "source=tencent-public-catalog updated_at=2026-04-27 17:18:02 plans=8 models=11 series=Hy Token Plan:4,通用 Token Plan:4 dry_run=true" && echo runtime-ok`
- expected_evidence: `runtime-ok`
- evidence_grade: `runtime-verified`
- task_type: `code`
- timeout_seconds: 60
### T-Data-6 ✅ 腾讯云 Token Plan 套餐映射设计
- **Task**:明确腾讯云 `Token Plan` / `Coding Plan` 的价格模型映射方案,判断是扩展 `region_pricing` 兼容套餐信息,还是新增 `subscription_plan` 表单独承载订阅型价格
- **Owner**:产品架构师
- **状态**:✅ 完成2026-05-13
- **依赖**`T-Data-5`
- **结果**:已确认 `region_pricing` 继续只承载按模型按量价格,腾讯云 `Token Plan` / `Coding Plan` 单独进入 `subscription_plan`;同时明确了 DDL 草案、映射规则、日报/API 展示边界
- **交付语义**:规划基线完成,代表腾讯云套餐价格的落库路径和验收边界已被明确;不代表数据已自动入库
- **verification**:
- mode: `test_pass`
- command: `grep -q "CREATE TABLE subscription_plan" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && grep -q "为什么不能继续复用" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && grep -q "/api/v1/subscription-plans" /home/long/project/llm-intelligence/PHASE2_REQUIREMENTS.md && echo verified`
- expected_evidence: `verified`
- evidence_grade: `artifact-present`
- task_type: `documentation`
- timeout_seconds: 10
### T-Data-7 ✅ 腾讯云套餐表迁移与导入
- **Task**:新增 `subscription_plan` 数据库迁移和腾讯云套餐导入入口,把 `fetch_tencent_catalog.go` 解析出的公开目录结果正式落到数据库
- **Owner**:数据后端
- **状态**:✅ 完成2026-05-13
- **依赖**`T-Data-5``T-Data-6`
- **结果**:已新增 `db/migrations/005_subscription_plan.sql``scripts/import_tencent_subscription.go`;基于公开目录 fixture 已真实落库 `8` 条腾讯云套餐记录
- **交付语义**:实现完成,代表腾讯云公开目录信息已正式落库,可供日报和 API 后续消费
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && grep -q "source=tencent-subscription-import updated_at=2026-04-27 17:18:02 plans=8 provider=Tencent operator=Tencent Cloud table_rows=8 dry_run=false" reports/verification/tencent_subscription_import_latest.txt && go test -tags llm_script scripts/tencent_catalog_lib.go scripts/import_tencent_subscription.go scripts/import_tencent_subscription_test.go >/tmp/llm_tdata7_test.log 2>&1 && echo runtime-ok`
- expected_evidence: `runtime-ok`
- evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 90
### T-Data-8 ✅ 日报接入腾讯云套餐订阅价
- **Task**:让 `generate_daily_report.go` 读取 `subscription_plan`,在日报 Markdown/HTML 中新增“腾讯云套餐订阅价”区块,并明确该区块不参与按模型价格排行
- **Owner**:数据后端
- **状态**:✅ 完成2026-05-13
- **依赖**`T-Data-7`
- **结果**:日报生成器已新增腾讯云套餐订阅价区块;`2026-05-13` 的 Markdown/HTML 日报都能展示 `8` 条套餐记录,且未混入模型价格排行
- **交付语义**:实现完成,代表腾讯云订阅型套餐价格已进入日报消费链路,但 API 独立查询入口仍待后续任务
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && go test -tags llm_script scripts/generate_daily_report.go scripts/generate_daily_report_test.go >/tmp/llm_tdata8_test.log 2>&1 && TODAY=$(date +%F) && grep -q "## 💳 腾讯云套餐订阅价" reports/daily/daily_report_${TODAY}.md && grep -q "3500万 Tokens/月" reports/daily/daily_report_${TODAY}.md && grep -q "腾讯云套餐订阅价" reports/daily/html/daily_report_${TODAY}.html && echo runtime-ok`
- expected_evidence: `runtime-ok`
- evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 120
### T-Data-9 ✅ 套餐订阅价独立 API
- **Task**:为 `subscription_plan` 增加独立 API `/api/v1/subscription-plans`,让前端和外部调用方可直接查询套餐数据,而不是只能从日报里读取
- **Owner**:数据后端
- **状态**:✅ 完成2026-05-13
- **依赖**`T-Data-7`
- **结果**`cmd/server` 已新增 `/api/v1/subscription-plans` 路由与查询逻辑,返回 `subscription_plan` 的 envelope JSONPhase 6 验收脚本已纳入新接口检查
- **交付语义**:实现完成,代表腾讯云套餐订阅价已具备独立 API 查询入口;前端消费和展示增强仍可后续单独演进
- **verification**:
- mode: `test_pass`
- command: `cd /home/long/project/llm-intelligence && go test ./cmd/server >/tmp/llm_tdata9_test.log 2>&1 && bash scripts/verify_phase6.sh`
- expected_evidence: `PHASE_RESULT: PASS`
- evidence_grade: `runtime-verified`
- task_type: `automation`
- timeout_seconds: 180

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LLM Intelligence Hub</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5183
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
frontend/package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "llm-intelligence-hub",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 5173",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest"
},
"dependencies": {
"echarts": "^5.5.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
"lighthouse": "^13.3.0",
"typescript": "^5.4.5",
"vite": "^5.2.13",
"vitest": "^1.6.0"
}
}

313
frontend/src/App.css Normal file
View File

@@ -0,0 +1,313 @@
/* LLM Intelligence Hub - App Styles */
.app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #e5e7eb;
}
.navbar h1 {
margin: 0;
font-size: 24px;
color: #1f2937;
}
.tabs {
display: flex;
gap: 12px;
}
.tabs button {
padding: 8px 16px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.tabs button:hover {
background: #f3f4f6;
}
.tabs button.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.main {
min-height: 600px;
}
.filters {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
}
.filters select {
padding: 8px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
}
.count {
color: #4b5563;
font-size: 14px;
}
.model-table {
width: 100%;
border-collapse: collapse;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.model-table th,
.model-table td {
padding: 12px 14px;
border-bottom: 1px solid #e5e7eb;
text-align: left;
vertical-align: top;
}
.model-table th {
background: #f9fafb;
color: #111827;
font-size: 14px;
}
.model-table tr.free {
background: #f0fdf4;
}
.model-table tr.stale {
background: #fff7ed;
}
.model-name {
font-weight: 600;
color: #111827;
}
.model-id {
margin-top: 4px;
color: #6b7280;
font-size: 12px;
word-break: break-all;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
text-transform: lowercase;
}
.status-fresh {
background: #dcfce7;
color: #166534;
}
.status-stale {
background: #fed7aa;
color: #9a3412;
}
.pagination {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
}
.pagination button {
padding: 8px 14px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
cursor: pointer;
}
.pagination button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.dashboard h2 {
margin: 0 0 16px;
color: #111827;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
padding: 18px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
}
.stat-card.subscription {
border-color: #f59e0b;
background: linear-gradient(180deg, #fffbeb 0%, #fff7ed 100%);
}
.stat-value {
font-size: 28px;
font-weight: 700;
line-height: 1.1;
color: #111827;
}
.stat-label {
margin-top: 8px;
color: #4b5563;
font-size: 14px;
}
.chart-container {
margin-bottom: 24px;
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #fff;
}
.subscription-section {
padding: 20px;
border: 1px solid #fde68a;
border-radius: 12px;
background: linear-gradient(180deg, #fffdf5 0%, #ffffff 100%);
}
.subscription-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.subscription-header h3 {
margin: 0;
color: #111827;
}
.subscription-header p {
margin: 6px 0 0;
color: #6b7280;
font-size: 14px;
}
.subscription-summary {
display: inline-flex;
flex-wrap: wrap;
gap: 10px;
padding: 10px 14px;
border-radius: 999px;
background: #fff7ed;
color: #9a3412;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
}
.subscription-table {
width: 100%;
border-collapse: collapse;
background: #fff;
border: 1px solid #f3f4f6;
border-radius: 10px;
overflow: hidden;
}
.subscription-table th,
.subscription-table td {
padding: 12px 14px;
border-bottom: 1px solid #f3f4f6;
text-align: left;
vertical-align: top;
}
.subscription-table th {
background: #fffbeb;
color: #111827;
font-size: 14px;
}
.subscription-table tbody tr:last-child td {
border-bottom: none;
}
.plan-name {
font-weight: 600;
color: #111827;
}
.plan-meta {
margin-top: 4px;
color: #6b7280;
font-size: 12px;
line-height: 1.4;
}
.subscription-empty {
padding: 18px;
border: 1px dashed #d1d5db;
border-radius: 10px;
background: #f9fafb;
color: #6b7280;
text-align: center;
}
@media (max-width: 768px) {
.app {
padding: 16px;
}
.navbar {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.subscription-header {
flex-direction: column;
}
.subscription-summary {
white-space: normal;
}
.subscription-table {
display: block;
overflow-x: auto;
}
}

38
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { useState } from 'react'
import Explorer from './pages/Explorer'
import Dashboard from './pages/Dashboard'
import './App.css'
type Tab = 'dashboard' | 'explorer'
function App() {
const [activeTab, setActiveTab] = useState<Tab>('dashboard')
return (
<div className="app">
<nav className="navbar">
<h1>🤖 LLM Intelligence Hub</h1>
<div className="tabs">
<button
className={activeTab === 'dashboard' ? 'active' : ''}
onClick={() => setActiveTab('dashboard')}
>
📊 Dashboard
</button>
<button
className={activeTab === 'explorer' ? 'active' : ''}
onClick={() => setActiveTab('explorer')}
>
🔍 Explorer
</button>
</div>
</nav>
<main className="main">
{activeTab === 'dashboard' && <Dashboard />}
{activeTab === 'explorer' && <Explorer />}
</main>
</div>
)
}
export default App

View File

@@ -0,0 +1,119 @@
import { describe, expect, it } from 'vitest'
import {
formatPrice,
formatSubscriptionQuota,
normalizeModel,
normalizeSubscriptionPlan,
providerDistribution,
summarizeModels,
summarizeSubscriptionPlans,
} from './models'
describe('models helpers', () => {
it('normalizes fallback pricing and stale flags', () => {
const model = normalizeModel({
id: 'anthropic/claude-sonnet-4.6',
provider_cn: 'Anthropic',
context_length: 200000,
input_price: '3',
output_price: '15',
data_confidence: 'stale',
})
expect(model).not.toBeNull()
expect(model?.providerCN).toBe('Anthropic')
expect(model?.inputPrice).toBe(3)
expect(model?.outputPrice).toBe(15)
expect(model?.stale).toBe(true)
expect(model?.pricingAvailable).toBe(true)
})
it('marks free models and pricing unavailable correctly', () => {
const freeModel = normalizeModel({
id: 'qwen/qwen3-coder:free',
})
const paidModel = normalizeModel({
id: 'openai/gpt-4.1',
pricing: {},
})
expect(formatPrice(freeModel!, 'input')).toContain('免费')
expect(formatPrice(paidModel!, 'input')).toBe('pricing unavailable')
})
it('summarizes providers and currencies', () => {
const models = [
normalizeModel({ id: 'deepseek/deepseek-chat', provider_cn: 'DeepSeek', currency: 'CNY', pricing: { input: 1, output: 2 } }),
normalizeModel({ id: 'deepseek/deepseek-reasoner', provider_cn: 'DeepSeek', currency: 'CNY', pricing: { input: 2, output: 4 } }),
normalizeModel({ id: 'anthropic/claude-sonnet-4.6', provider_cn: 'Anthropic', currency: 'USD', pricing: { input: 3, output: 15 } }),
].filter((model): model is NonNullable<typeof model> => model !== null)
expect(summarizeModels(models)).toEqual({
modelCount: 3,
providerCount: 2,
cnyCount: 2,
})
expect(providerDistribution(models)).toEqual([
{ name: 'DeepSeek', value: 2 },
{ name: 'Anthropic', value: 1 },
])
})
it('normalizes subscription plans from API payload', () => {
const plan = normalizeSubscriptionPlan({
planCode: 'token-plan-lite',
planName: '通用 Token Plan Lite',
planFamily: 'token_plan',
tier: 'Lite',
provider: 'Tencent',
providerCN: '腾讯',
operator: 'Tencent Cloud',
operatorCN: '腾讯云',
currency: 'CNY',
listPrice: 39,
priceUnit: 'CNY/month',
quotaValue: 35000000,
quotaUnit: 'tokens/month',
contextWindow: 0,
modelScope: ['tc-code-latest', 'glm-5', 'glm-5.1'],
})
expect(plan).not.toBeNull()
expect(plan?.planCode).toBe('token-plan-lite')
expect(plan?.providerCN).toBe('腾讯')
expect(plan?.modelScope.length).toBe(3)
expect(plan?.modelPreview).toBe('tc-code-latest, glm-5, glm-5.1')
})
it('formats subscription quotas and summarizes plan stats', () => {
const plans = [
normalizeSubscriptionPlan({
planCode: 'token-plan-lite',
planName: '通用 Token Plan Lite',
providerCN: '腾讯',
listPrice: 39,
quotaValue: 35000000,
quotaUnit: 'tokens/month',
modelScope: ['tc-code-latest', 'glm-5', 'glm-5.1'],
}),
normalizeSubscriptionPlan({
planCode: 'hy-token-plan-max',
planName: 'Hy Token Plan Max',
providerCN: '腾讯',
listPrice: 468,
quotaValue: 650000000,
quotaUnit: 'tokens/month',
contextWindow: 262144,
modelScope: ['hy3-preview'],
}),
].filter((plan): plan is NonNullable<typeof plan> => plan !== null)
expect(formatSubscriptionQuota(plans[0].quotaValue, plans[0].quotaUnit)).toBe('3500万 Tokens/月')
expect(formatSubscriptionQuota(plans[1].quotaValue, plans[1].quotaUnit)).toBe('6.5亿 Tokens/月')
expect(summarizeSubscriptionPlans(plans)).toEqual({
planCount: 2,
providerCount: 1,
minMonthlyPrice: 39,
})
})
})

256
frontend/src/lib/models.ts Normal file
View File

@@ -0,0 +1,256 @@
export interface Model {
id: string
name: string
provider: string
providerCN: string
modality: string
contextLength: number
inputPrice: number
outputPrice: number
currency: string
isFree: boolean
stale: boolean
pricingAvailable: boolean
dataConfidence: string
}
export interface SubscriptionPlan {
planFamily: string
planCode: string
planName: string
tier: string
provider: string
providerCN: string
operator: string
operatorCN: string
currency: string
listPrice: number
priceUnit: string
quotaValue: number
quotaUnit: string
contextWindow: number
modelScope: string[]
modelCount: number
modelPreview: string
sourceUrl: string
publishedAt: string
effectiveDate: string
}
export function deriveProviderLabel(id: string, fallback?: string) {
if (fallback) {
return fallback
}
const provider = id.split('/')[0] || 'unknown'
return provider.replace(/[-_]/g, ' ')
}
export function deriveModelName(id: string, fallback?: string) {
if (fallback) {
return fallback
}
const raw = id.split('/')[1] || id
return raw.replace(/:free$/, '')
}
export function deriveModality(raw: any) {
if (typeof raw.modality === 'string' && raw.modality) {
return raw.modality
}
const capabilities = Array.isArray(raw.capabilities) ? raw.capabilities : []
if (capabilities.includes('vision')) {
return 'multimodal'
}
if (capabilities.includes('code')) {
return 'code'
}
return 'text'
}
export function normalizePrice(value: unknown) {
if (typeof value === 'number') {
return value
}
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
return null
}
export function normalizeModel(raw: any): Model | null {
const id = typeof raw?.id === 'string' ? raw.id : ''
if (!id) {
return null
}
const inputPrice = normalizePrice(raw.inputPrice ?? raw.input_price ?? raw.pricing?.input)
const outputPrice = normalizePrice(raw.outputPrice ?? raw.output_price ?? raw.pricing?.output)
const isFree = Boolean(raw.isFree ?? raw.is_free ?? id.endsWith(':free') ?? false)
const pricingAvailable = isFree || (inputPrice !== null && outputPrice !== null)
const dataConfidence = typeof raw.dataConfidence === 'string'
? raw.dataConfidence
: (typeof raw.data_confidence === 'string' ? raw.data_confidence : 'official')
return {
id,
name: deriveModelName(id, raw.name),
provider: deriveProviderLabel(id, raw.provider),
providerCN: deriveProviderLabel(id, raw.providerCN ?? raw.provider_cn ?? raw.provider),
modality: deriveModality(raw),
contextLength: Number(raw.contextLength ?? raw.context_length ?? 0),
inputPrice: inputPrice ?? 0,
outputPrice: outputPrice ?? 0,
currency: raw.currency ?? raw.pricing?.currency ?? 'USD',
isFree,
stale: Boolean(raw.stale ?? dataConfidence === 'stale'),
pricingAvailable,
dataConfidence,
}
}
export async function loadFallbackModels() {
const sources = [
() => import('../data/latest_models.json'),
() => import('../data/models.json'),
]
for (const load of sources) {
try {
const module = await load()
const raw = module.default as any
const arr: any[] = Array.isArray(raw) ? raw : (raw.models || [])
const normalized = arr
.map(normalizeModel)
.filter((model: Model | null): model is Model => model !== null)
if (normalized.length > 0) {
return normalized
}
} catch {
// 继续尝试下一个回退源
}
}
return []
}
export function formatPrice(model: Model, kind: 'input' | 'output') {
if (model.isFree) {
return '🆓 免费'
}
if (!model.pricingAvailable) {
return 'pricing unavailable'
}
const value = kind === 'input' ? model.inputPrice : model.outputPrice
return `${value} ${model.currency}/M`
}
export function summarizeModels(models: Model[]) {
const providerSet = new Set<string>()
let cnyCount = 0
for (const model of models) {
if (model.providerCN) {
providerSet.add(model.providerCN)
}
if (model.currency === 'CNY') {
cnyCount++
}
}
return {
modelCount: models.length,
providerCount: providerSet.size,
cnyCount,
}
}
export function normalizeSubscriptionPlan(raw: any): SubscriptionPlan | null {
const planCode = typeof raw?.planCode === 'string' ? raw.planCode : ''
if (!planCode) {
return null
}
const modelScope = Array.isArray(raw.modelScope)
? raw.modelScope.filter((value: unknown): value is string => typeof value === 'string' && value.length > 0)
: []
return {
planFamily: typeof raw.planFamily === 'string' ? raw.planFamily : 'token_plan',
planCode,
planName: typeof raw.planName === 'string' ? raw.planName : planCode,
tier: typeof raw.tier === 'string' ? raw.tier : '',
provider: typeof raw.provider === 'string' ? raw.provider : 'unknown',
providerCN: typeof raw.providerCN === 'string' ? raw.providerCN : deriveProviderLabel(planCode, raw.providerCN ?? raw.provider),
operator: typeof raw.operator === 'string' ? raw.operator : 'unknown',
operatorCN: typeof raw.operatorCN === 'string' ? raw.operatorCN : deriveProviderLabel(planCode, raw.operatorCN ?? raw.operator),
currency: typeof raw.currency === 'string' ? raw.currency : 'CNY',
listPrice: normalizePrice(raw.listPrice) ?? 0,
priceUnit: typeof raw.priceUnit === 'string' ? raw.priceUnit : 'CNY/month',
quotaValue: typeof raw.quotaValue === 'number' ? raw.quotaValue : Number(raw.quotaValue ?? 0),
quotaUnit: typeof raw.quotaUnit === 'string' ? raw.quotaUnit : '',
contextWindow: Number(raw.contextWindow ?? 0),
modelScope,
modelCount: typeof raw.modelCount === 'number' ? raw.modelCount : modelScope.length,
modelPreview: typeof raw.modelPreview === 'string'
? raw.modelPreview
: modelScope.slice(0, 3).join(', '),
sourceUrl: typeof raw.sourceUrl === 'string' ? raw.sourceUrl : '',
publishedAt: typeof raw.publishedAt === 'string' ? raw.publishedAt : '',
effectiveDate: typeof raw.effectiveDate === 'string' ? raw.effectiveDate : '',
}
}
export function formatSubscriptionQuota(value: number, unit: string) {
if (!value || value <= 0) {
return '-'
}
if (unit === 'tokens/month') {
if (value >= 100000000 && value % 100000000 === 0) {
return `${value / 100000000}亿 Tokens/月`
}
if (value >= 100000000) {
return `${(value / 100000000).toFixed(1)}亿 Tokens/月`
}
if (value >= 10000 && value % 10000 === 0) {
return `${value / 10000}万 Tokens/月`
}
}
return `${value} ${unit}`
}
export function summarizeSubscriptionPlans(plans: SubscriptionPlan[]) {
const providerSet = new Set<string>()
let minMonthlyPrice = Number.POSITIVE_INFINITY
for (const plan of plans) {
if (plan.providerCN) {
providerSet.add(plan.providerCN)
}
if (plan.listPrice > 0 && plan.listPrice < minMonthlyPrice) {
minMonthlyPrice = plan.listPrice
}
}
return {
planCount: plans.length,
providerCount: providerSet.size,
minMonthlyPrice: Number.isFinite(minMonthlyPrice) ? minMonthlyPrice : 0,
}
}
export function providerDistribution(models: Model[]) {
const counts = new Map<string, number>()
for (const model of models) {
const key = model.providerCN || model.provider || 'Unknown'
counts.set(key, (counts.get(key) || 0) + 1)
}
return Array.from(counts.entries())
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value)
.slice(0, 8)
}

9
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,194 @@
import { useEffect, useRef, useState } from 'react'
import * as echarts from 'echarts'
import {
formatSubscriptionQuota,
loadFallbackModels,
normalizeModel,
normalizeSubscriptionPlan,
providerDistribution,
summarizeModels,
summarizeSubscriptionPlans,
type Model,
type SubscriptionPlan,
} from '../lib/models'
function Dashboard() {
const chartRef = useRef<HTMLDivElement>(null)
const [modelCount, setModelCount] = useState(0)
const [providerCount, setProviderCount] = useState(0)
const [cnyCount, setCnyCount] = useState(0)
const [subscriptionPlans, setSubscriptionPlans] = useState<SubscriptionPlan[]>([])
const [planCount, setPlanCount] = useState(0)
const [planMinPrice, setPlanMinPrice] = useState(0)
useEffect(() => {
let chart: echarts.ECharts | null = null
let disposed = false
const renderChart = (models: Model[]) => {
if (!chartRef.current) {
return
}
chart = echarts.init(chartRef.current)
const option: echarts.EChartsOption = {
title: { text: '厂商模型分布', left: 'center' },
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
radius: '60%',
data: providerDistribution(models),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
}
chart.setOption(option)
}
const updateStats = (models: Model[]) => {
const summary = summarizeModels(models)
setModelCount(summary.modelCount)
setProviderCount(summary.providerCount)
setCnyCount(summary.cnyCount)
renderChart(models)
}
const updatePlans = (plans: SubscriptionPlan[]) => {
const summary = summarizeSubscriptionPlans(plans)
setSubscriptionPlans(plans)
setPlanCount(summary.planCount)
setPlanMinPrice(summary.minMonthlyPrice)
}
const loadModels = async () => {
try {
const response = await fetch('/api/v1/models')
if (!response.ok) {
throw new Error(`models request failed: ${response.status}`)
}
const payload = await response.json()
const rawModels: any[] = Array.isArray(payload?.data) ? payload.data : []
const models = rawModels
.map(normalizeModel)
.filter((model: Model | null): model is Model => model !== null)
if (!disposed) {
updateStats(models)
}
} catch {
const fallback = await loadFallbackModels()
if (!disposed) {
updateStats(fallback)
}
}
}
const loadSubscriptionPlans = async () => {
try {
const response = await fetch('/api/v1/subscription-plans')
if (!response.ok) {
throw new Error(`subscription plans request failed: ${response.status}`)
}
const payload = await response.json()
const rawPlans: any[] = Array.isArray(payload?.data) ? payload.data : []
const plans = rawPlans
.map(normalizeSubscriptionPlan)
.filter((plan: SubscriptionPlan | null): plan is SubscriptionPlan => plan !== null)
if (!disposed) {
updatePlans(plans)
}
} catch {
if (!disposed) {
updatePlans([])
}
}
}
void loadModels()
void loadSubscriptionPlans()
return () => {
disposed = true
chart?.dispose()
}
}, [])
return (
<div className="dashboard">
<h2>📊 </h2>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-value">{modelCount}</div>
<div className="stat-label"></div>
</div>
<div className="stat-card">
<div className="stat-value">{providerCount}</div>
<div className="stat-label"></div>
</div>
<div className="stat-card">
<div className="stat-value">{cnyCount}</div>
<div className="stat-label">CNY定价</div>
</div>
<div className="stat-card subscription">
<div className="stat-value">{planCount}</div>
<div className="stat-label"></div>
</div>
</div>
<div className="chart-container">
<div ref={chartRef} style={{ width: '100%', height: '400px' }} />
</div>
<section className="subscription-section">
<div className="subscription-header">
<div>
<h3>💳 </h3>
<p></p>
</div>
{planCount > 0 && (
<div className="subscription-summary">
<span>{planCount} </span>
<span> ¥{planMinPrice.toFixed(0)}/</span>
</div>
)}
</div>
{subscriptionPlans.length === 0 ? (
<div className="subscription-empty"></div>
) : (
<table className="subscription-table">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{subscriptionPlans.map(plan => (
<tr key={plan.planCode}>
<td>
<div className="plan-name">{plan.planName}</div>
<div className="plan-meta">{plan.operatorCN || plan.operator}</div>
</td>
<td>¥{plan.listPrice.toFixed(2)}/</td>
<td>{formatSubscriptionQuota(plan.quotaValue, plan.quotaUnit)}</td>
<td>{plan.contextWindow > 0 ? `${Math.round(plan.contextWindow / 1024)}K` : '-'}</td>
<td>
<div>{plan.modelCount} </div>
{plan.modelPreview && <div className="plan-meta">{plan.modelPreview}</div>}
</td>
</tr>
))}
</tbody>
</table>
)}
</section>
</div>
)
}
export default Dashboard

21
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

19
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
}
}
},
build: {
outDir: 'dist',
sourcemap: true,
}
})