const axios = require('axios'); class ArkChatService { constructor() { this.baseUrl = 'https://ark.cn-beijing.volces.com/api/v3'; } _getAuth() { return process.env.VOLC_ARK_API_KEY || process.env.VOLC_ACCESS_KEY_ID; } isMockMode() { const ep = process.env.VOLC_ARK_ENDPOINT_ID; return !ep || ep === 'your_ark_endpoint_id'; } _isMockMode() { return this.isMockMode(); } /** * 获取方舟知识库配置(如果已配置) * @returns {object|null} 知识库 metadata 配置 */ _getKnowledgeBaseConfig(kbIdsOverride = null) { const kbIds = kbIdsOverride || process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS; if (!kbIds || kbIds === 'your_knowledge_base_dataset_id') return null; const datasetIds = kbIds.split(',').map(id => id.trim()).filter(Boolean); if (datasetIds.length === 0) return null; const topK = parseInt(process.env.VOLC_ARK_KNOWLEDGE_TOP_K) || 3; const threshold = parseFloat(process.env.VOLC_ARK_KNOWLEDGE_THRESHOLD) || 0.5; return { dataset_ids: datasetIds, top_k: topK, threshold: threshold, }; } async summarizeContextForHandoff(messages, maxRounds = 3) { const normalizedMessages = (Array.isArray(messages) ? messages : []) .filter((item) => item && (item.role === 'user' || item.role === 'assistant') && String(item.content || '').trim()); let startIndex = 0; let userRounds = 0; for (let index = normalizedMessages.length - 1; index >= 0; index -= 1) { if (normalizedMessages[index].role === 'user') { userRounds += 1; startIndex = index; if (userRounds >= Math.max(1, maxRounds)) { break; } } } const recentMessages = normalizedMessages.slice(startIndex); if (!recentMessages.length) { return ''; } const transcript = recentMessages .map((item, index) => `${index + 1}. ${item.role === 'user' ? '用户' : '助手'}:${String(item.content || '').trim()}`) .join('\n'); if (this._isMockMode()) { const lastUserMessage = [...recentMessages].reverse().find((item) => item.role === 'user'); return lastUserMessage ? `用户当前主要在追问:${lastUserMessage.content}` : ''; } const result = await this.chat([ { role: 'system', content: '你是对话交接摘要助手。请基于最近几轮对话生成一段简洁中文摘要,供另一个模型无缝接管会话。摘要必须同时包含:用户当前主要问题、已经确认的信息、仍待解决的问题。不要使用标题、项目符号或编号,不要虚构事实,控制在120字以内。', }, { role: 'user', content: `请总结以下最近${Math.ceil(recentMessages.length / 2)}轮对话:\n${transcript}`, }, ], []); return String(result.content || '').trim(); } /** * 非流式调用方舟 LLM */ async chat(messages, tools = [], options = {}) { if (this._isMockMode()) { console.warn('[ArkChat] EndPointId not configured, returning mock response'); return this._mockChat(messages); } const { useKnowledgeBase = false, knowledgeBaseIds = null } = options || {}; const body = { model: process.env.VOLC_ARK_ENDPOINT_ID, messages, stream: false, }; if (tools.length > 0) body.tools = tools; const kbConfig = useKnowledgeBase ? this._getKnowledgeBaseConfig(knowledgeBaseIds) : null; if (kbConfig) { body.metadata = { knowledge_base: kbConfig }; console.log('[ArkChat] Knowledge base enabled:', kbConfig.dataset_ids); } try { const response = await axios.post(`${this.baseUrl}/chat/completions`, body, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this._getAuth()}`, }, timeout: 30000, }); const choice = response.data.choices?.[0]; if (!choice) throw new Error('No response from Ark LLM'); const msg = choice.message; return { content: msg.content || '', toolCalls: msg.tool_calls || null, finishReason: choice.finish_reason, usage: response.data.usage, }; } catch (error) { if (error.response) { console.error('[ArkChat] API error:', error.response.status, error.response.data); } throw error; } } /** * SSE 流式调用方舟 LLM,通过回调逐块输出 * @param {Array} messages * @param {Array} tools * @param {function} onChunk - (text: string) => void * @param {function} onToolCall - (toolCalls: Array) => void * @param {function} onDone - (fullContent: string) => void */ async chatStream(messages, tools = [], { onChunk, onToolCall, onDone, useKnowledgeBase = false, knowledgeBaseIds = null } = {}) { if (this._isMockMode()) { return this._mockChatStream(messages, { onChunk, onDone }); } const body = { model: process.env.VOLC_ARK_ENDPOINT_ID, messages, stream: true, }; if (tools.length > 0) body.tools = tools; const kbConfig = useKnowledgeBase ? this._getKnowledgeBaseConfig(knowledgeBaseIds) : null; if (kbConfig) { body.metadata = { knowledge_base: kbConfig }; } const response = await axios.post(`${this.baseUrl}/chat/completions`, body, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this._getAuth()}`, }, timeout: 60000, responseType: 'stream', }); return new Promise((resolve, reject) => { let fullContent = ''; let toolCalls = []; let buffer = ''; response.data.on('data', (chunk) => { buffer += chunk.toString(); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || !trimmed.startsWith('data: ')) continue; const data = trimmed.slice(6); if (data === '[DONE]') continue; try { const parsed = JSON.parse(data); const delta = parsed.choices?.[0]?.delta; if (!delta) continue; if (delta.content) { fullContent += delta.content; onChunk?.(delta.content); } if (delta.tool_calls) { for (const tc of delta.tool_calls) { if (!toolCalls[tc.index]) { toolCalls[tc.index] = { id: tc.id, type: tc.type, function: { name: '', arguments: '' } }; } if (tc.function?.name) toolCalls[tc.index].function.name += tc.function.name; if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments; } } } catch (e) { // skip malformed lines } } }); response.data.on('end', () => { if (toolCalls.length > 0) { onToolCall?.(toolCalls); } onDone?.(fullContent); resolve({ content: fullContent, toolCalls: toolCalls.length > 0 ? toolCalls : null }); }); response.data.on('error', reject); }); } /** * 处理包含工具调用的完整对话循环(非流式) */ async chatWithTools(messages, tools, toolExecutor) { const result = await this.chat(messages, tools); if (!result.toolCalls || result.toolCalls.length === 0) { return result; } const updatedMessages = [ ...messages, { role: 'assistant', content: result.content, tool_calls: result.toolCalls }, ]; for (const tc of result.toolCalls) { const args = typeof tc.function.arguments === 'string' ? JSON.parse(tc.function.arguments) : tc.function.arguments; const toolResult = await toolExecutor.execute(tc.function.name, args, messages); updatedMessages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(toolResult), }); } const finalResult = await this.chat(updatedMessages, tools); return { ...finalResult, messages: updatedMessages, }; } /** * 流式版工具调用循环:先流式输出,如遇工具调用则执行后再流式输出最终结果 */ async chatStreamWithTools(messages, tools, toolExecutor, { onChunk, onToolCall, onDone }) { const result = await this.chatStream(messages, tools, { onChunk, onToolCall, onDone: () => {}, // don't fire onDone yet }); if (!result.toolCalls || result.toolCalls.length === 0) { onDone?.(result.content); return result; } // 通知前端正在调用工具 onToolCall?.(result.toolCalls); const updatedMessages = [ ...messages, { role: 'assistant', content: result.content, tool_calls: result.toolCalls }, ]; for (const tc of result.toolCalls) { const args = typeof tc.function.arguments === 'string' ? JSON.parse(tc.function.arguments) : tc.function.arguments; const toolResult = await toolExecutor.execute(tc.function.name, args, messages); updatedMessages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(toolResult), }); } // 工具执行完后,流式输出最终回答 const finalResult = await this.chatStream(updatedMessages, tools, { onChunk, onToolCall: null, onDone }); return { ...finalResult, messages: updatedMessages, }; } _mockChat(messages) { const lastMsg = messages[messages.length - 1]; const userText = lastMsg?.content || ''; console.log(`[ArkChat][MOCK] User: ${userText}`); return { content: this._getMockReply(userText), toolCalls: null, finishReason: 'stop', usage: { prompt_tokens: 0, completion_tokens: 0 }, }; } async _mockChatStream(messages, { onChunk, onDone }) { const lastMsg = messages[messages.length - 1]; const userText = lastMsg?.content || ''; console.log(`[ArkChat][MOCK-STREAM] User: ${userText}`); const reply = this._getMockReply(userText); // 模拟逐字输出 for (let i = 0; i < reply.length; i++) { onChunk?.(reply[i]); await new Promise((r) => setTimeout(r, 30)); } onDone?.(reply); return { content: reply, toolCalls: null }; } _getMockReply(userText) { if (userText.includes('天气')) { return '根据 query_weather 工具查询,北京今天晴,气温 22°C,湿度 45%,北风3级。适合外出活动!'; } if (userText.includes('订单')) { return '通过 query_order 工具查询,您的订单(ID: 12345)当前状态为:已发货,预计明天送达。快递单号:SF1234567890。'; } if (userText.includes('你好') || userText.includes('嗨') || userText.includes('hi')) { return '你好!我是小智,很高兴为你服务。有什么我可以帮你的吗?'; } if (userText.includes('知识') || userText.includes('退货') || userText.includes('政策')) { return '根据知识库查询,我们的退货政策如下:自签收之日起7天内可无理由退货,15天内可换货。请保持商品及包装完好。如需退货,请在"我的订单"中提交退货申请。'; } return `收到你的消息:"${userText}"。当前为模拟模式,配置方舟 LLM 凭证后将接入真实 AI 模型。你可以试试问我天气、订单、退货政策等问题。`; } } module.exports = new ArkChatService();