mirror of
https://github.com/labring/FastGPT.git
synced 2026-02-28 01:02:28 +08:00
fix: surrender;perf: llm response (#6190)
* feat: workflow route to detail * llm response * fix: surrender * fix: surrender * fix: surrender * fix: test
This commit is contained in:
@@ -7,17 +7,19 @@ description: 'FastGPT V4.14.5 更新说明'
|
||||
## 🚀 新增内容
|
||||
|
||||
1. 工作流画布增加演示模式,同时优化折叠模式样式。
|
||||
2. 对话记录使用侧改成软删除,增加从日志管理里删除对话记录。
|
||||
3. 更新Agent/工具时,会更新其上层所有目录的更新时间,以便其会排在列表前面。
|
||||
4. 门户页支持配置单个应用运行可见度配。
|
||||
5. 导出单个知识库集合分块接口。
|
||||
6. 升级 Mongo5.x 至 5.0.32 解决CVE-2025-14847。
|
||||
2. 工作流增加嵌套应用快速跳转按钮。
|
||||
3. 对话记录使用侧改成软删除,增加从日志管理里删除对话记录。
|
||||
4. 更新Agent/工具时,会更新其上层所有目录的更新时间,以便其会排在列表前面。
|
||||
5. 门户页支持配置单个应用运行可见度配。
|
||||
6. 导出单个知识库集合分块接口。
|
||||
7. 升级 Mongo5.x 至 5.0.32 解决CVE-2025-14847。
|
||||
|
||||
## ⚙️ 优化
|
||||
|
||||
1. 优化获取 redis 所有 key 的逻辑,避免大量获取时导致阻塞。
|
||||
2. MongoDB, Redis 和 MQ 的重连逻辑优化。
|
||||
3. 变量输入框禁用状态可复制。
|
||||
4. LLM 请求空响应判断,排除敏感过滤错误被误认为无响应。
|
||||
|
||||
## 🐛 修复
|
||||
|
||||
|
||||
@@ -116,11 +116,11 @@
|
||||
"document/content/docs/upgrading/4-13/4131.mdx": "2025-09-30T15:47:06+08:00",
|
||||
"document/content/docs/upgrading/4-13/4132.mdx": "2025-12-15T11:50:00+08:00",
|
||||
"document/content/docs/upgrading/4-14/4140.mdx": "2025-11-06T15:43:00+08:00",
|
||||
"document/content/docs/upgrading/4-14/4141.mdx": "2025-11-19T10:15:27+08:00",
|
||||
"document/content/docs/upgrading/4-14/4141.mdx": "2025-12-31T09:54:29+08:00",
|
||||
"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": "2026-01-05T11:19:08+08:00",
|
||||
"document/content/docs/upgrading/4-14/4145.mdx": "2026-01-05T13:44:33+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",
|
||||
|
||||
@@ -44,3 +44,10 @@ export function splitCombineToolId(id: string) {
|
||||
}
|
||||
return { source, pluginId: id };
|
||||
}
|
||||
|
||||
export const getToolRawId = (id: string) => {
|
||||
const toolId = splitCombineToolId(id).pluginId;
|
||||
|
||||
// 兼容 toolset
|
||||
return toolId.split('/')[0];
|
||||
};
|
||||
|
||||
30
packages/global/openapi/core/app/common/api.ts
Normal file
30
packages/global/openapi/core/app/common/api.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ObjectIdSchema } from '../../../../common/type/mongo';
|
||||
import { z } from 'zod';
|
||||
|
||||
/* Get App Permission */
|
||||
export const GetAppPermissionQuerySchema = z.object({
|
||||
appId: ObjectIdSchema.meta({
|
||||
example: '68ad85a7463006c963799a05',
|
||||
description: '应用 ID'
|
||||
})
|
||||
});
|
||||
export type GetAppPermissionQueryType = z.infer<typeof GetAppPermissionQuerySchema>;
|
||||
|
||||
export const GetAppPermissionResponseSchema = z.object({
|
||||
hasReadPer: z.boolean().meta({
|
||||
description: '是否有读权限'
|
||||
}),
|
||||
hasWritePer: z.boolean().meta({
|
||||
description: '是否有写权限'
|
||||
}),
|
||||
hasManagePer: z.boolean().meta({
|
||||
description: '是否有管理权限'
|
||||
}),
|
||||
hasReadChatLogPer: z.boolean().meta({
|
||||
description: '是否有读取对话日志权限'
|
||||
}),
|
||||
isOwner: z.boolean().meta({
|
||||
description: '是否为所有者'
|
||||
})
|
||||
});
|
||||
export type GetAppPermissionResponseType = z.infer<typeof GetAppPermissionResponseSchema>;
|
||||
26
packages/global/openapi/core/app/common/index.ts
Normal file
26
packages/global/openapi/core/app/common/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { OpenAPIPath } from '../../../type';
|
||||
import { TagsMap } from '../../../tag';
|
||||
import { GetAppPermissionQuerySchema, GetAppPermissionResponseSchema } from './api';
|
||||
|
||||
export const AppCommonPath: OpenAPIPath = {
|
||||
'/core/app/getPermission': {
|
||||
get: {
|
||||
summary: '获取应用权限',
|
||||
description: '根据应用 ID 获取当前用户对该应用的权限信息',
|
||||
tags: [TagsMap.appCommon],
|
||||
requestParams: {
|
||||
query: GetAppPermissionQuerySchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: '成功获取应用权限',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: GetAppPermissionResponseSchema
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { OpenAPIPath } from '../../type';
|
||||
import { AppLogPath } from './log';
|
||||
import { PublishChannelPath } from './publishChannel';
|
||||
import { AppCommonPath } from './common';
|
||||
|
||||
export const AppPath: OpenAPIPath = {
|
||||
...AppLogPath,
|
||||
...PublishChannelPath
|
||||
...PublishChannelPath,
|
||||
...AppCommonPath
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export const openAPIDocument = createDocument({
|
||||
'x-tagGroups': [
|
||||
{
|
||||
name: 'Agent 应用',
|
||||
tags: [TagsMap.appLog, TagsMap.publishChannel]
|
||||
tags: [TagsMap.appCommon, TagsMap.appLog, TagsMap.publishChannel]
|
||||
},
|
||||
{
|
||||
name: '对话管理',
|
||||
|
||||
@@ -2,6 +2,8 @@ export const TagsMap = {
|
||||
/* Core */
|
||||
// Agent - log
|
||||
appLog: 'Agent 日志',
|
||||
// Agent - common
|
||||
appCommon: 'Agent 管理',
|
||||
|
||||
// Chat - home
|
||||
chatPage: '对话页',
|
||||
|
||||
@@ -211,7 +211,7 @@ export const runAgentCall = async ({
|
||||
answerText: answer,
|
||||
toolCalls = [],
|
||||
usage,
|
||||
getEmptyResponseTip,
|
||||
responseEmptyTip,
|
||||
assistantMessage: llmAssistantMessage,
|
||||
finish_reason: finishReason
|
||||
} = await createLLMResponse({
|
||||
@@ -235,8 +235,8 @@ export const runAgentCall = async ({
|
||||
|
||||
finish_reason = finishReason;
|
||||
|
||||
if (!answer && !reasoningContent && !toolCalls.length) {
|
||||
return Promise.reject(getEmptyResponseTip());
|
||||
if (responseEmptyTip) {
|
||||
return Promise.reject(responseEmptyTip);
|
||||
}
|
||||
|
||||
// 3. 更新 messages
|
||||
|
||||
@@ -51,7 +51,7 @@ type LLMResponse = {
|
||||
reasoningText: string;
|
||||
toolCalls?: ChatCompletionMessageToolCall[];
|
||||
finish_reason: CompletionFinishReason;
|
||||
getEmptyResponseTip: () => string;
|
||||
responseEmptyTip?: string;
|
||||
usage: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
@@ -92,7 +92,7 @@ export const createLLMResponse = async <T extends CompletionsBodyType>(
|
||||
});
|
||||
|
||||
// console.log(JSON.stringify(requestBody, null, 2));
|
||||
const { response, isStreamResponse, getEmptyResponseTip } = await createChatCompletion({
|
||||
const { response, isStreamResponse } = await createChatCompletion({
|
||||
body: requestBody,
|
||||
modelData,
|
||||
userKey,
|
||||
@@ -151,9 +151,33 @@ export const createLLMResponse = async <T extends CompletionsBodyType>(
|
||||
usage?.prompt_tokens || (await countGptMessagesTokens(requestBody.messages, requestBody.tools));
|
||||
const outputTokens = usage?.completion_tokens || (await countGptMessagesTokens(assistantMessage));
|
||||
|
||||
const getEmptyResponseTip = () => {
|
||||
if (userKey?.baseUrl) {
|
||||
addLog.warn(`User LLM response empty`, {
|
||||
baseUrl: userKey?.baseUrl,
|
||||
requestBody,
|
||||
finish_reason
|
||||
});
|
||||
return `您的 OpenAI key 没有响应: ${JSON.stringify(body)}`;
|
||||
} else {
|
||||
addLog.error(`LLM response empty`, {
|
||||
message: '',
|
||||
data: requestBody,
|
||||
finish_reason
|
||||
});
|
||||
}
|
||||
return i18nT('chat:LLM_model_response_empty');
|
||||
};
|
||||
const isNotResponse =
|
||||
!answerText &&
|
||||
!reasoningText &&
|
||||
!toolCalls?.length &&
|
||||
(finish_reason === 'stop' || !finish_reason);
|
||||
const responseEmptyTip = isNotResponse ? getEmptyResponseTip() : undefined;
|
||||
|
||||
return {
|
||||
isStreamResponse,
|
||||
getEmptyResponseTip,
|
||||
responseEmptyTip,
|
||||
answerText,
|
||||
reasoningText,
|
||||
toolCalls,
|
||||
@@ -535,7 +559,8 @@ const llmCompletionsBodyFormat = async <T extends CompletionsBodyType>({
|
||||
maxToken: body.max_tokens || undefined
|
||||
});
|
||||
|
||||
const requestBody = {
|
||||
const formatStop = stop?.split('|').filter((item) => !!item.trim());
|
||||
let requestBody = {
|
||||
...body,
|
||||
max_tokens: maxTokens,
|
||||
model: modelData.model,
|
||||
@@ -546,9 +571,8 @@ const llmCompletionsBodyFormat = async <T extends CompletionsBodyType>({
|
||||
temperature: body.temperature
|
||||
})
|
||||
: undefined,
|
||||
...modelData?.defaultConfig,
|
||||
response_format,
|
||||
stop: stop?.split('|').filter((item) => !!item.trim()),
|
||||
stop: formatStop?.length ? formatStop : undefined,
|
||||
...(toolCallMode === 'toolChoice' && {
|
||||
tools,
|
||||
tool_choice,
|
||||
@@ -556,6 +580,11 @@ const llmCompletionsBodyFormat = async <T extends CompletionsBodyType>({
|
||||
})
|
||||
} as T;
|
||||
|
||||
// Filter null value
|
||||
requestBody = Object.fromEntries(
|
||||
Object.entries(requestBody).filter(([_, value]) => value !== null)
|
||||
) as T;
|
||||
|
||||
// field map
|
||||
if (modelData.fieldMap) {
|
||||
Object.entries(modelData.fieldMap).forEach(([sourceKey, targetKey]) => {
|
||||
@@ -566,6 +595,11 @@ const llmCompletionsBodyFormat = async <T extends CompletionsBodyType>({
|
||||
});
|
||||
}
|
||||
|
||||
requestBody = {
|
||||
...requestBody,
|
||||
...modelData?.defaultConfig
|
||||
};
|
||||
|
||||
return {
|
||||
requestBody: requestBody as unknown as InferCompletionsBody<T>,
|
||||
modelData
|
||||
@@ -584,18 +618,14 @@ const createChatCompletion = async ({
|
||||
timeout?: number;
|
||||
options?: OpenAI.RequestOptions;
|
||||
}): Promise<
|
||||
{
|
||||
getEmptyResponseTip: () => string;
|
||||
} & (
|
||||
| {
|
||||
response: StreamChatType;
|
||||
isStreamResponse: true;
|
||||
}
|
||||
| {
|
||||
response: UnStreamChatType;
|
||||
isStreamResponse: false;
|
||||
}
|
||||
)
|
||||
| {
|
||||
response: StreamChatType;
|
||||
isStreamResponse: true;
|
||||
}
|
||||
| {
|
||||
response: UnStreamChatType;
|
||||
isStreamResponse: false;
|
||||
}
|
||||
> => {
|
||||
try {
|
||||
if (!modelData) {
|
||||
@@ -627,34 +657,16 @@ const createChatCompletion = async ({
|
||||
response !== null &&
|
||||
('iterator' in response || 'controller' in response);
|
||||
|
||||
const getEmptyResponseTip = () => {
|
||||
if (userKey?.baseUrl) {
|
||||
addLog.warn(`User LLM response empty`, {
|
||||
baseUrl: userKey?.baseUrl,
|
||||
requestBody: body
|
||||
});
|
||||
return `您的 OpenAI key 没有响应: ${JSON.stringify(body)}`;
|
||||
} else {
|
||||
addLog.error(`LLM response empty`, {
|
||||
message: '',
|
||||
data: body
|
||||
});
|
||||
}
|
||||
return i18nT('chat:LLM_model_response_empty');
|
||||
};
|
||||
|
||||
if (isStreamResponse) {
|
||||
return {
|
||||
response,
|
||||
isStreamResponse: true,
|
||||
getEmptyResponseTip
|
||||
isStreamResponse: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response,
|
||||
isStreamResponse: false,
|
||||
getEmptyResponseTip
|
||||
isStreamResponse: false
|
||||
};
|
||||
} catch (error) {
|
||||
if (userKey?.baseUrl) {
|
||||
|
||||
@@ -177,56 +177,50 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise<ChatResp
|
||||
|
||||
const write = res ? responseWriteController({ res, readStream: stream }) : undefined;
|
||||
|
||||
const {
|
||||
completeMessages,
|
||||
reasoningText,
|
||||
answerText,
|
||||
finish_reason,
|
||||
getEmptyResponseTip,
|
||||
usage
|
||||
} = await createLLMResponse({
|
||||
body: {
|
||||
model: modelConstantsData.model,
|
||||
stream,
|
||||
messages: filterMessages,
|
||||
temperature,
|
||||
max_tokens,
|
||||
top_p: aiChatTopP,
|
||||
stop: aiChatStopSign,
|
||||
response_format: {
|
||||
type: aiChatResponseFormat,
|
||||
json_schema: aiChatJsonSchema
|
||||
const { completeMessages, reasoningText, answerText, finish_reason, responseEmptyTip, usage } =
|
||||
await createLLMResponse({
|
||||
body: {
|
||||
model: modelConstantsData.model,
|
||||
stream,
|
||||
messages: filterMessages,
|
||||
temperature,
|
||||
max_tokens,
|
||||
top_p: aiChatTopP,
|
||||
stop: aiChatStopSign,
|
||||
response_format: {
|
||||
type: aiChatResponseFormat,
|
||||
json_schema: aiChatJsonSchema
|
||||
},
|
||||
retainDatasetCite,
|
||||
useVision: aiChatVision,
|
||||
requestOrigin
|
||||
},
|
||||
retainDatasetCite,
|
||||
useVision: aiChatVision,
|
||||
requestOrigin
|
||||
},
|
||||
userKey: externalProvider.openaiAccount,
|
||||
isAborted: checkIsStopping,
|
||||
onReasoning({ text }) {
|
||||
if (!aiChatReasoning) return;
|
||||
workflowStreamResponse?.({
|
||||
write,
|
||||
event: SseResponseEventEnum.answer,
|
||||
data: textAdaptGptResponse({
|
||||
reasoning_content: text
|
||||
})
|
||||
});
|
||||
},
|
||||
onStreaming({ text }) {
|
||||
if (!isResponseAnswerText) return;
|
||||
workflowStreamResponse?.({
|
||||
write,
|
||||
event: SseResponseEventEnum.answer,
|
||||
data: textAdaptGptResponse({
|
||||
text
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
userKey: externalProvider.openaiAccount,
|
||||
isAborted: checkIsStopping,
|
||||
onReasoning({ text }) {
|
||||
if (!aiChatReasoning) return;
|
||||
workflowStreamResponse?.({
|
||||
write,
|
||||
event: SseResponseEventEnum.answer,
|
||||
data: textAdaptGptResponse({
|
||||
reasoning_content: text
|
||||
})
|
||||
});
|
||||
},
|
||||
onStreaming({ text }) {
|
||||
if (!isResponseAnswerText) return;
|
||||
workflowStreamResponse?.({
|
||||
write,
|
||||
event: SseResponseEventEnum.answer,
|
||||
data: textAdaptGptResponse({
|
||||
text
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!answerText && !reasoningText) {
|
||||
return getNodeErrResponse({ error: getEmptyResponseTip() });
|
||||
if (responseEmptyTip) {
|
||||
return getNodeErrResponse({ error: responseEmptyTip });
|
||||
}
|
||||
|
||||
const { totalPoints, modelName } = formatModelChars2Points({
|
||||
|
||||
@@ -414,7 +414,6 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
|
||||
return;
|
||||
}
|
||||
|
||||
// Thread avoidance
|
||||
await surrenderProcess();
|
||||
const nodeId = this.activeRunQueue.keys().next().value;
|
||||
const node = nodeId ? this.runtimeNodesMap.get(nodeId) : undefined;
|
||||
@@ -430,10 +429,6 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
|
||||
this.processActiveNode();
|
||||
});
|
||||
}
|
||||
// 兜底,除非极端情况,否则不可能触发
|
||||
else {
|
||||
this.processActiveNode();
|
||||
}
|
||||
}
|
||||
|
||||
private addSkipNode(node: RuntimeNodeItemType, skippedNodeIdList: Set<string>) {
|
||||
@@ -446,9 +441,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
|
||||
this.skipNodeQueue.set(node.nodeId, { node, skippedNodeIdList: concatSkippedNodeIdList });
|
||||
}
|
||||
private async processSkipNodes() {
|
||||
// Thread avoidance
|
||||
await surrenderProcess();
|
||||
// 取一个 node,并且从队列里删除
|
||||
await surrenderProcess();
|
||||
const skipItem = this.skipNodeQueue.values().next().value;
|
||||
if (skipItem) {
|
||||
this.skipNodeQueue.delete(skipItem.node.nodeId);
|
||||
|
||||
@@ -25,7 +25,7 @@ const MyIconButton = ({
|
||||
...props
|
||||
}: Props) => {
|
||||
return (
|
||||
<MyTooltip label={tip}>
|
||||
<MyTooltip label={tip} shouldWrapChildren={false}>
|
||||
<Flex
|
||||
position={'relative'}
|
||||
p={1}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg viewBox="0 0 17 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.98608 2.34313C10.5482 0.78103 13.0808 0.78103 14.6429 2.34313C16.205 3.90522 16.205 6.43788 14.6429 7.99998L13.7001 8.94279C13.4398 9.20314 13.0177 9.20314 12.7573 8.94279C12.497 8.68244 12.497 8.26033 12.7573 7.99998L13.7001 7.05717C14.7415 6.01577 14.7415 4.32733 13.7001 3.28594C12.6587 2.24454 10.9703 2.24454 9.92889 3.28594L8.98608 4.22875C8.72573 4.48909 8.30362 4.48909 8.04327 4.22875C7.78292 3.9684 7.78292 3.54629 8.04327 3.28594L8.98608 2.34313ZM11.7908 5.19523C12.0512 5.45558 12.0512 5.87769 11.7908 6.13804L7.12415 10.8047C6.8638 11.0651 6.44169 11.0651 6.18134 10.8047C5.92099 10.5444 5.92099 10.1222 6.18134 9.8619L10.848 5.19523C11.1084 4.93488 11.5305 4.93488 11.7908 5.19523ZM5.21484 7.05717C5.47519 7.31752 5.47519 7.73963 5.21484 7.99998L4.27204 8.94279C3.23064 9.98419 3.23064 11.6726 4.27204 12.714C5.31343 13.7554 7.00187 13.7554 8.04327 12.714L8.98608 11.7712C9.24643 11.5109 9.66854 11.5109 9.92889 11.7712C10.1892 12.0316 10.1892 12.4537 9.92889 12.714L8.98608 13.6568C7.42398 15.2189 4.89132 15.2189 3.32923 13.6568C1.76713 12.0947 1.76713 9.56208 3.32923 7.99998L4.27204 7.05717C4.53239 6.79682 4.9545 6.79682 5.21484 7.05717Z" fill="#485264"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.98608 2.34313C10.5482 0.78103 13.0808 0.78103 14.6429 2.34313C16.205 3.90522 16.205 6.43788 14.6429 7.99998L13.7001 8.94279C13.4398 9.20314 13.0177 9.20314 12.7573 8.94279C12.497 8.68244 12.497 8.26033 12.7573 7.99998L13.7001 7.05717C14.7415 6.01577 14.7415 4.32733 13.7001 3.28594C12.6587 2.24454 10.9703 2.24454 9.92889 3.28594L8.98608 4.22875C8.72573 4.48909 8.30362 4.48909 8.04327 4.22875C7.78292 3.9684 7.78292 3.54629 8.04327 3.28594L8.98608 2.34313ZM11.7908 5.19523C12.0512 5.45558 12.0512 5.87769 11.7908 6.13804L7.12415 10.8047C6.8638 11.0651 6.44169 11.0651 6.18134 10.8047C5.92099 10.5444 5.92099 10.1222 6.18134 9.8619L10.848 5.19523C11.1084 4.93488 11.5305 4.93488 11.7908 5.19523ZM5.21484 7.05717C5.47519 7.31752 5.47519 7.73963 5.21484 7.99998L4.27204 8.94279C3.23064 9.98419 3.23064 11.6726 4.27204 12.714C5.31343 13.7554 7.00187 13.7554 8.04327 12.714L8.98608 11.7712C9.24643 11.5109 9.66854 11.5109 9.92889 11.7712C10.1892 12.0316 10.1892 12.4537 9.92889 12.714L8.98608 13.6568C7.42398 15.2189 4.89132 15.2189 3.32923 13.6568C1.76713 12.0947 1.76713 9.56208 3.32923 7.99998L4.27204 7.05717C4.53239 6.79682 4.9545 6.79682 5.21484 7.05717Z" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -136,6 +136,7 @@
|
||||
"mouse_priority": "Mouse first\n- Press the left button to drag the canvas\n- Hold down shift and left click to select batches",
|
||||
"new_context": "New Context",
|
||||
"next": "Next",
|
||||
"no_edit_permission": "No editing rights",
|
||||
"no_match_node": "No results",
|
||||
"no_node_found": "No node was not found",
|
||||
"not_contains": "Does Not Contain",
|
||||
@@ -195,6 +196,7 @@
|
||||
"text_to_extract": "Text to Extract",
|
||||
"these_variables_will_be_input_parameters_for_code_execution": "These variables will be input parameters for code execution",
|
||||
"to_add_node": "to add",
|
||||
"to_app_detail": "Go to edit app",
|
||||
"to_connect_node": "to connect",
|
||||
"tool.tool_result": "Tool operation results",
|
||||
"tool_active_config": "Tool active",
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
"mouse_priority": "鼠标优先\n- 左键按下后可拖动画布\n- 按住 shift 后左键可批量选择",
|
||||
"new_context": "新的上下文",
|
||||
"next": "下一个",
|
||||
"no_edit_permission": "没有编辑权限",
|
||||
"no_match_node": "无结果",
|
||||
"no_node_found": "未搜索到节点",
|
||||
"not_contains": "不包含",
|
||||
@@ -195,6 +196,7 @@
|
||||
"text_to_extract": "需要提取的文本",
|
||||
"these_variables_will_be_input_parameters_for_code_execution": "这些变量会作为代码的运行的输入参数",
|
||||
"to_add_node": "添加节点",
|
||||
"to_app_detail": "前去编辑应用",
|
||||
"to_connect_node": "连接节点",
|
||||
"tool.tool_result": "工具运行结果",
|
||||
"tool_active_config": "工具激活",
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
"mouse_priority": "滑鼠優先\n- 按下左鍵拖曳畫布\n- 按住 Shift 鍵並點選左鍵可批次選取",
|
||||
"new_context": "新的脈絡",
|
||||
"next": "下一個",
|
||||
"no_edit_permission": "沒有編輯權限",
|
||||
"no_match_node": "無結果",
|
||||
"no_node_found": "未搜索到節點",
|
||||
"not_contains": "不包含",
|
||||
@@ -195,6 +196,7 @@
|
||||
"text_to_extract": "要擷取的文字",
|
||||
"these_variables_will_be_input_parameters_for_code_execution": "這些變數會作為程式碼執行的輸入參數",
|
||||
"to_add_node": "添加節點",
|
||||
"to_app_detail": "前去編輯應用",
|
||||
"to_connect_node": "連接節點",
|
||||
"tool.tool_result": "工具運行結果",
|
||||
"tool_active_config": "工具激活",
|
||||
|
||||
@@ -55,7 +55,8 @@ import {
|
||||
PluginStatusMap,
|
||||
type PluginStatusType
|
||||
} from '@fastgpt/global/core/plugin/type';
|
||||
import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils';
|
||||
import { splitCombineToolId, getToolRawId } from '@fastgpt/global/core/app/tool/utils';
|
||||
import { getAppPermission } from '@/web/core/app/api';
|
||||
|
||||
type Props = FlowNodeItemType & {
|
||||
children?: React.ReactNode | React.ReactNode[] | string;
|
||||
@@ -309,8 +310,8 @@ const NodeCard = (props: Props) => {
|
||||
'& .controller-debug': {
|
||||
display: 'block'
|
||||
},
|
||||
'& .controller-rename': {
|
||||
display: 'block'
|
||||
'& .node-hover-controller': {
|
||||
visibility: 'visible'
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setHoverNodeId(nodeId)}
|
||||
@@ -346,6 +347,7 @@ const NodeCard = (props: Props) => {
|
||||
avatar={avatar}
|
||||
name={name}
|
||||
searchedText={searchedText}
|
||||
appId={pluginId}
|
||||
/>
|
||||
|
||||
<Box flex={1} mr={1} />
|
||||
@@ -428,11 +430,14 @@ const NodeTitleSection = React.memo<{
|
||||
avatar: string;
|
||||
name: string;
|
||||
searchedText?: string;
|
||||
}>(({ nodeId, avatar, name, searchedText }) => {
|
||||
appId?: string;
|
||||
}>(({ nodeId, avatar, name, searchedText, appId }) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const onChangeNode = useContextSelector(WorkflowActionsContext, (v) => v.onChangeNode);
|
||||
|
||||
const childAppId = useMemo(() => (appId ? getToolRawId(appId) : undefined), [appId]);
|
||||
|
||||
// custom title edit
|
||||
const { onOpenModal: onOpenCustomTitleModal, EditModal: EditTitleModal } = useEditTitle({
|
||||
title: t('common:custom_title'),
|
||||
@@ -459,8 +464,21 @@ const NodeTitleSection = React.memo<{
|
||||
});
|
||||
}, [onOpenCustomTitleModal, name, onChangeNode, nodeId, toast, t]);
|
||||
|
||||
const { runAsync: onGetPermission } = useRequest2(getAppPermission, {
|
||||
onSuccess(permission) {
|
||||
if (permission.hasWritePer) {
|
||||
window.open(`/app/detail?appId=${childAppId}`, '_blank');
|
||||
} else {
|
||||
toast({
|
||||
title: t('workflow:no_edit_permission'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={avatar} borderRadius={'sm'} objectFit={'contain'} w={'24px'} h={'24px'} />
|
||||
<Box ml={2} fontSize={'18px'} fontWeight={'medium'} color={'myGray.900'}>
|
||||
<HighlightText
|
||||
@@ -470,20 +488,22 @@ const NodeTitleSection = React.memo<{
|
||||
color={'#ffe82d'}
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
display={'none'}
|
||||
variant={'grayGhost'}
|
||||
size={'xs'}
|
||||
ml={0.5}
|
||||
className="controller-rename"
|
||||
cursor={'pointer'}
|
||||
onClick={handleRenameClick}
|
||||
>
|
||||
<MyIcon name={'edit'} w={'14px'} />
|
||||
</Button>
|
||||
<Box ml={1} visibility={'hidden'}>
|
||||
<MyIconButton className="node-hover-controller" icon="edit" onClick={handleRenameClick} />
|
||||
</Box>
|
||||
{childAppId && (
|
||||
<Box ml={1} visibility={'hidden'}>
|
||||
<MyIconButton
|
||||
className="node-hover-controller"
|
||||
icon="common/link"
|
||||
tip={t('workflow:to_app_detail')}
|
||||
onClick={() => onGetPermission(childAppId)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<EditTitleModal maxLength={100} />
|
||||
</>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
NodeTitleSection.displayName = 'NodeTitleSection';
|
||||
|
||||
@@ -205,18 +205,7 @@ export const WorkflowUtilsProvider = ({ children }: { children: ReactNode }) =>
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
getNodes,
|
||||
edges,
|
||||
onRemoveError,
|
||||
fitView,
|
||||
getViewport,
|
||||
setViewport,
|
||||
toast,
|
||||
t,
|
||||
onUpdateNodeError,
|
||||
onChangeNode
|
||||
]
|
||||
[getNodes, edges, onRemoveError, fitView, toast, t, onUpdateNodeError]
|
||||
);
|
||||
|
||||
// 4. initData - 初始化工作流数据
|
||||
|
||||
45
projects/app/src/pages/api/core/app/getPermission.ts
Normal file
45
projects/app/src/pages/api/core/app/getPermission.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import {
|
||||
GetAppPermissionQuerySchema,
|
||||
GetAppPermissionResponseSchema,
|
||||
type GetAppPermissionResponseType
|
||||
} from '@fastgpt/global/openapi/core/app/common/api';
|
||||
|
||||
/* Get app permission */
|
||||
async function handler(
|
||||
req: ApiRequestProps,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<GetAppPermissionResponseType> {
|
||||
const { appId } = GetAppPermissionQuerySchema.parse(req.query);
|
||||
|
||||
// Auth app permission
|
||||
try {
|
||||
const { app } = await authApp({
|
||||
req,
|
||||
authToken: true,
|
||||
appId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
return GetAppPermissionResponseSchema.parse({
|
||||
hasReadPer: app.permission.hasReadPer,
|
||||
hasWritePer: app.permission.hasWritePer,
|
||||
hasManagePer: app.permission.hasManagePer,
|
||||
hasReadChatLogPer: app.permission.hasReadChatLogPer,
|
||||
isOwner: app.permission.isOwner
|
||||
});
|
||||
} catch (error) {
|
||||
return GetAppPermissionResponseSchema.parse({
|
||||
hasReadPer: false,
|
||||
hasWritePer: false,
|
||||
hasManagePer: false,
|
||||
hasReadChatLogPer: false,
|
||||
isOwner: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -5,6 +5,7 @@ import type { CreateAppBody } from '@/pages/api/core/app/create';
|
||||
import type { ListAppBody } from '@/pages/api/core/app/list';
|
||||
|
||||
import type { getBasicInfoResponse } from '@/pages/api/core/app/getBasicInfo';
|
||||
import type { GetAppPermissionResponseType } from '@fastgpt/global/openapi/core/app/common/api';
|
||||
|
||||
/**
|
||||
* 获取应用列表
|
||||
@@ -36,6 +37,9 @@ export const getAppDetailById = (id: string) => GET<AppDetailType>(`/core/app/de
|
||||
export const putAppById = (id: string, data: AppUpdateParams) =>
|
||||
PUT(`/core/app/update?appId=${id}`, data);
|
||||
|
||||
export const getAppPermission = (appId: string) =>
|
||||
GET<GetAppPermissionResponseType>(`/core/app/getPermission?appId=${appId}`);
|
||||
|
||||
/**
|
||||
* Get app basic info by ids
|
||||
*/
|
||||
|
||||
120
projects/app/test/api/core/app/getPermission.test.ts
Normal file
120
projects/app/test/api/core/app/getPermission.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as getPermissionApi from '@/pages/api/core/app/getPermission';
|
||||
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { getFakeUsers } from '@test/datas/users';
|
||||
import { Call } from '@test/utils/request';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type {
|
||||
GetAppPermissionQueryType,
|
||||
GetAppPermissionResponseType
|
||||
} from '@fastgpt/global/openapi/core/app/common/api';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
describe('get app permission api', () => {
|
||||
it('should return permission when user has access', async () => {
|
||||
const users = await getFakeUsers(1);
|
||||
const user = users.members[0];
|
||||
|
||||
// Create a test app
|
||||
const app = await MongoApp.create({
|
||||
name: 'test-app',
|
||||
type: AppTypeEnum.simple,
|
||||
modules: [],
|
||||
edges: [],
|
||||
teamId: user.teamId,
|
||||
tmbId: user.tmbId
|
||||
});
|
||||
|
||||
const res = await Call<{}, GetAppPermissionQueryType, GetAppPermissionResponseType>(
|
||||
getPermissionApi.default,
|
||||
{
|
||||
auth: user,
|
||||
query: {
|
||||
appId: String(app._id)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toBeDefined();
|
||||
expect(res.data.isOwner).toBe(true);
|
||||
expect(res.data.hasReadPer).toBe(true);
|
||||
expect(res.data.hasWritePer).toBe(true);
|
||||
expect(res.data.hasManagePer).toBe(true);
|
||||
});
|
||||
|
||||
it('should return error when appId is missing', async () => {
|
||||
const users = await getFakeUsers(1);
|
||||
const user = users.members[0];
|
||||
|
||||
const res = await Call<{}, GetAppPermissionQueryType, GetAppPermissionResponseType>(
|
||||
getPermissionApi.default,
|
||||
{
|
||||
auth: user,
|
||||
query: {
|
||||
appId: ''
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(res.error, 232);
|
||||
expect(res.error instanceof ZodError).toBe(true);
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should return error when user does not have access', async () => {
|
||||
const users = await getFakeUsers(2);
|
||||
const user1 = users.members[0];
|
||||
const user2 = users.members[1];
|
||||
|
||||
// Create a test app by user1
|
||||
const app = await MongoApp.create({
|
||||
name: 'test-app',
|
||||
type: AppTypeEnum.simple,
|
||||
modules: [],
|
||||
edges: [],
|
||||
teamId: user1.teamId,
|
||||
tmbId: user1.tmbId
|
||||
});
|
||||
|
||||
// Try to get permission as user2 (different team)
|
||||
const res = await Call<{}, GetAppPermissionQueryType, GetAppPermissionResponseType>(
|
||||
getPermissionApi.default,
|
||||
{
|
||||
auth: user2,
|
||||
query: {
|
||||
appId: String(app._id)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.data.isOwner).toBe(false);
|
||||
expect(res.data.hasReadPer).toBe(false);
|
||||
expect(res.data.hasWritePer).toBe(false);
|
||||
expect(res.data.hasManagePer).toBe(false);
|
||||
expect(res.data.hasReadChatLogPer).toBe(false);
|
||||
expect(res.code).toBe(200);
|
||||
});
|
||||
|
||||
it('should return error when app does not exist', async () => {
|
||||
const users = await getFakeUsers(1);
|
||||
const user = users.members[0];
|
||||
|
||||
const res = await Call<{}, GetAppPermissionQueryType, GetAppPermissionResponseType>(
|
||||
getPermissionApi.default,
|
||||
{
|
||||
auth: user,
|
||||
query: {
|
||||
appId: '507f1f77bcf86cd799439011' // Non-existent appId
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(res.data.isOwner).toBe(false);
|
||||
expect(res.data.hasReadPer).toBe(false);
|
||||
expect(res.data.hasWritePer).toBe(false);
|
||||
expect(res.data.hasManagePer).toBe(false);
|
||||
expect(res.data.hasReadChatLogPer).toBe(false);
|
||||
expect(res.code).toBe(200);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user