Files
yu-zhi-ran/scripts/compliance_optimizer.py
T
lt 31d6306e3b 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 流水线完整通过。
2026-05-07 11:25:42 +08:00

275 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
合规审查与优化任务(数据库版)
每天 05:45 运行,处理当天所有 draft 文章:
1. 执行合规检查(compliance_checker
2. 自动修复已知问题(标题、标签)
3. 重写合规版本
4. 更新选题状态为「待发布」
5. 生成优化报告通知
"""
import json, datetime, logging, sys, re
from pathlib import Path
from typing import Dict, List
from dataclasses import dataclass, asdict
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.compliance_checker import check_article
# 导入 LLM 客户端(合规优化使用 NVIDIA)
sys.path.insert(0, str(PROJECT_ROOT / "platform" / "backend"))
try:
from app.core.modelscope_client import call_llm
HAVE_LLM = True
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"
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", "可持续", "生活方式"]
}
@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 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>都控制长度(考虑后缀)"""
suffix = f" - {TODAY} - 微信公众号"
max_base_len = 32 - len(suffix) # <title> 中 base 部分允许的最大长度
# 处理 <title>...</title>
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>...</h1>
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 optimize_article(html: str, platform: str, topic_data: Dict) -> (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]}... 需手动修复")
if HAVE_LLM:
try:
polish_prompt = f"""你是一个专业的内容润色助手。请优化以下文章内容,提升表达的专业性和可读性,保持原文事实、数据、章节结构不变,输出相同的HTML格式(保留<h2>, <h3>, <p>标签)。
原文:
{html}
优化后:"""
polished = call_llm(polish_prompt, temperature=0.5, max_tokens=4000)
if '<h2' in polished or '<p>' in polished:
html = polished
logs.append("LLM 内容优化(NVIDIA")
except Exception as e:
logger.warning(f"LLM 优化失败: {e}")
return html, logs
def main(topic_ids: List[str] = None):
logger.info("=== 合规审查与优化开始 ===")
release_dir = RELEASES_DIR / TODAY
if not release_dir.exists():
logger.warning(f"今日发布目录不存在: {release_dir}")
return
topic_map = load_topic_map()
results = []
all_passed = True
for platform_dir in ["zhihu", "wechat", "xiaohongshu"]:
platform_path = release_dir / platform_dir
if not platform_path.exists():
continue
for html_file in platform_path.glob("*.html"):
stem = html_file.stem
parts = stem.split('_')
if len(parts) < 2:
continue
topic_id = parts[1]
if topic_ids is not None and topic_id not in topic_ids:
continue
topic_data = topic_map.get(topic_id)
if not topic_data:
logger.warning(f"未找到选题: {topic_id}")
continue
html = html_file.read_text(encoding='utf-8')
check_result = check_article(html, platform_dir, topic_data)
issues = check_result['issues']
score = check_result['score']
if any(issue['type'] == '平台规则' for issue in issues):
optimized_html, opt_logs = optimize_article(html, platform_dir, topic_data)
recheck = check_article(optimized_html, platform_dir, topic_data)
if recheck['passed']:
html_file.write_text(optimized_html, encoding='utf-8')
logger.info(f"{html_file.name} 已优化并通过合规检查")
results.append(OptimizationResult(
file=str(html_file.relative_to(PROJECT_ROOT)),
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"
))
update_topic_status_db_only(topic_id, 'pending')
else:
logger.warning(f"⚠️ {html_file.name} 优化后仍有问题,需人工审核")
results.append(OptimizationResult(
file=str(html_file.relative_to(PROJECT_ROOT)),
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=str(html_file.relative_to(PROJECT_ROOT)),
platform=platform_dir,
topic_id=topic_id,
title=topic_data.get('title',''),
original_issues=0,
fixed_issues=0,
final_score=score,
status="passed" if check_result['passed'] else "manual_review"
))
if not check_result['passed']:
all_passed = False
# 更新选题状态 (JSON + 数据库)
for res in results:
if res.status == "passed":
tid = res.topic_id
# 更新数据库状态为 '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"
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)