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:
lt
2026-05-07 11:25:42 +08:00
parent 8dd19a2179
commit 31d6306e3b
24 changed files with 1018 additions and 530 deletions
+66 -70
View File
@@ -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()}