1822 lines
75 KiB
JavaScript
1822 lines
75 KiB
JavaScript
/**
|
||
* 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, '"').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);
|
||
}
|
||
}
|