Initial commit: yu-zhi-ran platform with automation integration
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# API routes
|
||||
@@ -0,0 +1,54 @@
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pathlib import Path
|
||||
import os
|
||||
from datetime import datetime, date
|
||||
|
||||
router = APIRouter(prefix="/api/articles", tags=["articles"])
|
||||
|
||||
PROJECT_ROOT = Path('/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran')
|
||||
|
||||
@router.get("/drafts")
|
||||
def list_drafts(publish_date: str = None):
|
||||
"""列出指定日期的草稿文件(三平台)"""
|
||||
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"]
|
||||
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}
|
||||
|
||||
@router.get("/{topic_id}/preview")
|
||||
def preview_article(topic_id: str, platform: str = "zhihu", publish_date: str = None):
|
||||
"""预览某选题的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
|
||||
# DEBUG
|
||||
print(f"[DEBUG] file_path={file_path}, exists={file_path.exists()}")
|
||||
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}
|
||||
|
||||
@router.get("/optimization-report")
|
||||
def get_optimization_report(publish_date: str = None):
|
||||
"""获取合规优化报告"""
|
||||
if not publish_date:
|
||||
publish_date = date.today().isoformat()
|
||||
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")
|
||||
report = report_path.read_text(encoding='utf-8')
|
||||
import json
|
||||
return json.loads(report)
|
||||
@@ -0,0 +1,156 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import Topic
|
||||
|
||||
router = APIRouter(prefix="/api/publisher", tags=["publisher"])
|
||||
|
||||
# 项目根目录(从 api/publisher.py 上升到 yu-zhi-ran 根目录)
|
||||
import os
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
SCRIPTS_DIR = PROJECT_ROOT / "scripts"
|
||||
|
||||
@router.get("/ready")
|
||||
def get_ready_topics(
|
||||
platform: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取待发布的选题(状态为 ready)"""
|
||||
query = db.query(Topic).filter(Topic.status == "ready")
|
||||
if platform:
|
||||
# 筛选未在该平台发布的选题
|
||||
# platform_urls 是 JSON 字段,需要特殊处理
|
||||
pass # 简化:暂不筛选
|
||||
topics = query.order_by(Topic.ready_at.desc()).all()
|
||||
return topics
|
||||
|
||||
@router.post("/generate/{topic_id}")
|
||||
def generate_publish_package(
|
||||
topic_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""为指定选题生成发布包(所有平台HTML)"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
|
||||
# 调用 publisher.py 脚本
|
||||
script_path = SCRIPTS_DIR / "publisher.py"
|
||||
if not script_path.exists():
|
||||
raise HTTPException(status_code=500, detail="Publisher script not found")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["python3", str(script_path), "--topic-id", topic_id],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
cwd=str(PROJECT_ROOT)
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Publisher failed: {result.stderr}")
|
||||
|
||||
return {
|
||||
"message": "Publish package generated",
|
||||
"topic_id": topic_id,
|
||||
"output": result.stdout
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
raise HTTPException(status_code=504, detail="Publisher timeout")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/packages/{topic_id}")
|
||||
def list_platform_packages(topic_id: str):
|
||||
"""列出某个选题的所有平台发布包"""
|
||||
release_dir = PROJECT_ROOT / "automation" / "data" / "releases"
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
packages = []
|
||||
for platform in ["zhihu", "wechat", "xiaohongshu", "bilibili", "toutiao"]:
|
||||
html_file = release_dir / today / platform / f"{platform}_{topic_id}_{platform}.html"
|
||||
if html_file.exists():
|
||||
packages.append({
|
||||
"platform": platform,
|
||||
"file": str(html_file.relative_to(PROJECT_ROOT)),
|
||||
"size": html_file.stat().st_size
|
||||
})
|
||||
|
||||
published_dir = PROJECT_ROOT / "content" / "published" / topic_id / "手动发布"
|
||||
if published_dir.exists():
|
||||
for platform_dir in published_dir.iterdir():
|
||||
if platform_dir.is_dir():
|
||||
html_file = platform_dir / "文章.html"
|
||||
if html_file.exists():
|
||||
packages.append({
|
||||
"platform": platform_dir.name,
|
||||
"file": str(html_file.relative_to(PROJECT_ROOT)),
|
||||
"size": html_file.stat().st_size,
|
||||
"manual": True
|
||||
})
|
||||
|
||||
return {"topic_id": topic_id, "packages": packages}
|
||||
|
||||
@router.get("/package/{topic_id}/{platform}")
|
||||
def get_package_html(topic_id: str, platform: str):
|
||||
"""获取指定平台发布包的HTML内容"""
|
||||
# 优先查找 published 目录(手动发布包)
|
||||
published_html = PROJECT_ROOT / "content" / "published" / topic_id / "手动发布" / platform / "文章.html"
|
||||
if published_html.exists():
|
||||
return {"html": published_html.read_text(encoding='utf-8')}
|
||||
|
||||
# 其次查找 releases 目录(自动生成)
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
release_html = PROJECT_ROOT / "automation" / "data" / "releases" / today / platform / f"{platform}_{topic_id}_{platform}.html"
|
||||
if release_html.exists():
|
||||
return {"html": release_html.read_text(encoding='utf-8')}
|
||||
|
||||
raise HTTPException(status_code=404, detail="Package not found")
|
||||
|
||||
@router.post("/mark/{topic_id}/published")
|
||||
def mark_as_published(
|
||||
topic_id: str,
|
||||
platform_urls: dict,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""手动标记选题为已发布,记录平台链接"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
|
||||
topic.status = "published"
|
||||
topic.published_at = datetime.now().date()
|
||||
topic.platform_urls = platform_urls
|
||||
db.commit()
|
||||
|
||||
return {"message": "Topic marked as published", "topic_id": topic_id}
|
||||
|
||||
@router.get("/status")
|
||||
def get_publisher_status():
|
||||
"""获取发布统计"""
|
||||
# 统计今日已发布数量等
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
release_dir = PROJECT_ROOT / "automation" / "data" / "releases" / today
|
||||
|
||||
stats = {
|
||||
"today_releases": 0,
|
||||
"platforms": {}
|
||||
}
|
||||
|
||||
if release_dir.exists():
|
||||
for platform_dir in release_dir.iterdir():
|
||||
if platform_dir.is_dir():
|
||||
count = len(list(platform_dir.glob("*.html")))
|
||||
stats["platforms"][platform_dir.name] = count
|
||||
stats["today_releases"] += count
|
||||
|
||||
return stats
|
||||
@@ -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))
|
||||
@@ -0,0 +1,43 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import Topic
|
||||
from ..schemas import TopicResponse, PublishRequest
|
||||
|
||||
router = APIRouter(prefix="/api/topics", tags=["topics"])
|
||||
|
||||
@router.get("", response_model=List[TopicResponse])
|
||||
def list_topics(
|
||||
status: str = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
query = db.query(Topic)
|
||||
if status:
|
||||
query = query.filter(Topic.status == status)
|
||||
topics = query.order_by(Topic.priority_score.desc(), Topic.created_at.desc()).all()
|
||||
return topics
|
||||
|
||||
@router.get("/{topic_id}", response_model=TopicResponse)
|
||||
def get_topic(topic_id: str, db: Session = Depends(get_db)):
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
return topic
|
||||
|
||||
@router.post("/{topic_id}/publish")
|
||||
def publish_topic(topic_id: str, req: PublishRequest, db: Session = Depends(get_db)):
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
if topic.status != "ready":
|
||||
raise HTTPException(status_code=400, detail="Topic not in ready status")
|
||||
|
||||
topic.status = "published"
|
||||
topic.published_at = datetime.now().date()
|
||||
topic.platform_urls = req.platform_urls
|
||||
db.commit()
|
||||
|
||||
return {"message": "Topic marked as published", "topic_id": topic_id}
|
||||
Reference in New Issue
Block a user