Perf system plugin and worker (#2126)

* perf: worker pool

* perf: worker register

* perf: worker controller

* perf: system plugin worker

* perf: system plugin worker

* perf: worker

* perf: worker

* worker timeout

* perf: copy icon
This commit is contained in:
Archer
2024-07-23 11:23:42 +08:00
committed by GitHub
parent a4787bce5c
commit e99c91aaa6
34 changed files with 433 additions and 235 deletions

View File

@@ -23,3 +23,8 @@ weight: 816
1. 新增 - 重构系统插件的结构。允许向开源社区 PR 系统插件,具体可见: [如何向 FastGPT 社区提交系统插件](https://fael3z0zfze.feishu.cn/wiki/ERZnw9R26iRRG0kXZRec6WL9nwh)。 1. 新增 - 重构系统插件的结构。允许向开源社区 PR 系统插件,具体可见: [如何向 FastGPT 社区提交系统插件](https://fael3z0zfze.feishu.cn/wiki/ERZnw9R26iRRG0kXZRec6WL9nwh)。
2. 新增 - DuckDuckGo 系统插件。 2. 新增 - DuckDuckGo 系统插件。
3. 优化 - 节点图标。 3. 优化 - 节点图标。
4. 优化 - 对话框引用增加额外复制案件,便于复制。增加引用内容折叠。
5. 修复 - Permission 表声明问题。
6. 修复 - 并行执行节点,运行时间未正确记录。
7. 修复 - 简易模式,首次进入,无法正确获取知识库配置。
8. 修复 - Log level 配置

View File

@@ -4,6 +4,7 @@
"dependencies": { "dependencies": {
"duck-duck-scrape": "^2.2.5", "duck-duck-scrape": "^2.2.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"axios": "^1.5.1",
"expr-eval": "^2.0.2" "expr-eval": "^2.0.2"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -4,10 +4,12 @@ import { FastGPTProUrl, isProduction } from '../service/common/system/constants'
import { GET, POST } from '@fastgpt/service/common/api/plusRequest'; import { GET, POST } from '@fastgpt/service/common/api/plusRequest';
import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type'; import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { WorkerNameEnum, runWorker } from '@fastgpt/service/worker/utils';
let list = [ // Run in main thread
'getTime', const staticPluginList = ['getTime', 'fetchUrl'];
'fetchUrl', // Run in worker thread (Have npm packages)
const packagePluginList = [
'mathExprVal', 'mathExprVal',
'duckduckgo', 'duckduckgo',
'duckduckgo/search', 'duckduckgo/search',
@@ -16,6 +18,8 @@ let list = [
'duckduckgo/searchVideo' 'duckduckgo/searchVideo'
]; ];
const list = [...staticPluginList, ...packagePluginList];
/* Get plugins */ /* Get plugins */
export const getCommunityPlugins = () => { export const getCommunityPlugins = () => {
return list.map<SystemPluginTemplateItemType>((name) => { return list.map<SystemPluginTemplateItemType>((name) => {
@@ -58,8 +62,7 @@ export const getSystemPluginTemplates = async (refresh = false) => {
}; };
export const getCommunityCb = async () => { export const getCommunityCb = async () => {
// Do not modify the following code const loadCommunityModule = async (name: string) => {
const loadModule = async (name: string) => {
const module = await import(`./src/${name}/index`); const module = await import(`./src/${name}/index`);
return module.default; return module.default;
}; };
@@ -70,7 +73,14 @@ export const getCommunityCb = async () => {
try { try {
return { return {
name, name,
cb: await loadModule(name) cb: staticPluginList.includes(name)
? await loadCommunityModule(name)
: (e: any) => {
return runWorker(WorkerNameEnum.systemPluginRun, {
pluginName: name,
data: e
});
}
}; };
} catch (error) {} } catch (error) {}
}) })

View File

@@ -0,0 +1,24 @@
import { SystemPluginResponseType } from '../type';
import { parentPort } from 'worker_threads';
const loadModule = async (name: string): Promise<(e: any) => SystemPluginResponseType> => {
const module = await import(`../src/${name}/index`);
return module.default;
};
parentPort?.on('message', async ({ pluginName, data }: { pluginName: string; data: any }) => {
try {
const cb = await loadModule(pluginName);
parentPort?.postMessage({
type: 'success',
data: await cb(data)
});
} catch (error) {
parentPort?.postMessage({
type: 'error',
data: error
});
}
process.exit();
});

View File

@@ -32,14 +32,13 @@ const main = async (props: Props, retry = 3): Response => {
}; };
} catch (error) { } catch (error) {
if (retry <= 0) { if (retry <= 0) {
addLog.warn('DuckDuckGo error', { error });
return { return {
result: 'Failed to fetch data' result: 'Failed to fetch data'
}; };
} }
addLog.warn('DuckDuckGo error', { error }); await delay(Math.random() * 5000);
await delay(Math.random() * 2000);
return main(props, retry - 1); return main(props, retry - 1);
} }
}; };

View File

@@ -31,14 +31,13 @@ const main = async (props: Props, retry = 3): Response => {
}; };
} catch (error) { } catch (error) {
if (retry <= 0) { if (retry <= 0) {
addLog.warn('DuckDuckGo error', { error });
return { return {
result: 'Failed to fetch data' result: 'Failed to fetch data'
}; };
} }
addLog.warn('DuckDuckGo error', { error }); await delay(Math.random() * 5000);
await delay(Math.random() * 2000);
return main(props, retry - 1); return main(props, retry - 1);
} }
}; };

View File

@@ -32,14 +32,13 @@ const main = async (props: Props, retry = 3): Response => {
}; };
} catch (error) { } catch (error) {
if (retry <= 0) { if (retry <= 0) {
addLog.warn('DuckDuckGo error', { error });
return { return {
result: 'Failed to fetch data' result: 'Failed to fetch data'
}; };
} }
addLog.warn('DuckDuckGo error', { error }); await delay(Math.random() * 5000);
await delay(Math.random() * 2000);
return main(props, retry - 1); return main(props, retry - 1);
} }
}; };

View File

@@ -32,14 +32,13 @@ const main = async (props: Props, retry = 3): Response => {
}; };
} catch (error) { } catch (error) {
if (retry <= 0) { if (retry <= 0) {
addLog.warn('DuckDuckGo error', { error });
return { return {
result: 'Failed to fetch data' result: 'Failed to fetch data'
}; };
} }
addLog.warn('DuckDuckGo error', { error }); await delay(Math.random() * 5000);
await delay(Math.random() * 2000);
return main(props, retry - 1); return main(props, retry - 1);
} }
}; };

View File

@@ -1,4 +1,3 @@
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { Parser } from 'expr-eval'; import { Parser } from 'expr-eval';
type Props = { type Props = {

View File

@@ -6,7 +6,7 @@ import { addHours } from 'date-fns';
import { WorkerNameEnum, runWorker } from '../../../worker/utils'; import { WorkerNameEnum, runWorker } from '../../../worker/utils';
import fs from 'fs'; import fs from 'fs';
import { detectFileEncoding } from '@fastgpt/global/common/file/tools'; import { detectFileEncoding } from '@fastgpt/global/common/file/tools';
import { ReadFileResponse } from '../../../worker/file/type'; import type { ReadFileResponse } from '../../../worker/readFile/type';
export const initMarkdownText = ({ export const initMarkdownText = ({
teamId, teamId,

View File

@@ -6,95 +6,41 @@ import {
} from '@fastgpt/global/core/ai/type'; } from '@fastgpt/global/core/ai/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt'; import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { ChatItemType } from '@fastgpt/global/core/chat/type'; import { ChatItemType } from '@fastgpt/global/core/chat/type';
import { WorkerNameEnum, getWorker } from '../../../worker/utils'; import { WorkerNameEnum, getWorkerController } from '../../../worker/utils';
import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants'; import { ChatCompletionRequestMessageRoleEnum } from '@fastgpt/global/core/ai/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { addLog } from '../../system/log'; import { addLog } from '../../system/log';
export const getTiktokenWorker = () => { export const countGptMessagesTokens = async (
const maxWorkers = global.systemEnv?.tokenWorkers || 20;
if (!global.tiktokenWorkers) {
global.tiktokenWorkers = [];
}
if (global.tiktokenWorkers.length >= maxWorkers) {
return global.tiktokenWorkers[Math.floor(Math.random() * global.tiktokenWorkers.length)];
}
const worker = getWorker(WorkerNameEnum.countGptMessagesTokens);
const i = global.tiktokenWorkers.push({
index: global.tiktokenWorkers.length,
worker,
callbackMap: {}
});
worker.on('message', ({ id, data }: { id: string; data: number }) => {
const callback = global.tiktokenWorkers[i - 1]?.callbackMap?.[id];
if (callback) {
callback?.(data);
delete global.tiktokenWorkers[i - 1].callbackMap[id];
}
});
return global.tiktokenWorkers[i - 1];
};
export const countGptMessagesTokens = (
messages: ChatCompletionMessageParam[], messages: ChatCompletionMessageParam[],
tools?: ChatCompletionTool[], tools?: ChatCompletionTool[],
functionCall?: ChatCompletionCreateParams.Function[] functionCall?: ChatCompletionCreateParams.Function[]
) => { ) => {
return new Promise<number>(async (resolve) => { try {
try { const workerController = getWorkerController<
const start = Date.now(); {
messages: ChatCompletionMessageParam[];
tools?: ChatCompletionTool[];
functionCall?: ChatCompletionCreateParams.Function[];
},
number
>({
name: WorkerNameEnum.countGptMessagesTokens,
maxReservedThreads: global.systemEnv?.tokenWorkers || 20
});
const { worker, callbackMap } = getTiktokenWorker(); const total = await workerController.run({ messages, tools, functionCall });
const id = getNanoid(); return total;
} catch (error) {
const timer = setTimeout(() => { addLog.error('Count token error', error);
console.log('Count token Time out'); const total = messages.reduce((sum, item) => {
resolve( if (item.content) {
messages.reduce((sum, item) => { return sum + item.content.length * 0.5;
if (item.content) { }
return sum + item.content.length * 0.5; return sum;
} }, 0);
return sum; return total;
}, 0) }
);
delete callbackMap[id];
}, 60000);
callbackMap[id] = (data) => {
// 检测是否有内存泄漏
addLog.debug(`Count token time: ${Date.now() - start}, token: ${data}`);
// console.log(process.memoryUsage());
resolve(data);
clearTimeout(timer);
};
// 可以进一步优化(传递100w token数据,实际需要300ms,较慢)
worker.postMessage({
id,
messages,
tools,
functionCall
});
} catch (error) {
addLog.error('Count token error', error);
const total = messages.reduce((sum, item) => {
if (item.content) {
return sum + item.content.length;
}
return sum;
}, 0);
resolve(total);
}
});
}; };
export const countMessagesTokens = (messages: ChatItemType[]) => { export const countMessagesTokens = (messages: ChatItemType[]) => {

View File

@@ -30,7 +30,7 @@ const { LOG_LEVEL, STORE_LOG_LEVEL } = (() => {
const STORE_LOG_LEVEL = (process.env.STORE_LOG_LEVEL || '').toLocaleLowerCase(); const STORE_LOG_LEVEL = (process.env.STORE_LOG_LEVEL || '').toLocaleLowerCase();
return { return {
LOG_LEVEL: envLogLevelMap[LOG_LEVEL] || LogLevelEnum.info, LOG_LEVEL: envLogLevelMap[LOG_LEVEL] ?? LogLevelEnum.info,
STORE_LOG_LEVEL: envLogLevelMap[STORE_LOG_LEVEL] ?? 99 STORE_LOG_LEVEL: envLogLevelMap[STORE_LOG_LEVEL] ?? 99
}; };
})(); })();

View File

@@ -441,11 +441,18 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
// token filter // token filter
const filterMaxTokensResult = await (async () => { const filterMaxTokensResult = await (async () => {
const tokensScoreFilter = await Promise.all(
scoreFilter.map(async (item) => ({
...item,
tokens: await countPromptTokens(item.q + item.a)
}))
);
const results: SearchDataResponseItemType[] = []; const results: SearchDataResponseItemType[] = [];
let totalTokens = 0; let totalTokens = 0;
for await (const item of scoreFilter) { for await (const item of tokensScoreFilter) {
totalTokens += await countPromptTokens(item.q + item.a); totalTokens += item.tokens;
if (totalTokens > maxTokens + 500) { if (totalTokens > maxTokens + 500) {
break; break;

View File

@@ -122,7 +122,6 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
let chatAssistantResponse: AIChatItemValueItemType[] = []; // The value will be returned to the user let chatAssistantResponse: AIChatItemValueItemType[] = []; // The value will be returned to the user
let chatNodeUsages: ChatNodeUsageType[] = []; let chatNodeUsages: ChatNodeUsageType[] = [];
let toolRunResponse: ToolRunResponseItemType; let toolRunResponse: ToolRunResponseItemType;
let runningTime = Date.now();
let debugNextStepRunNodes: RuntimeNodeItemType[] = []; let debugNextStepRunNodes: RuntimeNodeItemType[] = [];
/* Store special response field */ /* Store special response field */
@@ -142,13 +141,8 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
[DispatchNodeResponseKeyEnum.assistantResponses]?: AIChatItemValueItemType[]; // tool module, save the response value [DispatchNodeResponseKeyEnum.assistantResponses]?: AIChatItemValueItemType[]; // tool module, save the response value
} }
) { ) {
const time = Date.now();
if (responseData) { if (responseData) {
chatResponses.push({ chatResponses.push(responseData);
...responseData,
runningTime: +((time - runningTime) / 1000).toFixed(2)
});
} }
if (nodeDispatchUsages) { if (nodeDispatchUsages) {
chatNodeUsages = chatNodeUsages.concat(nodeDispatchUsages); chatNodeUsages = chatNodeUsages.concat(nodeDispatchUsages);
@@ -175,8 +169,6 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
}); });
} }
} }
runningTime = time;
} }
/* Pass the output of the module to the next stage */ /* Pass the output of the module to the next stage */
function nodeOutput( function nodeOutput(
@@ -328,6 +320,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
status: 'running' status: 'running'
}); });
} }
const startTime = Date.now();
// get node running params // get node running params
const params = getNodeRunParams(node); const params = getNodeRunParams(node);
@@ -362,6 +355,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
nodeId: node.nodeId, nodeId: node.nodeId,
moduleName: node.name, moduleName: node.name,
moduleType: node.flowNodeType, moduleType: node.flowNodeType,
runningTime: +((Date.now() - startTime) / 1000).toFixed(2),
...dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse] ...dispatchRes[DispatchNodeResponseKeyEnum.nodeResponse]
}; };
})(); })();

View File

@@ -7,6 +7,7 @@ import {
LLMModelItemType LLMModelItemType
} from '@fastgpt/global/core/ai/model.d'; } from '@fastgpt/global/core/ai/model.d';
import { SubPlanType } from '@fastgpt/global/support/wallet/sub/type'; import { SubPlanType } from '@fastgpt/global/support/wallet/sub/type';
import { WorkerNameEnum, WorkerPool } from './worker/utils';
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
declare global { declare global {
@@ -20,12 +21,8 @@ declare global {
var whisperModel: WhisperModelType; var whisperModel: WhisperModelType;
var reRankModels: ReRankModelItemType[]; var reRankModels: ReRankModelItemType[];
var tiktokenWorkers: {
index: number;
worker: Worker;
callbackMap: Record<string, (e: number) => void>;
}[];
var systemLoadedGlobalVariables: boolean; var systemLoadedGlobalVariables: boolean;
var systemLoadedGlobalConfig: boolean; var systemLoadedGlobalConfig: boolean;
var workerPoll: Record<WorkerNameEnum, WorkerPool>;
} }

View File

@@ -59,16 +59,16 @@ export const readPdfFile = async ({ buffer }: ReadRawTextByBuffer): Promise<Read
const loadingTask = pdfjs.getDocument(buffer.buffer); const loadingTask = pdfjs.getDocument(buffer.buffer);
const doc = await loadingTask.promise; const doc = await loadingTask.promise;
const pageTextPromises = []; // Avoid OOM.
for (let pageNo = 1; pageNo <= doc.numPages; pageNo++) { let result = '';
pageTextPromises.push(readPDFPage(doc, pageNo)); const pageArr = Array.from({ length: doc.numPages }, (_, i) => i + 1);
for await (const pageNo of pageArr) {
result += await readPDFPage(doc, pageNo);
} }
const pageTexts = await Promise.all(pageTextPromises);
loadingTask.destroy(); loadingTask.destroy();
return { return {
rawText: pageTexts.join('') rawText: result
}; };
}; };

