Files
bigwo/test2/server/services/arkChatService.js

350 lines
11 KiB
JavaScript
Raw Permalink Normal View History

2026-03-12 12:47:56 +08:00
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() {
2026-03-12 12:47:56 +08:00
const ep = process.env.VOLC_ARK_ENDPOINT_ID;
return !ep || ep === 'your_ark_endpoint_id';
}
_isMockMode() {
return this.isMockMode();
}
2026-03-12 12:47:56 +08:00
/**
* 获取方舟知识库配置如果已配置
* @returns {object|null} 知识库 metadata 配置
*/
_getKnowledgeBaseConfig(kbIdsOverride = null) {
const kbIds = kbIdsOverride || process.env.VOLC_ARK_KNOWLEDGE_BASE_IDS;
2026-03-12 12:47:56 +08:00
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.4;
2026-03-12 12:47:56 +08:00
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();
}
2026-03-12 12:47:56 +08:00
/**
* 非流式调用方舟 LLM
*/
async chat(messages, tools = [], options = {}) {
2026-03-12 12:47:56 +08:00
if (this._isMockMode()) {
console.warn('[ArkChat] EndPointId not configured, returning mock response');
return this._mockChat(messages);
}
const { useKnowledgeBase = false, knowledgeBaseIds = null } = options || {};
2026-03-12 12:47:56 +08:00
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;
2026-03-12 12:47:56 +08:00
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 } = {}) {
2026-03-12 12:47:56 +08:00
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;
2026-03-12 12:47:56 +08:00
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();