/** * TalkingQ Platform - Agent Configuration Workbench * Pure JS component functions following the existing static SPA router. */ const AGENT_CONFIG_STEPS = [ { id: 'voice', index: 1, title: '语音交互' }, { id: 'understand', index: 2, title: '理解和生成' }, { id: 'skills', index: 3, title: '技能' }, ]; const AGENT_CONFIG_DEFAULT_APPS = [ { id: 'app-001', type: 'multimodal', name: '小Q 故事机器人', status: 'published' }, { id: 'app-002', type: 'voice', name: '英语学习助手', status: 'editing' }, { id: 'app-003', type: 'voice', name: '科学百科探索者', status: 'draft' }, { id: 'app-004', type: 'multimodal', name: '数学小课堂', status: 'published' }, ]; const AGENT_INTERACTION_LANGUAGES = [ { id: 'zh', name: '中文' }, { id: 'en', name: '英语' }, { id: 'fr', name: '法语' }, { id: 'de', name: '德语' }, { id: 'es', name: '西班牙语' }, { id: 'it', name: '意大利语' }, { id: 'ru', name: '俄语' }, { id: 'pt', name: '葡萄牙语' }, { id: 'ko', name: '韩语' }, { id: 'ja', name: '日语' }, { id: 'th', name: '泰语' }, { id: 'vi', name: '越南语' }, { id: 'id', name: '印度尼西亚语' }, { id: 'ms', name: '马来语' }, { id: 'fil', name: '菲律宾语' }, { id: 'yue', name: '粤语' }, { id: 'ar', name: '阿拉伯语' }, { id: 'hi', name: '印地语' }, { id: 'tr', name: '土耳其语' }, { id: 'uk', name: '乌克兰语' }, { id: 'cs', name: '捷克语' }, { id: 'da', name: '丹麦语' }, { id: 'fi', name: '芬兰语' }, { id: 'is', name: '冰岛语' }, { id: 'no', name: '挪威语' }, { id: 'pl', name: '波兰语' }, { id: 'nl', name: '荷兰语' }, { id: 'sv', name: '瑞典语' }, ]; const SPEECH_MODEL_STATUS_META = { validating: { label: '连接中', className: 'pending' }, available: { label: '可用', className: 'success' }, failed: { label: '连接失败', className: 'danger' }, disabled: { label: '已禁用', className: 'disabled' }, }; const ASR_MODEL_STATUS_META = SPEECH_MODEL_STATUS_META; const TTS_MODEL_STATUS_META = SPEECH_MODEL_STATUS_META; const TEXT_MODEL_STATUS_META = SPEECH_MODEL_STATUS_META; const MCP_SERVICE_STATUS_META = { pending: { label: '待连接', className: 'pending' }, validating: { label: '连接中', className: 'pending' }, available: { label: '可用', className: 'success' }, failed: { label: '连接失败', className: 'danger' }, disabled: { label: '已禁用', className: 'disabled' }, }; const TTS_AUDIO_FORMAT_OPTIONS = [ { id: 'mp3', name: 'MP3' }, { id: 'wav', name: 'WAV' }, { id: 'pcm', name: 'PCM' }, { id: 'opus', name: 'OPUS' }, { id: 'flac', name: 'FLAC' }, ]; const AGENT_CONFIG_MOCK = { voice: { defaultHotwords: ['TalkingQ', '小Q', '白雪公主', '睡前故事', '恐龙百科'], }, understand: { intentModel: { icon: 'route', label: '意图识别', desc: '关闭将不再触发调用工具/Agent 等基于语义理解的交互功能。', }, prompt: `# 角色 你是小Q,一个专为 3-10 岁儿童设计的 AI 玩具智能体。 ## 沟通风格 - 语言简单、生动、温暖。 - 回复通常控制在 150 字以内。 - 优先使用比喻、拟声词和故事化表达。 - 不能输出任何不适合儿童的危险内容。 ## 行为准则 1. 当孩子要求讲故事时,先确认主题,再生成短故事。 2. 当孩子提问百科知识时,先给出结论,再给出一个生活例子。 3. 当识别到情绪陪伴需求时,优先安抚,再引导表达。 \`\`\`policy 安全等级:儿童模式 联网搜索:必要时开启 知识库优先级:高 \`\`\``, variables: [ { id: 'var-empty-1', name: '', desc: '' }, ], knowledgeBases: [ { id: 'kb-001', name: '儿童科学百科', desc: '动物、太空、自然科学问答素材', weight: '0.72' }, { id: 'kb-002', name: '故事素材库 V2', desc: '睡前故事、角色设定、儿童互动剧情', weight: '0.68' }, ], }, skills: { commands: [], plugins: [], }, }; const AGENT_KNOWLEDGE_BASE_CATALOG = [ { id: 'kb-001', name: '儿童科学百科', description: '包含各类儿童科学知识的百科全书', docs: 2840, size: '128MB', typeName: '文档搜索', status: 'ready', tags: ['儿童', '科学'] }, { id: 'kb-002', name: '故事素材库 V2', description: '精选各类儿童故事素材', docs: 5200, size: '340MB', typeName: '文档搜索', status: 'ready', tags: ['故事', '儿童'] }, { id: 'kb-003', name: '小学数学知识点', description: '小学数学知识体系整理', docs: 1560, size: '86MB', typeName: '文档搜索', status: 'processing', tags: ['数学', '教育'] }, { id: 'kb-004', name: '英语启蒙词典', description: '儿童英语启蒙词汇', docs: 3200, size: '210MB', typeName: '图片问答', status: 'ready', tags: ['英语', '启蒙'] }, { id: 'kb-005', name: '唐诗宋词精选', description: '经典唐诗宋词收录', docs: 890, size: '45MB', typeName: '文档搜索', status: 'ready', tags: ['诗词', '文学'] }, { id: 'kb-006', name: '历史人物故事', description: '历史名人故事集', docs: 1200, size: '78MB', typeName: '音视频搜索', status: 'error', tags: ['历史', '故事'] }, { id: 'kb-007', name: '产品参数数据库', description: '公司产品技术参数', docs: 450, size: '32MB', typeName: '数据查询', status: 'ready', tags: ['产品', '技术'] }, { id: 'kb-008', name: '音乐教育资源库', description: '儿童音乐教育资源', docs: 560, size: '65MB', typeName: '音视频搜索', status: 'uploading', tags: ['音乐', '教育'] }, ]; function ensureAsrModelRegistry() { if (!Array.isArray(window.__asrModelRegistry)) { window.__asrModelRegistry = []; } window.__asrModelRegistry.forEach(normalizeSpeechModelStatus); return window.__asrModelRegistry; } function ensureTtsModelRegistry() { if (!Array.isArray(window.__ttsModelRegistry)) { window.__ttsModelRegistry = []; } window.__ttsModelRegistry.forEach(normalizeSpeechModelStatus); return window.__ttsModelRegistry; } function ensureTextModelRegistry() { if (!Array.isArray(window.__textModelRegistry)) { window.__textModelRegistry = []; } window.__textModelRegistry.forEach(normalizeSpeechModelStatus); return window.__textModelRegistry; } function ensureCommandLibrary() { if (!Array.isArray(window.__agentCommandLibrary)) { window.__agentCommandLibrary = []; } return window.__agentCommandLibrary; } function ensurePluginLibrary() { if (!Array.isArray(window.__agentPluginLibrary)) { window.__agentPluginLibrary = []; } return window.__agentPluginLibrary; } function ensureMcpServiceLibrary() { if (!Array.isArray(window.__agentMcpServiceLibrary)) { window.__agentMcpServiceLibrary = []; } return window.__agentMcpServiceLibrary; } function ensureAgentConfigAutosaves() { if (!window.__agentConfigAutosaves || typeof window.__agentConfigAutosaves !== 'object') { window.__agentConfigAutosaves = {}; } return window.__agentConfigAutosaves; } function ensureAgentRunTimers() { if (!window.__agentRunTimers || typeof window.__agentRunTimers !== 'object') { window.__agentRunTimers = {}; } return window.__agentRunTimers; } function normalizeSpeechModelStatus(model) { if (!model) return model; if (model.status === 'connected') model.status = 'available'; if (model.status === 'unconnected') { model.status = 'failed'; model.errorMessage = model.errorMessage || '尚未完成连接校验'; } if (model.enabled === false && model.status === 'available') { model.status = 'disabled'; } return model; } function getNowText() { return new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }).replace(/\//g, '-'); } function getTimeOnly(value) { const text = String(value || ''); const match = text.match(/(\d{2}:\d{2}:\d{2})/); return match ? match[1] : text; } function getAvailableAsrModels() { return ensureAsrModelRegistry() .filter(model => model.enabled !== false && model.status === 'available') .map(model => ({ id: model.id, label: model.name, desc: `${model.provider} · ${model.baseUrl}`, provider: model.provider, })); } function getAvailableTtsModels() { return ensureTtsModelRegistry() .filter(model => model.enabled !== false && model.status === 'available') .map(model => ({ id: model.id, label: model.name, desc: `${model.provider} · ${getLanguageTypeName(model.languageType)} · ${getTtsVoiceSummary(model)} · ${String(model.audioFormat || '').toUpperCase() || '默认格式'}`, provider: model.provider, audioFormat: model.audioFormat, voiceId: model.voiceId, })); } function getAvailableTextModels() { return ensureTextModelRegistry() .filter(model => model.enabled !== false && model.status === 'available') .map(model => ({ id: model.id, label: model.name, desc: `${model.provider} · ${model.modelCode} · 上下文 ${model.contextWindow || '-'} tokens`, provider: model.provider, modelCode: model.modelCode, })); } registerPage('agent-config', function(container) { if (typeof window.__ensureAgentHotwordGroups === 'function') { window.__ensureAgentHotwordGroups(); } else if (!window.__agentHotwordGroups) { window.__agentHotwordGroups = [ { id: 'hw-001', name: '品牌基础热词', mode: 'manual', words: [ { text: 'TalkingQ', lang: '英文' }, { text: '小Q', lang: '中文' }, { text: '故事机', lang: '中文' }, ], updatedAt: '2026-05-20 11:57:38', }, { id: 'hw-002', name: '儿童内容热词', mode: 'manual', words: [ { text: '白雪公主', lang: '中文' }, { text: '睡前故事', lang: '中文' }, ], updatedAt: '2026-05-20 12:08:16', }, ]; } const apps = window.__agentManagedApps || AGENT_CONFIG_DEFAULT_APPS; const selectedApp = apps.find(app => app.id === window.__selectedAppId) || apps[0] || AGENT_CONFIG_DEFAULT_APPS[0]; const firstHotwordGroup = window.__agentHotwordGroups?.[0]?.id || ''; const availableAsrModels = getAvailableAsrModels(); const availableTtsModels = getAvailableTtsModels(); const availableTextModels = getAvailableTextModels(); const state = { app: selectedApp, activeStep: 'voice', preview: 'empty', saveStatus: 'saved', lastSavedAt: '', selectedInteractionLanguage: window.__selectedInteractionLanguage || 'zh', voiceEnabled: true, asrEnabled: true, ttsEnabled: true, selectedAsrModelId: availableAsrModels[0]?.id || '', selectedTtsModelId: availableTtsModels[0]?.id || '', selectedHotwordGroupId: firstHotwordGroup, ttsFilterEnabled: false, ttsFilterPairs: [ { enabled: true, left: '/*', right: '*/' }, { enabled: true, left: '[', right: ']' }, { enabled: false, left: '{', right: '}' }, ], ttsFilterSingles: [], interruptPrimaryEnabled: true, interruptSecondaryEnabled: true, interruptMode: 'full', semanticStopEnabled: true, selectedTextModelId: availableTextModels[0]?.id || '', intentEnabled: true, dialogueContinuationEnabled: true, knowledgeSearchEnabled: true, longTermMemoryEnabled: false, prompt: AGENT_CONFIG_MOCK.understand.prompt, knowledgeBases: AGENT_CONFIG_MOCK.understand.knowledgeBases.map(item => ({ ...item })), variables: AGENT_CONFIG_MOCK.understand.variables.map(item => ({ ...item })), selectedCommandIds: [], selectedPluginIds: [], selectedMcpServiceIds: [], runSession: { apiKey: '', apiKeyConfirmed: false, apiKeyName: '', isRecording: false, micMuted: false, elapsedSeconds: 0, latencyMs: null, transcript: '', aiResponse: '', textInput: '', textResponse: '', textPending: false, chatMessages: [], startedAt: 0, }, }; function renderAfterConfigChange() { scheduleAgentConfigAutoSave(state); render(); } function render() { const scrollArea = container.querySelector('.bailian-config-scroll'); const previousScrollTop = scrollArea?.scrollTop || 0; if (state.selectedHotwordGroupId && !(window.__agentHotwordGroups || []).some(group => group.id === state.selectedHotwordGroupId)) { state.selectedHotwordGroupId = window.__agentHotwordGroups?.[0]?.id || ''; } container.innerHTML = renderAgentWorkbench(state); renderAgentIcons(container); bindWorkbenchEvents(container, state, renderAfterConfigChange); bindPromptCounter(container, state); bindRuntimePreviewEvents(container, state, () => updatePreviewOnly(container, state)); requestAnimationFrame(() => { const nextScrollArea = container.querySelector('.bailian-config-scroll'); if (nextScrollArea) { nextScrollArea.scrollTop = previousScrollTop; } }); } const onHotwordGroupsChanged = () => { if (!container.isConnected || window.__currentPage !== 'agent-config') { window.removeEventListener('hotwordGroupsChanged', onHotwordGroupsChanged); return; } render(); }; window.addEventListener('hotwordGroupsChanged', onHotwordGroupsChanged); render(); }); function renderAgentWorkbench(state) { return `
${renderAgentHeader(state)}
${renderPreviewPane(state)}
`; } function renderAgentHeader(state) { const app = state.app; const status = getAgentStatus(app.status); const saveText = state.saveStatus === 'saving' ? '自动保存中' : state.lastSavedAt ? `已自动保存 ${getTimeOnly(state.lastSavedAt)}` : '等待自动保存'; return `

${escapeHtml(app.name)}

${status.label}
ID: ${escapeHtml(app.id)} · AI 智能体配置平台 ${escapeHtml(saveText)}
`; } function renderStepTabs(activeStep) { return ` `; } function renderAllStepSections(state) { return AGENT_CONFIG_STEPS.map(step => `
${renderStepContent(step.id, state)}
`).join(''); } function renderStepContent(stepId, state) { const renderers = { voice: renderVoiceStep, understand: renderUnderstandStep, skills: renderSkillsStep, }; return (renderers[stepId] || renderVoiceStep)(state); } function renderVoiceStep(state) { const data = AGENT_CONFIG_MOCK.voice; const hotwordGroups = window.__agentHotwordGroups || []; const asrModels = getAvailableAsrModels(); const ttsModels = getAvailableTtsModels(); if (state.selectedAsrModelId && !asrModels.some(model => model.id === state.selectedAsrModelId)) { state.selectedAsrModelId = ''; } if (state.selectedTtsModelId && !ttsModels.some(model => model.id === state.selectedTtsModelId)) { state.selectedTtsModelId = ''; } if (!state.selectedAsrModelId && asrModels.length) { state.selectedAsrModelId = asrModels[0].id; } if (!state.selectedTtsModelId && ttsModels.length) { state.selectedTtsModelId = ttsModels[0].id; } return `

语音交互

开启语音交互
打开后运行将使用语音进行交互,关闭后仅保留文字交互
${state.voiceEnabled ? `
语音 AI
${renderFeatureSelect({ title: '语音识别', toggleKey: 'asr-enabled', checked: state.asrEnabled, activeId: state.selectedAsrModelId, options: asrModels, hint: 'API Key 统一在 ASR 模型管理中维护,连接成功且启用后才可选择', emptyText: '暂无可用 ASR 模型,请先创建并通过自动校验', manageAction: 'asr-model-manager', manageText: '管理 ASR 模型', })} ${renderFeatureSelect({ title: '语音合成', toggleKey: 'tts-enabled', checked: state.ttsEnabled, activeId: state.selectedTtsModelId, options: ttsModels, hint: 'API Key、Voice ID 和音频格式统一在 TTS 模型管理中维护', emptyText: '暂无可用 TTS 模型,请先创建并通过自动校验', manageAction: 'tts-model-manager', manageText: '管理 TTS 模型', })}
热词
热词组绑定
热词组来自热词库管理,新增后会自动出现在这里
语音合成播报过滤
开启播报过滤
${state.ttsFilterEnabled ? renderTtsFilterSummary(state) : ''}
对话打断
此配置仅用于效果体验
长按对话(半双工,仅移动端支持) 运行后可在移动端长按触发
语义判停与拒识
` : `
当前为文字交互模式 关闭语音交互后,不展示语音 AI、热词、播报过滤和对话打断配置。
`}
`; } function renderUnderstandStep(state) { const data = AGENT_CONFIG_MOCK.understand; const textModels = getAvailableTextModels(); if (state.selectedTextModelId && !textModels.some(model => model.id === state.selectedTextModelId)) { state.selectedTextModelId = ''; } if (!state.selectedTextModelId && textModels.length) { state.selectedTextModelId = textModels[0].id; } const selectedModel = textModels.find(item => item.id === state.selectedTextModelId) || textModels[0] || null; return `
${renderSection('意图识别', '', `${data.intentModel.desc} 关闭将不再触发调用工具/Agent 等基于语义理解的交互功能,适用于低成本、轻量级语音交互场景。`, ` `)} ${renderSection('文本模型', `
模型选择
模型凭证统一在文本模型管理中维护,校验通过并启用后才可选择
${selectedModel ? `
${escapeHtml(selectedModel.desc)}
` : ''} `)} ${renderPromptEditor(state.prompt || data.prompt)} ${renderSection('对话变量', ` ${renderVariableSimpleEditor(state)} `, '变量是智能体运行时的状态存储,可用于注入 Prompt、节点传递、流程控制和上下文记忆。', ` `)} ${renderSection('上下文轮数', `
携带最近 轮对话作为上下文
`)} ${renderKnowledgeBaseSection(state.knowledgeBases || data.knowledgeBases)} ${renderSection('对话承接语', '', '大模型基于用户输入自动生成承接语,减少回复等待时间', ` `)} ${renderSection('互联网搜索', '', '关闭后不展示搜索模式,仅保留本地知识和模型能力', ` `)} ${renderSection('长期记忆', '', '基于对话历史形成专属记忆库,并在后续对话中体现。开启长期记忆后,结合记忆内容进行回复的对话会增加额外耗时。', ` `)}
`; } function renderSkillsStep(state) { const data = AGENT_CONFIG_MOCK.skills; const commandLibrary = ensureCommandLibrary(); const selectedCommandIds = new Set(state.selectedCommandIds || []); const commands = commandLibrary.filter(item => selectedCommandIds.has(item.id)); const pluginLibrary = ensurePluginLibrary(); const selectedPluginIds = new Set(state.selectedPluginIds || []); const plugins = pluginLibrary.filter(item => selectedPluginIds.has(item.id)); return `
${renderSection('指令', `
当前智能体只会调用已挂载的指令。指令创建后会保存在指令库中,可重复选择。
${commands.length ? commands.map(item => `
${escapeHtml(item.name)} ${escapeHtml(item.commandType || '自定义指令')}
${escapeHtml(item.desc)}
`).join('') : `
暂无已挂载指令 从 0 开始创建指令,创建后可在指令库中重复选择。
`}
`)} ${renderSection('插件', `
当前智能体只会调用已挂载的插件。插件创建后会保存在插件库中,可重复选择。
${plugins.length ? plugins.map(item => `
${escapeHtml(item.name)} ${escapeHtml(item.pluginType || '自定义插件')}
${escapeHtml(item.desc)}
${escapeHtml(item.status || '系统执行')}
`).join('') : `
暂无已挂载插件 从 0 开始创建插件,创建后可在插件库中重复选择。
`}
`)} ${renderMcpSection(state)}
`; } function renderPreviewPane(state) { if (state.preview === 'voice-auth') { return renderRuntimeAuthPreview(state, 'voice'); } if (state.preview === 'text-auth') { return renderRuntimeAuthPreview(state, 'text'); } if (state.preview === 'voice-runtime') { return renderVoiceRuntimePreview(state); } if (state.preview === 'text-runtime') { return renderTextRuntimePreview(state); } return `

请填好配置后点击运行

当前应用测试会产生实际调用,按对话次数计费

建议佩戴耳机,并在安静环境下测试,对话体验更佳。
`; } function renderRuntimeAuthPreview(state, mode = 'voice') { const savedKeys = getRuntimeApiKeysForApp(state); const hasKeys = savedKeys.length > 0; const isVoice = mode === 'voice'; return `
${isVoice ? 'Voice Runtime' : 'Text Runtime'}

配置运行 API Key

运行前已自动保存当前配置。请选择系统设置里已保存的 API Key 后进入${isVoice ? '语音测试' : '文字聊天测试'}。

配置已保存
${hasKeys ? '' : ` `}
运行面板只引用系统设置中已保存的 API Key。后端接入后,这里应只传递 Key ID,由后端安全读取密钥并创建运行会话。
`; } function renderVoiceRuntimePreview(state) { const session = getRunSession(state); const latency = session.latencyMs == null ? '3377ms' : `${session.latencyMs}ms`; const recordText = session.isRecording ? formatRuntimeDuration(session.elapsedSeconds) : '00:00'; const transcript = session.transcript || '正在监听,请开始说话...'; const aiResponse = session.aiDisplayResponse !== undefined ? session.aiDisplayResponse : (session.aiResponse || ''); const userName = getAgentRuntimeUserName(); const hasVoiceConversation = session.isRecording || Boolean(session.transcript || session.aiResponse || session.aiDisplayResponse); return `

语音交互体验

${escapeHtml(userName)}
${hasVoiceConversation ? `
${escapeHtml(transcript)}
${aiResponse ? escapeHtml(aiResponse) : ''}
端到端首包延迟:${escapeHtml(latency)}
` : `

${escapeHtml(state.app?.name || 'TalkingQ Agent')}

建议佩戴耳机,并在安静环境下测试,对话体验更佳。

`}
${session.isRecording ? `
${escapeHtml(recordText)}
` : ` `}
${session.micMuted ? '
麦克风已关闭,当前暂停收音。
' : ''}
`; } function renderTextRuntimePreview(state) { const session = getRunSession(state); const messages = Array.isArray(session.chatMessages) ? session.chatMessages : []; const userName = getAgentRuntimeUserName(); return `

文本交互体验

${escapeHtml(userName)}
${messages.length ? messages.map((message, index) => renderTextChatMessage(message, index)).join('') : `
开始输入第一条测试消息
`} ${session.textPending ? `
` : ''} ${session.latencyMs ? `
端到端 ${escapeHtml(String(session.latencyMs))}ms
` : ''}
`; } function renderTextChatMessage(message, index) { if (message.role === 'user') { return `
${escapeHtml(message.content)}
`; } const content = message.displayContent !== undefined ? message.displayContent : message.content; return `
${escapeHtml(content || '')}
${message.content ? `
` : ''}
`; } function getRunSession(state) { if (!state.runSession) { state.runSession = {}; } return state.runSession; } function getRuntimeApiKeysForApp(state) { const settings = typeof loadSettings === 'function' ? loadSettings() : (window.__talkingqSettings || {}); if (Array.isArray(settings.apiKeyRecords)) { const records = settings.apiKeyRecords .filter(record => String(record.value || '').trim()) .map(record => ({ id: record.id, name: record.name || 'API Key', provider: record.provider || record.providerId || 'Custom', value: record.value, })); if (records.length) return records; } const apiKeys = settings.apiKeys || {}; return [ { id: 'openai', name: 'OpenAI API Key', provider: 'OpenAI', value: apiKeys.openai || '' }, { id: 'ali', name: '阿里云百炼 API Key', provider: 'Alibaba Cloud Bailian', value: apiKeys.ali || '' }, { id: 'gemini', name: 'Google Gemini API Key', provider: 'Google Gemini', value: apiKeys.gemini || '' }, ].filter(item => String(item.value || '').trim()); } function formatRuntimeDuration(seconds) { const safe = Math.max(0, Number(seconds) || 0); const min = Math.floor(safe / 60); const sec = safe % 60; return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; } function getAgentRunTimerKey(state) { return state.app?.id || 'default'; } function clearAgentRunTimer(state) { const timers = ensureAgentRunTimers(); const key = getAgentRunTimerKey(state); if (timers[key]) { window.clearInterval(timers[key]); delete timers[key]; } } function buildAgentConfigSnapshot(state) { return { appId: state.app?.id, appName: state.app?.name, savedAt: state.lastSavedAt, voiceEnabled: state.voiceEnabled, asrEnabled: state.asrEnabled, ttsEnabled: state.ttsEnabled, selectedInteractionLanguage: state.selectedInteractionLanguage, selectedAsrModelId: state.selectedAsrModelId, selectedTtsModelId: state.selectedTtsModelId, selectedTextModelId: state.selectedTextModelId, selectedHotwordGroupId: state.selectedHotwordGroupId, ttsFilterEnabled: state.ttsFilterEnabled, interruptPrimaryEnabled: state.interruptPrimaryEnabled, interruptSecondaryEnabled: state.interruptSecondaryEnabled, semanticStopEnabled: state.semanticStopEnabled, intentEnabled: state.intentEnabled, dialogueContinuationEnabled: state.dialogueContinuationEnabled, knowledgeSearchEnabled: state.knowledgeSearchEnabled, longTermMemoryEnabled: state.longTermMemoryEnabled, prompt: state.prompt, variables: state.variables, knowledgeBases: state.knowledgeBases, selectedCommandIds: state.selectedCommandIds, selectedPluginIds: state.selectedPluginIds, selectedMcpServiceIds: state.selectedMcpServiceIds, }; } function updateAgentAutosaveIndicator(state) { const indicator = document.querySelector('#agentAutosaveStatus'); if (!indicator) return; indicator.classList.toggle('saving', state.saveStatus === 'saving'); indicator.textContent = state.saveStatus === 'saving' ? '自动保存中' : state.lastSavedAt ? `已自动保存 ${getTimeOnly(state.lastSavedAt)}` : '等待自动保存'; } function autoSaveAgentConfig(state) { state.saveStatus = 'saved'; state.lastSavedAt = getNowText(); const store = ensureAgentConfigAutosaves(); store[state.app?.id || 'default'] = buildAgentConfigSnapshot(state); updateAgentAutosaveIndicator(state); } function scheduleAgentConfigAutoSave(state) { if (state.__saveTimer) { window.clearTimeout(state.__saveTimer); } state.saveStatus = 'saving'; updateAgentAutosaveIndicator(state); state.__saveTimer = window.setTimeout(() => { autoSaveAgentConfig(state); state.__saveTimer = null; }, 300); } function getMockTranscriptForSecond(second) { const fragments = [ '你好小Q', '你好小Q,今天可以给我讲一个太空故事吗', '你好小Q,今天可以给我讲一个太空故事吗,里面要有机器人和小朋友', '你好小Q,今天可以给我讲一个太空故事吗,里面要有机器人和小朋友,最后要开心一点', ]; if (second <= 1) return fragments[0]; if (second <= 2) return fragments[1]; if (second <= 4) return fragments[2]; return fragments[3]; } function getMockAiResponseForSecond(second) { if (second < 2) return ''; if (second < 4) return '可以呀,我正在准备一个温柔的太空小故事。'; return '当然可以。小朋友坐上银色小飞船,和机器人小Q一起飞向月亮。他们发现一颗会唱歌的小星星,最后把星光带回家,送给每一个睡前还想听故事的小朋友。'; } function getAgentRuntimeUserName() { return window.__currentUser?.displayName || window.__currentUser?.name || window.currentUser?.displayName || window.currentUser?.name || 'Admin'; } function syncVoiceRuntimeDom(container, state) { const panel = container.querySelector('.agent-voice-panel.recording'); if (!panel || state.preview !== 'voice-runtime') return false; const session = getRunSession(state); const latency = session.latencyMs == null ? '3377ms' : `${session.latencyMs}ms`; const recordText = session.isRecording ? formatRuntimeDuration(session.elapsedSeconds) : '00:00'; const transcript = session.transcript || '正在监听,请开始说话...'; const aiResponse = session.aiDisplayResponse !== undefined ? session.aiDisplayResponse : (session.aiResponse || ''); const transcriptNode = panel.querySelector('[data-voice-runtime="transcript"]'); if (transcriptNode) transcriptNode.textContent = transcript; const aiContentNode = panel.querySelector('[data-voice-runtime="ai-content"]'); if (aiContentNode) { if (aiResponse) { aiContentNode.textContent = aiResponse; } else if (!aiContentNode.querySelector('.agent-runtime-typing')) { aiContentNode.innerHTML = ''; } } const actionsNode = panel.querySelector('[data-voice-runtime="ai-actions"]'); if (actionsNode) { actionsNode.classList.toggle('is-visible', Boolean(session.aiResponse)); actionsNode.setAttribute('aria-hidden', session.aiResponse ? 'false' : 'true'); } const latencyNode = panel.querySelector('[data-voice-runtime="latency"]'); if (latencyNode) latencyNode.textContent = `端到端首包延迟:${latency}`; const recordNode = panel.querySelector('[data-voice-runtime="record-time"]'); if (recordNode) recordNode.textContent = recordText; const waveNode = panel.querySelector('[data-voice-runtime="wave"]'); if (waveNode) waveNode.classList.toggle('muted', Boolean(session.micMuted)); const micButton = panel.querySelector('[data-voice-runtime="mic-button"]'); if (micButton) { micButton.classList.toggle('muted', Boolean(session.micMuted)); micButton.setAttribute('aria-label', session.micMuted ? '打开麦克风' : '关闭麦克风'); const iconName = session.micMuted ? 'mic-off' : 'mic'; const icon = micButton.querySelector('i, svg'); if (icon?.getAttribute('data-lucide') !== iconName) { micButton.innerHTML = ``; renderAgentIcons(micButton); } } const voiceScrollArea = panel.querySelector('.agent-voice-stage') || panel.querySelector('.agent-voice-conversation'); if (voiceScrollArea) { voiceScrollArea.scrollTop = voiceScrollArea.scrollHeight; } return true; } function revealVoiceReply(container, state, fullText) { const session = getRunSession(state); let cursor = 0; const step = Math.max(1, Math.ceil(fullText.length / 36)); if (session.__voiceRevealTimer) { window.clearInterval(session.__voiceRevealTimer); } session.__voiceRevealTimer = window.setInterval(() => { cursor = Math.min(fullText.length, cursor + step); session.aiDisplayResponse = fullText.slice(0, cursor); if (!syncVoiceRuntimeDom(container, state)) { updatePreviewOnly(container, state); } if (cursor >= fullText.length) { session.aiDisplayResponse = fullText; window.clearInterval(session.__voiceRevealTimer); session.__voiceRevealTimer = null; if (!syncVoiceRuntimeDom(container, state)) { updatePreviewOnly(container, state); } } }, 38); } function buildMockTextReply(value, retryCount = 0) { const suffix = retryCount ? `这是第 ${retryCount + 1} 版重新生成结果。` : '这是当前前端 mock 回复效果。'; return `我收到了:“${value}”。${suffix} 后端接入后,这里会返回真实模型的流式回复。`; } function revealTextReply(container, state, messageIndex, fullText) { const session = getRunSession(state); let cursor = 0; const step = Math.max(1, Math.ceil(fullText.length / 34)); const timer = window.setInterval(() => { const messages = Array.isArray(session.chatMessages) ? session.chatMessages : []; const message = messages[messageIndex]; if (!message) { window.clearInterval(timer); return; } cursor = Math.min(fullText.length, cursor + step); message.displayContent = fullText.slice(0, cursor); updatePreviewOnly(container, state); if (cursor >= fullText.length) { message.displayContent = fullText; window.clearInterval(timer); updatePreviewOnly(container, state); } }, 38); } function startVoiceRuntimeCall(container, state) { const session = getRunSession(state); clearAgentRunTimer(state); if (session.__voiceRevealTimer) { window.clearInterval(session.__voiceRevealTimer); session.__voiceRevealTimer = null; } session.isRecording = true; session.micMuted = false; session.elapsedSeconds = 0; session.latencyMs = 3377; session.transcript = '正在监听,请开始说话...'; session.aiResponse = ''; session.aiDisplayResponse = ''; session.lastVoiceAiResponse = ''; session.startedAt = Date.now(); updatePreviewOnly(container, state); const timers = ensureAgentRunTimers(); timers[getAgentRunTimerKey(state)] = window.setInterval(() => { if (!session.isRecording) { clearAgentRunTimer(state); return; } session.elapsedSeconds = Math.floor((Date.now() - session.startedAt) / 1000); if (!session.micMuted) { const nextTranscript = getMockTranscriptForSecond(session.elapsedSeconds); const nextResponse = getMockAiResponseForSecond(session.elapsedSeconds); session.transcript = nextTranscript; session.latencyMs = 3377 + ((session.elapsedSeconds * 83) % 420); if (nextResponse && nextResponse !== session.lastVoiceAiResponse) { session.aiResponse = nextResponse; session.aiDisplayResponse = ''; session.lastVoiceAiResponse = nextResponse; revealVoiceReply(container, state, nextResponse); return; } } if (!syncVoiceRuntimeDom(container, state)) { updatePreviewOnly(container, state); } }, 1000); } function stopVoiceRuntimeCall(container, state) { const session = getRunSession(state); clearAgentRunTimer(state); session.isRecording = false; session.elapsedSeconds = Math.max(session.elapsedSeconds || 0, Math.floor((Date.now() - (session.startedAt || Date.now())) / 1000)); session.latencyMs = session.latencyMs || 3377; session.transcript = session.transcript || '本轮没有检测到有效语音。'; session.aiResponse = session.aiResponse || '我已经结束本轮收音,可以再次点击电话按钮开始新的语音测试。'; session.aiDisplayResponse = session.aiDisplayResponse || session.aiResponse; updatePreviewOnly(container, state); } function renderSection(title, content, tooltip = '', headerAction = '') { const hasContent = Boolean(String(content || '').trim()); const titleInfo = tooltip ? ` ` : ''; const titleMarkup = `

${escapeHtml(title)} ${titleInfo}

`; return `
${headerAction ? `
${titleMarkup} ${headerAction}
` : titleMarkup} ${hasContent ? content : ''}
`; } function renderModelSelect(model) { return ` `; } function renderFeatureSelect(config) { const { title, toggleKey, checked, activeId, options = [], groupedOptions = null, hint = '', emptyText = '暂无可选模型', manageAction = '', manageText = '', } = config; const fieldAction = toggleKey === 'asr-enabled' ? 'asr-model' : toggleKey === 'tts-enabled' ? 'tts-model' : 'model'; const selectedOption = options.find(item => item.id === activeId) || options[0] || null; return `
${escapeHtml(title)}
${escapeHtml(hint)}
${selectedOption ? `
${escapeHtml(selectedOption.desc)}
` : ''} ${manageAction ? ` ` : ''}
`; } function renderRadio(name, value, label, checked = false) { return ` `; } function renderTtsFilterSummary(state) { if (!state.ttsFilterEnabled) return ''; const pairKeys = new Set(); const pairs = (state.ttsFilterPairs || []).reduce((list, item) => { if (item.enabled === false) return list; const left = String(item.left || '').trim(); const right = String(item.right || '').trim(); if (!left && !right) return list; const key = `${left}__${right}`; if (pairKeys.has(key)) return list; pairKeys.add(key); list.push(`${left} ${right}`.trim()); return list; }, []); const singles = [...new Set((state.ttsFilterSingles || []).map(item => String(item || '').trim()).filter(Boolean))]; const tokens = [...pairs, ...singles.map(item => `字符 ${item}`)]; const visibleTokens = tokens.slice(0, 5); const hiddenCount = Math.max(tokens.length - visibleTokens.length, 0); return `
播报过滤规则
${pairs.length} 组符号对 · ${singles.length} 个单字符
${visibleTokens.length ? visibleTokens.map(item => `${escapeHtml(item)}`).join('') : '暂无规则'} ${hiddenCount ? `+${hiddenCount}` : ''}
`; } function renderVariableSimpleEditor(state) { const variables = state.variables || []; return `
变量名 描述
${variables.length ? variables.map(item => `
`).join('') : `
暂无变量,点击右上角添加。
`}
`; } function getSpeechModelDisplayStatus(model) { if (!model) return 'failed'; if (model.status === 'connected') return 'available'; if (model.status === 'unconnected') return 'failed'; if (model.status === 'available' && model.enabled === false) return 'disabled'; return model.status || 'failed'; } function getAsrModelDisplayStatus(model) { return getSpeechModelDisplayStatus(model); } function getTtsModelDisplayStatus(model) { return getSpeechModelDisplayStatus(model); } function getAsrStatusMeta(model) { return ASR_MODEL_STATUS_META[getAsrModelDisplayStatus(model)] || ASR_MODEL_STATUS_META.failed; } function getTtsStatusMeta(model) { return TTS_MODEL_STATUS_META[getTtsModelDisplayStatus(model)] || TTS_MODEL_STATUS_META.failed; } function maskSecret(value) { if (!value) return '未填写'; return `${String(value).slice(0, 4)}••••••••${String(value).slice(-3)}`; } function getLanguageTypeName(languageId) { return AGENT_INTERACTION_LANGUAGES.find(item => item.id === languageId)?.name || languageId || '多语言'; } function getTtsVoiceIds(model) { if (Array.isArray(model?.voiceIds)) { return model.voiceIds.filter(Boolean); } return model?.voiceId ? [model.voiceId] : []; } function getTtsVoiceSummary(model) { const voiceIds = getTtsVoiceIds(model); if (!voiceIds.length) return '未配置 Voice'; if (voiceIds.length === 1) return `Voice ${voiceIds[0]}`; return `${voiceIds.length} 个 Voice`; } function parseTtsVoiceIds(value) { return [...new Set(String(value || '') .split(/[\n,,;;\s]+/) .map(item => item.trim()) .filter(Boolean))]; } function getSpeechModelValidationError(model, type) { const apiKey = String(model.apiKey || '').trim().toLowerCase(); const baseUrl = String(model.baseUrl || '').trim().toLowerCase(); const voiceIds = getTtsVoiceIds(model).map(item => String(item).trim().toLowerCase()); if (!apiKey || apiKey.includes('invalid') || apiKey.startsWith('bad') || apiKey === 'sk-invalid') { return 'Key 无效'; } if (!/^https?:\/\//i.test(model.baseUrl || '') || /down|timeout|error|404|500/.test(baseUrl)) { return '接口异常'; } if (type === 'TTS' && voiceIds.some(voiceId => /missing|notfound|not-found|none/.test(voiceId))) { return 'Voice 不存在'; } if (type === 'TTS' && !TTS_AUDIO_FORMAT_OPTIONS.some(item => item.id === model.audioFormat)) { return '返回格式错误'; } return ''; } function markSpeechModelValidating(model) { model.status = 'validating'; model.enabled = false; model.errorMessage = ''; model.updatedAt = getNowText(); } function markSpeechModelAvailable(model, shouldEnable = true) { model.status = shouldEnable ? 'available' : 'disabled'; model.enabled = Boolean(shouldEnable); model.errorMessage = ''; model.validatedAt = getNowText(); model.updatedAt = getNowText(); } function markSpeechModelFailed(model, message) { model.status = 'failed'; model.enabled = false; model.errorMessage = message || '连接校验失败'; model.updatedAt = getNowText(); } function renderAsrModelManagerContent(showForm = false) { const models = ensureAsrModelRegistry(); return `
ASR 模型凭证统一维护在这里。创建后系统会自动校验接口连通性,只有状态为「可用」且已启用的模型才会进入智能体的语音识别下拉框。
${showForm ? `
新增 ASR 模型配置
研发可先登记模型名称、Provider、Base URL 与密钥;创建后自动开始后台校验。
` : `
ASR 模型列表
模型创建并校验通过后,才会出现在智能体配置下拉框中。
`}
已添加模型
${models.length} 个配置
${models.length ? models.map(model => { const meta = getAsrStatusMeta(model); const status = getAsrModelDisplayStatus(model); const canToggle = status === 'available' || status === 'disabled'; return `
${escapeHtml(model.name)} ${escapeHtml(meta.label)}
Provider:${escapeHtml(model.provider)} Base URL:${escapeHtml(model.baseUrl)} API Key:${escapeHtml(maskSecret(model.apiKey))}
Secret Key:${escapeHtml(maskSecret(model.secretKey))} 更新时间:${escapeHtml(model.updatedAt || '-')} ${model.validatedAt ? `最近校验:${escapeHtml(model.validatedAt)}` : ''}
${model.errorMessage ? `
失败原因:${escapeHtml(model.errorMessage)}
` : ''}
`; }).join('') : `
暂无 ASR 模型 创建并自动校验成功后,智能体即可选择该识别模型。
`}
`; } function collectAsrModelForm(body) { const name = body.querySelector('#asrModelName')?.value.trim(); const provider = body.querySelector('#asrProviderName')?.value.trim(); const baseUrl = body.querySelector('#asrBaseUrl')?.value.trim(); const apiKey = body.querySelector('#asrApiKey')?.value.trim(); const secretKey = body.querySelector('#asrSecretKey')?.value.trim(); const enableAfterValidation = body.querySelector('#asrEnableAfterValidation')?.checked !== false; if (!name || !provider || !baseUrl || !apiKey) { Toast.warning('请填写模型名称、Provider、Base URL 和 API Key'); return null; } if (!/^https?:\/\//i.test(baseUrl)) { Toast.warning('Base URL 需要以 http:// 或 https:// 开头'); return null; } return { id: `asr-${Date.now()}`, name, provider, baseUrl, apiKey, secretKey, enableAfterValidation, status: 'validating', enabled: false, updatedAt: getNowText(), }; } function validateAsrModelConnection(model) { const validationError = getSpeechModelValidationError(model, 'ASR'); if (validationError) { return new Promise(resolve => setTimeout(() => resolve({ success: false, message: validationError }), 650)); } const validate = API.agentDev.validateASRModel || API.agentDev.testASRModel; return validate(model.id, { provider: model.provider, baseUrl: model.baseUrl, }); } function openAsrModelManagerModal(state, rerender) { let showCreateForm = false; const modal = createModal({ title: 'ASR 模型管理', size: 'xl', showCancel: false, confirmText: '关闭', content: renderAsrModelManagerContent(showCreateForm), onConfirm: () => true, }); const refreshModal = () => { const body = modal.overlay.querySelector('.modal-body'); if (!body) return; body.innerHTML = renderAsrModelManagerContent(showCreateForm); lucide.createIcons({ elements: [body] }); bindAsrModelManagerEvents(body); rerender(); }; const finishValidation = async (model, shouldEnable = true) => { const result = await validateAsrModelConnection(model); if (result.success === false) { markSpeechModelFailed(model, result.message || 'ASR 模型连接失败'); Toast.error(model.errorMessage); } else { markSpeechModelAvailable(model, shouldEnable); if (model.enabled && !state.selectedAsrModelId) state.selectedAsrModelId = model.id; Toast.success(`ASR 模型「${model.name}」校验通过`); } }; const bindAsrModelManagerEvents = (body) => { body.querySelectorAll('[data-asr-action="show-create"]').forEach(btn => btn.addEventListener('click', () => { showCreateForm = true; refreshModal(); })); body.querySelectorAll('[data-asr-action="cancel-create"]').forEach(btn => btn.addEventListener('click', () => { showCreateForm = false; refreshModal(); })); body.querySelectorAll('[data-asr-action="create-new"]').forEach(btn => btn.addEventListener('click', async () => { const model = collectAsrModelForm(body); if (!model) return; markSpeechModelValidating(model); ensureAsrModelRegistry().push(model); await API.agentDev.createASRModel(model); Toast.info(`ASR 模型「${model.name}」开始自动校验`, 800); showCreateForm = false; refreshModal(); await finishValidation(model, model.enableAfterValidation !== false); refreshModal(); })); body.querySelectorAll('[data-asr-action="validate-existing"]').forEach(btn => { btn.addEventListener('click', async () => { const model = ensureAsrModelRegistry().find(item => item.id === btn.dataset.id); if (!model) return; markSpeechModelValidating(model); refreshModal(); await finishValidation(model, true); refreshModal(); }); }); body.querySelectorAll('[data-asr-action="toggle-existing"]').forEach(btn => { btn.addEventListener('click', async () => { const model = ensureAsrModelRegistry().find(item => item.id === btn.dataset.id); if (!model) return; const status = getAsrModelDisplayStatus(model); if (status !== 'available' && status !== 'disabled') { Toast.warning('只有校验通过的模型才可以启用或禁用'); return; } if (status === 'disabled') { await API.agentDev.setASRModelEnabled(model.id, true); markSpeechModelAvailable(model, true); if (!state.selectedAsrModelId) state.selectedAsrModelId = model.id; Toast.success(`ASR 模型「${model.name}」已启用`); } else { await API.agentDev.setASRModelEnabled(model.id, false); markSpeechModelAvailable(model, false); if (state.selectedAsrModelId === model.id) state.selectedAsrModelId = ''; Toast.info(`ASR 模型「${model.name}」已禁用`); } refreshModal(); }); }); body.querySelectorAll('[data-asr-action="delete-existing"]').forEach(btn => { btn.addEventListener('click', async () => { const registry = ensureAsrModelRegistry(); const model = registry.find(item => item.id === btn.dataset.id); if (!model) return; await API.agentDev.deleteASRModel(model.id); window.__asrModelRegistry = registry.filter(item => item.id !== model.id); if (state.selectedAsrModelId === model.id) state.selectedAsrModelId = ''; Toast.success(`ASR 模型「${model.name}」已删除`); refreshModal(); }); }); }; const body = modal.overlay.querySelector('.modal-body'); if (body) { lucide.createIcons({ elements: [body] }); bindAsrModelManagerEvents(body); } } function renderTtsModelManagerContent(showForm = false) { const models = ensureTtsModelRegistry(); return `
TTS 模型凭证、Voice ID、音频格式和语言类型统一维护在这里。创建模型后系统会自动校验 Key、接口、Voice 与音频返回,校验通过后才会进入智能体的语音合成下拉框。
${showForm ? `
新增 TTS 模型配置
无需手动测试连接;点击创建模型后自动进入后台验证流程。
Voice 配置(可选)
` : `
TTS 模型列表
模型创建并自动校验通过后,才会出现在智能体配置下拉框中。
`}
已添加模型
${models.length} 个配置
${models.length ? models.map(model => { const meta = getTtsStatusMeta(model); const status = getTtsModelDisplayStatus(model); const canToggle = status === 'available' || status === 'disabled'; return `
${escapeHtml(model.name)} ${escapeHtml(meta.label)}
Provider:${escapeHtml(model.provider)} Base URL:${escapeHtml(model.baseUrl)} API Key:${escapeHtml(maskSecret(model.apiKey))}
Secret Key:${escapeHtml(maskSecret(model.secretKey))} Voice:${escapeHtml(getTtsVoiceSummary(model))} 音频格式:${escapeHtml(String(model.audioFormat || '').toUpperCase())} 语言类型:${escapeHtml(getLanguageTypeName(model.languageType))}
更新时间:${escapeHtml(model.updatedAt || '-')} ${model.validatedAt ? `最近校验:${escapeHtml(model.validatedAt)}` : ''}
${model.errorMessage ? `
失败原因:${escapeHtml(model.errorMessage)}
` : ''}
`; }).join('') : `
暂无 TTS 模型 创建并自动校验成功后,智能体即可选择该合成模型。
`}
`; } function collectTtsModelForm(body) { const name = body.querySelector('#ttsModelName')?.value.trim(); const provider = body.querySelector('#ttsProviderName')?.value.trim(); const baseUrl = body.querySelector('#ttsBaseUrl')?.value.trim(); const apiKey = body.querySelector('#ttsApiKey')?.value.trim(); const secretKey = body.querySelector('#ttsSecretKey')?.value.trim(); const voiceMode = body.querySelector('input[name="ttsVoiceMode"]:checked')?.value || 'single'; const voiceId = body.querySelector('#ttsVoiceId')?.value.trim(); const voiceIds = voiceMode === 'batch' ? parseTtsVoiceIds(body.querySelector('#ttsVoiceIds')?.value) : (voiceId ? [voiceId] : []); const audioFormat = body.querySelector('#ttsAudioFormat')?.value || 'mp3'; const languageType = body.querySelector('#ttsLanguageType')?.value || 'zh'; const enableAfterValidation = body.querySelector('#ttsEnableAfterValidation')?.checked !== false; if (!name || !provider || !baseUrl || !apiKey || !audioFormat || !languageType) { Toast.warning('请填写模型名称、Provider、Base URL、API Key、音频格式和语言类型'); return null; } if (!/^https?:\/\//i.test(baseUrl)) { Toast.warning('Base URL 需要以 http:// 或 https:// 开头'); return null; } if (voiceMode === 'batch' && voiceIds.length === 0) { Toast.warning('批量添加 Voice 时,请至少填写一个 Voice ID'); return null; } if (voiceIds.length > 50) { Toast.warning('单次最多批量添加 50 个 Voice ID'); return null; } return { id: `tts-${Date.now()}`, name, provider, baseUrl, apiKey, secretKey, voiceMode, voiceId: voiceIds[0] || '', voiceIds, audioFormat, languageType, enableAfterValidation, status: 'validating', enabled: false, updatedAt: getNowText(), }; } function validateTtsModelConnection(model) { const validationError = getSpeechModelValidationError(model, 'TTS'); if (validationError) { return new Promise(resolve => setTimeout(() => resolve({ success: false, message: validationError }), 750)); } const validate = API.agentDev.validateTTSModel || API.agentDev.testTTS; return validate(model.id, { provider: model.provider, baseUrl: model.baseUrl, voiceId: model.voiceId, voiceIds: getTtsVoiceIds(model), audioFormat: model.audioFormat, languageType: model.languageType, }); } function openTtsModelManagerModal(state, rerender) { let showCreateForm = false; const modal = createModal({ title: 'TTS 模型管理', size: 'xl', showCancel: false, confirmText: '关闭', content: renderTtsModelManagerContent(showCreateForm), onConfirm: () => true, }); const refreshModal = () => { const body = modal.overlay.querySelector('.modal-body'); if (!body) return; body.innerHTML = renderTtsModelManagerContent(showCreateForm); lucide.createIcons({ elements: [body] }); bindTtsModelManagerEvents(body); rerender(); }; const finishValidation = async (model, shouldEnable = true) => { const result = await validateTtsModelConnection(model); if (result.success === false) { markSpeechModelFailed(model, result.message || 'TTS 模型连接失败'); Toast.error(model.errorMessage); } else { markSpeechModelAvailable(model, shouldEnable); if (model.enabled && !state.selectedTtsModelId) state.selectedTtsModelId = model.id; Toast.success(`TTS 模型「${model.name}」校验通过`); } }; const bindTtsModelManagerEvents = (body) => { const syncTtsVoiceMode = () => { const mode = body.querySelector('input[name="ttsVoiceMode"]:checked')?.value || 'single'; body.querySelectorAll('.bailian-voice-mode-card').forEach(card => { const input = card.querySelector('input[name="ttsVoiceMode"]'); card.classList.toggle('active', input?.value === mode); }); body.querySelectorAll('[data-tts-voice-panel]').forEach(panel => { panel.classList.toggle('hidden', panel.dataset.ttsVoicePanel !== mode); }); }; body.querySelectorAll('input[name="ttsVoiceMode"]').forEach(radio => { radio.addEventListener('change', syncTtsVoiceMode); }); syncTtsVoiceMode(); body.querySelectorAll('[data-tts-action="show-create"]').forEach(btn => btn.addEventListener('click', () => { showCreateForm = true; refreshModal(); })); body.querySelectorAll('[data-tts-action="cancel-create"]').forEach(btn => btn.addEventListener('click', () => { showCreateForm = false; refreshModal(); })); body.querySelectorAll('[data-tts-action="create-new"]').forEach(btn => btn.addEventListener('click', async () => { const model = collectTtsModelForm(body); if (!model) return; markSpeechModelValidating(model); ensureTtsModelRegistry().push(model); await API.agentDev.createTTSModel(model); Toast.info(`TTS 模型「${model.name}」开始自动校验`, 800); showCreateForm = false; refreshModal(); await finishValidation(model, model.enableAfterValidation !== false); refreshModal(); })); body.querySelectorAll('[data-tts-action="validate-existing"]').forEach(btn => { btn.addEventListener('click', async () => { const model = ensureTtsModelRegistry().find(item => item.id === btn.dataset.id); if (!model) return; markSpeechModelValidating(model); refreshModal(); await finishValidation(model, true); refreshModal(); }); }); body.querySelectorAll('[data-tts-action="toggle-existing"]').forEach(btn => { btn.addEventListener('click', async () => { const model = ensureTtsModelRegistry().find(item => item.id === btn.dataset.id); if (!model) return; const status = getTtsModelDisplayStatus(model); if (status !== 'available' && status !== 'disabled') { Toast.warning('只有校验通过的模型才可以启用或禁用'); return; } if (status === 'disabled') { await API.agentDev.setTTSModelEnabled(model.id, true); markSpeechModelAvailable(model, true); if (!state.selectedTtsModelId) state.selectedTtsModelId = model.id; Toast.success(`TTS 模型「${model.name}」已启用`); } else { await API.agentDev.setTTSModelEnabled(model.id, false); markSpeechModelAvailable(model, false); if (state.selectedTtsModelId === model.id) state.selectedTtsModelId = ''; Toast.info(`TTS 模型「${model.name}」已禁用`); } refreshModal(); }); }); body.querySelectorAll('[data-tts-action="delete-existing"]').forEach(btn => { btn.addEventListener('click', async () => { const registry = ensureTtsModelRegistry(); const model = registry.find(item => item.id === btn.dataset.id); if (!model) return; await API.agentDev.deleteTTSModel(model.id); window.__ttsModelRegistry = registry.filter(item => item.id !== model.id); if (state.selectedTtsModelId === model.id) state.selectedTtsModelId = ''; Toast.success(`TTS 模型「${model.name}」已删除`); refreshModal(); }); }); }; const body = modal.overlay.querySelector('.modal-body'); if (body) { lucide.createIcons({ elements: [body] }); bindTtsModelManagerEvents(body); } } function getTextModelDisplayStatus(model) { return getSpeechModelDisplayStatus(model); } function getTextStatusMeta(model) { return TEXT_MODEL_STATUS_META[getTextModelDisplayStatus(model)] || TEXT_MODEL_STATUS_META.failed; } function getTextModelValidationError(model) { const apiKey = String(model.apiKey || '').trim().toLowerCase(); const baseUrl = String(model.baseUrl || '').trim().toLowerCase(); const modelCode = String(model.modelCode || '').trim().toLowerCase(); if (!apiKey || apiKey.includes('invalid') || apiKey.startsWith('bad') || apiKey === 'sk-invalid') { return 'Key 无效'; } if (!/^https?:\/\//i.test(model.baseUrl || '') || /down|timeout|error|404|500/.test(baseUrl)) { return '接口异常'; } if (!modelCode || /missing|notfound|not-found|none|404/.test(modelCode)) { return '模型 ID 不存在'; } if (!Number.isFinite(Number(model.contextWindow)) || Number(model.contextWindow) < 1024) { return '上下文长度无效'; } return ''; } function renderTextModelManagerContent(showForm = false) { const models = ensureTextModelRegistry(); return `
文本模型凭证统一维护在这里。创建模型后系统会自动校验 API Key、接口连通性和模型 ID,状态为「可用」且已启用后才会进入智能体的文本模型下拉框。
${showForm ? `
新增文本模型配置
适用于通义千问、DeepSeek、ChatGPT、豆包或 OpenAI Compatible 接口。
` : `
文本模型列表
模型创建并自动校验通过后,才会出现在智能体配置下拉框中。
`}
已添加模型
${models.length} 个配置
${models.length ? models.map(model => { const meta = getTextStatusMeta(model); const status = getTextModelDisplayStatus(model); const canToggle = status === 'available' || status === 'disabled'; return `
${escapeHtml(model.name)} ${escapeHtml(meta.label)}
Provider:${escapeHtml(model.provider)} 模型 ID:${escapeHtml(model.modelCode)} 协议:${escapeHtml(model.protocol || 'openai-compatible')}
Base URL:${escapeHtml(model.baseUrl)} API Key:${escapeHtml(maskSecret(model.apiKey))} 上下文:${escapeHtml(model.contextWindow || '-')} tokens 最大输出:${escapeHtml(model.maxOutputTokens || '-')} tokens
Secret Key:${escapeHtml(maskSecret(model.secretKey))} 更新时间:${escapeHtml(model.updatedAt || '-')} ${model.validatedAt ? `最近校验:${escapeHtml(model.validatedAt)}` : ''}
${model.errorMessage ? `
失败原因:${escapeHtml(model.errorMessage)}
` : ''}
`; }).join('') : `
暂无文本模型 创建并自动校验成功后,智能体即可选择该文本模型。
`}
`; } function collectTextModelForm(body) { const name = body.querySelector('#textModelName')?.value.trim(); const provider = body.querySelector('#textProviderName')?.value.trim(); const modelCode = body.querySelector('#textModelCode')?.value.trim(); const protocol = body.querySelector('#textProtocol')?.value || 'openai-compatible'; const baseUrl = body.querySelector('#textBaseUrl')?.value.trim(); const apiKey = body.querySelector('#textApiKey')?.value.trim(); const secretKey = body.querySelector('#textSecretKey')?.value.trim(); const contextWindow = Number.parseInt(body.querySelector('#textContextWindow')?.value || '128000', 10); const maxOutputTokens = Number.parseInt(body.querySelector('#textMaxOutputTokens')?.value || '4096', 10); const enableAfterValidation = body.querySelector('#textEnableAfterValidation')?.checked !== false; if (!name || !provider || !modelCode || !baseUrl || !apiKey) { Toast.warning('请填写模型名称、Provider、模型 ID、Base URL 和 API Key'); return null; } if (!/^https?:\/\//i.test(baseUrl)) { Toast.warning('Base URL 需要以 http:// 或 https:// 开头'); return null; } if (!Number.isFinite(contextWindow) || contextWindow < 1024) { Toast.warning('上下文长度至少为 1024 tokens'); return null; } if (!Number.isFinite(maxOutputTokens) || maxOutputTokens < 256) { Toast.warning('最大输出 Token 至少为 256'); return null; } return { id: `text-${Date.now()}`, name, provider, modelCode, protocol, baseUrl, apiKey, secretKey, contextWindow, maxOutputTokens, enableAfterValidation, status: 'validating', enabled: false, updatedAt: getNowText(), }; } function validateTextModelConnection(model) { const validationError = getTextModelValidationError(model); if (validationError) { return new Promise(resolve => setTimeout(() => resolve({ success: false, message: validationError }), 750)); } const validate = API.agentDev.validateTextModel || API.agentDev.testTextModel; if (!validate) { return Promise.resolve({ success: true, data: model }); } return validate(model.id, { provider: model.provider, modelCode: model.modelCode, protocol: model.protocol, baseUrl: model.baseUrl, contextWindow: model.contextWindow, maxOutputTokens: model.maxOutputTokens, }); } function openTextModelManagerModal(state, rerender) { let showCreateForm = false; const modal = createModal({ title: '文本模型管理', size: 'xl', showCancel: false, confirmText: '关闭', content: renderTextModelManagerContent(showCreateForm), onConfirm: () => true, }); const refreshModal = () => { const body = modal.overlay.querySelector('.modal-body'); if (!body) return; body.innerHTML = renderTextModelManagerContent(showCreateForm); lucide.createIcons({ elements: [body] }); bindTextModelManagerEvents(body); rerender(); }; const finishValidation = async (model, shouldEnable = true) => { const result = await validateTextModelConnection(model); if (result.success === false) { markSpeechModelFailed(model, result.message || '文本模型连接失败'); Toast.error(model.errorMessage); } else { markSpeechModelAvailable(model, shouldEnable); if (model.enabled && !state.selectedTextModelId) state.selectedTextModelId = model.id; Toast.success(`文本模型「${model.name}」校验通过`); } }; const bindTextModelManagerEvents = (body) => { body.querySelectorAll('[data-text-action="show-create"]').forEach(btn => btn.addEventListener('click', () => { showCreateForm = true; refreshModal(); })); body.querySelectorAll('[data-text-action="cancel-create"]').forEach(btn => btn.addEventListener('click', () => { showCreateForm = false; refreshModal(); })); body.querySelectorAll('[data-text-action="create-new"]').forEach(btn => btn.addEventListener('click', async () => { const model = collectTextModelForm(body); if (!model) return; markSpeechModelValidating(model); ensureTextModelRegistry().push(model); await API.agentDev.createTextModel(model); Toast.info(`文本模型「${model.name}」开始自动校验`, 800); showCreateForm = false; refreshModal(); await finishValidation(model, model.enableAfterValidation !== false); refreshModal(); })); body.querySelectorAll('[data-text-action="validate-existing"]').forEach(btn => { btn.addEventListener('click', async () => { const model = ensureTextModelRegistry().find(item => item.id === btn.dataset.id); if (!model) return; markSpeechModelValidating(model); refreshModal(); await finishValidation(model, true); refreshModal(); }); }); body.querySelectorAll('[data-text-action="toggle-existing"]').forEach(btn => { btn.addEventListener('click', async () => { const model = ensureTextModelRegistry().find(item => item.id === btn.dataset.id); if (!model) return; const status = getTextModelDisplayStatus(model); if (status !== 'available' && status !== 'disabled') { Toast.warning('只有校验通过的模型才可以启用或禁用'); return; } if (status === 'disabled') { await API.agentDev.setTextModelEnabled(model.id, true); markSpeechModelAvailable(model, true); if (!state.selectedTextModelId) state.selectedTextModelId = model.id; Toast.success(`文本模型「${model.name}」已启用`); } else { await API.agentDev.setTextModelEnabled(model.id, false); markSpeechModelAvailable(model, false); if (state.selectedTextModelId === model.id) state.selectedTextModelId = ''; Toast.info(`文本模型「${model.name}」已禁用`); } refreshModal(); }); }); body.querySelectorAll('[data-text-action="delete-existing"]').forEach(btn => { btn.addEventListener('click', async () => { const registry = ensureTextModelRegistry(); const model = registry.find(item => item.id === btn.dataset.id); if (!model) return; await API.agentDev.deleteTextModel(model.id); window.__textModelRegistry = registry.filter(item => item.id !== model.id); if (state.selectedTextModelId === model.id) state.selectedTextModelId = ''; Toast.success(`文本模型「${model.name}」已删除`); refreshModal(); }); }); }; const body = modal.overlay.querySelector('.modal-body'); if (body) { lucide.createIcons({ elements: [body] }); bindTextModelManagerEvents(body); } } function renderPromptEditor(prompt) { return `

Prompt 提示词

`; } function renderKnowledgeBaseSection(items) { return renderSection('知识库', `
${items.length ? items.map((item, index) => `
${escapeHtml(item.name)}
${escapeHtml(item.desc)}
权重 ${escapeHtml(item.weight)}
`).join('') : `
暂无挂载知识库
`}
`, '', `
`); } function getKnowledgeBaseCatalog() { const catalog = Array.isArray(window.__knowledgeBaseCatalog) && window.__knowledgeBaseCatalog.length ? window.__knowledgeBaseCatalog : AGENT_KNOWLEDGE_BASE_CATALOG; return catalog.map(item => ({ id: item.id, name: item.name, description: item.description || item.desc || '', docs: item.docs || item.docCount || 0, size: item.size || '', typeName: item.typeName || '文档搜索', status: item.status || 'ready', tags: Array.isArray(item.tags) ? item.tags : [], })); } function getKnowledgeBaseStatusLabel(status) { const map = { ready: '已就绪', processing: '处理中', uploading: '上传中', error: '异常', }; return map[status] || status || '未知'; } function openKnowledgeBasePickerModal(state, rerender) { const catalog = getKnowledgeBaseCatalog(); const attached = state.knowledgeBases || []; const selectedIds = new Set(attached.map(item => item.id || item.name)); let searchValue = ''; const modal = createModal({ title: '添加知识库', size: 'xl', confirmText: '挂载所选', cancelText: '取消', content: `
`, onConfirm: () => { if (!selectedIds.size) { Toast.warning('请先选择需要挂载的知识库'); return false; } const previousById = new Map(attached.map(item => [item.id || item.name, item])); state.knowledgeBases = catalog .filter(item => selectedIds.has(item.id)) .map(item => { const existing = previousById.get(item.id); return { id: item.id, name: item.name, desc: item.description, weight: existing?.weight || '1.00', docs: item.docs, status: item.status, }; }); Toast.success(`已挂载 ${state.knowledgeBases.length} 个知识库`); rerender(); return true; }, }); const overlay = modal.overlay; const listEl = overlay.querySelector('#agentKbPickerList'); const searchInput = overlay.querySelector('#agentKbPickerSearch'); function getFilteredCatalog() { const keyword = searchValue.trim().toLowerCase(); if (!keyword) return catalog; return catalog.filter(item => [ item.name, item.description, item.typeName, ...item.tags, ].some(value => String(value || '').toLowerCase().includes(keyword))); } function renderList() { const filtered = getFilteredCatalog(); listEl.innerHTML = filtered.length ? filtered.map(item => ` `).join('') : `
没有找到匹配的知识库
`; listEl.querySelectorAll('input[type="checkbox"]').forEach(input => { input.addEventListener('change', () => { if (input.checked) { selectedIds.add(input.value); } else { selectedIds.delete(input.value); } renderList(); renderAgentIcons(overlay); }); }); } searchInput?.addEventListener('input', event => { searchValue = event.target.value; renderList(); renderAgentIcons(overlay); }); overlay.querySelector('#agentCreateKbFromPicker')?.addEventListener('click', () => { modal.close?.(); navigateTo('kb-create'); }); renderList(); renderAgentIcons(overlay); } function renderMiniAppSection(title, items, iconName) { return renderSection(title, `
${items.map(item => `
${escapeHtml(item.name)} ${escapeHtml(item.desc)}
`).join('')}
`); } function getMcpStatusMeta(service) { return MCP_SERVICE_STATUS_META[service?.status || 'pending'] || MCP_SERVICE_STATUS_META.pending; } function renderMcpSection(state) { const library = ensureMcpServiceLibrary(); const selectedIds = new Set(state.selectedMcpServiceIds || []); const services = library.filter(item => selectedIds.has(item.id)); return renderSection('MCP服务', `
智能体可以通过标准化协议 MCP 连接企业内部服务 API,并将服务工具暴露给模型调用。
${services.length ? services.map(item => { const meta = getMcpStatusMeta(item); const tools = Array.isArray(item.tools) ? item.tools.filter(tool => tool.name) : []; return `
${escapeHtml(item.name)} ${escapeHtml(item.namespace || 'default')}
${escapeHtml(item.desc)}
${escapeHtml(item.transport || 'Streamable HTTP')} ${tools.length} 个工具 ${escapeHtml(item.endpoint || '-')}
${escapeHtml(meta.label)}
`; }).join('') : `
暂无已挂载 MCP 服务 从 0 开始创建 MCP 服务,连接通过后可挂载到当前智能体。
`}
`); } function bindWorkbenchEvents(container, state, rerender) { container.querySelectorAll('[data-step]').forEach(tab => { tab.addEventListener('click', () => { state.activeStep = tab.dataset.step; updateActiveStepTab(container, state.activeStep); scrollToAgentStep(container, state.activeStep); }); }); bindStepScrollSpy(container, state); container.querySelectorAll('[data-toggle]').forEach(input => { input.addEventListener('change', () => { const key = input.dataset.toggle; const checked = input.checked; const map = { 'voice-enabled': 'voiceEnabled', 'asr-enabled': 'asrEnabled', 'tts-enabled': 'ttsEnabled', 'tts-filter': 'ttsFilterEnabled', 'interrupt-primary': 'interruptPrimaryEnabled', 'interrupt-secondary': 'interruptSecondaryEnabled', 'semantic-stop': 'semanticStopEnabled', 'intent-enabled': 'intentEnabled', 'continuation': 'dialogueContinuationEnabled', 'knowledge-search': 'knowledgeSearchEnabled', 'long-term-memory': 'longTermMemoryEnabled', }; const stateKey = map[key]; if (stateKey) { state[stateKey] = checked; rerender(); } }); }); container.querySelectorAll('[data-action="text-model"]').forEach(select => { select.addEventListener('change', () => { state.selectedTextModelId = select.value; rerender(); }); }); container.querySelectorAll('[data-action="asr-model"]').forEach(select => { select.addEventListener('change', () => { state.selectedAsrModelId = select.value; rerender(); }); }); container.querySelectorAll('[data-action="tts-model"]').forEach(select => { select.addEventListener('change', () => { state.selectedTtsModelId = select.value; rerender(); }); }); container.querySelectorAll('[data-action="hotword-group"]').forEach(select => { select.addEventListener('change', () => { state.selectedHotwordGroupId = select.value; rerender(); }); }); container.querySelectorAll('[data-action="interaction-language"]').forEach(select => { select.addEventListener('change', () => { state.selectedInteractionLanguage = select.value; window.__selectedInteractionLanguage = select.value; rerender(); }); }); container.querySelectorAll('[data-action="delete-variable"]').forEach(btn => { btn.addEventListener('click', event => { event.preventDefault(); event.stopImmediatePropagation(); const variable = (state.variables || []).find(item => item.id === btn.dataset.variableId); if (!variable) return; state.variables = (state.variables || []).filter(item => item.id !== variable.id); Toast.success(variable.name ? `变量 ${variable.name} 已删除` : '变量已删除'); rerender(); }); }); container.querySelectorAll('[data-variable-field]').forEach(input => { input.addEventListener('input', () => { const variable = (state.variables || []).find(item => item.id === input.dataset.variableId); if (!variable) return; variable[input.dataset.variableField] = input.value; scheduleAgentConfigAutoSave(state); }); }); container.querySelectorAll('[data-action="remove-kb"]').forEach(btn => { btn.addEventListener('click', event => { event.preventDefault(); event.stopImmediatePropagation(); const index = Number.parseInt(btn.dataset.kbIndex, 10); const list = state.knowledgeBases || []; const target = list[index]; if (!target) return; state.knowledgeBases = list.filter((_, itemIndex) => itemIndex !== index); Toast.success(`知识库「${target.name}」已移除`); rerender(); }); }); container.querySelectorAll('[data-action="remove-command"]').forEach(btn => { btn.addEventListener('click', event => { event.preventDefault(); event.stopImmediatePropagation(); const commandId = btn.dataset.commandId; const command = ensureCommandLibrary().find(item => item.id === commandId); state.selectedCommandIds = (state.selectedCommandIds || []).filter(id => id !== commandId); Toast.success(command?.name ? `指令「${command.name}」已从当前智能体移除` : '指令已移除'); rerender(); }); }); container.querySelectorAll('[data-action="remove-plugin"]').forEach(btn => { btn.addEventListener('click', event => { event.preventDefault(); event.stopImmediatePropagation(); const pluginId = btn.dataset.pluginId; const plugin = ensurePluginLibrary().find(item => item.id === pluginId); state.selectedPluginIds = (state.selectedPluginIds || []).filter(id => id !== pluginId); Toast.success(plugin?.name ? `插件「${plugin.name}」已从当前智能体移除` : '插件已移除'); rerender(); }); }); container.querySelectorAll('[data-action="remove-mcp-service"]').forEach(btn => { btn.addEventListener('click', event => { event.preventDefault(); event.stopImmediatePropagation(); const serviceId = btn.dataset.mcpId; const service = ensureMcpServiceLibrary().find(item => item.id === serviceId); state.selectedMcpServiceIds = (state.selectedMcpServiceIds || []).filter(id => id !== serviceId); Toast.success(service?.name ? `MCP 服务「${service.name}」已从当前智能体移除` : 'MCP 服务已移除'); rerender(); }); }); container.querySelectorAll('[data-action]').forEach(el => { el.addEventListener('click', event => { const action = el.dataset.action; if (action === 'back') { navigateTo('agent-manage-multimodal'); return; } if (action === 'rename') { openRenameModal(state, rerender); return; } if (action === 'publish') { state.app.status = 'published'; autoSaveAgentConfig(state); Toast.success('应用已提交发布,预计 2-5 分钟生效'); rerender(); return; } if (action === 'run') { autoSaveAgentConfig(state); clearAgentRunTimer(state); const session = getRunSession(state); const configuredKeys = getRuntimeApiKeysForApp(state); const activeKeyStillExists = configuredKeys.some(key => key.value === session.apiKey); if (!activeKeyStillExists) { session.apiKeyConfirmed = false; session.apiKey = ''; session.apiKeyName = ''; session.apiKeyProvider = ''; } session.isRecording = false; session.elapsedSeconds = 0; session.latencyMs = null; session.transcript = ''; session.aiResponse = ''; state.preview = state.voiceEnabled ? (session.apiKeyConfirmed ? 'voice-runtime' : 'voice-auth') : (session.apiKeyConfirmed ? 'text-runtime' : 'text-auth'); updatePreviewOnly(container, state); Toast.success(state.voiceEnabled ? '配置已自动保存,请配置运行 API Key' : '配置已自动保存,请配置文字运行 API Key'); return; } if (action === 'add-kb') { openKnowledgeBasePickerModal(state, rerender); return; } if (action === 'knowledge-weight') { openKnowledgeWeightModal(state, rerender); return; } if (action === 'asr-model-manager') { openAsrModelManagerModal(state, rerender); return; } if (action === 'tts-model-manager') { openTtsModelManagerModal(state, rerender); return; } if (action === 'text-model-manager') { openTextModelManagerModal(state, rerender); return; } if (action === 'add-variable' || action === 'variable') { state.variables = [ ...(state.variables || []), { id: `var-${Date.now()}`, name: '', desc: '' }, ]; rerender(); return; } if (action === 'hotwords') { navigateTo('hotword-library'); return; } if (action === 'tts-filter-config') { openTtsFilterModal(state, rerender); return; } if (action === 'command-config') { openCommandConfigModal(state, rerender); return; } if (action === 'plugin-config') { openPluginConfigModal(state, rerender); return; } if (action === 'mcp-config') { openMcpConfigModal(state, rerender); return; } const actionText = { history: '打开历史记录', api: '打开 API 接入文档', model: '切换模型', variable: '打开变量配置', setting: '打开设置', remove: '已移除当前项', 'add-app': '添加应用', }; Toast.info(actionText[action] || '操作已触发'); event.stopPropagation(); }); }); } function bindRuntimePreviewEvents(container, state, refreshPreview) { const pane = container.querySelector('.bailian-preview-pane'); if (!pane) return; const session = getRunSession(state); pane.querySelector('[data-preview-action="confirm-runtime-key"]')?.addEventListener('click', () => { const select = pane.querySelector('[data-preview-field="runtime-key-select"]'); const keys = getRuntimeApiKeysForApp(state); const selected = keys.find(item => item.id === select?.value); if (!selected) { Toast.warning('请先选择系统设置中已保存的 API Key'); select?.focus(); return; } session.apiKey = selected.value; session.apiKeyName = `${selected.name} · ${maskSecret(selected.value)}`; session.apiKeyProvider = selected.provider; session.apiKeyConfirmed = true; session.isRecording = false; state.preview = state.preview === 'text-auth' || !state.voiceEnabled ? 'text-runtime' : 'voice-runtime'; Toast.success(state.preview === 'text-runtime' ? 'API Key 已配置,可以开始文字聊天' : 'API Key 已配置,可以开始语音测试'); refreshPreview(); }); pane.querySelector('[data-preview-action="go-settings-api-keys"]')?.addEventListener('click', () => { clearAgentRunTimer(state); session.isRecording = false; Toast.info('请在系统设置中添加并保存 API Key'); navigateTo('settings'); }); pane.querySelector('[data-preview-action="change-runtime-key"]')?.addEventListener('click', () => { clearAgentRunTimer(state); session.isRecording = false; session.apiKeyConfirmed = false; state.preview = state.voiceEnabled ? 'voice-auth' : 'text-auth'; refreshPreview(); }); pane.querySelector('[data-preview-action="toggle-call"]')?.addEventListener('click', () => { if (!session.apiKeyConfirmed) { Toast.warning('请先配置运行 API Key'); state.preview = state.voiceEnabled ? 'voice-auth' : 'text-auth'; refreshPreview(); return; } if (session.isRecording) { stopVoiceRuntimeCall(container, state); Toast.info('本轮语音通话已结束', 900); } else { startVoiceRuntimeCall(container, state); Toast.info('已开始录音,请开始说话', 900); } }); pane.querySelector('[data-preview-action="toggle-mic"]')?.addEventListener('click', () => { session.micMuted = !session.micMuted; Toast.info(session.micMuted ? '麦克风已关闭' : '麦克风已打开', 800); if (!syncVoiceRuntimeDom(container, state)) { refreshPreview(); } }); pane.querySelector('[data-preview-action="voice-device"]')?.addEventListener('click', () => { Toast.info('当前为 MacBook Pro 麦克风,后端接入后可读取浏览器音频设备列表', 1200); }); pane.querySelector('[data-preview-action="voice-runtime-settings"]')?.addEventListener('click', () => { Toast.info('语音体验设置入口,后续可接入采样率、降噪和播放设备配置', 1200); }); pane.querySelector('[data-preview-action="copy-voice-ai"]')?.addEventListener('click', async () => { const text = session.aiResponse || session.aiDisplayResponse || ''; if (!text) return; try { await navigator.clipboard?.writeText(text); Toast.success('AI 回复已复制'); } catch (error) { Toast.warning('当前浏览器不允许自动复制,请手动复制'); } }); pane.querySelector('[data-preview-action="regenerate-voice-ai"]')?.addEventListener('click', () => { const base = session.aiResponse || '我已经准备好继续语音对话。'; const next = `${base}(重新生成版本)`; session.aiResponse = next; session.aiDisplayResponse = ''; session.lastVoiceAiResponse = next; Toast.info('正在重新生成语音回复', 800); revealVoiceReply(container, state, next); }); pane.querySelector('[data-preview-field="runtime-text-input"]')?.addEventListener('input', event => { session.textInput = event.target.value; }); pane.querySelector('[data-preview-field="runtime-text-input"]')?.addEventListener('keydown', event => { if (event.key !== 'Enter' || event.shiftKey) return; event.preventDefault(); pane.querySelector('[data-preview-action="send-text-runtime"]')?.click(); }); pane.querySelector('[data-preview-action="send-text-runtime"]')?.addEventListener('click', () => { const input = pane.querySelector('[data-preview-field="runtime-text-input"]'); const value = input?.value.trim() || ''; if (!session.apiKeyConfirmed) { Toast.warning('请先配置运行 API Key'); state.preview = 'text-auth'; refreshPreview(); return; } if (!value) { Toast.warning('请输入测试内容'); input?.focus(); return; } session.chatMessages = [ ...(Array.isArray(session.chatMessages) ? session.chatMessages : []), { role: 'user', content: value }, ]; session.textInput = ''; session.textPending = true; session.latencyMs = null; refreshPreview(); window.setTimeout(() => { const reply = buildMockTextReply(value); session.textPending = false; session.latencyMs = 390 + (value.length * 13) % 260; const messages = Array.isArray(session.chatMessages) ? session.chatMessages : []; messages.push({ role: 'ai', sourceText: value, retryCount: 0, content: reply, displayContent: '', }); session.chatMessages = messages; const messageIndex = messages.length - 1; refreshPreview(); revealTextReply(container, state, messageIndex, reply); }, 850); }); pane.querySelectorAll('[data-preview-action="copy-ai-message"]').forEach(btn => { btn.addEventListener('click', async () => { const index = Number.parseInt(btn.dataset.messageIndex, 10); const message = (session.chatMessages || [])[index]; const text = message?.content || ''; if (!text) return; try { await navigator.clipboard?.writeText(text); Toast.success('AI 回复已复制'); } catch (error) { Toast.warning('当前浏览器不允许自动复制,请手动复制'); } }); }); pane.querySelectorAll('[data-preview-action="regenerate-ai-message"]').forEach(btn => { btn.addEventListener('click', () => { const index = Number.parseInt(btn.dataset.messageIndex, 10); const message = (session.chatMessages || [])[index]; if (!message || message.role !== 'ai') return; const nextRetry = (message.retryCount || 0) + 1; const sourceText = message.sourceText || '这条消息'; const reply = buildMockTextReply(sourceText, nextRetry); message.retryCount = nextRetry; message.content = reply; message.displayContent = ''; session.latencyMs = 420 + (nextRetry * 37) % 180; Toast.info('正在重新生成回复', 800); refreshPreview(); revealTextReply(container, state, index, reply); }); }); } function updateActiveStepTab(container, stepId) { container.querySelectorAll('.bailian-step-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.step === stepId); }); } function scrollToAgentStep(container, stepId) { const scrollArea = container.querySelector('.bailian-config-scroll'); const target = container.querySelector(`#agent-step-${stepId}`); if (!scrollArea || !target) return; const offset = scrollArea.scrollTop + target.getBoundingClientRect().top - scrollArea.getBoundingClientRect().top - 8; scrollArea.scrollTo({ top: Math.max(offset, 0), behavior: 'smooth' }); } function bindStepScrollSpy(container, state) { const scrollArea = container.querySelector('.bailian-config-scroll'); if (!scrollArea) return; let ticking = false; scrollArea.addEventListener('scroll', () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { const marker = scrollArea.getBoundingClientRect().top + 96; let currentStep = AGENT_CONFIG_STEPS[0].id; AGENT_CONFIG_STEPS.forEach(step => { const section = container.querySelector(`#agent-step-${step.id}`); if (section && section.getBoundingClientRect().top <= marker) { currentStep = step.id; } }); if (state.activeStep !== currentStep) { state.activeStep = currentStep; updateActiveStepTab(container, currentStep); } ticking = false; }); }); } function updatePreviewOnly(container, state) { const preview = container.querySelector('.bailian-preview-pane'); if (!preview) return; const temp = document.createElement('div'); temp.innerHTML = renderPreviewPane(state); preview.replaceWith(temp.firstElementChild); renderAgentIcons(container.querySelector('.bailian-preview-pane')); bindRuntimePreviewEvents(container, state, () => updatePreviewOnly(container, state)); const textMessageArea = container.querySelector('.agent-text-message-area'); if (textMessageArea) { textMessageArea.scrollTop = textMessageArea.scrollHeight; } const voiceScrollArea = container.querySelector('.agent-voice-stage') || container.querySelector('.agent-voice-conversation'); if (voiceScrollArea) { voiceScrollArea.scrollTop = voiceScrollArea.scrollHeight; } } function bindPromptCounter(container, state) { const editor = container.querySelector('#agentPromptEditor'); const counter = container.querySelector('#promptCounter'); if (!editor || !counter) return; const update = () => { counter.textContent = `${editor.value.length}/8000`; if (state) { state.prompt = editor.value; scheduleAgentConfigAutoSave(state); } }; editor.addEventListener('input', update); counter.textContent = `${editor.value.length}/8000`; } function openKnowledgeWeightModal(state, rerender) { const draft = (state.knowledgeBases || AGENT_CONFIG_MOCK.understand.knowledgeBases).map(item => ({ ...item })); const modal = createModal({ title: '知识库权重配置', size: 'lg', confirmText: '确定', cancelText: '取消', content: `
调整知识库权重可人工干预召回片段排序。知识库召回片段得分 = 知识库权重 × 切片相似度。
${draft.map((item, index) => `
${escapeHtml(item.name)}
${escapeHtml(item.desc)}
`).join('')}
`, onConfirm: () => { const inputs = Array.from(modal.overlay.querySelectorAll('.bailian-weight-input')); const next = inputs.map((input, index) => { const weight = Number.parseFloat(input.value); if (Number.isNaN(weight) || weight < 0.5 || weight > 2) { Toast.warning('知识库权重需在 0.5 到 2 之间'); input.focus(); return null; } return { ...draft[index], weight: weight.toFixed(2) }; }); if (next.some(item => !item)) return false; state.knowledgeBases = next; Toast.success('知识库权重已更新'); rerender(); return true; }, }); renderAgentIcons(modal.overlay); } function openTtsFilterModal(state, rerender) { const commonPairs = [ { id: 'slash', left: '/*', right: '*/', label: '/* */' }, { id: 'square', left: '[', right: ']', label: '[ ]' }, { id: 'curly', left: '{', right: '}', label: '{ }' }, ]; const getPairKey = (pair) => `${String(pair.left || '').trim()}__${String(pair.right || '').trim()}`; const commonPairKeys = new Set(commonPairs.map(getPairKey)); const savedPairs = (state.ttsFilterPairs || []).map(item => ({ ...item })); const draftPairs = savedPairs.filter(pair => !commonPairKeys.has(getPairKey(pair))); const draftSingles = [...new Set((state.ttsFilterSingles || []).map(item => String(item || '').trim()).filter(Boolean))]; const modal = createModal({ title: '语音合成播报过滤设置', size: 'xl', confirmText: '确定', cancelText: '取消', content: `
选中符号对内的文字将不会通过 TTS 播报;单字符过滤在所有场景下都不会播报,请谨慎评估业务文本特点。
常用过滤符号对
${commonPairs.map(item => ` `).join('')}
自定义过滤符号对
自定义过滤单字符
`, onConfirm: () => { const pairRows = Array.from(modal.overlay.querySelectorAll('[data-filter-pair-row]')); const singleRows = Array.from(modal.overlay.querySelectorAll('[data-filter-single-row]')); const nextPairs = []; const nextPairKeys = new Set(); const pushPair = (leftValue, rightValue) => { const left = String(leftValue || '').trim(); const right = String(rightValue || '').trim(); if (!left && !right) return; const key = `${left}__${right}`; if (nextPairKeys.has(key)) return; nextPairKeys.add(key); nextPairs.push({ enabled: true, left, right }); }; commonPairs.forEach(item => { const enabled = modal.overlay.querySelector(`[data-common-pair="${item.id}"]`)?.checked; if (enabled) pushPair(item.left, item.right); }); pairRows.forEach(row => { const left = row.querySelector('[data-left]')?.value.trim() || ''; const right = row.querySelector('[data-right]')?.value.trim() || ''; pushPair(left, right); }); const nextSingles = [...new Set(singleRows.map(row => row.querySelector('[data-single]')?.value.trim() || '').filter(Boolean))]; state.ttsFilterEnabled = true; state.ttsFilterPairs = nextPairs; state.ttsFilterSingles = nextSingles; Toast.success('播报过滤设置已保存'); rerender(); return true; }, }); const overlay = modal.overlay; const pairList = overlay.querySelector('#filterPairList'); const singleList = overlay.querySelector('#filterSingleList'); function renderPairRows() { pairList.innerHTML = draftPairs.length ? draftPairs.map((pair, index) => `
`).join('') : '
暂无自定义符号对
'; } function renderSingleRows() { singleList.innerHTML = draftSingles.length ? draftSingles.map((char, index) => `
`).join('') : '
暂无自定义单字符
'; } function bindDynamicEvents() { overlay.querySelectorAll('[data-delete-pair]').forEach(btn => { btn.addEventListener('click', () => { draftPairs.splice(Number(btn.dataset.deletePair), 1); renderPairRows(); bindDynamicEvents(); renderAgentIcons(overlay); }); }); overlay.querySelectorAll('[data-delete-single]').forEach(btn => { btn.addEventListener('click', () => { draftSingles.splice(Number(btn.dataset.deleteSingle), 1); renderSingleRows(); bindDynamicEvents(); renderAgentIcons(overlay); }); }); } overlay.querySelector('#addFilterPairBtn')?.addEventListener('click', () => { draftPairs.push({ enabled: true, left: '', right: '' }); renderPairRows(); bindDynamicEvents(); renderAgentIcons(overlay); }); overlay.querySelector('#addFilterSingleBtn')?.addEventListener('click', () => { draftSingles.push(''); renderSingleRows(); bindDynamicEvents(); renderAgentIcons(overlay); }); renderPairRows(); renderSingleRows(); bindDynamicEvents(); renderAgentIcons(overlay); } function openCommandConfigModal(state, rerender) { const library = ensureCommandLibrary().map(item => ({ ...item })); const selectedIds = new Set((state.selectedCommandIds || []).filter(id => library.some(item => item.id === id))); let draftForm = null; const replyModes = ['根据执行情况回复', '执行前回复', '执行后回复', '不回复']; const paramTypes = ['string', 'number', 'boolean', 'object', 'array']; const createEmptyCommand = () => ({ id: '', icon: 'terminal-square', commandType: '自定义指令', name: '', desc: '', example: '', paramName: '', paramType: 'string', paramDesc: '', replyMode: '根据执行情况回复', count: 1, }); const modal = createModal({ title: '指令配置', size: 'xl', confirmText: '保存挂载', cancelText: '取消', content: `
先创建指令到指令库,再勾选挂载到当前智能体。删除当前智能体里的指令只会移除挂载关系,不会删除指令库定义。
`, onConfirm: () => { if (draftForm) { Toast.warning('请先保存或取消正在编辑的指令'); return false; } window.__agentCommandLibrary = library.map(item => ({ ...item })); state.selectedCommandIds = Array.from(selectedIds).filter(id => library.some(item => item.id === id)); Toast.success(`已保存 ${state.selectedCommandIds.length} 个已挂载指令`); rerender(); return true; }, }); const overlay = modal.overlay; const libraryPanel = overlay.querySelector('#commandLibraryPanel'); const editorPanel = overlay.querySelector('#commandEditorPanel'); function renderLibraryPanel() { libraryPanel.innerHTML = `

指令库

${library.length} 条
${library.length ? library.map(item => `
${escapeHtml(item.name)} ${escapeHtml(item.commandType || '自定义指令')}

${escapeHtml(item.desc)}

示例:${escapeHtml(item.example || '-')} 参数:${escapeHtml(item.paramName || '-')} 回复:${escapeHtml(item.replyMode || '根据执行情况回复')}
`).join('') : `
暂无可用指令 点击创建指令,从 0 开始配置当前智能体可调用的能力。
`}
`; } function renderEditorPanel() { if (!draftForm) { editorPanel.innerHTML = ''; return; } editorPanel.innerHTML = `

${draftForm.id ? '编辑指令' : '创建指令'}

`; } function readDraftForm() { overlay.querySelectorAll('[data-command-field]').forEach(field => { draftForm[field.dataset.commandField] = field.value.trim(); }); } function saveDraftForm() { readDraftForm(); if (!draftForm.name || !draftForm.desc || !draftForm.commandType) { Toast.warning('请填写指令类型、指令名称和指令说明'); return; } const duplicate = library.some(item => item.id !== draftForm.id && item.name === draftForm.name); if (duplicate) { Toast.warning('指令名称已存在'); return; } if (!draftForm.id) { draftForm.id = `cmd-${Date.now()}`; library.push({ ...draftForm, icon: 'terminal-square', count: 1 }); } else { const index = library.findIndex(item => item.id === draftForm.id); if (index >= 0) library[index] = { ...library[index], ...draftForm, icon: library[index].icon || 'terminal-square', count: 1 }; } selectedIds.add(draftForm.id); Toast.success(`指令「${draftForm.name}」已保存`); draftForm = null; renderAll(); } function bindCommandEvents() { overlay.querySelector('#createCommandBtn')?.addEventListener('click', () => { draftForm = createEmptyCommand(); renderAll(); }); overlay.querySelector('#createCommandEmptyBtn')?.addEventListener('click', () => { draftForm = createEmptyCommand(); renderAll(); }); overlay.querySelectorAll('.bailian-command-select input').forEach(input => { input.addEventListener('change', () => { if (input.checked) selectedIds.add(input.value); else selectedIds.delete(input.value); renderAll(); }); }); overlay.querySelectorAll('.bailian-command-library-card').forEach(card => { card.addEventListener('click', event => { if (event.target.closest('button') || event.target.closest('label')) return; const input = card.querySelector('.bailian-command-select input'); if (!input) return; if (selectedIds.has(input.value)) selectedIds.delete(input.value); else selectedIds.add(input.value); renderAll(); }); }); overlay.querySelectorAll('[data-edit-command]').forEach(btn => { btn.addEventListener('click', () => { const target = library.find(item => item.id === btn.dataset.editCommand); if (!target) return; draftForm = { ...target }; renderAll(); }); }); overlay.querySelectorAll('[data-delete-library-command]').forEach(btn => { btn.addEventListener('click', () => { const commandId = btn.dataset.deleteLibraryCommand; const index = library.findIndex(item => item.id === commandId); if (index < 0) return; const [deleted] = library.splice(index, 1); selectedIds.delete(commandId); if (draftForm?.id === commandId) draftForm = null; Toast.success(`指令「${deleted.name}」已删除`); renderAll(); }); }); overlay.querySelector('#cancelCommandEditBtn')?.addEventListener('click', () => { draftForm = null; renderAll(); }); overlay.querySelector('#cancelCommandEditBottomBtn')?.addEventListener('click', () => { draftForm = null; renderAll(); }); overlay.querySelector('#saveCommandEditBtn')?.addEventListener('click', saveDraftForm); } function renderAll() { renderLibraryPanel(); renderEditorPanel(); bindCommandEvents(); renderAgentIcons(overlay); } renderAll(); } function openPluginConfigModal(state, rerender) { const library = ensurePluginLibrary().map(item => ({ ...item })); const selectedIds = new Set((state.selectedPluginIds || []).filter(id => library.some(item => item.id === id))); let draftForm = null; const authTypes = ['无鉴权', 'API Key', 'Bearer Token', 'OAuth2', '自定义签名']; const executionModes = ['系统执行', '端侧执行', '工作流执行', '第三方服务执行']; const statusOptions = ['系统执行', '已启用', '待接入', '已禁用']; const createEmptyPlugin = () => ({ id: '', icon: 'plug', pluginType: '自定义插件', name: '', desc: '', provider: '', endpoint: '', authType: 'API Key', executionMode: '系统执行', status: '系统执行', }); const modal = createModal({ title: '插件配置', size: 'xl', confirmText: '保存挂载', cancelText: '取消', content: `
先创建插件到插件库,再勾选挂载到当前智能体。删除当前智能体里的插件只会移除挂载关系,不会删除插件库定义。
`, onConfirm: () => { if (draftForm) { Toast.warning('请先保存或取消正在编辑的插件'); return false; } window.__agentPluginLibrary = library.map(item => ({ ...item })); state.selectedPluginIds = Array.from(selectedIds).filter(id => library.some(item => item.id === id)); Toast.success(`已保存 ${state.selectedPluginIds.length} 个已挂载插件`); rerender(); return true; }, }); const overlay = modal.overlay; const libraryPanel = overlay.querySelector('#pluginLibraryPanel'); const editorPanel = overlay.querySelector('#pluginEditorPanel'); function renderLibraryPanel() { libraryPanel.innerHTML = `

插件库

${library.length} 个
${library.length ? library.map(item => `
${escapeHtml(item.name)} ${escapeHtml(item.pluginType || '自定义插件')}

${escapeHtml(item.desc)}

Provider:${escapeHtml(item.provider || '-')} 调用地址:${escapeHtml(item.endpoint || '-')} 鉴权:${escapeHtml(item.authType || '-')} 执行:${escapeHtml(item.executionMode || '系统执行')}
`).join('') : `
暂无可用插件 点击创建插件,从 0 开始配置当前智能体可调用的外部能力。
`}
`; } function renderEditorPanel() { if (!draftForm) { editorPanel.innerHTML = ''; return; } editorPanel.innerHTML = `

${draftForm.id ? '编辑插件' : '创建插件'}

`; } function readDraftForm() { overlay.querySelectorAll('[data-plugin-field]').forEach(field => { draftForm[field.dataset.pluginField] = field.value.trim(); }); } function saveDraftForm() { readDraftForm(); if (!draftForm.name || !draftForm.desc || !draftForm.pluginType) { Toast.warning('请填写插件类型、插件名称和插件说明'); return; } const duplicate = library.some(item => item.id !== draftForm.id && item.name === draftForm.name); if (duplicate) { Toast.warning('插件名称已存在'); return; } if (!draftForm.id) { draftForm.id = `plugin-${Date.now()}`; library.push({ ...draftForm, icon: draftForm.icon || 'plug' }); } else { const index = library.findIndex(item => item.id === draftForm.id); if (index >= 0) library[index] = { ...library[index], ...draftForm, icon: draftForm.icon || library[index].icon || 'plug' }; } selectedIds.add(draftForm.id); Toast.success(`插件「${draftForm.name}」已保存`); draftForm = null; renderAll(); } function bindPluginEvents() { overlay.querySelector('#createPluginBtn')?.addEventListener('click', () => { draftForm = createEmptyPlugin(); renderAll(); }); overlay.querySelector('#createPluginEmptyBtn')?.addEventListener('click', () => { draftForm = createEmptyPlugin(); renderAll(); }); overlay.querySelectorAll('.bailian-command-select input').forEach(input => { input.addEventListener('change', () => { if (input.checked) selectedIds.add(input.value); else selectedIds.delete(input.value); renderAll(); }); }); overlay.querySelectorAll('.bailian-command-library-card').forEach(card => { card.addEventListener('click', event => { if (event.target.closest('button') || event.target.closest('label')) return; const input = card.querySelector('.bailian-command-select input'); if (!input) return; if (selectedIds.has(input.value)) selectedIds.delete(input.value); else selectedIds.add(input.value); renderAll(); }); }); overlay.querySelectorAll('[data-edit-plugin]').forEach(btn => { btn.addEventListener('click', () => { const target = library.find(item => item.id === btn.dataset.editPlugin); if (!target) return; draftForm = { ...target }; renderAll(); }); }); overlay.querySelectorAll('[data-delete-library-plugin]').forEach(btn => { btn.addEventListener('click', () => { const pluginId = btn.dataset.deleteLibraryPlugin; const index = library.findIndex(item => item.id === pluginId); if (index < 0) return; const [deleted] = library.splice(index, 1); selectedIds.delete(pluginId); if (draftForm?.id === pluginId) draftForm = null; Toast.success(`插件「${deleted.name}」已删除`); renderAll(); }); }); overlay.querySelector('#cancelPluginEditBtn')?.addEventListener('click', () => { draftForm = null; renderAll(); }); overlay.querySelector('#cancelPluginEditBottomBtn')?.addEventListener('click', () => { draftForm = null; renderAll(); }); overlay.querySelector('#savePluginEditBtn')?.addEventListener('click', saveDraftForm); } function renderAll() { renderLibraryPanel(); renderEditorPanel(); bindPluginEvents(); renderAgentIcons(overlay); } renderAll(); } function getMcpConnectionError(service) { const endpoint = String(service.endpoint || '').trim(); const authType = String(service.authType || '').trim(); const credential = String(service.credential || '').trim(); if (!service.name || !service.desc) return '服务名称和服务说明不能为空'; if (!endpoint) return 'Server URL 不能为空'; if (!/^https?:\/\//i.test(endpoint) && !/^wss?:\/\//i.test(endpoint) && !/^stdio:/i.test(endpoint)) { return 'Server URL 格式不正确'; } if (/fail|error|timeout|invalid|404|500/i.test(endpoint)) { return '服务不可访问'; } if (authType !== '无鉴权' && !credential) { return '缺少鉴权凭证'; } return ''; } function openMcpConfigModal(state, rerender) { const library = ensureMcpServiceLibrary().map(item => ({ ...item, tools: Array.isArray(item.tools) ? item.tools.map(tool => ({ ...tool })) : [], })); const selectedIds = new Set((state.selectedMcpServiceIds || []).filter(id => library.some(item => item.id === id))); let draftForm = null; const transports = ['Streamable HTTP', 'SSE', 'WebSocket', 'stdio(后端代理)']; const authTypes = ['无鉴权', 'API Key', 'Bearer Token', 'OAuth2', '自定义 Header']; const statusOptions = [ { id: 'pending', name: '待连接' }, { id: 'available', name: '可用' }, { id: 'failed', name: '连接失败' }, { id: 'disabled', name: '已禁用' }, ]; const createEmptyTool = () => ({ id: `mcp-tool-${Date.now()}-${Math.random().toString(16).slice(2)}`, name: '', desc: '', inputSchema: '', }); const createEmptyService = () => ({ id: '', name: '', desc: '', transport: 'Streamable HTTP', endpoint: '', namespace: '', authType: '无鉴权', credential: '', status: 'pending', errorMessage: '', tools: [createEmptyTool()], }); const modal = createModal({ title: 'MCP 服务配置', size: 'xl', confirmText: '保存挂载', cancelText: '取消', content: `
先创建 MCP 服务到服务库,再测试连接。连接可用后可挂载到当前智能体,模型运行时可调用该服务暴露的工具。
`, onConfirm: () => { if (draftForm) { Toast.warning('请先保存或取消正在编辑的 MCP 服务'); return false; } window.__agentMcpServiceLibrary = library.map(item => ({ ...item, tools: Array.isArray(item.tools) ? item.tools.map(tool => ({ ...tool })) : [], })); state.selectedMcpServiceIds = Array.from(selectedIds).filter(id => library.some(item => item.id === id)); Toast.success(`已保存 ${state.selectedMcpServiceIds.length} 个已挂载 MCP 服务`); rerender(); return true; }, }); const overlay = modal.overlay; const libraryPanel = overlay.querySelector('#mcpLibraryPanel'); const editorPanel = overlay.querySelector('#mcpEditorPanel'); function renderLibraryPanel() { libraryPanel.innerHTML = `

MCP 服务库

${library.length} 个
${library.length ? library.map(item => { const meta = getMcpStatusMeta(item); const tools = Array.isArray(item.tools) ? item.tools.filter(tool => tool.name) : []; return `
${escapeHtml(item.name)} ${escapeHtml(item.namespace || 'default')} ${escapeHtml(meta.label)}

${escapeHtml(item.desc)}

连接:${escapeHtml(item.transport || 'Streamable HTTP')} URL:${escapeHtml(item.endpoint || '-')} 鉴权:${escapeHtml(item.authType || '无鉴权')} 工具:${tools.length} 个
${item.status === 'failed' && item.errorMessage ? `
${escapeHtml(item.errorMessage)}
` : ''}
`; }).join('') : `
暂无可用 MCP 服务 点击创建服务,配置连接地址、鉴权方式和工具清单。
`}
`; } function renderEditorPanel() { if (!draftForm) { editorPanel.innerHTML = ''; return; } editorPanel.innerHTML = `

${draftForm.id ? '编辑 MCP 服务' : '创建 MCP 服务'}

工具清单

`; renderToolRows(); } function renderToolRows() { const list = overlay.querySelector('#mcpToolList'); if (!list || !draftForm) return; const tools = Array.isArray(draftForm.tools) ? draftForm.tools : []; list.innerHTML = tools.length ? tools.map((tool, index) => `
`).join('') : '
暂无工具,可先保存服务,后续由后端自动同步 MCP tools/list。
'; bindToolEvents(); renderAgentIcons(overlay); } function readDraftForm() { overlay.querySelectorAll('[data-mcp-field]').forEach(field => { draftForm[field.dataset.mcpField] = field.value.trim(); }); draftForm.tools = Array.from(overlay.querySelectorAll('[data-mcp-tool-row]')).map(row => ({ id: draftForm.tools?.[Number(row.dataset.mcpToolRow)]?.id || createEmptyTool().id, name: row.querySelector('[data-mcp-tool-field="name"]')?.value.trim() || '', desc: row.querySelector('[data-mcp-tool-field="desc"]')?.value.trim() || '', inputSchema: row.querySelector('[data-mcp-tool-field="inputSchema"]')?.value.trim() || '', })).filter(tool => tool.name || tool.desc || tool.inputSchema); } function saveDraftForm() { readDraftForm(); if (!draftForm.name || !draftForm.desc || !draftForm.endpoint) { Toast.warning('请填写服务名称、服务说明和 Server URL'); return; } const duplicate = library.some(item => item.id !== draftForm.id && item.name === draftForm.name); if (duplicate) { Toast.warning('MCP 服务名称已存在'); return; } if (!draftForm.id) { draftForm.id = `mcp-${Date.now()}`; library.push({ ...draftForm, tools: draftForm.tools.map(tool => ({ ...tool })) }); } else { const index = library.findIndex(item => item.id === draftForm.id); if (index >= 0) library[index] = { ...library[index], ...draftForm, tools: draftForm.tools.map(tool => ({ ...tool })) }; } if (draftForm.status === 'available') selectedIds.add(draftForm.id); else selectedIds.delete(draftForm.id); Toast.success(`MCP 服务「${draftForm.name}」已保存`); draftForm = null; renderAll(); } function testService(serviceId) { const target = library.find(item => item.id === serviceId); if (!target) return; target.status = 'validating'; target.errorMessage = '连接测试中'; renderAll(); window.setTimeout(() => { const service = library.find(item => item.id === serviceId); if (!service) return; const error = getMcpConnectionError(service); if (error) { service.status = 'failed'; service.errorMessage = error; selectedIds.delete(service.id); Toast.warning(`MCP 服务「${service.name}」连接失败:${error}`); } else { service.status = 'available'; service.errorMessage = ''; selectedIds.add(service.id); Toast.success(`MCP 服务「${service.name}」连接可用,已挂载到当前智能体`); } renderAll(); }, 450); } function bindToolEvents() { overlay.querySelectorAll('[data-delete-mcp-tool]').forEach(btn => { btn.addEventListener('click', () => { readDraftForm(); draftForm.tools.splice(Number(btn.dataset.deleteMcpTool), 1); renderToolRows(); }); }); } function bindMcpEvents() { overlay.querySelector('#createMcpBtn')?.addEventListener('click', () => { draftForm = createEmptyService(); renderAll(); }); overlay.querySelector('#createMcpEmptyBtn')?.addEventListener('click', () => { draftForm = createEmptyService(); renderAll(); }); overlay.querySelectorAll('.bailian-command-select input').forEach(input => { input.addEventListener('change', () => { const service = library.find(item => item.id === input.value); if (!service || service.status !== 'available') { input.checked = false; Toast.warning('请先测试连接通过后再挂载 MCP 服务'); return; } if (input.checked) selectedIds.add(input.value); else selectedIds.delete(input.value); renderAll(); }); }); overlay.querySelectorAll('.bailian-command-library-card').forEach(card => { card.addEventListener('click', event => { if (event.target.closest('button') || event.target.closest('label')) return; const input = card.querySelector('.bailian-command-select input'); if (!input || input.disabled) { Toast.warning('请先测试连接通过后再挂载 MCP 服务'); return; } if (selectedIds.has(input.value)) selectedIds.delete(input.value); else selectedIds.add(input.value); renderAll(); }); }); overlay.querySelectorAll('[data-test-mcp]').forEach(btn => { btn.addEventListener('click', () => testService(btn.dataset.testMcp)); }); overlay.querySelectorAll('[data-edit-mcp]').forEach(btn => { btn.addEventListener('click', () => { const target = library.find(item => item.id === btn.dataset.editMcp); if (!target) return; draftForm = { ...target, tools: Array.isArray(target.tools) && target.tools.length ? target.tools.map(tool => ({ ...tool })) : [createEmptyTool()], }; renderAll(); }); }); overlay.querySelectorAll('[data-delete-library-mcp]').forEach(btn => { btn.addEventListener('click', () => { const serviceId = btn.dataset.deleteLibraryMcp; const index = library.findIndex(item => item.id === serviceId); if (index < 0) return; const [deleted] = library.splice(index, 1); selectedIds.delete(serviceId); if (draftForm?.id === serviceId) draftForm = null; Toast.success(`MCP 服务「${deleted.name}」已删除`); renderAll(); }); }); overlay.querySelector('#cancelMcpEditBtn')?.addEventListener('click', () => { draftForm = null; renderAll(); }); overlay.querySelector('#cancelMcpEditBottomBtn')?.addEventListener('click', () => { draftForm = null; renderAll(); }); overlay.querySelector('#saveMcpEditBtn')?.addEventListener('click', saveDraftForm); overlay.querySelector('#addMcpToolBtn')?.addEventListener('click', () => { readDraftForm(); draftForm.tools = [...(draftForm.tools || []), createEmptyTool()]; renderToolRows(); }); } function renderAll() { renderLibraryPanel(); renderEditorPanel(); bindMcpEvents(); renderAgentIcons(overlay); } renderAll(); } function openRenameModal(state, rerender) { if (typeof createModal !== 'function') { const name = prompt('请输入应用名称', state.app.name); if (name && name.trim()) { state.app.name = name.trim(); rerender(); } return; } createModal({ title: '编辑应用名称', content: `
`, confirmText: '保存', cancelText: '取消', onConfirm: () => { const input = document.getElementById('renameAgentInput'); const nextName = input?.value.trim(); if (!nextName) { Toast.warning('请输入应用名称'); return false; } state.app.name = nextName; Toast.success('应用名称已更新'); rerender(); return true; }, }); setTimeout(() => document.getElementById('renameAgentInput')?.focus(), 50); } function getAgentStatus(status) { const map = { published: { label: '已发布', className: 'published' }, editing: { label: '编辑中', className: 'editing' }, draft: { label: '草稿', className: 'draft' }, }; return map[status] || map.draft; } function escapeHtml(value) { const div = document.createElement('div'); div.textContent = String(value ?? ''); return div.innerHTML; } function escapeAttr(value) { return escapeHtml(value).replace(/"/g, '"').replace(/'/g, '''); } function renderAgentIcons(root) { if (!root || !window.lucide?.createIcons) return; try { lucide.createIcons({ elements: [root] }); } catch (error) { console.warn('Agent config icon render failed:', error); } }