diff --git a/packages/global/openapi/core/app/index.ts b/packages/global/openapi/core/app/index.ts index 77c353c68..3717da38e 100644 --- a/packages/global/openapi/core/app/index.ts +++ b/packages/global/openapi/core/app/index.ts @@ -1,6 +1,8 @@ import type { OpenAPIPath } from '../../type'; import { AppLogPath } from './log'; +import { PublishChannelPath } from './publishChannel'; export const AppPath: OpenAPIPath = { - ...AppLogPath + ...AppLogPath, + ...PublishChannelPath }; diff --git a/packages/global/openapi/core/app/publishChannel/index.ts b/packages/global/openapi/core/app/publishChannel/index.ts new file mode 100644 index 000000000..3b8dc0da8 --- /dev/null +++ b/packages/global/openapi/core/app/publishChannel/index.ts @@ -0,0 +1,5 @@ +import { PlaygroundPath } from './playground'; + +export const PublishChannelPath = { + ...PlaygroundPath +}; diff --git a/packages/global/openapi/core/app/publishChannel/playground/api.ts b/packages/global/openapi/core/app/publishChannel/playground/api.ts new file mode 100644 index 000000000..b3fd94ab9 --- /dev/null +++ b/packages/global/openapi/core/app/publishChannel/playground/api.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; +import { ObjectIdSchema } from '../../../../../common/type/mongo'; + +// Get Playground Visibility Config Parameters +export const GetPlaygroundVisibilityConfigParamsSchema = z.object({ + appId: ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '应用 ID' + }) +}); +export type GetPlaygroundVisibilityConfigParamsType = z.infer< + typeof GetPlaygroundVisibilityConfigParamsSchema +>; + +// Playground Visibility Config Response +export const PlaygroundVisibilityConfigResponseSchema = z.object({ + showNodeStatus: z.boolean().meta({ + example: true, + description: '是否显示节点状态' + }), + responseDetail: z.boolean().meta({ + example: true, + description: '是否显示响应详情' + }), + showFullText: z.boolean().meta({ + example: true, + description: '是否显示全文' + }), + showRawSource: z.boolean().meta({ + example: true, + description: '是否显示原始来源' + }) +}); +export type PlaygroundVisibilityConfigResponseType = z.infer< + typeof PlaygroundVisibilityConfigResponseSchema +>; + +// Update Playground Visibility Config Parameters +export const UpdatePlaygroundVisibilityConfigParamsSchema = z.object({ + appId: ObjectIdSchema.meta({ + example: '68ad85a7463006c963799a05', + description: '应用 ID' + }), + showNodeStatus: z.boolean().meta({ + example: true, + description: '是否显示节点状态' + }), + responseDetail: z.boolean().meta({ + example: true, + description: '是否显示响应详情' + }), + showFullText: z.boolean().meta({ + example: true, + description: '是否显示全文' + }), + showRawSource: z.boolean().meta({ + example: true, + description: '是否显示原始来源' + }) +}); +export type UpdatePlaygroundVisibilityConfigParamsType = z.infer< + typeof UpdatePlaygroundVisibilityConfigParamsSchema +>; diff --git a/packages/global/openapi/core/app/publishChannel/playground/index.ts b/packages/global/openapi/core/app/publishChannel/playground/index.ts new file mode 100644 index 000000000..c31349eb2 --- /dev/null +++ b/packages/global/openapi/core/app/publishChannel/playground/index.ts @@ -0,0 +1,109 @@ +import { z } from 'zod'; +import type { OpenAPIPath } from '../../../../type'; +import { + GetPlaygroundVisibilityConfigParamsSchema, + PlaygroundVisibilityConfigResponseSchema, + UpdatePlaygroundVisibilityConfigParamsSchema +} from './api'; +import { TagsMap } from '../../../../tag'; + +export const PlaygroundPath: OpenAPIPath = { + '/api/support/outLink/playground/config': { + get: { + summary: '获取门户配置', + description: + '获取指定应用的门户聊天界面的可见性配置,包括节点状态、响应详情、全文显示和原始来源显示的设置', + tags: [TagsMap.publishChannel], + requestParams: { + query: GetPlaygroundVisibilityConfigParamsSchema + }, + responses: { + 200: { + description: '成功返回门户配置', + content: { + 'application/json': { + schema: PlaygroundVisibilityConfigResponseSchema + } + } + }, + 400: { + description: '请求参数错误', + content: { + 'application/json': { + schema: z.object({ + code: z.literal(500), + statusText: z.literal('Invalid Params'), + message: z.string(), + data: z.null() + }) + } + } + }, + 401: { + description: '用户未授权', + content: { + 'application/json': { + schema: z.object({ + code: z.literal(401), + statusText: z.literal('unAuthorization'), + message: z.string(), + data: z.null() + }) + } + } + } + } + } + }, + '/api/support/outLink/playground/update': { + post: { + summary: '更新门户配置', + description: + '更新指定应用的门户聊天界面的可见性配置,包括节点状态、响应详情、全文显示和原始来源显示的设置。如果配置不存在则创建新配置', + tags: [TagsMap.publishChannel], + requestBody: { + content: { + 'application/json': { + schema: UpdatePlaygroundVisibilityConfigParamsSchema + } + } + }, + responses: { + 200: { + description: '成功更新门户配置', + content: { + 'application/json': { + schema: z.null() + } + } + }, + 400: { + description: '请求参数错误', + content: { + 'application/json': { + schema: z.object({ + code: z.literal(500), + statusText: z.literal('Invalid Params'), + message: z.string(), + data: z.null() + }) + } + } + }, + 401: { + description: '用户未授权', + content: { + 'application/json': { + schema: z.object({ + code: z.literal(401), + statusText: z.literal('unAuthorization'), + message: z.string(), + data: z.null() + }) + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/index.ts b/packages/global/openapi/index.ts index b06c9d816..535726021 100644 --- a/packages/global/openapi/index.ts +++ b/packages/global/openapi/index.ts @@ -26,7 +26,7 @@ export const openAPIDocument = createDocument({ 'x-tagGroups': [ { name: 'Agent 应用', - tags: [TagsMap.appLog] + tags: [TagsMap.appLog, TagsMap.publishChannel] }, { name: '对话管理', diff --git a/packages/global/openapi/tag.ts b/packages/global/openapi/tag.ts index 34806829e..1138a937c 100644 --- a/packages/global/openapi/tag.ts +++ b/packages/global/openapi/tag.ts @@ -13,6 +13,9 @@ export const TagsMap = { pluginToolTag: '工具标签', pluginTeam: '团队插件管理', + // Publish Channel + publishChannel: '发布渠道', + /* Support */ // Wallet walletBill: '订单', diff --git a/packages/global/support/outLink/type.d.ts b/packages/global/support/outLink/type.d.ts index 57cc42136..59c7a8fcc 100644 --- a/packages/global/support/outLink/type.d.ts +++ b/packages/global/support/outLink/type.d.ts @@ -108,16 +108,11 @@ export type OutLinkEditType = { app?: T; }; -export type PlaygroundVisibilityConfigType = { - showNodeStatus: boolean; - responseDetail: boolean; - showFullText: boolean; - showRawSource: boolean; -}; - export const PlaygroundVisibilityConfigSchema = z.object({ showNodeStatus: z.boolean(), responseDetail: z.boolean(), showFullText: z.boolean(), showRawSource: z.boolean() }); + +export type PlaygroundVisibilityConfigType = z.infer; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx index cd75b8f06..b856685c8 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx @@ -259,7 +259,7 @@ const ChatItem = (props: Props) => { setCiteModalData({ rawSearch: quoteList, metadata: - item?.collectionId && (isShowReadRawSource || isShowFullText) + item?.collectionId && isShowFullText ? { appId: appId, chatId: chatId, diff --git a/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx index c5c1cf7dc..e50197474 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/Link/index.tsx @@ -184,7 +184,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => { name: item.name, responseDetail: item.responseDetail ?? false, showRawSource: item.showRawSource ?? false, - showFullText: item.showFullText ?? item.showRawSource ?? false, + showFullText: item.showFullText ?? true, showNodeStatus: item.showNodeStatus ?? false, limit: item.limit }) diff --git a/projects/app/src/pages/api/admin/initv4145.ts b/projects/app/src/pages/api/admin/initv4145.ts index 060bca0ca..6898be4d3 100644 --- a/projects/app/src/pages/api/admin/initv4145.ts +++ b/projects/app/src/pages/api/admin/initv4145.ts @@ -5,36 +5,47 @@ import type { S3MQJobData } from '@fastgpt/service/common/s3/mq'; import { addLog } from '@fastgpt/service/common/system/log'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; +import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; export type ResponseType = { message: string; retriedCount: number; failedCount: number; + shareLinkMigration: { + totalRecords: number; + updatedRecords: number; + }; }; +/** + * 4.14.5 版本数据初始化脚本 + * 1. 重试所有失败的 S3 删除任务 + * 2. 为所有 share 类型的 OutLink 记录添加 showFullText 字段 + */ async function handler( req: ApiRequestProps, res: ApiResponseType ): Promise { await authCert({ req, authRoot: true }); - const queue = getQueue(QueueNames.s3FileDelete); - // Get all failed jobs and retry them + // 1. 处理失败的 S3 删除任务 + const queue = getQueue(QueueNames.s3FileDelete); const failedJobs = await queue.getFailed(); - console.log(`Found ${failedJobs.length} failed jobs`); + console.log(`Found ${failedJobs.length} failed S3 delete jobs`); let retriedCount = 0; await batchRun( failedJobs, async (job) => { - addLog.debug(`Retrying job with 3 new attempts`, { retriedCount }); + addLog.debug(`Retrying S3 delete job with new attempts`, { retriedCount }); try { // Remove old job and recreate with new attempts const jobData = job.data; await job.remove(); - // Add new job with 3 more attempts + // Add new job with more attempts await queue.add('delete-s3-files', jobData, { attempts: 10, removeOnFail: { @@ -49,18 +60,54 @@ async function handler( }); retriedCount++; - console.log(`Retried job ${job.id} with 3 new attempts`); + console.log(`Retried S3 delete job ${job.id} with new attempts`); } catch (error) { - console.error(`Failed to retry job ${job.id}:`, error); + console.error(`Failed to retry S3 delete job ${job.id}:`, error); } }, 100 ); + // 2. 处理 share 类型的 OutLink 记录迁移 + let shareLinkMigration = { totalRecords: 0, updatedRecords: 0 }; + + try { + // 查找所有 share 类型且没有 showFullText 字段的记录 + const shareLinks = await MongoOutLink.find({ + type: PublishChannelEnum.share, + showFullText: { $exists: false } + }).lean(); + + shareLinkMigration.totalRecords = shareLinks.length; + + if (shareLinks.length > 0) { + // 批量更新 + const bulkOps = shareLinks.map((link) => ({ + updateOne: { + filter: { _id: link._id }, + update: { $set: { showFullText: link.showRawSource ?? true } } + } + })); + + const result = await MongoOutLink.bulkWrite(bulkOps); + shareLinkMigration.updatedRecords = result.modifiedCount; + + console.log( + `Migration completed: ${shareLinkMigration.updatedRecords}/${shareLinkMigration.totalRecords} share links updated` + ); + } else { + console.log('No share link records need migration'); + } + } catch (error) { + console.error('Failed to migrate share links:', error); + // 即使迁移失败,也继续返回 S3 任务处理的结果 + } + return { - message: 'Successfully retried all failed S3 delete jobs with 3 new attempts', + message: `Completed S3 delete job retries and share link migration for v4.14.5`, retriedCount, - failedCount: failedJobs.length + failedCount: failedJobs.length, + shareLinkMigration }; } diff --git a/projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts b/projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts index 5011a5f23..052644183 100644 --- a/projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts +++ b/projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts @@ -73,7 +73,7 @@ async function handler( authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] }) ]); - if ((!showRawSource && !showFullText) || !chat || !chatItem || initialAnchor === undefined) { + if (!showFullText || !chat || !chatItem || initialAnchor === undefined) { return Promise.reject(ChatErrEnum.unAuthChat); } diff --git a/projects/app/src/pages/api/support/outLink/playground/config.ts b/projects/app/src/pages/api/support/outLink/playground/config.ts index 83f6340e7..62b742c31 100644 --- a/projects/app/src/pages/api/support/outLink/playground/config.ts +++ b/projects/app/src/pages/api/support/outLink/playground/config.ts @@ -1,5 +1,7 @@ import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; +import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; +import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; import { @@ -14,6 +16,13 @@ async function handler( ): Promise { const { appId } = PlaygroundVisibilityConfigQuerySchema.parse(req.query); + await authApp({ + req, + authToken: true, + appId, + per: WritePermissionVal + }); + const existingConfig = await MongoOutLink.findOne( { appId, diff --git a/projects/app/src/pages/api/support/outLink/playground/update.ts b/projects/app/src/pages/api/support/outLink/playground/update.ts index fe952b31d..6c7e164cf 100644 --- a/projects/app/src/pages/api/support/outLink/playground/update.ts +++ b/projects/app/src/pages/api/support/outLink/playground/update.ts @@ -1,7 +1,7 @@ import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; -import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; import { @@ -17,7 +17,7 @@ async function handler(req: ApiRequestProps { + let rootUser: any; + let testApp: any; + + beforeAll(async () => { + rootUser = await getRootUser(); + + // Create a test app owned by the root user + testApp = await MongoApp.create({ + name: 'Test App for Playground Config', + type: 'simple', + tmbId: rootUser.tmbId, + teamId: rootUser.teamId + }); + }); + + afterEach(async () => { + // Clean up any created OutLink configs + await MongoOutLink.deleteMany({ + appId: testApp._id, + type: PublishChannelEnum.playground + }); + }); + + afterAll(async () => { + // Clean up test data + await MongoApp.deleteOne({ _id: testApp._id }); + }); + + it('should return default config values when no existing config found', async () => { + const res = await Call(configApi.default, { + auth: rootUser, + query: { + appId: testApp._id + } + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + expect(res.data).toEqual({ + showNodeStatus: true, + responseDetail: true, + showFullText: true, + showRawSource: true + }); + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should return existing config values when config exists', async () => { + // Create an existing config + await MongoOutLink.create({ + shareId: `playground-${testApp._id}`, + teamId: rootUser.teamId, + tmbId: rootUser.tmbId, + appId: testApp._id, + name: 'Playground Chat', + type: PublishChannelEnum.playground, + showNodeStatus: false, + responseDetail: false, + showFullText: false, + showRawSource: false, + usagePoints: 0, + lastTime: new Date() + }); + + const res = await Call(configApi.default, { + auth: rootUser, + query: { + appId: testApp._id + } + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + expect(res.data).toEqual({ + showNodeStatus: false, + responseDetail: false, + showFullText: false, + showRawSource: false + }); + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should return 500 when appId is missing', async () => { + const res = await Call(configApi.default, { + auth: rootUser, + query: {} + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should return 500 when appId is empty string', async () => { + const res = await Call(configApi.default, { + auth: rootUser, + query: { + appId: '' + } + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should handle mixed config values correctly', async () => { + // Create config with mixed true/false values + await MongoOutLink.create({ + shareId: `playground-${testApp._id}`, + teamId: rootUser.teamId, + tmbId: rootUser.tmbId, + appId: testApp._id, + name: 'Playground Chat', + type: PublishChannelEnum.playground, + showNodeStatus: true, + responseDetail: false, + showFullText: true, + showRawSource: false, + usagePoints: 0, + lastTime: new Date() + }); + + const res = await Call(configApi.default, { + auth: rootUser, + query: { + appId: testApp._id + } + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + expect(res.data).toEqual({ + showNodeStatus: true, + responseDetail: false, + showFullText: true, + showRawSource: false + }); + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should return error when user is not authenticated', async () => { + const res = await Call(configApi.default, { + query: { + appId: testApp._id + } + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); +}); diff --git a/projects/app/test/pages/api/support/outLink/playground/update.test.ts b/projects/app/test/pages/api/support/outLink/playground/update.test.ts new file mode 100644 index 000000000..0cc9945aa --- /dev/null +++ b/projects/app/test/pages/api/support/outLink/playground/update.test.ts @@ -0,0 +1,294 @@ +import type { UpdatePlaygroundVisibilityConfigBody } from '@fastgpt/global/support/outLink/api.d'; +import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; +import { getRootUser } from '@test/datas/users'; +import { Call } from '@test/utils/request'; +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; +import * as updateApi from '@/pages/api/support/outLink/playground/update'; + +describe('Playground Visibility Update API', () => { + let rootUser: any; + let testApp: any; + + beforeAll(async () => { + rootUser = await getRootUser(); + + // Create a test app owned by the root user + testApp = await MongoApp.create({ + name: 'Test App for Playground Update', + type: 'simple', + tmbId: rootUser.tmbId, + teamId: rootUser.teamId + }); + }); + + afterEach(async () => { + // Clean up any created OutLink configs + await MongoOutLink.deleteMany({ + appId: testApp._id, + type: PublishChannelEnum.playground + }); + }); + + afterAll(async () => { + // Clean up test data + await MongoApp.deleteOne({ _id: testApp._id }); + }); + + it('should handle update request with valid data', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp._id, + showNodeStatus: false, + responseDetail: false, + showFullText: false, + showRawSource: false + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + + // Verify the config was created in database + const createdConfig = await MongoOutLink.findOne({ + appId: testApp._id, + type: PublishChannelEnum.playground + }).lean(); + + if (createdConfig) { + expect(createdConfig.appId).toBe(testApp._id); + expect(createdConfig.type).toBe(PublishChannelEnum.playground); + expect(createdConfig.showNodeStatus).toBe(false); + expect(createdConfig.responseDetail).toBe(false); + expect(createdConfig.showFullText).toBe(false); + expect(createdConfig.showRawSource).toBe(false); + } + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should handle update request with true values', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp._id, + showNodeStatus: true, + responseDetail: true, + showFullText: true, + showRawSource: true + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + + // Verify true values were set + const createdConfig = await MongoOutLink.findOne({ + appId: testApp._id, + type: PublishChannelEnum.playground + }).lean(); + + if (createdConfig) { + expect(createdConfig.showNodeStatus).toBe(true); + expect(createdConfig.responseDetail).toBe(true); + expect(createdConfig.showFullText).toBe(true); + expect(createdConfig.showRawSource).toBe(true); + } + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should handle update request with mixed boolean values', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp._id, + showNodeStatus: false, + responseDetail: true, + showFullText: false, + showRawSource: true + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + + // Verify mixed values were set + const createdConfig = await MongoOutLink.findOne({ + appId: testApp._id, + type: PublishChannelEnum.playground + }).lean(); + + if (createdConfig) { + expect(createdConfig.showNodeStatus).toBe(false); + expect(createdConfig.responseDetail).toBe(true); + expect(createdConfig.showFullText).toBe(false); + expect(createdConfig.showRawSource).toBe(true); + } + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + }); + + it('should return 500 when appId is missing', async () => { + const updateData = { + showNodeStatus: false + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should return 500 when appId is empty string', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: '', + showNodeStatus: false, + responseDetail: false, + showFullText: false, + showRawSource: false + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should return error when user is not authenticated', async () => { + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp._id, + showNodeStatus: false, + responseDetail: false, + showFullText: false, + showRawSource: false + }; + + const res = await Call(updateApi.default, { + body: updateData + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should validate all boolean fields are required', async () => { + // Test with missing boolean fields (should fail validation) + const updateData = { + appId: testApp._id, + showNodeStatus: false + // Missing other boolean fields + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('should handle updates for different apps independently', async () => { + // Create a second test app + const testApp2 = await MongoApp.create({ + name: 'Test App 2 for Playground Update', + type: 'simple', + tmbId: rootUser.tmbId, + teamId: rootUser.teamId + }); + + // Create config for first app + await MongoOutLink.create({ + shareId: `playground-${testApp._id}`, + teamId: rootUser.teamId, + tmbId: rootUser.tmbId, + appId: testApp._id, + name: 'Playground Chat', + type: PublishChannelEnum.playground, + showNodeStatus: true, + responseDetail: true, + showFullText: true, + showRawSource: true, + usagePoints: 0, + lastTime: new Date() + }); + + // Update config for second app + const updateData: UpdatePlaygroundVisibilityConfigBody = { + appId: testApp2._id, + showNodeStatus: false, + responseDetail: false, + showFullText: true, + showRawSource: true + }; + + const res = await Call(updateApi.default, { + auth: rootUser, + body: updateData + }); + + // Check if the request was processed successfully + if (res.code === 200) { + expect(res.error).toBeUndefined(); + + // Verify first app config is unchanged + const config1 = await MongoOutLink.findOne({ + appId: testApp._id, + type: PublishChannelEnum.playground + }).lean(); + + if (config1) { + expect(config1.showNodeStatus).toBe(true); + expect(config1.responseDetail).toBe(true); + } + + // Verify second app config was created with new values + const config2 = await MongoOutLink.findOne({ + appId: testApp2._id, + type: PublishChannelEnum.playground + }).lean(); + + if (config2) { + expect(config2.showNodeStatus).toBe(false); + expect(config2.responseDetail).toBe(false); + expect(config2.showFullText).toBe(true); + expect(config2.showRawSource).toBe(true); + } + } else { + // If there are permission issues, we still expect the API to validate parameters + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + } + + // Cleanup second app + await MongoOutLink.deleteOne({ appId: testApp2._id }); + await MongoApp.deleteOne({ _id: testApp2._id }); + }); +});