View File

@@ -1,19 +1,32 @@
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
import path from 'path'; import path from 'path';
import { addLog } from '../common/system/log';
export enum WorkerNameEnum { export enum WorkerNameEnum {
readFile = 'readFile', readFile = 'readFile',
htmlStr2Md = 'htmlStr2Md', htmlStr2Md = 'htmlStr2Md',
countGptMessagesTokens = 'countGptMessagesTokens' countGptMessagesTokens = 'countGptMessagesTokens',
systemPluginRun = 'systemPluginRun'
} }
export const getSafeEnv = () => {
return {
LOG_LEVEL: process.env.LOG_LEVEL,
STORE_LOG_LEVEL: process.env.STORE_LOG_LEVEL,
NODE_ENV: process.env.NODE_ENV
};
};
export const getWorker = (name: WorkerNameEnum) => { export const getWorker = (name: WorkerNameEnum) => {
const workerPath = path.join(process.cwd(), '.next', 'server', 'worker', `${name}.js`); const workerPath = path.join(process.cwd(), '.next', 'server', 'worker', `${name}.js`);
return new Worker(workerPath); return new Worker(workerPath, {
env: getSafeEnv()
});
}; };
export const runWorker = <T = any>(name: WorkerNameEnum, params?: Record<string, any>) => { export const runWorker = <T = any>(name: WorkerNameEnum, params?: Record<string, any>) => {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const start = Date.now();
const worker = getWorker(name); const worker = getWorker(name);
worker.postMessage(params); worker.postMessage(params);
@@ -22,6 +35,11 @@ export const runWorker = <T = any>(name: WorkerNameEnum, params?: Record<string,
if (msg.type === 'error') return reject(msg.data); if (msg.type === 'error') return reject(msg.data);
resolve(msg.data); resolve(msg.data);
const time = Date.now() - start;
if (time > 1000) {
addLog.info(`Worker ${name} run time: ${time}ms`);
}
}); });
worker.on('error', (err) => { worker.on('error', (err) => {
@@ -34,3 +52,169 @@ export const runWorker = <T = any>(name: WorkerNameEnum, params?: Record<string,
}); });
}); });
}; };
type WorkerRunTaskType<T> = { data: T; resolve: (e: any) => void; reject: (e: any) => void };
type WorkerQueueItem = {
id: string;
worker: Worker;
status: 'running' | 'idle';
taskTime: number;
timeoutId?: NodeJS.Timeout;
resolve: (e: any) => void;
reject: (e: any) => void;
};
type WorkerResponse<T = any> = {
id: string;
type: 'success' | 'error';
data: T;
};
/*
多线程任务管理
* 全局只需要创建一个示例
* 可以设置最大常驻线程(不会被销毁),线程满了后,后续任务会等待执行。
* 每次执行,会把数据丢到一个空闲线程里运行。主线程需要监听子线程返回的数据,并执行对于的 callback主要是通过 workerId 进行标记。
* 务必保证,每个线程只会同时运行 1 个任务,否则 callback 会对应不上。
*/
export class WorkerPool<Props = Record<string, any>, Response = any> {
name: WorkerNameEnum;
maxReservedThreads: number;
workerQueue: WorkerQueueItem[] = [];
waitQueue: WorkerRunTaskType<Props>[] = [];
constructor({ name, maxReservedThreads }: { name: WorkerNameEnum; maxReservedThreads: number }) {
this.name = name;
this.maxReservedThreads = maxReservedThreads;
}
runTask({ data, resolve, reject }: WorkerRunTaskType<Props>) {
// Get idle worker or create a new worker
const runningWorker = (() => {
const worker = this.workerQueue.find((item) => item.status === 'idle');
if (worker) return worker;
if (this.workerQueue.length < this.maxReservedThreads) {
return this.createWorker();
}
})();
if (runningWorker) {
// Update memory data to latest task
runningWorker.status = 'running';
runningWorker.taskTime = Date.now();
runningWorker.resolve = resolve;
runningWorker.reject = reject;
runningWorker.timeoutId = setTimeout(() => {
reject('Worker timeout');
}, 30000);
runningWorker.worker.postMessage({
id: runningWorker.id,
...data
});
} else {
// Not enough worker, push to wait queue
this.waitQueue.push({ data, resolve, reject });
}
}
run(data: Props) {
// watch memory
addLog.debug(`${this.name} worker queueLength: ${this.workerQueue.length}`);
return new Promise<Response>((resolve, reject) => {
/*
Whether the task is executed immediately or delayed, the promise callback will dispatch after task complete.
*/
this.runTask({
data,
resolve,
reject
});
}).finally(() => {
// Run wait queue
const waitTask = this.waitQueue.shift();
if (waitTask) {
this.runTask(waitTask);
}
});
}
createWorker() {
// Create a new worker and push it queue.
const workerId = `${Date.now()}${Math.random()}`;
const worker = getWorker(this.name);
const item: WorkerQueueItem = {
id: workerId,
worker,
status: 'running',
taskTime: Date.now(),
resolve: () => {},
reject: () => {}
};
this.workerQueue.push(item);
// watch response
worker.on('message', ({ id, type, data }: WorkerResponse<Response>) => {
// Run callback
const workerItem = this.workerQueue.find((item) => item.id === id);
if (!workerItem) {
addLog.warn('Invalid worker', { id, type, data });
return;
}
if (type === 'success') {
workerItem.resolve(data);
} else if (type === 'error') {
workerItem.reject(data);
}
// Clear timeout timer and update worker status
clearTimeout(workerItem.timeoutId);
workerItem.status = 'idle';
});
// Worker error, terminate and delete it.Un catch error)
worker.on('error', (err) => {
addLog.warn('Worker error', { err });
this.deleteWorker(workerId);
});
worker.on('messageerror', (err) => {
addLog.warn('Worker error', { err });
this.deleteWorker(workerId);
});
return item;
}
deleteWorker(workerId: string) {
const item = this.workerQueue.find((item) => item.id === workerId);
if (item) {
item.reject?.('error');
clearTimeout(item.timeoutId);
item.worker.terminate();
}
this.workerQueue = this.workerQueue.filter((item) => item.id !== workerId);
}
}
export const getWorkerController = <Props, Response>(props: {
name: WorkerNameEnum;
maxReservedThreads: number;
}) => {
if (!global.workerPoll) {
// @ts-ignore
global.workerPoll = {};
}
const name = props.name;
if (global.workerPoll[name]) return global.workerPoll[name] as WorkerPool<Props, Response>;
global.workerPoll[name] = new WorkerPool(props);
return global.workerPoll[name] as WorkerPool<Props, Response>;
};

