feat: 内容数据迁移至数据库,合规审查全链路打通
- 文章 HTML 存储从文件系统迁移至 articles 表,删除 releases 目录 - 合规审查从 DB 读取 HTML,审查结果写回 DB,通过后自动推进至待发布 - 新增 todayCount 筛选按钮,与系统概览统计数据一致 - 全屏预览修复:提升 z-index 超过侧边栏,添加退出全屏/关闭按钮 - 统一 '优化' → '审查' 命名,消除前后端术语不一致 - 调度器创作完成后自动触发审查(生成 → 审查 → 待发布) - 清理旧备份/调试文件、过期大纲和研究笔记
This commit is contained in:
@@ -1,53 +1,42 @@
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from pathlib import Path
|
||||
import os
|
||||
from datetime import datetime, date
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
from ..models import User, Article
|
||||
from .auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/articles", tags=["articles"])
|
||||
|
||||
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
|
||||
|
||||
@router.get("/drafts")
|
||||
def list_drafts(publish_date: str = None, current_user: User = Depends(get_current_user)):
|
||||
"""列出指定日期的草稿文件(三平台)"""
|
||||
if not publish_date:
|
||||
publish_date = date.today().isoformat()
|
||||
base_dir = PROJECT_ROOT / "automation" / "data" / "releases" / publish_date
|
||||
if not base_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="No releases for this date")
|
||||
|
||||
platforms = ["zhihu", "wechat", "xiaohongshu"]
|
||||
def list_drafts(topic_id: str = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""从 articles 表列出草稿"""
|
||||
query = db.query(Article)
|
||||
if topic_id:
|
||||
query = query.filter(Article.topic_id == topic_id)
|
||||
articles = query.order_by(Article.created_at.desc()).all()
|
||||
result = {}
|
||||
for p in platforms:
|
||||
path = base_dir / p
|
||||
if path.exists():
|
||||
files = sorted([f.name for f in path.glob("*.html") if f.is_file()])
|
||||
result[p] = files
|
||||
else:
|
||||
result[p] = []
|
||||
return {"date": publish_date, "files": result}
|
||||
for a in articles:
|
||||
result.setdefault(a.platform, []).append({"id": a.id, "topic_id": a.topic_id, "status": a.status})
|
||||
return {"articles": result}
|
||||
|
||||
@router.get("/{topic_id}/preview")
|
||||
def preview_article(topic_id: str, platform: str = "zhihu", publish_date: str = None, current_user: User = Depends(get_current_user)):
|
||||
"""预览某选题的HTML内容"""
|
||||
if not publish_date:
|
||||
publish_date = date.today().isoformat()
|
||||
filename = f"{platform}_{topic_id}_{platform}.html"
|
||||
file_path = PROJECT_ROOT / "automation" / "data" / "releases" / publish_date / platform / filename
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Article not found: {file_path}")
|
||||
content = file_path.read_text(encoding='utf-8')
|
||||
return {"topic_id": topic_id, "platform": platform, "html": content}
|
||||
def preview_article(topic_id: str, platform: str = "zhihu", current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""从 articles 表预览某选题的 HTML 内容"""
|
||||
article_id = f"{platform}_{topic_id}"
|
||||
article = db.query(Article).filter(Article.id == article_id).first()
|
||||
if not article or not article.html_content:
|
||||
raise HTTPException(status_code=404, detail=f"Article not found for {topic_id} on {platform}")
|
||||
return {"topic_id": topic_id, "platform": platform, "html": article.html_content}
|
||||
|
||||
@router.get("/optimization-report")
|
||||
def get_optimization_report(publish_date: str = None, current_user: User = Depends(get_current_user)):
|
||||
"""获取合规优化报告"""
|
||||
if not publish_date:
|
||||
publish_date = date.today().isoformat()
|
||||
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
|
||||
report_path = PROJECT_ROOT / "automation" / "data" / "drafts" / publish_date / "optimization_report.json"
|
||||
if not report_path.exists():
|
||||
raise HTTPException(status_code=404, detail="No optimization report for this date")
|
||||
|
||||
@@ -17,11 +17,14 @@ router = APIRouter(prefix="/api/calendar", tags=["calendar"])
|
||||
|
||||
@router.get("", response_model=List[ContentCalendarResponse])
|
||||
def get_calendar(
|
||||
year: int = Query(...),
|
||||
month: int = Query(...),
|
||||
year: Optional[int] = Query(None),
|
||||
month: Optional[int] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
today = date.today()
|
||||
year = year or today.year
|
||||
month = month or today.month
|
||||
start = date(year, month, 1)
|
||||
last_day = monthrange(year, month)[1]
|
||||
end = date(year, month, last_day)
|
||||
|
||||
@@ -35,8 +35,8 @@ async def create_publish_record(
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail=f"选题 {req.topic_id} 不存在")
|
||||
|
||||
if topic.status != '待发布':
|
||||
raise HTTPException(status_code=400, detail=f"选题 {req.topic_id} 状态不是待发布")
|
||||
if topic.status not in ('ready', '待发布'):
|
||||
raise HTTPException(status_code=400, detail=f"选题 {req.topic_id} 状态不是待发布(当前: {topic.status})")
|
||||
|
||||
# 更新选题状态
|
||||
topic.status = '已发布'
|
||||
|
||||
@@ -69,18 +69,37 @@ def trigger_generation(topic_id: str = Body(None, embed=True), db: Session = Dep
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/optimize/run", dependencies=[Depends(get_current_user)])
|
||||
def trigger_optimization(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db)):
|
||||
@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)):
|
||||
try:
|
||||
result = run_optimizer(topic_ids)
|
||||
if not result["ok"]:
|
||||
raise HTTPException(status_code=500, detail=result["error"])
|
||||
report = result.get("report")
|
||||
if report:
|
||||
sync_all_topics()
|
||||
return {"message": "Optimization completed", "summary": report["summary"]}
|
||||
if report and report["summary"]["total_articles"] > 0:
|
||||
s = report["summary"]
|
||||
passed = s["passed_auto"]
|
||||
manual = s["need_manual"]
|
||||
total = s["total_articles"]
|
||||
avg = s["average_score"]
|
||||
msg = f"审查完成: {total} 篇, {passed} 篇通过 ({avg:.0f}分)"
|
||||
if manual:
|
||||
msg += f", {manual} 篇需人工处理"
|
||||
return {"message": msg, "summary": s}
|
||||
else:
|
||||
return {"message": "Optimization completed", "stdout": result.get("stdout", "")}
|
||||
if topic_ids:
|
||||
updated = 0
|
||||
for tid in topic_ids:
|
||||
topic = db.query(Topic).filter(Topic.id == tid).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", "")}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@@ -152,4 +171,48 @@ def refresh_all():
|
||||
|
||||
@router.get("/scheduler/status", dependencies=[Depends(get_current_user)])
|
||||
def get_scheduler_status():
|
||||
return {"jobs": scheduler.get_jobs()}
|
||||
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()
|
||||
module_logs = {
|
||||
"creator": {
|
||||
"key": "🤖 内容创作引擎",
|
||||
"log": LOGS_DIR / f"creator_{today_str}.log",
|
||||
"config_file": PROJECT_ROOT / "automation" / "data" / "outlines" / today_str,
|
||||
},
|
||||
"optimizer": {
|
||||
"key": "🔍 内容优化器",
|
||||
"log": LOGS_DIR / f"optimizer_{today_str}.log",
|
||||
"config_file": PROJECT_ROOT / "automation" / "data" / "drafts" / today_str,
|
||||
},
|
||||
"collector": {
|
||||
"key": "📡 内容收集器",
|
||||
"log": LOGS_DIR / f"collector_{today_str}.log",
|
||||
"config_file": PROJECT_ROOT / "automation" / "data",
|
||||
},
|
||||
}
|
||||
modules = []
|
||||
for mod_id, cfg in module_logs.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
|
||||
modules.append({
|
||||
"id": mod_id,
|
||||
"title": cfg["key"],
|
||||
"status": "running" if log_file.exists() else "stopped",
|
||||
"last_run": last_run or "从未运行",
|
||||
"task_count": task_count,
|
||||
"success_rate": success_rate,
|
||||
})
|
||||
return {"modules": modules, "scheduler": {"running": scheduler._started, "jobs": scheduler.get_jobs()}}
|
||||
|
||||
@@ -88,9 +88,9 @@ def start_task(
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
task.status = "running"
|
||||
task.started_at = datetime.now()
|
||||
task.started_at = datetime.now(timezone.utc)
|
||||
task.message = "任务已启动"
|
||||
task.progress = 0
|
||||
db.commit()
|
||||
@@ -130,16 +130,17 @@ def complete_task(
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
finished = datetime.now(timezone.utc)
|
||||
task.status = "completed"
|
||||
task.finished_at = datetime.now()
|
||||
task.finished_at = finished
|
||||
task.progress = 100
|
||||
if message:
|
||||
task.message = message
|
||||
if result_data:
|
||||
task.result_data = result_data
|
||||
if task.started_at:
|
||||
task.duration = int((task.finished_at - task.started_at).total_seconds())
|
||||
task.duration = int((finished - task.started_at).total_seconds())
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
@@ -156,12 +157,13 @@ def fail_task(
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
finished = datetime.now(timezone.utc)
|
||||
task.status = "failed"
|
||||
task.finished_at = datetime.now()
|
||||
task.finished_at = finished
|
||||
task.error_msg = error_msg
|
||||
if task.started_at:
|
||||
task.duration = int((task.finished_at - task.started_at).total_seconds())
|
||||
task.duration = int((finished - task.started_at).total_seconds())
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
@@ -189,8 +191,9 @@ def run_creator_task(
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
task_id = f"task_{uuid.uuid4().hex[:16]}"
|
||||
|
||||
task = ContentTask(
|
||||
@@ -198,7 +201,7 @@ def run_creator_task(
|
||||
topic_id=topic_id,
|
||||
stage="creator",
|
||||
status="running",
|
||||
started_at=datetime.now(),
|
||||
started_at=now,
|
||||
created_by=current_user.username
|
||||
)
|
||||
db.add(task)
|
||||
@@ -210,22 +213,22 @@ def run_creator_task(
|
||||
def _run():
|
||||
try:
|
||||
result = run_creator(topic_id)
|
||||
from datetime import datetime
|
||||
finished = datetime.now(timezone.utc)
|
||||
task.status = "completed"
|
||||
task.finished_at = datetime.now()
|
||||
task.finished_at = finished
|
||||
task.progress = 100
|
||||
task.message = "创作完成"
|
||||
task.result_data = result or {}
|
||||
if task.started_at:
|
||||
task.duration = int((task.finished_at - task.started_at).total_seconds())
|
||||
task.duration = int((finished - task.started_at).total_seconds())
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
from datetime import datetime
|
||||
finished = datetime.now(timezone.utc)
|
||||
task.status = "failed"
|
||||
task.finished_at = datetime.now()
|
||||
task.finished_at = finished
|
||||
task.error_msg = str(e)
|
||||
if task.started_at:
|
||||
task.duration = int((task.finished_at - task.started_at).total_seconds())
|
||||
task.duration = int((finished - task.started_at).total_seconds())
|
||||
db.commit()
|
||||
|
||||
thread = threading.Thread(target=_run)
|
||||
|
||||
@@ -167,9 +167,13 @@ def delete_topic(
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
db.delete(topic)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
try:
|
||||
db.delete(topic)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=f"删除失败:该选题有关联数据(文章/发布记录等),请先删除关联数据。{str(e)}")
|
||||
|
||||
|
||||
@router.post("/{topic_id}/score")
|
||||
|
||||
Reference in New Issue
Block a user