Commit 2fea73bb by Archer Committed by GitHub

perf: index (#6131)

* perf: index

* stop design doc

* perf: stop workflow;perf: mongo connection

* fix: ts

* mq export
parent 4f95f686
......@@ -11,7 +11,7 @@ description: 'FastGPT V4.14.5 更新说明'
## ⚙️ 优化
1. 优化获取 redis 所有 key 的逻辑,避免大量获取时导致阻塞。
2. Redis 和 MQ 的重连逻辑优化。
2. MongoDB, Redis 和 MQ 的重连逻辑优化。
## 🐛 修复
......
......@@ -120,7 +120,7 @@
"document/content/docs/upgrading/4-14/4142.mdx": "2025-11-18T19:27:14+08:00",
"document/content/docs/upgrading/4-14/4143.mdx": "2025-11-26T20:52:05+08:00",
"document/content/docs/upgrading/4-14/4144.mdx": "2025-12-16T14:56:04+08:00",
"document/content/docs/upgrading/4-14/4145.mdx": "2025-12-19T00:08:30+08:00",
"document/content/docs/upgrading/4-14/4145.mdx": "2025-12-20T13:11:02+08:00",
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",
......
......@@ -40,6 +40,7 @@ export type ExternalProviderType = {
/* workflow props */
export type ChatDispatchProps = {
res?: NextApiResponse;
checkIsStopping: () => boolean;
lang?: localeType;
requestOrigin?: string;
mode: 'test' | 'chat' | 'debug';
......@@ -63,7 +64,7 @@ export type ChatDispatchProps = {
};
uid: string; // Who run this workflow
chatId?: string;
chatId: string;
responseChatItemId?: string;
histories: ChatItemType[];
variables: Record<string, any>; // global variable
......@@ -76,7 +77,7 @@ export type ChatDispatchProps = {
maxRunTimes: number;
isToolCall?: boolean;
workflowStreamResponse?: WorkflowResponseType;
version?: 'v1' | 'v2';
apiVersion?: 'v1' | 'v2';
workflowDispatchDeep: number;
......
import type { OutLinkChatAuthType } from '../../../support/permission/chat/type';
import { OutLinkChatAuthSchema } from '../../../support/permission/chat/type';
import { ObjectIdSchema } from '../../../common/type/mongo';
import z from 'zod';
/* ============ v2/chat/stop ============ */
export const StopV2ChatSchema = z
.object({
appId: ObjectIdSchema.describe('应用ID'),
chatId: z.string().min(1).describe('对话ID'),
outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据')
})
.meta({
example: {
appId: '1234567890',
chatId: '1234567890',
outLinkAuthData: {
shareId: '1234567890',
outLinkUid: '1234567890'
}
}
});
export type StopV2ChatParams = z.infer<typeof StopV2ChatSchema>;
export const StopV2ChatResponseSchema = z
.object({
success: z.boolean().describe('是否成功停止')
})
.meta({
example: {
success: true
}
});
export type StopV2ChatResponse = z.infer<typeof StopV2ChatResponseSchema>;
/* ============ chat file ============ */
export const PresignChatFileGetUrlSchema = z
.object({
key: z.string().min(1).describe('文件key'),
......
......@@ -5,7 +5,12 @@ import { ChatFeedbackPath } from './feedback/index';
import { ChatHistoryPath } from './history/index';
import { z } from 'zod';
import { CreatePostPresignedUrlResultSchema } from '../../../../service/common/s3/type';
import { PresignChatFileGetUrlSchema, PresignChatFilePostUrlSchema } from './api';
import {
PresignChatFileGetUrlSchema,
PresignChatFilePostUrlSchema,
StopV2ChatSchema,
StopV2ChatResponseSchema
} from './api';
import { TagsMap } from '../../tag';
export const ChatPath: OpenAPIPath = {
......@@ -14,6 +19,31 @@ export const ChatPath: OpenAPIPath = {
...ChatFeedbackPath,
...ChatHistoryPath,
'/v2/chat/stop': {
post: {
summary: '停止 Agent 运行',
description: `优雅停止正在运行的 Agent, 会尝试等待当前节点结束后返回,最长 5s,超过 5s 仍未结束,则会返回成功。
LLM 节点,流输出时会同时被终止,但 HTTP 请求节点这种可能长时间运行的,不会被终止。`,
tags: [TagsMap.chatPage],
requestBody: {
content: {
'application/json': {
schema: StopV2ChatSchema
}
}
},
responses: {
200: {
description: '成功停止工作流',
content: {
'application/json': {
schema: StopV2ChatResponseSchema
}
}
}
}
}
},
'/core/chat/presignChatFilePostUrl': {
post: {
summary: '获取文件上传 URL',
......
......@@ -60,7 +60,7 @@ export function getQueue<DataType, ReturnType = void>(
// default error handler, to avoid unhandled exceptions
newQueue.on('error', (error) => {
addLog.error(`MQ Queue [${name}]: ${error.message}`, error);
addLog.error(`MQ Queue] error`, error);
});
queues.set(name, newQueue);
return newQueue;
......@@ -76,44 +76,59 @@ export function getWorker<DataType, ReturnType = void>(
return worker as Worker<DataType, ReturnType>;
}
const newWorker = new Worker<DataType, ReturnType>(name.toString(), processor, {
connection: newWorkerRedisConnection(),
...defaultWorkerOpts,
// BullMQ Worker important settings
lockDuration: 600000, // 10 minutes for large file operations
stalledInterval: 30000, // Check for stalled jobs every 30s
maxStalledCount: 3, // Move job to failed after 1 stall (default behavior)
...opts
});
// default error handler, to avoid unhandled exceptions
newWorker.on('error', async (error) => {
addLog.error(`MQ Worker error`, {
message: error.message,
data: { name }
const createWorker = () => {
const newWorker = new Worker<DataType, ReturnType>(name.toString(), processor, {
connection: newWorkerRedisConnection(),
...defaultWorkerOpts,
// BullMQ Worker important settings
lockDuration: 600000, // 10 minutes for large file operations
stalledInterval: 30000, // Check for stalled jobs every 30s
maxStalledCount: 3, // Move job to failed after 1 stall (default behavior)
...opts
});
await newWorker.close();
});
// Critical: Worker has been closed - remove from pool
newWorker.on('closed', async () => {
addLog.error(`MQ Worker [${name}] closed unexpectedly`, {
data: {
name,
message: 'Worker will need to be manually restarted'
// Worker is ready to process jobs (fired on initial connection and after reconnection)
newWorker.on('ready', () => {
addLog.info(`[MQ Worker] ready`, { name });
});
// default error handler, to avoid unhandled exceptions
newWorker.on('error', async (error) => {
addLog.error(`[MQ Worker] error`, {
message: error.message,
data: { name }
});
});
// Critical: Worker has been closed - remove from pool and restart
newWorker.on('closed', async () => {
addLog.warn(`[MQ Worker] closed, attempting restart...`);
// Clean up: remove all listeners to prevent memory leaks
newWorker.removeAllListeners();
// Retry create new worker with infinite retries
while (true) {
try {
// Call getWorker to create a new worker (now workers.get(name) returns undefined)
const worker = createWorker();
workers.set(name, worker);
addLog.info(`[MQ Worker] restarted successfully`);
break;
} catch (error) {
addLog.error(`[MQ Worker] failed to restart, retrying...`, error);
await delay(1000);
}
}
});
try {
newWorker.on('paused', async () => {
addLog.warn(`[MQ Worker] paused`);
await delay(1000);
workers.delete(name);
getWorker(name, processor, opts);
} catch (error) {}
});
newWorker.resume();
});
newWorker.on('paused', async () => {
addLog.warn(`MQ Worker [${name}] paused`);
await delay(1000);
newWorker.resume();
});
return newWorker;
};
const newWorker = createWorker();
workers.set(name, newWorker);
return newWorker;
}
......
......@@ -31,26 +31,13 @@ export async function connectMongo(props: {
db.set('strictQuery', 'throw');
db.connection.on('error', async (error) => {
console.log('mongo error', error);
try {
if (db.connection.readyState !== 0) {
RemoveListeners();
await db.disconnect();
await delay(1000);
await connectMongo(props);
}
} catch (error) {}
console.error('mongo error', error);
});
db.connection.on('connected', async () => {
console.log('mongo connected');
});
db.connection.on('disconnected', async () => {
console.log('mongo disconnected');
try {
if (db.connection.readyState !== 0) {
RemoveListeners();
await db.disconnect();
await delay(1000);
await connectMongo(props);
}
} catch (error) {}
console.error('mongo disconnected');
});
await db.connect(url, {
......@@ -64,9 +51,9 @@ export async function connectMongo(props: {
maxIdleTimeMS: 300000, // 空闲连接超时: 5分钟,防止空闲连接长时间占用资源
retryWrites: true, // 重试写入: 重试写入失败的操作
retryReads: true, // 重试读取: 重试读取失败的操作
serverSelectionTimeoutMS: 10000 // 服务器选择超时: 10秒,防止副本集故障时长时间阻塞
serverSelectionTimeoutMS: 10000, // 服务器选择超时: 10秒,防止副本集故障时长时间阻塞
heartbeatFrequencyMS: 5000 // 5s 进行一次健康检查
});
console.log('mongo connected');
connectedCb?.();
......
......@@ -19,9 +19,11 @@ const REDIS_BASE_OPTION = {
// Reconnect on specific errors (Redis master-slave switch, network issues)
reconnectOnError: (err: any) => {
const reconnectErrors = ['READONLY', 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET'];
const shouldReconnect = reconnectErrors.some((errType) => err.message.includes(errType));
const message = typeof err?.message === 'string' ? err.message : String(err ?? '');
const shouldReconnect = reconnectErrors.some((errType) => message.includes(errType));
if (shouldReconnect) {
addLog.warn(`Redis reconnecting due to error: ${err.message}`);
addLog.warn(`Redis reconnecting due to error: ${message}`);
}
return shouldReconnect;
},
......@@ -37,9 +39,6 @@ export const newQueueRedisConnection = () => {
// Limit retries for queue operations
maxRetriesPerRequest: 3
});
redis.on('error', (error) => {
addLog.error('[Redis Queue connection error]', error);
});
return redis;
};
......@@ -49,9 +48,6 @@ export const newWorkerRedisConnection = () => {
// BullMQ requires maxRetriesPerRequest: null for blocking operations
maxRetriesPerRequest: null
});
redis.on('error', (error) => {
addLog.error('[Redis Worker connection error]', error);
});
return redis;
};
......@@ -65,11 +61,14 @@ export const getGlobalRedisConnection = () => {
maxRetriesPerRequest: 3
});
global.redisClient.on('connect', () => {
addLog.info('[Global Redis] connected');
});
global.redisClient.on('error', (error) => {
addLog.error('[Redis Global connection error]', error);
addLog.error('[Global Redis] connection error', error);
});
global.redisClient.on('close', () => {
addLog.warn('[Redis Global connection closed]');
addLog.warn('[Global Redis] connection closed');
});
return global.redisClient;
......
......@@ -40,7 +40,7 @@ export const addS3DelJob = async (data: S3MQJobData): Promise<void> => {
await queue.add('delete-s3-files', data, { jobId, ...jobOption });
};
const prefixDel = async (bucket: S3BaseBucket, prefix: string) => {
export const prefixDel = async (bucket: S3BaseBucket, prefix: string) => {
addLog.debug(`[S3 delete] delete prefix: ${prefix}`);
let tasks: Promise<any>[] = [];
return new Promise<void>(async (resolve, reject) => {
......@@ -103,7 +103,7 @@ export const startS3DelWorker = async () => {
}
},
{
concurrency: 3
concurrency: 6
}
);
};
......@@ -196,6 +196,7 @@ try {
// timer, clear history
ChatSchema.index({ updateTime: -1, teamId: 1 });
ChatSchema.index({ teamId: 1, updateTime: -1 });
} catch (error) {
console.log(error);
}
......
......@@ -64,6 +64,7 @@ export type ChatResponse = DispatchNodeResultType<
export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResponse> => {
let {
res,
checkIsStopping,
requestOrigin,
stream = false,
retainDatasetCite = true,
......@@ -201,7 +202,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
requestOrigin
},
userKey: externalProvider.openaiAccount,
isAborted: () => res?.closed,
isAborted: checkIsStopping,
onReasoning({ text }) {
if (!aiChatReasoning) return;
workflowStreamResponse?.({
......
......@@ -18,6 +18,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<RunTo
const { messages, toolNodes, toolModel, childrenInteractiveParams, ...workflowProps } = props;
const {
res,
checkIsStopping,
requestOrigin,
runtimeNodes,
runtimeEdges,
......@@ -129,7 +130,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<RunTo
retainDatasetCite,
useVision: aiChatVision
},
isAborted: () => res?.closed,
isAborted: checkIsStopping,
userKey: externalProvider.openaiAccount,
onReasoning({ text }) {
if (!aiChatReasoning) return;
......
......@@ -59,10 +59,11 @@ import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { i18nT } from '../../../../web/i18n/utils';
import { clone } from 'lodash';
import { validateFileUrlDomain } from '../../../common/security/fileUrlValidator';
import { delAgentRuntimeStopSign, shouldWorkflowStop } from './workflowStatus';
type Props = Omit<
ChatDispatchProps,
'workflowDispatchDeep' | 'timezone' | 'externalProvider' | 'cloneVariables'
'checkIsStopping' | 'workflowDispatchDeep' | 'timezone' | 'externalProvider' | 'cloneVariables'
> & {
runtimeNodes: RuntimeNodeItemType[];
runtimeEdges: RuntimeEdgeItemType[];
......@@ -87,7 +88,17 @@ export async function dispatchWorkFlow({
concatUsage,
...data
}: Props & WorkflowUsageProps): Promise<DispatchFlowResponse> {
const { res, stream, runningUserInfo, runningAppInfo, lastInteractive, histories, query } = data;
const {
res,
stream,
runningUserInfo,
runningAppInfo,
lastInteractive,
histories,
query,
chatId,
apiVersion
} = data;
// Check url valid
const invalidInput = query.some((item) => {
......@@ -101,6 +112,8 @@ export async function dispatchWorkFlow({
addLog.info('[Workflow run] Invalid file url');
return Promise.reject(new UserError('Invalid file url'));
}
/* Init function */
// Check point
await checkTeamAIPoints(runningUserInfo.teamId);
......@@ -120,7 +133,22 @@ export async function dispatchWorkFlow({
});
}
return usageId;
})()
})(),
// Add preview url to chat items
await addPreviewUrlToChatItems(histories, 'chatFlow'),
// Add preview url to query
...query.map(async (item) => {
if (item.type !== ChatItemValueTypeEnum.file || !item.file?.key) return;
item.file.url = await getS3ChatSource().createGetChatFileURL({
key: item.file.key,
external: true
});
}),
// Remove stopping sign
delAgentRuntimeStopSign({
appId: runningAppInfo.id,
chatId
})
]);
let streamCheckTimer: NodeJS.Timeout | null = null;
......@@ -152,16 +180,6 @@ export async function dispatchWorkFlow({
}
}
// Add preview url to chat items
await addPreviewUrlToChatItems(histories, 'chatFlow');
for (const item of query) {
if (item.type !== ChatItemValueTypeEnum.file || !item.file?.key) continue;
item.file.url = await getS3ChatSource().createGetChatFileURL({
key: item.file.key,
external: true
});
}
// Get default variables
const cloneVariables = clone(data.variables);
const defaultVariables = {
......@@ -173,12 +191,34 @@ export async function dispatchWorkFlow({
timezone
}))
};
// MCP
let mcpClientMemory = {} as Record<string, MCPClient>;
// Stop sign(没有 apiVersion,说明不会有暂停)
let stopping = false;
const checkIsStopping = (): boolean => {
if (apiVersion === 'v2') {
return stopping;
}
if (apiVersion === 'v1') {
if (!res) return false;
return res.closed || !!res.errored;
}
return false;
};
const checkStoppingTimer =
apiVersion === 'v2'
? setInterval(async () => {
stopping = await shouldWorkflowStop({
appId: runningAppInfo.id,
chatId
});
}, 100)
: undefined;
// Init some props
return runWorkflow({
...data,
checkIsStopping,
query,
histories,
timezone,
......@@ -189,15 +229,24 @@ export async function dispatchWorkFlow({
concatUsage,
mcpClientMemory,
cloneVariables
}).finally(() => {
}).finally(async () => {
if (streamCheckTimer) {
clearInterval(streamCheckTimer);
}
if (checkStoppingTimer) {
clearInterval(checkStoppingTimer);
}
// Close mcpClient connections
Object.values(mcpClientMemory).forEach((client) => {
client.closeConnection();
});
// 工作流完成后删除 Redis 记录
await delAgentRuntimeStopSign({
appId: runningAppInfo.id,
chatId
});
});
}
......@@ -210,14 +259,14 @@ type RunWorkflowProps = ChatDispatchProps & {
};
export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowResponse> => {
let {
res,
apiVersion,
checkIsStopping,
runtimeNodes = [],
runtimeEdges = [],
histories = [],
variables = {},
externalProvider,
retainDatasetCite = true,
version = 'v1',
responseDetail = true,
responseAllData = true,
usageId,
......@@ -328,10 +377,6 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
});
}
get connectionIsActive(): boolean {
return !res?.closed && !res?.errored;
}
// Add active node to queue (if already in the queue, it will not be added again)
addActiveNode(nodeId: string) {
if (this.activeRunQueue.has(nodeId)) {
......@@ -585,7 +630,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
})();
// Response node response
if (version === 'v2' && !data.isToolCall && isRootRuntime && formatResponseData) {
if (apiVersion === 'v2' && !data.isToolCall && isRootRuntime && formatResponseData) {
data.workflowStreamResponse?.({
event: SseResponseEventEnum.flowNodeResponse,
data: responseAllData
......@@ -813,8 +858,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
});
return;
}
if (!this.connectionIsActive) {
addLog.warn('Request is closed/errored', {
if (checkIsStopping()) {
addLog.warn('Workflow stopped', {
appId: data.runningAppInfo.id,
nodeId: node.nodeId,
nodeName: node.name
......
import { addLog } from '../../../common/system/log';
import { getGlobalRedisConnection } from '../../../common/redis/index';
import { delay } from '@fastgpt/global/common/system/utils';
const WORKFLOW_STATUS_PREFIX = 'agent_runtime_stopping';
const TTL = 60; // 1分钟
export const StopStatus = 'STOPPING';
export type WorkflowStatusParams = {
appId: string;
chatId: string;
};
// 获取工作流状态键
export const getRuntimeStatusKey = (params: WorkflowStatusParams): string => {
return `${WORKFLOW_STATUS_PREFIX}:${params.appId}:${params.chatId}`;
};
// 暂停任务
export const setAgentRuntimeStop = async (params: WorkflowStatusParams): Promise<void> => {
const redis = getGlobalRedisConnection();
const key = getRuntimeStatusKey(params);
await redis.set(key, 1, 'EX', TTL);
};
// 删除任务状态
export const delAgentRuntimeStopSign = async (params: WorkflowStatusParams): Promise<void> => {
const redis = getGlobalRedisConnection();
const key = getRuntimeStatusKey(params);
await redis.del(key).catch((err) => {
addLog.error(`[Agent Runtime Stop] Delete stop sign error`, err);
});
};
// 检查工作流是否应该停止
export const shouldWorkflowStop = (params: WorkflowStatusParams): Promise<boolean> => {
const redis = getGlobalRedisConnection();
const key = getRuntimeStatusKey(params);
return redis
.get(key)
.then((res) => !!res)
.catch(() => false);
};
/**
* 等待工作流完成(记录被删除)
* @param params 工作流参数
* @param timeout 超时时间(毫秒),默认5秒
* @param pollInterval 轮询间隔(毫秒),默认50毫秒
* @returns true=正常完成, false=超时
*/
export const waitForWorkflowComplete = async ({
appId,
chatId,
timeout = 5000,
pollInterval = 50
}: {
appId: string;
chatId: string;
timeout?: number;
pollInterval?: number;
}) => {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const sign = await shouldWorkflowStop({ appId, chatId });
// 如果没有暂停中的标志,则认为已经完成任务了。
if (!sign) {
return;
}
// 等待下一次轮询
await delay(pollInterval);
}
return;
};
......@@ -17,7 +17,7 @@
"clear_input_value": "清空输入",
"click_contextual_preview": "点击查看上下文预览",
"click_to_add_url": "输入文件链接",
"completion_finish_close": "连接断开",
"completion_finish_close": "请求关闭",
"completion_finish_content_filter": "触发安全风控",
"completion_finish_function_call": "函数调用",
"completion_finish_length": "超出回复限制",
......
......@@ -19,6 +19,8 @@ import { useFileUpload } from '../hooks/useFileUpload';
import ComplianceTip from '@/components/common/ComplianceTip/index';
import { useToast } from '@fastgpt/web/hooks/useToast';
import VoiceInput, { type VoiceInputComponentRef } from './VoiceInput';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { postStopV2Chat } from '@/web/core/chat/api';
const InputGuideBox = dynamic(() => import('./InputGuideBox'));
......@@ -124,6 +126,19 @@ const ChatInput = ({
},
[TextareaDom, canSendMessage, fileList, onSendMessage, replaceFiles]
);
const { runAsync: handleStop, loading: isStopping } = useRequest2(async () => {
try {
if (isChatting) {
await postStopV2Chat({
appId,
chatId,
outLinkAuthData
}).catch();
}
} finally {
onStop();
}
});
const RenderTextarea = useMemo(
() => (
......@@ -329,7 +344,9 @@ const ChatInput = ({
{/* Send Button Container */}
<Flex alignItems={'center'} w={[8, 9]} h={[8, 9]} borderRadius={'lg'}>
<Flex
<MyBox
isLoading={isStopping}
display={'flex'}
alignItems={'center'}
justifyContent={'center'}
w={[7, 9]}
......@@ -343,7 +360,7 @@ const ChatInput = ({
onClick={(e) => {
e.stopPropagation();
if (isChatting) {
return onStop();
return handleStop();
}
return handleSend();
}}
......@@ -355,7 +372,7 @@ const ChatInput = ({
<MyIcon name={'core/chat/sendFill'} {...iconSize} color={'white'} />
</MyTooltip>
)}
</Flex>
</MyBox>
</Flex>
</Flex>
</Flex>
......@@ -370,12 +387,13 @@ const ChatInput = ({
whisperConfig?.open,
inputValue,
t,
isStopping,
isChatting,
canSendMessage,
onOpenSelectFile,
onSelectFile,
handleSend,
onStop
handleStop
]);
const activeStyles: FlexProps = {
......
......@@ -432,10 +432,10 @@ const ChatBox = ({
}, [questionGuide, appId, chatId, outLinkAuthData, scrollToBottom]);
/* Abort chat completions, questionGuide */
const abortRequest = useMemoizedFn((signal: string = 'stop') => {
chatController.current?.abort(signal);
questionGuideController.current?.abort(signal);
pluginController.current?.abort(signal);
const abortRequest = useMemoizedFn((reason: string = 'stop') => {
chatController.current?.abort(new Error(reason));
questionGuideController.current?.abort(new Error(reason));
pluginController.current?.abort(new Error(reason));
});
/**
......@@ -463,8 +463,7 @@ const ChatBox = ({
}
// Abort the previous request
abortRequest();
questionGuideController.current?.abort('stop');
questionGuideController.current?.abort(new Error('stop'));
text = text.trim();
......@@ -605,16 +604,18 @@ const ChatBox = ({
newChatHistories = state.map((item, index) => {
if (index !== state.length - 1) return item;
// Check node response error
const responseData = mergeChatResponseData(item.responseData || []);
const err =
responseData[responseData.length - 1]?.error ||
responseData[responseData.length - 1]?.errorText;
if (err) {
toast({
title: t(getErrText(err)),
status: 'warning'
});
// Check node response error
if (!abortSignal?.signal?.aborted) {
const err =
responseData[responseData.length - 1]?.error ||
responseData[responseData.length - 1]?.errorText;
if (err) {
toast({
title: t(getErrText(err)),
status: 'warning'
});
}
}
return {
......@@ -1184,7 +1185,7 @@ const ChatBox = ({
) : (
<ChatInput
onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')}
onStop={() => abortRequest('stop')}
TextareaDom={TextareaDom}
resetInputVal={resetInputVal}
chatForm={chatForm}
......@@ -1206,7 +1207,7 @@ const ChatBox = ({
<ChatInput
onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')}
onStop={() => abortRequest('stop')}
TextareaDom={TextareaDom}
resetInputVal={resetInputVal}
chatForm={chatForm}
......
......@@ -181,6 +181,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
/* start process */
const { flowResponses, assistantResponses, system_memories, newVariables, durationSeconds } =
await dispatchWorkFlow({
apiVersion: 'v2',
res,
lang: getLocale(req),
requestOrigin: req.headers.origin,
......@@ -209,7 +210,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
stream: true,
maxRunTimes: WORKFLOW_MAX_RUN_TIMES,
workflowStreamResponse: workflowResponseWrite,
version: 'v2',
responseDetail: true
});
......
......@@ -11,7 +11,7 @@ import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants
import { getLastInteractiveValue } from '@fastgpt/global/core/workflow/runtime/utils';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { createChatUsageRecord } from '@fastgpt/service/support/wallet/usage/controller';
import { clone } from 'lodash';
import { getNanoid } from '@fastgpt/global/common/string/tools';
async function handler(
req: NextApiRequest,
......@@ -73,6 +73,7 @@ async function handler(
tmbId: app.tmbId
},
runningUserInfo: await getRunningUserInfoByTmbId(tmbId),
chatId: getNanoid(),
runtimeNodes: nodes,
runtimeEdges: edges,
defaultSkipNodeQueue: skipNodeQueue,
......
......@@ -278,6 +278,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
showNodeStatus
});
const saveChatId = chatId || getNanoid(24);
/* start flow controller */
const {
flowResponses,
......@@ -289,6 +291,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} = await (async () => {
if (app.version === 'v2') {
return dispatchWorkFlow({
apiVersion: 'v1',
res,
lang: getLocale(req),
requestOrigin: req.headers.origin,
......@@ -304,7 +307,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
runningUserInfo: await getRunningUserInfoByTmbId(tmbId),
uid: String(outLinkUserId || tmbId),
chatId,
chatId: saveChatId,
responseChatItemId,
runtimeNodes,
runtimeEdges: storeEdges2RuntimeEdges(edges, interactive),
......@@ -351,7 +354,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
memories: system_memories
};
const saveChatId = chatId || getNanoid(24);
const params: SaveChatProps = {
chatId: saveChatId,
appId: app._id,
......
......@@ -278,6 +278,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
showNodeStatus
});
const saveChatId = chatId || getNanoid(24);
/* start flow controller */
const {
flowResponses,
......@@ -289,6 +291,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} = await (async () => {
if (app.version === 'v2') {
return dispatchWorkFlow({
apiVersion: 'v2',
res,
lang: getLocale(req),
requestOrigin: req.headers.origin,
......@@ -304,7 +307,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
runningUserInfo: await getRunningUserInfoByTmbId(tmbId),
uid: String(outLinkUserId || tmbId),
chatId,
chatId: saveChatId,
responseChatItemId,
runtimeNodes,
runtimeEdges: storeEdges2RuntimeEdges(edges, interactive),
......@@ -317,7 +320,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
retainDatasetCite,
maxRunTimes: WORKFLOW_MAX_RUN_TIMES,
workflowStreamResponse: workflowResponseWrite,
version: 'v2',
responseAllData,
responseDetail
});
......@@ -354,7 +356,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
memories: system_memories
};
const saveChatId = chatId || getNanoid(24);
const params: SaveChatProps = {
chatId: saveChatId,
appId: app._id,
......
import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import {
setAgentRuntimeStop,
waitForWorkflowComplete
} from '@fastgpt/service/core/workflow/dispatch/workflowStatus';
import { StopV2ChatSchema, type StopV2ChatResponse } from '@fastgpt/global/openapi/core/chat/api';
async function handler(req: NextApiRequest, res: NextApiResponse): Promise<StopV2ChatResponse> {
const { appId, chatId, outLinkAuthData } = StopV2ChatSchema.parse(req.body);
await authChatCrud({
req,
authToken: true,
authApiKey: true,
appId,
chatId,
...outLinkAuthData
});
// 设置停止状态
await setAgentRuntimeStop({
appId,
chatId
});
// 等待工作流完成 (最多等待 5 秒)
await waitForWorkflowComplete({ appId, chatId, timeout: 5000 });
return {
success: true
};
}
export default NextAPI(handler);
......@@ -24,6 +24,7 @@ import type {
UpdateFavouriteAppParamsType
} from '@fastgpt/global/openapi/core/chat/favourite/api';
import type { ChatFavouriteAppType } from '@fastgpt/global/core/chat/favouriteApp/type';
import type { StopV2ChatParams } from '@fastgpt/global/openapi/core/chat/api';
/**
* 获取初始化聊天内容
......@@ -76,3 +77,6 @@ export const updateFavouriteAppTags = (data: { id: string; tags: string[] }[]) =
export const deleteFavouriteApp = (data: { id: string }) =>
DELETE<null>('/proApi/core/chat/setting/favourite/delete', data);
/* Chat controller */
export const postStopV2Chat = (data: StopV2ChatParams) => POST('/v2/chat/stop', data);
......@@ -13,7 +13,7 @@ import { getUser } from '@test/datas/users';
import { Call } from '@test/utils/request';
import { describe, expect, it, beforeEach } from 'vitest';
describe.sequential('closeCustom api test', () => {
describe('closeCustom api test', () => {
let testUser: Awaited<ReturnType<typeof getUser>>;
let appId: string;
let chatId: string;
......
......@@ -13,7 +13,7 @@ import { getUser } from '@test/datas/users';
import { Call } from '@test/utils/request';
import { describe, expect, it, beforeEach } from 'vitest';
describe.sequential('updateFeedbackReadStatus api test', () => {
describe('updateFeedbackReadStatus api test', () => {
let testUser: Awaited<ReturnType<typeof getUser>>;
let appId: string;
let chatId: string;
......
......@@ -14,7 +14,7 @@ import { getUser } from '@test/datas/users';
import { Call } from '@test/utils/request';
import { describe, expect, it, beforeEach } from 'vitest';
describe.sequential('updateUserFeedback api test', () => {
describe('updateUserFeedback api test', () => {
let testUser: Awaited<ReturnType<typeof getUser>>;
let appId: string;
let chatId: string;
......
......@@ -3,6 +3,7 @@ import type { Model, Schema } from 'mongoose';
import { Mongoose } from 'mongoose';
export const MONGO_URL = process.env.MONGODB_URI ?? '';
const maxConnecting = Math.max(30, Number(process.env.DB_MAX_LINK || 20));
declare global {
var mongodb: Mongoose | undefined;
......@@ -52,49 +53,30 @@ export async function connectMongo(db: Mongoose, url: string): Promise<Mongoose>
db.connection.removeAllListeners('disconnected');
db.set('strictQuery', 'throw');
db.connection.on('error', async (error: any) => {
addLog.error('mongo error', error);
try {
if (db.connection.readyState !== 0) {
await db.disconnect();
await delay(1000);
await connectMongo(db, url);
}
} catch (_error) {
addLog.error('Error during reconnection:', _error);
}
db.connection.on('error', async (error) => {
console.error('mongo error', error);
});
db.connection.on('connected', async () => {
console.log('mongo connected');
});
db.connection.on('disconnected', async () => {
addLog.warn('mongo disconnected');
try {
if (db.connection.readyState !== 0) {
await db.disconnect();
await delay(1000);
await connectMongo(db, url);
}
} catch (_error) {
addLog.error('Error during reconnection:', _error);
}
console.error('mongo disconnected');
});
const options = {
await db.connect(url, {
bufferCommands: true,
maxPoolSize: Math.max(30, Number(process.env.MONGO_MAX_LINK || 20)),
minPoolSize: 20,
connectTimeoutMS: 60000,
waitQueueTimeoutMS: 60000,
socketTimeoutMS: 60000,
maxIdleTimeMS: 300000,
retryWrites: true,
retryReads: true,
serverSelectionTimeoutMS: 60000,
heartbeatFrequencyMS: 20000,
maxStalenessSeconds: 120
};
await db.connect(url, options);
addLog.info('mongo connected');
maxConnecting: maxConnecting, // 最大连接数: 防止连接数过多时无法满足需求
maxPoolSize: maxConnecting, // 最大连接池大小: 防止连接池过大时无法满足需求
minPoolSize: 20, // 最小连接数: 20,防止连接数过少时无法满足需求
connectTimeoutMS: 60000, // 连接超时: 60秒,防止连接失败时长时间阻塞
waitQueueTimeoutMS: 60000, // 等待队列超时: 60秒,防止等待队列长时间阻塞
socketTimeoutMS: 60000, // Socket 超时: 60秒,防止Socket连接失败时长时间阻塞
maxIdleTimeMS: 300000, // 空闲连接超时: 5分钟,防止空闲连接长时间占用资源
retryWrites: true, // 重试写入: 重试写入失败的操作
retryReads: true, // 重试读取: 重试读取失败的操作
serverSelectionTimeoutMS: 10000, // 服务器选择超时: 10秒,防止副本集故障时长时间阻塞
heartbeatFrequencyMS: 5000 // 5s 进行一次健康检查
});
return db;
} catch (error) {
addLog.error('Mongo connect error', error);
......
import { describe, test, expect, beforeEach } from 'vitest';
import {
setAgentRuntimeStop,
delAgentRuntimeStopSign,
shouldWorkflowStop,
waitForWorkflowComplete
} from '@fastgpt/service/core/workflow/dispatch/workflowStatus';
describe('Workflow Status Redis Functions', () => {
const testAppId = 'test_app_123';
const testChatId = 'test_chat_456';
beforeEach(async () => {
// 清理测试数据
await delAgentRuntimeStopSign({ appId: testAppId, chatId: testChatId });
});
test('should set stopping sign', async () => {
await setAgentRuntimeStop({
appId: testAppId,
chatId: testChatId
});
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
expect(shouldStop).toBe(true);
});
test('should return false for non-existent status', async () => {
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
expect(shouldStop).toBe(false);
});
test('should detect stopping status', async () => {
await setAgentRuntimeStop({
appId: testAppId,
chatId: testChatId
});
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
expect(shouldStop).toBe(true);
});
test('should return false after deleting stop sign', async () => {
await setAgentRuntimeStop({
appId: testAppId,
chatId: testChatId
});
await delAgentRuntimeStopSign({
appId: testAppId,
chatId: testChatId
});
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
expect(shouldStop).toBe(false);
});
test('should wait for workflow completion', async () => {
// 设置初始停止标志
await setAgentRuntimeStop({
appId: testAppId,
chatId: testChatId
});
// 模拟异步完成(删除停止标志)
setTimeout(async () => {
await delAgentRuntimeStopSign({ appId: testAppId, chatId: testChatId });
}, 500);
// 等待完成,waitForWorkflowComplete 现在是 void 返回
await waitForWorkflowComplete({
appId: testAppId,
chatId: testChatId,
timeout: 2000
});
// 验证停止标志已被删除
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
expect(shouldStop).toBe(false);
});
test('should timeout when waiting too long', async () => {
await setAgentRuntimeStop({
appId: testAppId,
chatId: testChatId
});
// 等待超时(不删除标志)
await waitForWorkflowComplete({
appId: testAppId,
chatId: testChatId,
timeout: 100
});
// 验证停止标志仍然存在
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
expect(shouldStop).toBe(true);
});
test('should delete workflow stop sign', async () => {
await setAgentRuntimeStop({
appId: testAppId,
chatId: testChatId
});
await delAgentRuntimeStopSign({ appId: testAppId, chatId: testChatId });
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
expect(shouldStop).toBe(false);
});
test('should handle concurrent stop sign operations', async () => {
// 并发设置停止标志
await Promise.all([
setAgentRuntimeStop({ appId: testAppId, chatId: testChatId }),
setAgentRuntimeStop({ appId: testAppId, chatId: testChatId })
]);
// 停止标志应该存在
const shouldStop = await shouldWorkflowStop({ appId: testAppId, chatId: testChatId });
expect(shouldStop).toBe(true);
});
});
......@@ -49,8 +49,6 @@ beforeEach(async () => {
onTestFinished(async () => {
clean();
// Wait for any ongoing transactions and operations to complete
await delay(500);
// Ensure all sessions are closed before dropping database
try {
......@@ -62,9 +60,6 @@ beforeEach(async () => {
// Ignore errors during cleanup
console.warn('Error during test cleanup:', error);
}
// Additional delay to prevent lock contention between tests
await delay(100);
});
});
......
......@@ -20,8 +20,10 @@ export default defineConfig({
outputFile: 'test-results.json',
setupFiles: 'test/setup.ts',
globalSetup: 'test/globalSetup.ts',
// fileParallelism: false,
maxConcurrency: 5,
// File-level execution: serial (one file at a time to avoid MongoDB conflicts)
fileParallelism: false,
// Test-level execution within a file: parallel (up to 5 concurrent tests)
maxConcurrency: 10,
pool: 'threads',
include: [
'test/test.ts',
......@@ -31,6 +33,7 @@ export default defineConfig({
'projects/marketplace/test/**/*.test.ts'
],
testTimeout: 20000,
hookTimeout: 30000,
reporters: ['github-actions', 'default']
}
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment