Files
yu-zhi-ran/scripts/compliance_optimizer.py
T
Yuzhiran Dev 233e23016c feat: 内容数据迁移至数据库,合规审查全链路打通
- 文章 HTML 存储从文件系统迁移至 articles 表,删除 releases 目录
- 合规审查从 DB 读取 HTML,审查结果写回 DB,通过后自动推进至待发布
- 新增 todayCount 筛选按钮,与系统概览统计数据一致
- 全屏预览修复:提升 z-index 超过侧边栏,添加退出全屏/关闭按钮
- 统一 '优化' → '审查' 命名,消除前后端术语不一致
- 调度器创作完成后自动触发审查(生成 → 审查 → 待发布)
- 清理旧备份/调试文件、过期大纲和研究笔记
2026-05-13 17:33:56 +08:00

282 lines
11 KiB
Python

#!/usr/bin/env python3
import json, datetime, logging, sys, re
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, asdict
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "platform" / "backend"))
from scripts.compliance_checker import check_article
try:
from app.core.nvidia_client import call_llm
HAVE_LLM = True
except ImportError:
HAVE_LLM = False
from db_helper import get_topic_by_id, update_topic_status, get_active_llm_config, get_articles_by_topic, save_article
DATA_DIR = PROJECT_ROOT / "automation" / "data"
DRAFTS_DIR = DATA_DIR / "drafts"
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"optimizer_{TODAY}.log"), logging.StreamHandler()])
logger = logging.getLogger(__name__)
PLATFORM_TAGS = {
"zhihu": ["科技", "职场"],
"xiaohongshu": ["AI", "可持续", "生活方式"]
}
_llm_config_cache = None
def get_llm_config():
global _llm_config_cache
if _llm_config_cache is None:
_llm_config_cache = get_active_llm_config()
return _llm_config_cache
@dataclass
class OptimizationResult:
file: str
platform: str
topic_id: str
title: str
original_issues: int
fixed_issues: int
final_score: int
status: str
def load_topic_map():
from db_helper import export_topics_to_json
topics = export_topics_to_json()
return {t['id']: t for t in topics}
def get_articles_from_db(topic_ids: Optional[List[str]] = None) -> List[Tuple[str, str, str]]:
"""从 articles 表读取 HTML 内容
Returns: [(html_content, platform, topic_id), ...]
"""
from db_helper import get_articles_by_topic
results = []
seen_topics = set()
if topic_ids:
for tid in topic_ids:
articles = get_articles_by_topic(tid)
for a in articles:
if a.get("html_content"):
results.append((a["html_content"], a["platform"], a["topic_id"]))
seen_topics.add(a["topic_id"])
else:
from app.database import SessionLocal
from app.models import Article
db = SessionLocal()
try:
all_articles = db.query(Article).filter(Article.html_content.isnot(None)).all()
for a in all_articles:
results.append((a.html_content, a.platform, a.topic_id))
finally:
db.close()
return results
def fix_wechat_title(html: str, title: str) -> str:
suffix = f" - {TODAY} - 微信公众号"
max_base_len = 32 - len(suffix)
title_tag = re.search(r'<title>([^<]+)</title>', html)
if title_tag:
full_title = title_tag.group(1)
if full_title.endswith(suffix):
base = full_title[:-len(suffix)]
else:
base = full_title.split(" - ")[0]
if len(base) > max_base_len:
base = base[:max_base_len-3] + "..."
new_full = base + suffix
html = html.replace(full_title, new_full)
h1_match = re.search(r'<h1[^>]*>([^<]+)</h1>', html)
if h1_match:
current_h1 = h1_match.group(1)
base_h1 = current_h1.split(" - ")[0] if " - " in current_h1 else current_h1
if len(base_h1) > 32:
base_h1 = base_h1[:29] + "..."
html = html.replace(current_h1, base_h1)
return html
def fix_tags(html: str, platform: str) -> str:
if platform == "zhihu":
tags_str = " ".join(f"#{t}" for t in PLATFORM_TAGS["zhihu"])
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>')
elif platform == "xiaohongshu":
tags_str = " ".join(f"#{t}" for t in PLATFORM_TAGS["xiaohongshu"])
if '<div class="hashtags">' in html:
old = html.split('<div class="hashtags">')[1].split('</div>')[0]
html = html.replace(f'<div class="hashtags">{old}</div>', f'<div class="hashtags">{tags_str}</div>')
return html
def polish_with_llm(html: str, platform: str) -> Tuple[str, Optional[str]]:
"""用 LLM 优化文章内容,返回 (html, log_message_or_None)"""
if not HAVE_LLM:
return html, None
llm_cfg = get_llm_config()
try:
model = llm_cfg.get('model') if llm_cfg else None
temperature = llm_cfg.get('temperature', 0.5) if llm_cfg else 0.5
max_tokens = llm_cfg.get('max_tokens', 4000) if llm_cfg else 4000
system_prompt = llm_cfg.get('system_prompt') if llm_cfg else "你是一个专业的内容创作助手。"
polish_prompt = f"""你是一个专业的内容润色助手。请优化以下文章内容,提升表达的专业性和可读性,保持原文事实、数据、章节结构不变,输出相同的HTML格式(保留<h2>, <h3>, <p>标签)。
原文:
{html}
优化后:"""
polished = call_llm(polish_prompt, model=model, temperature=temperature, max_tokens=max_tokens, system_prompt=system_prompt)
if '<h2' in polished or '<p>' in polished:
return polished, "LLM 内容优化"
except Exception as e:
logger.warning(f"LLM 优化失败: {e}")
return html, None
def optimize_article(html: str, platform: str, topic_data: Dict) -> Tuple[str, List[str]]:
logs = []
if platform == "wechat":
html = fix_wechat_title(html, topic_data.get("title", ""))
logs.append("标题截断(含后缀)")
if platform in ["zhihu", "xiaohongshu"]:
before = html
html = fix_tags(html, platform)
if html != before:
logs.append(f"标签标准化为{PLATFORM_TAGS[platform]}")
img_tags = re.findall(r'<img[^>]*>', html, re.IGNORECASE)
for tag in img_tags:
m = re.search(r'src=["\']([^"\']+)["\']', tag, re.IGNORECASE)
if m:
src = m.group(1)
if not src.startswith('data:image/'):
logs.append(f"图片未内联: {src[:50]}... 需手动修复")
polished, pol_log = polish_with_llm(html, platform)
if pol_log:
html = polished
logs.append(pol_log)
return html, logs
def main(topic_ids: List[str] = None):
logger.info("=== 合规审查与优化开始 ===")
llm_cfg = get_llm_config()
if llm_cfg:
logger.info(f"LLM 配置: {llm_cfg['name']} (model={llm_cfg['model']})")
else:
logger.info("LLM 配置: 使用环境变量默认值")
articles = get_articles_from_db(topic_ids)
if not articles:
logger.warning("未找到任何文章(可能尚未创作或同步到 DB)")
report_file = DRAFTS_DIR / TODAY / "optimization_report.json"
report_file.parent.mkdir(parents=True, exist_ok=True)
with open(report_file, 'w', encoding='utf-8') as f:
json.dump({
"date": TODAY,
"summary": {"total_articles": 0, "passed_auto": 0, "need_manual": 0, "average_score": 0},
"details": [], "all_passed": True
}, f, ensure_ascii=False, indent=2)
print("OPTIMIZATION_COMPLETE: 0 articles found")
sys.exit(0)
topic_map = load_topic_map()
results = []
all_passed = True
for html, platform_dir, topic_id in articles:
topic_data = topic_map.get(topic_id)
if not topic_data:
logger.warning(f"未找到选题: {topic_id}")
continue
check_result = check_article(html, platform_dir, topic_data)
issues = check_result['issues']
score = check_result['score']
label = f"{platform_dir}/{topic_id}"
if issues:
optimized_html, opt_logs = optimize_article(html, platform_dir, topic_data)
recheck = check_article(optimized_html, platform_dir, topic_data)
if recheck['passed']:
save_article(topic_id, platform_dir, optimized_html)
logger.info(f"{label} 已修复并通过审查 ({len(issues)} issues fixed)")
results.append(OptimizationResult(
file=f"db:{platform_dir}_{topic_id}",
platform=platform_dir,
topic_id=topic_id,
title=topic_data.get('title',''),
original_issues=len(issues),
fixed_issues=len(issues) - len(recheck['issues']),
final_score=recheck['score'],
status="passed"
))
else:
logger.warning(f"⚠️ {label} 仍有 {len(recheck['issues'])} 个问题需人工处理")
results.append(OptimizationResult(
file=f"db:{platform_dir}_{topic_id}",
platform=platform_dir,
topic_id=topic_id,
title=topic_data.get('title',''),
original_issues=len(issues),
fixed_issues=len(issues) - len(recheck['issues']),
final_score=recheck['score'],
status="manual_review"
))
all_passed = False
else:
results.append(OptimizationResult(
file=f"db:{platform_dir}_{topic_id}",
platform=platform_dir,
topic_id=topic_id,
title=topic_data.get('title',''),
original_issues=0,
fixed_issues=0,
final_score=score,
status="passed"
))
logger.info(f"{label} 合规检查通过 ({score}分)")
passed_ids = set()
for res in results:
if res.status == "passed":
passed_ids.add(res.topic_id)
for tid in passed_ids:
update_topic_status(tid, 'ready')
logger.info(f"选题 {tid} 状态 → ready(待发布)")
report_file = DRAFTS_DIR / TODAY / "optimization_report.json"
report_file.parent.mkdir(parents=True, exist_ok=True)
report = {
"date": TODAY,
"summary": {
"total_articles": len(results),
"passed_auto": sum(1 for r in results if r.status == "passed"),
"need_manual": sum(1 for r in results if r.status == "manual_review"),
"average_score": sum(r.final_score for r in results) / len(results) if results else 0
},
"details": [asdict(r) for r in results],
"all_passed": all_passed
}
with open(report_file, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
logger.info(f"✅ 合规审查完成: {len(results)} 篇文章, {sum(1 for r in results if r.status=='passed')} 篇通过")
print(f"OPTIMIZATION_COMPLETE: {len(results)} articles, {sum(1 for r in results if r.status=='passed')} passed, {sum(1 for r in results if r.status=='manual_review')} need manual review")
sys.exit(0)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='合规审查与优化任务')
parser.add_argument('--topic-ids', help='逗号分隔的选题ID列表,例如: A01,B02')
args = parser.parse_args()
topic_ids = args.topic_ids.split(',') if args.topic_ids else None
main(topic_ids)