全线异步化: 所有手动触发任务不再阻塞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
This commit is contained in:
Yuzhiran Dev
2026-05-21 09:20:13 +08:00
parent 6f81167691
commit 6b790e77f6
7 changed files with 295 additions and 156 deletions
+58 -62
View File
@@ -1,18 +1,19 @@
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
from pathlib import Path
import os
import json
from ..database import get_db
from ..models import Topic, Article
from ..core.generator import run_creator
from ..core.optimizer import run_optimizer
from ..core.collector import run_collector
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
@@ -61,59 +62,51 @@ def get_status(db: Session = Depends(get_db)):
@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"Received topic_id={topic_id}")
logger.info(f"Generation triggered by {current_user.username}, topic_id={topic_id}")
try:
result = run_creator(topic_id)
if not result["ok"]:
raise HTTPException(status_code=500, detail=result["error"])
sync_all_topics()
return {"message": "Generation triggered", "result": result}
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}
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)
if not result["ok"]:
raise HTTPException(status_code=500, detail=result["error"])
report = result.get("report")
if report and report["summary"]["total_articles"] > 0:
s = report["summary"]
total = s["total_articles"]
avg = s["average_score"]
msg = f"审查完成: {total} 篇全部通过 ({avg:.0f}分)"
return {"message": msg, "summary": s}
else:
if topic_ids:
updated = 0
for tid in topic_ids:
q = db.query(Topic).filter(Topic.id == tid)
of = org_filter(current_user, Topic)
if of is not True:
q = q.filter(of)
topic = q.first()
if topic and topic.status in ('review', '待审查'):
topic.status = 'ready'
if not topic.generated_at:
topic.generated_at = datetime.utcnow()
updated += 1
db.commit()
if updated:
logger.info(f"Review: {updated} topics advanced to 'ready' (no release files)")
return {"message": "审查完成(未找到 release 文件,仅推进状态)", "stdout": result.get("stdout", "")}
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"
@@ -157,11 +150,14 @@ def run_sync():
def trigger_optimize_sources():
try:
from ..core.scheduler import scheduler
scheduler._run_optimize_sources()
log_file = LOGS_DIR / f"optimizer_sources_{date.today().isoformat()}.log"
log_file.parent.mkdir(parents=True, exist_ok=True)
log_file.write_text(f"{datetime.now().isoformat()} - 信息源优化完成\n")
return {"message": "信息源优化已完成"}
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))
@@ -169,44 +165,44 @@ def trigger_optimize_sources():
def trigger_metrics_sync():
try:
from ..core.scheduler import scheduler
scheduler._run_metrics_sync()
log_file = LOGS_DIR / f"metrics_sync_{date.today().isoformat()}.log"
log_file.parent.mkdir(parents=True, exist_ok=True)
log_file.write_text(f"{datetime.now().isoformat()} - 指标同步完成\n")
return {"message": "指标同步已完成"}
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 subprocess, sys as sys_mod
import sys as sys_mod
scripts_dir = Path(__file__).parent.parent.parent.parent / "scripts"
result = subprocess.run(
proc = subprocess.Popen(
[sys_mod.executable, str(scripts_dir / "opencode_search.py"), "--refresh-cache"],
capture_output=True, text=True, timeout=600
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
cwd=scripts_dir.parent.parent
)
if result.returncode != 0:
raise Exception(result.stderr[-500:])
return {"message": "搜索缓存已刷新", "output": result.stdout.strip()}
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 subprocess, sys as sys_mod
from pathlib import Path
import sys as sys_mod
scripts_dir = Path(__file__).parent.parent.parent.parent / "scripts"
result = subprocess.run(
proc = subprocess.Popen(
[sys_mod.executable, str(scripts_dir / "trends.py")],
capture_output=True, text=True, timeout=120
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
cwd=scripts_dir.parent.parent
)
if result.returncode != 0:
raise Exception(result.stderr[-500:])
return {"message": "热点趋势已刷新", "output": result.stdout.strip()}
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))