refactor: 清理无用模块、修复前后端对齐、添加注册页面

- 删除后端 risk_exceptions 模块(API/Model/Schema/迁移/测试)
- 删除后端 metrics 模块(API/测试)
- 删除后端 ManualTask 模型和相关 Schema
- 修复搜索接口响应缺少 total 字段的问题
- 统一 Platform 枚举(前端去掉后端不支持的 weibo/wechat)
- 新增前端注册页面 /register,登录页添加注册链接

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-09 14:51:17 +08:00
co-authored by Claude Opus 4.6
parent a32102f583
commit 4a3c7e7923
22 changed files with 725 additions and 1298 deletions
+8
View File
@@ -162,6 +162,14 @@ function LoginForm() {
</button>
</form>
{/* 注册链接 */}
<p className="text-center text-sm text-text-secondary">
{' '}
<Link href="/register" className="text-accent-indigo hover:underline font-medium">
</Link>
</p>
{/* Demo 登录 */}
<div className="pt-6 border-t border-border-subtle">
<p className="text-sm text-text-tertiary text-center mb-4">Demo </p>
+233
View File
@@ -0,0 +1,233 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '@/contexts/AuthContext'
import { ShieldCheck, AlertCircle, ArrowLeft, Mail, Lock, User, Phone } from 'lucide-react'
import Link from 'next/link'
import type { UserRole } from '@/lib/api'
const roleOptions: { value: UserRole; label: string; desc: string }[] = [
{ value: 'brand', label: '品牌方', desc: '创建项目、管理代理商、配置审核规则' },
{ value: 'agency', label: '代理商', desc: '管理达人、分配任务、审核内容' },
{ value: 'creator', label: '达人', desc: '上传脚本和视频、查看审核结果' },
]
export default function RegisterPage() {
const router = useRouter()
const { register } = useAuth()
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [role, setRole] = useState<UserRole>('creator')
const [error, setError] = useState('')
const [isLoading, setIsLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
if (!name.trim()) {
setError('请输入用户名')
return
}
if (!email && !phone) {
setError('请填写邮箱或手机号')
return
}
if (password.length < 6) {
setError('密码至少 6 位')
return
}
if (password !== confirmPassword) {
setError('两次密码不一致')
return
}
setIsLoading(true)
const result = await register({
name: name.trim(),
email: email || undefined,
phone: phone || undefined,
password,
role,
})
if (result.success) {
switch (role) {
case 'creator':
router.push('/creator')
break
case 'agency':
router.push('/agency')
break
case 'brand':
router.push('/brand')
break
}
} else {
setError(result.error || '注册失败')
}
setIsLoading(false)
}
return (
<div className="min-h-screen bg-bg-page flex flex-col items-center justify-center px-6 py-12">
<div className="w-full max-w-sm space-y-8">
{/* 返回 */}
<Link
href="/login"
className="inline-flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors"
>
<ArrowLeft className="w-4 h-4" />
</Link>
{/* Logo */}
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-accent-indigo to-[#4F46E5] flex items-center justify-center shadow-[0px_8px_24px_-4px_rgba(99,102,241,0.4)]">
<ShieldCheck className="w-7 h-7 text-white" />
</div>
<div>
<span className="text-2xl font-bold text-text-primary"></span>
<p className="text-sm text-text-secondary"></p>
</div>
</div>
{/* 注册表单 */}
<form onSubmit={handleSubmit} className="space-y-5">
{error && (
<div className="flex items-center gap-2 p-3 bg-accent-coral/10 text-accent-coral rounded-lg text-sm">
<AlertCircle size={16} />
{error}
</div>
)}
{/* 角色选择 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-text-primary"></label>
<div className="grid grid-cols-3 gap-2">
{roleOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setRole(opt.value)}
className={`p-3 rounded-xl border text-center transition-all ${
role === opt.value
? 'border-accent-indigo bg-accent-indigo/10 text-accent-indigo'
: 'border-border-subtle bg-bg-card text-text-secondary hover:bg-bg-elevated'
}`}
>
<div className="font-medium text-sm">{opt.label}</div>
</button>
))}
</div>
<p className="text-xs text-text-tertiary">
{roleOptions.find((o) => o.value === role)?.desc}
</p>
</div>
{/* 用户名 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-text-primary"></label>
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-tertiary" />
<input
type="text"
placeholder="请输入用户名"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full pl-12 pr-4 py-3.5 bg-bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-accent-indigo focus:border-transparent transition-all"
required
/>
</div>
</div>
{/* 邮箱 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-text-primary"></label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-tertiary" />
<input
type="email"
placeholder="请输入邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full pl-12 pr-4 py-3.5 bg-bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-accent-indigo focus:border-transparent transition-all"
/>
</div>
</div>
{/* 手机号 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-text-primary">
<span className="text-text-tertiary font-normal">()</span>
</label>
<div className="relative">
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-tertiary" />
<input
type="tel"
placeholder="请输入手机号"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full pl-12 pr-4 py-3.5 bg-bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-accent-indigo focus:border-transparent transition-all"
/>
</div>
</div>
{/* 密码 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-text-primary"></label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-tertiary" />
<input
type="password"
placeholder="至少 6 位密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-12 pr-4 py-3.5 bg-bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-accent-indigo focus:border-transparent transition-all"
required
/>
</div>
</div>
{/* 确认密码 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-text-primary"></label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-tertiary" />
<input
type="password"
placeholder="请再次输入密码"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full pl-12 pr-4 py-3.5 bg-bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-accent-indigo focus:border-transparent transition-all"
required
/>
</div>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full py-3.5 rounded-xl bg-gradient-to-r from-accent-indigo to-[#4F46E5] text-white font-semibold text-base shadow-[0px_8px_24px_-4px_rgba(99,102,241,0.4)] hover:opacity-90 transition-opacity disabled:opacity-50"
>
{isLoading ? '注册中...' : '注册'}
</button>
</form>
{/* 底部链接 */}
<p className="text-center text-sm text-text-secondary">
{' '}
<Link href="/login" className="text-accent-indigo hover:underline font-medium">
</Link>
</p>
</div>
</div>
)
}
+15 -15
View File
@@ -5,7 +5,7 @@ import { api } from '@/lib/api'
import type {
VideoReviewRequest,
ReviewTask,
TaskStatus,
ReviewTaskStatus,
} from '@/types/review'
interface UseReviewOptions {
@@ -36,11 +36,11 @@ export function useReview(options: UseReviewOptions = {}) {
try {
const response = await api.submitVideoReview(data)
setTask({
reviewId: response.reviewId,
review_id: response.review_id,
status: response.status,
createdAt: new Date().toISOString(),
created_at: new Date().toISOString(),
})
return response.reviewId
return response.review_id
} catch (err) {
const error = err instanceof Error ? err : new Error('提交失败')
setError(error)
@@ -59,11 +59,11 @@ export function useReview(options: UseReviewOptions = {}) {
const progress = await api.getReviewProgress(reviewId)
setTask((prev) => ({
...prev,
reviewId: progress.reviewId,
review_id: progress.review_id,
status: progress.status,
progress: progress.progress,
currentStep: progress.currentStep,
createdAt: prev?.createdAt || new Date().toISOString(),
current_step: progress.current_step,
created_at: prev?.created_at || new Date().toISOString(),
}))
return progress
} catch (err) {
@@ -80,14 +80,14 @@ export function useReview(options: UseReviewOptions = {}) {
try {
const result = await api.getReviewResult(reviewId)
const updatedTask: ReviewTask = {
reviewId: result.reviewId,
review_id: result.review_id,
status: result.status,
score: result.score,
summary: result.summary,
violations: result.violations,
softWarnings: result.softWarnings,
createdAt: task?.createdAt || new Date().toISOString(),
completedAt: new Date().toISOString(),
soft_warnings: result.soft_warnings,
created_at: task?.created_at || new Date().toISOString(),
completed_at: new Date().toISOString(),
}
setTask(updatedTask)
return updatedTask
@@ -96,7 +96,7 @@ export function useReview(options: UseReviewOptions = {}) {
setError(error)
throw error
}
}, [task?.createdAt])
}, [task?.created_at])
/**
* 清除轮询定时器
@@ -203,13 +203,13 @@ export function useReviewResult(reviewId: string | null) {
try {
const result = await api.getReviewResult(reviewId)
setTask({
reviewId: result.reviewId,
review_id: result.review_id,
status: result.status,
score: result.score,
summary: result.summary,
violations: result.violations,
softWarnings: result.softWarnings,
createdAt: new Date().toISOString(),
soft_warnings: result.soft_warnings,
created_at: new Date().toISOString(),
})
} catch (err) {
setError(err instanceof Error ? err : new Error('查询失败'))
+176 -9
View File
@@ -8,6 +8,8 @@ import type {
VideoReviewResponse,
ReviewProgressResponse,
ReviewResultResponse,
ScriptReviewRequest,
ScriptReviewResponse,
} from '@/types/review'
import type {
TaskResponse,
@@ -40,6 +42,29 @@ import type {
AgencyDashboard,
BrandDashboard,
} from '@/types/dashboard'
import type {
ForbiddenWordCreate,
ForbiddenWordResponse,
ForbiddenWordListResponse,
WhitelistCreate,
WhitelistResponse,
WhitelistListResponse,
CompetitorCreate,
CompetitorResponse,
CompetitorListResponse,
PlatformRuleResponse,
PlatformListResponse,
RuleValidateRequest,
RuleValidateResponse,
} from '@/types/rules'
import type {
AIConfigUpdate,
AIConfigResponse,
GetModelsRequest,
ModelsListResponse,
TestConnectionRequest,
ConnectionTestResponse,
} from '@/types/ai-config'
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
const STORAGE_KEY_ACCESS = 'miaosi_access_token'
@@ -67,7 +92,8 @@ export interface User {
export interface LoginRequest {
email?: string
phone?: string
password: string
password?: string
sms_code?: string
}
export interface RegisterRequest {
@@ -102,6 +128,14 @@ export interface UploadPolicyResponse {
max_size_mb: number
}
export interface FileUploadedResponse {
url: string
file_key: string
file_name: string
file_size: number
file_type: string
}
// ==================== Token 管理 ====================
function getAccessToken(): string | null {
@@ -283,8 +317,8 @@ class ApiClient {
/**
* 文件上传完成回调
*/
async fileUploaded(fileKey: string, fileName: string, fileSize: number, fileType: string): Promise<{ url: string }> {
const response = await this.client.post<{ url: string }>('/upload/complete', {
async fileUploaded(fileKey: string, fileName: string, fileSize: number, fileType: string): Promise<FileUploadedResponse> {
const response = await this.client.post<FileUploadedResponse>('/upload/complete', {
file_key: fileKey,
file_name: fileName,
file_size: fileSize,
@@ -299,12 +333,7 @@ class ApiClient {
* 提交视频审核
*/
async submitVideoReview(data: VideoReviewRequest): Promise<VideoReviewResponse> {
const response = await this.client.post<VideoReviewResponse>('/videos/review', {
video_url: data.videoUrl,
platform: data.platform,
brand_id: data.brandId,
creator_id: data.creatorId,
})
const response = await this.client.post<VideoReviewResponse>('/videos/review', data)
return response.data
}
@@ -603,6 +632,144 @@ class ApiClient {
return response.data
}
// ==================== 脚本预审 ====================
/**
* 脚本预审(AI 审核)
*/
async reviewScriptContent(data: ScriptReviewRequest): Promise<ScriptReviewResponse> {
const response = await this.client.post<ScriptReviewResponse>('/scripts/review', data)
return response.data
}
// ==================== 规则管理 ====================
/**
* 查询违禁词列表
*/
async listForbiddenWords(category?: string): Promise<ForbiddenWordListResponse> {
const response = await this.client.get<ForbiddenWordListResponse>('/rules/forbidden-words', {
params: category ? { category } : undefined,
})
return response.data
}
/**
* 添加违禁词
*/
async addForbiddenWord(data: ForbiddenWordCreate): Promise<ForbiddenWordResponse> {
const response = await this.client.post<ForbiddenWordResponse>('/rules/forbidden-words', data)
return response.data
}
/**
* 删除违禁词
*/
async deleteForbiddenWord(wordId: string): Promise<void> {
await this.client.delete(`/rules/forbidden-words/${wordId}`)
}
/**
* 查询白名单
*/
async listWhitelist(brandId?: string): Promise<WhitelistListResponse> {
const response = await this.client.get<WhitelistListResponse>('/rules/whitelist', {
params: brandId ? { brand_id: brandId } : undefined,
})
return response.data
}
/**
* 添加白名单
*/
async addToWhitelist(data: WhitelistCreate): Promise<WhitelistResponse> {
const response = await this.client.post<WhitelistResponse>('/rules/whitelist', data)
return response.data
}
/**
* 查询竞品列表
*/
async listCompetitors(brandId?: string): Promise<CompetitorListResponse> {
const response = await this.client.get<CompetitorListResponse>('/rules/competitors', {
params: brandId ? { brand_id: brandId } : undefined,
})
return response.data
}
/**
* 添加竞品
*/
async addCompetitor(data: CompetitorCreate): Promise<CompetitorResponse> {
const response = await this.client.post<CompetitorResponse>('/rules/competitors', data)
return response.data
}
/**
* 删除竞品
*/
async deleteCompetitor(competitorId: string): Promise<void> {
await this.client.delete(`/rules/competitors/${competitorId}`)
}
/**
* 查询所有平台规则
*/
async listPlatformRules(): Promise<PlatformListResponse> {
const response = await this.client.get<PlatformListResponse>('/rules/platforms')
return response.data
}
/**
* 查询指定平台规则
*/
async getPlatformRules(platform: string): Promise<PlatformRuleResponse> {
const response = await this.client.get<PlatformRuleResponse>(`/rules/platforms/${platform}`)
return response.data
}
/**
* 规则冲突检测
*/
async validateRules(data: RuleValidateRequest): Promise<RuleValidateResponse> {
const response = await this.client.post<RuleValidateResponse>('/rules/validate', data)
return response.data
}
// ==================== AI 配置 ====================
/**
* 获取 AI 配置
*/
async getAIConfig(): Promise<AIConfigResponse> {
const response = await this.client.get<AIConfigResponse>('/ai-config')
return response.data
}
/**
* 更新 AI 配置
*/
async updateAIConfig(data: AIConfigUpdate): Promise<AIConfigResponse> {
const response = await this.client.put<AIConfigResponse>('/ai-config', data)
return response.data
}
/**
* 获取可用模型列表
*/
async getAIModels(data: GetModelsRequest): Promise<ModelsListResponse> {
const response = await this.client.post<ModelsListResponse>('/ai-config/models', data)
return response.data
}
/**
* 测试 AI 连接
*/
async testAIConnection(data: TestConnectionRequest): Promise<ConnectionTestResponse> {
const response = await this.client.post<ConnectionTestResponse>('/ai-config/test', data)
return response.data
}
// ==================== 健康检查 ====================
/**
-2
View File
@@ -4,8 +4,6 @@ export const platformOptions = [
{ id: 'xiaohongshu', name: '小红书', icon: '📕', bgColor: 'bg-[#fe2c55]/15', textColor: 'text-[#fe2c55]', borderColor: 'border-[#fe2c55]/30' },
{ id: 'bilibili', name: 'B站', icon: '📺', bgColor: 'bg-[#00a1d6]/15', textColor: 'text-[#00a1d6]', borderColor: 'border-[#00a1d6]/30' },
{ id: 'kuaishou', name: '快手', icon: '⚡', bgColor: 'bg-[#ff4906]/15', textColor: 'text-[#ff4906]', borderColor: 'border-[#ff4906]/30' },
{ id: 'weibo', name: '微博', icon: '🔴', bgColor: 'bg-[#e6162d]/15', textColor: 'text-[#e6162d]', borderColor: 'border-[#e6162d]/30' },
{ id: 'wechat', name: '微信视频号', icon: '💬', bgColor: 'bg-[#07c160]/15', textColor: 'text-[#07c160]', borderColor: 'border-[#07c160]/30' },
]
export type PlatformId = typeof platformOptions[number]['id']
+87
View File
@@ -0,0 +1,87 @@
/**
* AI 配置类型定义
* 与后端 schemas/ai_config.py 对齐
*/
export type AIProvider =
| 'oneapi'
| 'openrouter'
| 'anthropic'
| 'openai'
| 'deepseek'
| 'qwen'
| 'doubao'
| 'zhipu'
| 'moonshot'
export interface AIModelsConfig {
text: string
vision: string
audio: string
}
export interface AIParametersConfig {
temperature: number
max_tokens: number
}
// ===== 请求 =====
export interface AIConfigUpdate {
provider: AIProvider
base_url: string
api_key: string
models: AIModelsConfig
parameters: AIParametersConfig
}
export interface GetModelsRequest {
provider: AIProvider
base_url: string
api_key: string
}
export interface TestConnectionRequest {
provider: AIProvider
base_url: string
api_key: string
models: AIModelsConfig
}
// ===== 响应 =====
export interface AIConfigResponse {
provider: string
base_url: string
api_key_masked: string
models: AIModelsConfig
parameters: AIParametersConfig
available_models: Record<string, ModelInfo[]>
is_configured: boolean
last_test_at?: string | null
last_test_result?: Record<string, unknown> | null
}
export interface ModelInfo {
id: string
name: string
}
export interface ModelsListResponse {
success: boolean
models: Record<string, ModelInfo[]>
error?: string | null
}
export interface ModelTestResult {
success: boolean
latency_ms?: number | null
error?: string | null
model: string
}
export interface ConnectionTestResponse {
success: boolean
results: Record<string, ModelTestResult>
message: string
}
+80 -30
View File
@@ -1,75 +1,125 @@
/**
* 视频审核相关类型定义
* 与后端 schemas/review.py 对齐
*/
export type TaskStatus = 'pending' | 'processing' | 'completed' | 'failed'
// 审核任务状态(区别于 task.ts 中的 TaskStatus
export type ReviewTaskStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'approved' | 'rejected'
export type RiskLevel = 'high' | 'medium' | 'low'
export type ViolationType =
| 'forbidden_word'
| 'efficacy_claim'
| 'competitor_logo'
| 'duration_short'
| 'mention_missing'
| 'brand_safety'
export type ViolationSource = 'speech' | 'subtitle' | 'visual'
export type ViolationSource = 'text' | 'speech' | 'subtitle' | 'visual'
export type SoftRiskAction = 'confirm' | 'note'
export type Platform = 'douyin' | 'xiaohongshu' | 'bilibili' | 'kuaishou'
// 文本位置(脚本审核)
export interface Position {
start: number
end: number
}
// 违规项(与后端 Violation 对齐)
export interface Violation {
id: string
type: ViolationType
content: string
timestamp: number
source: ViolationSource
riskLevel: RiskLevel
severity: RiskLevel
suggestion: string
// 文本审核字段
position?: Position | null
// 视频审核字段
timestamp?: number | null
timestamp_end?: number | null
source?: ViolationSource | null
}
export interface SoftWarning {
id: string
type: string
content: string
suggestion: string
// 软性风控提示(与后端 SoftRiskWarning 对齐)
export interface SoftRiskWarning {
code: string
message: string
action_required: SoftRiskAction
blocking: boolean
context?: Record<string, unknown> | null
}
// 前端内部使用的审核任务状态对象
export interface ReviewTask {
reviewId: string
review_id: string
title?: string
status: TaskStatus
status: ReviewTaskStatus
progress?: number
currentStep?: string
current_step?: string
score?: number
summary?: string
violations?: Violation[]
softWarnings?: SoftWarning[]
createdAt: string
completedAt?: string
soft_warnings?: SoftRiskWarning[]
created_at: string
completed_at?: string
}
// ==================== 请求/响应类型 ====================
export interface VideoReviewRequest {
videoUrl?: string
platform: string
brandId?: string
creatorId?: string
title?: string
video_url: string
platform: Platform
brand_id: string
creator_id: string
competitors?: string[]
requirements?: Record<string, unknown>
}
export interface VideoReviewResponse {
reviewId: string
status: TaskStatus
review_id: string
status: ReviewTaskStatus
}
export interface ReviewProgressResponse {
reviewId: string
status: TaskStatus
review_id: string
status: ReviewTaskStatus
progress: number
currentStep: string
current_step: string
}
export interface ReviewResultResponse {
reviewId: string
status: TaskStatus
review_id: string
status: ReviewTaskStatus
score: number
summary: string
violations: Violation[]
softWarnings: SoftWarning[]
soft_warnings: SoftRiskWarning[]
}
// ==================== 脚本预审 ====================
export interface SoftRiskContext {
violation_rate?: number
violation_threshold?: number
asr_confidence?: number
ocr_confidence?: number
has_history_violation?: boolean
}
export interface ScriptReviewRequest {
content: string
platform: Platform
brand_id: string
required_points?: string[]
soft_risk_context?: SoftRiskContext
}
export interface ScriptReviewResponse {
score: number
summary: string
violations: Violation[]
missing_points?: string[]
soft_warnings: SoftRiskWarning[]
}
+98
View File
@@ -0,0 +1,98 @@
/**
* 规则管理类型定义
* 与后端 api/rules.py 对齐
*/
// ===== 违禁词 =====
export interface ForbiddenWordCreate {
word: string
category: string
severity: string
}
export interface ForbiddenWordResponse {
id: string
word: string
category: string
severity: string
}
export interface ForbiddenWordListResponse {
items: ForbiddenWordResponse[]
total: number
}
// ===== 白名单 =====
export interface WhitelistCreate {
term: string
reason: string
brand_id: string
}
export interface WhitelistResponse {
id: string
term: string
reason: string
brand_id: string
}
export interface WhitelistListResponse {
items: WhitelistResponse[]
total: number
}
// ===== 竞品 =====
export interface CompetitorCreate {
name: string
brand_id: string
logo_url?: string
keywords: string[]
}
export interface CompetitorResponse {
id: string
name: string
brand_id: string
logo_url?: string | null
keywords: string[]
}
export interface CompetitorListResponse {
items: CompetitorResponse[]
total: number
}
// ===== 平台规则 =====
export interface PlatformRuleResponse {
platform: string
rules: Record<string, unknown>[]
version: string
updated_at: string
}
export interface PlatformListResponse {
items: PlatformRuleResponse[]
total: number
}
// ===== 规则冲突检测 =====
export interface RuleValidateRequest {
brand_id: string
platform: string
brief_rules: Record<string, unknown>
}
export interface RuleConflict {
brief_rule: string
platform_rule: string
suggestion: string
}
export interface RuleValidateResponse {
conflicts: RuleConflict[]
}