diff --git a/document/content/docs/upgrading/4-14/4145.mdx b/document/content/docs/upgrading/4-14/4145.mdx index a50158082e..09db9a7249 100644 --- a/document/content/docs/upgrading/4-14/4145.mdx +++ b/document/content/docs/upgrading/4-14/4145.mdx @@ -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 请求空响应判断,排除敏感过滤错误被误认为无响应。 ## 🐛 修复 diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index f96d2b849c..b7e195d909 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -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", diff --git a/packages/global/core/app/tool/utils.ts b/packages/global/core/app/tool/utils.ts index 1838a1d3af..b3a0113104 100644 --- a/packages/global/core/app/tool/utils.ts +++ b/packages/global/core/app/tool/utils.ts @@ -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]; +}; diff --git a/packages/global/openapi/core/app/common/api.ts b/packages/global/openapi/core/app/common/api.ts new file mode 100644 index 0000000000..9537e77747 --- /dev/null +++ b/packages/global/openapi/core/app/common/api.ts @@ -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; + +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; diff --git a/packages/global/openapi/core/app/common/index.ts b/packages/global/openapi/core/app/common/index.ts new file mode 100644 index 0000000000..912bca02ec --- /dev/null +++ b/packages/global/openapi/core/app/common/index.ts @@ -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 + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/core/app/index.ts b/packages/global/openapi/core/app/index.ts index 3717da38e6..5b59d6e293 100644 --- a/packages/global/openapi/core/app/index.ts +++ b/packages/global/openapi/core/app/index.ts @@ -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 }; diff --git a/packages/global/openapi/index.ts b/packages/global/openapi/index.ts index 824fa91017..f7c2366b50 100644 --- a/packages/global/openapi/index.ts +++ b/packages/global/openapi/index.ts @@ -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: '对话管理', diff --git a/packages/global/openapi/tag.ts b/packages/global/openapi/tag.ts index fca3850355..c12b78df20 100644 --- a/packages/global/openapi/tag.ts +++ b/packages/global/openapi/tag.ts @@ -2,6 +2,8 @@ export const TagsMap = { /* Core */ // Agent - log appLog: 'Agent 日志', + // Agent - common + appCommon: 'Agent 管理', // Chat - home chatPage: '对话页', diff --git a/packages/service/core/ai/llm/agentCall/index.ts b/packages/service/core/ai/llm/agentCall/index.ts index f59affd9d9..d937c58c05 100644 --- a/packages/service/core/ai/llm/agentCall/index.ts +++ b/packages/service/core/ai/llm/agentCall/index.ts @@ -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 diff --git a/packages/service/core/ai/llm/request.ts b/packages/service/core/ai/llm/request.ts index 1d132c1290..0c0e1d089e 100644 --- a/packages/service/core/ai/llm/request.ts +++ b/packages/service/core/ai/llm/request.ts @@ -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 ( }); // 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 ( 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 ({ 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 ({ 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 ({ }) } 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 ({ }); } + requestBody = { + ...requestBody, + ...modelData?.defaultConfig + }; + return { requestBody: requestBody as unknown as InferCompletionsBody, 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) { diff --git a/packages/service/core/workflow/dispatch/ai/chat.ts b/packages/service/core/workflow/dispatch/ai/chat.ts index 9234578600..8abaa1eb09 100644 --- a/packages/service/core/workflow/dispatch/ai/chat.ts +++ b/packages/service/core/workflow/dispatch/ai/chat.ts @@ -177,56 +177,50 @@ export const dispatchChatCompletion = async (props: ChatProps): Promise) { @@ -446,9 +441,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise { return ( - + - + diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index 619bf2077a..57e1f85485 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -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", diff --git a/packages/web/i18n/zh-CN/workflow.json b/packages/web/i18n/zh-CN/workflow.json index f7129cc9d2..77e8a07f0e 100644 --- a/packages/web/i18n/zh-CN/workflow.json +++ b/packages/web/i18n/zh-CN/workflow.json @@ -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": "工具激活", diff --git a/packages/web/i18n/zh-Hant/workflow.json b/packages/web/i18n/zh-Hant/workflow.json index 0c1dd8254d..be3bc94b46 100644 --- a/packages/web/i18n/zh-Hant/workflow.json +++ b/packages/web/i18n/zh-Hant/workflow.json @@ -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": "工具激活", diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index d649c121ea..dec269c4c3 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -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} /> @@ -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 ( - <> + - + + + + {childAppId && ( + + onGetPermission(childAppId)} + /> + + )} - + ); }); NodeTitleSection.displayName = 'NodeTitleSection'; diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowUtilsContext.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowUtilsContext.tsx index 58c85a1565..a7f565a3b2 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowUtilsContext.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/context/workflowUtilsContext.tsx @@ -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 - 初始化工作流数据 diff --git a/projects/app/src/pages/api/core/app/getPermission.ts b/projects/app/src/pages/api/core/app/getPermission.ts new file mode 100644 index 0000000000..a49f12ca15 --- /dev/null +++ b/projects/app/src/pages/api/core/app/getPermission.ts @@ -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 +): Promise { + 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); diff --git a/projects/app/src/web/core/app/api.ts b/projects/app/src/web/core/app/api.ts index fa31ec5d28..ccfb9ffca1 100644 --- a/projects/app/src/web/core/app/api.ts +++ b/projects/app/src/web/core/app/api.ts @@ -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(`/core/app/de export const putAppById = (id: string, data: AppUpdateParams) => PUT(`/core/app/update?appId=${id}`, data); +export const getAppPermission = (appId: string) => + GET(`/core/app/getPermission?appId=${appId}`); + /** * Get app basic info by ids */ diff --git a/projects/app/test/api/core/app/getPermission.test.ts b/projects/app/test/api/core/app/getPermission.test.ts new file mode 100644 index 0000000000..bd2491e898 --- /dev/null +++ b/projects/app/test/api/core/app/getPermission.test.ts @@ -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); + }); +});