fix: ai response test (#5544)

* fix: ai response test

* fix: skip edge check

* fix: app list

* fix: toolset conflict interactive node

* fix: username show
This commit is contained in:
Archer
2025-08-27 00:08:22 +08:00
committed by GitHub
parent d2d4c76bd5
commit 324aaae769
15 changed files with 75 additions and 34 deletions
@@ -7,7 +7,7 @@ description: 'FastGPT V4.12.2 更新说明'
### 1. 更新镜像: ### 1. 更新镜像:
- 更新 FastGPT 镜像tag: v4.12.2 - 更新 FastGPT 镜像tag: v4.12.2-fix
- 更新 FastGPT 商业版镜像tag: v4.12.2 - 更新 FastGPT 商业版镜像tag: v4.12.2
- 更新 fastgpt-plugin 镜像 tag: v0.1.11 - 更新 fastgpt-plugin 镜像 tag: v0.1.11
- mcp_server 无需更新 - mcp_server 无需更新
@@ -42,6 +42,7 @@ description: 'FastGPT V4.12.2 更新说明'
8. 工作流,添加团队应用,搜索无效。 8. 工作流,添加团队应用,搜索无效。
9. 应用版本,ref 字段错误,导致无法正常使用。 9. 应用版本,ref 字段错误,导致无法正常使用。
10. Oceanbase 批量插入时,未正确返回插入的 id。 10. Oceanbase 批量插入时,未正确返回插入的 id。
11. 交互节点与工具集存在冲突,导致交互节点后工具集无法正常使用。
## 🔨 工具更新 ## 🔨 工具更新
+1 -1
View File
@@ -104,7 +104,7 @@
"document/content/docs/upgrading/4-11/4111.mdx": "2025-08-07T22:49:09+08:00", "document/content/docs/upgrading/4-11/4111.mdx": "2025-08-07T22:49:09+08:00",
"document/content/docs/upgrading/4-12/4120.mdx": "2025-08-12T22:45:19+08:00", "document/content/docs/upgrading/4-12/4120.mdx": "2025-08-12T22:45:19+08:00",
"document/content/docs/upgrading/4-12/4121.mdx": "2025-08-15T22:53:06+08:00", "document/content/docs/upgrading/4-12/4121.mdx": "2025-08-15T22:53:06+08:00",
"document/content/docs/upgrading/4-12/4122.mdx": "2025-08-26T17:29:42+08:00", "document/content/docs/upgrading/4-12/4122.mdx": "2025-08-26T23:08:11+08:00",
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+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/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",
+5
View File
@@ -18,6 +18,11 @@ export const getErrText = (err: any, def = ''): any => {
return ERROR_RESPONSE[msg].message; return ERROR_RESPONSE[msg].message;
} }
// Axios special
if (err?.errors && Array.isArray(err.errors) && err.errors.length > 0) {
return err.errors[0].message;
}
// msg && console.log('error =>', msg); // msg && console.log('error =>', msg);
return replaceSensitiveText(msg); return replaceSensitiveText(msg);
}; };
+1 -1
View File
@@ -61,7 +61,7 @@ export const getMCPToolRuntimeNode = ({
parentId: string; parentId: string;
}): RuntimeNodeItemType => { }): RuntimeNodeItemType => {
return { return {
nodeId: getNanoid(16), nodeId: getNanoid(),
flowNodeType: FlowNodeTypeEnum.tool, flowNodeType: FlowNodeTypeEnum.tool,
avatar, avatar,
intro: tool.description, intro: tool.description,
-4
View File
@@ -67,8 +67,6 @@ export const createLLMResponse = async <T extends CompletionsBodyType>(
const { body, custonHeaders, userKey } = args; const { body, custonHeaders, userKey } = args;
const { messages, useVision, requestOrigin, tools, toolCallMode } = body; const { messages, useVision, requestOrigin, tools, toolCallMode } = body;
const modelData = getLLMModel(body.model);
// Messages process // Messages process
const requestMessages = await loadRequestMessages({ const requestMessages = await loadRequestMessages({
messages, messages,
@@ -475,13 +473,11 @@ type LLMRequestBodyType<T> = Omit<T, 'model' | 'stop' | 'response_format' | 'mes
// Custom field // Custom field
retainDatasetCite?: boolean; retainDatasetCite?: boolean;
reasoning?: boolean; // Whether to response reasoning content
toolCallMode?: 'toolChoice' | 'prompt'; toolCallMode?: 'toolChoice' | 'prompt';
useVision?: boolean; useVision?: boolean;
requestOrigin?: string; requestOrigin?: string;
}; };
const llmCompletionsBodyFormat = async <T extends CompletionsBodyType>({ const llmCompletionsBodyFormat = async <T extends CompletionsBodyType>({
reasoning,
retainDatasetCite, retainDatasetCite,
useVision, useVision,
requestOrigin, requestOrigin,
@@ -287,7 +287,6 @@ export const runToolCall = async (
body: { body: {
model: toolModel.model, model: toolModel.model,
stream, stream,
reasoning: aiChatReasoning,
messages: filterMessages, messages: filterMessages,
tool_choice: 'auto', tool_choice: 'auto',
toolCallMode: toolModel.toolChoice ? 'toolChoice' : 'prompt', toolCallMode: toolModel.toolChoice ? 'toolChoice' : 'prompt',
@@ -308,6 +307,7 @@ export const runToolCall = async (
isAborted: () => res?.closed, isAborted: () => res?.closed,
userKey: externalProvider.openaiAccount, userKey: externalProvider.openaiAccount,
onReasoning({ text }) { onReasoning({ text }) {
if (!aiChatReasoning) return;
workflowStreamResponse?.({ workflowStreamResponse?.({
write, write,
event: SseResponseEventEnum.answer, event: SseResponseEventEnum.answer,
@@ -184,7 +184,6 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
body: { body: {
model: modelConstantsData.model, model: modelConstantsData.model,
stream, stream,
reasoning: aiChatReasoning,
messages: filterMessages, messages: filterMessages,
temperature, temperature,
max_tokens, max_tokens,
@@ -201,6 +200,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
userKey: externalProvider.openaiAccount, userKey: externalProvider.openaiAccount,
isAborted: () => res?.closed, isAborted: () => res?.closed,
onReasoning({ text }) { onReasoning({ text }) {
if (!aiChatReasoning) return;
workflowStreamResponse?.({ workflowStreamResponse?.({
write, write,
event: SseResponseEventEnum.answer, event: SseResponseEventEnum.answer,
@@ -210,6 +210,7 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
}); });
}, },
onStreaming({ text }) { onStreaming({ text }) {
if (!isResponseAnswerText) return;
workflowStreamResponse?.({ workflowStreamResponse?.({
write, write,
event: SseResponseEventEnum.answer, event: SseResponseEventEnum.answer,
@@ -152,11 +152,13 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
方案: 方案:
- 采用回调的方式,避免深度递归。 - 采用回调的方式,避免深度递归。
- 使用 activeRunQueue 记录待运行检查的节点(可能可以运行),并控制并发数量。 - 使用 activeRunQueue 记录待运行检查的节点(可能可以运行),并控制并发数量。
- 每次添加新节点,以及节点运行结束后,均会执行一次 processNextNode 方法。 processNextNode 方法,如果没触发跳出条件,则必定会取一个 activeRunQueue 继续检查处理。 - 每次添加新节点,以及节点运行结束后,均会执行一次 processActiveNode 方法。 processActiveNode 方法,如果没触发跳出条件,则必定会取一个 activeRunQueue 继续检查处理。
- checkNodeCanRun 会检查该节点状态 - checkNodeCanRun 会检查该节点状态
- 没满足运行条件:跳出函数 - 没满足运行条件:跳出函数
- 运行:执行节点逻辑,并返回结果,将 target node 加入到 activeRunQueue 中,等待队列处理。 - 运行:执行节点逻辑,并返回结果,将 target node 加入到 activeRunQueue 中,等待队列处理。
- 跳过:执行跳过逻辑,并将其后续的 target node 也进行一次检查。 - 跳过:执行跳过逻辑,并将其后续的 target node 也进行一次检查。
特殊情况:
- 触发交互节点后,需要跳过所有 skip 节点,避免后续执行了 skipNode。
*/ */
class WorkflowQueue { class WorkflowQueue {
// Workflow variables // Workflow variables
@@ -176,6 +178,7 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
// Queue variables // Queue variables
private activeRunQueue = new Set<string>(); private activeRunQueue = new Set<string>();
private skipNodeQueue: { node: RuntimeNodeItemType; skippedNodeIdList: Set<string> }[] = [];
private runningNodeCount = 0; private runningNodeCount = 0;
private maxConcurrency: number; private maxConcurrency: number;
private resolve: (e: WorkflowQueue) => void; private resolve: (e: WorkflowQueue) => void;
@@ -198,13 +201,17 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
} }
this.activeRunQueue.add(nodeId); this.activeRunQueue.add(nodeId);
this.processNextNode(); this.processActiveNode();
} }
// Process next active node // Process next active node
private processNextNode() { private processActiveNode() {
// Finish // Finish
if (this.activeRunQueue.size === 0 && this.runningNodeCount === 0) { if (this.activeRunQueue.size === 0 && this.runningNodeCount === 0) {
this.resolve(this); if (this.skipNodeQueue.length > 0 && !this.nodeInteractiveResponse) {
this.processSkipNodes();
} else {
this.resolve(this);
}
return; return;
} }
@@ -224,12 +231,26 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
this.checkNodeCanRun(node).finally(() => { this.checkNodeCanRun(node).finally(() => {
this.runningNodeCount--; this.runningNodeCount--;
this.processNextNode(); this.processActiveNode();
}); });
} }
// 兜底,除非极端情况,否则不可能触发 // 兜底,除非极端情况,否则不可能触发
else { else {
this.processNextNode(); this.processActiveNode();
}
}
private addSkipNode(node: RuntimeNodeItemType, skippedNodeIdList: Set<string>) {
this.skipNodeQueue.push({ node, skippedNodeIdList });
}
private processSkipNodes() {
const skipItem = this.skipNodeQueue.shift();
if (skipItem) {
this.checkNodeCanRun(skipItem.node, skipItem.skippedNodeIdList).finally(() => {
this.processActiveNode();
});
} else {
this.processActiveNode();
} }
} }
@@ -695,9 +716,9 @@ export async function dispatchWorkFlow(data: Props): Promise<DispatchFlowRespons
nodeRunResult.result nodeRunResult.result
); );
await Promise.all( nextStepSkipNodes.forEach((node) => {
nextStepSkipNodes.map((node) => this.checkNodeCanRun(node, skippedNodeIdList)) this.addSkipNode(node, skippedNodeIdList);
); });
// Run next nodes // Run next nodes
nextStepActiveNodes.forEach((node) => { nextStepActiveNodes.forEach((node) => {
@@ -5,7 +5,6 @@ import axios from 'axios';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { SandboxCodeTypeEnum } from '@fastgpt/global/core/workflow/template/system/sandbox/constants'; import { SandboxCodeTypeEnum } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { getNodeErrResponse } from '../utils';
type RunCodeType = ModuleDispatchProps<{ type RunCodeType = ModuleDispatchProps<{
[NodeInputKeyEnum.codeType]: string; [NodeInputKeyEnum.codeType]: string;
@@ -210,17 +210,22 @@ export const rewriteRuntimeWorkFlow = async ({
if (!app) continue; if (!app) continue;
const toolList = await getMCPChildren(app); const toolList = await getMCPChildren(app);
for (const tool of toolList) { const parentId = mcpToolsetVal.toolId ?? toolSetNode.pluginId;
toolList.forEach((tool, index) => {
const newToolNode = getMCPToolRuntimeNode({ const newToolNode = getMCPToolRuntimeNode({
avatar: toolSetNode.avatar, avatar: toolSetNode.avatar,
tool, tool,
// New ?? Old // New ?? Old
parentId: mcpToolsetVal.toolId ?? toolSetNode.pluginId parentId
}); });
newToolNode.nodeId = `${parentId}${index}`; // ID 不能随机,否则下次生成时候就和之前的记录对不上
nodes.push({ ...newToolNode, name: `${toolSetNode.name}/${tool.name}` }); nodes.push({
...newToolNode,
name: `${toolSetNode.name}/${tool.name}`
});
pushEdges(newToolNode.nodeId); pushEdges(newToolNode.nodeId);
} });
} }
} }
+2 -2
View File
@@ -48,7 +48,7 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({
(item) => item.parentId === systemToolId && item.isActive !== false (item) => item.parentId === systemToolId && item.isActive !== false
); );
const nodes = await Promise.all( const nodes = await Promise.all(
children.map(async (child) => { children.map(async (child, index) => {
const toolListItem = toolSetNode.toolConfig?.systemToolSet?.toolList.find( const toolListItem = toolSetNode.toolConfig?.systemToolSet?.toolList.find(
(item) => item.toolId === child.id (item) => item.toolId === child.id
); );
@@ -70,7 +70,7 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({
name: toolListItem?.name || parseI18nString(tool.name, lang), name: toolListItem?.name || parseI18nString(tool.name, lang),
intro: toolListItem?.description || parseI18nString(tool.intro, lang), intro: toolListItem?.description || parseI18nString(tool.intro, lang),
flowNodeType: FlowNodeTypeEnum.tool, flowNodeType: FlowNodeTypeEnum.tool,
nodeId: getNanoid(), nodeId: `${toolSetNode.nodeId}${index}`,
toolConfig: { toolConfig: {
systemTool: { systemTool: {
toolId: child.id toolId: child.id
@@ -362,7 +362,6 @@ const BottomSection = () => {
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const isLoggedIn = !!userInfo; const isLoggedIn = !!userInfo;
const avatar = userInfo?.avatar; const avatar = userInfo?.avatar;
const username = userInfo?.username;
const isAdmin = !!userInfo?.team.permission.hasManagePer; const isAdmin = !!userInfo?.team.permission.hasManagePer;
const isShare = pathname === '/chat/share'; const isShare = pathname === '/chat/share';
@@ -448,7 +447,7 @@ const BottomSection = () => {
fontWeight={500} fontWeight={500}
minW={0} minW={0}
> >
{username} {userInfo?.team?.memberName}
</AnimatedText> </AnimatedText>
</Flex> </Flex>
</UserAvatarPopover> </UserAvatarPopover>
+4 -1
View File
@@ -106,7 +106,9 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
// Filter apps by permission, if not owner, only get apps that I have permission to access // Filter apps by permission, if not owner, only get apps that I have permission to access
const idList = { _id: { $in: myPerList.map((item) => item.resourceId) } }; const idList = { _id: { $in: myPerList.map((item) => item.resourceId) } };
const appPerQuery = teamPer.isOwner const appPerQuery = teamPer.isOwner
? {} ? {
parentId: parentId ? parseParentIdInMongo(parentId) : null
}
: parentId : parentId
? { ? {
$or: [idList, parseParentIdInMongo(parentId)] $or: [idList, parseParentIdInMongo(parentId)]
@@ -138,6 +140,7 @@ async function handler(req: ApiRequestProps<ListAppBody>): Promise<AppListItemTy
...searchMatch, ...searchMatch,
type: _type type: _type
}; };
// @ts-ignore // @ts-ignore
delete data.parentId; delete data.parentId;
return data; return data;
@@ -5,9 +5,11 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import type { McpToolConfigType } from '@fastgpt/global/core/app/type'; import type { McpToolConfigType } from '@fastgpt/global/core/app/type';
import { UserError } from '@fastgpt/global/common/error/utils'; import { UserError } from '@fastgpt/global/common/error/utils';
import { getMCPChildren } from '@fastgpt/service/core/app/mcp'; import { getMCPChildren } from '@fastgpt/service/core/app/mcp';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
export type McpGetChildrenmQuery = { export type McpGetChildrenmQuery = {
id: string; id: string;
searchKey?: string;
}; };
export type McpGetChildrenmBody = {}; export type McpGetChildrenmBody = {};
export type McpGetChildrenmResponse = (McpToolConfigType & { export type McpGetChildrenmResponse = (McpToolConfigType & {
@@ -19,7 +21,7 @@ async function handler(
req: ApiRequestProps<McpGetChildrenmBody, McpGetChildrenmQuery>, req: ApiRequestProps<McpGetChildrenmBody, McpGetChildrenmQuery>,
_res: ApiResponseType<any> _res: ApiResponseType<any>
): Promise<McpGetChildrenmResponse> { ): Promise<McpGetChildrenmResponse> {
const { id } = req.query; const { id, searchKey } = req.query;
const app = await MongoApp.findOne({ _id: id }).lean(); const app = await MongoApp.findOne({ _id: id }).lean();
@@ -28,6 +30,12 @@ async function handler(
if (app.type !== AppTypeEnum.toolSet) if (app.type !== AppTypeEnum.toolSet)
return Promise.reject(new UserError('the parent is not a mcp toolset')); return Promise.reject(new UserError('the parent is not a mcp toolset'));
return getMCPChildren(app); return (await getMCPChildren(app)).filter((item) => {
if (searchKey && searchKey.trim() !== '') {
const regx = new RegExp(replaceRegChars(searchKey.trim()), 'i');
return regx.test(item.name);
}
return true;
});
} }
export default NextAPI(handler); export default NextAPI(handler);
+7 -4
View File
@@ -29,7 +29,10 @@ import type {
getToolVersionListProps, getToolVersionListProps,
getToolVersionResponse getToolVersionResponse
} from '@/pages/api/core/app/plugin/getVersionList'; } from '@/pages/api/core/app/plugin/getVersionList';
import type { McpGetChildrenmResponse } from '@/pages/api/core/app/mcpTools/getChildren'; import type {
McpGetChildrenmQuery,
McpGetChildrenmResponse
} from '@/pages/api/core/app/mcpTools/getChildren';
/* ============ team plugin ============== */ /* ============ team plugin ============== */
export const getTeamPlugTemplates = async (data?: { export const getTeamPlugTemplates = async (data?: {
@@ -40,7 +43,7 @@ export const getTeamPlugTemplates = async (data?: {
// handle get mcptools // handle get mcptools
const app = await getAppDetailById(data.parentId); const app = await getAppDetailById(data.parentId);
if (app.type === AppTypeEnum.toolSet) { if (app.type === AppTypeEnum.toolSet) {
const children = await getMcpChildren(data.parentId); const children = await getMcpChildren({ id: data.parentId, searchKey: data.searchKey });
return children.map((item) => ({ return children.map((item) => ({
...item, ...item,
flowNodeType: FlowNodeTypeEnum.tool, flowNodeType: FlowNodeTypeEnum.tool,
@@ -109,8 +112,8 @@ export const getMCPTools = (data: getMCPToolsBody) =>
export const postRunMCPTool = (data: RunMCPToolBody) => export const postRunMCPTool = (data: RunMCPToolBody) =>
POST('/support/mcp/client/runTool', data, { timeout: 300000 }); POST('/support/mcp/client/runTool', data, { timeout: 300000 });
export const getMcpChildren = (id: string) => export const getMcpChildren = (data: McpGetChildrenmQuery) =>
GET<McpGetChildrenmResponse>('/core/app/mcpTools/getChildren', { id }); GET<McpGetChildrenmResponse>('/core/app/mcpTools/getChildren', data);
/* ============ http plugin ============== */ /* ============ http plugin ============== */
export const postCreateHttpPlugin = (data: createHttpPluginBody) => export const postCreateHttpPlugin = (data: createHttpPluginBody) =>