View File

@@ -1,3 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.96967 11.7803C4.26256 12.0732 4.73744 12.0732 5.03033 11.7803L9 7.81066L12.9697 11.7803C13.2626 12.0732 13.7374 12.0732 14.0303 11.7803C14.3232 11.4874 14.3232 11.0126 14.0303 10.7197L9.53033 6.21967C9.23744 5.92678 8.76256 5.92678 8.46967 6.21967L3.96967 10.7197C3.67678 11.0126 3.67678 11.4874 3.96967 11.7803Z" fill="#667085"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M3.96967 11.7803C4.26256 12.0732 4.73744 12.0732 5.03033 11.7803L9 7.81066L12.9697 11.7803C13.2626 12.0732 13.7374 12.0732 14.0303 11.7803C14.3232 11.4874 14.3232 11.0126 14.0303 10.7197L9.53033 6.21967C9.23744 5.92678 8.76256 5.92678 8.46967 6.21967L3.96967 10.7197C3.67678 11.0126 3.67678 11.4874 3.96967 11.7803Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 449 B

9
pnpm-lock.yaml generated
View File

@@ -81,6 +81,9 @@ importers:
packages/plugins: packages/plugins:
dependencies: dependencies:
axios:
specifier: ^1.5.1
version: 1.7.2
duck-duck-scrape: duck-duck-scrape:
specifier: ^2.2.5 specifier: ^2.2.5
version: 2.2.5 version: 2.2.5
@@ -13823,7 +13826,7 @@ snapshots:
eslint: 8.56.0 eslint: 8.56.0
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0) eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.56.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.56.0)
eslint-plugin-react: 7.34.4(eslint@8.56.0) eslint-plugin-react: 7.34.4(eslint@8.56.0)
eslint-plugin-react-hooks: 4.6.2(eslint@8.56.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.56.0)
@@ -13847,7 +13850,7 @@ snapshots:
enhanced-resolve: 5.17.0 enhanced-resolve: 5.17.0
eslint: 8.56.0 eslint: 8.56.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)
fast-glob: 3.3.2 fast-glob: 3.3.2
get-tsconfig: 4.7.5 get-tsconfig: 4.7.5
is-core-module: 2.14.0 is-core-module: 2.14.0
@@ -13869,7 +13872,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0):
dependencies: dependencies:
array-includes: 3.1.8 array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5 array.prototype.findlastindex: 1.2.5

