import { debuglog } from 'node:util' const debug = debuglog('@cloudbase/aiagent-framework:tools') type FetchOptions = { method: string headers: { 'Content-Type': string Accept: string Authorization: string } body?: string // body 是可选的 } export interface toolsCommonResp { code?: string message?: string } export interface ChatDBSearchResult { relateTables: unknown[] answerPrompt: string } export interface ChatDBDataEvent extends toolsCommonResp { type: 'db' created: number role: 'assistant' content: '' finishReason: string | 'continue' | 'stop' searchResult: ChatDBSearchResult } export interface SearchDBResult extends toolsCommonResp { searchResult: ChatDBSearchResult } export async function searchDB( baseURL: string, token: string, botId: string, msg: string, databaseModel: string[] ): Promise { const url = `${baseURL}/v1/aibot/tool/chat-db` let searchResult!: ChatDBSearchResult const callToolsResult = await callStreamTool( url, token, { botId, msg, databaseModel }, (result: ChatDBDataEvent) => { searchResult = result?.searchResult // 解析 JSON 数据 } ) if (callToolsResult?.code && callToolsResult?.code?.length !== 0) { return { ...callToolsResult, searchResult: searchResult } } return { searchResult: searchResult } } export interface KnowledgeBaseDocument { score: number data: { text: string startPos: number endPos: number pre: unknown[] next: unknown[] paragraphTitle: string allParentParagraphTitles: string[] } documentSet: { documentSetId: string documentSetName: string author: string fileTitle: string fileMetaData: string fileId: string } } export interface ChatKnowledgeDataEvent extends toolsCommonResp { type: 'knowledge' created: number role: 'assistant' content: string finishReason: string | 'continue' | 'stop' documents: KnowledgeBaseDocument[] knowledgeBase: string[] knowledgeMeta: string[] } export interface SearchKnowledgeResult extends toolsCommonResp { documents: KnowledgeBaseDocument[] knowledgeBase?: string[] knowledgeMeta?: string[] } export async function searchKnowledgeBase( baseURL: string, token: string, botId: string, msg: string, knowledgeBase: string[] ): Promise { const url = `${baseURL}/v1/aibot/tool/chat-knowledge` const documents: KnowledgeBaseDocument[] = [] const knowledgeBaseFile: string[] = [] const knowledgeMeta: string[] = [] const callToolsResult = await callStreamTool( url, token, { botId, msg, knowledgeBase }, (result: ChatKnowledgeDataEvent) => { documents.push(...result.documents) knowledgeBaseFile.push(...result.knowledgeBase) knowledgeMeta.push(...result.knowledgeMeta) } ) if (callToolsResult?.code && callToolsResult?.code?.length !== 0) { return { ...callToolsResult, documents, knowledgeBase: knowledgeBaseFile, knowledgeMeta } } return { documents, knowledgeBase: knowledgeBaseFile, knowledgeMeta } } export interface SearchResult { index: number title: string url: string publisher: string abstract: string publishTime: number source: 'thirdparty' | 'knowledgebase' extra?: string // 可选字段,可能包含插件消息和排名信息 } export interface SearchInfo { searchResults: SearchResult[] } export interface SearchNetworkDataEvent extends toolsCommonResp { type: 'search' created: number model: 'hunyuan' version: string role: '' content: string finishReason: string | 'continue' | 'stop' searchInfo: SearchInfo } export interface SearchNetworkResult extends toolsCommonResp { content: string searchInfo: SearchInfo } export async function searchNetwork(baseURL: string, token: string, botId: string, msg: string): Promise { const url = `${baseURL}/v1/aibot/tool/search-network` let fullContent = '' let searchInfo!: SearchInfo const callToolsResult = await callStreamTool( url, token, { botId, msg }, (result: SearchNetworkDataEvent) => { fullContent = fullContent + (result?.content ?? '') searchInfo = result?.searchInfo } ) if (callToolsResult?.code && callToolsResult?.code?.length !== 0) { return { ...callToolsResult, content: fullContent, searchInfo } } return { content: fullContent, searchInfo } } export interface ChatFileDataEvent extends toolsCommonResp { type: 'search_file' created: number model: 'hunyuan' version: string role: 'assistant' content: string finishReason: string | 'continue' | 'stop' } export interface SearchFileResult extends toolsCommonResp { content: string } /** * callChatFile 识别图片内容,返回图片内容的文字描述 */ export async function searchFile(baseURL: string, token: string, botId: string, msg: string, files: string[]): Promise { const url = `${baseURL}/v1/aibot/tool/chat-file` let fullContent = '' const callToolsResult = await callStreamTool( url, token, { botId, msg, fileList: files }, (result: ChatFileDataEvent) => { fullContent = fullContent + (result?.content ?? '') } ) if (callToolsResult?.code && callToolsResult?.code?.length !== 0) { return { ...callToolsResult, content: fullContent } } return { content: fullContent } } /** * callChatFile 识别图片内容,返回图片内容的文字描述 */ async function callStreamTool(url: string, token: string, body: object, onData: (data: T) => void): Promise { try { const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', Authorization: `Bearer ${token}` }, body: JSON.stringify(body) }) if (!resp.ok) { throw new Error(`HTTP error! status: ${resp.status}, statusText: ${resp.statusText}`) } if (!resp.body) { throw new Error('Response body is null or undefined') } const contentType = resp.headers.get('content-type') // 判断 Content-Type 是否为 JSON, 只有报错的时候 才会返回JSON if (contentType && contentType.includes('application/json')) { const result: toolsCommonResp = await resp.json() return { code: result.code, message: result.message } } const reader = resp.body.getReader() const decoder = new TextDecoder('utf-8') let loopDone: boolean let txt = '' do { const { done, value } = await reader.read() loopDone = done if (loopDone) { break } txt += decoder.decode(value, { stream: true }) debug('callStreamTool txt:', txt) // 处理接收到的完整事件,SSE 事件以双换行分隔 const events = txt.split('\n\n') for (let i = 0; i < events.length - 1; i++) { const event = events[i].trim() if (event === 'data: [DONE]' || event === 'data:[DONE]') { continue } if (event.startsWith('data:')) { const data = event.substring(5).trim() // 获取 data: 后面的数据 const result: T = safeJsonParse(data) // 解析 JSON 数据 // TODO: 考虑处理异常 onData(result) } } txt = events[events.length - 1] } while (!loopDone) } catch (error) { throw new Error(`callStreamTool: ${error}`) } return { code: '', message: '' } } function safeJsonParse(jsonString: string, defaultValue = null) { try { return JSON.parse(jsonString) /* eslint-disable-next-line */ } catch (e) { return defaultValue } } async function callTool(url: string, method: string, token: string, body: object): Promise { try { const options: FetchOptions = { method: method, headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: `Bearer ${token}` } } const noBodyMethods = ['GET', 'HEAD'] const upperMethod = method.toUpperCase() if (!noBodyMethods.includes(upperMethod)) { options.body = JSON.stringify(body) } const resp = await fetch(url, options) if (!resp.ok) { throw new Error(`HTTP error! status: ${resp.status}, statusText: ${resp.statusText}`) } if (!resp.body) { throw new Error('Response body is null or undefined') } const result: T = await resp.json() return result } catch (error) { throw new Error(`callTool: ${error}`) } } export interface SpeechToTextResult extends toolsCommonResp { result: string } export async function speechToText( baseURL: string, token: string, botId: string, engSerViceType: string, voiceFormat: string, voiceUrl: string ): Promise { const url = `${baseURL}/v1/aibot/tool/speech-to-text` return await callTool(url, 'POST', token, { botId, engSerViceType, voiceFormat, url: voiceUrl }) } export interface TextToSpeechResult extends toolsCommonResp { taskId: string } export async function textToSpeech( baseURL: string, token: string, botId: string, text: string, voiceType: number ): Promise { const url = `${baseURL}/v1/aibot/tool/text-to-speech` return await callTool(url, 'POST', token, { botId, text, voiceType }) } export interface GetTextToSpeechResult extends toolsCommonResp { taskId: string status: number statusStr: string resultUrl: string } export async function getTextToSpeech(baseURL: string, token: string, botId: string, taskId: string): Promise { const url = `${baseURL}/v1/aibot/tool/text-to-speech?botId=${botId}&taskId=${taskId}` return await callTool(url, 'GET', token, {}) } export interface WxClientMessageDto { msgType: string touser: string text: { content: string } msgId: string openKfId?: string } export async function sendWxClientMessage( baseURL: string, token: string, botId: string, triggerSrc: string, wxClientMessage: WxClientMessageDto ): Promise { const url = `${baseURL}/v1/aibot/tool/wx-client-message` return await callTool(url, 'POST', token, { botId, triggerSrc, wxClientMessage }) } export interface GetWxMediaContentResult extends toolsCommonResp { content: string } export async function getWxMediaContent( baseURL: string, token: string, botId: string, triggerSrc: string, media: string ): Promise { const url = `${baseURL}/v1/aibot/tool/wx-media-content?botId=${encodeURIComponent(botId)}&triggerSrc=${encodeURIComponent(triggerSrc)}&media=${encodeURIComponent(media)}` return await callTool(url, 'GET', token, {}) }