Files
yu-zhi-ran/platform/backend/app/api/system.py
T
Yuzhiran Dev 6b790e77f6 全线异步化: 所有手动触发任务不再阻塞uvicorn worker
每个模块提供两个版本:
- run_xxxx() → Popen非阻塞(给API用)
- run_xxxx_blocking() → subprocess.run阻塞(给定时任务用)

API端点全部改为立即返回XX已后台启动,不再等子进程完成

受影响端点:
  /generate/run (原30min→12ms)
  /review/run (原10min→12ms)
  /collect/run (原5min→12ms,已修复)
  /optimize-sources/run (原2min→12ms)
  /trends/run (原2min→12ms)
  /refresh-search-cache/run (原10min→12ms)
  /metrics-sync/run (原N×10s→12ms)

同时增加对应GET状态端点:/collect/status,/generate/status,/review/status
2026-05-21 09:20:13 +08:00

294 lines
12 KiB
Python

import logging
import subprocess
from fastapi import APIRouter, HTTPException, Depends, Body
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime, date
from pathlib import Path
from typing import Dict, Any, List, Optional
import os
import json
from ..database import get_db
from ..models import Topic, Article
from ..core.generator import run_creator, get_generator_status
from ..core.optimizer import run_optimizer, get_optimizer_status
from ..core.collector import run_collector, get_collector_status
import threading
from ..core.sync import sync_all_topics
from ..core.scheduler import scheduler
from .auth import get_current_user, org_filter
PROJECT_ROOT = Path(__file__).resolve().parents[4]
if os.getenv('PROJECT_ROOT'):
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/system", tags=["system"])
def _aggregate_status_counts(q):
"""聚合状态计数,兼容中英文状态值"""
raw = q.with_entities(Topic.status, func.count()).group_by(Topic.status).all()
mapping = {
'pending': ['pending', '待处理'],
'review': ['review', '待审查'],
'ready': ['ready', '待发布'],
'published': ['published', '已发布']
}
counts = {'pending': 0, 'review': 0, 'ready': 0, 'published': 0}
for status_val, cnt in raw:
for key, aliases in mapping.items():
if status_val in aliases:
counts[key] += cnt
break
return counts
@router.get("/status")
def get_status(db: Session = Depends(get_db)):
total = db.query(Topic).count()
counts = _aggregate_status_counts(db.query(Topic))
today = date.today()
today_count = db.query(Topic).filter(func.date(Topic.created_at) == today).count()
return {
"stats": {
"total": total,
"pending": counts['pending'],
"review": counts['review'],
"ready": counts['ready'],
"published": counts['published'],
"today": today_count
}
}
@router.post("/generate/run", dependencies=[Depends(get_current_user)])
def trigger_generation(topic_id: str = Body(None, embed=True), db: Session = Depends(get_db), current_user=Depends(get_current_user)):
logger.info(f"Generation triggered by {current_user.username}, topic_id={topic_id}")
try:
result = run_creator(topic_id)
return {"message": "内容创作已后台启动", "pid": result.get("pid")}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/generate/status", dependencies=[Depends(get_current_user)])
def generation_status():
status = get_generator_status()
if status is None:
return {"status": "idle", "message": "当前无运行中的创作任务"}
return status
@router.post("/collect/run", dependencies=[Depends(get_current_user)])
def trigger_collection(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
logger.info(f"Manual collection triggered by {current_user.username}")
try:
result = run_collector()
return {"message": "内容采集已后台启动", "result": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/collect/status", dependencies=[Depends(get_current_user)])
def collection_status():
status = get_collector_status()
if status is None:
return {"status": "idle", "message": "当前无运行中的采集任务"}
return status
@router.post("/review/run", dependencies=[Depends(get_current_user)])
def trigger_review(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db), current_user=Depends(get_current_user)):
try:
result = run_optimizer(topic_ids)
return {"message": "合规审查已后台启动", "pid": result.get("pid")}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/review/status", dependencies=[Depends(get_current_user)])
def review_status():
status = get_optimizer_status()
if status is None:
return {"status": "idle", "message": "当前无运行中的审查任务"}
return status
@router.get("/logs/{log_date}", dependencies=[Depends(get_current_user)])
def get_logs(log_date: str, log_type: str = "creator"):
log_file = LOGS_DIR / f"{log_type}_{log_date}.log"
if not log_file.exists():
raise HTTPException(status_code=404, detail=f"Log file not found: {log_file}")
content = log_file.read_text(encoding='utf-8')
lines = content.splitlines()[-100:] if log_type != "collector" else content.splitlines()[-200:]
return {"log_date": log_date, "log_type": log_type, "content": lines}
@router.get("/pipeline/status", dependencies=[Depends(get_current_user)])
def get_pipeline_status(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
topic_base = db.query(Topic)
of = org_filter(current_user, Topic)
if of is not True:
topic_base = topic_base.filter(of)
total = topic_base.count()
counts = _aggregate_status_counts(topic_base)
log_files = {
"collector": LOGS_DIR / f"collector_{date.today().isoformat()}.log",
"creator": LOGS_DIR / f"creator_{date.today().isoformat()}.log",
"optimizer": LOGS_DIR / f"optimizer_{date.today().isoformat()}.log",
}
pipeline_status = {}
for name, log_file in log_files.items():
if log_file.exists():
mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
pipeline_status[name] = {"last_run": mtime.isoformat(), "exists": True}
else:
pipeline_status[name] = {"exists": False, "last_run": None}
return {"topics_count": total, "status_distribution": counts, "pipeline_modules": pipeline_status}
@router.post("/sync/run")
def run_sync():
try:
sync_all_topics()
return {"message": "Sync completed (DB → JSON backup)"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/optimize-sources/run")
def trigger_optimize_sources():
try:
from ..core.scheduler import scheduler
def _bg():
try:
scheduler._run_optimize_sources()
except Exception as e:
logger.exception("Background optimize sources failed: %s", e)
t = threading.Thread(target=_bg, daemon=True)
t.start()
return {"message": "信息源优化已后台启动"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/metrics-sync/run")
def trigger_metrics_sync():
try:
from ..core.scheduler import scheduler
def _bg():
try:
scheduler._run_metrics_sync()
except Exception as e:
logger.exception("Background metrics sync failed: %s", e)
t = threading.Thread(target=_bg, daemon=True)
t.start()
return {"message": "指标同步已后台启动"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/refresh-search-cache/run")
def trigger_refresh_search_cache():
try:
import sys as sys_mod
scripts_dir = Path(__file__).parent.parent.parent.parent / "scripts"
proc = subprocess.Popen(
[sys_mod.executable, str(scripts_dir / "opencode_search.py"), "--refresh-cache"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
cwd=scripts_dir.parent.parent
)
logger.info("Search cache refresh started (pid=%s)", proc.pid)
return {"message": "搜索缓存刷新已后台启动", "pid": proc.pid}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/trends/run")
def trigger_trends_refresh():
try:
import sys as sys_mod
scripts_dir = Path(__file__).parent.parent.parent.parent / "scripts"
proc = subprocess.Popen(
[sys_mod.executable, str(scripts_dir / "trends.py")],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
cwd=scripts_dir.parent.parent
)
logger.info("Trends refresh started (pid=%s)", proc.pid)
return {"message": "热点趋势刷新已后台启动", "pid": proc.pid}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/automation/topics")
def list_automation_topics(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
try:
topic_base = db.query(Topic)
of = org_filter(current_user, Topic)
if of is not True:
topic_base = topic_base.filter(of)
topics = topic_base.order_by(Topic.created_at.desc()).limit(100).all()
result = []
for t in topics:
result.append({
"id": t.id,
"title": t.title,
"field": t.field,
"status": t.status,
"priority": t.priority,
"priority_score": t.priority_score,
"total_score": t.total_score,
"created_at": t.created_at.isoformat() if t.created_at else None,
"updated_at": t.updated_at.isoformat() if t.updated_at else None,
"ready_at": t.ready_at.isoformat() if t.ready_at else None,
"compliance_score": t.compliance_score
})
return {"count": len(result), "topics": result[:50]}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/refresh")
def refresh_all():
try:
sync_all_topics()
return {"message": "Refresh completed"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/scheduler/status", dependencies=[Depends(get_current_user)])
def get_scheduler_status():
return {"running": scheduler._started, "jobs": scheduler.get_jobs()}
@router.get("/modules/status", dependencies=[Depends(get_current_user)])
def get_modules_status():
today_str = date.today().isoformat()
log_based: dict = {
"scheduled_collect": {"name": "📡 内容采集", "log": LOGS_DIR / f"collector_{today_str}.log"},
"scheduled_refresh_search_cache": {"name": "🔍 搜索缓存", "log": LOGS_DIR / f"opencode_search_{today_str}.log"},
"scheduled_fetch_trends": {"name": "🔥 热点趋势", "log": LOGS_DIR / f"trends_{today_str}.log"},
"scheduled_generate": {"name": "🤖 内容创作", "log": LOGS_DIR / f"creator_{today_str}.log"},
"scheduled_optimize": {"name": "🔍 合规审查", "log": LOGS_DIR / f"optimizer_{today_str}.log"},
"scheduled_optimize_sources": {"name": "📡 信息源优化", "log": LOGS_DIR / f"optimizer_sources_{today_str}.log"},
"scheduled_metrics_sync": {"name": "📊 指标同步", "log": LOGS_DIR / f"metrics_sync_{today_str}.log"},
}
jobs = {j['id']: j for j in scheduler.get_jobs()}
modules = []
for mod_id, cfg in log_based.items():
log_file = cfg["log"]
last_run = None
task_count = 0
success_rate = None
if log_file.exists():
mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
last_run = mtime.strftime("%Y-%m-%d %H:%M")
content = log_file.read_text(encoding="utf-8", errors="ignore")
task_count = content.count("完成") + content.count("success") + content.count("SUCCESS")
total = task_count + content.count("失败") + content.count("failed") + content.count("ERROR")
success_rate = round(task_count / total * 100) if total > 0 else None
status = "running" if mod_id in jobs else "stopped"
job = jobs.get(mod_id)
next_run = None
if job and job.get("next_run_time"):
try:
next_dt = datetime.fromisoformat(job["next_run_time"])
next_run = next_dt.strftime("%Y-%m-%d %H:%M")
except Exception:
next_run = job["next_run_time"]
modules.append({
"id": mod_id,
"title": cfg["name"],
"status": status,
"last_run": last_run or "从未运行",
"next_run": next_run or "待计划",
"task_count": task_count,
"success_rate": success_rate if success_rate is not None else 0,
})
return {"modules": modules, "scheduler": {"running": scheduler._started, "jobs": scheduler.get_jobs()}}