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
+33 -59
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
"""
合规审查与优化任务
合规审查与优化任务(数据库版)
每天 05:45 运行,处理当天所有 draft 文章:
1. 执行合规检查(compliance_checker
2. 自动修复已知问题(标题、标签)
3. 重写合规版本
4. 更新选题状态为「审查通过待发布」
4. 更新选题状态为「待发布」
5. 生成优化报告通知
"""
@@ -26,10 +26,12 @@ try:
except ImportError:
HAVE_LLM = False
# 导入数据库辅助模块
from db_helper import get_topic_by_id, update_topic_status
DATA_DIR = PROJECT_ROOT / "automation" / "data"
RELEASES_DIR = DATA_DIR / "releases"
DRAFTS_DIR = DATA_DIR / "drafts"
TOPICS_FILE = DATA_DIR / "sustainability_topics.json"
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
@@ -55,42 +57,15 @@ class OptimizationResult:
status: str
def load_topic_map():
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
"""从数据库加载所有选题数据"""
from db_helper import export_topics_to_json
topics = export_topics_to_json()
return {t['id']: t for t in topics}
def update_topic_status(topic_id: str, status: str):
"""更新选题状态(JSON + 数据库)"""
# 更新 JSON
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
updated = False
for t in topics:
if t.get('id') == topic_id:
t['status'] = status
updated = True
break
if updated:
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
# 更新数据库
try:
import sys
from pathlib import Path
backend_path = Path(__file__).resolve().parents[2] / 'platform' / 'backend'
if str(backend_path) not in sys.path:
sys.path.insert(0, str(backend_path))
from app.database import SessionLocal
from app.models import Topic
db = SessionLocal()
topic_db = db.query(Topic).filter(Topic.id == topic_id).first()
if topic_db:
topic_db.status = status
db.commit()
db.close()
except Exception as e:
logger.error(f"更新数据库失败: {e}")
def update_topic_status_db_only(topic_id: str, status: str):
"""仅更新数据库状态(不更新JSON"""
from db_helper import update_topic_status
update_topic_status(topic_id, status)
def fix_wechat_title(html: str, title: str) -> str:
"""微信标题优化:<title>和<h1>都控制长度(考虑后缀)"""
@@ -101,7 +76,6 @@ def fix_wechat_title(html: str, title: str) -> str:
title_tag = re.search(r'<title>([^<]+)</title>', html)
if title_tag:
full_title = title_tag.group(1)
# 提取 base(去掉后缀)
if full_title.endswith(suffix):
base = full_title[:-len(suffix)]
else:
@@ -111,11 +85,10 @@ def fix_wechat_title(html: str, title: str) -> str:
new_full = base + suffix
html = html.replace(full_title, new_full)
# 处理 <h1>...</h1>(不含后缀,但要截断)
# 处理 <h1>...</h1>
h1_match = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
if h1_match:
current_h1 = h1_match.group(1)
# 如果 h1 包含后缀(不应该),去掉
base_h1 = current_h1.split(" - ")[0] if " - " in current_h1 else current_h1
if len(base_h1) > 32:
base_h1 = base_h1[:29] + "..."
@@ -127,7 +100,6 @@ def fix_tags(html: str, platform: str) -> str:
"""强制替换标签为平台白名单"""
if platform == "zhihu":
tags_str = " ".join(f"#{t}" for t in PLATFORM_TAGS["zhihu"])
# 替换 <div class="tags">...</div>
if '<div class="tags">' in html:
old = html.split('<div class="tags">')[1].split('</div>')[0]
html = html.replace(f'<div class="tags">{old}</div>', f'<div class="tags">{tags_str}</div>')
@@ -140,18 +112,14 @@ def fix_tags(html: str, platform: str) -> str:
def optimize_article(html: str, platform: str, topic_data: Dict) -> (str, List[str]):
logs = []
# 1. 标题优化(微信)
if platform == "wechat":
html = fix_wechat_title(html, topic_data.get("title", ""))
logs.append("标题截断(含后缀)")
# 2. 标签优化
if platform in ["zhihu", "xiaohongshu"]:
before = html
html = fix_tags(html, platform)
if html != before:
logs.append(f"标签标准化为{PLATFORM_TAGS[platform]}")
# 3. 图片内联检查
# 提取所有 img 标签
img_tags = re.findall(r'<img[^>]*>', html, re.IGNORECASE)
for tag in img_tags:
m = re.search(r'src=["\']([^"\']+)["\']', tag, re.IGNORECASE)
@@ -160,7 +128,6 @@ def optimize_article(html: str, platform: str, topic_data: Dict) -> (str, List[s
if not src.startswith('data:image/'):
logs.append(f"图片未内联: {src[:50]}... 需手动修复")
# 4. LLM 内容优化(使用 NVIDIA step-3.5-flash
if HAVE_LLM:
try:
polish_prompt = f"""你是一个专业的内容润色助手。请优化以下文章内容,提升表达的专业性和可读性,保持原文事实、数据、章节结构不变,输出相同的HTML格式(保留<h2>, <h3>, <p>标签)。
@@ -198,7 +165,6 @@ def main(topic_ids: List[str] = None):
if len(parts) < 2:
continue
topic_id = parts[1]
# 如果指定了 topic_ids,则只处理匹配的
if topic_ids is not None and topic_id not in topic_ids:
continue
topic_data = topic_map.get(topic_id)
@@ -227,7 +193,7 @@ def main(topic_ids: List[str] = None):
final_score=recheck['score'],
status="passed"
))
update_topic_status(topic_id, '待发布')
update_topic_status_db_only(topic_id, 'pending')
else:
logger.warning(f"⚠️ {html_file.name} 优化后仍有问题,需人工审核")
results.append(OptimizationResult(
@@ -255,20 +221,28 @@ def main(topic_ids: List[str] = None):
if not check_result['passed']:
all_passed = False
# 更新选题状态
# 更新选题状态 (JSON + 数据库)
for res in results:
if res.status == "passed":
tid = res.topic_id
with open(TOPICS_FILE, 'r', encoding='utf-8') as f:
topics = json.load(f)
for t in topics:
if t.get('id') == tid:
t['status'] = '待发布'
t['ready_at'] = TODAY
t['compliance_score'] = res.final_score
break
with open(TOPICS_FILE, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
# 更新数据库状态为 'pending'(待发布)
update_topic_status(tid, 'ready')
# 可选:同时更新 JSON 以保持兼容
# (已废弃,但保留更新,避免其他组件出错)
try:
json_path = DATA_DIR / "sustainability_topics.json"
with open(json_path, 'r', encoding='utf-8') as f:
topics = json.load(f)
for t in topics:
if t.get('id') == tid:
t['status'] = 'ready'
t['ready_at'] = TODAY
t['compliance_score'] = res.final_score
break
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(topics, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.warning(f"更新 JSON 失败: {e}")
# 生成报告
report_file = DRAFTS_DIR / TODAY / "optimization_report.json"