Files
TalkingQ_Dashboard/scripts/agent-dev.js
“yuanmengy” a225272d1e data&device ready
2026-06-05 17:11:56 +08:00

1822 lines
75 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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: '技能' },
{ id: 'advanced', index: 4, 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_SCENE_TEMPLATES = [
{ id: 'ai-audio-glasses', name: 'AI音频眼镜', icon: 'headphones', desc: '随身携带的语音助手' },
{ id: 'ar-glasses', name: 'AR眼镜', icon: 'glasses', desc: '具备屏幕显示能力' },
{ id: 'desktop-robot', name: '桌面机器人', icon: 'bot', desc: '实时语音对话,日常情感陪伴' },
{ id: 'smart-home', name: '智能家居', icon: 'home', desc: '语音控制家中设备' },
{ id: 'toy', name: '儿童玩具', icon: 'star', desc: '陪伴孩子聊天、放音乐、讲故事' },
{ id: 'story-player', name: '儿童故事机', icon: 'book-open', desc: '给孩子讲故事、放儿歌' },
{ id: 'smart-speaker', name: '智能音箱', icon: 'volume-2', desc: '语音控制播放音乐' },
{ id: 'mobile-app', name: '手机应用', icon: 'smartphone', desc: '实时语音互动应用' },
];
const AGENT_SCENE_VERSIONS = [
{
id: 'full',
name: '全能版',
desc: '支持意图识别、工具调用、联网搜索、多场景 Agent适合完整智能体体验。',
},
{
id: 'lite',
name: '轻量版',
desc: '更快速、更低价的语音闲聊,不启用复杂工具调用和 Agent 编排。',
},
];
const AGENT_SCENE_LANGUAGES = [
{ id: 'zh', name: '中文', desc: '适合中文普通话和中文儿童内容场景' },
{ id: 'en', name: '英文', desc: '适合英语对话、英语学习和双语启蒙场景' },
];
const AGENT_CONFIG_MOCK = {
voice: {
languageOptions: [
{ id: 'zh', label: '中文' },
{ id: 'en', label: '英文' },
],
asrModels: [
{
id: 'fun-asr',
icon: 'audio-lines',
label: 'Fun-ASR实时语音识别',
desc: '中英文自由切换,多地区方言覆盖,适应多样复杂环境。',
},
{
id: 'qwen3-asr',
icon: 'audio-lines',
label: '通义千问3-ASR-Flash-Realtime',
desc: '高精度语音识别,中英日韩德法俄意西葡阿和多种方言。',
},
{
id: 'paraformer',
icon: 'audio-lines',
label: 'Paraformer实时语音识别',
desc: '新一代非自回归端到端模型,支持中英日韩德法俄等语言。',
},
{
id: 'multimodal-lite-asr',
icon: 'audio-lines',
label: '多模态交互轻量版语音识别',
desc: '超低价格,预算友好,支持中英等语言。',
},
],
ttsGroups: [
{
id: 'cosyvoice',
title: 'CosyVoice-实时语音合成',
models: [
{ id: 'cosyvoice-v3-flash', label: 'CosyVoice-v3-Flash大模型', desc: '独家音色,性价比高,支持情感控制。' },
{ id: 'cosyvoice-v3-plus', label: 'CosyVoice-v3-Plus大模型', desc: '情感表达最佳,支持声音复刻。' },
{ id: 'cosyvoice-v35-plus', label: 'CosyVoice-v3.5-Plus大模型', desc: '声音设计/复刻最佳,多种方言和外语。' },
{ id: 'cosyvoice-v35-flash', label: 'CosyVoice-v3.5-Flash大模型', desc: '声音设计/复刻性价比高,多种方言和外语。' },
{ id: 'cosyvoice-v2', label: 'CosyVoice-v2大模型', desc: '音色丰富70+音色可选,一句话声音复刻。' },
],
},
{
id: 'qwen-tts',
title: '通义千问3-实时语音合成',
models: [
{ id: 'qwen3-tts-flash', label: '通义千问3-TTS-Flash-Realtime', desc: '多语言拥有17种高表现力的拟人音色。' },
{ id: 'qwen3-tts-instruct', label: '通义千问3-TTS-Instruct-Flash-Realtime', desc: '支持指令控制,调整音调、语速、情感、特点。' },
{ id: 'qwen3-tts-design', label: '通义千问3-TTS-声音设计', desc: '通过文本描述生成定制化音色,支持多维度定义。' },
{ id: 'qwen3-tts-clone', label: '通义千问3-TTS-声音复刻', desc: '10-20秒音频即可生成效果最佳的定制音色。' },
],
},
{
id: 'others',
title: '其他语音合成模型',
models: [
{ id: 'sambert-tts', label: 'Sambert-实时语音合成', desc: '音色选择多,多语言,合成速度快。' },
{ id: 'lite-tts', label: '多模态交互轻量版语音合成', desc: '预算友好,支持多语种和方言。' },
],
},
],
defaultHotwords: ['TalkingQ', '小Q', '白雪公主', '睡前故事', '恐龙百科'],
correctionHint: '通过接口上传词表,对语音识别结果进行实时干预。',
},
understand: {
intentModel: {
icon: 'route',
label: '意图识别',
desc: '关闭将不再触发调用工具/Agent 等基于语义理解的交互功能。',
},
textModels: [
{ id: 'recommended-fast', group: '推荐模型', label: '多模态交互专有模型-高速版', desc: '对话速度更快,适合通用场景、闲聊陪伴场景。' },
{ id: 'recommended-tool', group: '推荐模型', label: '多模态交互专有模型', desc: '指令遵循能力更强,适合工作学习场景。' },
{ id: 'recommended-style', group: '推荐模型', label: '多模态交互专有模型-风格化版', desc: '风格化能力更强,适合角色扮演等场景。' },
{ id: 'qwen', group: '更多模型', label: '多千问', desc: '通用能力均衡,适合大多数任务。' },
{ id: 'deepseek', group: '更多模型', label: 'DeepSeek', desc: '推理和代码表现更强。' },
{ id: 'chatgpt', group: '更多模型', label: 'ChatGPT', desc: '多场景通用对话能力。' },
{ id: 'doubao', group: '更多模型', label: '豆包', desc: '适合中文对话和内容生成。' },
],
prompt: `# 角色
你是小Q一个专为 3-10 岁儿童设计的 AI 玩具智能体。
## 沟通风格
- 语言简单、生动、温暖。
- 回复通常控制在 150 字以内。
- 优先使用比喻、拟声词和故事化表达。
- 不能输出任何不适合儿童的危险内容。
## 行为准则
1. 当孩子要求讲故事时,先确认主题,再生成短故事。
2. 当孩子提问百科知识时,先给出结论,再给出一个生活例子。
3. 当识别到情绪陪伴需求时,优先安抚,再引导表达。
\`\`\`policy
安全等级:儿童模式
联网搜索:必要时开启
知识库优先级:高
\`\`\``,
variables: [
{ name: 'child_age', desc: '儿童年龄' },
{ name: 'device_mode', desc: '设备当前模式' },
{ name: 'ambient_noise', desc: '环境噪音等级' },
{ name: 'weather', desc: '当前位置天气' },
],
knowledgeBases: [
{ name: '儿童科学百科', desc: '动物、太空、自然科学问答素材', weight: '0.72' },
{ name: '故事素材库 V2', desc: '睡前故事、角色设定、儿童互动剧情', weight: '0.68' },
],
},
skills: {
commands: [
{ icon: 'phone-call', name: '打电话', count: 6, desc: '拨打联系人电话、语音拨号等电话相关控制' },
{ icon: 'alarm-clock', name: '定闹钟', count: 1, desc: '创建、关闭、修改闹钟' },
{ icon: 'volume-2', name: '音量设置', count: 7, desc: '调节媒体音量、铃声音量、静音等' },
{ icon: 'bluetooth', name: '蓝牙', count: 4, desc: '打开/关闭蓝牙、连接蓝牙设备' },
{ icon: 'toggle-right', name: '应用开关', count: 10, desc: '打开/关闭指定 App 或系统功能' },
{ icon: 'cpu', name: '设备控制', count: 5, desc: '系统级设备控制能力(当前已选中)' },
{ icon: 'music', name: '音乐播放', count: 5, desc: '播放音乐、暂停、下一首等' },
{ icon: 'circle-play', name: '多媒体控制', count: 6, desc: '控制媒体播放状态与音频输出' },
{ icon: 'camera', name: '拍照录像', count: 6, desc: '打开相机、拍照、录像' },
{ icon: 'mic', name: '录音', count: 2, desc: '开始/停止录音' },
{ icon: 'book-open', name: '朗读文字', count: 1, desc: '语音朗读输入文本' },
{ icon: 'message-circle', name: '发短信', count: 3, desc: '发送短信消息' },
{ icon: 'sun', name: '亮度设置', count: 5, desc: '调节屏幕亮度' },
{ icon: 'sun-medium', name: '色温设置', count: 3, desc: '调节屏幕色温/护眼模式' },
{ icon: 'video', name: '视频播放', count: 1, desc: '打开并播放视频' },
{ icon: 'map-pinned', name: '打开地图', count: 1, desc: '打开地图导航类应用' },
{ icon: 'monitor', name: '屏幕控制', count: 4, desc: '锁屏、亮屏、熄屏、旋转等屏幕操作' },
],
plugins: [
{ icon: 'cloud-sun', name: '天气查询', desc: '查询实时天气、空气质量和未来预报', status: '系统执行' },
{ icon: 'calendar', name: '万年历', desc: '查询日期、农历、节假日和宜忌信息', status: '系统执行' },
{ icon: 'newspaper', name: '新闻资讯', desc: '获取最新新闻资讯和摘要播报', status: '系统执行' },
{ icon: 'navigation', name: '路线规划', desc: '规划出行路线、距离和预计耗时', status: '系统执行' },
{ icon: 'map', name: '本地生活', desc: '查询周边商户、生活服务和地点信息', status: '系统执行' },
],
bailianApps: [
{ name: '故事生成器', desc: '基于主题生成短故事' },
{ name: '儿童内容安全', desc: '对回答内容进行安全审查' },
],
thirdPartyApps: [
{ name: '音频内容库', desc: '接入第三方儿歌和故事资源' },
],
},
};
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 state = {
app: selectedApp,
activeStep: 'voice',
preview: 'empty',
selectedSceneId: window.__selectedSceneTemplateId || 'toy',
selectedSceneVersion: window.__selectedSceneVersion || 'full',
selectedSceneLanguage: window.__selectedSceneLanguage || 'zh',
voiceEnabled: true,
asrEnabled: true,
ttsEnabled: true,
selectedAsrModelId: AGENT_CONFIG_MOCK.voice.asrModels[0]?.id || '',
selectedTtsGroupId: AGENT_CONFIG_MOCK.voice.ttsGroups[0]?.id || '',
selectedTtsModelId: AGENT_CONFIG_MOCK.voice.ttsGroups[0]?.models[0]?.id || '',
selectedHotwordGroupId: firstHotwordGroup,
realtimeCorrectionEnabled: true,
ttsFilterEnabled: false,
ttsFilterPairs: [
{ enabled: true, left: '/*', right: '*/' },
{ enabled: true, left: '[', right: ']' },
{ enabled: false, left: '{', right: '}' },
],
ttsFilterSingles: [],
interruptPrimaryEnabled: true,
interruptSecondaryEnabled: true,
interruptMode: 'full',
semanticStopEnabled: true,
edgeAlgorithmExpanded: true,
selectedTextModelMode: 'recommended',
selectedTextModelId: AGENT_CONFIG_MOCK.understand.textModels[0]?.id || '',
intentEnabled: true,
dialogueContinuationEnabled: true,
knowledgeSearchEnabled: true,
searchMode: 'fast',
longTermMemoryEnabled: false,
knowledgeBases: AGENT_CONFIG_MOCK.understand.knowledgeBases.map(item => ({ ...item })),
customCommands: [],
customPlugins: [],
};
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, render);
bindPromptCounter(container);
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 `
<div class="bailian-agent-config">
${renderAgentHeader(state.app)}
<div class="bailian-agent-body">
<aside class="bailian-config-pane">
${renderStepTabs(state.activeStep)}
<div class="bailian-config-scroll">
${renderAllStepSections(state)}
</div>
</aside>
${renderPreviewPane(state.preview)}
</div>
</div>
`;
}
function renderAgentHeader(app) {
const status = getAgentStatus(app.status);
return `
<header class="bailian-agent-header">
<div class="bailian-header-left">
<button class="bailian-icon-btn" data-action="back" aria-label="返回应用列表">
<i data-lucide="arrow-left"></i>
</button>
<div class="bailian-agent-title-wrap">
<div class="bailian-agent-title-row">
<h1 class="bailian-agent-title">${escapeHtml(app.name)}</h1>
<button class="bailian-plain-icon bailian-title-edit-btn" data-action="rename" aria-label="编辑名称">
<i data-lucide="pencil"></i>
</button>
<span class="bailian-status-pill ${status.className}">${status.label}</span>
</div>
<div class="bailian-agent-subtitle">ID: ${escapeHtml(app.id)} · AI 智能体配置平台</div>
</div>
</div>
<div class="bailian-header-actions">
<button class="bailian-action-btn bailian-action-icon" data-action="history" aria-label="历史记录">
<i data-lucide="clock-3"></i>
</button>
<button class="bailian-action-btn" data-action="api">API 接入</button>
<button class="bailian-action-btn" data-action="license">购买 License</button>
<button class="bailian-action-btn bailian-primary-btn" data-action="publish">发布</button>
</div>
</header>
`;
}
function renderStepTabs(activeStep) {
return `
<nav class="bailian-step-tabs" aria-label="配置步骤">
${AGENT_CONFIG_STEPS.map(step => `
<button class="bailian-step-tab ${activeStep === step.id ? 'active' : ''}" data-step="${step.id}" aria-label="${escapeAttr(step.title)}">
<span class="bailian-step-number">${step.index}</span>
<span class="bailian-step-title">${escapeHtml(step.title)}</span>
</button>
`).join('')}
</nav>
`;
}
function renderAllStepSections(state) {
return AGENT_CONFIG_STEPS.map(step => `
<section class="bailian-step-section" id="agent-step-${step.id}" data-step-section="${step.id}">
${renderStepContent(step.id, state)}
</section>
`).join('');
}
function renderStepContent(stepId, state) {
const renderers = {
voice: renderVoiceStep,
understand: renderUnderstandStep,
skills: renderSkillsStep,
advanced: renderAdvancedStep,
};
return (renderers[stepId] || renderVoiceStep)(state);
}
function renderVoiceStep(state) {
const data = AGENT_CONFIG_MOCK.voice;
const scene = getSelectedScene(state);
const version = getSelectedSceneVersion(state);
const language = getSelectedSceneLanguage(state);
const hotwordGroups = window.__agentHotwordGroups || [];
return `
<div class="bailian-step-content">
<section class="bailian-card-section bailian-voice-panel">
<h3>语音交互</h3>
${renderSceneCard(scene, version, language)}
<div class="bailian-voice-section">
<label class="bailian-field">
<span class="bailian-field-label">交互语言</span>
<select class="bailian-input" data-action="scene-language">
${data.languageOptions.map(lang => `
<option value="${escapeAttr(lang.id)}" ${lang.id === state.selectedSceneLanguage ? 'selected' : ''}>${escapeHtml(lang.label)}</option>
`).join('')}
</select>
</label>
<div class="bailian-switch-row compact">
<div>
<div class="bailian-switch-title">开启语音交互</div>
<div class="bailian-switch-desc">打开后运行将使用语音进行交互,关闭后仅保留文字交互</div>
</div>
<label class="agent-switch">
<input type="checkbox" data-toggle="voice-enabled" ${state.voiceEnabled ? 'checked' : ''}>
<span></span>
</label>
</div>
</div>
${state.voiceEnabled ? `
<div class="bailian-voice-section">
<div class="bailian-voice-subtitle">语音 AI</div>
<div class="bailian-split-grid">
${renderFeatureSelect({
title: '语音识别',
toggleKey: 'asr-enabled',
checked: state.asrEnabled,
activeId: state.selectedAsrModelId,
options: data.asrModels,
hint: '前面的开关决定是否启用 ASR后面的下拉用于切换识别模型',
})}
${renderFeatureSelect({
title: '语音合成',
toggleKey: 'tts-enabled',
checked: state.ttsEnabled,
activeId: state.selectedTtsModelId,
options: data.ttsGroups.flatMap(group => group.models.map(model => ({ ...model, groupTitle: group.title }))),
groupedOptions: data.ttsGroups,
hint: '前面的开关决定是否启用 TTS后面的下拉用于切换播报模型',
})}
</div>
</div>
<div class="bailian-voice-section">
<div class="bailian-voice-subtitle">热词</div>
<div class="bailian-section-inline-action">
<div>
<div class="bailian-hotword-title">热词组绑定</div>
<div class="bailian-hotword-desc">热词组来自热词库管理,新增后会自动出现在这里</div>
</div>
<button class="bailian-link-btn" data-action="hotwords">管理热词库</button>
</div>
<label class="bailian-field" style="margin-top:12px">
<span class="bailian-field-label">选择热词组</span>
<select class="bailian-input" data-action="hotword-group">
${hotwordGroups.length ? hotwordGroups.map(group => `
<option value="${escapeAttr(group.id)}" ${group.id === state.selectedHotwordGroupId ? 'selected' : ''}>${escapeHtml(group.name)}</option>
`).join('') : '<option value="">暂无热词组</option>'}
</select>
</label>
</div>
<div class="bailian-voice-section">
<div class="bailian-voice-subtitle">即时纠错</div>
<div class="bailian-switch-row compact">
<div>
<div class="bailian-switch-title">开启即时纠错</div>
<div class="bailian-switch-desc">${escapeHtml(data.correctionHint)}</div>
</div>
<label class="agent-switch">
<input type="checkbox" data-toggle="realtime-correction" ${state.realtimeCorrectionEnabled ? 'checked' : ''}>
<span></span>
</label>
</div>
<div class="bailian-section-inline-action" style="margin-top:12px">
<span>可上传词表对识别结果进行实时干预</span>
<button class="bailian-link-btn" data-action="correction-upload">上传词表</button>
</div>
</div>
<div class="bailian-voice-section">
<div class="bailian-voice-subtitle">语音合成播报过滤</div>
<div class="bailian-switch-row compact">
<div>
<div class="bailian-switch-title">开启播报过滤</div>
<div class="bailian-switch-desc">特定符号对中的内容将不会进行 TTS 播报</div>
</div>
<label class="agent-switch">
<input type="checkbox" data-toggle="tts-filter" ${state.ttsFilterEnabled ? 'checked' : ''}>
<span></span>
</label>
</div>
${state.ttsFilterEnabled ? `
<div class="bailian-filter-preview">
<div class="bailian-filter-note">选中符号对内的文字将不会通过 TTS 播报,如 [动作/表情编码]、[设备参数] 及 $$公式$$ 等。</div>
<button class="bailian-action-btn small" data-action="tts-filter-config">语音合成播报过滤设置</button>
</div>
` : ''}
</div>
${state.ttsFilterEnabled ? renderTtsFilterSummary(state) : ''}
<div class="bailian-voice-section">
<div class="bailian-voice-subtitle">对话打断</div>
<div class="bailian-section-tip">此配置仅用于效果体验</div>
<div class="bailian-stack">
<label class="bailian-check-card">
<input type="checkbox" data-toggle="interrupt-primary" ${state.interruptPrimaryEnabled ? 'checked' : ''}>
<span>
<strong>任意语音打断(全双工)</strong>
<small>运行后语音测试会出现同等效果</small>
</span>
</label>
<label class="bailian-check-card">
<input type="checkbox" data-toggle="interrupt-secondary" ${state.interruptSecondaryEnabled ? 'checked' : ''}>
<span>
<strong>点击打断(半双工)</strong>
<small>运行后可点击触发中断</small>
</span>
</label>
<div class="bailian-check-card readonly">
<span class="bailian-radio-dot active"></span>
<span>
<strong>长按对话(半双工,仅移动端支持)</strong>
<small>运行后可在移动端长按触发</small>
</span>
</div>
</div>
</div>
<div class="bailian-voice-section">
<div class="bailian-voice-subtitle">语义判停与拒识</div>
<div class="bailian-switch-row compact">
<div>
<div class="bailian-switch-title">开启语义判停与拒识</div>
<div class="bailian-switch-desc">开启后可能导致对话延迟增加</div>
</div>
<label class="agent-switch">
<input type="checkbox" data-toggle="semantic-stop" ${state.semanticStopEnabled ? 'checked' : ''}>
<span></span>
</label>
</div>
<div class="bailian-section-note">基于用户语义,判断是否结束语音输入,以及是否需要模型回复。全双工对话场景下,本功能效果更显著。</div>
</div>
<div class="bailian-voice-section">
<div class="bailian-voice-subtitle">端侧算法</div>
<button class="bailian-collapse-head" data-action="edge-collapse">
<span>展开/收起端侧算法</span>
<i data-lucide="${state.edgeAlgorithmExpanded ? 'chevron-up' : 'chevron-down'}"></i>
</button>
${state.edgeAlgorithmExpanded ? `
<div class="bailian-edge-grid">
${[
['语音唤醒', '默认唤醒词“小云小云”'],
['端侧 VAD', '语音活性检测,判断是否存在语音信号'],
['AEC', '回声消除,减少自身播报的干扰'],
['定向拾音', '增强特定方向拾音,抑制其他方向'],
].map(([title, desc]) => `
<div class="bailian-edge-card">
<strong>${escapeHtml(title)}</strong>
<span>${escapeHtml(desc)}</span>
</div>
`).join('')}
</div>
` : ''}
</div>
<button class="bailian-run-btn" data-action="run">立即运行</button>
` : `
<div class="bailian-voice-disabled">
<strong>当前为文字交互模式</strong>
<span>关闭语音交互后,不展示语音 AI、热词、纠错、播报过滤、打断和端侧算法配置。</span>
</div>
`}
</section>
</div>
`;
}
function renderUnderstandStep(state) {
const data = AGENT_CONFIG_MOCK.understand;
const selectedMode = state.selectedTextModelMode || 'recommended';
const selectedModelId = state.selectedTextModelId || data.textModels[0]?.id || '';
const selectedModel = data.textModels.find(item => item.id === selectedModelId)
|| data.textModels.find(item => item.group === '推荐模型')
|| data.textModels[0];
return `
<div class="bailian-step-content">
${renderSection('意图识别', `
<div class="bailian-switch-row compact">
<div>
<div class="bailian-switch-title">开启意图识别</div>
<div class="bailian-switch-desc">${escapeHtml(data.intentModel.desc)}</div>
</div>
<label class="agent-switch">
<input type="checkbox" data-toggle="intent-enabled" ${state.intentEnabled !== false ? 'checked' : ''}>
<span></span>
</label>
</div>
<div class="bailian-section-note">关闭将不再触发调用工具/Agent 等基于语义理解的交互功能,适用于低成本、轻量级语音交互场景。</div>
`)}
${renderSection('文本模型', `
<div class="bailian-section-inline-action">
<div>
<div class="bailian-hotword-title">模型选择</div>
<div class="bailian-hotword-desc">建议使用推荐模型,为用户提供流畅的对话体验</div>
</div>
<span class="bailian-small-pill">推荐 / 更多</span>
</div>
<div class="bailian-model-mode-grid">
<label class="bailian-mode-card ${selectedMode === 'recommended' ? 'active' : ''}">
<input type="radio" name="textModelMode" value="recommended" ${selectedMode === 'recommended' ? 'checked' : ''}>
<strong>推荐模型</strong>
<span>适合通用对话、工作学习和角色扮演场景。</span>
</label>
<label class="bailian-mode-card ${selectedMode === 'more' ? 'active' : ''}">
<input type="radio" name="textModelMode" value="more" ${selectedMode === 'more' ? 'checked' : ''}>
<strong>更多模型</strong>
<span>适合需要切换不同模型供应商的场景。</span>
</label>
</div>
<select class="bailian-input" data-action="text-model">
${data.textModels.filter(item => item.group === (selectedMode === 'recommended' ? '推荐模型' : '更多模型')).map(item => `
<option value="${escapeAttr(item.id)}" ${item.id === selectedModel.id ? 'selected' : ''}>${escapeHtml(item.label)}</option>
`).join('')}
</select>
<div class="bailian-model-desc">${escapeHtml(selectedModel.desc)}</div>
`)}
${renderPromptEditor(data.prompt)}
${renderSection('对话变量', `
<div class="bailian-section-inline-action">
<span>支持在对话输入中添加变量,为模型提供用户语音之外的更多信息,如天气、周围环境等。</span>
<button class="bailian-link-btn" data-action="variable">添加变量</button>
</div>
<div class="bailian-variable-grid">
${data.variables.map(item => `
<div class="bailian-variable-card">
<div class="bailian-variable-name">${escapeHtml(item.name)}</div>
<div class="bailian-variable-desc">${escapeHtml(item.desc)}</div>
</div>
`).join('')}
<button class="bailian-add-card" data-action="variable">
<i data-lucide="plus"></i>
<span>自定义变量</span>
</button>
</div>
`)}
${renderSection('上下文轮数', `
<div class="bailian-inline-field">
<span>携带最近</span>
<input class="bailian-number-input" type="number" min="1" max="50" value="10">
<span>轮对话作为上下文</span>
</div>
`)}
${renderKnowledgeBaseSection(state.knowledgeBases || data.knowledgeBases)}
${renderSection('对话承接语', `
<div class="bailian-switch-row compact">
<div>
<div class="bailian-switch-title">开启对话承接语</div>
<div class="bailian-switch-desc">大模型基于用户输入自动生成承接语,减少回复等待时间</div>
</div>
<label class="agent-switch">
<input type="checkbox" data-toggle="continuation" ${state.dialogueContinuationEnabled !== false ? 'checked' : ''}>
<span></span>
</label>
</div>
`)}
${renderSection('互联网搜索', `
<div class="bailian-switch-row compact">
<div>
<div class="bailian-switch-title">开启互联网搜索</div>
<div class="bailian-switch-desc">关闭后不展示搜索模式,仅保留本地知识和模型能力</div>
</div>
<label class="agent-switch">
<input type="checkbox" data-toggle="knowledge-search" ${state.knowledgeSearchEnabled !== false ? 'checked' : ''}>
<span></span>
</label>
</div>
${state.knowledgeSearchEnabled !== false ? `
<div class="bailian-mode-grid">
<label class="bailian-mode-card ${state.searchMode !== 'quality' ? 'active' : ''}">
<input type="radio" name="webSearchMode" value="fast" ${state.searchMode !== 'quality' ? 'checked' : ''}>
<strong>效率模式</strong>
<span>搜索速度更快,对话延迟低。</span>
</label>
<label class="bailian-mode-card ${state.searchMode === 'quality' ? 'active' : ''}">
<input type="radio" name="webSearchMode" value="quality" ${state.searchMode === 'quality' ? 'checked' : ''}>
<strong>效果模式</strong>
<span>搜索质量更高,对话延迟可能高于效率模式。</span>
</label>
</div>
` : ''}
`)}
${renderSection('长期记忆', `
<div class="bailian-switch-row compact">
<div>
<div class="bailian-switch-title">开启长期记忆</div>
<div class="bailian-switch-desc">基于对话历史形成专属记忆库,并在后续对话中体现</div>
</div>
<label class="agent-switch">
<input type="checkbox" data-toggle="long-term-memory" ${state.longTermMemoryEnabled ? 'checked' : ''}>
<span></span>
</label>
</div>
<div class="bailian-section-note">开启长期记忆后,结合记忆内容进行回复的对话会增加额外耗时。</div>
`)}
</div>
`;
}
function renderSkillsStep(state) {
const data = AGENT_CONFIG_MOCK.skills;
const commands = [...data.commands, ...(state.customCommands || [])];
const plugins = [...data.plugins, ...(state.customPlugins || [])];
return `
<div class="bailian-step-content">
${renderSection('指令', `
<div class="bailian-section-inline-action">
<span>当前已选择的指令可以编辑,右侧加号可添加新的指令类型</span>
<button class="bailian-link-btn" data-action="command-config">添加/管理指令</button>
</div>
<div class="bailian-list">
${commands.map(item => `
<div class="bailian-list-card">
<div class="bailian-list-icon"><i data-lucide="${item.icon}"></i></div>
<div class="bailian-list-main">
<div class="bailian-list-title">${escapeHtml(item.name)} <span>${item.count || 1} 指令</span></div>
<div class="bailian-list-desc">${escapeHtml(item.desc)}</div>
</div>
<button class="bailian-plain-icon" data-action="command-config"><i data-lucide="settings"></i></button>
<button class="bailian-plain-icon danger" data-action="remove"><i data-lucide="trash-2"></i></button>
</div>
`).join('')}
</div>
`)}
${renderSection('插件', `
<div class="bailian-section-inline-action">
<span>插件能力默认由系统执行,也可以独立配置启用状态</span>
<button class="bailian-link-btn" data-action="plugin-config">配置插件</button>
</div>
<div class="bailian-list">
${plugins.map(item => `
<div class="bailian-list-card">
<div class="bailian-list-icon"><i data-lucide="${item.icon}"></i></div>
<div class="bailian-list-main">
<div class="bailian-list-title">${escapeHtml(item.name)}</div>
<div class="bailian-list-desc">${escapeHtml(item.desc)}</div>
</div>
<span class="bailian-small-pill">${escapeHtml(item.status || '系统执行')}</span>
<button class="bailian-plain-icon" data-action="plugin-config"><i data-lucide="chevron-down"></i></button>
<button class="bailian-plain-icon danger" data-action="remove"><i data-lucide="trash-2"></i></button>
</div>
`).join('')}
</div>
`)}
${renderMiniAppSection('百炼应用', data.bailianApps, 'boxes')}
${renderMiniAppSection('三方应用', data.thirdPartyApps, 'plug')}
${renderMcpSection()}
${renderSection('Planning / Function Calling', `
${renderSwitchRow('开启 Planning', '让智能体先规划步骤,再调用函数或插件完成复杂任务', true)}
${renderSwitchRow('开启 Function Calling', '允许模型按照结构化参数调用已授权函数', true)}
`)}
</div>
`;
}
function renderAdvancedStep() {
return `
<div class="bailian-step-content">
${renderSection('高级设置', `
${renderSwitchRow('儿童安全模式', '强制启用儿童内容安全策略、危险行为拒答和敏感信息保护', true)}
${renderSwitchRow('链路追踪', '记录 ASR、LLM、RAG、TTS 的耗时和调用链路', true)}
${renderSwitchRow('自动降级', '主模型异常时自动切换到备用模型,保证设备可用性', true)}
`)}
${renderSection('调用预算', `
<div class="bailian-budget-grid">
<label class="bailian-field">
<span class="bailian-field-label">单次对话上限</span>
<input class="bailian-input" value="0.12 元">
</label>
<label class="bailian-field">
<span class="bailian-field-label">每日调用上限</span>
<input class="bailian-input" value="3000 次">
</label>
</div>
`)}
${renderSection('发布策略', `
<div class="bailian-radio-list compact">
${renderRadio('release', 'gray', '灰度发布 10%', true)}
${renderRadio('release', 'full', '全量发布')}
${renderRadio('release', 'schedule', '定时发布')}
</div>
`)}
</div>
`;
}
function renderPreviewPane(previewState) {
const title = previewState === 'running' ? '预览环境已准备好' : '请填好配置后点击运行';
const desc = previewState === 'running'
? '当前智能体已加载最新 mock 配置,可继续调整参数后再次运行。'
: '当前应用测试会产生实际调用,按对话次数计费';
return `
<section class="bailian-preview-pane">
<div class="bailian-preview-empty">
<div class="bailian-cube" aria-hidden="true">
<span></span><span></span><span></span>
</div>
<h2>${title}</h2>
<p>${desc}</p>
<div class="bailian-preview-tip">
<i data-lucide="info"></i>
<span>建议佩戴耳机,并在安静环境下测试,对话体验更佳。</span>
</div>
</div>
</section>
`;
}
function renderSceneCard(scene, version, language) {
return `
<button class="bailian-scene-card" data-action="scene">
<span class="bailian-scene-icon"><i data-lucide="${scene.icon}"></i></span>
<span class="bailian-scene-main">
<span class="bailian-scene-title-row">
<strong>${escapeHtml(scene.name)}</strong>
<em>${escapeHtml(version.name)}</em>
<em>${escapeHtml(language.name)}</em>
</span>
<span>${escapeHtml(scene.desc)}</span>
</span>
<i data-lucide="refresh-cw"></i>
</button>
`;
}
function renderSection(title, content) {
return `
<section class="bailian-card-section">
<h3>${escapeHtml(title)}</h3>
${content}
</section>
`;
}
function renderModelSelect(model) {
return `
<button class="bailian-model-select" type="button" data-action="model">
<span class="bailian-model-icon"><i data-lucide="${model.icon}"></i></span>
<span class="bailian-model-main">
<strong>${escapeHtml(model.label)}</strong>
<small>${escapeHtml(model.desc)}</small>
</span>
<i data-lucide="chevron-down"></i>
</button>
`;
}
function renderFeatureSelect(config) {
const {
title,
toggleKey,
checked,
activeId,
options = [],
groupedOptions = null,
hint = '',
} = 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 `
<div class="bailian-feature-card">
<div class="bailian-feature-head">
<div>
<div class="bailian-feature-title">${escapeHtml(title)}</div>
<div class="bailian-feature-desc">${escapeHtml(hint)}</div>
</div>
<label class="agent-switch">
<input type="checkbox" data-toggle="${escapeAttr(toggleKey)}" ${checked ? 'checked' : ''}>
<span></span>
</label>
</div>
<select class="bailian-input" data-action="${fieldAction}" ${checked ? '' : 'disabled'}>
${groupedOptions ? groupedOptions.map(group => `
<optgroup label="${escapeAttr(group.title)}">
${group.models.map(model => `
<option value="${escapeAttr(model.id)}" ${model.id === activeId ? 'selected' : ''}>${escapeHtml(model.label)}</option>
`).join('')}
</optgroup>
`).join('') : options.map(item => `
<option value="${escapeAttr(item.id)}" ${item.id === activeId ? 'selected' : ''}>${escapeHtml(item.label)}</option>
`).join('')}
</select>
${selectedOption ? `<div class="bailian-model-desc">${escapeHtml(selectedOption.desc)}</div>` : ''}
</div>
`;
}
function renderSwitchRow(label, desc, checked) {
return `
<div class="bailian-switch-row">
<div>
<div class="bailian-switch-title">${escapeHtml(label)}</div>
<div class="bailian-switch-desc">${escapeHtml(desc)}</div>
</div>
<label class="agent-switch">
<input type="checkbox" ${checked ? 'checked' : ''}>
<span></span>
</label>
</div>
`;
}
function renderRadio(name, value, label, checked = false) {
return `
<label class="bailian-radio-card">
<input type="radio" name="${escapeAttr(name)}" value="${escapeAttr(value)}" ${checked ? 'checked' : ''}>
<span class="bailian-radio-dot"></span>
<span>${escapeHtml(label)}</span>
</label>
`;
}
function renderTtsFilterSummary(state) {
if (!state.ttsFilterEnabled) return '';
const commonPairs = (state.ttsFilterPairs || [])
.filter(item => item.enabled !== false)
.map(item => `${item.left} ${item.right}`.trim());
const singles = state.ttsFilterSingles || [];
return `
<div class="bailian-filter-summary">
<div class="bailian-filter-group">
<div class="bailian-filter-group-title">常用过滤符号对</div>
<div class="bailian-token-row">
${commonPairs.length ? commonPairs.map(item => `<span class="bailian-token">${escapeHtml(item)}</span>`).join('') : '<span class="bailian-token">暂无</span>'}
</div>
</div>
<div class="bailian-filter-group">
<div class="bailian-filter-group-title">自定义过滤符号对与单字符</div>
<div class="bailian-token-row">
${singles.length ? singles.map(item => `<span class="bailian-token">${escapeHtml(item)}</span>`).join('') : '<span class="bailian-token">暂无</span>'}
</div>
<button class="bailian-action-btn small" data-action="tts-filter-config">打开配置弹窗</button>
</div>
</div>
`;
}
function renderPromptEditor(prompt) {
return `
<section class="bailian-card-section prompt-section">
<div class="bailian-section-head">
<h3>Prompt 提示词</h3>
<button class="bailian-action-btn small" data-action="variable">
<i data-lucide="braces"></i>
自定义变量
</button>
</div>
<textarea class="bailian-prompt-editor" id="agentPromptEditor" maxlength="8000">${escapeHtml(prompt)}</textarea>
<div class="bailian-prompt-footer">
<span>支持 Markdown、标题、列表和代码风格文本</span>
<strong id="promptCounter">1524/8000</strong>
</div>
</section>
`;
}
function renderKnowledgeBaseSection(items) {
return renderSection(`知识库 ${items.length}/10`, `
<div class="bailian-section-inline-action">
<span>已挂载知识库会参与 RAG 检索和答案补充</span>
<div style="display:flex;gap:10px">
<button class="bailian-link-btn" data-action="knowledge-weight">知识库权重配置</button>
<button class="bailian-link-btn" data-action="add-kb">添加知识库</button>
</div>
</div>
<div class="bailian-list">
${items.map(item => `
<div class="bailian-kb-card">
<div class="bailian-list-icon"><i data-lucide="file-text"></i></div>
<div class="bailian-list-main">
<div class="bailian-list-title">${escapeHtml(item.name)}</div>
<div class="bailian-list-desc">${escapeHtml(item.desc)}</div>
<div class="bailian-kb-weight">权重 ${escapeHtml(item.weight)}</div>
</div>
<button class="bailian-plain-icon danger" data-action="remove"><i data-lucide="trash-2"></i></button>
</div>
`).join('')}
</div>
`);
}
function renderWebSearchSection(modes) {
return renderSection('联网搜索', `
<div class="bailian-mode-grid">
${modes.map(mode => `
<label class="bailian-mode-card ${mode.active ? 'active' : ''}">
<input type="radio" name="webSearchMode" ${mode.active ? 'checked' : ''}>
<strong>${escapeHtml(mode.title)}</strong>
<span>${escapeHtml(mode.desc)}</span>
</label>
`).join('')}
</div>
`);
}
function renderMiniAppSection(title, items, iconName) {
return renderSection(title, `
<div class="bailian-mini-grid">
${items.map(item => `
<div class="bailian-mini-card">
<span><i data-lucide="${iconName}"></i></span>
<strong>${escapeHtml(item.name)}</strong>
<small>${escapeHtml(item.desc)}</small>
</div>
`).join('')}
<button class="bailian-add-card compact" data-action="add-app">
<i data-lucide="plus"></i>
<span>添加</span>
</button>
</div>
`);
}
function renderMcpSection() {
return renderSection('MCP服务 0', `
<div class="bailian-mcp-empty">
<div>
<strong>MCP服务 0</strong>
<p>智能体可以通过标准化协议MCP连接企业内部服务API并发起调用</p>
</div>
<button class="bailian-add-round" data-action="mcp"><i data-lucide="plus"></i></button>
</div>
`);
}
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',
'realtime-correction': 'realtimeCorrectionEnabled',
'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('input[name="textModelMode"]').forEach(radio => {
radio.addEventListener('change', () => {
if (!radio.checked) return;
state.selectedTextModelMode = radio.value;
const group = radio.value === 'recommended' ? '推荐模型' : '更多模型';
const candidates = AGENT_CONFIG_MOCK.understand.textModels.filter(item => item.group === group);
state.selectedTextModelId = candidates[0]?.id || state.selectedTextModelId;
rerender();
});
});
container.querySelectorAll('input[name="webSearchMode"]').forEach(radio => {
radio.addEventListener('change', () => {
if (!radio.checked) return;
state.searchMode = radio.value;
rerender();
});
});
container.querySelectorAll('[data-action="text-model"]').forEach(select => {
select.addEventListener('change', () => {
state.selectedTextModelId = select.value;
const current = AGENT_CONFIG_MOCK.understand.textModels.find(item => item.id === select.value);
if (current) state.selectedTextModelMode = current.group === '更多模型' ? 'more' : 'recommended';
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;
const matchedGroup = AGENT_CONFIG_MOCK.voice.ttsGroups.find(group => group.models.some(model => model.id === select.value));
state.selectedTtsGroupId = matchedGroup?.id || state.selectedTtsGroupId;
rerender();
});
});
container.querySelectorAll('[data-action="hotword-group"]').forEach(select => {
select.addEventListener('change', () => {
state.selectedHotwordGroupId = select.value;
rerender();
});
});
container.querySelectorAll('[data-action="scene-language"]').forEach(select => {
select.addEventListener('change', () => {
state.selectedSceneLanguage = select.value;
window.__selectedSceneLanguage = select.value;
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';
Toast.success('应用已提交发布,预计 2-5 分钟生效');
rerender();
return;
}
if (action === 'run') {
state.preview = 'running';
updatePreviewOnly(container, state.preview);
Toast.success('运行预览已启动');
return;
}
if (action === 'add-kb') {
navigateTo('knowledge-base');
return;
}
if (action === 'knowledge-weight') {
openKnowledgeWeightModal(state, rerender);
return;
}
if (action === 'scene') {
openSceneTemplateModal(state, 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 === 'edge-collapse') {
state.edgeAlgorithmExpanded = !state.edgeAlgorithmExpanded;
rerender();
return;
}
const actionText = {
history: '打开历史记录',
api: '打开 API 接入文档',
license: '打开 License 购买页',
model: '切换模型',
variable: '打开变量配置',
setting: '打开设置',
remove: '已移除当前项',
mcp: '添加 MCP 服务',
'add-app': '添加应用',
'correction-upload': '上传词表',
};
Toast.info(actionText[action] || '操作已触发');
event.stopPropagation();
});
});
}
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, previewState) {
const preview = container.querySelector('.bailian-preview-pane');
if (!preview) return;
const temp = document.createElement('div');
temp.innerHTML = renderPreviewPane(previewState);
preview.replaceWith(temp.firstElementChild);
renderAgentIcons(container.querySelector('.bailian-preview-pane'));
}
function bindPromptCounter(container) {
const editor = container.querySelector('#agentPromptEditor');
const counter = container.querySelector('#promptCounter');
if (!editor || !counter) return;
const update = () => {
counter.textContent = `${editor.value.length}/8000`;
};
editor.addEventListener('input', update);
update();
}
function openSceneTemplateModal(state, rerender) {
let pendingSceneId = state.selectedSceneId;
let pendingVersionId = state.selectedSceneVersion;
let pendingLanguageId = state.selectedSceneLanguage;
if (typeof createModal !== 'function') {
Toast.info('场景模板选择器暂不可用');
return;
}
const modal = createModal({
title: '切换场景模板',
size: 'xl',
confirmText: '应用模板',
cancelText: '取消',
content: `
<div class="bailian-scene-modal">
<div class="bailian-scene-modal-section">
<div class="bailian-scene-modal-title">选择场景</div>
<div class="bailian-scene-option-grid">
${AGENT_SCENE_TEMPLATES.map(scene => `
<label class="bailian-scene-option ${scene.id === pendingSceneId ? 'selected' : ''}" data-scene-id="${scene.id}">
<input type="radio" name="sceneTemplate" value="${scene.id}" ${scene.id === pendingSceneId ? 'checked' : ''}>
<span class="bailian-scene-option-icon"><i data-lucide="${scene.icon}"></i></span>
<span class="bailian-scene-option-main">
<strong>${escapeHtml(scene.name)}</strong>
<small>${escapeHtml(scene.desc)}</small>
</span>
</label>
`).join('')}
</div>
</div>
<div class="bailian-scene-modal-section">
<div class="bailian-scene-modal-title">选择版本</div>
<div class="bailian-scene-version-grid">
${AGENT_SCENE_VERSIONS.map(version => `
<label class="bailian-scene-version-option ${version.id === pendingVersionId ? 'selected' : ''}" data-version-id="${version.id}">
<input type="radio" name="sceneVersion" value="${version.id}" ${version.id === pendingVersionId ? 'checked' : ''}>
<strong>${escapeHtml(version.name)}</strong>
<span>${escapeHtml(version.desc)}</span>
</label>
`).join('')}
</div>
</div>
<div class="bailian-scene-modal-section">
<div class="bailian-scene-modal-title">对话语言 <span class="bailian-required">*</span></div>
<div class="bailian-scene-language-grid">
${AGENT_SCENE_LANGUAGES.map(language => `
<label class="bailian-scene-language-option ${language.id === pendingLanguageId ? 'selected' : ''}" data-language-id="${language.id}">
<input type="radio" name="sceneLanguage" value="${language.id}" ${language.id === pendingLanguageId ? 'checked' : ''} required>
<strong>${escapeHtml(language.name)}</strong>
<span>${escapeHtml(language.desc)}</span>
</label>
`).join('')}
</div>
</div>
</div>
`,
onConfirm: () => {
if (!pendingLanguageId || !AGENT_SCENE_LANGUAGES.some(language => language.id === pendingLanguageId)) {
Toast.warning('请选择对话语言');
return false;
}
state.selectedSceneId = pendingSceneId;
state.selectedSceneVersion = pendingVersionId;
state.selectedSceneLanguage = pendingLanguageId;
window.__selectedSceneTemplateId = pendingSceneId;
window.__selectedSceneVersion = pendingVersionId;
window.__selectedSceneLanguage = pendingLanguageId;
const scene = getSelectedScene(state);
const version = getSelectedSceneVersion(state);
const language = getSelectedSceneLanguage(state);
Toast.success(`已切换至「${scene.name} · ${version.name} · ${language.name}`);
rerender();
return true;
},
});
const overlay = modal.overlay;
const syncSceneOptions = () => {
overlay.querySelectorAll('.bailian-scene-option').forEach(option => {
option.classList.toggle('selected', option.dataset.sceneId === pendingSceneId);
option.querySelector('input').checked = option.dataset.sceneId === pendingSceneId;
});
};
const syncVersionOptions = () => {
overlay.querySelectorAll('.bailian-scene-version-option').forEach(option => {
option.classList.toggle('selected', option.dataset.versionId === pendingVersionId);
option.querySelector('input').checked = option.dataset.versionId === pendingVersionId;
});
};
const syncLanguageOptions = () => {
overlay.querySelectorAll('.bailian-scene-language-option').forEach(option => {
option.classList.toggle('selected', option.dataset.languageId === pendingLanguageId);
option.querySelector('input').checked = option.dataset.languageId === pendingLanguageId;
});
};
overlay.querySelectorAll('.bailian-scene-option').forEach(option => {
option.addEventListener('click', () => {
pendingSceneId = option.dataset.sceneId;
syncSceneOptions();
});
});
overlay.querySelectorAll('.bailian-scene-version-option').forEach(option => {
option.addEventListener('click', () => {
pendingVersionId = option.dataset.versionId;
syncVersionOptions();
});
});
overlay.querySelectorAll('.bailian-scene-language-option').forEach(option => {
option.addEventListener('click', () => {
pendingLanguageId = option.dataset.languageId;
syncLanguageOptions();
});
});
renderAgentIcons(overlay);
}
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: `
<div style="display:flex;flex-direction:column;gap:14px">
<div style="font-size:13px;color:var(--text-muted)">调整知识库权重可人工干预召回片段排序。知识库召回片段得分 = 知识库权重 × 切片相似度。</div>
${draft.map((item, index) => `
<div class="bailian-weight-row">
<div>
<div class="bailian-weight-name">${escapeHtml(item.name)}</div>
<div class="bailian-weight-desc">${escapeHtml(item.desc)}</div>
</div>
<input class="bailian-input bailian-weight-input" type="number" min="0.5" max="2" step="0.1" data-index="${index}" value="${escapeAttr(item.weight)}">
</div>
`).join('')}
</div>
`,
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 draftPairs = (state.ttsFilterPairs || []).map(item => ({ ...item }));
const draftSingles = [...(state.ttsFilterSingles || [])];
const modal = createModal({
title: '语音合成播报过滤设置',
size: 'xl',
confirmText: '确定',
cancelText: '取消',
content: `
<div class="bailian-filter-modal">
<div class="bailian-filter-modal-desc">选中符号对内的文字将不会通过 TTS 播报;单字符过滤在所有场景下都不会播报,请谨慎评估业务文本特点。</div>
<div class="bailian-filter-modal-section">
<div class="bailian-filter-group-title">常用过滤符号对</div>
<div class="bailian-check-grid">
${commonPairs.map(item => `
<label class="bailian-check-card">
<input type="checkbox" data-common-pair="${escapeAttr(item.id)}" ${draftPairs.some(pair => pair.left === item.left && pair.right === item.right && pair.enabled !== false) ? 'checked' : ''}>
<span>
<strong>${escapeHtml(item.label)}</strong>
<small>将过滤 ${escapeHtml(item.left)} ... ${escapeHtml(item.right)} 内容</small>
</span>
</label>
`).join('')}
</div>
</div>
<div class="bailian-filter-modal-section">
<div class="bailian-filter-head">
<div class="bailian-filter-group-title">自定义过滤符号对</div>
<button class="bailian-link-btn" type="button" id="addFilterPairBtn">+ 添加</button>
</div>
<div id="filterPairList"></div>
</div>
<div class="bailian-filter-modal-section">
<div class="bailian-filter-head">
<div class="bailian-filter-group-title">自定义过滤单字符</div>
<button class="bailian-link-btn" type="button" id="addFilterSingleBtn">+ 添加</button>
</div>
<div id="filterSingleList"></div>
</div>
</div>
`,
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 = [];
commonPairs.forEach(item => {
const enabled = modal.overlay.querySelector(`[data-common-pair="${item.id}"]`)?.checked;
if (enabled) nextPairs.push({ enabled: true, left: item.left, right: item.right });
});
pairRows.forEach(row => {
const left = row.querySelector('[data-left]')?.value.trim() || '';
const right = row.querySelector('[data-right]')?.value.trim() || '';
if (left || right) nextPairs.push({ enabled: true, left, right });
});
const nextSingles = 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) => `
<div class="bailian-filter-row" data-filter-pair-row>
<input class="bailian-input" data-left value="${escapeAttr(pair.left || '')}" placeholder="请输入左界定符">
<input class="bailian-input" data-right value="${escapeAttr(pair.right || '')}" placeholder="请输入右界定符">
<button class="bailian-plain-icon danger" type="button" data-delete-pair="${index}"><i data-lucide="trash-2"></i></button>
</div>
`).join('')
: '<div class="bailian-empty-inline">暂无自定义符号对</div>';
}
function renderSingleRows() {
singleList.innerHTML = draftSingles.length
? draftSingles.map((char, index) => `
<div class="bailian-filter-row single" data-filter-single-row>
<input class="bailian-input" data-single value="${escapeAttr(char)}" maxlength="1" placeholder="请输入单字符">
<button class="bailian-plain-icon danger" type="button" data-delete-single="${index}"><i data-lucide="trash-2"></i></button>
</div>
`).join('')
: '<div class="bailian-empty-inline">暂无自定义单字符</div>';
}
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 baseCommands = AGENT_CONFIG_MOCK.skills.commands;
const customCommands = state.customCommands || [];
const modal = createModal({
title: '指令配置',
size: 'xl',
confirmText: '确定',
cancelText: '取消',
content: `
<div class="bailian-command-modal">
<div class="bailian-filter-modal-desc">下发设备控制指令由端侧执行,可在这里查看系统指令、编辑已选指令并添加自定义指令类型。</div>
<div class="bailian-command-table-wrap">
<table class="bailian-command-table">
<thead>
<tr>
<th>启用</th>
<th>指令名称</th>
<th>指令说明</th>
<th>指令示例</th>
<th>参数名称</th>
<th>回复模式</th>
</tr>
</thead>
<tbody>
${[...baseCommands, ...customCommands].map(item => `
<tr>
<td><input type="checkbox" checked></td>
<td>${escapeHtml(item.name)}</td>
<td>${escapeHtml(item.desc)}</td>
<td>${escapeHtml(item.example || `${item.name} 示例`)}</td>
<td>${escapeHtml(item.params || 'amount')}</td>
<td>
<select class="form-control form-control-sm">
<option selected>根据执行情况回复</option>
<option>执行前回复</option>
<option>执行后回复</option>
</select>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div class="bailian-command-form">
<div class="bailian-section-head">
<h3>设置自定义指令</h3>
<button class="bailian-link-btn" type="button" id="addCustomCommandBtn">+ 添加指令</button>
</div>
<div id="customCommandList"></div>
</div>
</div>
`,
onConfirm: () => {
const rows = Array.from(modal.overlay.querySelectorAll('[data-custom-command-row]'));
const next = rows.map(row => ({
icon: 'settings-2',
name: row.querySelector('[data-name]')?.value.trim() || '',
desc: row.querySelector('[data-desc]')?.value.trim() || '',
example: row.querySelector('[data-example]')?.value.trim() || '',
params: row.querySelector('[data-params]')?.value.trim() || '',
count: 1,
})).filter(item => item.name);
state.customCommands = next;
Toast.success('自定义指令已保存');
rerender();
return true;
},
});
const overlay = modal.overlay;
const list = overlay.querySelector('#customCommandList');
const draft = (state.customCommands || []).map(item => ({ ...item }));
function renderRows() {
list.innerHTML = draft.length ? draft.map((item, index) => `
<div class="bailian-command-row" data-custom-command-row>
<input class="bailian-input" data-name value="${escapeAttr(item.name || '')}" placeholder="请输入指令名称">
<textarea class="bailian-input" data-desc placeholder="请输入指令说明">${escapeHtml(item.desc || '')}</textarea>
<input class="bailian-input" data-example value="${escapeAttr(item.example || '')}" placeholder="请输入指令示例">
<input class="bailian-input" data-params value="${escapeAttr(item.params || '')}" placeholder="请输入参数名称">
<button class="bailian-plain-icon danger" type="button" data-delete-command="${index}"><i data-lucide="trash-2"></i></button>
</div>
`).join('') : '<div class="bailian-empty-inline">暂无自定义指令</div>';
}
function bindCommandEvents() {
overlay.querySelectorAll('[data-delete-command]').forEach(btn => {
btn.addEventListener('click', () => {
draft.splice(Number(btn.dataset.deleteCommand), 1);
renderRows();
bindCommandEvents();
renderAgentIcons(overlay);
});
});
}
overlay.querySelector('#addCustomCommandBtn')?.addEventListener('click', () => {
draft.push({ name: '', desc: '', example: '', params: '' });
renderRows();
bindCommandEvents();
renderAgentIcons(overlay);
});
renderRows();
bindCommandEvents();
renderAgentIcons(overlay);
}
function openPluginConfigModal(state, rerender) {
const basePlugins = AGENT_CONFIG_MOCK.skills.plugins;
const customPlugins = (state.customPlugins || []).map(item => ({ ...item }));
const modal = createModal({
title: '插件配置',
size: 'xl',
confirmText: '确定',
cancelText: '取消',
content: `
<div class="bailian-command-modal">
<div class="bailian-filter-modal-desc">插件默认由系统执行,也可以在这里单独增删自定义插件并控制是否启用。</div>
<div class="bailian-section-head">
<h3>系统插件</h3>
<span class="bailian-small-pill">${basePlugins.length} 项</span>
</div>
<div style="display:flex;flex-direction:column;gap:10px">
${basePlugins.map(item => `
<label class="bailian-check-card">
<input type="checkbox" checked disabled>
<span>
<strong>${escapeHtml(item.name)}</strong>
<small>${escapeHtml(item.desc)}</small>
</span>
<span class="bailian-small-pill">${escapeHtml(item.status || '系统执行')}</span>
</label>
`).join('')}
</div>
<div class="bailian-section-head" style="margin-top:6px">
<h3>自定义插件</h3>
<button class="bailian-link-btn" type="button" id="addCustomPluginBtn">+ 添加</button>
</div>
<div id="customPluginList"></div>
</div>
`,
onConfirm: () => {
const rows = Array.from(modal.overlay.querySelectorAll('[data-custom-plugin-row]'));
state.customPlugins = rows.map(row => ({
icon: row.querySelector('[data-icon]')?.value.trim() || 'plug',
name: row.querySelector('[data-name]')?.value.trim() || '',
desc: row.querySelector('[data-desc]')?.value.trim() || '',
status: row.querySelector('[data-status]')?.value.trim() || '系统执行',
})).filter(item => item.name);
Toast.success('插件配置已保存');
rerender();
return true;
},
});
renderAgentIcons(modal.overlay);
const overlay = modal.overlay;
const list = overlay.querySelector('#customPluginList');
const draft = customPlugins;
function renderRows() {
list.innerHTML = draft.length ? draft.map((item, index) => `
<div class="bailian-command-row" data-custom-plugin-row>
<input class="bailian-input" data-icon value="${escapeAttr(item.icon || 'plug')}" placeholder="图标名称">
<input class="bailian-input" data-name value="${escapeAttr(item.name || '')}" placeholder="插件名称">
<input class="bailian-input" data-desc value="${escapeAttr(item.desc || '')}" placeholder="插件描述">
<input class="bailian-input" data-status value="${escapeAttr(item.status || '系统执行')}" placeholder="状态">
<div></div>
<button class="bailian-plain-icon danger" type="button" data-delete-plugin="${index}"><i data-lucide="trash-2"></i></button>
</div>
`).join('') : '<div class="bailian-empty-inline">暂无自定义插件</div>';
}
function bindRows() {
overlay.querySelectorAll('[data-delete-plugin]').forEach(btn => {
btn.addEventListener('click', () => {
draft.splice(Number(btn.dataset.deletePlugin), 1);
renderRows();
bindRows();
renderAgentIcons(overlay);
});
});
}
overlay.querySelector('#addCustomPluginBtn')?.addEventListener('click', () => {
draft.push({ icon: 'plug', name: '', desc: '', status: '系统执行' });
renderRows();
bindRows();
renderAgentIcons(overlay);
});
renderRows();
bindRows();
renderAgentIcons(overlay);
}
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: `
<div class="form-group">
<label class="form-label">应用名称</label>
<input class="form-control" id="renameAgentInput" value="${escapeAttr(state.app.name)}" maxlength="50">
</div>
`,
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 getSelectedScene(state) {
return AGENT_SCENE_TEMPLATES.find(scene => scene.id === state.selectedSceneId)
|| AGENT_SCENE_TEMPLATES.find(scene => scene.id === 'toy')
|| AGENT_SCENE_TEMPLATES[0];
}
function getSelectedSceneVersion(state) {
return AGENT_SCENE_VERSIONS.find(version => version.id === state.selectedSceneVersion)
|| AGENT_SCENE_VERSIONS[0];
}
function getSelectedSceneLanguage(state) {
return AGENT_SCENE_LANGUAGES.find(language => language.id === state.selectedSceneLanguage)
|| AGENT_SCENE_LANGUAGES[0];
}
function escapeHtml(value) {
const div = document.createElement('div');
div.textContent = String(value ?? '');
return div.innerHTML;
}
function escapeAttr(value) {
return escapeHtml(value).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function renderAgentIcons(root) {
if (!root || !window.lucide?.createIcons) return;
try {
lucide.createIcons({ elements: [root] });
} catch (error) {
console.warn('Agent config icon render failed:', error);
}
}