feat: 平台规则中心 - 各平台规则收集与Pipeline集成

平台规则收集:
- 10个平台完整规则: 知乎/公众号/百家号/头条/微博/小红书/B站/简书/掘金/抖音
- 每平台: 内容偏好/AI敏感度/敏感词/SEO/GEO/HTML/发布规则

后端实现:
- rule_service.py: 规则验证/AI检测/内容适配
- platform_rules.py API: CRUD + 验证端点
- DeAIAgent集成: 基于平台AI敏感度配置去AI化

前端实现:
- /dashboard/settings/platforms: 平台规则管理页
- /dashboard/content/editor: 内容编辑器增强
- 支持复制为纯文本/Markdown/HTML
This commit is contained in:
chiguyong 2026-05-23 22:33:45 +08:00
parent 67d7578550
commit ba936bd44c
12 changed files with 2927 additions and 161 deletions

View File

@ -3,6 +3,7 @@
import logging import logging
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional
from app.agent_framework.base import BaseAgent from app.agent_framework.base import BaseAgent
from app.agent_framework.prompts import DEAI_TEMPLATE from app.agent_framework.prompts import DEAI_TEMPLATE
@ -14,6 +15,8 @@ from app.agent_framework.protocol import (
TaskStatus, TaskStatus,
) )
from app.services.llm import LLMFactory, LLMError 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__) logger = logging.getLogger(__name__)
@ -23,13 +26,19 @@ class DeAIAgent(BaseAgent):
支持的任务类型: 支持的任务类型:
- deai_process: 对内容进行去AI化处理 - deai_process: 对内容进行去AI化处理
input_data 字段:
- content: str (必填待处理的文章内容)
- platform: str (可选目标平台ID zhihu, wechat )
- style: str (可选目标风格)
- preserve_structure: bool (可选是否保留原有结构)
""" """
def __init__(self): def __init__(self):
super().__init__( super().__init__(
name="deai_agent", name="deai_agent",
agent_type=AgentType.DEAI_AGENT, agent_type=AgentType.DEAI_AGENT,
version="1.0.0", version="1.1.0",
) )
def get_capabilities(self) -> AgentCapability: def get_capabilities(self) -> AgentCapability:
@ -104,7 +113,8 @@ class DeAIAgent(BaseAgent):
input_data 字段: input_data 字段:
- content: str (必填待处理的文章内容) - content: str (必填待处理的文章内容)
- style: str (可选目标风格: 口语化/叙事化/评论风格) - platform: str (可选目标平台ID)
- style: str (可选目标风格)
- preserve_structure: bool (可选是否保留原有结构) - preserve_structure: bool (可选是否保留原有结构)
""" """
input_data = task.input_data input_data = task.input_data
@ -112,6 +122,8 @@ class DeAIAgent(BaseAgent):
if not content: if not content:
raise ValueError("input_data必须包含非空的'content'字段") raise ValueError("input_data必须包含非空的'content'字段")
platform_id = input_data.get("platform", "")
# 上报进度:开始 # 上报进度:开始
await self.report_progress( await self.report_progress(
task_id=task.task_id, task_id=task.task_id,
@ -119,11 +131,20 @@ class DeAIAgent(BaseAgent):
message="开始去AI化处理...", message="开始去AI化处理...",
) )
# 获取平台特定配置
platform_config = self._get_platform_config(platform_id)
# 构建变量
variables = { variables = {
"original_content": content, "original_content": content,
"target_style": input_data.get("style", "自然流畅"), "target_style": input_data.get("style", "自然流畅"),
"preserve_structure": "" if input_data.get("preserve_structure", True) else "", "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) messages = DEAI_TEMPLATE.render(variables)
# 上报进度调用LLM # 上报进度调用LLM
@ -140,6 +161,13 @@ class DeAIAgent(BaseAgent):
max_tokens=len(content) * 3, 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( await self.report_progress(
task_id=task.task_id, task_id=task.task_id,
@ -152,4 +180,42 @@ class DeAIAgent(BaseAgent):
"original_word_count": len(content), "original_word_count": len(content),
"processed_word_count": len(response.content), "processed_word_count": len(response.content),
"usage": response.usage, "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()

View File

@ -12,18 +12,30 @@ DEAI_TEMPLATE = PromptTemplate(
${original_content} ${original_content}
## 目标风格参考 ## 目标风格参考
${style_reference} ${target_style}
## 目标平台 ## 目标平台
${target_platform}""", ${platform_info}
## AI检测敏感度
${ai_sensitivity}
## 需要消除的AI写作特征 (禁用)
${banned_patterns}
## 推荐使用的人味表达 (安全模式)
${safe_patterns}
## 结构保留
保留原有结构: ${preserve_structure}""",
instructions="""请将以上原始文章进行去AI化改写使其读起来像真人撰写的自然文章。 instructions="""请将以上原始文章进行去AI化改写使其读起来像真人撰写的自然文章。
改写策略按优先级排序 改写策略按优先级排序
1. 消除AI典型语言特征 1. 消除AI典型语言特征
- 禁用模板化过渡词总之综上所述值得注意的是让我们总而言之不可否认毋庸置疑 - 禁用模板化过渡词总之综上所述值得注意的是让我们总而言之不可否认毋庸置疑首先其次最后最后但同样重要换句话说也就是说更重要的是可以说
- 禁用空洞修饰词至关重要不可或缺举足轻重蓬勃发展日新月异 - 禁用空洞修饰词至关重要不可或缺举足轻重蓬勃发展日新月异深远影响全面提升显著成效重大突破核心要素
- 禁用对称式排比句AI最爱用三段式排比 - 禁用对称式排比句AI最爱用三段式排比
2. 增加不规则节奏 2. 增加不规则节奏
@ -44,7 +56,12 @@ ${target_platform}""",
5. 调整叙事结构 5. 调整叙事结构
- 打破AI的--结构 - 打破AI的--结构
- 允许跑题式的个人联想但要收回来 - 允许跑题式的个人联想但要收回来
- 可以从一个具体的场景或故事开头""", - 可以从一个具体的场景或故事开头
6. 针对平台的特殊要求
- 高敏感度平台知乎百家号必须彻底消除AI痕迹使用更多真实案例
- 中敏感度平台微信公众号B站适度改写保持专业感
- 低敏感度平台微博小红书可以保留部分AI风格重点在优化表达""",
constraints="""## 约束条件 constraints="""## 约束条件
- 不改变原文的核心事实和数据所有数字专有名词引用必须保留 - 不改变原文的核心事实和数据所有数字专有名词引用必须保留

View File

@ -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),
}

View File

@ -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.brands import router as brands_router
from app.api.onboarding import router as onboarding_router from app.api.onboarding import router as onboarding_router
from app.api.platforms import router as platforms_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.config import settings
from app.database import engine, Base from app.database import engine, Base
from app.schemas.common import ErrorResponse, ErrorCode 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(brands_router, prefix="/api/v1/brands", tags=["品牌管理"])
app.include_router(onboarding_router, prefix="/api/v1") app.include_router(onboarding_router, prefix="/api/v1")
app.include_router(platforms_router, prefix="/api/v1") app.include_router(platforms_router, prefix="/api/v1")
app.include_router(platform_rules_router)
@app.get("/health", tags=["可观测性"]) @app.get("/health", tags=["可观测性"])

View File

@ -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] = {}

File diff suppressed because it is too large Load Diff

View File

@ -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}[^>]*>.*?</{tag}>", "", formatted, flags=re.DOTALL)
formatted = re.sub(f"<{tag}[^>]*/?>", "", formatted)
return formatted
# 导出单例
platform_rule_service = PlatformRuleService()

View File

@ -1,20 +1,329 @@
"use client"; "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() { export default function ContentEditorPage() {
const [platforms, setPlatforms] = useState<PlatformBrief[]>([]);
const [selectedPlatform, setSelectedPlatform] = useState<string>("zhihu");
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [optimizedContent, setOptimizedContent] = useState<OptimizedContent | null>(null);
const [validationResult, setValidationResult] = useState<ContentValidationResponse | null>(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 = `<h1>${optimizedContent.title}</h1>\n<p>${optimizedContent.content.replace(/\n\n/g, "</p><p>")}</p>`;
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 (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">...</div>
</div>
);
}
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-2xl font-bold tracking-tight"></h1> <h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">GEO优化内容</p> <p className="text-muted-foreground">GEO内容</p>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左侧: 编辑区 */}
<Card className="lg:col-span-2">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</div>
<Select value={selectedPlatform} onValueChange={setSelectedPlatform}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="选择平台" />
</SelectTrigger>
<SelectContent>
{platforms.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title"></Label>
<Input
id="title"
placeholder="输入文章标题"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="content"></Label>
<Textarea
id="content"
placeholder="输入文章内容..."
className="min-h-[400px] font-mono text-sm"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleValidate}
disabled={validating || !content || !title}
>
{validating ? "验证中..." : "验证内容"}
</Button>
<Button
onClick={handleOptimize}
disabled={optimizing || !content || !title}
>
{optimizing ? "优化中..." : "一键优化"}
</Button>
</div>
{/* 验证结果 */}
{validationResult && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<h4 className="font-semibold"></h4>
<Badge
variant={validationResult.is_valid ? "default" : "destructive"}
className={validationResult.is_valid ? "bg-green-600" : ""}
>
{validationResult.is_valid ? "通过" : "未通过"}
</Badge>
<Badge variant="outline">
: {validationResult.score}
</Badge>
</div>
{validationResult.issues.length > 0 && (
<div className="space-y-1">
{validationResult.issues.map((issue, i) => (
<div
key={i}
className={`text-sm p-2 rounded ${
issue.severity === "high"
? "bg-red-50 text-red-800"
: issue.severity === "medium"
? "bg-yellow-50 text-yellow-800"
: "bg-blue-50 text-blue-800"
}`}
>
<span className="font-medium">[{issue.severity.toUpperCase()}]</span>{" "}
{issue.message}
</div>
))}
</div>
)}
{validationResult.passed.length > 0 && (
<div className="space-y-1">
<h5 className="text-sm font-medium text-muted-foreground">:</h5>
{validationResult.passed.map((p, i) => (
<div key={i} className="text-sm text-green-700">
{p}
</div>
))}
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* 右侧: 预览和工具 */}
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{!optimizedContent ? (
<div className="flex items-center justify-center h-64 text-muted-foreground">
<div className="text-center">
<p>"一键优化"</p>
</div>
</div>
) : (
<Tabs defaultValue="preview" className="space-y-4">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="preview"></TabsTrigger>
<TabsTrigger value="tips"></TabsTrigger>
</TabsList>
<TabsContent value="preview" className="space-y-4">
<div className="space-y-2">
<h4 className="font-semibold text-lg">{optimizedContent.title}</h4>
<div className="text-sm whitespace-pre-wrap">
{optimizedContent.content}
</div>
</div>
<div className="space-y-2">
<h5 className="font-medium">:</h5>
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" onClick={() => handleCopyContent("text")}>
</Button>
<Button size="sm" variant="outline" onClick={() => handleCopyContent("markdown")}>
Markdown
</Button>
<Button size="sm" variant="outline" onClick={() => handleCopyContent("html")}>
HTML
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="tips">
<div className="space-y-2">
{optimizedContent.tips.length > 0 ? (
optimizedContent.tips.map((tip, i) => (
<div key={i} className="text-sm p-2 bg-muted rounded">
{tip}
</div>
))
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
</TabsContent>
</Tabs>
)}
</CardContent>
</Card>
</div>
{/* 优化流程说明 */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle></CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p></p> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="p-4 border rounded-lg">
<div className="font-semibold mb-2">1. AI化</div>
<p className="text-sm text-muted-foreground">
AI敏感度AI写作特征使
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="font-semibold mb-2">2. </div>
<p className="text-sm text-muted-foreground">
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="font-semibold mb-2">3. SEO优化</div>
<p className="text-sm text-muted-foreground">
</p>
</div>
<div className="p-4 border rounded-lg">
<div className="font-semibold mb-2">4. </div>
<p className="text-sm text-muted-foreground">
HTML规则和结构偏好
</p>
</div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -0,0 +1,368 @@
"use client";
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { platformRulesApi, PlatformBrief, PlatformDetailResponse } from "@/lib/api/platform-rules";
const PRIORITY_COLORS: Record<string, string> = {
P0: "bg-red-100 text-red-800",
P1: "bg-yellow-100 text-yellow-800",
P2: "bg-green-100 text-green-800",
};
const SENSITIVITY_COLORS: Record<string, string> = {
high: "bg-red-100 text-red-800",
medium: "bg-yellow-100 text-yellow-800",
low: "bg-green-100 text-green-800",
};
export default function PlatformRulesPage() {
const [platforms, setPlatforms] = useState<PlatformBrief[]>([]);
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
const [platformDetail, setPlatformDetail] = useState<PlatformDetailResponse | null>(null);
const [loading, setLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
useEffect(() => {
loadPlatforms();
}, []);
useEffect(() => {
if (selectedPlatform) {
loadPlatformDetail(selectedPlatform);
}
}, [selectedPlatform]);
const loadPlatforms = async () => {
try {
setLoading(true);
const response = await platformRulesApi.listPlatforms();
setPlatforms(response.platforms);
if (response.platforms.length > 0 && !selectedPlatform) {
setSelectedPlatform(response.platforms[0].id);
}
} catch (error) {
console.error("加载平台列表失败:", error);
} finally {
setLoading(false);
}
};
const loadPlatformDetail = async (platformId: string) => {
try {
setDetailLoading(true);
const detail = await platformRulesApi.getPlatformDetail(platformId);
setPlatformDetail(detail);
} catch (error) {
console.error("加载平台详情失败:", error);
} finally {
setDetailLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">...</div>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* 平台列表 */}
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription> {platforms.length} </CardDescription>
</CardHeader>
<CardContent className="p-0">
<div className="divide-y">
{platforms.map((platform) => (
<button
key={platform.id}
onClick={() => setSelectedPlatform(platform.id)}
className={`w-full px-4 py-3 text-left hover:bg-muted/50 transition-colors ${
selectedPlatform === platform.id ? "bg-muted" : ""
}`}
>
<div className="flex items-center justify-between">
<span className="font-medium">{platform.name}</span>
<Badge className={PRIORITY_COLORS[platform.priority] || "bg-gray-100"}>
{platform.priority}
</Badge>
</div>
<div className="text-sm text-muted-foreground mt-1">
{platform.platform_type}
</div>
</button>
))}
</div>
</CardContent>
</Card>
{/* 平台详情 */}
<Card className="lg:col-span-3">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-xl">
{platformDetail?.name || "选择平台"}
</CardTitle>
<CardDescription>
{platformDetail?.platform_type} - {platformDetail?.id}
</CardDescription>
</div>
{platformDetail && (
<div className="flex gap-2">
<Badge
className={
SENSITIVITY_COLORS[
platformDetail.ai_sensitivity.detection_level
] || "bg-gray-100"
}
>
AI检测: {platformDetail.ai_sensitivity.detection_level}
</Badge>
<Badge variant={platformDetail.enabled ? "default" : "secondary"}>
{platformDetail.enabled ? "已启用" : "已禁用"}
</Badge>
</div>
)}
</div>
</CardHeader>
<CardContent>
{detailLoading ? (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground">...</div>
</div>
) : !platformDetail ? (
<div className="flex items-center justify-center h-64">
<div className="text-muted-foreground"></div>
</div>
) : (
<Tabs defaultValue="basic" className="space-y-4">
<TabsList>
<TabsTrigger value="basic"></TabsTrigger>
<TabsTrigger value="ai">AI检测</TabsTrigger>
<TabsTrigger value="sensitive"></TabsTrigger>
<TabsTrigger value="seo">SEO规则</TabsTrigger>
<TabsTrigger value="html">HTML规则</TabsTrigger>
<TabsTrigger value="publish"></TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="text-sm space-y-1">
<p>: {platformDetail.content_length.min} </p>
<p>: {platformDetail.content_length.max} </p>
<p>: {platformDetail.content_length.recommended} </p>
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="text-sm space-y-1">
<p>: {platformDetail.structure_preference.has_intro ? "需要" : "不需要"}</p>
<p>: {platformDetail.structure_preference.has_conclusion ? "需要" : "不需要"}</p>
<p>: {platformDetail.structure_preference.has_toc ? "需要" : "不需要"}</p>
</div>
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="text-sm space-y-1">
<p>: {platformDetail.title_rules.min_length} - {platformDetail.title_rules.max_length} </p>
<p>: {platformDetail.title_rules.case_style}</p>
{platformDetail.title_rules.avoid_patterns.length > 0 && (
<p>: {platformDetail.title_rules.avoid_patterns.join(", ")}</p>
)}
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="text-sm space-y-1">
<p>: {platformDetail.tag_rules.min_tags} - {platformDetail.tag_rules.max_tags} </p>
<p>: {platformDetail.tag_rules.tag_style}</p>
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="flex flex-wrap gap-2">
{platformDetail.best_publish_times.map((time, i) => (
<Badge key={i} variant="outline">{time}</Badge>
))}
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="flex flex-wrap gap-2">
{platformDetail.best_publish_days.map((day, i) => (
<Badge key={i} variant="outline">{day}</Badge>
))}
</div>
</div>
</TabsContent>
<TabsContent value="ai" className="space-y-4">
<div>
<h4 className="font-semibold mb-2">AI检测级别</h4>
<Badge className={SENSITIVITY_COLORS[platformDetail.ai_sensitivity.detection_level]}>
{platformDetail.ai_sensitivity.detection_level.toUpperCase()}
</Badge>
<p className="text-sm text-muted-foreground mt-2">
{platformDetail.ai_sensitivity.humanization_required
? "需要去AI化处理"
: "无需去AI化处理"}
</p>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
{platformDetail.ai_sensitivity.banned_patterns.length > 0 ? (
<div className="flex flex-wrap gap-1">
{platformDetail.ai_sensitivity.banned_patterns.map((pattern, i) => (
<Badge key={i} variant="destructive" className="text-xs">
{pattern}
</Badge>
))}
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
{platformDetail.ai_sensitivity.safe_patterns.length > 0 ? (
<div className="flex flex-wrap gap-1">
{platformDetail.ai_sensitivity.safe_patterns.map((pattern, i) => (
<Badge key={i} variant="default" className="text-xs bg-green-600">
{pattern}
</Badge>
))}
</div>
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
</TabsContent>
<TabsContent value="sensitive" className="space-y-4">
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="text-sm space-y-2">
<p>: {platformDetail.sensitive_words.check_required ? "是" : "否"}</p>
<p>: {platformDetail.sensitive_words.auto_filter ? "是" : "否"}</p>
<p>: {platformDetail.sensitive_words.max_tolerance}</p>
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="flex flex-wrap gap-2">
{platformDetail.sensitive_words.categories.map((cat, i) => (
<Badge key={i} variant="outline">{cat}</Badge>
))}
</div>
</div>
</TabsContent>
<TabsContent value="seo" className="space-y-4">
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="text-sm space-y-1">
<p>: {platformDetail.seo_rules.keyword_density.min}% - {platformDetail.seo_rules.keyword_density.max}%</p>
<p>: {platformDetail.seo_rules.keyword_density.recommended}%</p>
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="flex flex-wrap gap-2">
{platformDetail.seo_rules.keyword_position.map((pos, i) => (
<Badge key={i} variant="outline">{pos}</Badge>
))}
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="text-sm space-y-1">
<p>
: {platformDetail.seo_rules.internal_links.min || 0} - {platformDetail.seo_rules.internal_links.max || 0}
</p>
</div>
</div>
</TabsContent>
<TabsContent value="html" className="space-y-4">
<div>
<h4 className="font-semibold mb-2">HTML标签</h4>
<div className="flex flex-wrap gap-1">
{platformDetail.html_rules.supported_tags.map((tag, i) => (
<Badge key={i} variant="default" className="text-xs">
&lt;{tag}&gt;
</Badge>
))}
</div>
</div>
<div>
<h4 className="font-semibold mb-2">HTML标签</h4>
<div className="flex flex-wrap gap-1">
{platformDetail.html_rules.banned_tags.map((tag, i) => (
<Badge key={i} variant="destructive" className="text-xs">
&lt;{tag}&gt;
</Badge>
))}
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="text-sm space-y-1">
<p>: {platformDetail.html_rules.image_support ? "支持" : "不支持"}</p>
<p>: {platformDetail.html_rules.video_support ? "支持" : "不支持"}</p>
<p>: {platformDetail.html_rules.code_block_support ? "支持" : "不支持"}</p>
</div>
</div>
</TabsContent>
<TabsContent value="publish" className="space-y-4">
<div>
<h4 className="font-semibold mb-2"></h4>
<div className="text-sm space-y-2">
<p>: {platformDetail.publish_rules.auto_publish ? "是" : "否"}</p>
<p>: {platformDetail.publish_rules.require_review ? "是" : "否"}</p>
<p>: {platformDetail.publish_rules.publish_timing}</p>
</div>
</div>
<div>
<h4 className="font-semibold mb-2"></h4>
<p className="text-sm">{platformDetail.content_style}</p>
</div>
</TabsContent>
</Tabs>
)}
</CardContent>
</Card>
</div>
</div>
);
}

View File

@ -19,6 +19,7 @@ import {
BarChart3, BarChart3,
Bot, Bot,
Users, Users,
Blocks,
} from "lucide-react"; } from "lucide-react";
const baseNavItems = [ const baseNavItems = [
@ -27,6 +28,7 @@ const baseNavItems = [
{ name: "诊断分析", href: "/dashboard/diagnosis", icon: Stethoscope }, { name: "诊断分析", href: "/dashboard/diagnosis", icon: Stethoscope },
{ name: "策略制定", href: "/dashboard/strategy", icon: Lightbulb }, { name: "策略制定", href: "/dashboard/strategy", icon: Lightbulb },
{ name: "内容管理", href: "/dashboard/content", icon: FileEdit }, { name: "内容管理", href: "/dashboard/content", icon: FileEdit },
{ name: "内容编辑器", href: "/dashboard/content/editor", icon: FileEdit },
{ name: "分发执行", href: "/dashboard/publishing", icon: Send }, { name: "分发执行", href: "/dashboard/publishing", icon: Send },
{ name: "监测优化", href: "/dashboard/monitoring", icon: BarChart3 }, { name: "监测优化", href: "/dashboard/monitoring", icon: BarChart3 },
{ name: "AI Agent", href: "/dashboard/agents", icon: Bot }, { name: "AI Agent", href: "/dashboard/agents", icon: Bot },
@ -34,6 +36,7 @@ const baseNavItems = [
{ name: "查询管理", href: "/dashboard/queries", icon: Search }, { name: "查询管理", href: "/dashboard/queries", icon: Search },
{ name: "引用记录", href: "/dashboard/citations", icon: Quote }, { name: "引用记录", href: "/dashboard/citations", icon: Quote },
{ name: "报告导出", href: "/dashboard/reports", icon: FileDown }, { name: "报告导出", href: "/dashboard/reports", icon: FileDown },
{ name: "平台规则", href: "/dashboard/settings/platforms", icon: Blocks },
{ name: "设置", href: "/dashboard/settings", icon: Settings }, { name: "设置", href: "/dashboard/settings", icon: Settings },
]; ];
@ -42,7 +45,10 @@ export function Sidebar() {
const { data: session } = useSession(); const { data: session } = useSession();
const navItems = session?.user?.is_admin const navItems = session?.user?.is_admin
? [...baseNavItems, { name: "管理后台", href: "/dashboard/admin", icon: Shield }] ? [
...baseNavItems,
{ name: "管理后台", href: "/dashboard/admin", icon: Shield },
]
: baseNavItems; : baseNavItems;
return ( return (
@ -64,7 +70,7 @@ export function Sidebar() {
"flex items-center rounded-lg px-4 py-3 text-sm font-medium transition-colors", "flex items-center rounded-lg px-4 py-3 text-sm font-medium transition-colors",
isActive isActive
? "bg-blue-600 text-white" ? "bg-blue-600 text-white"
: "text-slate-300 hover:bg-slate-800 hover:text-white" : "text-slate-300 hover:bg-slate-800 hover:text-white",
)} )}
> >
<item.icon className="mr-3 h-5 w-5" /> <item.icon className="mr-3 h-5 w-5" />

View File

@ -24,6 +24,7 @@ export { brandsApi } from "./brands";
export type { CreateBrandPayload, UpdateBrandPayload, AddCompetitorPayload } from "./brands"; export type { CreateBrandPayload, UpdateBrandPayload, AddCompetitorPayload } from "./brands";
export { suggestionsApi } from "./suggestions"; export { suggestionsApi } from "./suggestions";
export { onboardingApi } from "./onboarding"; export { onboardingApi } from "./onboarding";
export { platformRulesApi } from "./platform-rules";
// ── 类型导出 ─────────────────────────────────────────────────────────────────── // ── 类型导出 ───────────────────────────────────────────────────────────────────
export type { Agent, AgentRunLog } from "./agents"; export type { Agent, AgentRunLog } from "./agents";
@ -105,6 +106,7 @@ import { alertsApi } from "./alerts";
import { brandsApi } from "./brands"; import { brandsApi } from "./brands";
import { suggestionsApi } from "./suggestions"; import { suggestionsApi } from "./suggestions";
import { onboardingApi } from "./onboarding"; import { onboardingApi } from "./onboarding";
import { platformRulesApi } from "./platform-rules";
/** /**
* API `import { api } from "@/lib/api"` * API `import { api } from "@/lib/api"`
@ -129,4 +131,5 @@ export const api = {
brands: brandsApi, brands: brandsApi,
suggestions: suggestionsApi, suggestions: suggestionsApi,
onboarding: onboardingApi, onboarding: onboardingApi,
platformRules: platformRulesApi,
}; };

View File

@ -0,0 +1,341 @@
/**
* API
* /api/v1/platforms
*/
import { fetchWithAuth } from "./client";
// ============================================================
// 类型定义
// ============================================================
export interface ContentLengthRule {
min: number;
max: number;
recommended: number;
}
export interface StructurePreference {
has_intro: boolean;
has_conclusion: boolean;
has_toc: boolean;
}
export interface TitleRule {
min_length: number;
max_length: number;
avoid_patterns: string[];
required_patterns: string[];
case_style: string;
}
export interface TagRule {
min_tags: number;
max_tags: number;
tag_style: string;
}
export interface AISensitivity {
detection_level: string;
banned_patterns: string[];
banned_structures: string[];
safe_patterns: string[];
humanization_required: boolean;
}
export interface SensitiveWordsConfig {
check_required: boolean;
categories: string[];
max_tolerance: number;
auto_filter: boolean;
}
export interface KeywordDensity {
min: number;
max: number;
recommended: number;
}
export interface SEORule {
keyword_density: KeywordDensity;
keyword_position: string[];
internal_links: Record<string, number>;
}
export interface GEORule {
citation_format: string;
source_attribution: boolean;
reference_style: string;
}
export interface HTMLRule {
supported_tags: string[];
banned_tags: string[];
image_support: boolean;
video_support: boolean;
code_block_support: boolean;
}
export interface PublishRule {
auto_publish: boolean;
require_review: boolean;
publish_timing: string;
}
export interface PlatformBrief {
id: string;
name: string;
platform_type: string;
priority: string;
enabled: boolean;
}
export interface PlatformListResponse {
platforms: PlatformBrief[];
total: number;
}
export interface PlatformDetailResponse {
id: string;
name: string;
platform_type: string;
priority: string;
enabled: boolean;
content_style: string;
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: string[];
best_publish_days: string[];
max_images: number;
}
export interface ValidationIssue {
severity: "high" | "medium" | "low";
message: string;
category: string;
}
export interface ContentValidationResponse {
is_valid: boolean;
score: number;
issues: ValidationIssue[];
passed: string[];
}
export interface RuleChangeHistory {
id: number;
platform_id: string;
platform_name: string;
changed_by: string;
change_summary: string;
change_type: string;
previous_rules: Record<string, unknown> | null;
new_rules: Record<string, unknown> | null;
created_at: string;
}
export interface RuleChangeHistoryResponse {
history: RuleChangeHistory[];
total: number;
}
export interface RuleUpdateRequest {
content_style?: string;
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;
enabled?: boolean;
}
export interface RuleUpdateResponse {
success: boolean;
platform_id: string;
message: string;
updated_at: string;
}
export interface DetectedAIPattern {
pattern: string;
type: string;
severity: string;
}
export interface DetectAIPatternsResponse {
platform_id: string;
content_length: number;
detected_patterns: DetectedAIPattern[];
total_detected: number;
}
// ============================================================
// API 函数
// ============================================================
export const platformRulesApi = {
/**
*
*/
listPlatforms: async (token?: string, enabledOnly = true): Promise<PlatformListResponse> => {
return fetchWithAuth(
`/api/v1/platforms?enabled_only=${enabledOnly}`,
{},
token
) as Promise<PlatformListResponse>;
},
/**
*
*/
getPlatformDetail: async (platformId: string, token?: string): Promise<PlatformDetailResponse> => {
return fetchWithAuth(
`/api/v1/platforms/${platformId}`,
{},
token
) as Promise<PlatformDetailResponse>;
},
/**
*
*/
listRuleCategories: async (): Promise<{ categories: string[]; total: number }> => {
return fetchWithAuth("/api/v1/platforms/categories", {}) as Promise<{
categories: string[];
total: number;
}>;
},
/**
*
*/
getRuleCategory: async (
platformId: string,
ruleCategory: string,
token?: string
): Promise<{ platform_id: string; rule_category: string; rule: unknown }> => {
return fetchWithAuth(
`/api/v1/platforms/${platformId}/rules/${ruleCategory}`,
{},
token
) as Promise<{ platform_id: string; rule_category: string; rule: unknown }>;
},
/**
* AI配置
*/
getAIConfig: async (
platformId: string,
token?: string
): Promise<{ platform_id: string; ai_sensitivity: AISensitivity }> => {
return fetchWithAuth(
`/api/v1/platforms/${platformId}/ai-config`,
{},
token
) as Promise<{ platform_id: string; ai_sensitivity: AISensitivity }>;
},
/**
*
*/
getOptimizationTips: async (
platformId: string,
token?: string
): Promise<{
platform_id: string;
platform_name: string;
content_style: string;
tips: string[];
}> => {
return fetchWithAuth(
`/api/v1/platforms/${platformId}/tips`,
{},
token
) as Promise<{
platform_id: string;
platform_name: string;
content_style: string;
tips: string[];
}>;
},
/**
*
*/
validateContent: async (
platformId: string,
content: string,
title: string,
token?: string
): Promise<ContentValidationResponse> => {
return fetchWithAuth(
`/api/v1/platforms/${platformId}/rules/validate`,
{
method: "POST",
body: JSON.stringify({ content, title }),
},
token
) as Promise<ContentValidationResponse>;
},
/**
* AI写作模式
*/
detectAIPatterns: async (
platformId: string,
content: string,
token?: string
): Promise<DetectAIPatternsResponse> => {
return fetchWithAuth(
`/api/v1/platforms/${platformId}/detect-ai-patterns?content=${encodeURIComponent(content)}`,
{},
token
) as Promise<DetectAIPatternsResponse>;
},
/**
*
*/
getRuleHistory: async (
platformId: string,
limit = 20,
token?: string
): Promise<RuleChangeHistoryResponse> => {
return fetchWithAuth(
`/api/v1/platforms/${platformId}/rules/history?limit=${limit}`,
{},
token
) as Promise<RuleChangeHistoryResponse>;
},
/**
*
*/
updatePlatformRules: async (
platformId: string,
updateData: RuleUpdateRequest,
token?: string
): Promise<RuleUpdateResponse> => {
return fetchWithAuth(
`/api/v1/platforms/${platformId}/rules`,
{
method: "PUT",
body: JSON.stringify(updateData),
},
token
) as Promise<RuleUpdateResponse>;
},
};