#!/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'([^<]+)', 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']*>([^<]+)', 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 '
' in html: old = html.split('
')[1].split('
')[0] html = html.replace(f'
{old}
', f'
{tags_str}
') elif platform == "xiaohongshu": tags_str = " ".join(f"#{t}" for t in PLATFORM_TAGS["xiaohongshu"]) if '
' in html: old = html.split('
')[1].split('
')[0] html = html.replace(f'
{old}
', f'
{tags_str}
') 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格式(保留

,

,

标签)。 原文: {html} 优化后:""" polished = call_llm(polish_prompt, model=model, temperature=temperature, max_tokens=max_tokens, system_prompt=system_prompt) if '' 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']*>', 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)