/**
* 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 `
`;
}
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)}
${Array.from({ length: 28 }).map((_, index) => ``).join('')}
` : `
`}
${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 = `
`;
return `
`;
}
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 `
${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 `
`;
}
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 ? `
` : `
`}
已添加模型
${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 ? `
` : `
`}
已添加模型
${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 ? `
` : `
`}
已添加模型
${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 `
`;
}
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) => `
`).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 播报;单字符过滤在所有场景下都不会播报,请谨慎评估业务文本特点。
`,
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 = `
`;
}
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 = `
`;
}
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 = `
`;
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);
}
}