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
+23 -66
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
宇之然内容创作流水线(研究 → 大纲 → 撰写 → 合规优化)v2
宇之然内容创作流水线(研究 → 大纲 → 撰写 → 合规优化)v3 - DB version
"""
import json, datetime, logging, sys, subprocess
@@ -10,8 +10,11 @@ from typing import Dict
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
sys.path.insert(0, str(PROJECT_ROOT))
# 导入数据库辅助模块
from db_helper import get_topic_by_id, get_next_topic, update_topic_status
DATA_DIR = PROJECT_ROOT / "automation" / "data"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json" # 保留用于备份
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
@@ -26,71 +29,35 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
def select_next_topic(topic_id: str = None) -> Dict:
"""选择并锁定要创作的选题"""
def save_topics(topics_list):
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics_list, f, ensure_ascii=False, indent=2)
topics = json.loads(TOPICS_FILE.read_text(encoding='utf-8'))
"""选择并锁定要创作的选题(从数据库)"""
if topic_id:
# 指定ID尝试直接锁定
topic = next((t for t in topics if t['id'] == topic_id), None)
# 指定ID查询数据库
topic = get_topic_by_id(topic_id)
if not topic:
raise ValueError(f"Topic {topic_id} not found")
# 检查状态:禁止已发布状态重新创作
current_status = topic.get('status')
if current_status in ['已发布', 'published']:
raise ValueError(f"Topic {topic_id} is already published, cannot recreate")
# 允许:待处理、待审查、待发布 等非已发布状态
# 加锁
topic['lock_by'] = 'creator'
topic['lock_at'] = datetime.datetime.now().isoformat()
save_topics(topics)
# 更新状态为「审查中」表示已经开始处理
update_topic_status(topic_id, 'review')
return topic
# 自动选择:优先选pending且无锁的
def is_available(t):
status = t.get('status')
# 只处理 pending 或 待处理
if status not in ['pending', '待处理']:
return False
# 检查锁
lock_by = t.get('lock_by')
if lock_by:
# 如果有人锁了,检查是否超时(>2小时)
lock_at_str = t.get('lock_at')
if lock_at_str:
try:
lock_at = datetime.datetime.fromisoformat(lock_at_str)
if (datetime.datetime.now() - lock_at).total_seconds() < 7200:
return False
except:
pass # 解析失败,认为是有效锁
else:
return False
return True
available = [t for t in topics if is_available(t)]
if not available:
# 自动选择:下一个待处理的选题
topic = get_next_topic(priority='') or get_next_topic()
if not topic:
raise ValueError("No available topics to create (all locked or wrong status)")
available.sort(key=lambda t: t.get('priority_score', 0), reverse=True)
chosen = available[0]
# 锁定
chosen['lock_by'] = 'creator'
chosen['lock_at'] = datetime.datetime.now().isoformat()
save_topics(topics)
return chosen
# 更新状态为「审查中」表示已锁定
update_topic_status(topic['id'], 'review')
return topic
def run_step(script_name: str, topic_id: str) -> bool:
"""运行一个流水线步骤(research/outline/writer"""
script_path = PROJECT_ROOT / "scripts" / script_name
cmd = ["python3", str(script_path), "--topic-id", topic_id]
logger.info(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT), capture_output=True, text=True, timeout=1800) # 30分钟超时,适应AI撰写
result = subprocess.run(cmd, cwd=str(PROJECT_ROOT), capture_output=True, text=True, timeout=1800)
if result.returncode != 0:
logger.error(f"{script_name} 失败: {result.stderr}")
return False
@@ -119,41 +86,31 @@ def run_pipeline(topic_id: str = None) -> Dict:
# 1. 研究
if not run_step("research.py", tid):
update_topic_status(tid, 'pending')
return {"ok": False, "error": "research step failed"}
# 2. 大纲
if not run_step("outline.py", tid):
update_topic_status(tid, 'pending')
return {"ok": False, "error": "outline step failed"}
# 3. 撰写
if not run_step("writer.py", tid):
update_topic_status(tid, 'pending')
return {"ok": False, "error": "writer step failed"}
# 4. 合规优化(自动审核并标记为「待发布」)
if not run_optimizer_step(tid):
update_topic_status(tid, 'pending')
return {"ok": False, "error": "optimizer step failed"}
logger.info(f"创作流水线完成: topic_id={tid}")
return {"ok": True, "topic_id": tid, "stdout": f"SUCCESS: Topic {tid} processed through full pipeline"}
except Exception as e:
logger.exception("流水线执行失败")
return {"ok": False, "error": str(e)}
finally:
# 清理锁(无论成功失败)
if tid:
try:
topics = json.loads(TOPICS_FILE.read_text(encoding='utf-8'))
for t in topics:
if t.get('id') == tid:
# 如果成功或需要人工,保留状态,但清除锁
t['lock_by'] = None
t['lock_at'] = None
break
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
logger.debug(f"已清理选题锁: {tid}")
except Exception as ex:
logger.error(f"清理锁失败: {ex}")
update_topic_status(tid, 'pending')
return {"ok": False, "error": str(e)}
def main():
import argparse