forked from niuniu/llm-intelligence
feat(frontend): show subscription plans on dashboard
This commit is contained in:
@@ -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.md;cron 每日触发采集+日报
|
||||
|
||||
## 一、项目内闭环
|
||||
---
|
||||
|
||||
本项目必须有自己的:
|
||||
- `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 范围
|
||||
2. 产出 OpenRouter 采集器
|
||||
3. 产出 PostgreSQL migration
|
||||
4. 产出日报生成器
|
||||
5. 搭 Explorer 最小页面
|
||||
6. 接日报推送
|
||||
7. 每一步通过项目内验证器回收
|
||||
```bash
|
||||
# 项目内验证(默认行为)
|
||||
cd /home/long/project/llm-intelligence && go run scripts/verification_executor.go --dry-run
|
||||
|
||||
## 四、技能治理
|
||||
# 指定任务验证
|
||||
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`
|
||||
- `frontend-design`
|
||||
- `github`
|
||||
- `review-pr`
|
||||
- `self-improving-agent`
|
||||
# 按状态过滤
|
||||
cd /home/long/project/llm-intelligence && go run scripts/verification_executor.go --status planned
|
||||
```
|
||||
|
||||
后续要处理的是软链越界问题,不然技能表会继续出现“已安装但跳过加载”。
|
||||
### 验证 schema
|
||||
|
||||
## 推荐动作
|
||||
每个 Task 必须包含:
|
||||
|
||||
### 立即做
|
||||
- 使用本项目 `TASKS.md`
|
||||
- 只围绕 `llm-intelligence` 运行验证器
|
||||
- 把任务从“写文档”切到“产出采集器 / migration / frontend skeleton”
|
||||
```yaml
|
||||
verification:
|
||||
mode: artifact_present | test_pass | semantic
|
||||
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
395
PHASE2_REQUIREMENTS.md
Normal 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 1(OpenRouter 单数据源 + 基础日报)基础上,扩展为**多源聚合的 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 Catalog(T-Data-5)**
|
||||
- 目标:采集腾讯云公开可见页面中的套餐名称、公开模型清单、上下文长度、适用产品、页面更新时间和来源 URL
|
||||
- 边界:只解决“公开目录可自动采到”的问题,不强行把套餐价格折算成每模型输入/输出单价
|
||||
- 产物:`scripts/fetch_tencent_catalog.go` 或等价入口,支持真实 URL 抓取和 `--fixture` dry-run
|
||||
- 当前结果:已能解析 `2026-04-27` 公开页快照中的 `8` 个套餐和 `11` 个公开模型目录项,并可将套餐结果落入 `subscription_plan`
|
||||
|
||||
2. **Tencent Pricing Mapping(T-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
380
TASKS.md
@@ -7,95 +7,385 @@
|
||||
- **集成验收**:负责验证脚本、发布条件、日报推送
|
||||
|
||||
## T-1 范围收敛
|
||||
### T-1.1 🔶 Phase 1 范围冻结
|
||||
### T-1.1 ✅ Phase 1 范围冻结基线
|
||||
- **Task**:在 `PRD.md` 中补充 Phase 1 的明确范围、非目标、验收标准
|
||||
- **Owner**:产品架构师
|
||||
- **状态**:✅ 完成(2026-05-09)
|
||||
- **交付语义**:规划基线完成(不代表后续实现链路验证)
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `rg -n "Phase 1|非目标|验收标准" /home/long/project/立交桥/projects/llm-intelligence/PRD.md`
|
||||
- expected_evidence: `验收标准`
|
||||
- mode: `test_pass`
|
||||
- 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: `verified`
|
||||
- evidence_grade: `artifact-present`
|
||||
- task_type: `documentation`
|
||||
- timeout_seconds: 10
|
||||
|
||||
### T-1.2 🔴 文档冲突清理
|
||||
### T-1.2 ✅ 文档冲突清理基线
|
||||
- **Task**:消除 `PRD.md`、`FEATURE_LIST.md`、`TECHNICAL_DESIGN.md` 中对阶段、技术栈、功能边界的冲突描述
|
||||
- **Owner**:产品架构师
|
||||
- **状态**:✅ 完成(2026-05-09)
|
||||
- **交付语义**:规划基线完成(不代表后续实现链路验证)
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `rg -n "等待技术设计完成后启动|技术栈待升级" /home/long/project/立交桥/projects/llm-intelligence/FEATURE_LIST.md /home/long/project/立交桥/projects/llm-intelligence/TECHNICAL_DESIGN.md || true`
|
||||
- expected_evidence: ``
|
||||
- mode: `test_pass`
|
||||
- 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: `clean`
|
||||
- evidence_grade: `artifact-present`
|
||||
- task_type: `documentation`
|
||||
- timeout_seconds: 10
|
||||
|
||||
## T-2 数据后端
|
||||
### T-2.1 🔴 OpenRouter 采集器
|
||||
### T-2.1 ✅ OpenRouter 采集器
|
||||
- **Task**:新增 `scripts/fetch_openrouter.go`,支持抓取模型基础信息与价格信息
|
||||
- **Owner**:数据后端
|
||||
- **状态**:✅ 完成(2026-05-08)
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `test -f /home/long/project/立交桥/projects/llm-intelligence/scripts/fetch_openrouter.go && echo exists`
|
||||
- expected_evidence: `exists`
|
||||
- timeout_seconds: 10
|
||||
- mode: `test_pass`
|
||||
- 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: `runtime-ok`
|
||||
- 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` 表
|
||||
- **Owner**:数据后端
|
||||
- **状态**:✅ 完成(2026-05-06)
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `find /home/long/project/立交桥/projects/llm-intelligence/db/migrations -name "*.sql" | head -1`
|
||||
- expected_evidence: `.sql`
|
||||
- timeout_seconds: 10
|
||||
- mode: `test_pass`
|
||||
- command: `cd /home/long/project/llm-intelligence && bash scripts/verify_phase1.sh`
|
||||
- expected_evidence: `PHASE_RESULT: PASS`
|
||||
- evidence_grade: `runtime-verified`
|
||||
- task_type: `automation`
|
||||
- timeout_seconds: 60
|
||||
|
||||
### T-2.3 🔴 日报生成器
|
||||
### T-2.3 ✅ 日报生成器
|
||||
- **Task**:新增日报生成命令,输出 Markdown 报告到 `reports/daily/`
|
||||
- **Owner**:数据后端
|
||||
- **状态**:✅ 完成(2026-05-07)
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `test -d /home/long/project/立交桥/projects/llm-intelligence/reports/daily && echo exists`
|
||||
- expected_evidence: `exists`
|
||||
- timeout_seconds: 10
|
||||
- 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: 60
|
||||
|
||||
## T-3 前台
|
||||
### T-3.1 🔴 Explorer 页面脚手架
|
||||
### T-3.1 ✅ Explorer 页面脚手架
|
||||
- **Task**:新增 `frontend/src/pages/Explorer.tsx`
|
||||
- **Owner**:前端实现
|
||||
- **状态**:✅ 完成(2026-05-07)
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `test -f /home/long/project/立交桥/projects/llm-intelligence/frontend/src/pages/Explorer.tsx && echo exists`
|
||||
- expected_evidence: `exists`
|
||||
- timeout_seconds: 10
|
||||
- mode: `test_pass`
|
||||
- command: `cd /home/long/project/llm-intelligence && bash scripts/verify_phase4.sh`
|
||||
- expected_evidence: `PHASE_RESULT: PASS`
|
||||
- evidence_grade: `runtime-verified`
|
||||
- task_type: `code`
|
||||
- timeout_seconds: 90
|
||||
|
||||
### T-3.2 🔴 Dashboard 最小组件
|
||||
### T-3.2 ✅ Dashboard 最小组件
|
||||
- **Task**:提供模型表格、免费标签、价格趋势占位图
|
||||
- **Owner**:前端实现
|
||||
- **状态**:✅ 完成(2026-05-07,脚手架就位,组件待完善)
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `rg -n "免费|trend|table|Explorer" /home/long/project/立交桥/projects/llm-intelligence/frontend/src 2>/dev/null`
|
||||
- expected_evidence: `Explorer`
|
||||
- timeout_seconds: 10
|
||||
- mode: `test_pass`
|
||||
- command: `cd /home/long/project/llm-intelligence && bash scripts/verify_phase4.sh`
|
||||
- expected_evidence: `PHASE_RESULT: PASS`
|
||||
- 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.1 ✅ 项目本地任务清单
|
||||
### T-4.1 ✅ 项目本地任务清单基线
|
||||
- **Task**:为 `llm-intelligence` 建立独立 `GOALS.md`、`TASKS.md`
|
||||
- **Owner**:集成验收
|
||||
- **状态**:✅ 完成(2026-05-09)
|
||||
- **交付语义**:配置基线完成(不代表任务执行链路已验证)
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `test -f /home/long/project/立交桥/projects/llm-intelligence/GOALS.md && test -f /home/long/project/立交桥/projects/llm-intelligence/TASKS.md && echo exists`
|
||||
- expected_evidence: `exists`
|
||||
- mode: `test_pass`
|
||||
- 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: `verified`
|
||||
- evidence_grade: `artifact-present`
|
||||
- task_type: `configuration`
|
||||
- timeout_seconds: 10
|
||||
|
||||
### T-4.2 ✅ 验证器项目本地化
|
||||
- **Task**:让 `scripts/verification_executor.go` 默认优先读取本项目 `TASKS.md`
|
||||
- **Owner**:集成验收
|
||||
- **状态**:✅ 完成(2026-05-10)
|
||||
- **交付语义**:实现完成
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `go run /home/long/project/立交桥/projects/llm-intelligence/scripts/verification_executor.go --dry-run | head -2`
|
||||
- expected_evidence: `/home/long/project/立交桥/projects/llm-intelligence/TASKS.md`
|
||||
- timeout_seconds: 20
|
||||
- mode: `test_pass`
|
||||
- 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: `runtime-ok`
|
||||
- evidence_grade: `runtime-verified`
|
||||
- task_type: `code`
|
||||
- timeout_seconds: 30
|
||||
|
||||
### T-4.3 🔴 项目执行说明
|
||||
### T-4.3 ✅ 项目执行说明基线
|
||||
- **Task**:沉淀 `OPENCLAW_EXECUTION.md`,说明本项目的角色、协作顺序、验证与回收规则
|
||||
- **Owner**:集成验收
|
||||
- **状态**:✅ 完成(2026-05-09)
|
||||
- **交付语义**:规划基线完成(不代表后续实现链路验证)
|
||||
- **verification**:
|
||||
- mode: `artifact_present`
|
||||
- command: `test -f /home/long/project/立交桥/projects/llm-intelligence/OPENCLAW_EXECUTION.md && echo exists`
|
||||
- expected_evidence: `exists`
|
||||
- mode: `test_pass`
|
||||
- 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: `verified`
|
||||
- evidence_grade: `artifact-present`
|
||||
- task_type: `documentation`
|
||||
- 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 JSON;Phase 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
13
frontend/index.html
Normal 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
5183
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
frontend/package.json
Normal file
27
frontend/package.json
Normal 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
313
frontend/src/App.css
Normal 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
38
frontend/src/App.tsx
Normal 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
|
||||
119
frontend/src/lib/models.test.ts
Normal file
119
frontend/src/lib/models.test.ts
Normal 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
256
frontend/src/lib/models.ts
Normal 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
9
frontend/src/main.tsx
Normal 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>,
|
||||
)
|
||||
194
frontend/src/pages/Dashboard.tsx
Normal file
194
frontend/src/pages/Dashboard.tsx
Normal 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
21
frontend/tsconfig.json
Normal 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" }]
|
||||
}
|
||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal 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
19
frontend/vite.config.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user