feat: 任务进度实时展示、接口测试、暗色主题重构及多项 bug 修复

后端
- 新增 app/task_progress.py 线程安全进度注册表
- 任务改为后台线程异步执行(_run_task_background),手动触发立即返回 task_key
- 6 个任务函数(summarizer/tagger/scorer/deduplicator/brief/taxonomy)循环内上报进度
- scheduler 定时任务同步上报进度(trigger=scheduled)
- 新增 GET /api/tasks/progress 与 POST /api/tasks/progress/reset 接口
- 新增 POST /api/test-connection 接口连通性测试(独立短超时客户端)
- 修复 ai_client/rss_client 配置在 import 时固化的 bug(改为 property 运行时读取 settings),
  导致实际任务用 .env 假 key 调 LLM 401
- 修复 ai_client 对 reasoning 模型(MiniMax-M3 等)输出 <think> 块的 JSON 解析失败
- 修复 taxonomy bootstrap:LLM 超时(改用 300s 专用 client)、MiniMax 输出审查
  (精简样本仅标题 + 约束生成中性类目名)、失败误报 success(改抛异常如实标记)
- 修复 models.py 双外键关系映射启动崩溃(显式 foreign_keys)
- 修复 main.py SPA 路由 404、ArticleOut.published_at 序列化 500
- 移除 lifespan 同步 bootstrap 阻塞启动,改由 scheduler 后台异步执行

前端
- Deep Ink 高对比度暗色主题重构,修复 Element Plus 暗色模式对比度问题
- Tasks 页面任务进度实时展示(进度条/阶段/计数/状态/触发来源)+ 1.5s 轮询
- 接口测试面板(rssKeeper / LLM 连通性 + 延迟)
- 修复 nextJobs jobId 映射 bug

