const { v4: uuidv4 } = require('uuid'); class VoiceChatConfigBuilder { /** * 构建 StartVoiceChat 的完整配置(S2S 端到端语音大模型 + LLM 混合编排) * OutputMode=1: 混合模式,S2S 处理普通对话,LLM 处理工具调用 */ static build(options) { const { roomId, taskId, userId, botName = '小智', systemRole = '你是一个友善的智能助手。', speakingStyle = '请使用温和、清晰的口吻。', modelVersion = '1.2.1.0', speaker = 'zh_female_vv_jupiter_bigtts', tools = [], llmSystemPrompt = '', enableWebSearch = false, vadEndMs = 1200, chatHistory = [], } = options; const botUserId = `bot_${uuidv4().slice(0, 8)}`; const providerParams = { app: { appid: process.env.VOLC_S2S_APP_ID, token: process.env.VOLC_S2S_TOKEN, }, dialog: this._buildDialogConfig(modelVersion, botName, systemRole, speakingStyle, enableWebSearch, chatHistory), tts: { speaker: speaker, }, asr: { extra: { enable_custom_vad: true, end_smooth_window_ms: vadEndMs, }, }, }; // === 调试模式:纯 S2S(OutputMode=0),排除 LLM 干扰 === // ARK 端点已配置正确,启用混合编排模式 const DEBUG_PURE_S2S = false; const llmConfig = { Mode: 'ArkV3', EndPointId: process.env.VOLC_ARK_ENDPOINT_ID, MaxTokens: 1024, Temperature: 0.1, TopP: 0.3, SystemMessages: [llmSystemPrompt || this._buildDefaultLLMPrompt(tools)], HistoryLength: 10, ThinkingType: 'disabled', }; if (tools.length > 0) { llmConfig.Tools = tools; } // 混合模式:通过 UserPrompts 传入聊天历史作为上下文(官方推荐方式) if (chatHistory && chatHistory.length > 0 && !DEBUG_PURE_S2S) { const userPrompts = chatHistory.slice(-10).map(m => ({ Role: m.role === 'user' ? 'user' : 'assistant', Content: m.content, })); llmConfig.UserPrompts = userPrompts; console.log(`[VoiceChatConfig] Injected ${userPrompts.length} UserPrompts into LLMConfig`); } const config = { AppId: process.env.VOLC_RTC_APP_ID, RoomId: roomId, TaskId: taskId, AgentConfig: { TargetUserId: [userId], WelcomeMessage: `你好,我是${botName},有什么需要帮忙的吗?`, UserId: botUserId, EnableConversationStateCallback: true, }, Config: { S2SConfig: { Provider: 'volcano', OutputMode: DEBUG_PURE_S2S ? 0 : 1, ProviderParams: providerParams, }, // 注意:S2S 端到端模式下不需要独立 TTSConfig // ExternalTextToSpeech 在 S2S 模式下不产生音频,只用 Command:function SubtitleConfig: { SubtitleMode: 1, }, InterruptMode: 0, }, }; // 混合模式才需要 LLMConfig if (!DEBUG_PURE_S2S) { config.Config.LLMConfig = llmConfig; // Function Calling 回调配置:RTC 服务通过此 URL 发送 tool call 请求 if (tools.length > 0) { const serverUrl = process.env.FC_SERVER_URL || 'https://demo.tensorgrove.com.cn/api/voice/fc_callback'; config.Config.FunctionCallingConfig = { ServerMessageUrl: serverUrl, ServerMessageSignature: process.env.FC_SIGNATURE || 'default_signature', }; console.log(`[VoiceChatConfig] FunctionCallingConfig enabled, URL: ${serverUrl}`); } } console.log('[VoiceChatConfig] DEBUG_PURE_S2S:', DEBUG_PURE_S2S); console.log('[VoiceChatConfig] OutputMode:', config.Config.S2SConfig.OutputMode); console.log('[VoiceChatConfig] ProviderParams type:', typeof config.Config.S2SConfig.ProviderParams); console.log('[VoiceChatConfig] S2S AppId:', providerParams.app.appid); console.log('[VoiceChatConfig] S2S Token:', providerParams.app.token ? '***set***' : '***MISSING***'); return { config, botUserId }; } static _buildDialogConfig(modelVersion, botName, systemRole, speakingStyle, enableWebSearch, chatHistory = []) { const isOSeries = modelVersion === 'O' || modelVersion.startsWith('1.'); const dialog = { extra: { model: modelVersion }, }; // 如果有文字聊天历史,将其追加到 system_role 作为上下文 let fullSystemRole = systemRole; if (chatHistory && chatHistory.length > 0) { const historyText = chatHistory .slice(-10) .map(m => `${m.role === 'user' ? '用户' : '助手'}:${m.content}`) .join('\n'); fullSystemRole += `\n\n## 之前的对话记录(请延续此上下文)\n${historyText}`; console.log(`[VoiceChatConfig] Injected ${chatHistory.length} chat history messages into system_role`); } if (isOSeries) { dialog.bot_name = botName; dialog.system_role = fullSystemRole; dialog.speaking_style = speakingStyle; } else { dialog.character_manifest = `${fullSystemRole}\n你的名字是${botName}。${speakingStyle}`; } if (enableWebSearch && process.env.VOLC_WEBSEARCH_API_KEY) { dialog.extra.enable_volc_websearch = true; dialog.extra.volc_websearch_api_key = process.env.VOLC_WEBSEARCH_API_KEY; dialog.extra.volc_websearch_type = 'web_summary'; dialog.extra.volc_websearch_no_result_message = '抱歉,我没有查到相关信息。'; } return dialog; } static _buildDefaultLLMPrompt(tools) { const toolNames = tools.map((t) => t.function?.name).filter(Boolean); if (toolNames.length === 0) { return '你是一个智能助手。对于所有问题直接回答即可。'; } return `你是一个企业智能客服助手。你可以使用以下工具:${toolNames.join('、')}。 ## 最高优先级规则 1. 每次用户提问,你**必须**先调用 search_knowledge 工具查询知识库 2. 收到工具返回的知识库内容后,你**必须完整、详细地朗读**知识库返回的内容给用户 3. 不要省略、总结或缩写知识库的内容,要逐字朗读 4. 如果知识库没有相关内容,再用你自己的知识简洁回答 5. 如果知识库返回"未找到相关信息",直接告诉用户并提供建议`; } } module.exports = VoiceChatConfigBuilder;