feat: 数据源统一与前端预览修复
=== 后端核心 === - db_helper: 统一数据库访问抽象层 - system.py API: * 参数绑定修复: 使用 Body(embed=True) 接收 JSON * 添加请求日志记录 - sync.py: 仅导出 DB→JSON(备份) === 合规与流水线 === - compliance_checker: 标签检测优化(仅检查容器,避免正文误判) - 所有脚本(creator/collector/writer/outline/research等)统一使用数据库 === 前端改版 === - topics.html: * 创作/优化 API 路径修正 * 预览弹窗重设计:多平台并行加载、富文本显示、单复制按钮 * 状态中文映射(getStatusLabel) * 认证检查 - 所有 HTML 静态资源路径修复(移除 /static 前缀) === 数据一致性 === - 数据库状态统一为英文(pending/review/ready/published) - 前端显示中文化映射 已测试 A03 流水线完整通过。
This commit is contained in:
@@ -1,63 +1,65 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
import logging
|
||||
from fastapi import APIRouter, HTTPException, Depends, Body
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from datetime import datetime, date, timedelta
|
||||
from datetime import datetime, date
|
||||
from typing import Dict, Any, List, Optional
|
||||
from pathlib import Path
|
||||
from .auth import get_current_user
|
||||
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_all_topics
|
||||
from ..core.scheduler import scheduler
|
||||
from .auth import get_current_user
|
||||
|
||||
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"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/system", tags=["system"])
|
||||
|
||||
def _aggregate_status_counts(db: Session):
|
||||
"""聚合状态计数,兼容中英文状态值"""
|
||||
raw = db.query(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()
|
||||
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}
|
||||
|
||||
# 确保返回所有状态(数据库存中文,返回前端需要英文)
|
||||
status_map = {
|
||||
'pending': by_status.get('待处理', 0),
|
||||
'review': by_status.get('待审查', 0),
|
||||
'ready': by_status.get('待发布', 0),
|
||||
'published': by_status.get('已发布', 0)
|
||||
}
|
||||
|
||||
# 计算今日新增
|
||||
counts = _aggregate_status_counts(db)
|
||||
today = date.today()
|
||||
today_count = db.query(Topic).filter(
|
||||
func.date(Topic.created_at) == today
|
||||
).count()
|
||||
|
||||
today_count = db.query(Topic).filter(func.date(Topic.created_at) == today).count()
|
||||
return {
|
||||
"stats": {
|
||||
"total": total,
|
||||
"pending": status_map['pending'],
|
||||
"review": status_map['review'],
|
||||
"ready": status_map['ready'],
|
||||
"published": status_map['published'],
|
||||
"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 = None, db: Session = Depends(get_db)):
|
||||
"""手动触发内容创作任务"""
|
||||
def trigger_generation(topic_id: str = Body(None, embed=True), db: Session = Depends(get_db)):
|
||||
logger.info(f"Received topic_id={topic_id}")
|
||||
try:
|
||||
result = run_creator(topic_id)
|
||||
if not result["ok"]:
|
||||
@@ -68,8 +70,7 @@ def trigger_generation(topic_id: str = None, db: Session = Depends(get_db)):
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/optimize/run", dependencies=[Depends(get_current_user)])
|
||||
def trigger_optimization(topic_ids: List[str] = None, db: Session = Depends(get_db)):
|
||||
"""手动触发合规优化任务"""
|
||||
def trigger_optimization(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db)):
|
||||
try:
|
||||
result = run_optimizer(topic_ids)
|
||||
if not result["ok"]:
|
||||
@@ -85,7 +86,6 @@ def trigger_optimization(topic_ids: List[str] = None, db: Session = Depends(get_
|
||||
|
||||
@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}")
|
||||
@@ -94,59 +94,56 @@ def get_logs(log_date: str, log_type: str = "creator"):
|
||||
return {"log_date": log_date, "log_type": log_type, "content": lines}
|
||||
|
||||
@router.get("/pipeline/status", dependencies=[Depends(get_current_user)])
|
||||
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",
|
||||
}
|
||||
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": len(topics), "status_distribution": status_counts, "pipeline_modules": pipeline_status}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
def get_pipeline_status(db: Session = Depends(get_db)):
|
||||
total = db.query(Topic).count()
|
||||
counts = _aggregate_status_counts(db)
|
||||
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"}
|
||||
return {"message": "Sync completed (DB → JSON backup)"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/automation/topics")
|
||||
def list_automation_topics():
|
||||
"""直接读取自动化流水线的选题 JSON"""
|
||||
def list_automation_topics(db: Session = Depends(get_db)):
|
||||
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:]}
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(status_code=500, detail=f"JSON parse error: {e}")
|
||||
topics = db.query(Topic).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"}
|
||||
@@ -155,5 +152,4 @@ def refresh_all():
|
||||
|
||||
@router.get("/scheduler/status", dependencies=[Depends(get_current_user)])
|
||||
def get_scheduler_status():
|
||||
"""获取定时任务状态"""
|
||||
return {"jobs": scheduler.get_jobs()}
|
||||
|
||||
Reference in New Issue
Block a user