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

117 lines
3.6 KiB
Python

"""调用 rssKeeper 外部 API"""
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Any
import logging
import requests
from config import settings
logger = logging.getLogger(__name__)
class RSSKeeperClient:
"""rssKeeper 外部 API 客户端。
配置以 property 形式运行时从 settings 读取,避免模块 import 时
固化旧值(settings 在 FastAPI lifespan 启动后才会被数据库配置覆盖)。
"""
def __init__(self, base_url: Optional[str] = None, timeout: Optional[int] = None):
self._base_url = base_url
self._timeout = timeout
@property
def base_url(self) -> str:
return (self._base_url or settings.RSSKEEPER_BASE_URL).rstrip("/")
@property
def timeout(self) -> int:
return self._timeout if self._timeout is not None else 30
def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
url = f"{self.base_url}{path}"
try:
resp = requests.get(url, params=params, timeout=self.timeout)
resp.raise_for_status()
return resp.json()
except requests.RequestException as exc:
logger.error("请求 rssKeeper 失败: %s - %s", url, exc)
raise
def fetch_recent(
self,
hours: int = 24,
limit: int = 200,
feed_id: Optional[int] = None,
category: Optional[str] = None,
search: Optional[str] = None,
unread_only: bool = False,
) -> List[Dict[str, Any]]:
"""获取最近 N 小时的文章"""
params = {
"hours": hours,
"limit": limit,
"unread_only": unread_only,
}
if feed_id is not None:
params["feed_id"] = feed_id
if category is not None:
params["category"] = category
if search is not None:
params["search"] = search
data = self._get("/api/v1/external/recent", params=params)
return data.get("articles", [])
def fetch_by_date(self, date: str, category: Optional[str] = None) -> Dict[str, Any]:
"""获取指定日期的文章聚合"""
params: Dict[str, Any] = {"date": date}
if category is not None:
params["category"] = category
return self._get("/api/v1/external/summary", params=params)
def fetch_feeds(
self,
health_status: Optional[str] = None,
category: Optional[str] = None,
error_type: Optional[str] = None,
is_active: Optional[bool] = True,
) -> List[Dict[str, Any]]:
"""获取 RSS 源列表"""
params: Dict[str, Any] = {}
if health_status is not None:
params["health_status"] = health_status
if category is not None:
params["category"] = category
if error_type is not None:
params["error_type"] = error_type
if is_active is not None:
params["is_active"] = is_active
data = self._get("/api/v1/external/feeds", params=params)
return data.get("feeds", [])
def fulltext_search(
self,
q: str,
limit: int = 50,
offset: int = 0,
category: Optional[str] = None,
feed_id: Optional[int] = None,
) -> Dict[str, Any]:
"""全文搜索文章"""
params: Dict[str, Any] = {
"q": q,
"limit": limit,
"offset": offset,
}
if category is not None:
params["category"] = category
if feed_id is not None:
params["feed_id"] = feed_id
return self._get("/api/v1/external/search", params=params)
rss_client = RSSKeeperClient()