778ccefb22
后端 - 新增 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>
144 lines
5.9 KiB
Python
144 lines
5.9 KiB
Python
"""分类/标签/打分规则体系的初始化与维护"""
|
||
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()
|