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:
Your Name
2026-02-11 20:24:32 +08:00
co-authored by Claude Opus 4.6
parent 0ef7650c09
commit 0b3dfa3c52
10 changed files with 476 additions and 190 deletions
+26 -2
View File
@@ -35,6 +35,8 @@ type ScriptTaskUI = {
brandName: string
scriptStatus: string
scriptFile: string | null
aiAutoRejected?: boolean
aiRejectReason?: string
aiResult: null | {
score: number
dimensions?: ReviewDimensions
@@ -56,8 +58,11 @@ type BriefUI = {
function mapApiToScriptUI(task: TaskResponse): ScriptTaskUI {
const stage = task.stage
let status = 'pending_upload'
const aiAutoRejected = task.script_ai_result?.ai_auto_rejected === true
switch (stage) {
case 'script_upload': status = 'pending_upload'; break
case 'script_upload':
status = aiAutoRejected ? 'ai_rejected' : 'pending_upload'
break
case 'script_ai_review': status = 'ai_reviewing'; break
case 'script_agency_review': status = 'agent_reviewing'; break
case 'script_brand_review': status = 'brand_reviewing'; break
@@ -99,6 +104,8 @@ function mapApiToScriptUI(task: TaskResponse): ScriptTaskUI {
brandName: task.project?.brand_name || '',
scriptStatus: status,
scriptFile: task.script_file_name || null,
aiAutoRejected,
aiRejectReason: task.script_ai_result?.ai_reject_reason,
aiResult,
agencyReview,
brandReview,
@@ -596,7 +603,7 @@ export default function CreatorScriptPage() {
const getStatusDisplay = () => {
const map: Record<string, string> = {
pending_upload: '待上传脚本', ai_reviewing: 'AI 审核中', ai_result: 'AI 审核完成',
pending_upload: '待上传脚本', ai_rejected: 'AI 审核未通过', ai_reviewing: 'AI 审核中', ai_result: 'AI 审核完成',
agent_reviewing: '代理商审核中', agent_rejected: '代理商驳回',
brand_reviewing: '品牌方终审中', brand_passed: '审核通过', brand_rejected: '品牌方驳回',
}
@@ -622,6 +629,23 @@ export default function CreatorScriptPage() {
<AgencyBriefSection toast={toast} briefData={briefData} />
{task.scriptStatus === 'pending_upload' && <UploadSection taskId={taskId} onUploaded={loadTask} />}
{task.scriptStatus === 'ai_rejected' && (
<>
<Card className="border-accent-coral/30 bg-accent-coral/5">
<CardContent className="py-4">
<div className="flex items-start gap-3">
<XCircle size={20} className="text-accent-coral mt-0.5 flex-shrink-0" />
<div>
<p className="text-text-primary font-medium">AI </p>
{task.aiRejectReason && <p className="text-sm text-text-secondary mt-1">{task.aiRejectReason}</p>}
</div>
</div>
</CardContent>
</Card>
<AIResultSection task={task} />
<UploadSection taskId={taskId} onUploaded={loadTask} />
</>
)}
{task.scriptStatus === 'ai_reviewing' && <AIReviewingSection />}
{task.scriptStatus === 'ai_result' && <><AIResultSection task={task} /><WaitingSection message="等待代理商审核" /></>}
{task.scriptStatus === 'agent_reviewing' && <><AIResultSection task={task} /><WaitingSection message="等待代理商审核" /></>}
+24 -2
View File
@@ -22,6 +22,8 @@ type VideoTaskUI = {
brandName: string
videoStatus: string
videoFile: string | null
aiAutoRejected?: boolean
aiRejectReason?: string
aiResult: null | {
score: number
hardViolations: Array<{ type: string; content: string; timestamp: number; suggestion: string }>
@@ -36,8 +38,9 @@ type VideoTaskUI = {
function mapApiToVideoUI(task: TaskResponse): VideoTaskUI {
const stage = task.stage
let status = 'pending_upload'
const aiAutoRejected = task.video_ai_result?.ai_auto_rejected === true
switch (stage) {
case 'video_upload': status = 'pending_upload'; break
case 'video_upload': status = aiAutoRejected ? 'ai_rejected' : 'pending_upload'; break
case 'video_ai_review': status = 'ai_reviewing'; break
case 'video_agency_review': status = 'agent_reviewing'; break
case 'video_brand_review': status = 'brand_reviewing'; break
@@ -80,6 +83,8 @@ function mapApiToVideoUI(task: TaskResponse): VideoTaskUI {
brandName: task.project?.brand_name || '',
videoStatus: status,
videoFile: task.video_file_name || null,
aiAutoRejected,
aiRejectReason: task.video_ai_result?.ai_reject_reason,
aiResult,
agencyReview,
brandReview,
@@ -371,7 +376,7 @@ export default function CreatorVideoPage() {
const getStatusDisplay = () => {
const map: Record<string, string> = {
pending_upload: '待上传视频', ai_reviewing: 'AI 审核中', ai_result: 'AI 审核完成',
pending_upload: '待上传视频', ai_rejected: 'AI 审核未通过', ai_reviewing: 'AI 审核中', ai_result: 'AI 审核完成',
agent_reviewing: '代理商审核中', agent_rejected: '代理商驳回',
brand_reviewing: '品牌方终审中', brand_passed: '审核通过', brand_rejected: '品牌方驳回',
}
@@ -395,6 +400,23 @@ export default function CreatorVideoPage() {
<Card><CardContent className="py-4"><ReviewSteps steps={getReviewSteps(task.videoStatus)} /></CardContent></Card>
{task.videoStatus === 'pending_upload' && <UploadSection taskId={taskId} onUploaded={loadTask} />}
{task.videoStatus === 'ai_rejected' && (
<>
<Card className="border-accent-coral/30 bg-accent-coral/5">
<CardContent className="py-4">
<div className="flex items-start gap-3">
<XCircle size={20} className="text-accent-coral mt-0.5 flex-shrink-0" />
<div>
<p className="text-text-primary font-medium"> AI </p>
{task.aiRejectReason && <p className="text-sm text-text-secondary mt-1">{task.aiRejectReason}</p>}
</div>
</div>
</CardContent>
</Card>
<AIResultSection task={task} />
<UploadSection taskId={taskId} onUploaded={loadTask} />
</>
)}
{task.videoStatus === 'ai_reviewing' && <AIReviewingSection />}
{task.videoStatus === 'ai_result' && <><AIResultSection task={task} /><WaitingSection message="等待代理商审核" /></>}
{task.videoStatus === 'agent_reviewing' && <><AIResultSection task={task} /><WaitingSection message="等待代理商审核" /></>}