refactor: remove publisher, add auto DB sync, clean backups, update docs
This commit is contained in:
@@ -41,7 +41,7 @@ python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
|||||||
| 📊 仪表盘 | 选题总数、待发布数、今日生成 |
|
| 📊 仪表盘 | 选题总数、待发布数、今日生成 |
|
||||||
| 🔄 流水线控制 | 触发创作、合规优化、状态监控 |
|
| 🔄 流水线控制 | 触发创作、合规优化、状态监控 |
|
||||||
| 📝 选题管理 | 列表、筛选、预览、发布 |
|
| 📝 选题管理 | 列表、筛选、预览、发布 |
|
||||||
| 📦 发布包管理 | 生成多平台HTML发布包、复制 |
|
|
||||||
| 📋 日志查看 | creator/optimizer/collector 日志 |
|
| 📋 日志查看 | creator/optimizer/collector 日志 |
|
||||||
|
|
||||||
## 与自动化流水线的集成
|
## 与自动化流水线的集成
|
||||||
|
|||||||
@@ -177,22 +177,6 @@ def list_packages(topic_id: str):
|
|||||||
return {"packages": packages}
|
return {"packages": packages}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{topic_id}/packages/generate")
|
|
||||||
def generate_packages(topic_id: str):
|
|
||||||
"""
|
|
||||||
手动触发单个选题的发布包生成。
|
|
||||||
相当于执行 publisher.py 针对单个选题。
|
|
||||||
"""
|
|
||||||
from ..core.publisher import run_publisher
|
|
||||||
result = run_publisher(topic_id)
|
|
||||||
if not result["ok"]:
|
|
||||||
raise HTTPException(status_code=500, detail=result["error"])
|
|
||||||
return {
|
|
||||||
"message": f"Package generation completed for topic {topic_id}",
|
|
||||||
"topic_id": topic_id,
|
|
||||||
"output": result.get("result", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
@router.delete("/{topic_id}")
|
@router.delete("/{topic_id}")
|
||||||
def delete_topic(topic_id: str, db: Session = Depends(get_db)):
|
def delete_topic(topic_id: str, db: Session = Depends(get_db)):
|
||||||
"""删除选题"""
|
"""删除选题"""
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ def run_creator(topic_id: str = None):
|
|||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
logger.error(f"Creator failed: {result.stderr}")
|
logger.error(f"Creator failed: {result.stderr}")
|
||||||
|
if topic_id:
|
||||||
|
try:
|
||||||
|
sync_topic_to_db(topic_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Sync after creation failed: {e}")
|
||||||
return {"ok": False, "error": result.stderr}
|
return {"ok": False, "error": result.stderr}
|
||||||
|
|
||||||
# 解析日志,找出选择了哪个选题
|
# 解析日志,找出选择了哪个选题
|
||||||
@@ -42,6 +47,7 @@ def run_creator(topic_id: str = None):
|
|||||||
if "选题" in line and "已标记为「待发布」" in line:
|
if "选题" in line and "已标记为「待发布」" in line:
|
||||||
# 如: 2026-04-16 ... INFO - 选题 A01 已标记为「待发布」
|
# 如: 2026-04-16 ... INFO - 选题 A01 已标记为「待发布」
|
||||||
import re
|
import re
|
||||||
|
from .sync import sync_topic_to_db
|
||||||
m = re.search(r'选题\s+([A-Za-z0-9]+)', line)
|
m = re.search(r'选题\s+([A-Za-z0-9]+)', line)
|
||||||
if m:
|
if m:
|
||||||
topic_id = m.group(1)
|
topic_id = m.group(1)
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
|
||||||
|
|
||||||
def run_publisher(topic_id: str = None):
|
|
||||||
"""运行发布包生成脚本
|
|
||||||
|
|
||||||
Args:
|
|
||||||
topic_id: 可选,指定单个选题ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 包含 ok, result/error, topic_id 等字段
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
script_path = PROJECT_ROOT / "scripts" / "publisher.py"
|
|
||||||
if not script_path.exists():
|
|
||||||
return {"ok": False, "error": f"Publisher script not found: {script_path}"}
|
|
||||||
|
|
||||||
cmd = [sys.executable, str(script_path)]
|
|
||||||
if topic_id:
|
|
||||||
cmd.extend(["--topic-id", topic_id])
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
cwd=PROJECT_ROOT,
|
|
||||||
timeout=300 # 5分钟超时
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"result": result.stdout,
|
|
||||||
"topic_id": topic_id
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"ok": False,
|
|
||||||
"error": result.stderr,
|
|
||||||
"result": result.stdout,
|
|
||||||
"topic_id": topic_id
|
|
||||||
}
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return {"ok": False, "error": "Publisher timed out after 5 minutes", "topic_id": topic_id}
|
|
||||||
except Exception as e:
|
|
||||||
return {"ok": False, "error": str(e), "topic_id": topic_id}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
多平台内容发布脚本
|
|
||||||
将「待发布」的文章发布到各平台(知乎/公众号/小红书/B站/头条号)
|
|
||||||
支持单个 topic 生成发布包模式(--topic-id)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json, datetime, logging, sys, subprocess, time
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
|
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
|
||||||
|
|
||||||
DATA_DIR = PROJECT_ROOT / "automation" / "data"
|
|
||||||
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
|
|
||||||
RELEASES_DIR = DATA_DIR / "releases"
|
|
||||||
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
|
|
||||||
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[
|
|
||||||
logging.FileHandler(LOGS_DIR / f"publisher_{TODAY}.log"),
|
|
||||||
logging.StreamHandler()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# 命令行参数
|
|
||||||
parser = argparse.ArgumentParser(description='发布管理脚本')
|
|
||||||
parser.add_argument('--topic-id', help='仅处理指定 topic ID')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# 平台配置
|
|
||||||
PLATFORMS = {
|
|
||||||
"zhihu": {"name": "知乎", "enabled": True, "template": "zhihu.html"},
|
|
||||||
"wechat": {"name": "微信公众号", "enabled": True, "template": "wechat.html"}, # 需手动授权
|
|
||||||
"xiaohongshu": {"name": "小红书", "enabled": True, "template": "xiaohongshu.html"},
|
|
||||||
"bilibili": {"name": "B站", "enabled": False, "template": "bilibili.html"}, # 规划中
|
|
||||||
"toutiao": {"name": "头条号", "enabled": False, "template": "toutiao.html"} # 规划中
|
|
||||||
}
|
|
||||||
|
|
||||||
def load_topics() -> List[Dict]:
|
|
||||||
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
def save_topics(topics: List[Dict]):
|
|
||||||
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(topics, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
def get_ready_topics() -> List[Dict]:
|
|
||||||
topics = load_topics()
|
|
||||||
ready = [t for t in topics if t.get('status') == '待发布']
|
|
||||||
ready.sort(key=lambda t: t.get('ready_at', ''), reverse=True) # 优先最新
|
|
||||||
return ready, topics
|
|
||||||
|
|
||||||
def publish_to_xiaohongshu(html_path: Path, topic: Dict) -> bool:
|
|
||||||
"""小红书:复制HTML到发布目录(供手动发布)"""
|
|
||||||
logger.info(f"准备小红书发布: {topic['id']}")
|
|
||||||
try:
|
|
||||||
publish_base = PROJECT_ROOT / "content" / "published"
|
|
||||||
dest_dir = publish_base / topic['id'] / "手动发布" / "小红书"
|
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
dest_html = dest_dir / "文章.html"
|
|
||||||
import shutil
|
|
||||||
shutil.copy2(html_path, dest_html)
|
|
||||||
|
|
||||||
logger.info(f"✅ 小红书发布包就绪: {dest_dir}")
|
|
||||||
return True, str(dest_dir)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"小红书发布准备失败: {e}")
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
def publish_to_zhihu(html_path: Path, topic: Dict) -> bool:
|
|
||||||
"""知乎:复制HTML到发布目录"""
|
|
||||||
logger.info(f"准备知乎发布: {topic['id']}")
|
|
||||||
try:
|
|
||||||
publish_base = PROJECT_ROOT / "content" / "published"
|
|
||||||
dest_dir = publish_base / topic['id'] / "手动发布" / "知乎"
|
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
dest_html = dest_dir / "文章.html"
|
|
||||||
import shutil
|
|
||||||
shutil.copy2(html_path, dest_html)
|
|
||||||
|
|
||||||
logger.info(f"✅ 知乎发布包就绪: {dest_dir}")
|
|
||||||
return True, str(dest_dir)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"知乎发布准备失败: {e}")
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
def publish_to_wechat(html_path: Path, topic: Dict) -> bool:
|
|
||||||
"""微信公众号:复制HTML到发布目录"""
|
|
||||||
logger.info(f"准备微信公众号发布: {topic['id']}")
|
|
||||||
try:
|
|
||||||
publish_base = PROJECT_ROOT / "content" / "published"
|
|
||||||
dest_dir = publish_base / topic['id'] / "手动发布" / "微信公众号"
|
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
dest_html = dest_dir / "文章.html"
|
|
||||||
import shutil
|
|
||||||
shutil.copy2(html_path, dest_html)
|
|
||||||
|
|
||||||
logger.info(f"✅ 微信公众号发布包就绪: {dest_dir}")
|
|
||||||
return True, str(dest_dir)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"微信公众号发布准备失败: {e}")
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
def publish_to_platform(platform: str, html_path: Path, topic: Dict) -> (bool, str):
|
|
||||||
"""生成平台发布包(人工发布)"""
|
|
||||||
if platform == "xiaohongshu":
|
|
||||||
return publish_to_xiaohongshu(html_path, topic)
|
|
||||||
elif platform == "zhihu":
|
|
||||||
return publish_to_zhihu(html_path, topic)
|
|
||||||
elif platform == "wechat":
|
|
||||||
return publish_to_wechat(html_path, topic)
|
|
||||||
else:
|
|
||||||
logger.warning(f"平台 {platform} 暂未支持")
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
def main():
|
|
||||||
logger.info("=== 多平台内容发布包生成开始 ===")
|
|
||||||
ready, all_topics = get_ready_topics()
|
|
||||||
if not ready:
|
|
||||||
logger.info("没有待发布内容")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# 如果指定了 topic-id,只处理该选题
|
|
||||||
if args.topic_id:
|
|
||||||
ready = [t for t in ready if t['id'] == args.topic_id]
|
|
||||||
if not ready:
|
|
||||||
logger.info(f"未找到指定 topic ID: {args.topic_id}")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for topic in ready:
|
|
||||||
tid = topic['id']
|
|
||||||
title = topic.get('title', '')[:50]
|
|
||||||
release_date = topic.get('ready_at', TODAY)
|
|
||||||
release_dir = RELEASES_DIR / release_date
|
|
||||||
|
|
||||||
platform_urls = topic.get('platform_urls', {})
|
|
||||||
|
|
||||||
for platform, config in PLATFORMS.items():
|
|
||||||
if not config['enabled']:
|
|
||||||
continue
|
|
||||||
# 检查是否已发布过
|
|
||||||
if platform in platform_urls and platform_urls[platform]:
|
|
||||||
logger.info(f"跳过已发布: {tid} - {platform}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
html_file = release_dir / platform / f"{platform}_{tid}_{platform}.html"
|
|
||||||
if not html_file.exists():
|
|
||||||
logger.warning(f"HTML文件不存在: {html_file}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 生成发布包(不自动发布)
|
|
||||||
success, info = publish_to_platform(platform, html_file, topic)
|
|
||||||
if success:
|
|
||||||
results.append((tid, platform, info))
|
|
||||||
logger.info(f"✅ {tid} 发布包已准备: {platform}")
|
|
||||||
else:
|
|
||||||
logger.error(f"❌ {tid} 发布包准备失败: {platform}")
|
|
||||||
|
|
||||||
# 汇总报告
|
|
||||||
summary_file = LOGS_DIR / f"publisher_summary_{TODAY}.json"
|
|
||||||
summary = {
|
|
||||||
"date": TODAY,
|
|
||||||
"total_ready": len(ready),
|
|
||||||
"packages_generated": len(results),
|
|
||||||
"details": [{"topic_id": r[0], "platform": r[1], "path": r[2]} for r in results]
|
|
||||||
}
|
|
||||||
with open(summary_file, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(summary, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
# === 发送日报通知 ===
|
|
||||||
try:
|
|
||||||
# 统计发布数据(按话题去重)
|
|
||||||
unique_tids = set(r[0] for r in results)
|
|
||||||
platforms_set = set(r[1] for r in results)
|
|
||||||
notify_data = {
|
|
||||||
"task": "daily_summary",
|
|
||||||
"date": TODAY,
|
|
||||||
"published_count": len(unique_tids),
|
|
||||||
"platforms": list(platforms_set),
|
|
||||||
"publish_dir": str(PROJECT_ROOT / "content" / "published")
|
|
||||||
}
|
|
||||||
notify_file = LOGS_DIR / f"publisher_notify_{TODAY}.json"
|
|
||||||
with open(notify_file, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(notify_data, f, ensure_ascii=False, indent=2)
|
|
||||||
# 调用 notifier
|
|
||||||
notifier_script = PROJECT_ROOT / "scripts" / "wecom_notifier.py"
|
|
||||||
if notifier_script.exists():
|
|
||||||
subprocess.run(
|
|
||||||
[sys.executable, str(notifier_script), str(notify_file)],
|
|
||||||
cwd=str(PROJECT_ROOT),
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warning("Notifier script not found, skipping notification")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"发送日报通知失败: {e}")
|
|
||||||
# === 通知结束 ===
|
|
||||||
|
|
||||||
logger.info(f"📦 发布包生成完成: {len(results)} 个平台发布包已就绪")
|
|
||||||
print(f"PUBLISH_PACKAGES_READY: {len(results)} packages generated")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
cd /root/openclaw-workspace/projects/yu-zhi-ran
|
|
||||||
python3 scripts/publisher.py --topic-id A01
|
|
||||||
Reference in New Issue
Block a user