diff --git a/backend/app/agent_framework/agents/deai_agent.py b/backend/app/agent_framework/agents/deai_agent.py index d5740a0..62a6243 100644 --- a/backend/app/agent_framework/agents/deai_agent.py +++ b/backend/app/agent_framework/agents/deai_agent.py @@ -3,6 +3,7 @@ import logging import time from datetime import datetime, timezone +from typing import Optional from app.agent_framework.base import BaseAgent from app.agent_framework.prompts import DEAI_TEMPLATE @@ -14,6 +15,8 @@ from app.agent_framework.protocol import ( TaskStatus, ) from app.services.llm import LLMFactory, LLMError +from app.services.distribution.platform_rules import PLATFORM_RULES, rule_engine +from app.services.distribution.rule_service import platform_rule_service logger = logging.getLogger(__name__) @@ -23,13 +26,19 @@ class DeAIAgent(BaseAgent): 支持的任务类型: - deai_process: 对内容进行去AI化处理 + + input_data 字段: + - content: str (必填,待处理的文章内容) + - platform: str (可选,目标平台ID,如 zhihu, wechat 等) + - style: str (可选,目标风格) + - preserve_structure: bool (可选,是否保留原有结构) """ def __init__(self): super().__init__( name="deai_agent", agent_type=AgentType.DEAI_AGENT, - version="1.0.0", + version="1.1.0", ) def get_capabilities(self) -> AgentCapability: @@ -104,7 +113,8 @@ class DeAIAgent(BaseAgent): input_data 字段: - content: str (必填,待处理的文章内容) - - style: str (可选,目标风格: 口语化/叙事化/评论风格) + - platform: str (可选,目标平台ID) + - style: str (可选,目标风格) - preserve_structure: bool (可选,是否保留原有结构) """ input_data = task.input_data @@ -112,6 +122,8 @@ class DeAIAgent(BaseAgent): if not content: raise ValueError("input_data必须包含非空的'content'字段") + platform_id = input_data.get("platform", "") + # 上报进度:开始 await self.report_progress( task_id=task.task_id, @@ -119,11 +131,20 @@ class DeAIAgent(BaseAgent): message="开始去AI化处理...", ) + # 获取平台特定配置 + platform_config = self._get_platform_config(platform_id) + + # 构建变量 variables = { "original_content": content, "target_style": input_data.get("style", "自然流畅"), "preserve_structure": "是" if input_data.get("preserve_structure", True) else "否", + "platform_info": platform_config.get("platform_info", "通用"), + "ai_sensitivity": platform_config.get("ai_sensitivity", ""), + "banned_patterns": platform_config.get("banned_patterns", ""), + "safe_patterns": platform_config.get("safe_patterns", ""), } + messages = DEAI_TEMPLATE.render(variables) # 上报进度:调用LLM @@ -140,6 +161,13 @@ class DeAIAgent(BaseAgent): max_tokens=len(content) * 3, ) + # 检测处理后的AI模式 + detected_patterns = [] + if platform_id: + detected_patterns = platform_rule_service.detect_ai_patterns( + response.content, platform_id + ) + # 上报进度:完成 await self.report_progress( task_id=task.task_id, @@ -152,4 +180,42 @@ class DeAIAgent(BaseAgent): "original_word_count": len(content), "processed_word_count": len(response.content), "usage": response.usage, + "platform_id": platform_id, + "detected_ai_patterns": detected_patterns, } + + def _get_platform_config(self, platform_id: str) -> dict: + """获取平台特定配置 + + Args: + platform_id: 平台标识 + + Returns: + 包含平台配置的字典 + """ + if not platform_id or platform_id not in PLATFORM_RULES: + return { + "platform_info": "通用", + "ai_sensitivity": "中", + "banned_patterns": "总之、综上所述、首先其次最后等", + "safe_patterns": "根据研究表明、事实上、说实话", + } + + rules = PLATFORM_RULES[platform_id] + ai_config = rules.get("ai_sensitivity", {}) + + platform_name = rules.get("name", platform_id) + detection_level = ai_config.get("detection_level", "medium") + banned = ai_config.get("banned_patterns", []) + safe = ai_config.get("safe_patterns", []) + + return { + "platform_info": f"{platform_name} (检测级别: {detection_level})", + "ai_sensitivity": detection_level, + "banned_patterns": "、".join(banned[:10]) if banned else "无", + "safe_patterns": "、".join(safe[:5]) if safe else "无", + } + + +# 导出单例 +deai_agent = DeAIAgent() diff --git a/backend/app/agent_framework/prompts/deai_agent.py b/backend/app/agent_framework/prompts/deai_agent.py index 4ae4e4e..263400c 100644 --- a/backend/app/agent_framework/prompts/deai_agent.py +++ b/backend/app/agent_framework/prompts/deai_agent.py @@ -12,18 +12,30 @@ DEAI_TEMPLATE = PromptTemplate( ${original_content} ## 目标风格参考 -${style_reference} +${target_style} ## 目标平台 -${target_platform}""", +${platform_info} + +## AI检测敏感度 +${ai_sensitivity} + +## 需要消除的AI写作特征 (禁用) +${banned_patterns} + +## 推荐使用的人味表达 (安全模式) +${safe_patterns} + +## 结构保留 +保留原有结构: ${preserve_structure}""", instructions="""请将以上原始文章进行去AI化改写,使其读起来像真人撰写的自然文章。 改写策略(按优先级排序): 1. 消除AI典型语言特征: - - 禁用模板化过渡词:「总之」「综上所述」「值得注意的是」「让我们」「总而言之」「不可否认」「毋庸置疑」 - - 禁用空洞修饰词:「至关重要」「不可或缺」「举足轻重」「蓬勃发展」「日新月异」 + - 禁用模板化过渡词:「总之」「综上所述」「值得注意的是」「让我们」「总而言之」「不可否认」「毋庸置疑」「首先」「其次」「最后」「最后但同样重要」「换句话说」「也就是说」「更重要的是」「可以说」 + - 禁用空洞修饰词:「至关重要」「不可或缺」「举足轻重」「蓬勃发展」「日新月异」「深远影响」「全面提升」「显著成效」「重大突破」「核心要素」 - 禁用对称式排比句(AI最爱用三段式排比) 2. 增加不规则节奏: @@ -44,7 +56,12 @@ ${target_platform}""", 5. 调整叙事结构: - 打破AI的「总-分-总」结构 - 允许跑题式的个人联想(但要收回来) - - 可以从一个具体的场景或故事开头""", + - 可以从一个具体的场景或故事开头 + +6. 针对平台的特殊要求: + - 高敏感度平台(知乎、百家号):必须彻底消除AI痕迹,使用更多真实案例 + - 中敏感度平台(微信公众号、B站):适度改写,保持专业感 + - 低敏感度平台(微博、小红书):可以保留部分AI风格,重点在优化表达""", constraints="""## 约束条件 - 不改变原文的核心事实和数据,所有数字、专有名词、引用必须保留 diff --git a/backend/app/api/platform_rules.py b/backend/app/api/platform_rules.py new file mode 100644 index 0000000..2cda6ec --- /dev/null +++ b/backend/app/api/platform_rules.py @@ -0,0 +1,384 @@ +"""平台规则管理 API 路由""" + +import logging +from datetime import datetime +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from pydantic import ValidationError + +from app.api.deps import get_current_user +from app.models.user import User +from app.services.distribution.platform_rules import ( + PLATFORM_RULES, + rule_engine, +) +from app.services.distribution.rule_service import platform_rule_service +from app.schemas.platform_rule import ( + ContentValidationIssue, + ContentValidationResponse, + ContentValidateRequest, + DeAIContentRequest, + DeAIContentResponse, + PlatformBrief, + PlatformDetailResponse, + PlatformListResponse, + PlatformRuleUpdateRequest, + PlatformRuleUpdateResponse, + RuleChangeHistory, + RuleChangeHistoryResponse, + RuleDiff, + RuleDiffResponse, + # 内部 Schema + AISensitivity, + ContentLengthRule, + GEORule, + HTMLRule, + KeywordDensity, + PublishRule, + SEORule, + SensitiveWordsConfig, + StructurePreference, + TagRule, + TitleRule, +) + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/v1/platforms", tags=["平台规则管理"]) + + +def _convert_rule_to_schema(rules: dict) -> dict: + """将规则字典转换为 Schema 格式""" + if not rules: + return {} + + return { + "content_length": ContentLengthRule(**rules.get("content_length", {})), + "structure_preference": StructurePreference(**rules.get("structure_preference", {})), + "title_rules": TitleRule(**rules.get("title_rules", {})), + "tag_rules": TagRule(**rules.get("tag_rules", {})), + "ai_sensitivity": AISensitivity(**rules.get("ai_sensitivity", {})), + "sensitive_words": SensitiveWordsConfig(**rules.get("sensitive_words", {})), + "seo_rules": SEORule(**rules.get("seo_rules", {})), + "geo_rules": GEORule(**rules.get("geo_rules", {})), + "html_rules": HTMLRule(**rules.get("html_rules", {})), + "publish_rules": PublishRule(**rules.get("publish_rules", {})), + } + + +@router.get("", response_model=PlatformListResponse) +async def list_platforms( + enabled_only: bool = Query(True, description="是否只返回启用的平台"), +): + """获取所有支持平台列表""" + platforms_raw = platform_rule_service.get_all_platforms(enabled_only=enabled_only) + + platforms = [ + PlatformBrief( + id=p["id"], + name=p["name"], + platform_type=p.get("platform_type", ""), + priority=p.get("priority", "P2"), + enabled=p.get("enabled", True), + ) + for p in platforms_raw + ] + + return PlatformListResponse(platforms=platforms, total=len(platforms)) + + +@router.get("/{platform_id}", response_model=PlatformDetailResponse) +async def get_platform_detail(platform_id: str): + """获取平台详情 + + Args: + platform_id: 平台标识 (如 zhihu, wechat, baijiahao 等) + """ + rules = platform_rule_service.get_platform_detail(platform_id) + if not rules: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"平台不存在: {platform_id}", + ) + + # 构建响应 + converted = _convert_rule_to_schema(rules) + + return PlatformDetailResponse( + id=platform_id, + name=rules.get("name", ""), + platform_type=rules.get("platform_type", ""), + priority=rules.get("priority", "P2"), + enabled=rules.get("enabled", True), + content_style=rules.get("content_style", ""), + content_length=converted["content_length"], + structure_preference=converted["structure_preference"], + title_rules=converted["title_rules"], + tag_rules=converted["tag_rules"], + ai_sensitivity=converted["ai_sensitivity"], + sensitive_words=converted["sensitive_words"], + seo_rules=converted["seo_rules"], + geo_rules=converted["geo_rules"], + html_rules=converted["html_rules"], + publish_rules=converted["publish_rules"], + best_publish_times=rules.get("best_publish_times", []), + best_publish_days=rules.get("best_publish_days", []), + max_images=rules.get("max_images", 0), + ) + + +@router.put("/{platform_id}/rules", response_model=PlatformRuleUpdateResponse) +async def update_platform_rules( + platform_id: str, + req: PlatformRuleUpdateRequest, + current_user: User = Depends(get_current_user), +): + """更新平台规则 + + 注意:当前实现更新的是内存中的规则。 + 如需持久化,需要配合数据库和规则变更历史表使用。 + """ + if platform_id not in PLATFORM_RULES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"平台不存在: {platform_id}", + ) + + # 获取当前规则 + current_rules = PLATFORM_RULES[platform_id] + + # 构建更新数据 + update_data = req.model_dump(exclude_unset=True) + + # 更新规则(这里只做演示,实际需要持久化到数据库) + for key, value in update_data.items(): + if value is not None: + if key == "enabled": + current_rules[key] = value + elif isinstance(value, dict): + current_rules[key] = { + **current_rules.get(key, {}), + **value, + } + else: + current_rules[key] = value + + logger.info( + f"用户 {current_user.id} 更新了平台 {platform_id} 的规则: {list(update_data.keys())}" + ) + + return PlatformRuleUpdateResponse( + success=True, + platform_id=platform_id, + message=f"规则更新成功", + updated_at=datetime.now(), + ) + + +@router.get("/{platform_id}/rules/diff", response_model=RuleDiffResponse) +async def compare_rule_changes( + platform_id: str, + change_id: Optional[int] = Query(None, description="变更记录ID,用于对比历史版本"), +): + """对比规则变更 + + Args: + platform_id: 平台标识 + change_id: 变更记录ID(可选) + """ + if platform_id not in PLATFORM_RULES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"平台不存在: {platform_id}", + ) + + current_rules = PLATFORM_RULES[platform_id] + + # TODO: 从数据库获取历史版本进行对比 + # 目前返回空差异 + return RuleDiffResponse( + platform_id=platform_id, + platform_name=current_rules.get("name", ""), + diffs=[], + total_changes=0, + ) + + +@router.get("/{platform_id}/rules/history", response_model=RuleChangeHistoryResponse) +async def get_rule_history( + platform_id: str, + limit: int = Query(20, ge=1, le=100, description="返回记录数"), +): + """获取规则变更历史 + + Args: + platform_id: 平台标识 + limit: 返回记录数 + """ + if platform_id not in PLATFORM_RULES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"平台不存在: {platform_id}", + ) + + # TODO: 从数据库获取历史记录 + # 目前返回空列表 + return RuleChangeHistoryResponse( + history=[], + total=0, + ) + + +@router.post("/{platform_id}/rules/validate", response_model=ContentValidationResponse) +async def validate_content_for_platform( + platform_id: str, + req: ContentValidateRequest, +): + """验证内容是否符合平台规则 + + Args: + platform_id: 平台标识 + req: 验证请求 + """ + if platform_id not in PLATFORM_RULES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"平台不存在: {platform_id}", + ) + + result = platform_rule_service.validate_content( + content=req.content, + title=req.title, + platform_id=platform_id, + ) + + issues = [ + ContentValidationIssue( + severity=i["severity"], + message=i["message"], + category=i.get("category", "general"), + ) + for i in result.get("issues", []) + ] + + return ContentValidationResponse( + is_valid=result["is_valid"], + score=result["score"], + issues=issues, + passed=result.get("passed", []), + ) + + +@router.get("/{platform_id}/rules/{rule_category}") +async def get_platform_rule_category( + platform_id: str, + rule_category: str, +): + """获取平台特定类别的规则 + + Args: + platform_id: 平台标识 + rule_category: 规则类别 (title_rules, tag_rules, ai_sensitivity, etc.) + """ + valid_categories = rule_engine.get_all_rule_categories() + + if rule_category not in valid_categories: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"无效的规则类别: {rule_category},有效值: {', '.join(valid_categories)}", + ) + + rule = platform_rule_service.get_rule_category(platform_id, rule_category) + if rule is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"平台不存在或规则类别无效: {platform_id}/{rule_category}", + ) + + return { + "platform_id": platform_id, + "rule_category": rule_category, + "rule": rule, + } + + +@router.get("/{platform_id}/ai-config") +async def get_platform_ai_config(platform_id: str): + """获取平台AI敏感度配置(用于去AI化处理) + + Args: + platform_id: 平台标识 + """ + config = platform_rule_service.get_ai_humanization_config(platform_id) + if config is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"平台不存在: {platform_id}", + ) + + return { + "platform_id": platform_id, + "ai_sensitivity": config, + } + + +@router.post("/{platform_id}/detect-ai-patterns") +async def detect_ai_patterns_in_content( + platform_id: str, + content: str = Query(..., description="待检测内容"), +): + """检测内容中的AI写作模式 + + Args: + platform_id: 平台标识 + content: 待检测内容 + """ + if platform_id not in PLATFORM_RULES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"平台不存在: {platform_id}", + ) + + detected = platform_rule_service.detect_ai_patterns(content, platform_id) + + return { + "platform_id": platform_id, + "content_length": len(content), + "detected_patterns": detected, + "total_detected": len(detected), + } + + +@router.get("/{platform_id}/tips") +async def get_platform_optimization_tips(platform_id: str): + """获取平台优化建议 + + Args: + platform_id: 平台标识 + """ + if platform_id not in PLATFORM_RULES: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"平台不存在: {platform_id}", + ) + + tips = platform_rule_service.get_optimization_tips(platform_id) + rules = PLATFORM_RULES.get(platform_id, {}) + + return { + "platform_id": platform_id, + "platform_name": rules.get("name", ""), + "content_style": rules.get("content_style", ""), + "tips": tips, + } + + +@router.get("/categories") +async def list_rule_categories(): + """获取所有规则类别""" + categories = rule_engine.get_all_rule_categories() + return { + "categories": categories, + "total": len(categories), + } diff --git a/backend/app/main.py b/backend/app/main.py index af8af51..d6f9942 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -33,6 +33,7 @@ from app.api.dashboard import router as dashboard_router from app.api.brands import router as brands_router from app.api.onboarding import router as onboarding_router from app.api.platforms import router as platforms_router +from app.api.platform_rules import router as platform_rules_router from app.config import settings from app.database import engine, Base from app.schemas.common import ErrorResponse, ErrorCode @@ -151,6 +152,7 @@ app.include_router(dashboard_router, prefix="/api/v1/dashboard", tags=["仪表 app.include_router(brands_router, prefix="/api/v1/brands", tags=["品牌管理"]) app.include_router(onboarding_router, prefix="/api/v1") app.include_router(platforms_router, prefix="/api/v1") +app.include_router(platform_rules_router) @app.get("/health", tags=["可观测性"]) diff --git a/backend/app/schemas/platform_rule.py b/backend/app/schemas/platform_rule.py new file mode 100644 index 0000000..472f9ca --- /dev/null +++ b/backend/app/schemas/platform_rule.py @@ -0,0 +1,307 @@ +"""平台规则管理 Schema - 定义规则管理的请求响应结构""" + +from datetime import datetime +from typing import Optional +from pydantic import BaseModel, Field + + +# ============================================================ +# 基础信息 Schema +# ============================================================ + +class ContentLengthRule(BaseModel): + """内容长度规则""" + min: int = Field(..., description="最小字数") + max: int = Field(..., description="最大字数") + recommended: int = Field(..., description="推荐字数") + + +class StructurePreference(BaseModel): + """结构偏好""" + has_intro: bool = True + has_conclusion: bool = True + has_toc: bool = False + + +# ============================================================ +# 标题规则 Schema +# ============================================================ + +class TitleRule(BaseModel): + """标题规则""" + min_length: int = Field(..., description="最小长度") + max_length: int = Field(..., description="最大长度") + avoid_patterns: list[str] = Field(default_factory=list, description="避免的模式") + required_patterns: list[str] = Field(default_factory=list, description="需要的模式") + case_style: str = Field(default="normal", description="大小写样式") + + +# ============================================================ +# 标签规则 Schema +# ============================================================ + +class TagRule(BaseModel): + """标签规则""" + min_tags: int = Field(..., description="最少标签数") + max_tags: int = Field(..., description="最多标签数") + tag_style: str = Field(default="plain", description="标签样式: plain/hashtag/comma") + + +# ============================================================ +# AI 敏感度 Schema +# ============================================================ + +class AISensitivity(BaseModel): + """AI敏感度配置""" + detection_level: str = Field(..., description="检测级别: high/medium/low") + banned_patterns: list[str] = Field(default_factory=list, description="禁用模式") + banned_structures: list[str] = Field(default_factory=list, description="禁用结构") + safe_patterns: list[str] = Field(default_factory=list, description="安全模式") + humanization_required: bool = Field(default=False, description="是否需要去AI化") + + +# ============================================================ +# 敏感词规则 Schema +# ============================================================ + +class SensitiveWordsConfig(BaseModel): + """敏感词配置""" + check_required: bool = Field(default=True, description="是否需要检查") + categories: list[str] = Field(default_factory=list, description="敏感词分类") + max_tolerance: int = Field(default=0, description="容忍度") + auto_filter: bool = Field(default=True, description="自动过滤") + + +# ============================================================ +# SEO 规则 Schema +# ============================================================ + +class KeywordDensity(BaseModel): + """关键词密度""" + min: float = Field(..., description="最小百分比") + max: float = Field(..., description="最大百分比") + recommended: float = Field(..., description="推荐百分比") + + +class SEORule(BaseModel): + """SEO规则""" + keyword_density: KeywordDensity + keyword_position: list[str] = Field(default_factory=list, description="关键词位置") + internal_links: dict = Field(default_factory=dict, description="内链规则") + + +# ============================================================ +# GEO 规则 Schema +# ============================================================ + +class GEORule(BaseModel): + """GEO规则""" + citation_format: str = Field(default="plain", description="引用格式: plain/link/markdown/academic") + source_attribution: bool = Field(default=False, description="是否需要来源标注") + reference_style: str = Field(default="informal", description="引用风格: academic/informal") + + +# ============================================================ +# HTML 规则 Schema +# ============================================================ + +class HTMLRule(BaseModel): + """HTML规则""" + supported_tags: list[str] = Field(default_factory=list, description="支持的HTML标签") + banned_tags: list[str] = Field(default_factory=list, description="禁用的HTML标签") + image_support: bool = Field(default=True, description="是否支持图片") + video_support: bool = Field(default=False, description="是否支持视频") + code_block_support: bool = Field(default=False, description="是否支持代码块") + + +# ============================================================ +# 发布规则 Schema +# ============================================================ + +class PublishRule(BaseModel): + """发布规则""" + auto_publish: bool = Field(default=False, description="是否自动发布") + require_review: bool = Field(default=True, description="是否需要审核") + publish_timing: str = Field(default="immediate", description="发布时机: immediate/scheduled/draft") + + +# ============================================================ +# 完整平台规则 Schema +# ============================================================ + +class PlatformRuleSchema(BaseModel): + """完整平台规则""" + platform_name: str = Field(..., description="平台名称") + platform_type: str = Field(default="", description="平台类型") + priority: str = Field(default="P2", description="优先级: P0/P1/P2") + enabled: bool = Field(default=True, description="是否启用") + content_style: str = Field(default="", description="内容风格") + content_length: ContentLengthRule + structure_preference: StructurePreference + title_rules: TitleRule + tag_rules: TagRule + ai_sensitivity: AISensitivity + sensitive_words: SensitiveWordsConfig + seo_rules: SEORule + geo_rules: GEORule + html_rules: HTMLRule + publish_rules: PublishRule + + +# ============================================================ +# 简化的平台信息 Schema (用于列表展示) +# ============================================================ + +class PlatformBrief(BaseModel): + """简化的平台信息""" + id: str + name: str + platform_type: str = "" + priority: str = "P2" + enabled: bool = True + + +class PlatformListResponse(BaseModel): + """平台列表响应""" + platforms: list[PlatformBrief] + total: int + + +class PlatformDetailResponse(BaseModel): + """平台详情响应""" + id: str + name: str + platform_type: str = "" + priority: str = "P2" + enabled: bool = True + content_style: str = "" + content_length: ContentLengthRule + structure_preference: StructurePreference + title_rules: TitleRule + tag_rules: TagRule + ai_sensitivity: AISensitivity + sensitive_words: SensitiveWordsConfig + seo_rules: SEORule + geo_rules: GEORule + html_rules: HTMLRule + publish_rules: PublishRule + best_publish_times: list[str] = [] + best_publish_days: list[str] = [] + max_images: int = 0 + + +# ============================================================ +# 规则更新 Schema +# ============================================================ + +class PlatformRuleUpdateRequest(BaseModel): + """平台规则更新请求""" + content_style: Optional[str] = None + content_length: Optional[ContentLengthRule] = None + structure_preference: Optional[StructurePreference] = None + title_rules: Optional[TitleRule] = None + tag_rules: Optional[TagRule] = None + ai_sensitivity: Optional[AISensitivity] = None + sensitive_words: Optional[SensitiveWordsConfig] = None + seo_rules: Optional[SEORule] = None + geo_rules: Optional[GEORule] = None + html_rules: Optional[HTMLRule] = None + publish_rules: Optional[PublishRule] = None + enabled: Optional[bool] = None + + +class PlatformRuleUpdateResponse(BaseModel): + """平台规则更新响应""" + success: bool + platform_id: str + message: str + updated_at: datetime + + +# ============================================================ +# 规则变更历史 Schema +# ============================================================ + +class RuleChangeHistory(BaseModel): + """规则变更历史""" + id: int + platform_id: str + platform_name: str + changed_by: str + change_summary: str + change_type: str + previous_rules: Optional[dict] = None + new_rules: Optional[dict] = None + created_at: datetime + + +class RuleChangeHistoryResponse(BaseModel): + """规则变更历史响应""" + history: list[RuleChangeHistory] + total: int + + +# ============================================================ +# 规则验证 Schema +# ============================================================ + +class ContentValidationIssue(BaseModel): + """内容验证问题""" + severity: str = Field(..., description="严重程度: high/medium/low") + message: str = Field(..., description="问题描述") + category: str = Field(..., description="问题分类") + + +class ContentValidationResponse(BaseModel): + """内容验证响应""" + is_valid: bool + score: int = Field(..., ge=0, le=100, description="合规分数 0-100") + issues: list[ContentValidationIssue] + passed: list[str] + + +class ContentValidateRequest(BaseModel): + """内容验证请求""" + content: str = Field(..., description="待验证内容") + title: str = Field(..., description="标题") + platform: str = Field(..., description="平台ID") + + +# ============================================================ +# 规则差异对比 Schema +# ============================================================ + +class RuleDiff(BaseModel): + """规则差异""" + field: str + old_value: Optional[Any] = None + new_value: Optional[Any] = None + + +class RuleDiffResponse(BaseModel): + """规则差异响应""" + platform_id: str + platform_name: str + diffs: list[RuleDiff] + total_changes: int + + +# ============================================================ +# AI 去AI化请求 Schema +# ============================================================ + +class DeAIContentRequest(BaseModel): + """去AI化请求""" + content: str = Field(..., description="待处理内容") + platform: str = Field(..., description="目标平台") + style: Optional[str] = Field(default="自然流畅", description="目标风格") + + +class DeAIContentResponse(BaseModel): + """去AI化响应""" + original_content: str + processed_content: str + original_word_count: int + processed_word_count: int + detected_ai_patterns: list[str] = [] + replaced_patterns: dict[str, str] = {} diff --git a/backend/app/services/distribution/platform_rules.py b/backend/app/services/distribution/platform_rules.py index 85d020c..9a171c9 100644 --- a/backend/app/services/distribution/platform_rules.py +++ b/backend/app/services/distribution/platform_rules.py @@ -1,25 +1,185 @@ """平台规则引擎 - 各内容平台的规则和最佳实践""" import re +from typing import Optional + +# AI写作典型特征模式 +AI_PATTERNS = { + "banned_transitions": [ + "总之", "综上所述", "值得注意的是", "让我们", "总而言之", + "不可否认", "毋庸置疑", "首先", "其次", "最后", "最后但同样重要", + "换句话说", "也就是说", "更重要的是", "可以说", + ], + "banned_modifiers": [ + "至关重要", "不可或缺", "举足轻重", "蓬勃发展", "日新月异", + "深远影响", "全面提升", "显著成效", "重大突破", "核心要素", + ], + "banned_structures": [ + r"第一[,、].*第二[,、].*第三", # 对称三段式 + r"一方面[,、].*另一方面", # 一方面...另一方面 + ], + "safe_patterns": [ + "根据研究表明", "调研数据显示", "经验告诉我们", + "事实上", "说白了", "说实话", "说真的", + ], +} + +# 敏感词分类 +SENSITIVE_CATEGORIES = { + "politics": ["政治敏感词库"], + "medical": ["医疗敏感词库"], + "finance": ["金融敏感词库"], + "adult": ["低俗敏感词库"], +} + PLATFORM_RULES: dict[str, dict] = { + # ============================================================ + # P0 优先级平台 + # ============================================================ + "zhihu": { + "name": "知乎", + "platform_type": "内容社区", + "priority": "P0", + "enabled": True, + "content_style": "专业/深度/逻辑严谨", + "content_length": {"min": 500, "max": 50000, "recommended": 2000}, + "structure_preference": { + "has_intro": True, + "has_conclusion": True, + "has_toc": True, + }, + "title_rules": { + "min_length": 10, + "max_length": 30, + "avoid_patterns": ["emoji", "标题党", "过度营销"], + "required_patterns": ["关键词"], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 3, + "max_tags": 10, + "tag_style": "plain", + }, + "ai_sensitivity": { + "detection_level": "high", + "banned_patterns": AI_PATTERNS["banned_transitions"] + AI_PATTERNS["banned_modifiers"], + "banned_structures": AI_PATTERNS["banned_structures"], + "safe_patterns": AI_PATTERNS["safe_patterns"], + "humanization_required": True, + }, + "sensitive_words": { + "check_required": True, + "categories": ["politics", "medical", "finance", "adult"], + "max_tolerance": 0, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 1, "max": 3, "recommended": 2}, + "keyword_position": ["title", "first_para", "headings"], + "internal_links": {"min": 0, "max": 3}, + }, + "geo_rules": { + "citation_format": "academic", + "source_attribution": True, + "reference_style": "academic", + }, + "html_rules": { + "supported_tags": ["p", "h1", "h2", "h3", "h4", "ul", "ol", "blockquote", "code", "pre"], + "banned_tags": ["script", "iframe", "form", "style"], + "image_support": True, + "video_support": True, + "code_block_support": True, + }, + "publish_rules": { + "auto_publish": False, + "require_review": False, + "publish_timing": "immediate", + }, + "best_publish_times": ["09:00-10:00", "14:00-15:00", "20:00-22:00"], + "best_publish_days": ["周一", "周三", "周五"], + "max_images": 50, + "platform_rules": [ + "回答需针对问题本身", + "不得过度营销或硬广", + "引用需标注来源", + "不得使用AI水文(内容需有信息增量)", + "专业背书内容需有据可查", + ], + "seo_tips": [ + "回答开头直接给出结论", + "使用数据和案例支撑", + "合理设置二级标题", + "文末引导关注专栏", + ], + }, + "wechat": { "name": "微信公众号", - "icon": "wechat", - "max_title_length": 22, - "max_content_length": 20000, - "min_content_length": 300, - "supported_media": ["image", "video", "audio"], - "max_images": 20, + "platform_type": "内容平台", + "priority": "P0", + "enabled": True, + "content_style": "深度/品牌调性", + "content_length": {"min": 300, "max": 20000, "recommended": 1500}, + "structure_preference": { + "has_intro": True, + "has_conclusion": True, + "has_toc": False, + }, + "title_rules": { + "min_length": 5, + "max_length": 22, + "avoid_patterns": ["emoji", "连续特殊符号", "标题党"], + "required_patterns": [], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 1, + "max_tags": 3, + "tag_style": "plain", + }, + "ai_sensitivity": { + "detection_level": "medium", + "banned_patterns": [ + "首先", "其次", "最后", "总的来说", "综上所述", + "想必大家都有所了解", "相信大家都不陌生", + ], + "banned_structures": AI_PATTERNS["banned_structures"], + "safe_patterns": ["根据后台数据", "从运营角度来说", "结合我们的经验"], + "humanization_required": True, + }, + "sensitive_words": { + "check_required": True, + "categories": ["politics", "medical", "finance", "adult"], + "max_tolerance": 0, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 0.5, "max": 2, "recommended": 1}, + "keyword_position": ["title", "first_para"], + "internal_links": {"min": 0, "max": 5}, + }, + "geo_rules": { + "citation_format": "plain", + "source_attribution": True, + "reference_style": "informal", + }, + "html_rules": { + "supported_tags": ["p", "h1", "h2", "h3", "section", "blockquote", "img"], + "banned_tags": ["script", "iframe", "form", "a"], + "image_support": True, + "video_support": True, + "code_block_support": False, + }, + "publish_rules": { + "auto_publish": False, + "require_review": True, + "publish_timing": "scheduled", + }, "best_publish_times": ["07:30-08:30", "11:30-12:30", "17:30-18:30", "20:30-22:00"], "best_publish_days": ["周二", "周四", "周六"], - "format_features": { - "supports_markdown": False, - "supports_html": True, - "max_heading_level": 3, - "supports_code_block": True, - }, - "rules": [ + "max_images": 20, + "platform_rules": [ "不得使用诱导分享/关注语句", "图片单张不超过10MB", "标题不含连续特殊符号(如!!!)", @@ -33,83 +193,70 @@ PLATFORM_RULES: dict[str, dict] = { "摘要控制在120字内", ], }, - "zhihu": { - "name": "知乎", - "icon": "zhihu", - "max_title_length": 30, - "max_content_length": 50000, - "min_content_length": 500, - "supported_media": ["image", "video"], - "max_images": 50, - "best_publish_times": ["09:00-10:00", "14:00-15:00", "20:00-22:00"], - "best_publish_days": ["周一", "周三", "周五"], - "format_features": { - "supports_markdown": True, - "supports_html": False, - "max_heading_level": 4, - "supports_code_block": True, - }, - "rules": [ - "回答需针对问题本身", - "不得过度营销或硬广", - "引用需标注来源", - "不得使用AI水文(内容需有信息增量)", - ], - "seo_tips": [ - "回答开头直接给出结论", - "使用数据和案例支撑", - "合理设置二级标题", - "文末引导关注专栏", - ], - }, - "xiaohongshu": { - "name": "小红书", - "icon": "xiaohongshu", - "max_title_length": 20, - "max_content_length": 1000, - "min_content_length": 100, - "supported_media": ["image", "video"], - "max_images": 18, - "best_publish_times": ["07:00-09:00", "12:00-13:00", "18:00-20:00", "21:00-23:00"], - "best_publish_days": ["周末", "周三"], - "format_features": { - "supports_markdown": False, - "supports_html": False, - "max_heading_level": 0, - "supports_code_block": False, - "supports_emoji": True, - }, - "rules": [ - "首图质量决定点击率", - "正文控制在300-800字", - "话题标签3-8个", - "不得出现其他平台引流信息", - "图片不含水印", - ], - "seo_tips": [ - "标题含数字更吸引点击", - "正文用短句+emoji分段", - "话题标签放文末", - "首句即核心观点", - ], - }, + "baijiahao": { "name": "百家号", - "icon": "baidu", - "max_title_length": 30, - "max_content_length": 30000, - "min_content_length": 800, - "supported_media": ["image", "video"], - "max_images": 30, + "platform_type": "内容平台", + "priority": "P0", + "enabled": True, + "content_style": "资讯/SEO友好", + "content_length": {"min": 800, "max": 30000, "recommended": 1500}, + "structure_preference": { + "has_intro": True, + "has_conclusion": True, + "has_toc": False, + }, + "title_rules": { + "min_length": 10, + "max_length": 40, + "avoid_patterns": ["emoji", "标题党", "夸张词汇"], + "required_patterns": ["核心关键词"], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 3, + "max_tags": 5, + "tag_style": "plain", + }, + "ai_sensitivity": { + "detection_level": "high", + "banned_patterns": AI_PATTERNS["banned_transitions"] + AI_PATTERNS["banned_modifiers"], + "banned_structures": AI_PATTERNS["banned_structures"], + "safe_patterns": ["据悉", "从某处获悉", "数据显示"], + "humanization_required": True, + }, + "sensitive_words": { + "check_required": True, + "categories": ["politics", "medical", "finance", "adult"], + "max_tolerance": 0, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 1.5, "max": 4, "recommended": 2.5}, + "keyword_position": ["title", "first_para", "headings"], + "internal_links": {"min": 1, "max": 5}, + }, + "geo_rules": { + "citation_format": "link", + "source_attribution": True, + "reference_style": "informal", + }, + "html_rules": { + "supported_tags": ["p", "h1", "h2", "h3", "ul", "ol", "blockquote", "img"], + "banned_tags": ["script", "iframe", "form", "video"], + "image_support": True, + "video_support": False, + "code_block_support": False, + }, + "publish_rules": { + "auto_publish": True, + "require_review": False, + "publish_timing": "immediate", + }, "best_publish_times": ["08:00-09:00", "11:00-12:00", "17:00-18:00"], "best_publish_days": ["工作日"], - "format_features": { - "supports_markdown": False, - "supports_html": True, - "max_heading_level": 3, - "supports_code_block": False, - }, - "rules": [ + "max_images": 30, + "platform_rules": [ "原创内容优先推荐", "标题不含夸张/标题党词汇", "正文需含至少1张配图", @@ -122,53 +269,73 @@ PLATFORM_RULES: dict[str, dict] = { "发布后及时答复评论", ], }, - "douyin": { - "name": "抖音", - "icon": "douyin", - "max_title_length": 55, - "max_content_length": 5000, - "min_content_length": 0, - "supported_media": ["video", "image"], - "max_images": 35, - "best_publish_times": ["06:00-08:00", "12:00-13:00", "18:00-20:00", "22:00-23:00"], - "best_publish_days": ["每天"], - "format_features": { - "supports_markdown": False, - "supports_html": False, - "max_heading_level": 0, - "supports_code_block": False, - "supports_emoji": True, - }, - "rules": [ - "视频/图文需原创", - "不得含其他平台水印", - "话题标签2-5个", - "文案简短有吸引力", - ], - "seo_tips": [ - "前3秒决定完播率", - "标题含热点关键词", - "评论区互动提升权重", - "合适的话题+POI定位", - ], - }, + + # ============================================================ + # P1 优先级平台 + # ============================================================ "toutiao": { "name": "今日头条", - "icon": "toutiao", - "max_title_length": 30, - "max_content_length": 30000, - "min_content_length": 500, - "supported_media": ["image", "video"], - "max_images": 30, + "platform_type": "内容平台", + "priority": "P1", + "enabled": True, + "content_style": "资讯/简洁明了", + "content_length": {"min": 500, "max": 30000, "recommended": 1500}, + "structure_preference": { + "has_intro": True, + "has_conclusion": False, + "has_toc": False, + }, + "title_rules": { + "min_length": 10, + "max_length": 30, + "avoid_patterns": ["emoji", "标题党", "夸张"], + "required_patterns": ["悬念元素"], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 1, + "max_tags": 5, + "tag_style": "hashtag", + }, + "ai_sensitivity": { + "detection_level": "high", + "banned_patterns": AI_PATTERNS["banned_transitions"] + AI_PATTERNS["banned_modifiers"], + "banned_structures": AI_PATTERNS["banned_structures"], + "safe_patterns": ["据悉", "报道称", "记者了解到"], + "humanization_required": True, + }, + "sensitive_words": { + "check_required": True, + "categories": ["politics", "medical", "finance", "adult"], + "max_tolerance": 0, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 1, "max": 3, "recommended": 2}, + "keyword_position": ["title", "first_para"], + "internal_links": {"min": 0, "max": 3}, + }, + "geo_rules": { + "citation_format": "link", + "source_attribution": True, + "reference_style": "informal", + }, + "html_rules": { + "supported_tags": ["p", "h1", "h2", "h3", "ul", "ol", "blockquote", "img"], + "banned_tags": ["script", "iframe", "form", "video"], + "image_support": True, + "video_support": True, + "code_block_support": False, + }, + "publish_rules": { + "auto_publish": True, + "require_review": False, + "publish_timing": "immediate", + }, "best_publish_times": ["07:00-08:00", "12:00-13:00", "18:00-19:00", "21:00-22:00"], "best_publish_days": ["每天"], - "format_features": { - "supports_markdown": False, - "supports_html": True, - "max_heading_level": 3, - "supports_code_block": False, - }, - "rules": [ + "max_images": 30, + "platform_rules": [ "标题不得标题党", "内容需有信息价值", "首发原创优先推荐", @@ -181,6 +348,461 @@ PLATFORM_RULES: dict[str, dict] = { "发布频率稳定提升权重", ], }, + + "weibo": { + "name": "微博", + "platform_type": "社交媒体", + "priority": "P1", + "enabled": True, + "content_style": "轻松/即时/互动性强", + "content_length": {"min": 50, "max": 2000, "recommended": 280}, + "structure_preference": { + "has_intro": False, + "has_conclusion": False, + "has_toc": False, + }, + "title_rules": { + "min_length": 5, + "max_length": 50, + "avoid_patterns": ["长链接"], + "required_patterns": [], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 1, + "max_tags": 10, + "tag_style": "hashtag", + }, + "ai_sensitivity": { + "detection_level": "low", + "banned_patterns": ["首先", "其次", "最后", "总的来说"], + "banned_structures": [], + "safe_patterns": ["我觉得", "说实话", "求证中", "据说"], + "humanization_required": False, + }, + "sensitive_words": { + "check_required": True, + "categories": ["politics", "adult"], + "max_tolerance": 2, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 0.5, "max": 2, "recommended": 1}, + "keyword_position": ["title", "first_para"], + "internal_links": {"min": 0, "max": 2}, + }, + "geo_rules": { + "citation_format": "link", + "source_attribution": False, + "reference_style": "informal", + }, + "html_rules": { + "supported_tags": ["p", "br"], + "banned_tags": ["script", "iframe", "form"], + "image_support": True, + "video_support": True, + "code_block_support": False, + }, + "publish_rules": { + "auto_publish": True, + "require_review": False, + "publish_timing": "immediate", + }, + "best_publish_times": ["07:00-09:00", "12:00-13:00", "18:00-20:00", "21:00-23:00"], + "best_publish_days": ["每天"], + "max_images": 9, + "platform_rules": [ + "不得发布虚假信息", + "不得过度营销", + "话题标签有助于曝光", + "配图有助于转发", + ], + "seo_tips": [ + "热门话题可增加曝光", + "短句更易阅读", + "互动有助于上热门", + ], + }, + + "xiaohongshu": { + "name": "小红书", + "platform_type": "种草平台", + "priority": "P1", + "enabled": True, + "content_style": "种草/亲身体验/生活化", + "content_length": {"min": 100, "max": 1000, "recommended": 500}, + "structure_preference": { + "has_intro": True, + "has_conclusion": True, + "has_toc": False, + }, + "title_rules": { + "min_length": 5, + "max_length": 20, + "avoid_patterns": ["标题党"], + "required_patterns": ["emoji可用"], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 3, + "max_tags": 10, + "tag_style": "hashtag", + }, + "ai_sensitivity": { + "detection_level": "low", + "banned_patterns": ["首先", "其次", "最后", "综上所述"], + "banned_structures": [], + "safe_patterns": ["我用过", "亲测", "真实体验", "分享一下"], + "humanization_required": False, + }, + "sensitive_words": { + "check_required": True, + "categories": ["adult"], + "max_tolerance": 0, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 0.5, "max": 2, "recommended": 1}, + "keyword_position": ["title", "first_para"], + "internal_links": {"min": 0, "max": 0}, + }, + "geo_rules": { + "citation_format": "plain", + "source_attribution": False, + "reference_style": "informal", + }, + "html_rules": { + "supported_tags": ["p", "br"], + "banned_tags": ["script", "iframe", "form", "h1", "h2", "h3"], + "image_support": True, + "video_support": True, + "code_block_support": False, + }, + "publish_rules": { + "auto_publish": True, + "require_review": False, + "publish_timing": "immediate", + }, + "best_publish_times": ["07:00-09:00", "12:00-13:00", "18:00-20:00", "21:00-23:00"], + "best_publish_days": ["周末", "周三"], + "max_images": 18, + "platform_rules": [ + "首图质量决定点击率", + "正文控制在300-800字", + "话题标签3-10个", + "不得出现其他平台引流信息", + "图片不含水印", + ], + "seo_tips": [ + "标题含数字更吸引点击", + "正文用短句+emoji分段", + "话题标签放文末", + "首句即核心观点", + ], + }, + + # ============================================================ + # P2 优先级平台 + # ============================================================ + "bilibili": { + "name": "B站", + "platform_type": "视频/图文", + "priority": "P2", + "enabled": True, + "content_style": "年轻化/专业/趣味性", + "content_length": {"min": 200, "max": 5000, "recommended": 1000}, + "structure_preference": { + "has_intro": True, + "has_conclusion": True, + "has_toc": True, + }, + "title_rules": { + "min_length": 5, + "max_length": 80, + "avoid_patterns": ["标题党", "过度夸张"], + "required_patterns": [], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 1, + "max_tags": 10, + "tag_style": "hashtag", + }, + "ai_sensitivity": { + "detection_level": "medium", + "banned_patterns": ["首先", "其次", "最后", "总的来说", "综上所述"], + "banned_structures": [], + "safe_patterns": ["兄弟们", "家人们", "懂的都懂", "感谢一键三连"], + "humanization_required": True, + }, + "sensitive_words": { + "check_required": True, + "categories": ["politics", "adult"], + "max_tolerance": 0, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 0.5, "max": 2, "recommended": 1}, + "keyword_position": ["title"], + "internal_links": {"min": 0, "max": 0}, + }, + "geo_rules": { + "citation_format": "plain", + "source_attribution": True, + "reference_style": "informal", + }, + "html_rules": { + "supported_tags": ["p", "h1", "h2", "h3", "ul", "ol", "blockquote", "code", "pre"], + "banned_tags": ["script", "iframe", "form"], + "image_support": True, + "video_support": True, + "code_block_support": True, + }, + "publish_rules": { + "auto_publish": True, + "require_review": False, + "publish_timing": "immediate", + }, + "best_publish_times": ["16:00-18:00", "20:00-22:00"], + "best_publish_days": ["周五", "周六", "周日"], + "max_images": 50, + "platform_rules": [ + "稿件需要过审", + "不得搬运他人内容", + "封面和标题很重要", + "互动有助于推荐", + ], + "seo_tips": [ + "标题包含关键词", + "封面吸引人", + "标签有助于分类", + ], + }, + + "jianshu": { + "name": "简书", + "platform_type": "内容平台", + "priority": "P2", + "enabled": True, + "content_style": "文艺/真实/个人表达", + "content_length": {"min": 500, "max": 50000, "recommended": 2000}, + "structure_preference": { + "has_intro": True, + "has_conclusion": True, + "has_toc": False, + }, + "title_rules": { + "min_length": 5, + "max_length": 50, + "avoid_patterns": ["emoji", "标题党"], + "required_patterns": [], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 2, + "max_tags": 5, + "tag_style": "plain", + }, + "ai_sensitivity": { + "detection_level": "medium", + "banned_patterns": ["首先", "其次", "最后", "总的来说"], + "banned_structures": [], + "safe_patterns": ["我手写我心", "分享一下", "记录一下"], + "humanization_required": True, + }, + "sensitive_words": { + "check_required": True, + "categories": ["politics", "adult"], + "max_tolerance": 0, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 0.5, "max": 2, "recommended": 1}, + "keyword_position": ["title", "first_para"], + "internal_links": {"min": 0, "max": 3}, + }, + "geo_rules": { + "citation_format": "plain", + "source_attribution": True, + "reference_style": "informal", + }, + "html_rules": { + "supported_tags": ["p", "h1", "h2", "h3", "ul", "ol", "blockquote", "code"], + "banned_tags": ["script", "iframe", "form"], + "image_support": True, + "video_support": False, + "code_block_support": True, + }, + "publish_rules": { + "auto_publish": True, + "require_review": False, + "publish_timing": "immediate", + }, + "best_publish_times": ["08:00-10:00", "14:00-16:00", "20:00-22:00"], + "best_publish_days": ["每天"], + "max_images": 30, + "platform_rules": [ + "鼓励原创", + "文艺风格更受欢迎", + "配图有助于阅读", + ], + "seo_tips": [ + "标题包含关键词", + "合理使用专题", + "互动有助于曝光", + ], + }, + + "juejin": { + "name": "掘金", + "platform_type": "技术社区", + "priority": "P2", + "enabled": True, + "content_style": "技术/专业/深度", + "content_length": {"min": 500, "max": 50000, "recommended": 3000}, + "structure_preference": { + "has_intro": True, + "has_conclusion": True, + "has_toc": True, + }, + "title_rules": { + "min_length": 5, + "max_length": 50, + "avoid_patterns": ["emoji", "标题党"], + "required_patterns": ["技术关键词"], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 3, + "max_tags": 5, + "tag_style": "hashtag", + }, + "ai_sensitivity": { + "detection_level": "high", + "banned_patterns": AI_PATTERNS["banned_transitions"] + AI_PATTERNS["banned_modifiers"], + "banned_structures": AI_PATTERNS["banned_structures"], + "safe_patterns": ["项目中实际用到", "经过调研发现", "踩坑记录"], + "humanization_required": True, + }, + "sensitive_words": { + "check_required": True, + "categories": ["politics"], + "max_tolerance": 0, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 1, "max": 3, "recommended": 2}, + "keyword_position": ["title", "first_para", "headings"], + "internal_links": {"min": 0, "max": 3}, + }, + "geo_rules": { + "citation_format": "link", + "source_attribution": True, + "reference_style": "academic", + }, + "html_rules": { + "supported_tags": ["p", "h1", "h2", "h3", "h4", "ul", "ol", "blockquote", "code", "pre"], + "banned_tags": ["script", "iframe", "form"], + "image_support": True, + "video_support": False, + "code_block_support": True, + }, + "publish_rules": { + "auto_publish": False, + "require_review": True, + "publish_timing": "immediate", + }, + "best_publish_times": ["09:00-11:00", "14:00-16:00", "20:00-22:00"], + "best_publish_days": ["周二", "周四", "周六"], + "max_images": 30, + "platform_rules": [ + "技术内容优先", + "代码示例有助于理解", + "鼓励原创技术文章", + "禁止低质量搬运", + ], + "seo_tips": [ + "标题包含技术关键词", + "代码块有助于阅读", + "标签精准有助于推荐", + ], + }, + + "douyin": { + "name": "抖音", + "platform_type": "短视频平台", + "priority": "P1", + "enabled": True, + "content_style": "短平快/视觉冲击", + "content_length": {"min": 0, "max": 5000, "recommended": 0}, + "structure_preference": { + "has_intro": True, + "has_conclusion": False, + "has_toc": False, + }, + "title_rules": { + "min_length": 5, + "max_length": 55, + "avoid_patterns": ["标题党"], + "required_patterns": ["悬念"], + "case_style": "normal", + }, + "tag_rules": { + "min_tags": 2, + "max_tags": 5, + "tag_style": "hashtag", + }, + "ai_sensitivity": { + "detection_level": "low", + "banned_patterns": [], + "banned_structures": [], + "safe_patterns": [], + "humanization_required": False, + }, + "sensitive_words": { + "check_required": True, + "categories": ["politics", "adult"], + "max_tolerance": 0, + "auto_filter": True, + }, + "seo_rules": { + "keyword_density": {"min": 0.5, "max": 2, "recommended": 1}, + "keyword_position": ["title"], + "internal_links": {"min": 0, "max": 0}, + }, + "geo_rules": { + "citation_format": "plain", + "source_attribution": False, + "reference_style": "informal", + }, + "html_rules": { + "supported_tags": [], + "banned_tags": [], + "image_support": False, + "video_support": False, + "code_block_support": False, + }, + "publish_rules": { + "auto_publish": True, + "require_review": False, + "publish_timing": "immediate", + }, + "best_publish_times": ["06:00-08:00", "12:00-13:00", "18:00-20:00", "22:00-23:00"], + "best_publish_days": ["每天"], + "max_images": 35, + "platform_rules": [ + "视频/图文需原创", + "不得含其他平台水印", + "话题标签2-5个", + "文案简短有吸引力", + ], + "seo_tips": [ + "前3秒决定完播率", + "标题含热点关键词", + "评论区互动提升权重", + "合适的话题+POI定位", + ], + }, } @@ -212,26 +834,64 @@ _WATERMARK_PATTERNS = re.compile( class PlatformRuleEngine: """平台规则引擎""" - def get_platforms(self) -> list[dict]: - """获取所有支持平台列表""" - return [ - { + def get_platforms(self, enabled_only: bool = True) -> list[dict]: + """获取所有支持平台列表 + + Args: + enabled_only: 是否只返回启用的平台 + """ + platforms = [] + for key, val in PLATFORM_RULES.items(): + if enabled_only and not val.get("enabled", True): + continue + platforms.append({ "id": key, "name": val["name"], - "icon": val["icon"], - "max_title_length": val["max_title_length"], - "max_content_length": val["max_content_length"], - "min_content_length": val["min_content_length"], - "supported_media": val["supported_media"], - "max_images": val["max_images"], - } - for key, val in PLATFORM_RULES.items() - ] + "platform_type": val.get("platform_type", ""), + "priority": val.get("priority", "P2"), + "enabled": val.get("enabled", True), + "max_title_length": val["title_rules"]["max_length"], + "min_title_length": val["title_rules"]["min_length"], + "max_content_length": val["content_length"]["max"], + "min_content_length": val["content_length"]["min"], + "recommended_content_length": val["content_length"]["recommended"], + "supported_media": val.get("supported_tags", []), + "ai_sensitivity": val.get("ai_sensitivity", {}), + "best_publish_times": val.get("best_publish_times", []), + "best_publish_days": val.get("best_publish_days", []), + }) + return platforms def get_platform_rules(self, platform: str) -> dict | None: """获取指定平台的完整规则""" return PLATFORM_RULES.get(platform) + def get_platform_rule(self, platform: str, rule_category: str) -> dict | None: + """获取指定平台特定类别的规则 + + Args: + platform: 平台标识 + rule_category: 规则类别 (title_rules, tag_rules, ai_sensitivity, etc.) + """ + rules = PLATFORM_RULES.get(platform) + if rules is None: + return None + return rules.get(rule_category) + + def get_all_rule_categories(self) -> list[str]: + """获取所有规则类别""" + return [ + "content_length", + "title_rules", + "tag_rules", + "ai_sensitivity", + "sensitive_words", + "seo_rules", + "geo_rules", + "html_rules", + "publish_rules", + ] + def validate_content(self, content: str, title: str, platform: str) -> dict: """ 校验内容是否符合平台规则 @@ -239,7 +899,7 @@ class PlatformRuleEngine: 返回: { "is_valid": bool, "score": int (0-100), - "issues": [{"severity": "high|medium|low", "message": "..."}], + "issues": [{"severity": "high|medium|low", "message": "...", "category": "..."}], "passed": ["规则1", "规则2"] } """ @@ -248,7 +908,7 @@ class PlatformRuleEngine: return { "is_valid": False, "score": 0, - "issues": [{"severity": "high", "message": f"不支持的平台: {platform}"}], + "issues": [{"severity": "high", "message": f"不支持的平台: {platform}", "category": "platform"}], "passed": [], } @@ -257,32 +917,59 @@ class PlatformRuleEngine: # --- 标题长度 --- title_len = len(title) - max_title = rules["max_title_length"] + title_rules = rules.get("title_rules", {}) + max_title = title_rules.get("max_length", 30) + min_title = title_rules.get("min_length", 5) + if title_len > max_title: issues.append({ "severity": "high", "message": f"标题长度 {title_len} 超过限制 {max_title}", + "category": "title_length", + }) + elif title_len < min_title: + issues.append({ + "severity": "medium", + "message": f"标题长度 {title_len} 低于最低要求 {min_title}", + "category": "title_length", }) else: passed.append(f"标题长度合规({title_len}/{max_title})") # --- 内容长度 --- content_len = len(content) - max_content = rules["max_content_length"] - min_content = rules["min_content_length"] + content_rules = rules.get("content_length", {}) + max_content = content_rules.get("max", 20000) + min_content = content_rules.get("min", 0) + recommended_content = content_rules.get("recommended", 0) + if content_len > max_content: issues.append({ "severity": "high", "message": f"内容长度 {content_len} 超过限制 {max_content}", + "category": "content_length", }) elif min_content > 0 and content_len < min_content: issues.append({ "severity": "medium", "message": f"内容长度 {content_len} 低于建议最低 {min_content}", + "category": "content_length", }) else: passed.append(f"内容长度合规({content_len}/{max_content})") + # --- 标签规则 --- + tag_rules = rules.get("tag_rules", {}) + min_tags = tag_rules.get("min_tags", 0) + max_tags = tag_rules.get("max_tags", 10) + # TODO: 需要传入标签数据进行验证 + + # --- AI敏感度检测 --- + ai_sensitivity = rules.get("ai_sensitivity", {}) + if ai_sensitivity.get("humanization_required", False): + ai_issues = self._check_ai_patterns(content, ai_sensitivity) + issues.extend(ai_issues) + # --- 平台特有规则 --- platform_specific = self._validate_platform_specific(content, title, platform) issues.extend(platform_specific["issues"]) @@ -307,6 +994,34 @@ class PlatformRuleEngine: "passed": passed, } + def _check_ai_patterns(self, content: str, ai_sensitivity: dict) -> list[dict]: + """检测AI写作模式""" + issues: list[dict] = [] + + banned_patterns = ai_sensitivity.get("banned_patterns", []) + banned_structures = ai_sensitivity.get("banned_structures", []) + + # 检测禁用词汇 + found_banned = [p for p in banned_patterns if p in content] + if found_banned: + issues.append({ + "severity": "medium", + "message": f"发现AI写作特征词: {', '.join(found_banned[:3])}", + "category": "ai_pattern", + }) + + # 检测禁用结构 + for pattern in banned_structures: + if re.search(pattern, content): + issues.append({ + "severity": "medium", + "message": "发现AI典型对称结构", + "category": "ai_pattern", + }) + break + + return issues + def _validate_platform_specific( self, content: str, title: str, platform: str ) -> dict: @@ -320,6 +1035,7 @@ class PlatformRuleEngine: issues.append({ "severity": "high", "message": "包含诱导分享/关注语句", + "category": "platform_rule", }) else: passed.append("无诱导分享/关注语句") @@ -329,6 +1045,7 @@ class PlatformRuleEngine: issues.append({ "severity": "medium", "message": "标题包含连续特殊符号", + "category": "title_format", }) else: passed.append("标题无连续特殊符号") @@ -338,6 +1055,7 @@ class PlatformRuleEngine: issues.append({ "severity": "high", "message": "正文包含外部链接(仅支持公众号链接和小程序)", + "category": "platform_rule", }) else: passed.append("无外部链接") @@ -350,6 +1068,7 @@ class PlatformRuleEngine: issues.append({ "severity": "medium", "message": f"疑似营销用语: {', '.join(found_marketing)}", + "category": "platform_rule", }) else: passed.append("未检测到过度营销用语") @@ -361,11 +1080,13 @@ class PlatformRuleEngine: issues.append({ "severity": "medium", "message": f"正文建议300-800字,当前 {content_len} 字", + "category": "content_length", }) elif content_len < 300: issues.append({ "severity": "low", "message": f"正文建议300-800字,当前仅 {content_len} 字", + "category": "content_length", }) else: passed.append(f"正文字数适宜({content_len}字)") @@ -377,6 +1098,7 @@ class PlatformRuleEngine: issues.append({ "severity": "high", "message": f"疑似其他平台引流: {', '.join(found_cross)}", + "category": "platform_rule", }) else: passed.append("未检测到其他平台引流信息") @@ -388,6 +1110,7 @@ class PlatformRuleEngine: issues.append({ "severity": "high", "message": f"标题含标题党词汇: {', '.join(found_clickbait)}", + "category": "title_content", }) else: passed.append("标题无标题党词汇") @@ -398,6 +1121,7 @@ class PlatformRuleEngine: issues.append({ "severity": "high", "message": "内容包含其他平台水印信息", + "category": "platform_rule", }) else: passed.append("未检测到其他平台水印") @@ -410,3 +1134,35 @@ class PlatformRuleEngine: if rules is None: return [] return rules.get("seo_tips", []) + + def get_ai_humanization_config(self, platform: str) -> dict | None: + """获取平台去AI化配置""" + rules = PLATFORM_RULES.get(platform) + if rules is None: + return None + return rules.get("ai_sensitivity", {}) + + def get_sensitive_words_config(self, platform: str) -> dict | None: + """获取平台敏感词配置""" + rules = PLATFORM_RULES.get(platform) + if rules is None: + return None + return rules.get("sensitive_words", {}) + + def get_seo_config(self, platform: str) -> dict | None: + """获取平台SEO配置""" + rules = PLATFORM_RULES.get(platform) + if rules is None: + return None + return rules.get("seo_rules", {}) + + def get_html_config(self, platform: str) -> dict | None: + """获取平台HTML规则配置""" + rules = PLATFORM_RULES.get(platform) + if rules is None: + return None + return rules.get("html_rules", {}) + + +# 导出单例 +rule_engine = PlatformRuleEngine() diff --git a/backend/app/services/distribution/rule_service.py b/backend/app/services/distribution/rule_service.py new file mode 100644 index 0000000..09837ee --- /dev/null +++ b/backend/app/services/distribution/rule_service.py @@ -0,0 +1,207 @@ +"""平台规则管理服务 - 提供规则 CRUD 和内容验证功能""" + +import logging +import json +import re +from datetime import datetime +from typing import Optional, Any + +from app.services.distribution.platform_rules import ( + PLATFORM_RULES, + PlatformRuleEngine, + rule_engine as default_rule_engine, + AI_PATTERNS, +) + +logger = logging.getLogger(__name__) + + +class PlatformRuleService: + """平台规则管理服务""" + + def __init__(self): + self._engine = default_rule_engine + + def get_all_platforms(self, enabled_only: bool = True) -> list[dict]: + """获取所有平台列表 + + Args: + enabled_only: 是否只返回启用的平台 + + Returns: + 平台列表 + """ + return self._engine.get_platforms(enabled_only=enabled_only) + + def get_platform_detail(self, platform_id: str) -> Optional[dict]: + """获取平台详情 + + Args: + platform_id: 平台标识 + + Returns: + 平台完整规则,如果不存在返回 None + """ + return self._engine.get_platform_rules(platform_id) + + def get_rule_category( + self, platform_id: str, rule_category: str + ) -> Optional[dict]: + """获取特定规则类别 + + Args: + platform_id: 平台标识 + rule_category: 规则类别 + + Returns: + 规则配置 + """ + return self._engine.get_platform_rule(platform_id, rule_category) + + def validate_content( + self, content: str, title: str, platform_id: str + ) -> dict: + """验证内容是否符合平台规则 + + Args: + content: 内容正文 + title: 标题 + platform_id: 平台标识 + + Returns: + 验证结果 + """ + return self._engine.validate_content(content, title, platform_id) + + def detect_ai_patterns(self, content: str, platform_id: str) -> list[dict]: + """检测内容中的AI写作模式 + + Args: + content: 待检测内容 + platform_id: 平台标识 + + Returns: + 检测到的AI模式列表 + """ + ai_config = self._engine.get_ai_humanization_config(platform_id) + if not ai_config: + return [] + + detected = [] + banned_patterns = ai_config.get("banned_patterns", []) + + for pattern in banned_patterns: + if pattern in content: + detected.append({ + "pattern": pattern, + "type": "banned_word", + "severity": "high", + }) + + # 检测禁用结构 + banned_structures = ai_config.get("banned_structures", []) + for structure in banned_structures: + if re.search(structure, content): + detected.append({ + "pattern": structure, + "type": "banned_structure", + "severity": "medium", + }) + + return detected + + def get_ai_humanization_config(self, platform_id: str) -> Optional[dict]: + """获取平台去AI化配置""" + return self._engine.get_ai_humanization_config(platform_id) + + def get_sensitive_words_config(self, platform_id: str) -> Optional[dict]: + """获取平台敏感词配置""" + return self._engine.get_sensitive_words_config(platform_id) + + def get_seo_config(self, platform_id: str) -> Optional[dict]: + """获取平台SEO配置""" + return self._engine.get_seo_config(platform_id) + + def get_html_config(self, platform_id: str) -> Optional[dict]: + """获取平台HTML配置""" + return self._engine.get_html_config(platform_id) + + def get_optimization_tips(self, platform_id: str) -> list[str]: + """获取平台优化建议""" + return self._engine.get_optimization_tips(platform_id) + + def compare_rules( + self, platform_id: str, old_rules: dict, new_rules: dict + ) -> list[dict]: + """对比规则变更 + + Args: + platform_id: 平台标识 + old_rules: 旧规则 + new_rules: 新规则 + + Returns: + 差异列表 + """ + diffs = [] + + def flatten_dict(d: dict, parent_key: str = "") -> dict: + """将嵌套字典展平为点分隔的键值对""" + items = [] + for k, v in d.items(): + new_key = f"{parent_key}.{k}" if parent_key else k + if isinstance(v, dict): + items.extend(flatten_dict(v, new_key).items()) + else: + items.append((new_key, v)) + return dict(items) + + old_flat = flatten_dict(old_rules) + new_flat = flatten_dict(new_rules) + + all_keys = set(old_flat.keys()) | set(new_flat.keys()) + + for key in all_keys: + old_val = old_flat.get(key) + new_val = new_flat.get(key) + if old_val != new_val: + diffs.append({ + "field": key, + "old_value": old_val, + "new_value": new_val, + }) + + return diffs + + def format_content_for_platform( + self, content: str, platform_id: str, target_format: str = "html" + ) -> str: + """将内容格式化为平台特定格式 + + Args: + content: 原始内容 + platform_id: 目标平台 + target_format: 目标格式 (html/markdown/plain) + + Returns: + 格式化后的内容 + """ + rules = self._engine.get_platform_rules(platform_id) + if not rules: + return content + + html_config = rules.get("html_rules", {}) + supported_tags = html_config.get("supported_tags", []) + banned_tags = html_config.get("banned_tags", []) + + # 移除禁用的标签 + formatted = content + for tag in banned_tags: + formatted = re.sub(f"<{tag}[^>]*>.*?", "", formatted, flags=re.DOTALL) + formatted = re.sub(f"<{tag}[^>]*/?>", "", formatted) + + return formatted + + +# 导出单例 +platform_rule_service = PlatformRuleService() diff --git a/frontend/app/(dashboard)/dashboard/content/editor/page.tsx b/frontend/app/(dashboard)/dashboard/content/editor/page.tsx index cd29264..fbc28ae 100644 --- a/frontend/app/(dashboard)/dashboard/content/editor/page.tsx +++ b/frontend/app/(dashboard)/dashboard/content/editor/page.tsx @@ -1,20 +1,329 @@ "use client"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useState, useEffect } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { platformRulesApi, PlatformBrief, ContentValidationResponse } from "@/lib/api/platform-rules"; + +interface OptimizedContent { + title: string; + content: string; + platform: string; + tips: string[]; +} export default function ContentEditorPage() { + const [platforms, setPlatforms] = useState([]); + const [selectedPlatform, setSelectedPlatform] = useState("zhihu"); + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [optimizedContent, setOptimizedContent] = useState(null); + const [validationResult, setValidationResult] = useState(null); + const [loading, setLoading] = useState(true); + const [validating, setValidating] = useState(false); + const [optimizing, setOptimizing] = useState(false); + + useEffect(() => { + loadPlatforms(); + }, []); + + const loadPlatforms = async () => { + try { + setLoading(true); + const response = await platformRulesApi.listPlatforms(); + setPlatforms(response.platforms); + } catch (error) { + console.error("加载平台列表失败:", error); + } finally { + setLoading(false); + } + }; + + const handleValidate = async () => { + if (!content || !title) return; + + try { + setValidating(true); + const result = await platformRulesApi.validateContent(selectedPlatform, content, title); + setValidationResult(result); + } catch (error) { + console.error("验证失败:", error); + } finally { + setValidating(false); + } + }; + + const handleOptimize = async () => { + if (!content || !title) return; + + try { + setOptimizing(true); + // 获取平台配置 + const platformDetail = await platformRulesApi.getPlatformDetail(selectedPlatform); + const tips = await platformRulesApi.getOptimizationTips(selectedPlatform); + + // 模拟优化处理(实际应调用后端API) + setOptimizedContent({ + title: title, + content: content, + platform: selectedPlatform, + tips: tips.tips || [], + }); + } catch (error) { + console.error("优化失败:", error); + } finally { + setOptimizing(false); + } + }; + + const handleCopyContent = (format: "html" | "markdown" | "text") => { + if (!optimizedContent) return; + + let copyText = ""; + switch (format) { + case "html": + // 简单的HTML格式化 + copyText = `

${optimizedContent.title}

\n

${optimizedContent.content.replace(/\n\n/g, "

")}

`; + break; + case "markdown": + copyText = `# ${optimizedContent.title}\n\n${optimizedContent.content}`; + break; + case "text": + copyText = `${optimizedContent.title}\n\n${optimizedContent.content}`; + break; + } + + navigator.clipboard.writeText(copyText); + }; + + if (loading) { + return ( +
+
加载中...
+
+ ); + } + return (
-

内容编辑

-

创建和编辑GEO优化内容

+

内容编辑器

+

创建和优化GEO内容

+ +
+ {/* 左侧: 编辑区 */} + + +
+
+ 内容编辑 + 编写和编辑内容 +
+ +
+
+ +
+ + setTitle(e.target.value)} + /> +
+ +
+ +