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:
parent
67d7578550
commit
ba936bd44c
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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="""## 约束条件
|
||||
- 不改变原文的核心事实和数据,所有数字、专有名词、引用必须保留
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
@ -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=["可观测性"])
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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()
|
||||
|
|
@ -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<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 (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">内容编辑</h1>
|
||||
<p className="text-muted-foreground">创建和编辑GEO优化内容</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">内容编辑器</h1>
|
||||
<p className="text-muted-foreground">创建和优化GEO内容</p>
|
||||
</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>
|
||||
<CardHeader>
|
||||
<CardTitle>功能开发中</CardTitle>
|
||||
<CardTitle>优化流程</CardTitle>
|
||||
</CardHeader>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<{tag}>
|
||||
</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">
|
||||
<{tag}>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
BarChart3,
|
||||
Bot,
|
||||
Users,
|
||||
Blocks,
|
||||
} from "lucide-react";
|
||||
|
||||
const baseNavItems = [
|
||||
|
|
@ -27,6 +28,7 @@ const baseNavItems = [
|
|||
{ name: "诊断分析", href: "/dashboard/diagnosis", icon: Stethoscope },
|
||||
{ name: "策略制定", href: "/dashboard/strategy", icon: Lightbulb },
|
||||
{ name: "内容管理", href: "/dashboard/content", icon: FileEdit },
|
||||
{ name: "内容编辑器", href: "/dashboard/content/editor", icon: FileEdit },
|
||||
{ name: "分发执行", href: "/dashboard/publishing", icon: Send },
|
||||
{ name: "监测优化", href: "/dashboard/monitoring", icon: BarChart3 },
|
||||
{ name: "AI Agent", href: "/dashboard/agents", icon: Bot },
|
||||
|
|
@ -34,6 +36,7 @@ const baseNavItems = [
|
|||
{ name: "查询管理", href: "/dashboard/queries", icon: Search },
|
||||
{ name: "引用记录", href: "/dashboard/citations", icon: Quote },
|
||||
{ name: "报告导出", href: "/dashboard/reports", icon: FileDown },
|
||||
{ name: "平台规则", href: "/dashboard/settings/platforms", icon: Blocks },
|
||||
{ name: "设置", href: "/dashboard/settings", icon: Settings },
|
||||
];
|
||||
|
||||
|
|
@ -42,7 +45,10 @@ export function Sidebar() {
|
|||
const { data: session } = useSession();
|
||||
|
||||
const navItems = session?.user?.is_admin
|
||||
? [...baseNavItems, { name: "管理后台", href: "/dashboard/admin", icon: Shield }]
|
||||
? [
|
||||
...baseNavItems,
|
||||
{ name: "管理后台", href: "/dashboard/admin", icon: Shield },
|
||||
]
|
||||
: baseNavItems;
|
||||
|
||||
return (
|
||||
|
|
@ -64,7 +70,7 @@ export function Sidebar() {
|
|||
"flex items-center rounded-lg px-4 py-3 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "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" />
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export { brandsApi } from "./brands";
|
|||
export type { CreateBrandPayload, UpdateBrandPayload, AddCompetitorPayload } from "./brands";
|
||||
export { suggestionsApi } from "./suggestions";
|
||||
export { onboardingApi } from "./onboarding";
|
||||
export { platformRulesApi } from "./platform-rules";
|
||||
|
||||
// ── 类型导出 ───────────────────────────────────────────────────────────────────
|
||||
export type { Agent, AgentRunLog } from "./agents";
|
||||
|
|
@ -105,6 +106,7 @@ import { alertsApi } from "./alerts";
|
|||
import { brandsApi } from "./brands";
|
||||
import { suggestionsApi } from "./suggestions";
|
||||
import { onboardingApi } from "./onboarding";
|
||||
import { platformRulesApi } from "./platform-rules";
|
||||
|
||||
/**
|
||||
* 聚合 API 对象,保持与原 `import { api } from "@/lib/api"` 的向后兼容。
|
||||
|
|
@ -129,4 +131,5 @@ export const api = {
|
|||
brands: brandsApi,
|
||||
suggestions: suggestionsApi,
|
||||
onboarding: onboardingApi,
|
||||
platformRules: platformRulesApi,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
},
|
||||
};
|
||||
Loading…
Reference in New Issue