'use client' import { useState, useEffect, useCallback } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card' import { Button } from '@/components/ui/Button' import { useToast } from '@/components/ui/Toast' import { Bot, Eye, Mic, Settings, CheckCircle, XCircle, Loader2, Info, Shield, AlertTriangle, RefreshCw, Clock } from 'lucide-react' import { api } from '@/lib/api' import { USE_MOCK } from '@/contexts/AuthContext' import type { AIProvider, AIConfigResponse, ConnectionTestResponse, ModelInfo } from '@/types/ai-config' // AI 提供商选项 const providerOptions: { value: AIProvider | string; label: string }[] = [ { value: 'oneapi', label: 'OneAPI 中转服务' }, { value: 'anthropic', label: 'Anthropic Claude' }, { value: 'openai', label: 'OpenAI' }, { value: 'deepseek', label: 'DeepSeek' }, { value: 'qwen', label: '通义千问' }, { value: 'doubao', label: '豆包' }, { value: 'zhipu', label: '智谱' }, { value: 'moonshot', label: 'Moonshot' }, ] // 预设可用模型列表 const mockModels: Record = { text: [ // Anthropic Claude { id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5' }, { id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5' }, { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' }, { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' }, // OpenAI { id: 'gpt-4o', name: 'GPT-4o' }, { id: 'gpt-4o-mini', name: 'GPT-4o Mini' }, { id: 'gpt-4-turbo', name: 'GPT-4 Turbo' }, { id: 'o1', name: 'o1' }, { id: 'o3-mini', name: 'o3-mini' }, // Google { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash' }, { id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro' }, { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro' }, // DeepSeek { id: 'deepseek-chat', name: 'DeepSeek V3' }, { id: 'deepseek-reasoner', name: 'DeepSeek R1' }, // 通义千问 { id: 'qwen-max', name: '通义千问 Max' }, { id: 'qwen-plus', name: '通义千问 Plus' }, { id: 'qwen-turbo', name: '通义千问 Turbo' }, // 豆包 { id: 'doubao-pro-256k', name: '豆包 Pro 256K' }, { id: 'doubao-pro-32k', name: '豆包 Pro 32K' }, // 智谱 { id: 'glm-4-plus', name: 'GLM-4 Plus' }, { id: 'glm-4', name: 'GLM-4' }, // Moonshot { id: 'moonshot-v1-128k', name: 'Moonshot V1 128K' }, { id: 'moonshot-v1-32k', name: 'Moonshot V1 32K' }, ], vision: [ // Anthropic Claude (原生多模态) { id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5' }, { id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5' }, { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' }, { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' }, // OpenAI { id: 'gpt-4o', name: 'GPT-4o' }, { id: 'gpt-4o-mini', name: 'GPT-4o Mini' }, // Google { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash' }, { id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro' }, { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro' }, // 通义千问 VL { id: 'qwen-vl-max', name: '通义千问 VL Max' }, { id: 'qwen-vl-plus', name: '通义千问 VL Plus' }, // 智谱 { id: 'glm-4v-plus', name: 'GLM-4V Plus' }, { id: 'glm-4v', name: 'GLM-4V' }, ], audio: [ { id: 'whisper-large-v3', name: 'Whisper Large V3' }, { id: 'whisper-large-v3-turbo', name: 'Whisper Large V3 Turbo' }, { id: 'whisper-medium', name: 'Whisper Medium' }, { id: 'sensevoice-v1', name: 'SenseVoice V1 (阿里)' }, { id: 'paraformer-realtime-v2', name: 'Paraformer V2 (阿里)' }, ], } type TestStatus = 'idle' | 'testing' | 'success' | 'failed' function ConfigSkeleton() { return (
) } export default function AIConfigPage() { const toast = useToast() const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [provider, setProvider] = useState('oneapi') const [baseUrl, setBaseUrl] = useState('https://oneapi.intelligrow.cn') const [apiKey, setApiKey] = useState('') const [showApiKey, setShowApiKey] = useState(false) const [isConfigured, setIsConfigured] = useState(false) const [llmModel, setLlmModel] = useState('claude-opus-4-5-20251101') const [visionModel, setVisionModel] = useState('claude-opus-4-5-20251101') const [asrModel, setAsrModel] = useState('whisper-large-v3') const [temperature, setTemperature] = useState(0.7) const [maxTokens, setMaxTokens] = useState(2000) const [availableModels, setAvailableModels] = useState>(mockModels) const [customLlmModel, setCustomLlmModel] = useState('') const [customVisionModel, setCustomVisionModel] = useState('') const [customAsrModel, setCustomAsrModel] = useState('') const [testResults, setTestResults] = useState>({ text: { status: 'idle' }, vision: { status: 'idle' }, audio: { status: 'idle' }, }) const loadConfig = useCallback(async () => { if (USE_MOCK) { setLoading(false) return } try { const config = await api.getAIConfig() setProvider(config.provider) setBaseUrl(config.base_url) setApiKey('') // API key is masked, don't fill it setIsConfigured(config.is_configured) const models = availableModels // 如果后端返回的模型不在预设列表中,设为自定义 if (config.models.text && !(models.text || []).some(m => m.id === config.models.text)) { setLlmModel('__custom__') setCustomLlmModel(config.models.text) } else { setLlmModel(config.models.text) } if (config.models.vision && !(models.vision || []).some(m => m.id === config.models.vision)) { setVisionModel('__custom__') setCustomVisionModel(config.models.vision) } else { setVisionModel(config.models.vision) } if (config.models.audio && !(models.audio || []).some(m => m.id === config.models.audio)) { setAsrModel('__custom__') setCustomAsrModel(config.models.audio) } else { setAsrModel(config.models.audio) } setTemperature(config.parameters.temperature) setMaxTokens(config.parameters.max_tokens) if (config.available_models && Object.keys(config.available_models).length > 0) { setAvailableModels(config.available_models) } } catch (err: any) { // 后端 404 返回 "AI 服务未配置" → Axios 拦截器转为 Error(message) // 这是正常的"尚未配置"状态,不弹错误 const msg = err?.message || '' if (msg.includes('未配置')) { setIsConfigured(false) } else { console.error('Failed to load AI config:', err) toast.error('加载 AI 配置失败') } } finally { setLoading(false) } }, [toast]) useEffect(() => { loadConfig() }, [loadConfig]) // 获取实际使用的模型 ID(自定义时用输入值) const getActualModel = (selected: string, custom: string) => selected === '__custom__' ? custom : selected const handleTestConnection = async () => { setTestResults({ text: { status: 'testing' }, vision: { status: 'testing' }, audio: { status: 'testing' }, }) if (USE_MOCK) { await new Promise(resolve => setTimeout(resolve, 1500)) setTestResults(prev => ({ ...prev, text: { status: 'success', latency: 320 } })) await new Promise(resolve => setTimeout(resolve, 1000)) setTestResults(prev => ({ ...prev, vision: { status: 'success', latency: 450 } })) await new Promise(resolve => setTimeout(resolve, 800)) setTestResults(prev => ({ ...prev, audio: { status: 'success', latency: 280 } })) return } try { const result: ConnectionTestResponse = await api.testAIConnection({ provider: provider as AIProvider, base_url: baseUrl, api_key: apiKey || '***', // use existing key if not changed models: { text: getActualModel(llmModel, customLlmModel), vision: getActualModel(visionModel, customVisionModel), audio: getActualModel(asrModel, customAsrModel) }, }) const newResults: Record = {} for (const [key, r] of Object.entries(result.results)) { newResults[key] = { status: r.success ? 'success' : 'failed', latency: r.latency_ms ?? undefined, error: r.error ?? undefined, } } setTestResults(prev => ({ ...prev, ...newResults })) if (result.success) { toast.success(result.message) } else { toast.error(result.message) } } catch (err) { toast.error('连接测试失败') setTestResults({ text: { status: 'failed', error: '请求失败' }, vision: { status: 'failed', error: '请求失败' }, audio: { status: 'failed', error: '请求失败' }, }) } } const handleSave = async () => { setSaving(true) try { if (USE_MOCK) { await new Promise(resolve => setTimeout(resolve, 500)) } else { await api.updateAIConfig({ provider: provider as AIProvider, base_url: baseUrl, api_key: apiKey || '***', models: { text: getActualModel(llmModel, customLlmModel), vision: getActualModel(visionModel, customVisionModel), audio: getActualModel(asrModel, customAsrModel) }, parameters: { temperature, max_tokens: maxTokens }, }) } toast.success('配置已保存') } catch (err) { toast.error('保存失败') } finally { setSaving(false) } } const getTestStatusIcon = (key: string) => { const result = testResults[key] if (!result) return null switch (result.status) { case 'testing': return case 'success': return ( {result.latency && {result.latency}ms} ) case 'failed': return ( {result.error && {result.error}} ) default: return null } } if (loading) { return (

AI 服务配置

) } return (

AI 服务配置

配置 AI 服务提供商和模型参数

{isConfigured && (
已配置
)}
{/* 配置继承提示 */}

配置继承说明

品牌方配置后,所属代理商和达人将自动使用此配置。代理商和达人端不可见此配置项。

{/* AI 提供商 */} AI 提供商

推荐使用 OneAPI 等中转服务商,方便切换不同 AI 模型

{/* 模型配置 */} 模型配置 {/* 文字处理模型 */}
文字处理模型 (LLM) {getTestStatusIcon('text')}
{llmModel === '__custom__' && ( setCustomLlmModel(e.target.value)} placeholder="输入模型 ID,如 deepseek-chat" /> )}

用于 Brief 解析、脚本语义审核、卖点匹配分析

{/* 视觉理解模型 */}
视觉理解模型 (Vision) {getTestStatusIcon('vision')}
{visionModel === '__custom__' && ( setCustomVisionModel(e.target.value)} placeholder="输入模型 ID,如 gpt-4o" /> )}

用于脚本文档中的图片审核(竞品 logo、违规画面识别)及视频帧分析

{/* 音频解析模型 */}
音频解析模型 (ASR) {getTestStatusIcon('audio')}
{asrModel === '__custom__' && ( setCustomAsrModel(e.target.value)} placeholder="输入模型 ID,如 whisper-large-v3" /> )}

用于语音转文字、口播内容提取

{/* 连接配置 */} 连接配置
setBaseUrl(e.target.value)} placeholder="https://api.openai.com/v1" />
setApiKey(e.target.value)} placeholder={isConfigured ? '留空使用已保存的密钥' : 'sk-...'} />
{/* 生成参数 */} 生成参数
{temperature}
setTemperature(parseFloat(e.target.value))} className="w-full h-2 bg-bg-elevated rounded-lg appearance-none cursor-pointer accent-accent-indigo" />
精确 (0) 创意 (1)
setMaxTokens(parseInt(e.target.value))} min="100" max="8000" />
{/* 安全说明 */}

安全说明

  • • API Key 使用 AES-256-GCM 加密存储
  • • 所有 API 请求强制使用 HTTPS
  • • 仅品牌方管理员可查看/修改此配置
  • • 配置变更将被记录
{/* 操作按钮 */}
) }