Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/19125119617_ai-customer-service.json

1 line
89 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "19125119617/ai-customer-service", "files": {"src/app/api/ai/chat/route.ts": {"db4291e5eaaede2c19cffd573e01d953bafb54e5": {"t": 1785746034.3805614, "n": 3100, "h": "5ebe05ef50393fcf", "c": "// AI 聊天 API\nimport { NextRequest, NextResponse } from 'next/server';\nimport { ragQuery } from '@/lib/rag';\nimport prisma from '@/lib/prisma';\n\n// AI 提供商配置\nconst AI_PROVIDERS = {\n deepseek: {\n baseURL: 'https://api.deepseek.com/v1',\n model: 'deepseek-chat',\n },\n qwen: {\n baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n model: 'qwen-turbo',\n },\n moonshot: {\n baseURL: 'https://api.moonshot.cn/v1',\n model: 'moonshot-v1-8k',\n },\n zhipu: {\n baseURL: 'https://open.bigmodel.cn/api/paas/v4',\n model: 'glm-4-flash',\n },\n};\n\n// 调用 AI 接口\nasync function callAI(prompt: string, provider: keyof typeof AI_PROVIDERS = 'deepseek', apiKey?: string) {\n const config = AI_PROVIDERS[provider];\n const key = apiKey || process.env.AI_API_KEY;\n\n if (!key) {\n throw new Error('未配置 API Key');\n }\n\n const response = await fetch(`${config.baseURL}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${key}`,\n },\n body: JSON.stringify({\n model: config.model,\n messages: [\n { role: 'system', content: '你是一个专业的客服助手,请用友好的语气回答用户的问题。' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.7,\n max_tokens: 1000,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`AI 调用失败: ${error}`);\n }\n\n const data = await response.json();\n return data.choices[0]?.message?.content || '抱歉,我暂时无法回答这个问题。';\n}\n\n// POST /api/ai/chat - AI 聊天\nexport async function POST(request: NextRequest) {\n try {\n const body = await request.json();\n const { conversationId, message, provider = 'deepseek' } = body;\n\n if (!message) {\n return NextResponse.json({ error: '缺少消息内容' }, { status: 400 });\n }\n\n // 1. 从知识库检索相关内容\n const teamId = 'default-team';\n const ragResult = await ragQuery(message, teamId);\n\n // 2. 构建 prompt\n let prompt = message;\n if (ragResult.sources.length > 0) {\n const context = ragResult.sources\n .map((s, i) => `[参考${i + 1}]: ${s.content}`)\n .join('\\n\\n');\n prompt = `参考以下知识库内容回答用户问题:\\n\\n${context}\\n\\n用户问题:${message}\\n\\n请根据知识库内容给出回答,如果没有相关信息,请如实说明。`;\n }\n\n // 3. 调用 AI\n const aiResponse = await callAI(prompt, provider as any);\n\n // 4. 如果有会话ID,保存 AI 回复到数据库\n if (conversationId) {\n // 获取默认用户或创建系统消息\n const aiMessage = await prisma.message.create({\n data: {\n conversationId,\n senderId: 'ai-assistant',\n senderType: 'AI',\n content: aiResponse,\n isAiGenerated: true,\n },\n });\n\n return NextResponse.json({\n message: aiMessage,\n sources: ragResult.sources,\n elapsed: ragResult.elapsed,\n });\n }\n\n return NextResponse.json({\n response: aiResponse,\n sources: ragResult.sources,\n elapsed: ragResult.elapsed,\n });\n } catch (error) {\n console.error('Error in AI chat:', error);\n return NextResponse.json(\n { error: error instanceof Error ? error.message : 'AI 聊天失败' },\n { status: 500 }\n );\n }\n}\n"}}, "src/app/settings/page.tsx": {"db4291e5eaaede2c19cffd573e01d953bafb54e5": {"t": 1785747362.861727, "n": 78799, "h": "3bfce95adb7c851c", "c": "\"use client\";\nimport { useState } from \"react\";\nimport PageLayout from \"@/components/PageLayout\";\nimport {\n Settings, Users, Bell, Shield, Globe, Palette, Database, Key,\n Check, X, Plus, Trash2, Copy, Eye, EyeOff, RefreshCw, Download,\n Mail, Smartphone, Webhook, Trash, AlertTriangle,\n CheckCircle2, Image as ImageIcon, Moon, Sun, Monitor,\n Store, Link, TestTube, Edit2, ToggleLeft, ToggleRight,\n Book, ExternalLink,\n} from \"lucide-react\";\n\nconst menuItems = [\n { icon: Book, label: \"使用教程\", desc: \"各模块使用指南、API配置教程\" },\n { icon: Settings, label: \"项目设置\", desc: \"基本信息、时区、语言\" },\n { icon: Users, label: \"成员管理\", desc: \"添加成员、设置角色\" },\n { icon: Shield, label: \"权限配置\", desc: \"角色权限、访问控制\" },\n { icon: Bell, label: \"通知设置\", desc: \"邮件、短信、推送通知\" },\n { icon: Globe, label: \"渠道配置\", desc: \"渠道接入、Webhook\" },\n { icon: Palette, label: \"界面定制\", desc: \"主题、Logo、颜色\" },\n { icon: Database, label: \"数据管理\", desc: \"数据导出、清理\" },\n { icon: Key, label: \"API密钥\", desc: \"管理API访问密钥\" },\n];\n\nconst permissionModules = [\n { id: \"dashboard\", name: \"首页仪表盘\", permissions: [\"查看\", \"编辑\"] },\n { id: \"conversations\", name: \"聊天会话\", permissions: [\"查看\", \"回复\", \"转接\", \"关闭\"] },\n { id: \"customers\", name: \"客户管理\", permissions: [\"查看\", \"编辑\", \"删除\", \"导入导出\"] },\n { id: \"marketing\", name: \"营销模块\", permissions: [\"查看\", \"创建\", \"编辑\", \"删除\", \"发送\"] },\n { id: \"ai\", name: \"AI机器人\", permissions: [\"查看\", \"配置\", \"训练\", \"启停\"] },\n { id: \"knowledge\", name: \"知识库\", permissions: [\"查看\", \"上传\", \"编辑\", \"删除\"] },\n { id: \"channels\", name: \"渠道管理\", permissions: [\"查看\", \"添加\", \"编辑\", \"删除\"] },\n { id: \"reports\", name: \"报告分析\", permissions: [\"查看\", \"导出\"] },\n { id: \"settings\", name: \"系统设置\", permissions: [\"查看\", \"编辑\"] },\n];\n\nconst roles = [\n { id: \"admin\", name: \"管理员\", description: \"拥有所有权限\", color: \"bg-red-100 text-red-700\" },\n { id: \"supervisor\", name: \"主管\", description: \"管理团队和查看报告\", color: \"bg-purple-100 text-purple-700\" },\n { id: \"agent\", name: \"客服\", description: \"处理会话和客户\", color: \"bg-blue-100 text-blue-700\" },\n { id: \"viewer\", name: \"访客\", description: \"仅查看权限\", color: \"bg-gray-100 text-gray-700\" },\n];\n\nconst notificationTypes = [\n { id: \"new_message\", name: \"新消息提醒\", description: \"当有新客户消息时\", email: true, push: true, sms: false },\n { id: \"assigned\", name: \"会话分配\", description: \"当有新会话分配给你时\", email: true, push: true, sms: false },\n { id: \"ai_handover\", name: \"AI转人工\", description: \"当AI无法处理转人工时\", email: true, push: true, sms: true },\n { id: \"customer_join\", name: \"客户接入\", description: \"当高价值客户接入时\", email: false, push: true, sms: false },\n { id: \"daily_report\", name: \"日报\", description: \"每日工作汇总\", email: true, push: false, sms: false },\n { id: \"weekly_report\", name: \"周报\", description: \"每周工作汇总\", email: true, push: false, sms: false },\n { id: \"system_alert\", name: \"系统告警\", description: \"系统异常或维护通知\", email: true, push: true, sms: true },\n];\n\ntype ChannelConfig = {\n id: string; name: string; type: string; status: string; icon: string;\n webhook: string; appId: string; appSecret: string; bindStore: string; enabled: boolean;\n};\n\nconst channelConfigs: ChannelConfig[] = [\n { id: \"whatsapp\", name: \"WhatsApp\", type: \"即时通讯\", status: \"已连接\", icon: \"💬\", webhook: \"https://api.example.com/webhook/whatsapp\", appId: \"WA_APP_12345\", appSecret: \"whasec_xxxx\", bindStore: \"主店铺\", enabled: true },\n { id: \"messenger\", name: \"Messenger\", type: \"即时通讯\", status: \"已连接\", icon: \"📘\", webhook: \"https://api.example.com/webhook/messenger\", appId: \"MSG_APP_67890\", appSecret: \"msgsec_xxxx\", bindStore: \"主店铺\", enabled: true },\n { id: \"telegram\", name: \"Telegram\", type: \"即时通讯\", status: \"已连接\", icon: \"✈️\", webhook: \"https://api.example.com/webhook/telegram\", appId: \"TG_BOT_11111\", appSecret: \"tgsec_xxxx\", bindStore: \"\", enabled: true },\n { id: \"email\", name: \"Email\", type: \"邮件\", status: \"已连接\", icon: \"📧\", webhook: \"https://api.example.com/webhook/email\", appId: \"support@company.com\", appSecret: \"\", bindStore: \"主店铺\", enabled: true },\n { id: \"line\", name: \"LINE\", type: \"即时通讯\", status: \"未连接\", icon: \"🟢\", webhook: \"\", appId: \"\", appSecret: \"\", bindStore: \"\", enabled: false },\n { id: \"wechat\", name: \"微信公众号\", type: \"即时通讯\", status: \"未连接\", icon: \"💚\", webhook: \"\", appId: \"\", appSecret: \"\", bindStore: \"\", enabled: false },\n];\n\nconst apiKeys = [\n { id: \"1\", name: \"生产环境密钥\", key: \"ak_live_xxxxxxxxxxxx\", createdAt: \"2026-01-15\", lastUsed: \"2小时前\", status: \"active\" },\n { id: \"2\", name: \"测试环境密钥\", key: \"ak_test_xxxxxxxxxxxx\", createdAt: \"2026-02-01\", lastUsed: \"1天前\", status: \"active\" },\n { id: \"3\", name: \"开发环境密钥\", key: \"ak_dev_xxxxxxxxxxxx\", createdAt: \"2026-01-20\", lastUsed: \"3天前\", status: \"inactive\" },\n];\n\n// ---- Toast 组件 ----\nfunction Toast({ msg, type = \"success\", onClose }: { msg: string; type?: \"success\" | \"error\" | \"info\"; onClose: () => void }) {\n const colorMap = { success: \"bg-gray-900 text-white\", error: \"bg-red-600 text-white\", info: \"bg-blue-600 text-white\" };\n return (\n <div className={`fixed top-5 right-5 z-[100] flex items-center gap-2 px-4 py-2.5 rounded-xl shadow-xl text-sm ${colorMap[type]}`}>\n {type === \"success\" && <CheckCircle2 className=\"w-4 h-4 text-green-400 flex-shrink-0\" />}\n {type === \"error\" && <AlertTriangle className=\"w-4 h-4 text-red-200 flex-shrink-0\" />}\n <span>{msg}</span>\n <button onClick={onClose} className=\"ml-1 opacity-60 hover:opacity-100\"><X className=\"w-3.5 h-3.5\" /></button>\n </div>\n );\n}\n\nexport default function SettingsPage() {\n const [activeMenu, setActiveMenu] = useState(\"使用教程\");\n\n // ---- 全局 Toast ----\n const [toastMsg, setToastMsg] = useState(\"\");\n const [toastType, setToastType] = useState<\"success\" | \"error\" | \"info\">(\"success\");\n const [showToast, setShowToast] = useState(false);\n const toast = (msg: string, type: \"success\" | \"error\" | \"info\" = \"success\") => {\n setToastMsg(msg); setToastType(type); setShowToast(true);\n setTimeout(() => setShowToast(false), 2500);\n };\n\n // ---- 项目设置 ----\n const [projectName, setProjectName] = useState(\"我的AI客服系统\");\n const [timezone, setTimezone] = useState(\"Asia/Shanghai\");\n const [language, setLanguage] = useState(\"zh-CN\");\n\n // ---- AI 模型配置 ----\n type AIModel = { name: string; provider: string; type: string; status: string; badge: string; apiKey: string; configured: boolean };\n const [aiModels, setAIModels] = useState<AIModel[]>([\n { name: \"DeepSeek R1\", provider: \"DeepSeek\", type: \"推理模型\", status: \"已配置\", badge: \"主用\", apiKey: \"sk-••••••••••••••••\", configured: true },\n { name: \"通义千问 Max\", provider: \"阿里云\", type: \"对话模型\", status: \"已配置\", badge: \"\", apiKey: \"sk-••••••••••••••••\", configured: true },\n { name: \"GLM-4 Flash\", provider: \"智谱AI\", type: \"快速模型\", status: \"已配置\", badge: \"免费\", apiKey: \"sk-••••••••••••••••\", configured: true },\n ]);\n const [showAIConfigModal, setShowAIConfigModal] = useState(false);\n const [editingAIModel, setEditingAIModel] = useState<AIModel | null>(null);\n const [showAddModelModal, setShowAddModelModal] = useState(false);\n const [newModel, setNewModel] = useState({ name: \"\", provider: \"DeepSeek\", type: \"对话模型\", apiKey: \"\" });\n const [showAIKeyValue, setShowAIKeyValue] = useState<Record<string, boolean>>({});\n\n const handleEditAIModel = (m: AIModel) => { setEditingAIModel({ ...m, apiKey: \"\" }); setShowAIConfigModal(true); };\n const handleSaveAIModel = () => {\n if (!editingAIModel) return;\n setAIModels(prev => prev.map(m => m.name === editingAIModel.name ? { ...m, apiKey: editingAIModel.apiKey || m.apiKey, configured: true, status: \"已配置\" } : m));\n setShowAIConfigModal(false); setEditingAIModel(null); toast(\"模型配置已保存\");\n };\n const handleSaveNewModel = () => {\n if (!newModel.name.trim() || !newModel.apiKey.trim()) return;\n setAIModels(prev => [...prev, { name: newModel.name, provider: newModel.provider, type: newModel.type, status: \"已配置\", badge: \"\", apiKey: \"sk-••••••••••••••••\", configured: true }]);\n setNewModel({ name: \"\", provider: \"DeepSeek\", type: \"对话模型\", apiKey: \"\" });\n setShowAddModelModal(false); toast(\"模型添加成功\");\n };\n\n // ---- 成员管理 ----\n const members = [\n { name: \"张三\", email: \"zhang@company.com\", role: \"管理员\", status: \"在线\", joined: \"2026-01-01\" },\n { name: \"李四\", email: \"li@company.com\", role: \"客服\", status: \"在线\", joined: \"2026-01-15\" },\n { name: \"王五\", email: \"wang@company.com\", role: \"客服\", status: \"离线\", joined: \"2026-02-01\" },\n { name: \"赵六\", email: \"zhao@company.com\", role: \"主管\", status: \"忙碌\", joined: \"2026-01-20\" },\n ];\n type Member = typeof members[0];\n const [memberList, setMemberList] = useState<Member[]>(members);\n const [showEditMemberModal, setShowEditMemberModal] = useState(false);\n const [editingMember, setEditingMember] = useState<Member | null>(null);\n const [showDeleteMemberConfirm, setShowDeleteMemberConfirm] = useState(false);\n const [deletingMember, setDeletingMember] = useState<Member | null>(null);\n const [showInviteModal, setShowInviteModal] = useState(false);\n const [inviteEmail, setInviteEmail] = useState(\"\");\n const [inviteRole, setInviteRole] = useState(\"客服\");\n\n // ---- 权限配置 ----\n const [selectedRole, setSelectedRole] = useState(\"admin\");\n const [rolePermissions, setRolePermissions] = useState<Record<string, string[]>>({\n admin: permissionModules.flatMap(m => m.permissions.map(p => `${m.id}:${p}`)),\n supervisor: [\"dashboard:查看\", \"conversations:查看\", \"conversations:回复\", \"conversations:转接\", \"customers:查看\", \"customers:编辑\", \"marketing:查看\", \"ai:查看\", \"knowledge:查看\", \"channels:查看\", \"reports:查看\", \"reports:导出\"],\n agent: [\"dashboard:查看\", \"conversations:查看\", \"conversations:回复\", \"conversations:转接\", \"customers:查看\", \"customers:编辑\", \"knowledge:查看\"],\n viewer: [\"dashboard:查看\", \"conversations:查看\", \"customers:查看\", \"reports:查看\"],\n });\n const togglePermission = (moduleId: string, permission: string) => {\n const key = `${moduleId}:${permission}`;\n setRolePermissions(prev => ({ ...prev, [selectedRole]: prev[selectedRole].includes(key) ? prev[selectedRole].filter(p => p !== key) : [...prev[selectedRole], key] }));\n };\n\n // ---- 通知设置 ----\n const [notifications, setNotifications] = useState(notificationTypes);\n const [notifyEmail, setNotifyEmail] = useState(\"admin@company.com\");\n const [notifyPhone, setNotifyPhone] = useState(\"+86 138 0000 0000\");\n const [notifyEmailEnabled, setNotifyEmailEnabled] = useState(true);\n const [notifySmsEnabled, setNotifySmsEnabled] = useState(true);\n const [notifyPushEnabled, setNotifyPushEnabled] = useState(true);\n const toggleNotification = (id: string, type: \"email\" | \"push\" | \"sms\") => {\n setNotifications(prev => prev.map(n => n.id === id ? { ...n, [type]: !n[type] } : n));\n };\n\n // ---- 渠道配置 ----\n const [channels, setChannels] = useState<ChannelConfig[]>(channelConfigs);\n const [showChannelModal, setShowChannelModal] = useState(false);\n const [editingChannel, setEditingChannel] = useState<ChannelConfig | null>(null);\n const [webhookTestResult, setWebhookTestResult] = useState<Record<string, \"idle\" | \"testing\" | \"ok\" | \"fail\">>({});\n const [channelSecretVisible, setChannelSecretVisible] = useState<Record<string, boolean>>({});\n const openChannelEdit = (c: ChannelConfig) => { setEditingChannel({ ...c }); setShowChannelModal(true); };\n const saveChannel = () => {\n if (!editingChannel) return;\n setChannels(prev => prev.map(c => c.id === editingChannel.id ? { ...editingChannel, status: editingChannel.appId ? \"已连接\" : \"未连接\" } : c));\n setShowChannelModal(false); setEditingChannel(null); toast(\"渠道配置已保存\");\n };\n const testWebhook = (id: string) => {\n setWebhookTestResult(prev => ({ ...prev, [id]: \"testing\" }));\n setTimeout(() => setWebhookTestResult(prev => ({ ...prev, [id]: Math.random() > 0.2 ? \"ok\" : \"fail\" })), 1500);\n };\n const toggleChannel = (id: string) => {\n setChannels(prev => prev.map(c => c.id === id ? { ...c, enabled: !c.enabled } : c));\n };\n\n // ---- 界面定制 ----\n const [theme, setTheme] = useState(\"light\");\n const [primaryColor, setPrimaryColor] = useState(\"#3B82F6\");\n\n // ---- 数据管理 ----\n const [showExportConfirm, setShowExportConfirm] = useState<{ name: string; format: string } | null>(null);\n const [showCleanConfirm, setShowCleanConfirm] = useState<{ name: string; desc: string } | null>(null);\n const [exportRetentionDays, setExportRetentionDays] = useState(\"30 天\");\n const handleExport = (name: string, format: string) => { setShowExportConfirm({ name, format }); };\n const confirmExport = () => { toast(`${showExportConfirm?.name} 导出任务已提交,稍后将发送到您的邮箱`); setShowExportConfirm(null); };\n const handleClean = (name: string, desc: string) => { setShowCleanConfirm({ name, desc }); };\n const confirmClean = () => { toast(`${showCleanConfirm?.name} 已清理完成`); setShowCleanConfirm(null); };\n\n // ---- API密钥 ----\n const [keys, setKeys] = useState(apiKeys);\n const [showKeyId, setShowKeyId] = useState<string | null>(null);\n const [newKeyName, setNewKeyName] = useState(\"\");\n const [showNewKeyModal, setShowNewKeyModal] = useState(false);\n const [generatedKey, setGeneratedKey] = useState(\"\");\n const generateNewKey = () => {\n const k = `ak_live_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;\n setGeneratedKey(k); setShowNewKeyModal(true);\n };\n const saveNewKey = () => {\n if (newKeyName && generatedKey) {\n setKeys(prev => [...prev, { id: Date.now().toString(), name: newKeyName, key: generatedKey, createdAt: new Date().toISOString().split(\"T\")[0], lastUsed: \"从未\", status: \"active\" }]);\n setNewKeyName(\"\"); setGeneratedKey(\"\"); setShowNewKeyModal(false); toast(\"密钥已保存\");\n }\n };\n const deleteKey = (id: string) => { setKeys(prev => prev.filter(k => k.id !== id)); toast(\"密钥已删除\"); };\n const copyKey = (key: string) => { navigator.clipboard.writeText(key); toast(\"已复制到剪贴板\"); };\n\n return (\n <PageLayout>\n <div className=\"flex h-screen\">\n {/* Settings Nav */}\n <div className=\"w-56 border-r border-gray-200 bg-white p-4 flex-shrink-0\">\n <div className=\"text-xs font-medium text-gray-400 uppercase tracking-wider mb-3 px-2\">设置</div>\n <nav className=\"space-y-1\">\n {menuItems.map(({ icon: Icon, label }) => (\n <button\n key={label}\n onClick={() => setActiveMenu(label)}\n className={`w-full text-left flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${activeMenu === label ? \"bg-blue-50 text-blue-700\" : \"text-gray-700 hover:bg-gray-50\"}`}\n >\n <Icon className=\"w-4 h-4 flex-shrink-0\" />\n <span className=\"text-sm font-medium\">{label}</span>\n </button>\n ))}\n </nav>\n </div>\n\n {/* Settings Content */}\n <div className=\"flex-1 overflow-auto p-8\">\n\n {/* ───── 使用教程 ───── */}\n {activeMenu === \"使用教程\" && (\n <div className=\"max-w-4xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">使用教程</h2>\n <p className=\"text-sm text-gray-500\">学习系统各功能的使用方法和API配置指南</p>\n </div>\n\n {/* 教程分类卡片 */}\n <div className=\"grid grid-cols-2 gap-4\">\n {[\n {\n icon: \"🎯\",\n title: \"快速入门\",\n desc: \"5分钟快速上手系统核心功能\",\n color: \"bg-green-50 border-green-200\",\n href: \"/tutorials\",\n chapters: [\"创建第一个AI机器人\", \"连接第一个渠道\", \"配置知识库\", \"测试对话\"],\n },\n {\n icon: \"📚\",\n title: \"模块教程\",\n desc: \"各功能模块详细使用指南\",\n color: \"bg-blue-50 border-blue-200\",\n href: \"/tutorials\",\n chapters: [\"首页仪表盘\", \"聊天会话管理\", \"客户管理\", \"AI机器人配置\"],\n },\n {\n icon: \"🔌\",\n title: \"API配置教程\",\n desc: \"各平台API接入详细步骤\",\n color: \"bg-purple-50 border-purple-200\",\n href: \"/tutorials\",\n chapters: [\"WhatsApp Business\", \"LINE Messaging API\", \"企业邮箱配置\", \"Telegram Bot\"],\n },\n {\n icon: \"⚡\",\n title: \"高级配置\",\n desc: \"自动化工作流和高级功能\",\n color: \"bg-orange-50 border-orange-200\",\n href: \"/tutorials\",\n chapters: [\"自动化工作流\", \"AI员工配置\", \"代理IP配置\", \"指纹浏览器\"],\n },\n ].map((item, idx) => (\n <a key={idx} href={item.href} className={`${item.color} border rounded-xl p-5 hover:shadow-md transition-shadow block`}>\n <div className=\"flex items-start gap-3 mb-3\">\n <span className=\"text-3xl\">{item.icon}</span>\n <div>\n <h3 className=\"font-semibold text-gray-900\">{item.title}</h3>\n <p className=\"text-sm text-gray-500 mt-0.5\">{item.desc}</p>\n </div>\n </div>\n <ul className=\"space-y-1.5\">\n {item.chapters.map((ch, i) => (\n <li key={i} className=\"flex items-center gap-2 text-sm text-gray-600\">\n <span className=\"w-1.5 h-1.5 rounded-full bg-gray-400\"></span>\n {ch}\n </li>\n ))}\n </ul>\n <div className=\"mt-3 flex items-center text-sm text-blue-600 font-medium\">\n 查看完整教程\n <ExternalLink className=\"w-4 h-4 ml-1\" />\n </div>\n </a>\n ))}\n </div>\n\n {/* 快速链接 */}\n <div className=\"bg-gray-50 rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2\">\n <Book className=\"w-4 h-4\" />\n 热门教程\n </h3>\n <div className=\"grid grid-cols-2 gap-3\">\n {[\n { title: \"WhatsApp Business API 配置指南\", icon: \"💬\", page: \"/binding\" },\n { title: \"LINE 官方账号接入教程\", icon: \"🟢\", page: \"/binding\" },\n { title: \"企业邮箱 SMTP/IMAP 配置\", icon: \"📧\", page: \"/binding\" },\n { title: \"代理IP配置与轮换策略\", icon: \"🌐\", page: \"/proxies\" },\n { title: \"AI模型选择与配置\", icon: \"🤖\", page: \"/ai\" },\n { title: \"指纹浏览器集成指南\", icon: \"🔐\", page: \"/fingerprint\" },\n { title: \"知识库构建与训练\", icon: \"📖\", page: \"/knowledge\" },\n { title: \"健康监控与告警配置\", icon: \"📊\", page: \"/monitoring\" },\n ].map((item, idx) => (\n <a key={idx} href={item.page} className=\"flex items-center gap-3 p-3 bg-white rounded-lg border border-gray-100 hover:border-blue-200 hover:bg-blue-50/30 transition-colors\">\n <span className=\"text-xl\">{item.icon}</span>\n <span className=\"text-sm text-gray-700\">{item.title}</span>\n </a>\n ))}\n </div>\n </div>\n\n {/* 视频教程入口 */}\n <div className=\"bg-gradient-to-r from-blue-500 to-purple-600 rounded-xl p-6 text-white\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h3 className=\"font-semibold text-lg mb-1\">视频教程中心</h3>\n <p className=\"text-blue-100 text-sm\">观看完整操作视频,手把手教学</p>\n </div>\n <button className=\"px-4 py-2 bg-white text-blue-600 rounded-lg text-sm font-medium hover:bg-blue-50 flex items-center gap-2\">\n <Book className=\"w-4 h-4\" />\n 观看视频\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* ───── 项目设置 ───── */}\n {activeMenu === \"项目设置\" && (\n <div className=\"max-w-2xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">项目设置</h2>\n <p className=\"text-sm text-gray-500\">管理你的客服系统基本配置</p>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 divide-y divide-gray-100\">\n <div className=\"p-5\">\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">项目名称</label>\n <input value={projectName} onChange={e => setProjectName(e.target.value)} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-blue-400\" />\n </div>\n <div className=\"p-5\">\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">时区</label>\n <select value={timezone} onChange={e => setTimezone(e.target.value)} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none\">\n <option value=\"Asia/Shanghai\">Asia/Shanghai (UTC+8)</option>\n <option value=\"Asia/Hong_Kong\">Asia/Hong_Kong (UTC+8)</option>\n <option value=\"UTC\">UTC</option>\n </select>\n </div>\n <div className=\"p-5\">\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">系统语言</label>\n <select value={language} onChange={e => setLanguage(e.target.value)} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none\">\n <option value=\"zh-CN\">简体中文</option>\n <option value=\"zh-TW\">繁体中文</option>\n <option value=\"en-US\">English</option>\n </select>\n </div>\n <div className=\"p-5 flex justify-end\">\n <button onClick={() => toast(\"项目设置已保存\")} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存更改</button>\n </div>\n </div>\n\n {/* AI 模型配置 */}\n <div>\n <h3 className=\"text-base font-semibold text-gray-900 mb-3\">AI 模型配置</h3>\n <div className=\"bg-white rounded-xl border border-gray-200 divide-y divide-gray-100\">\n {aiModels.map(m => (\n <div key={m.name} className=\"p-4 flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium text-gray-900\">{m.name}</span>\n {m.badge && <span className=\"text-xs px-1.5 py-0.5 bg-blue-50 text-blue-600 rounded\">{m.badge}</span>}\n </div>\n <div className=\"text-xs text-gray-400 mt-0.5\">{m.provider} · {m.type}</div>\n </div>\n <div className=\"flex items-center gap-3\">\n <span className={`text-xs ${m.configured ? \"text-green-600\" : \"text-gray-400\"}`}>{m.status}</span>\n <button onClick={() => handleEditAIModel(m)} className=\"text-xs text-blue-600 border border-blue-200 rounded-lg px-2.5 py-1 hover:bg-blue-50\">配置 API Key</button>\n </div>\n </div>\n ))}\n <div className=\"p-4\">\n <button onClick={() => setShowAddModelModal(true)} className=\"text-sm text-blue-600 hover:underline flex items-center gap-1\">\n <Plus className=\"w-4 h-4\" /> 添加模型\n </button>\n </div>\n </div>\n </div>\n </div>\n )}\n\n {/* ───── 成员管理 ───── */}\n {activeMenu === \"成员管理\" && (\n <div className=\"max-w-3xl space-y-4\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900\">成员管理</h2>\n <p className=\"text-sm text-gray-500 mt-0.5\">管理团队成员和权限</p>\n </div>\n <button onClick={() => setShowInviteModal(true)} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2\">\n <Plus className=\"w-4 h-4\" /> 邀请成员\n </button>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <table className=\"w-full\">\n <thead className=\"bg-gray-50\">\n <tr>{[\"成员\", \"角色\", \"状态\", \"加入时间\", \"操作\"].map(h => <th key={h} className=\"px-4 py-3 text-left text-xs font-medium text-gray-500\">{h}</th>)}</tr>\n </thead>\n <tbody className=\"divide-y divide-gray-100\">\n {memberList.map((m, i) => (\n <tr key={i} className=\"hover:bg-gray-50\">\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-sm font-medium text-blue-700\">{m.name[0]}</div>\n <div>\n <div className=\"text-sm font-medium text-gray-900\">{m.name}</div>\n <div className=\"text-xs text-gray-400\">{m.email}</div>\n </div>\n </div>\n </td>\n <td className=\"px-4 py-3\">\n <span className={`text-xs px-2 py-1 rounded-full ${m.role === \"管理员\" ? \"bg-red-50 text-red-600\" : m.role === \"主管\" ? \"bg-purple-50 text-purple-600\" : \"bg-gray-100 text-gray-600\"}`}>{m.role}</span>\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-1\">\n <span className={`w-2 h-2 rounded-full ${m.status === \"在线\" ? \"bg-green-500\" : m.status === \"忙碌\" ? \"bg-yellow-500\" : \"bg-gray-300\"}`}></span>\n <span className=\"text-xs text-gray-500\">{m.status}</span>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm text-gray-400\">{m.joined}</td>\n <td className=\"px-4 py-3\">\n <button onClick={() => { setEditingMember({ ...m }); setShowEditMemberModal(true); }} className=\"text-xs text-blue-600 hover:underline mr-2\">编辑</button>\n <button onClick={() => { setDeletingMember(m); setShowDeleteMemberConfirm(true); }} className=\"text-xs text-red-400 hover:underline\">移除</button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n )}\n\n {/* ───── 权限配置 ───── */}\n {activeMenu === \"权限配置\" && (\n <div className=\"max-w-4xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">权限配置</h2>\n <p className=\"text-sm text-gray-500\">管理角色权限和访问控制</p>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 p-4\">\n <div className=\"flex gap-2\">\n {roles.map(role => (\n <button key={role.id} onClick={() => setSelectedRole(role.id)}\n className={`flex-1 p-3 rounded-lg border text-left transition-colors ${selectedRole === role.id ? \"border-blue-500 bg-blue-50\" : \"border-gray-200 hover:bg-gray-50\"}`}>\n <div className=\"flex items-center gap-2 mb-1\">\n <span className={`text-xs px-2 py-0.5 rounded-full ${role.color}`}>{role.name}</span>\n </div>\n <div className=\"text-xs text-gray-500\">{role.description}</div>\n </button>\n ))}\n </div>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <div className=\"px-4 py-3 bg-gray-50 border-b border-gray-200 flex items-center justify-between\">\n <h3 className=\"font-medium text-gray-900\">权限矩阵</h3>\n <span className=\"text-xs text-gray-400\">已勾选 {rolePermissions[selectedRole]?.length ?? 0} 项权限</span>\n </div>\n <table className=\"w-full\">\n <thead className=\"bg-gray-50\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-gray-500 w-32\">功能模块</th>\n {permissionModules[0]?.permissions.map(p => <th key={p} className=\"px-4 py-3 text-center text-xs font-medium text-gray-500\">{p}</th>)}\n </tr>\n </thead>\n <tbody className=\"divide-y divide-gray-100\">\n {permissionModules.map(module => (\n <tr key={module.id}>\n <td className=\"px-4 py-3 text-sm font-medium text-gray-900\">{module.name}</td>\n {module.permissions.map(permission => {\n const key = `${module.id}:${permission}`;\n const has = rolePermissions[selectedRole]?.includes(key);\n return (\n <td key={permission} className=\"px-4 py-3 text-center\">\n <button onClick={() => togglePermission(module.id, permission)}\n className={`w-6 h-6 rounded flex items-center justify-center mx-auto transition-colors ${has ? \"bg-blue-500 text-white\" : \"bg-gray-100 text-gray-300 hover:bg-gray-200\"}`}>\n {has ? <Check className=\"w-4 h-4\" /> : <X className=\"w-4 h-4\" />}\n </button>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <div className=\"flex justify-end gap-3\">\n <button onClick={() => toast(\"权限已重置为默认\", \"info\")} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">重置</button>\n <button onClick={() => toast(\"权限配置已保存\")} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存权限配置</button>\n </div>\n </div>\n )}\n\n {/* ───── 通知设置 ───── */}\n {activeMenu === \"通知设置\" && (\n <div className=\"max-w-3xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">通知设置</h2>\n <p className=\"text-sm text-gray-500\">配置邮件、短信和推送通知</p>\n </div>\n\n {/* 渠道开关 + 绑定地址 */}\n <div className=\"grid grid-cols-3 gap-4\">\n {/* 邮件 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Mail className=\"w-5 h-5 text-blue-600\" />\n <span className=\"font-medium text-gray-900\">邮件通知</span>\n </div>\n <button onClick={() => setNotifyEmailEnabled(v => !v)}\n className={`w-10 h-6 rounded-full relative transition-colors ${notifyEmailEnabled ? \"bg-blue-500\" : \"bg-gray-300\"}`}>\n <span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all ${notifyEmailEnabled ? \"left-[18px]\" : \"left-0.5\"}`} />\n </button>\n </div>\n <div>\n <label className=\"text-xs text-gray-500 mb-1 block\">接收邮箱</label>\n <input value={notifyEmail} onChange={e => setNotifyEmail(e.target.value)} disabled={!notifyEmailEnabled}\n className=\"w-full px-2 py-1.5 border border-gray-200 rounded-lg text-xs focus:outline-none focus:border-blue-400 disabled:bg-gray-50 disabled:text-gray-400\" />\n </div>\n </div>\n {/* 短信 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Smartphone className=\"w-5 h-5 text-green-600\" />\n <span className=\"font-medium text-gray-900\">短信通知</span>\n </div>\n <button onClick={() => setNotifySmsEnabled(v => !v)}\n className={`w-10 h-6 rounded-full relative transition-colors ${notifySmsEnabled ? \"bg-blue-500\" : \"bg-gray-300\"}`}>\n <span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all ${notifySmsEnabled ? \"left-[18px]\" : \"left-0.5\"}`} />\n </button>\n </div>\n <div>\n <label className=\"text-xs text-gray-500 mb-1 block\">接收手机</label>\n <input value={notifyPhone} onChange={e => setNotifyPhone(e.target.value)} disabled={!notifySmsEnabled}\n className=\"w-full px-2 py-1.5 border border-gray-200 rounded-lg text-xs focus:outline-none focus:border-blue-400 disabled:bg-gray-50 disabled:text-gray-400\" />\n </div>\n </div>\n {/* 推送 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-4 space-y-3\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Bell className=\"w-5 h-5 text-purple-600\" />\n <span className=\"font-medium text-gray-900\">推送通知</span>\n </div>\n <button onClick={() => setNotifyPushEnabled(v => !v)}\n className={`w-10 h-6 rounded-full relative transition-colors ${notifyPushEnabled ? \"bg-blue-500\" : \"bg-gray-300\"}`}>\n <span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-all ${notifyPushEnabled ? \"left-[18px]\" : \"left-0.5\"}`} />\n </button>\n </div>\n <p className=\"text-xs text-gray-400\">浏览器 / APP 推送通知,无需额外配置</p>\n </div>\n </div>\n\n {/* 通知类型 */}\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <div className=\"px-4 py-3 bg-gray-50 border-b border-gray-200\">\n <h3 className=\"font-medium text-gray-900\">通知类型配置</h3>\n </div>\n <table className=\"w-full\">\n <thead className=\"bg-gray-50\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-gray-500\">通知类型</th>\n <th className=\"px-4 py-3 text-center text-xs font-medium text-gray-500\">邮件</th>\n <th className=\"px-4 py-3 text-center text-xs font-medium text-gray-500\">推送</th>\n <th className=\"px-4 py-3 text-center text-xs font-medium text-gray-500\">短信</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-gray-100\">\n {notifications.map(n => (\n <tr key={n.id}>\n <td className=\"px-4 py-3\">\n <div className=\"text-sm font-medium text-gray-900\">{n.name}</div>\n <div className=\"text-xs text-gray-500\">{n.description}</div>\n </td>\n {([\"email\", \"push\", \"sms\"] as const).map(type => (\n <td key={type} className=\"px-4 py-3 text-center\">\n <input type=\"checkbox\" checked={n[type]} onChange={() => toggleNotification(n.id, type)} className=\"rounded text-blue-600\" />\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <div className=\"flex justify-end\">\n <button onClick={() => toast(\"通知设置已保存\")} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存通知设置</button>\n </div>\n </div>\n )}\n\n {/* ───── 渠道配置 ───── */}\n {activeMenu === \"渠道配置\" && (\n <div className=\"max-w-3xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">渠道配置</h2>\n <p className=\"text-sm text-gray-500\">管理各渠道接入、绑定店铺和 Webhook 设置</p>\n </div>\n\n {/* Webhook 基础配置 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2\">\n <Webhook className=\"w-4 h-4\" /> Webhook 基础配置\n </h3>\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">Webhook URL</label>\n <div className=\"flex gap-2\">\n <input value=\"https://api.example.com/webhook\" readOnly className=\"flex-1 px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm\" />\n <button onClick={() => copyKey(\"https://api.example.com/webhook\")} className=\"px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50\"><Copy className=\"w-4 h-4\" /></button>\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">Webhook Secret</label>\n <div className=\"flex gap-2\">\n <input type=\"password\" value=\"whsec_xxxxxxxxxxxxxxxx\" readOnly className=\"flex-1 px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm\" />\n <button onClick={() => toast(\"Secret 已重置\")} className=\"px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50\"><RefreshCw className=\"w-4 h-4\" /></button>\n </div>\n </div>\n </div>\n </div>\n\n {/* 渠道列表 */}\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <div className=\"px-4 py-3 bg-gray-50 border-b border-gray-200 flex items-center justify-between\">\n <h3 className=\"font-medium text-gray-900\">渠道列表</h3>\n <span className=\"text-xs text-gray-400\">{channels.filter(c => c.status === \"已连接\").length}/{channels.length} 已连接</span>\n </div>\n <div className=\"divide-y divide-gray-100\">\n {channels.map(channel => (\n <div key={channel.id} className=\"p-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <span className=\"text-2xl\">{channel.icon}</span>\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium text-gray-900\">{channel.name}</span>\n <span className={`text-xs px-1.5 py-0.5 rounded-full ${channel.status === \"已连接\" ? \"bg-green-100 text-green-700\" : \"bg-gray-100 text-gray-500\"}`}>{channel.status}</span>\n </div>\n <div className=\"text-xs text-gray-400 mt-0.5\">{channel.type}{channel.bindStore ? ` · 绑定店铺: ${channel.bindStore}` : \"\"}</div>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {/* 启用/禁用开关 */}\n <button onClick={() => toggleChannel(channel.id)}\n className={`w-9 h-5 rounded-full relative transition-colors ${channel.enabled ? \"bg-blue-500\" : \"bg-gray-300\"}`}>\n <span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-all ${channel.enabled ? \"left-[18px]\" : \"left-0.5\"}`} />\n </button>\n {/* Webhook 测试 */}\n {channel.status === \"已连接\" && (\n <button\n onClick={() => testWebhook(channel.id)}\n className={`text-xs px-2 py-1 rounded border flex items-center gap-1 transition-colors ${\n webhookTestResult[channel.id] === \"testing\" ? \"border-yellow-300 text-yellow-600 bg-yellow-50\" :\n webhookTestResult[channel.id] === \"ok\" ? \"border-green-300 text-green-600 bg-green-50\" :\n webhookTestResult[channel.id] === \"fail\" ? \"border-red-300 text-red-600 bg-red-50\" :\n \"border-gray-200 text-gray-600 hover:bg-gray-50\"\n }`}\n >\n <TestTube className=\"w-3 h-3\" />\n {webhookTestResult[channel.id] === \"testing\" ? \"测试中...\" : webhookTestResult[channel.id] === \"ok\" ? \"正常\" : webhookTestResult[channel.id] === \"fail\" ? \"失败\" : \"测试\"}\n </button>\n )}\n {/* 配置按钮 */}\n <button onClick={() => openChannelEdit(channel)} className=\"text-xs text-blue-600 border border-blue-200 rounded-lg px-2.5 py-1 hover:bg-blue-50 flex items-center gap-1\">\n <Edit2 className=\"w-3 h-3\" /> 配置\n </button>\n </div>\n </div>\n {/* 绑定店铺快捷信息 */}\n {channel.appId && (\n <div className=\"mt-2 ml-11 flex items-center gap-4 text-xs text-gray-400\">\n <span className=\"flex items-center gap-1\"><Link className=\"w-3 h-3\" /> App ID: {channel.appId.substring(0, 12)}{channel.appId.length > 12 ? \"...\" : \"\"}</span>\n {channel.bindStore && <span className=\"flex items-center gap-1\"><Store className=\"w-3 h-3\" /> {channel.bindStore}</span>}\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n </div>\n )}\n\n {/* ───── 界面定制 ───── */}\n {activeMenu === \"界面定制\" && (\n <div className=\"max-w-2xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">界面定制</h2>\n <p className=\"text-sm text-gray-500\">自定义主题、Logo和品牌颜色</p>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4\">主题模式</h3>\n <div className=\"grid grid-cols-3 gap-4\">\n {[{ key: \"light\", icon: <Sun className=\"w-8 h-8 mx-auto mb-2 text-orange-500\" />, label: \"浅色模式\", desc: \"默认明亮主题\" },\n { key: \"dark\", icon: <Moon className=\"w-8 h-8 mx-auto mb-2 text-indigo-500\" />, label: \"深色模式\", desc: \"护眼暗色主题\" },\n { key: \"auto\", icon: <Monitor className=\"w-8 h-8 mx-auto mb-2 text-blue-500\" />, label: \"跟随系统\", desc: \"自动切换\" }].map(t => (\n <button key={t.key} onClick={() => setTheme(t.key)}\n className={`p-4 rounded-xl border text-center transition-colors ${theme === t.key ? \"border-blue-500 bg-blue-50\" : \"border-gray-200 hover:bg-gray-50\"}`}>\n {t.icon}\n <div className=\"font-medium text-gray-900\">{t.label}</div>\n <div className=\"text-xs text-gray-500\">{t.desc}</div>\n </button>\n ))}\n </div>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4\">品牌颜色</h3>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">主色调</label>\n <div className=\"flex gap-2\">\n {[\"#3B82F6\", \"#10B981\", \"#8B5CF6\", \"#F59E0B\", \"#EF4444\", \"#EC4899\"].map(color => (\n <button key={color} onClick={() => setPrimaryColor(color)}\n className={`w-10 h-10 rounded-lg transition-transform ${primaryColor === color ? \"ring-2 ring-offset-2 ring-gray-400 scale-110\" : \"\"}`}\n style={{ backgroundColor: color }} />\n ))}\n </div>\n </div>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4\">品牌标识</h3>\n <div className=\"grid grid-cols-2 gap-4\">\n {[{ label: \"Logo\", hint: \"建议尺寸 200x50px\" }, { label: \"Favicon\", hint: \"建议尺寸 32x32px\" }].map(item => (\n <div key={item.label}>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">{item.label}</label>\n <div className=\"border-2 border-dashed border-gray-200 rounded-xl p-6 text-center hover:bg-gray-50 cursor-pointer\" onClick={() => toast(\"文件选择功能开发中\", \"info\")}>\n <ImageIcon className=\"w-8 h-8 mx-auto mb-2 text-gray-400\" />\n <div className=\"text-sm text-gray-600\">点击上传 {item.label}</div>\n <div className=\"text-xs text-gray-400 mt-1\">{item.hint}</div>\n </div>\n </div>\n ))}\n </div>\n </div>\n <div className=\"flex justify-end\">\n <button onClick={() => toast(\"界面设置已保存\")} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存界面设置</button>\n </div>\n </div>\n )}\n\n {/* ───── 数据管理 ───── */}\n {activeMenu === \"数据管理\" && (\n <div className=\"max-w-3xl space-y-6\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">数据管理</h2>\n <p className=\"text-sm text-gray-500\">数据导出、备份和清理</p>\n </div>\n\n {/* 数据导出 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2\"><Download className=\"w-4 h-4\" /> 数据导出</h3>\n <div className=\"space-y-3\">\n {[\n { name: \"客户数据\", desc: \"导出所有客户信息(姓名、联系方式、标签)\", format: \"CSV, Excel\" },\n { name: \"会话记录\", desc: \"导出历史会话数据(含会话摘要)\", format: \"JSON, CSV\" },\n { name: \"消息记录\", desc: \"导出所有消息内容(含附件链接)\", format: \"JSON\" },\n { name: \"报告数据\", desc: \"导出统计报告(含图表数据)\", format: \"Excel, PDF\" },\n ].map(item => (\n <div key={item.name} className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className=\"font-medium text-gray-900\">{item.name}</div>\n <div className=\"text-xs text-gray-500\">{item.desc}</div>\n </div>\n <div className=\"flex items-center gap-3\">\n <span className=\"text-xs text-gray-400\">{item.format}</span>\n <button onClick={() => handleExport(item.name, item.format)} className=\"px-3 py-1.5 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 flex items-center gap-1\">\n <Download className=\"w-3.5 h-3.5\" /> 导出\n </button>\n </div>\n </div>\n ))}\n </div>\n </div>\n\n {/* 数据备份 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2\"><Database className=\"w-4 h-4\" /> 数据备份</h3>\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className=\"font-medium text-gray-900\">自动备份</div>\n <div className=\"text-xs text-gray-500\">每天凌晨 2:00 自动备份</div>\n </div>\n <label className=\"relative inline-flex items-center cursor-pointer\">\n <input type=\"checkbox\" defaultChecked className=\"sr-only peer\" />\n <div className=\"w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600\"></div>\n </label>\n </div>\n <div className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className=\"font-medium text-gray-900\">备份保留期</div>\n <div className=\"text-xs text-gray-500\">超过保留期的备份将自动删除</div>\n </div>\n <select value={exportRetentionDays} onChange={e => setExportRetentionDays(e.target.value)} className=\"px-3 py-1.5 border border-gray-200 rounded text-sm\">\n <option>7 天</option>\n <option>30 天</option>\n <option>90 天</option>\n <option>永久</option>\n </select>\n </div>\n <div className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className=\"font-medium text-gray-900\">上次备份时间</div>\n <div className=\"text-xs text-gray-500\">2026-04-01 02:00:00 · 共 128 MB</div>\n </div>\n <button onClick={() => toast(\"正在创建备份...\")} className=\"px-3 py-1.5 bg-white border border-gray-200 rounded text-sm hover:bg-gray-50 flex items-center gap-1\">\n <RefreshCw className=\"w-3.5 h-3.5\" /> 立即备份\n </button>\n </div>\n </div>\n </div>\n\n {/* 数据清理 */}\n <div className=\"bg-white rounded-xl border border-gray-200 p-5\">\n <h3 className=\"font-medium text-gray-900 mb-4 flex items-center gap-2 text-red-600\"><Trash className=\"w-4 h-4\" /> 数据清理</h3>\n <div className=\"space-y-3\">\n {[\n { name: \"清理已删除客户\", desc: \"永久删除已标记删除的客户数据,不可恢复\", danger: true },\n { name: \"清理过期会话\", desc: \"删除超过 1 年的已结束会话记录\", danger: true },\n { name: \"清理系统日志\", desc: \"删除 90 天前的系统运行日志\", danger: false },\n ].map(item => (\n <div key={item.name} className=\"flex items-center justify-between p-3 bg-gray-50 rounded-lg\">\n <div>\n <div className={`font-medium ${item.danger ? \"text-red-600\" : \"text-gray-900\"}`}>{item.name}</div>\n <div className=\"text-xs text-gray-500\">{item.desc}</div>\n </div>\n <button onClick={() => handleClean(item.name, item.desc)}\n className={`px-3 py-1.5 rounded text-sm flex items-center gap-1 ${item.danger ? \"bg-red-50 text-red-600 hover:bg-red-100\" : \"bg-white border border-gray-200 hover:bg-gray-50\"}`}>\n {item.danger && <AlertTriangle className=\"w-3.5 h-3.5\" />} 执行\n </button>\n </div>\n ))}\n </div>\n </div>\n </div>\n )}\n\n {/* ───── API 密钥 ───── */}\n {activeMenu === \"API密钥\" && (\n <div className=\"max-w-3xl space-y-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold text-gray-900 mb-1\">API密钥</h2>\n <p className=\"text-sm text-gray-500\">管理API访问密钥和权限</p>\n </div>\n <button onClick={generateNewKey} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2\">\n <Plus className=\"w-4 h-4\" /> 新建密钥\n </button>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 overflow-hidden\">\n <table className=\"w-full\">\n <thead className=\"bg-gray-50\">\n <tr>{[\"名称\", \"密钥\", \"创建时间\", \"最后使用\", \"状态\", \"操作\"].map(h => <th key={h} className=\"px-4 py-3 text-left text-xs font-medium text-gray-500\">{h}</th>)}</tr>\n </thead>\n <tbody className=\"divide-y divide-gray-100\">\n {keys.map(k => (\n <tr key={k.id}>\n <td className=\"px-4 py-3 text-sm font-medium text-gray-900\">{k.name}</td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <code className=\"text-xs bg-gray-100 px-2 py-1 rounded\">{showKeyId === k.id ? k.key : `${k.key.substring(0, 8)}...`}</code>\n <button onClick={() => setShowKeyId(showKeyId === k.id ? null : k.id)} className=\"text-gray-400 hover:text-gray-600\">\n {showKeyId === k.id ? <EyeOff className=\"w-4 h-4\" /> : <Eye className=\"w-4 h-4\" />}\n </button>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm text-gray-500\">{k.createdAt}</td>\n <td className=\"px-4 py-3 text-sm text-gray-500\">{k.lastUsed}</td>\n <td className=\"px-4 py-3\">\n <span className={`text-xs px-2 py-1 rounded-full ${k.status === \"active\" ? \"bg-green-100 text-green-700\" : \"bg-gray-100 text-gray-600\"}`}>\n {k.status === \"active\" ? \"活跃\" : \"已禁用\"}\n </span>\n </td>\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <button onClick={() => copyKey(k.key)} className=\"text-gray-400 hover:text-gray-600\" title=\"复制\"><Copy className=\"w-4 h-4\" /></button>\n <button onClick={() => deleteKey(k.id)} className=\"text-gray-400 hover:text-red-600\" title=\"删除\"><Trash2 className=\"w-4 h-4\" /></button>\n </div>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n <div className=\"bg-blue-50 rounded-xl border border-blue-100 p-5\">\n <h3 className=\"font-medium text-blue-900 mb-2\">API 文档</h3>\n <p className=\"text-sm text-blue-700 mb-3\">查看完整的 API 文档和示例代码</p>\n <button className=\"text-sm text-blue-600 hover:underline\">查看文档 →</button>\n </div>\n </div>\n )}\n\n </div>{/* end Settings Content */}\n </div>{/* end flex h-screen */}\n\n {/* ═══════════════ 全局 Toast ═══════════════ */}\n {showToast && <Toast msg={toastMsg} type={toastType} onClose={() => setShowToast(false)} />}\n\n {/* ═══════════════ 邀请成员弹窗 ═══════════════ */}\n {showInviteModal && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-2xl w-[440px] p-6 shadow-2xl\">\n <div className=\"flex items-center justify-between mb-5\">\n <h3 className=\"text-lg font-semibold text-gray-900\">邀请成员</h3>\n <button onClick={() => setShowInviteModal(false)} className=\"text-gray-400 hover:text-gray-600\"><X className=\"w-5 h-5\" /></button>\n </div>\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">邮箱地址</label>\n <input type=\"email\" value={inviteEmail} onChange={e => setInviteEmail(e.target.value)} placeholder=\"colleague@company.com\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">角色</label>\n <select value={inviteRole} onChange={e => setInviteRole(e.target.value)} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>管理员</option><option>主管</option><option>客服</option><option>只读</option>\n </select>\n </div>\n <p className=\"text-xs text-gray-400\">邀请邮件将发送至上述地址,对方接受后成为团队成员。</p>\n </div>\n <div className=\"flex justify-end gap-2 mt-5\">\n <button onClick={() => setShowInviteModal(false)} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={() => { if (!inviteEmail.trim()) return; setShowInviteModal(false); setInviteEmail(\"\"); toast(`邀请已发送至 ${inviteEmail}`); }}\n disabled={!inviteEmail.trim()}\n className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50\">发送邀请</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 编辑成员弹窗 ═══════════════ */}\n {showEditMemberModal && editingMember && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl w-[440px] p-6 shadow-2xl\">\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"text-lg font-semibold\">编辑成员</h3>\n <button onClick={() => { setShowEditMemberModal(false); setEditingMember(null); }} className=\"text-gray-400 hover:text-gray-600\"><X className=\"w-5 h-5\" /></button>\n </div>\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">姓名</label>\n <input value={editingMember.name} onChange={e => setEditingMember({ ...editingMember, name: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">邮箱</label>\n <input type=\"email\" value={editingMember.email} onChange={e => setEditingMember({ ...editingMember, email: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">角色</label>\n <select value={editingMember.role} onChange={e => setEditingMember({ ...editingMember, role: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>管理员</option><option>主管</option><option>客服</option><option>只读</option>\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">状态</label>\n <select value={editingMember.status} onChange={e => setEditingMember({ ...editingMember, status: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>在线</option><option>忙碌</option><option>离线</option>\n </select>\n </div>\n </div>\n </div>\n <div className=\"flex justify-end gap-2 mt-6\">\n <button onClick={() => { setShowEditMemberModal(false); setEditingMember(null); }} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={() => {\n if (!editingMember.name.trim()) return;\n setMemberList(prev => prev.map(m => m.email === editingMember.email ? editingMember : m));\n setShowEditMemberModal(false); setEditingMember(null); toast(\"成员信息已更新\");\n }} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存修改</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 删除成员确认 ═══════════════ */}\n {showDeleteMemberConfirm && deletingMember && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl w-[400px] p-6 shadow-2xl\">\n <h3 className=\"text-lg font-semibold mb-2\">移除成员</h3>\n <p className=\"text-sm text-gray-600 mb-6\">确定要移除成员 <span className=\"font-medium text-gray-900\">\"{deletingMember.name}\"</span> 吗?移除后该成员将无法访问系统。</p>\n <div className=\"flex justify-end gap-2\">\n <button onClick={() => { setShowDeleteMemberConfirm(false); setDeletingMember(null); }} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={() => {\n setMemberList(prev => prev.filter(m => m.email !== deletingMember.email));\n setShowDeleteMemberConfirm(false); setDeletingMember(null); toast(\"成员已移除\");\n }} className=\"px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700\">确认移除</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 渠道配置弹窗 ═══════════════ */}\n {showChannelModal && editingChannel && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-2xl w-full max-w-lg mx-4 shadow-2xl\">\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-gray-200\">\n <div className=\"flex items-center gap-3\">\n <span className=\"text-2xl\">{editingChannel.icon}</span>\n <div>\n <div className=\"font-semibold text-gray-900\">{editingChannel.name} 配置</div>\n <div className=\"text-xs text-gray-400\">{editingChannel.type}</div>\n </div>\n </div>\n <button onClick={() => { setShowChannelModal(false); setEditingChannel(null); }} className=\"p-1 hover:bg-gray-100 rounded-lg\"><X className=\"w-5 h-5 text-gray-400\" /></button>\n </div>\n <div className=\"p-6 space-y-4\">\n {/* App ID */}\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">\n {editingChannel.type === \"邮件\" ? \"发件邮箱\" : \"App ID / Bot Token\"}\n </label>\n <input value={editingChannel.appId} onChange={e => setEditingChannel({ ...editingChannel, appId: e.target.value })}\n placeholder={editingChannel.type === \"邮件\" ? \"support@yourcompany.com\" : \"输入 App ID 或 Bot Token\"}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n {/* App Secret */}\n {editingChannel.type !== \"邮件\" && (\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">App Secret</label>\n <div className=\"relative\">\n <input type={channelSecretVisible[editingChannel.id] ? \"text\" : \"password\"}\n value={editingChannel.appSecret}\n onChange={e => setEditingChannel({ ...editingChannel, appSecret: e.target.value })}\n placeholder=\"输入 App Secret\"\n className=\"w-full px-3 py-2 pr-10 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n <button type=\"button\" onClick={() => setChannelSecretVisible(prev => ({ ...prev, [editingChannel.id]: !prev[editingChannel.id] }))}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600\">\n {channelSecretVisible[editingChannel.id] ? <EyeOff className=\"w-4 h-4\" /> : <Eye className=\"w-4 h-4\" />}\n </button>\n </div>\n </div>\n )}\n {/* 绑定店铺 */}\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5 flex items-center gap-1\">\n <Store className=\"w-3.5 h-3.5\" /> 绑定店铺\n </label>\n <select value={editingChannel.bindStore} onChange={e => setEditingChannel({ ...editingChannel, bindStore: e.target.value })}\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option value=\"\">-- 不绑定店铺 --</option>\n <option value=\"主店铺\">主店铺</option>\n <option value=\"跨境店\">跨境店</option>\n <option value=\"批发渠道\">批发渠道</option>\n <option value=\"代理商\">代理商渠道</option>\n </select>\n <p className=\"text-xs text-gray-400 mt-1\">绑定后,该渠道的会话将关联到对应店铺的客户数据</p>\n </div>\n {/* Webhook URL */}\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">Webhook 回调地址</label>\n <div className=\"flex gap-2\">\n <input value={editingChannel.webhook} onChange={e => setEditingChannel({ ...editingChannel, webhook: e.target.value })}\n placeholder=\"https://api.example.com/webhook/...\"\n className=\"flex-1 px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n <button onClick={() => testWebhook(editingChannel.id)} className=\"px-3 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 text-sm flex items-center gap-1 whitespace-nowrap\">\n <TestTube className=\"w-3.5 h-3.5\" /> 测试\n </button>\n </div>\n {webhookTestResult[editingChannel.id] === \"ok\" && <p className=\"text-xs text-green-600 mt-1\">✓ Webhook 连通性正常</p>}\n {webhookTestResult[editingChannel.id] === \"fail\" && <p className=\"text-xs text-red-500 mt-1\">✗ Webhook 测试失败,请检查地址</p>}\n </div>\n </div>\n <div className=\"flex items-center justify-between px-6 py-4 border-t border-gray-200\">\n <span className=\"text-xs text-gray-400\">配置保存后自动更新连接状态</span>\n <div className=\"flex gap-3\">\n <button onClick={() => { setShowChannelModal(false); setEditingChannel(null); }} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm text-gray-600 hover:bg-gray-50\">取消</button>\n <button onClick={saveChannel} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">保存配置</button>\n </div>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ AI 模型配置弹窗 ═══════════════ */}\n {showAIConfigModal && editingAIModel && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-2xl w-full max-w-md mx-4 shadow-2xl\">\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-gray-200\">\n <div>\n <div className=\"font-semibold text-gray-900\">配置 {editingAIModel.name}</div>\n <div className=\"text-xs text-gray-400 mt-0.5\">{editingAIModel.provider} · {editingAIModel.type}</div>\n </div>\n <button onClick={() => { setShowAIConfigModal(false); setEditingAIModel(null); }} className=\"p-1 hover:bg-gray-100 rounded-lg\"><X className=\"w-5 h-5 text-gray-400\" /></button>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">API Key</label>\n <div className=\"relative\">\n <input type={showAIKeyValue[editingAIModel.name] ? \"text\" : \"password\"}\n value={editingAIModel.apiKey}\n onChange={e => setEditingAIModel({ ...editingAIModel, apiKey: e.target.value })}\n placeholder=\"请输入 API Key(如 sk-xxxxxxxxxxxxxxxx)\"\n className=\"w-full px-3 py-2 pr-10 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n <button type=\"button\" onClick={() => setShowAIKeyValue(prev => ({ ...prev, [editingAIModel.name]: !prev[editingAIModel.name] }))}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600\">\n {showAIKeyValue[editingAIModel.name] ? <EyeOff className=\"w-4 h-4\" /> : <Eye className=\"w-4 h-4\" />}\n </button>\n </div>\n <p className=\"text-xs text-gray-400 mt-1\">API Key 将加密保存,不会在页面中明文展示</p>\n </div>\n <div className=\"bg-blue-50 rounded-lg p-3 text-xs text-blue-700\">\n 💡 获取 API Key:前往 {editingAIModel.provider} 官网 → 控制台 → API 密钥管理\n </div>\n </div>\n <div className=\"flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200\">\n <button onClick={() => { setShowAIConfigModal(false); setEditingAIModel(null); }} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm text-gray-600 hover:bg-gray-50\">取消</button>\n <button onClick={handleSaveAIModel} disabled={!editingAIModel.apiKey.trim()} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\">保存配置</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 添加模型弹窗 ═══════════════ */}\n {showAddModelModal && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-2xl w-full max-w-md mx-4 shadow-2xl\">\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-gray-200\">\n <span className=\"font-semibold text-gray-900\">添加 AI 模型</span>\n <button onClick={() => setShowAddModelModal(false)} className=\"p-1 hover:bg-gray-100 rounded-lg\"><X className=\"w-5 h-5 text-gray-400\" /></button>\n </div>\n <div className=\"p-6 space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">模型名称</label>\n <input type=\"text\" value={newModel.name} onChange={e => setNewModel({ ...newModel, name: e.target.value })} placeholder=\"例如:GPT-4o、Kimi-k2...\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">提供商</label>\n <select value={newModel.provider} onChange={e => setNewModel({ ...newModel, provider: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>DeepSeek</option><option>阿里云</option><option>智谱AI</option><option>月之暗面</option><option>百度文心</option><option>OpenAI</option><option>Anthropic</option><option>其他</option>\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">模型类型</label>\n <select value={newModel.type} onChange={e => setNewModel({ ...newModel, type: e.target.value })} className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\">\n <option>对话模型</option><option>推理模型</option><option>快速模型</option><option>多模态模型</option>\n </select>\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1.5\">API Key</label>\n <input type=\"password\" value={newModel.apiKey} onChange={e => setNewModel({ ...newModel, apiKey: e.target.value })} placeholder=\"请输入 API Key\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500\" />\n </div>\n </div>\n <div className=\"flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200\">\n <button onClick={() => setShowAddModelModal(false)} className=\"px-4 py-2 border border-gray-200 rounded-lg text-sm text-gray-600 hover:bg-gray-50\">取消</button>\n <button onClick={handleSaveNewModel} disabled={!newModel.name.trim() || !newModel.apiKey.trim()} className=\"px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\">添加</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 新建 API 密钥弹窗 ═══════════════ */}\n {showNewKeyModal && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl p-6 w-full max-w-md shadow-2xl\">\n <div className=\"flex items-center gap-3 mb-4\">\n <div className=\"w-10 h-10 bg-green-100 rounded-full flex items-center justify-center\">\n <CheckCircle2 className=\"w-5 h-5 text-green-600\" />\n </div>\n <div>\n <h3 className=\"font-semibold text-gray-900\">密钥已生成</h3>\n <p className=\"text-sm text-gray-500\">请立即复制保存,此密钥仅显示一次</p>\n </div>\n </div>\n <div className=\"bg-gray-50 rounded-lg p-3 mb-4 flex items-center gap-2\">\n <code className=\"text-sm break-all flex-1\">{generatedKey}</code>\n <button onClick={() => copyKey(generatedKey)} className=\"text-gray-400 hover:text-gray-600 flex-shrink-0\"><Copy className=\"w-4 h-4\" /></button>\n </div>\n <div className=\"mb-4\">\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">密钥名称</label>\n <input value={newKeyName} onChange={e => setNewKeyName(e.target.value)} placeholder=\"例如:生产环境\"\n className=\"w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:border-blue-400\" />\n </div>\n <div className=\"flex gap-3\">\n <button onClick={() => setShowNewKeyModal(false)} className=\"flex-1 px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={saveNewKey} disabled={!newKeyName.trim()} className=\"flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50\">保存密钥</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 导出确认弹窗 ═══════════════ */}\n {showExportConfirm && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl p-6 w-[420px] shadow-2xl\">\n <div className=\"flex items-center gap-3 mb-4\">\n <div className=\"w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center\">\n <Download className=\"w-5 h-5 text-blue-600\" />\n </div>\n <div>\n <h3 className=\"font-semibold text-gray-900\">确认导出</h3>\n <p className=\"text-sm text-gray-500\">即将导出 <span className=\"font-medium\">{showExportConfirm.name}</span></p>\n </div>\n </div>\n <div className=\"bg-gray-50 rounded-lg p-3 mb-4 text-sm text-gray-600 space-y-1\">\n <div>格式:<span className=\"font-medium\">{showExportConfirm.format}</span></div>\n <div>导出结果将通过邮件发送,请注意查收</div>\n </div>\n <div className=\"flex gap-3\">\n <button onClick={() => setShowExportConfirm(null)} className=\"flex-1 px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={confirmExport} className=\"flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700\">确认导出</button>\n </div>\n </div>\n </div>\n )}\n\n {/* ═══════════════ 清理确认弹窗 ═══════════════ */}\n {showCleanConfirm && (\n <div className=\"fixed inset-0 bg-black/50 flex items-center justify-center z-50\">\n <div className=\"bg-white rounded-xl p-6 w-[420px] shadow-2xl\">\n <div className=\"flex items-center gap-3 mb-4\">\n <div className=\"w-10 h-10 bg-red-100 rounded-full flex items-center justify-center\">\n <AlertTriangle className=\"w-5 h-5 text-red-600\" />\n </div>\n <div>\n <h3 className=\"font-semibold text-gray-900\">{showCleanConfirm.name}</h3>\n <p className=\"text-sm text-gray-500\">此操作不可恢复,请谨慎操作</p>\n </div>\n </div>\n <p className=\"text-sm text-gray-600 mb-5 bg-red-50 rounded-lg p-3\">{showCleanConfirm.desc}</p>\n <div className=\"flex gap-3\">\n <button onClick={() => setShowCleanConfirm(null)} className=\"flex-1 px-4 py-2 border border-gray-200 rounded-lg text-sm hover:bg-gray-50\">取消</button>\n <button onClick={confirmClean} className=\"flex-1 px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700\">确认清理</button>\n </div>\n </div>\n </div>\n )}\n\n </PageLayout>\n );\n}\n"}}}}