部署与文档
- Dockerfile 优化(BuildKit 缓存挂载、预编译 wheel、去 gcc、阿里云镜像源)
- 新增 API.md 接口文档

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
congsh
2026-06-14 15:14:40 +08:00
parent bae47a2411
commit 778ccefb22
24 changed files with 1853 additions and 312 deletions
+434
View File
@@ -0,0 +1,434 @@
# dataClean 接口文档
> 服务地址:`http://<host>:7331`
> 所有 `/api/*` 接口(除 `/health`)在配置了 `API_TOKEN` 时需要在请求头携带 `Authorization: Bearer <token>`。
## 目录
- [鉴权](#鉴权)
- [健康检查](#健康检查)
- [文章接口](#文章接口)
- [简报接口](#简报接口)
- [分类体系接口](#分类体系接口)
- [任务接口](#任务接口)
- [任务进度](#任务进度)
- [接口连通性测试](#接口连通性测试)
- [配置管理接口](#配置管理接口)
- [仪表盘统计](#仪表盘统计)
- [错误码](#错误码)
---
## 鉴权
若服务端配置了 `API_TOKEN`,除 `/health` 外所有接口需要在请求头携带:
```
Authorization: Bearer <API_TOKEN>
```
未携带或 token 无效时返回 `401` / `403`。未配置 `API_TOKEN` 时不启用鉴权(仅建议内网使用)。
---
## 健康检查
### `GET /health`
服务存活探针,无需鉴权。
**响应**
```json
{ "status": "ok", "service": "dataClean" }
```
---
## 文章接口
### `GET /api/articles`
分页查询加工后的文章,支持按日期、分类、标签过滤。
**查询参数**
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `date` | string | 否 | - | 日期 `YYYY-MM-DD`,按 `fetched_at` 过滤当天 |
| `category` | string | 否 | - | 精确分类名 |
| `tag` | string | 否 | - | 标签名(JSON 数组精确匹配) |
| `representative_only` | bool | 否 | `false` | 仅返回重复组代表文章 |
| `limit` | int | 否 | `50` | 1200 |
| `offset` | int | 否 | `0` | 分页偏移 |
**响应** `ArticleListOut`
```json
{
"total": 200,
"items": [
{
"id": 1,
"rk_article_id": 28124,
"title": "文章标题",
"link": "https://...",
"feed_title": "来源",
"category": "科技",
"tags": ["AI", "芯片"],
"heat_score": 45.2,
"importance_score": 60.0,
"duplication_score": 25.0,
"composite_score": 52.56,
"ai_summary": "AI 生成的摘要",
"is_representative": false,
"published_at": "2026-06-13T13:48:42"
}
]
}
```
### `GET /api/articles/{article_id}`
获取单篇文章详情。
**路径参数** `article_id`int
**响应** `ArticleOut`(同上 items 元素)
**错误** `404` 文章不存在
---
## 简报接口
### `GET /api/briefs`
列出每日简报(按日期倒序)。
**查询参数** `limit`int,默认 30,范围 1100
**响应** `List[BriefOut]`
```json
[
{
"id": 1,
"brief_date": "2026-06-13",
"total_articles": 200,
"unique_articles": 150,
"by_category": { "科技": [{...}], "财经": [{...}] },
"markdown_path": "/app/data/briefs/2026-06-13/daily-brief.md"
}
]
```
### `GET /api/briefs/{date}`
获取指定日期简报。
**路径参数** `date`string`YYYY-MM-DD`
**响应** `BriefOut` **错误** `404` 简报不存在
### `POST /api/briefs/{date}/regenerate`
强制重新生成指定日期简报。同步执行(需持任务锁)。
**响应**
```json
{ "message": "简报已重新生成", "data": { ... } }
```
**错误** `409` 已有任务执行中
---
## 分类体系接口
### `GET /api/taxonomy`
列出分类/标签/打分规则。
**查询参数** `kind`string,可选,过滤类型:`category` / `tag` / `heat_rule` / `importance_rule` / `duplication_rule`
**响应** `List[TaxonomyOut]`
```json
[
{
"id": 1,
"name": "科技",
"kind": "category",
"description": "人工智能、芯片、互联网等",
"keywords": ["AI", "芯片", "大模型"],
"weight": 1.0,
"created_by_ai": true
}
]
```
### `POST /api/taxonomy/bootstrap`
初始化或强制重建分类体系(后台异步执行)。
**查询参数** `force`bool,默认 `false``true` 时清空后重建。
**响应**(立即返回)
```json
{ "message": "taxonomy 初始化已开始", "task_key": "bootstrap_taxonomy" }
```
**错误** `409` 已有任务执行中。可通过 [`GET /api/tasks/progress`](#任务进度) 查看 `bootstrap_taxonomy` 进度。
---
## 任务接口
所有任务接口均为**后台异步执行**:提交后立即返回 `task_key`,任务在线程池执行,通过[进度接口](#任务进度)轮询。
任务全局互斥(共享 `_task_lock`):同一时刻仅一个任务运行。
### `POST /api/tasks/summarize`
拉取 rssKeeper 最近 24 小时文章,为无摘要/短摘要文章生成 AI 摘要。
**响应**
```json
{ "message": "摘要任务已开始", "task_key": "summarize" }
```
**错误** `409` 已有任务执行中
### `POST /api/tasks/tag-score-dedup`
对当天文章执行:分类打标 → 去重 → 打分(三阶段,进度合并显示)。
**响应**
```json
{ "message": "分类/去重/打分任务已开始", "task_key": "tag_score_dedup" }
```
### `POST /api/tasks/brief`
生成当天每日简报(force 重新生成)。
**响应**
```json
{ "message": "简报生成任务已开始", "task_key": "generate_daily_brief" }
```
---
## 任务进度
### `GET /api/tasks/progress`
返回所有任务的实时进度快照(前端每 ~1.5 秒轮询)。
**响应**
```json
{
"summarize": {
"status": "running",
"stage": "生成摘要",
"current": 75,
"total": 200,
"message": null,
"started_at": "2026-06-13T14:30:00+00:00",
"updated_at": "2026-06-13T14:32:15+00:00",
"finished_at": null,
"trigger": "manual"
},
"tag_score_dedup": { "status": "idle", "stage": "", "current": 0, "total": 0, "message": null, "started_at": null, "updated_at": null, "finished_at": null, "trigger": null },
"generate_daily_brief": { "..." : "同上结构" },
"bootstrap_taxonomy": { "..." : "同上结构" }
}
```
**字段说明**
| 字段 | 说明 |
|------|------|
| `status` | `idle` / `running` / `success` / `error` |
| `stage` | 当前阶段文案(如「生成摘要」「LLM 生成分类体系」) |
| `current` / `total` | 进度计数,`total=0` 时为阶段型任务(用 indeterminate 进度条) |
| `message` | 附加信息或错误详情(`status=error` 时为错误信息) |
| `started_at` / `finished_at` | ISO 8601 时间戳 |
| `trigger` | `manual`(手动触发)/ `scheduled`(定时触发) |
### `POST /api/tasks/progress/reset`
重置指定任务的进度为 idle(清除终态显示)。
**查询参数** `task_key`string,必填
**响应** `{ "message": "已重置" }`
---
## 接口连通性测试
### `POST /api/test-connection`
测试 rssKeeper 与 LLM API 连通性,返回状态与延迟。每个测试使用独立短超时客户端(10 秒、0 重试)。
**响应** `ConnectionTestResponse`
```json
{
"rss_keeper": {
"name": "rssKeeper",
"status": "ok",
"latency_ms": 26.0,
"error": null
},
"llm": {
"name": "LLM",
"status": "ok",
"latency_ms": 6871.4,
"error": null
}
}
```
`status``error``error` 字段含失败原因,`latency_ms``null`
---
## 配置管理接口
> 配置修改保存到数据库,部分配置(调度间隔等)需重启服务生效。
### `GET /api/settings`
列出所有可编辑配置。敏感项(`OPENAI_API_KEY` / `API_TOKEN`)返回脱敏值。
**响应** `List[SettingOut]`
```json
[
{
"key": "OPENAI_API_KEY",
"value": "sk-c...2R_8",
"description": "LLM API Key",
"is_sensitive": true,
"is_masked": true,
"updated_at": "2026-06-13T..."
}
]
```
### `PUT /api/settings/{key}`
更新单个配置项。
**请求体**
```json
{ "value": "新值" }
```
**响应** `{ "message": "配置已保存,重启服务后生效" }` **错误** `400` 无效配置项
### `PUT /api/settings`
批量更新配置。
**请求体**
```json
{ "settings": { "OPENAI_MODEL": "gpt-4o-mini", "OPENAI_TIMEOUT": "60" } }
```
**响应** `{ "message": "配置已保存,重启服务后生效" }` **错误** `400` 列出无效配置项
### `POST /api/settings/reset`
将所有配置重置为环境变量默认值。
**响应** `{ "message": "配置已重置为环境变量默认值,重启服务后生效" }`
### 可编辑配置清单
| key | 说明 | 敏感 |
|-----|------|------|
| `RSSKEEPER_BASE_URL` | rssKeeper 服务地址 | 否 |
| `OPENAI_API_KEY` | LLM API Key | 是 |
| `OPENAI_BASE_URL` | LLM API 基础地址 | 否 |
| `OPENAI_MODEL` | LLM 模型名 | 否 |
| `OPENAI_TIMEOUT` | LLM 调用超时(秒) | 否 |
| `OPENAI_MAX_RETRIES` | LLM 最大重试次数 | 否 |
| `SUMMARIZE_INTERVAL_MINUTES` | 摘要任务间隔(分钟) | 否 |
| `TAG_SCORE_INTERVAL_MINUTES` | 分类/打分/去重任务间隔(分钟) | 否 |
| `DAILY_BRIEF_HOUR` | 每日简报生成小时 | 否 |
| `DAILY_BRIEF_MINUTE` | 每日简报生成分钟 | 否 |
| `TITLE_SIMILARITY_THRESHOLD` | 标题相似度阈值 | 否 |
| `CONTENT_SIMILARITY_THRESHOLD` | 内容相似度阈值 | 否 |
| `MAX_AI_SUMMARY_LENGTH` | AI 摘要最大长度 | 否 |
| `MIN_ORIGINAL_SUMMARY_LENGTH` | 原始摘要最小长度 | 否 |
| `BRIEF_TOP_N_PER_CATEGORY` | 简报每分类显示文章数 | 否 |
| `LOG_LEVEL` | 日志级别 | 否 |
| `API_TOKEN` | API 鉴权 Token(空则不启用) | 是 |
| `CORS_ALLOWED_ORIGINS` | CORS 允许来源(逗号分隔) | 否 |
---
## 仪表盘统计
### `GET /api/stats`
返回仪表盘统计与下次定时任务时间。
**响应** `StatsOut`
```json
{
"total_articles": 200,
"today_articles": 50,
"ai_summarized": 180,
"categories": 12,
"tags": 43,
"duplicate_groups": 5,
"briefs": 1,
"next_jobs": {
"fetch_and_summarize": "2026-06-13T17:39:13+08:00",
"tag_score_deduplicate": "2026-06-14T16:39:13+08:00",
"generate_daily_brief": "2026-06-14T08:00:00+08:00"
}
}
```
---
## 错误码
| 状态码 | 含义 | 触发场景 |
|--------|------|----------|
| `200` | 成功 | 正常请求 |
| `400` | 参数错误 | 无效配置项 / 请求体格式错误 |
| `401` | 未认证 | 未携带 Authorization 头(启用了鉴权时) |
| `403` | 鉴权失败 | token 无效 |
| `404` | 资源不存在 | 文章/简报不存在 |
| `409` | 冲突 | 已有任务正在执行(任务全局互斥) |
| `422` | 校验失败 | 响应模型序列化失败等 |
| `500` | 服务器错误 | 内部异常 |
## 定时任务
服务启动后由 APScheduler 自动注册(时区 `Asia/Shanghai`):
| Job ID | 触发方式 | 默认 |
|--------|----------|------|
| `bootstrap_taxonomy` | 启动时一次(DateTrigger | taxonomy 为空时生成 |
| `fetch_and_summarize` | 间隔 | 每 60 分钟 |
| `tag_score_deduplicate` | 间隔 | 每 1440 分钟(24 小时) |
| `generate_daily_brief` | Cron | 每日 08:00 |
间隔参数可通过[配置接口](#配置管理接口)修改,修改后需重启服务生效。