View File

@@ -1,5 +1,6 @@
const { i18n } = require('./next-i18next.config'); const { i18n } = require('./next-i18next.config');
const path = require('path'); const path = require('path');
const fs = require('fs');
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
@@ -53,17 +54,10 @@ const nextConfig = {
const entries = await oldEntry(...args); const entries = await oldEntry(...args);
return { return {
...entries, ...entries,
'worker/htmlStr2Md': path.resolve( ...getWorkerConfig(),
'worker/systemPluginRun': path.resolve(
process.cwd(), process.cwd(),
'../../packages/service/worker/htmlStr2Md/index.ts' '../../packages/plugins/runtime/worker.ts'
),
'worker/countGptMessagesTokens': path.resolve(
process.cwd(),
'../../packages/service/worker/tiktoken/countGptMessagesTokens.ts'
),
'worker/readFile': path.resolve(
process.cwd(),
'../../packages/service/worker/file/read.ts'
) )
}; };
} }
@@ -95,3 +89,39 @@ const nextConfig = {
}; };
module.exports = nextConfig; module.exports = nextConfig;
function getWorkerConfig() {
const result = fs.readdirSync(path.resolve(__dirname, '../../packages/service/worker'));
// 获取所有的目录名
const folderList = result.filter((item) => {
return fs
.statSync(path.resolve(__dirname, '../../packages/service/worker', item))
.isDirectory();
});
/*
{
'worker/htmlStr2Md': path.resolve(
process.cwd(),
'../../packages/service/worker/htmlStr2Md/index.ts'
),
'worker/countGptMessagesTokens': path.resolve(
process.cwd(),
'../../packages/service/worker/countGptMessagesTokens/index.ts'
),
'worker/readFile': path.resolve(
process.cwd(),
'../../packages/service/worker/readFile/index.ts'
)
}
*/
const workerConfig = folderList.reduce((acc, item) => {
acc[`worker/${item}`] = path.resolve(
process.cwd(),
`../../packages/service/worker/${item}/index.ts`
);
return acc;
}, {});
return workerConfig;
}

