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..b61ed69b
--- /dev/null
+++ b/modules/tool/packages/chatBI/src/index.ts
@@ -0,0 +1,163 @@
+import { z } from 'zod';
+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().optional(),
+ 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,
+ corpId,
+ appId,
+ appAccessKey,
+ userId,
+ query,
+ 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();
+ let sentListLen = 0;
+ let buffer = '';
+ let isFinished = false;
+
+ 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: 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) {
+ 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();
+});