Files
congsh 778ccefb22 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>
2026-06-14 15:14:40 +08:00

144 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""分类/标签/打分规则体系的初始化与维护"""
import json
import logging
from typing import List, Dict, Any
from sqlalchemy.orm import Session
from app.ai_client import AIClient
from app.rss_client import rss_client
from app.task_progress import update_progress
from models import Taxonomy
logger = logging.getLogger(__name__)
TAXONOMY_SYSTEM_PROMPT = """你是一位专业的信息分类与内容分析专家。
请根据用户提供的 RSS 文章样本,生成一套适合的中文内容分类体系、标签体系和打分规则。
输出必须是合法的 JSON,格式如下:
{
"categories": [
{"name": "科技", "description": "人工智能、芯片、互联网、软件等", "keywords": ["AI", "芯片", "大模型", ...]}
],
"tags": [
{"name": "人工智能", "description": "...", "keywords": ["AI", "人工智能", "大模型", ...]}
],
"heat_rules": [
{"name": "热点事件", "keywords": ["突发", "重磅", "刚刚", "发布"], "weight": 1.5}
],
"importance_rules": [
{"name": "政策法规", "keywords": ["政策", "监管", "法规", "征求意见"], "weight": 1.5}
],
"duplication_indicators": [
{"name": "同一事件", "keywords": ["宣布", "发布", "推出"], "weight": 1.0}
]
}
要求:
1. categories 数量控制在 8-12 个,覆盖科技、财经、新闻、设计、生活等常见 RSS 主题。
2. tags 数量控制在 30-50 个,尽量细化但避免过度重叠。
3. heat_rules 和 importance_rules 各 10-20 条,weight 范围 0.5-2.0。
4. 所有 keywords 用中文或中英双语,便于后续关键词匹配。
5. 不要输出任何解释文字,只输出 JSON。
6. **分类与标签名称必须使用中性的主题领域词**(如科技、财经、文化、体育、生活、健康、设计、商业等),
禁止使用具体事件、人名、地名、国家名、机构名或任何政治/军事/冲突相关的敏感词作为名称或关键词,
以保证内容中立、避免触发内容审查。
"""
def _build_sample_prompt(articles: List[Dict[str, Any]]) -> str:
# 只用标题和来源,不带正文摘要——降低输入中的敏感内容,避免触发内容审查
lines = [f"共有 {len(articles)} 篇文章样本(仅展示标题用于归纳主题):"]
for idx, art in enumerate(articles[:40], 1):
title = art.get("title", "")
feed = art.get("feed_title", "")
lines.append(f"[{idx}] {title} (来源:{feed}")
return "\n".join(lines)
def bootstrap_taxonomy(db: Session, force: bool = False) -> bool:
"""
初始化分类/标签/打分规则。
若 force=True 则清空后重建;否则仅在表为空时初始化。
"""
existing = db.query(Taxonomy).first()
if existing and not force:
logger.info("taxonomy 表已存在,跳过初始化")
return False
if force:
db.query(Taxonomy).delete()
db.commit()
logger.info("强制重新初始化 taxonomy")
logger.info("开始从 rssKeeper 拉取样本文章并生成分类体系...")
update_progress("bootstrap_taxonomy", status="running", stage="拉取样本文章", current=0, total=0)
articles = rss_client.fetch_recent(hours=24 * 7, limit=200)
if not articles:
logger.warning("未获取到样本文章,无法生成分类体系")
raise RuntimeError("未获取到样本文章,无法生成分类体系")
user_prompt = _build_sample_prompt(articles)
update_progress("bootstrap_taxonomy", status="running", stage="LLM 生成分类体系", current=0, total=0, message="正在调用 LLM 生成分类规则,可能需要 2-4 分钟")
# bootstrap 是一次性大任务(生成 categories+tags+rules),MiniMax-M3 reasoning 模式较慢,
# 用专用大 timeout client(默认 60s 不够),失败抛异常由调用方捕获并如实标记进度
bootstrap_ai = AIClient(timeout=300, max_retries=2)
result = bootstrap_ai.chat_completion_json(
system_prompt=TAXONOMY_SYSTEM_PROMPT,
user_prompt=user_prompt,
temperature=0.5,
)
update_progress("bootstrap_taxonomy", status="running", stage="保存规则", current=0, total=0)
_save_taxonomy(db, result)
logger.info("taxonomy 初始化完成,共写入 %d 条规则", db.query(Taxonomy).count())
return True
def _save_taxonomy(db: Session, data: Dict[str, Any]) -> None:
"""把 LLM 返回的分类体系写入数据库"""
def _add(kind: str, items: List[Dict[str, Any]], default_weight: float = 1.0):
for item in items:
name = item.get("name", "").strip()
if not name:
continue
keywords = item.get("keywords", [])
if isinstance(keywords, str):
keywords = [keywords]
db.add(
Taxonomy(
name=name,
kind=kind,
description=item.get("description", ""),
keywords=keywords,
weight=float(item.get("weight", default_weight)),
created_by_ai=True,
)
)
_add("category", data.get("categories", []))
_add("tag", data.get("tags", []))
_add("heat_rule", data.get("heat_rules", []), default_weight=1.0)
_add("importance_rule", data.get("importance_rules", []), default_weight=1.0)
_add("duplication_rule", data.get("duplication_indicators", []), default_weight=1.0)
db.commit()
def ensure_taxonomy(db: Session) -> bool:
"""确保 taxonomy 表非空,若为空则触发初始化"""
existing = db.query(Taxonomy).first()
if existing:
return True
return bootstrap_taxonomy(db)
def list_taxonomy(db: Session, kind: str = None) -> List[Taxonomy]:
"""列出分类体系规则"""
query = db.query(Taxonomy)
if kind:
query = query.filter(Taxonomy.kind == kind)
return query.order_by(Taxonomy.kind, Taxonomy.name).all()