#!/usr/bin/env python3 """ 内容合规审查模块 检查文章是否符合法律法规、平台规则、品牌规范 """ import re import json from typing import Dict, List, Tuple SENSITIVE_WORDS = { "政治敏感": ["国家主席", "政治局", "常委", "军委", "统战部", "颠覆国家", "分裂主义", "台独", "疆独", "藏独"], "违禁内容": ["赌博", "毒品", "迷药", "枪支", "炸药", "色情", "低俗", "反动", "邪教"], "不实信息": [" guaranteed 赚钱", "一夜暴富", "100%有效", "包治百病", "绝对正确"], "领导人相关": ["主席", "总理", "总书记", "国家领导人"] } PLATFORM_RULES = { "zhihu": { "max_title_len": 100, "min_word_count": 1000, "allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"], "forbidden_patterns": [r"加微信", r"私聊", r"付费咨询", r"点击领取"] }, "wechat": { "max_title_len": 32, "min_word_count": 800, "allowed_tags": ["科技", "生活", "职场", "教育", "可持续", "AI", "远程工作", "个人成长"], "forbidden_patterns": [r"诱导分享", r"朋友圈", r"转发群"] }, "xiaohongshu": { "max_title_len": 50, "min_word_count": 400, "allowed_tags": ["生活方式", "可持续", "AI", "个人成长", "极简", "环保"], "forbidden_patterns": [r"私信", r"加群", r"导流"] } } # AI 套话检测模式(一旦出现在正文中,说明写作痕迹明显) AI_TELTALES = [ "说回到", "一个真实的.*案例很能说明问题", "这就是.*被.*后的样子", "如果你也", "值得注意的是", "首先其次最后", "综上所述", "总的来说", "说到这里", "我们来总结一下", "总而言之", "我们不难发现", "我们可以看出", "从以上分析可以看出", "无可否认", "众所周知", "毋庸置疑", "不知大家有没有发现", "不可否认", "毫无疑义", "从某种意义上", "从某种程度上", "在一定程度上", "换而言之", "换言之", "从本质", "归根结底", "说到底", "这为我们提供了", "为我们提供了宝贵的", "引发了我们", "不得不让人思考", "引人深思", "毫无悬念", "毫无意外", "毫无争议", ] _cached_sensitive_words = None _cached_platform_rules = None _cached_ai_telltales = None def _load_sensitive_words(): global _cached_sensitive_words if _cached_sensitive_words is not None: return _cached_sensitive_words try: from app.core.prompt_loader import _get_session from app.models import SensitiveWord session = _get_session() try: rows = session.query(SensitiveWord).filter(SensitiveWord.is_active == True).all() if rows: result = {} for r in rows: cat = r.category or "general" if cat not in result: result[cat] = [] result[cat].append(r.word) _cached_sensitive_words = result return _cached_sensitive_words finally: session.close() except Exception: pass _cached_sensitive_words = SENSITIVE_WORDS return _cached_sensitive_words def _load_platform_rules(): global _cached_platform_rules if _cached_platform_rules is not None: return _cached_platform_rules try: from app.core.prompt_loader import _get_session from app.models import PlatformConfig import json session = _get_session() try: rows = session.query(PlatformConfig).all() if rows: result = {} for r in rows: try: cfg = json.loads(r.config_data) if r.config_data else {} except Exception: cfg = {} if cfg: result[r.platform] = cfg if result: _cached_platform_rules = result return _cached_platform_rules finally: session.close() except Exception: pass _cached_platform_rules = PLATFORM_RULES return _cached_platform_rules def _load_ai_telltales(): """从 DB ContentCleanRule 加载 AI 套话模式(rule_type='ai_telltale'),无 DB 时回退硬编码列表""" global _cached_ai_telltales if _cached_ai_telltales is not None: return _cached_ai_telltales try: from app.core.prompt_loader import _get_session from app.models import ContentCleanRule session = _get_session() try: rows = session.query(ContentCleanRule).filter( ContentCleanRule.rule_type == 'ai_telltale', ContentCleanRule.is_active == True ).order_by(ContentCleanRule.sort_order).all() if rows: _cached_ai_telltales = [r.pattern for r in rows] return _cached_ai_telltales finally: session.close() except Exception: pass _cached_ai_telltales = list(AI_TELTALES) return _cached_ai_telltales class ComplianceChecker: """合规审查器""" def __init__(self, platform_config: Dict = None): self.issues = [] self.platform_config = platform_config or {} def _get_platform_rule(self, key: str, default=None): """从 platform_config 读取规则,fallback 到硬编码 PLATFORM_RULES""" if self.platform_config: compliance_rules = self.platform_config.get('compliance_rules', {}) if key in compliance_rules: return compliance_rules[key] if key == 'min_word_count' and self.platform_config.get('min_words'): return self.platform_config['min_words'] return default def check_text(self, text: str, platform: str, topic_data: Dict = None) -> Dict: """执行全面合规检查""" self.issues = [] # 1. 敏感词检查 self._check_sensitive_words(text) # 2. 平台规则检查 self._check_platform_rules(text, platform) # 3. 法律法规检查 self._check_legal_compliance(text) # 4. 品牌调性检查 self._check_brand_guidelines(text) # 5. 内容事实性检查(如有主题数据) if topic_data: self._check_factual_consistency(text, topic_data) # 6. 最小字数检查 self._check_min_length(text, platform) # 7. 内容质量检查 self._check_ai_telltales(text) self._check_pronoun_consistency(text, platform) self._check_reading_experience(text, platform) self._check_platform_engagement(text, platform) self._check_inline_images(text) self._check_timeliness(text) hard_types = ('敏感词', '法律法规', '平台规则', '品牌规范', '资源合规') hard_issues = [i for i in self.issues if i['type'] in hard_types] return { "passed": len(hard_issues) == 0, "issues": self.issues, "score": max(0, 100 - sum( 10 if i['type'] in hard_types else 5 for i in self.issues )) } def _check_sensitive_words(self, text: str): """检查敏感词""" words_map = _load_sensitive_words() for category, words in words_map.items(): for word in words: if word in text: self.issues.append({ "type": "敏感词", "category": category, "word": word, "suggestion": f"删除或替换'{word}'" }) def _check_platform_rules(self, text: str, platform: str): """检查平台特定规则""" rules_map = _load_platform_rules() rules = rules_map.get(platform, {}) # 标题长度(从HTML中提取) max_title_len = self._get_platform_rule('max_title_len', rules.get("max_title_len")) title_match = re.search(r'
(.*?)
', text, re.DOTALL) long_paras = [p for p in paragraphs if len(p) > 300] if len(long_paras) > len(paragraphs) * 0.3: self.issues.append({ "type": "内容质量", "category": "段落过长", "detail": f"超过30%的段落长度>300字(共{len(paragraphs)}段,{len(long_paras)}段过长),在手机上阅读体验差", "suggestion": "将长段拆分为2-3个短段,每段不超过150-200字" }) # 图片检测(各平台阈值不同) imgs = re.findall(r'(.*?)
', text, re.DOTALL) long_paras = [p for p in paragraphs if len(p) > 150] if len(long_paras) > len(paragraphs) * 0.2: self.issues.append({ "type": "内容质量", "category": "段落过长", "detail": f"小红书建议每段不超过80-100字,当前{len(long_paras)}/{len(paragraphs)}段超过150字", "suggestion": "将长段拆分为1-2句的短段落,每段不超过100字" }) # 小红书需要至少一些 emoji if not re.search(r'[\U0001F300-\U0001F9FF\u2600-\u27BF]', plain): self.issues.append({ "type": "内容质量", "category": "缺少emoji", "detail": "小红书笔记建议适当使用emoji来增加视觉吸引力", "suggestion": "在标题、章节分隔或重点句前添加相关emoji" }) def _check_inline_images(self, html: str): """检查图片是否以内联方式嵌入(data:image)""" # 提取所有 img 标签的 src 属性值 srcs = re.findall(r'