View File

@@ -144,7 +144,16 @@ const ChatItem = ({
)} )}
</Flex> </Flex>
{/* content */} {/* content */}
<Box mt={['6px', 2]} className="chat-box-card" textAlign={styleMap.textAlign}> <Box
mt={['6px', 2]}
className="chat-box-card"
textAlign={styleMap.textAlign}
_hover={{
'& .footer-copy': {
display: 'block'
}
}}
>
<Card <Card
{...MessageCardStyle} {...MessageCardStyle}
bg={styleMap.bg} bg={styleMap.bg}
@@ -156,19 +165,21 @@ const ChatItem = ({
{/* 对话框底部的复制按钮 */} {/* 对话框底部的复制按钮 */}
{type == ChatRoleEnum.AI && (!isChatting || (isChatting && !isLastChild)) && ( {type == ChatRoleEnum.AI && (!isChatting || (isChatting && !isLastChild)) && (
<Box <Box
className="footer-copy"
display={['block', 'none']}
position={'absolute'} position={'absolute'}
bottom={0} bottom={0}
right={[0, -2]} right={0}
color={'myGray.400'}
transform={'translateX(100%)'} transform={'translateX(100%)'}
> >
<MyTooltip label={t('common.Copy')}> <MyTooltip label={t('common:common.Copy')}>
<MyIcon <MyIcon
w={'14px'} w={'1rem'}
cursor="pointer" cursor="pointer"
p="5px" p="5px"
bg="white" bg="white"
name={'copy'} name={'copy'}
color={'myGray.500'}
_hover={{ color: 'primary.600' }} _hover={{ color: 'primary.600' }}
onClick={() => copyData(chatText)} onClick={() => copyData(chatText)}
/> />

View File

@@ -40,7 +40,6 @@ const ResponseTags = ({
sourceName: string; sourceName: string;
}; };
}>(); }>();
const [isOverflow, setIsOverflow] = useState<boolean>(true);
const [quoteFolded, setQuoteFolded] = useState<boolean>(true); const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
const [contextModalData, setContextModalData] = const [contextModalData, setContextModalData] =
useState<DispatchNodeResponseType['historyPreview']>(); useState<DispatchNodeResponseType['historyPreview']>();
@@ -51,11 +50,9 @@ const ResponseTags = ({
} = useDisclosure(); } = useDisclosure();
const quoteListSize = useSize(quoteListRef); const quoteListSize = useSize(quoteListRef);
useEffect(() => { const quoteIsOverflow = quoteListRef.current
setIsOverflow( ? quoteListRef.current.scrollHeight > (isPc ? 50 : 55)
quoteListRef.current ? quoteListRef.current.scrollHeight > (isPc ? 50 : 55) : true : true;
);
}, [isOverflow, quoteListSize]);
const { const {
llmModuleAccount, llmModuleAccount,
@@ -114,7 +111,7 @@ const ResponseTags = ({
<Box width={'100%'}> <Box width={'100%'}>
<ChatBoxDivider icon="core/chat/quoteFill" text={t('common:core.chat.Quote')} />{' '} <ChatBoxDivider icon="core/chat/quoteFill" text={t('common:core.chat.Quote')} />{' '}
</Box> </Box>
{quoteFolded && isOverflow && ( {quoteFolded && quoteIsOverflow && (
<MyIcon <MyIcon
_hover={{ color: 'primary.500', cursor: 'pointer' }} _hover={{ color: 'primary.500', cursor: 'pointer' }}
name="core/chat/chevronDown" name="core/chat/chevronDown"
@@ -124,89 +121,79 @@ const ResponseTags = ({
)} )}
</Flex> </Flex>
<Flex alignItems={'center'} flexWrap={'wrap'} gap={2} position={'relative'}> <Flex
{ ref={quoteListRef}
<Collapse alignItems={'center'}
startingHeight={isPc ? '50px' : '55px'} position={'relative'}
in={(!quoteFolded && isOverflow) || !isOverflow} flexWrap={'wrap'}
> gap={2}
<Flex maxH={quoteFolded && quoteIsOverflow ? ['50px', '55px'] : 'auto'}
ref={quoteListRef} overflow={'hidden'}
alignItems={'center'} _after={
position={'relative'} quoteFolded && quoteIsOverflow
flexWrap={'wrap'} ? {
gap={2} content: '""',
height={quoteFolded && isOverflow ? ['55px', '50px'] : 'auto'} position: 'absolute',
overflow={'hidden'} zIndex: 2,
_after={ bottom: 0,
quoteFolded && isOverflow left: 0,
? { width: '100%',
content: '""', height: '50%',
position: 'absolute', background:
zIndex: 2, 'linear-gradient(to bottom, rgba(247,247,247,0), rgba(247, 247, 247, 0.91))'
bottom: 0,
left: 0,
width: '100%',
height: '50%',
background:
'linear-gradient(to bottom, rgba(247,247,247,0), rgba(247, 247, 247, 0.91))',
pointerEvents: 'none'
}
: {}
} }
> : {}
{sourceList.map((item) => {
return (
<MyTooltip key={item.collectionId} label={t('core.chat.quote.Read Quote')}>
<Flex
alignItems={'center'}
fontSize={'xs'}
border={'sm'}
py={1.5}
px={2}
borderRadius={'sm'}
_hover={{
'.controller': {
display: 'flex'
}
}}
overflow={'hidden'}
position={'relative'}
cursor={'pointer'}
onClick={(e) => {
e.stopPropagation();
setQuoteModalData({
rawSearch: quoteList,
metadata: {
collectionId: item.collectionId,
sourceId: item.sourceId,
sourceName: item.sourceName
}
});
}}
>
<MyIcon name={item.icon as any} mr={1} flexShrink={0} w={'12px'} />
<Box className="textEllipsis3" wordBreak={'break-all'} flex={'1 0 0'}>
{item.sourceName}
</Box>
</Flex>
</MyTooltip>
);
})}
{isOverflow && !quoteFolded && (
<MyIcon
position={'absolute'}
bottom={0}
right={0}
_hover={{ color: 'primary.500', cursor: 'pointer' }}
name="core/chat/chevronUp"
w={'14px'}
onClick={() => setQuoteFolded(!quoteFolded)}
/>
)}
</Flex>
</Collapse>
} }
>
{sourceList.map((item) => {
return (
<MyTooltip key={item.collectionId} label={t('common:core.chat.quote.Read Quote')}>
<Flex
alignItems={'center'}
fontSize={'xs'}
border={'sm'}
py={1.5}
px={2}
borderRadius={'sm'}
_hover={{
'.controller': {
display: 'flex'
}
}}
overflow={'hidden'}
position={'relative'}
cursor={'pointer'}
onClick={(e) => {
e.stopPropagation();
setQuoteModalData({
rawSearch: quoteList,
metadata: {
collectionId: item.collectionId,
sourceId: item.sourceId,
sourceName: item.sourceName
}
});
}}
>
<MyIcon name={item.icon as any} mr={1} flexShrink={0} w={'12px'} />
<Box className="textEllipsis3" wordBreak={'break-all'} flex={'1 0 0'}>
{item.sourceName}
</Box>
</Flex>
</MyTooltip>
);
})}
{!quoteFolded && (
<MyIcon
position={'absolute'}
bottom={0}
right={0}
_hover={{ color: 'primary.500', cursor: 'pointer' }}
name="core/chat/chevronUp"
w={'14px'}
onClick={() => setQuoteFolded(!quoteFolded)}
/>
)}
</Flex> </Flex>
</> </>
)} )}

View File

@@ -11,21 +11,25 @@ import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context'; import { AppContext } from '../context';
import { useChatTest } from '../useChatTest'; import { useChatTest } from '../useChatTest';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => { const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { appT } = useI18n(); const { appT } = useI18n();
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);
// form2AppWorkflow dependent allDatasets
const { allDatasets } = useDatasetStore();
const [workflowData, setWorkflowData] = useSafeState({ const [workflowData, setWorkflowData] = useSafeState({
nodes: appDetail.modules || [], nodes: appDetail.modules || [],
edges: appDetail.edges || [] edges: appDetail.edges || []
}); });
useEffect(() => { useEffect(() => {
const { nodes, edges } = form2AppWorkflow(appForm); const { nodes, edges } = form2AppWorkflow(appForm);
setWorkflowData({ nodes, edges }); setWorkflowData({ nodes, edges });
}, [appForm, setWorkflowData]); }, [appForm, setWorkflowData, allDatasets]);
const { restartChat, ChatContainer } = useChatTest({ const { restartChat, ChatContainer } = useChatTest({
...workflowData, ...workflowData,