feat: AI 审核自动驳回 + 功效词可配置 + UI 修复
- AI 自动驳回:法规/品牌安全 HIGH 违规或总分<40 自动打回上传阶段 - 功效词可配置:从硬编码改为品牌方在规则页面自行管理 - 驳回通知:AI 驳回时只通知达人,含具体原因 - 达人端:脚本/视频页面展示 AI 驳回原因 + 重新上传入口 - 规则页面:新增"功效词"分类 - 种子数据:新增 6 条默认功效词 - 其他:代理商管理下拉修复、AI 配置模型列表扩展、视觉模型标签修正、规则编辑放开限制 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0ef7650c09
commit
0b3dfa3c52
@@ -38,8 +38,8 @@ router = APIRouter(prefix="/scripts", tags=["scripts"])
|
||||
# 内置违禁词库(广告极限词)
|
||||
ABSOLUTE_WORDS = ["最好", "第一", "最佳", "绝对", "100%"]
|
||||
|
||||
# 功效词库(医疗/功效宣称)
|
||||
EFFICACY_WORDS = ["根治", "治愈", "治疗", "药效", "疗效", "特效"]
|
||||
# 默认功效词库(品牌方未配置时的兜底)
|
||||
DEFAULT_EFFICACY_WORDS = ["根治", "治愈", "治疗", "药效", "疗效", "特效"]
|
||||
|
||||
# 广告语境关键词(用于判断是否为广告场景)
|
||||
AD_CONTEXT_KEYWORDS = ["产品", "购买", "销量", "品质", "推荐", "价格", "优惠", "促销"]
|
||||
@@ -282,7 +282,12 @@ async def review_script(
|
||||
|
||||
# 获取品牌方配置的所有规则数据
|
||||
whitelist = await get_whitelist_for_brand(x_tenant_id, request.brand_id, db)
|
||||
tenant_forbidden_words = await get_forbidden_words_for_tenant(x_tenant_id, db)
|
||||
all_tenant_words = await get_forbidden_words_for_tenant(x_tenant_id, db)
|
||||
# 分离功效词和普通违禁词
|
||||
efficacy_words = [w["word"] for w in all_tenant_words if w.get("category") == "功效词"]
|
||||
if not efficacy_words:
|
||||
efficacy_words = list(DEFAULT_EFFICACY_WORDS)
|
||||
tenant_forbidden_words = [w for w in all_tenant_words if w.get("category") != "功效词"]
|
||||
competitors = await get_competitors_for_brand(x_tenant_id, request.brand_id, db)
|
||||
db_platform_rules = await get_active_platform_rules(
|
||||
x_tenant_id, request.brand_id, request.platform.value, db,
|
||||
@@ -310,8 +315,8 @@ async def review_script(
|
||||
))
|
||||
start = pos + 1
|
||||
|
||||
# 1b. 功效词检测
|
||||
for word in EFFICACY_WORDS:
|
||||
# 1b. 功效词检测(从品牌方配置加载,未配置则用默认列表)
|
||||
for word in efficacy_words:
|
||||
if word in whitelist:
|
||||
continue
|
||||
start = 0
|
||||
@@ -372,7 +377,7 @@ async def review_script(
|
||||
start = pos + 1
|
||||
|
||||
# ===== Step 2: 平台规则检测 (platform) =====
|
||||
already_checked = set(ABSOLUTE_WORDS + [w["word"] for w in tenant_forbidden_words])
|
||||
already_checked = set(ABSOLUTE_WORDS + efficacy_words + [w["word"] for w in tenant_forbidden_words])
|
||||
platform_forbidden_words: list[str] = []
|
||||
platform_restricted_words: list[dict] = []
|
||||
platform_content_requirements: list[str] = []
|
||||
|
||||
+142
-88
@@ -132,53 +132,80 @@ async def _run_script_ai_review(task_id: str, tenant_id: str):
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"任务 {task_id} AI 审核完成,得分: {result.score}")
|
||||
ai_auto_rejected = task.script_ai_result and task.script_ai_result.get("ai_auto_rejected")
|
||||
logger.info(f"任务 {task_id} AI 审核完成,得分: {result.score},自动驳回: {ai_auto_rejected}")
|
||||
|
||||
# SSE 通知达人和代理商
|
||||
try:
|
||||
user_ids = []
|
||||
creator_result = await db.execute(
|
||||
select(Creator).where(Creator.id == task.creator_id)
|
||||
)
|
||||
creator_obj = creator_result.scalar_one_or_none()
|
||||
if creator_obj:
|
||||
user_ids.append(creator_obj.user_id)
|
||||
|
||||
agency_result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
agency_obj = agency_result.scalar_one_or_none()
|
||||
if agency_obj:
|
||||
user_ids.append(agency_obj.user_id)
|
||||
|
||||
if user_ids:
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=user_ids,
|
||||
data={"action": "ai_review_completed", "stage": task.stage.value, "score": result.score},
|
||||
if ai_auto_rejected:
|
||||
# AI 自动驳回:只通知达人
|
||||
try:
|
||||
creator_result = await db.execute(
|
||||
select(Creator).where(Creator.id == task.creator_id)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 创建消息通知代理商
|
||||
try:
|
||||
ag_result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
ag_obj = ag_result.scalar_one_or_none()
|
||||
if ag_obj:
|
||||
await create_message(
|
||||
db=db,
|
||||
user_id=ag_obj.user_id,
|
||||
type="task",
|
||||
title="脚本 AI 审核完成",
|
||||
content=f"任务「{task.name}」AI 审核完成,综合得分 {result.score} 分,请审核。",
|
||||
related_task_id=task.id,
|
||||
sender_name="系统",
|
||||
creator_obj = creator_result.scalar_one_or_none()
|
||||
if creator_obj:
|
||||
reject_reason = task.script_ai_result.get("ai_reject_reason", "")
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=[creator_obj.user_id],
|
||||
data={"action": "ai_auto_rejected", "stage": task.stage.value, "score": result.score},
|
||||
)
|
||||
await create_message(
|
||||
db=db,
|
||||
user_id=creator_obj.user_id,
|
||||
type="task",
|
||||
title="脚本未通过 AI 审核",
|
||||
content=f"任务「{task.name}」未通过 AI 审核({result.score} 分),原因:{reject_reason}。请修改后重新上传。",
|
||||
related_task_id=task.id,
|
||||
sender_name="系统",
|
||||
)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
# 正常通过:SSE 通知达人和代理商 + 消息通知代理商
|
||||
try:
|
||||
user_ids = []
|
||||
creator_result = await db.execute(
|
||||
select(Creator).where(Creator.id == task.creator_id)
|
||||
)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
creator_obj = creator_result.scalar_one_or_none()
|
||||
if creator_obj:
|
||||
user_ids.append(creator_obj.user_id)
|
||||
|
||||
agency_result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
agency_obj = agency_result.scalar_one_or_none()
|
||||
if agency_obj:
|
||||
user_ids.append(agency_obj.user_id)
|
||||
|
||||
if user_ids:
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=user_ids,
|
||||
data={"action": "ai_review_completed", "stage": task.stage.value, "score": result.score},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
ag_result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
ag_obj = ag_result.scalar_one_or_none()
|
||||
if ag_obj:
|
||||
await create_message(
|
||||
db=db,
|
||||
user_id=ag_obj.user_id,
|
||||
type="task",
|
||||
title="脚本 AI 审核完成",
|
||||
content=f"任务「{task.name}」AI 审核完成,综合得分 {result.score} 分,请审核。",
|
||||
related_task_id=task.id,
|
||||
sender_name="系统",
|
||||
)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# AI 未配置时通知品牌方
|
||||
if not result.ai_available:
|
||||
@@ -307,53 +334,80 @@ async def _run_video_ai_review(task_id: str, tenant_id: str):
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"任务 {task_id} 视频 AI 审核完成,得分: {video_score}")
|
||||
ai_auto_rejected = task.video_ai_result and task.video_ai_result.get("ai_auto_rejected")
|
||||
logger.info(f"任务 {task_id} 视频 AI 审核完成,得分: {video_score},自动驳回: {ai_auto_rejected}")
|
||||
|
||||
# SSE 通知
|
||||
try:
|
||||
user_ids = []
|
||||
creator_result = await db.execute(
|
||||
select(Creator).where(Creator.id == task.creator_id)
|
||||
)
|
||||
creator_obj = creator_result.scalar_one_or_none()
|
||||
if creator_obj:
|
||||
user_ids.append(creator_obj.user_id)
|
||||
|
||||
agency_result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
agency_obj = agency_result.scalar_one_or_none()
|
||||
if agency_obj:
|
||||
user_ids.append(agency_obj.user_id)
|
||||
|
||||
if user_ids:
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=user_ids,
|
||||
data={"action": "ai_review_completed", "stage": task.stage.value, "score": video_score},
|
||||
if ai_auto_rejected:
|
||||
# AI 自动驳回:只通知达人
|
||||
try:
|
||||
creator_result = await db.execute(
|
||||
select(Creator).where(Creator.id == task.creator_id)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 创建消息通知代理商
|
||||
try:
|
||||
ag_result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
ag_obj = ag_result.scalar_one_or_none()
|
||||
if ag_obj:
|
||||
await create_message(
|
||||
db=db,
|
||||
user_id=ag_obj.user_id,
|
||||
type="task",
|
||||
title="视频 AI 审核完成",
|
||||
content=f"任务「{task.name}」视频 AI 审核完成,得分 {video_score} 分,请审核。",
|
||||
related_task_id=task.id,
|
||||
sender_name="系统",
|
||||
creator_obj = creator_result.scalar_one_or_none()
|
||||
if creator_obj:
|
||||
reject_reason = task.video_ai_result.get("ai_reject_reason", "")
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=[creator_obj.user_id],
|
||||
data={"action": "ai_auto_rejected", "stage": task.stage.value, "score": video_score},
|
||||
)
|
||||
await create_message(
|
||||
db=db,
|
||||
user_id=creator_obj.user_id,
|
||||
type="task",
|
||||
title="视频未通过 AI 审核",
|
||||
content=f"任务「{task.name}」视频未通过 AI 审核({video_score} 分),原因:{reject_reason}。请修改后重新上传。",
|
||||
related_task_id=task.id,
|
||||
sender_name="系统",
|
||||
)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
# 正常通过:SSE 通知达人和代理商 + 消息通知代理商
|
||||
try:
|
||||
user_ids = []
|
||||
creator_result = await db.execute(
|
||||
select(Creator).where(Creator.id == task.creator_id)
|
||||
)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
creator_obj = creator_result.scalar_one_or_none()
|
||||
if creator_obj:
|
||||
user_ids.append(creator_obj.user_id)
|
||||
|
||||
agency_result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
agency_obj = agency_result.scalar_one_or_none()
|
||||
if agency_obj:
|
||||
user_ids.append(agency_obj.user_id)
|
||||
|
||||
if user_ids:
|
||||
await notify_task_updated(
|
||||
task_id=task.id,
|
||||
user_ids=user_ids,
|
||||
data={"action": "ai_review_completed", "stage": task.stage.value, "score": video_score},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
ag_result = await db.execute(
|
||||
select(Agency).where(Agency.id == task.agency_id)
|
||||
)
|
||||
ag_obj = ag_result.scalar_one_or_none()
|
||||
if ag_obj:
|
||||
await create_message(
|
||||
db=db,
|
||||
user_id=ag_obj.user_id,
|
||||
type="task",
|
||||
title="视频 AI 审核完成",
|
||||
content=f"任务「{task.name}」视频 AI 审核完成,得分 {video_score} 分,请审核。",
|
||||
related_task_id=task.id,
|
||||
sender_name="系统",
|
||||
)
|
||||
await db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# AI 未配置时通知品牌方
|
||||
if not result.ai_available:
|
||||
|
||||
@@ -195,6 +195,42 @@ async def upload_video(
|
||||
return task
|
||||
|
||||
|
||||
AI_AUTO_REJECT_SCORE = 40
|
||||
|
||||
|
||||
def _check_ai_auto_reject(score: int, result: dict) -> tuple[bool, str]:
|
||||
"""
|
||||
判断 AI 审核结果是否应自动驳回
|
||||
|
||||
触发条件(任一):
|
||||
1. 法规合规维度存在 HIGH 级违规(违禁词/功效词)
|
||||
2. 品牌安全维度存在 HIGH 级违规(竞品提及)
|
||||
3. 总分 < 40
|
||||
"""
|
||||
reasons = []
|
||||
violations = result.get("violations", [])
|
||||
|
||||
# 条件1: 法规 HIGH
|
||||
high_legal = [v for v in violations if v.get("dimension") == "legal" and v.get("severity") == "high"]
|
||||
if high_legal:
|
||||
words = [v.get("content", "") for v in high_legal[:5]]
|
||||
reasons.append(f"法规违规:{', '.join(words)}")
|
||||
|
||||
# 条件2: 品牌安全 HIGH
|
||||
high_brand = [v for v in violations if v.get("dimension") == "brand_safety" and v.get("severity") == "high"]
|
||||
if high_brand:
|
||||
words = [v.get("content", "") for v in high_brand[:5]]
|
||||
reasons.append(f"品牌安全违规:{', '.join(words)}")
|
||||
|
||||
# 条件3: 总分过低
|
||||
if score < AI_AUTO_REJECT_SCORE:
|
||||
reasons.append(f"综合评分 {score} 分,低于合格线 {AI_AUTO_REJECT_SCORE} 分")
|
||||
|
||||
if reasons:
|
||||
return True, ";".join(reasons)
|
||||
return False, ""
|
||||
|
||||
|
||||
async def complete_ai_review(
|
||||
db: AsyncSession,
|
||||
task: Task,
|
||||
@@ -206,9 +242,16 @@ async def complete_ai_review(
|
||||
完成 AI 审核
|
||||
|
||||
- 更新 AI 审核结果
|
||||
- 状态流转到代理商审核
|
||||
- 自动驳回:法规/品牌安全 HIGH 违规或总分 < 40 → 回到上传阶段
|
||||
- 正常:流转到代理商审核
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
auto_rejected, reject_reason = _check_ai_auto_reject(score, result)
|
||||
|
||||
# 将自动驳回信息写入 result,前端可据此展示
|
||||
if auto_rejected:
|
||||
result["ai_auto_rejected"] = True
|
||||
result["ai_reject_reason"] = reject_reason
|
||||
|
||||
if review_type == "script":
|
||||
if task.stage != TaskStage.SCRIPT_AI_REVIEW:
|
||||
@@ -217,7 +260,11 @@ async def complete_ai_review(
|
||||
task.script_ai_score = score
|
||||
task.script_ai_result = result
|
||||
task.script_ai_reviewed_at = now
|
||||
task.stage = TaskStage.SCRIPT_AGENCY_REVIEW
|
||||
|
||||
if auto_rejected:
|
||||
task.stage = TaskStage.SCRIPT_UPLOAD
|
||||
else:
|
||||
task.stage = TaskStage.SCRIPT_AGENCY_REVIEW
|
||||
|
||||
elif review_type == "video":
|
||||
if task.stage != TaskStage.VIDEO_AI_REVIEW:
|
||||
@@ -226,7 +273,11 @@ async def complete_ai_review(
|
||||
task.video_ai_score = score
|
||||
task.video_ai_result = result
|
||||
task.video_ai_reviewed_at = now
|
||||
task.stage = TaskStage.VIDEO_AGENCY_REVIEW
|
||||
|
||||
if auto_rejected:
|
||||
task.stage = TaskStage.VIDEO_UPLOAD
|
||||
else:
|
||||
task.stage = TaskStage.VIDEO_AGENCY_REVIEW
|
||||
|
||||
else:
|
||||
raise ValueError(f"不支持的审核类型: {review_type}")
|
||||
|
||||
@@ -403,10 +403,17 @@ async def seed_data() -> None:
|
||||
ForbiddenWord(id="FW100003", tenant_id=TENANT_ID, word="最好", category="绝对化用语", severity="medium"),
|
||||
ForbiddenWord(id="FW100004", tenant_id=TENANT_ID, word="第一", category="绝对化用语", severity="medium"),
|
||||
ForbiddenWord(id="FW100005", tenant_id=TENANT_ID, word="纯天然", category="虚假宣传", severity="medium"),
|
||||
# 功效词(品牌方可自行增删)
|
||||
ForbiddenWord(id="FW100006", tenant_id=TENANT_ID, word="根治", category="功效词", severity="high"),
|
||||
ForbiddenWord(id="FW100007", tenant_id=TENANT_ID, word="治愈", category="功效词", severity="high"),
|
||||
ForbiddenWord(id="FW100008", tenant_id=TENANT_ID, word="治疗", category="功效词", severity="high"),
|
||||
ForbiddenWord(id="FW100009", tenant_id=TENANT_ID, word="药效", category="功效词", severity="high"),
|
||||
ForbiddenWord(id="FW100010", tenant_id=TENANT_ID, word="疗效", category="功效词", severity="high"),
|
||||
ForbiddenWord(id="FW100011", tenant_id=TENANT_ID, word="特效", category="功效词", severity="high"),
|
||||
]
|
||||
db.add_all(forbidden_words)
|
||||
await db.flush()
|
||||
print(" ✓ 违禁词已创建: 5 条")
|
||||
print(" ✓ 违禁词已创建: 11 条(含 6 条功效词)")
|
||||
|
||||
competitors = [
|
||||
Competitor(id="CP100001", tenant_id=TENANT_ID, brand_id=BRAND_ID, name="安耐晒", keywords=["安耐晒", "ANESSA", "资生堂防晒"]),
|
||||
|
||||
Reference in New Issue
Block a user