Initial commit: yu-zhi-ran platform with automation integration

This commit is contained in:
lt
2026-04-19 14:05:09 +08:00
commit 3cb2df51c8
209 changed files with 80379 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime, date, timedelta
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 ..schemas import SystemStatus
from ..core.generator import run_creator
from ..core.optimizer import run_optimizer
from ..core.sync import sync_topic_to_db, sync_all_topics
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"
DATA_DIR = PROJECT_ROOT / "automation" / "data"
router = APIRouter(prefix="/api/system", tags=["system"])
@router.get("/status", response_model=SystemStatus)
def get_status(db: Session = Depends(get_db)):
"""系统状态概览"""
total = db.query(Topic).count()
by_status_result = db.query(Topic.status, func.count()).group_by(Topic.status).all()
by_status = {status: count for status, count in by_status_result}
# 确保返回所有状态,避免前端 undefined
for key in ('pending', 'ready', 'published'):
by_status.setdefault(key, 0)
ready = db.query(Topic).filter(Topic.status == "ready").all()
today_str = date.today().isoformat()
# 计算今日文章数:查找 releases/2026-04-16 目录下的 html 文件
# 这里简单统计数据库中 created_at 为今天的文章(不完全准确)
today_articles = db.query(Article).filter(
func.date(Article.created_at) == date.today()
).count()
# 合规率:假设所有 ready 的都是合规的(实际从report读取)
# 可以后续优化
# 获取最后一次优化时间
last_opt = db.query(Article).filter(
Article.status == "optimized"
).order_by(Article.created_at.desc()).first()
return SystemStatus(
total_topics=total,
topics_by_status=by_status,
ready_topics=ready,
today_articles=today_articles,
compliance_rate=100.0, # placeholder
last_optimization=last_opt.created_at if last_opt else None
)
@router.post("/generate/run")
def trigger_generation(topic_id: str = None, db: Session = Depends(get_db)):
"""手动触发内容创作任务
Args:
topic_id: 可选,指定要创作的选题ID。不指定则创作优先级最高的待处理选题。
"""
try:
result = run_creator(topic_id)
if not result["ok"]:
raise HTTPException(status_code=500, detail=result["error"])
from ..core.sync import sync_all_topics
sync_all_topics()
return {"message": "Generation triggered", "result": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/optimize/run")
def trigger_optimization(topic_ids: List[str] = None, db: Session = Depends(get_db)):
"""手动触发合规优化任务
Args:
topic_ids: 可选,指定要优化的选题ID列表。不指定则优化所有 draft 状态文章。
"""
try:
result = run_optimizer(topic_ids)
if not result["ok"]:
raise HTTPException(status_code=500, detail=result["error"])
report = result.get("report")
if report:
from ..core.sync import sync_all_topics
sync_all_topics()
return {
"message": "Optimization completed",
"summary": report["summary"]
}
else:
return {"message": "Optimization completed but no report found", "stdout": result.get("stdout", "")}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/logs/{log_date}")
def get_logs(log_date: str, log_type: str = "creator"):
"""读取日志文件内容,log_type: creator, optimizer, collector"""
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")
def get_pipeline_status():
"""获取流水线各模块状态(最后运行时间和结果)"""
try:
# 读取选题文件
topics_file = DATA_DIR / "sustainability_topics.json"
topics = []
if topics_file.exists():
topics = json.loads(topics_file.read_text(encoding='utf-8'))
# 统计状态分布
status_counts = {}
for t in topics:
s = t.get('status', 'unknown')
status_counts[s] = status_counts.get(s, 0) + 1
# 检查各日志文件的最新修改时间
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",
"publisher": LOGS_DIR / f"publisher_{date.today()}.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,
"size_bytes": log_file.stat().st_size
}
# 简单推断成功/失败(TODO: 解析日志加强)
last_lines = log_file.read_text(encoding='utf-8').splitlines()[-10:]
has_error = any("error" in line.lower() or "失败" in line or "failed" in line.lower() for line in last_lines)
pipeline_status[name]["has_error"] = has_error
else:
pipeline_status[name] = {"exists": False, "last_run": None}
return {
"topics_count": len(topics),
"status_distribution": status_counts,
"pipeline_modules": pipeline_status,
"data_dir": str(DATA_DIR),
"logs_dir": str(LOGS_DIR)
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/sync/run")
def run_sync():
"""手动触发数据同步(流水线JSON → 平台数据库)"""
try:
sync_all_topics()
return {"message": "Sync completed"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/automation/topics")
def list_automation_topics():
"""直接读取自动化流水线的选题JSON(供调试)"""
try:
topics_file = DATA_DIR / "sustainability_topics.json"
if not topics_file.exists():
raise HTTPException(status_code=404, detail="Topics JSON not found")
topics = json.loads(topics_file.read_text(encoding='utf-8'))
return {
"count": len(topics),
"topics": topics[-50:] # 只返回最近50个,避免过大
}
except json.JSONDecodeError as e:
raise HTTPException(status_code=500, detail=f"JSON parse error: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/refresh")
def refresh_all():
"""刷新所有数据:同步JSON + 更新状态"""
try:
sync_all_topics()
return {"message": "Refresh completed"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))