From b0c42250ff402b57cbd98f4581ec7dd83aa33a08 Mon Sep 17 00:00:00 2001 From: ctrlz526 <3167694193@qq.com> Date: Sun, 14 Sep 2025 14:06:40 +0800 Subject: [PATCH 1/2] feat(chatBI): add initial implementation of chatBI tool with configuration, input/output types, and example usage --- modules/tool/packages/chatBI/config.ts | 88 +++++++++++ modules/tool/packages/chatBI/index.ts | 10 ++ modules/tool/packages/chatBI/logo.svg | 1 + modules/tool/packages/chatBI/package.json | 17 +++ modules/tool/packages/chatBI/src/index.ts | 137 ++++++++++++++++++ .../tool/packages/chatBI/test/index.test.ts | 8 + 6 files changed, 261 insertions(+) create mode 100644 modules/tool/packages/chatBI/config.ts create mode 100644 modules/tool/packages/chatBI/index.ts create mode 100644 modules/tool/packages/chatBI/logo.svg create mode 100644 modules/tool/packages/chatBI/package.json create mode 100644 modules/tool/packages/chatBI/src/index.ts create mode 100644 modules/tool/packages/chatBI/test/index.test.ts diff --git a/modules/tool/packages/chatBI/config.ts b/modules/tool/packages/chatBI/config.ts new file mode 100644 index 00000000..5d769e32 --- /dev/null +++ b/modules/tool/packages/chatBI/config.ts @@ -0,0 +1,88 @@ +import { defineTool } from '@tool/type'; +import { + FlowNodeInputTypeEnum, + FlowNodeOutputTypeEnum, + WorkflowIOValueTypeEnum +} from '@tool/type/fastgpt'; +import { ToolTypeEnum } from '@tool/type/tool'; + +export default defineTool({ + name: { + 'zh-CN': 'chatBI', + en: 'Template tool' + }, + type: ToolTypeEnum.tools, + description: { + 'zh-CN': 'chatBI 工具', + en: 'chatBI Tool' + }, + toolDescription: + 'send user natural language instructions to ChatBI application for execution, sse stream interface returns results', + secretInputConfig: [ + { + key: 'chatBIUrl', + label: 'chatBI 服务根地址', + description: '根地址,例如:http://example.com', + required: true, + inputType: 'secret' + }, + { + key: 'sysAccessKey', + label: 'chatBI 系统AccessKey', + description: '系统AccessKey', + required: true, + inputType: 'secret' + }, + { + key: 'corpId', + label: 'chatBI 企业ID', + description: '企业ID', + required: true, + inputType: 'secret' + } + ], + versionList: [ + { + value: '0.1.0', + description: 'Default version', + inputs: [ + { + key: 'query', + label: '用户提问内容', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: true + }, + { + key: 'appId', + label: 'chatBI 应用ID', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: true + }, + { + key: 'appAccessKey', + label: 'chatBI 应用AccessKey', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: true + }, + { + key: 'sessionId', + label: '会话id', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + required: false + } + ], + outputs: [ + { + valueType: WorkflowIOValueTypeEnum.arrayAny, + key: 'displayContentList', + label: '展示内容列表', + description: 'ChatBI返回的核心展示内容,包含文本、图表、表格等多种类型' + } + ] + } + ] +}); diff --git a/modules/tool/packages/chatBI/index.ts b/modules/tool/packages/chatBI/index.ts new file mode 100644 index 00000000..d698ed48 --- /dev/null +++ b/modules/tool/packages/chatBI/index.ts @@ -0,0 +1,10 @@ +import config from './config'; +import { InputType, OutputType, tool as toolCb } from './src'; +import { exportTool } from '@tool/utils/tool'; + +export default exportTool({ + toolCb, + InputType, + OutputType, + config +}); diff --git a/modules/tool/packages/chatBI/logo.svg b/modules/tool/packages/chatBI/logo.svg new file mode 100644 index 00000000..ca10cec3 --- /dev/null +++ b/modules/tool/packages/chatBI/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/tool/packages/chatBI/package.json b/modules/tool/packages/chatBI/package.json new file mode 100644 index 00000000..a0d4ee76 --- /dev/null +++ b/modules/tool/packages/chatBI/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fastgpt-plugins/tool-chat-bi", + "module": "index.ts", + "type": "module", + "scripts": { + "build": "bun ../../../../scripts/build.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "zod": "^3.24.2" + } +} \ No newline at end of file diff --git a/modules/tool/packages/chatBI/src/index.ts b/modules/tool/packages/chatBI/src/index.ts new file mode 100644 index 00000000..868a0869 --- /dev/null +++ b/modules/tool/packages/chatBI/src/index.ts @@ -0,0 +1,137 @@ +import { z } from 'zod'; +import type { RunToolSecondParamsType } from '@tool/type/tool'; +import { StreamDataAnswerTypeEnum } from '@tool/type/tool'; +import { getErrText } from '@tool/utils/err'; + +export const InputType = z.object({ + query: z.string(), + appId: z.string(), + appAccessKey: z.string(), + sessionId: z.string(), + chatBIUrl: z.string(), + sysAccessKey: z.string(), + corpId: z.string() +}); + +export const OutputType = z.object({ + displayContentList: z.array(z.any()) +}); + +export async function tool( + { + query, + appId, + appAccessKey, + sessionId, + chatBIUrl, + sysAccessKey, + corpId + }: z.infer, + { systemVar, streamResponse }: RunToolSecondParamsType +): Promise> { + try { + const url = new URL('/v2/open/api/common/stream/chatbi/chatNew', chatBIUrl); + + // userID get from systemVar + const userId = systemVar.user.username.split('-')[1]; + + if (!sessionId) { + sessionId = userId; + } + + const response = await fetch(url.toString(), { + method: 'POST', + headers: { + Accept: 'text/event-stream', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + sysAccessKey: sysAccessKey, + corpId: corpId, + appId: appId, + appAccessKey: appAccessKey, + userId: userId, + query: query, + sessionId: sessionId + }) + }); + + if (!response.ok) { + return Promise.reject(`HTTP ${response.status}: ${response.statusText}`); + } + + const reader = response.body?.getReader(); + + if (!reader) { + return Promise.reject('无法获取响应流'); + } + + const decoder = new TextDecoder(); + const displayContentList: any[] = []; + let buffer = ''; + let isFinished = false; + let previousLength = 0; + + while (!isFinished) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + const events = buffer.split('\n\n'); + + buffer = events.pop() || ''; + + for (const event of events) { + if (!event.trim()) continue; + + const lines = event.split('\n'); + let eventData = ''; + + for (const line of lines) { + if (line.startsWith('data:')) { + // remove "data:" and trim + eventData = line.slice(5).trim(); + break; + } + } + + if (eventData && eventData !== '') { + try { + const data = JSON.parse(eventData); + + if (data.displayContentList && data.displayContentList.length > previousLength) { + const newItems = data.displayContentList.slice(previousLength); + + for (const item of newItems) { + // 只有等待 type 输出成功后再输出那一整个 + if (item.type !== undefined) { + await streamResponse({ + type: StreamDataAnswerTypeEnum.answer, + content: JSON.stringify(item) + }); + displayContentList.push(JSON.stringify([item])); + } + } + + previousLength = data.displayContentList.length; + } + + if (data.isFinished) { + isFinished = true; + break; + } + } catch (e) { + continue; + } + } + } + } + + return { + displayContentList + }; + } catch (error) { + return Promise.reject(getErrText(error)); + } +} diff --git a/modules/tool/packages/chatBI/test/index.test.ts b/modules/tool/packages/chatBI/test/index.test.ts new file mode 100644 index 00000000..b70e289f --- /dev/null +++ b/modules/tool/packages/chatBI/test/index.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'vitest'; +import tool from '..'; + +test(async () => { + expect(tool.name).toBeDefined(); + expect(tool.description).toBeDefined(); + expect(tool.cb).toBeDefined(); +}); From 45151a67b4954a733fcaf30bfa1a98f97c3b7e01 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Thu, 18 Sep 2025 18:18:31 +0800 Subject: [PATCH 2/2] feat: return RENDER code block to render --- modules/tool/packages/chatBI/src/index.ts | 84 +++++++++++++++-------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/modules/tool/packages/chatBI/src/index.ts b/modules/tool/packages/chatBI/src/index.ts index 868a0869..b61ed69b 100644 --- a/modules/tool/packages/chatBI/src/index.ts +++ b/modules/tool/packages/chatBI/src/index.ts @@ -3,18 +3,35 @@ import type { RunToolSecondParamsType } from '@tool/type/tool'; import { StreamDataAnswerTypeEnum } from '@tool/type/tool'; import { getErrText } from '@tool/utils/err'; +type DataType = { + isFinished: boolean; + isSuccess: boolean; + errorMessage?: string; + processList: unknown[]; + displayContentList: ContentType[]; + streamData: string; +}; + +enum ContentTypeEnum { + text = 'TEXT' +} +type ContentType = { + content: string; + type: ContentTypeEnum; +}; + export const InputType = z.object({ query: z.string(), appId: z.string(), appAccessKey: z.string(), - sessionId: z.string(), + sessionId: z.string().optional(), chatBIUrl: z.string(), sysAccessKey: z.string(), corpId: z.string() }); export const OutputType = z.object({ - displayContentList: z.array(z.any()) + // displayContentList: z.array(z.any()) }); export async function tool( @@ -46,13 +63,13 @@ export async function tool( 'Content-Type': 'application/json' }, body: JSON.stringify({ - sysAccessKey: sysAccessKey, - corpId: corpId, - appId: appId, - appAccessKey: appAccessKey, - userId: userId, - query: query, - sessionId: sessionId + sysAccessKey, + corpId, + appId, + appAccessKey, + userId, + query, + sessionId }) }); @@ -67,10 +84,9 @@ export async function tool( } const decoder = new TextDecoder(); - const displayContentList: any[] = []; + let sentListLen = 0; let buffer = ''; let isFinished = false; - let previousLength = 0; while (!isFinished) { const { done, value } = await reader.read(); @@ -98,23 +114,33 @@ export async function tool( if (eventData && eventData !== '') { try { - const data = JSON.parse(eventData); - - if (data.displayContentList && data.displayContentList.length > previousLength) { - const newItems = data.displayContentList.slice(previousLength); - - for (const item of newItems) { - // 只有等待 type 输出成功后再输出那一整个 - if (item.type !== undefined) { - await streamResponse({ - type: StreamDataAnswerTypeEnum.answer, - content: JSON.stringify(item) - }); - displayContentList.push(JSON.stringify([item])); - } - } - - previousLength = data.displayContentList.length; + const data: DataType = JSON.parse(eventData); + + if (data.displayContentList && data.displayContentList.length > sentListLen) { + // only send the last items + const sendList = data.displayContentList.slice(sentListLen); + const texts = sendList + .filter((item) => item.type === 'TEXT') + .map((item) => item.content) + .join('\n'); + const unTexts = sendList.filter((item) => item.type !== 'TEXT'); + const content = + texts + + '\n' + + (unTexts.length > 0 ? `\`\`\`RENDER\n${JSON.stringify(unTexts)}\n\`\`\`\n` : ''); + + streamResponse({ + content, + type: StreamDataAnswerTypeEnum.answer + }); + sentListLen = data.displayContentList.length; + } + + if (data.streamData) { + streamResponse({ + content: data.streamData, + type: StreamDataAnswerTypeEnum.answer + }); } if (data.isFinished) { @@ -129,7 +155,7 @@ export async function tool( } return { - displayContentList + // displayContentList }; } catch (error) { return Promise.reject(getErrText(error));