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));