test & openapi

This commit is contained in:
heheer 2025-12-18 14:55:29 +08:00
parent c75294f56e
commit 2901ad548a
No known key found for this signature in database
GPG Key ID: 37DCB43201661540
15 changed files with 725 additions and 23 deletions

View File

@ -1,6 +1,8 @@
import type { OpenAPIPath } from '../../type';
import { AppLogPath } from './log';
import { PublishChannelPath } from './publishChannel';
export const AppPath: OpenAPIPath = {
...AppLogPath
...AppLogPath,
...PublishChannelPath
};

View File

@ -0,0 +1,5 @@
import { PlaygroundPath } from './playground';
export const PublishChannelPath = {
...PlaygroundPath
};

View File

@ -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
>;

View File

@ -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()
})
}
}
}
}
}
}
};

View File

@ -26,7 +26,7 @@ export const openAPIDocument = createDocument({
'x-tagGroups': [
{
name: 'Agent 应用',
tags: [TagsMap.appLog]
tags: [TagsMap.appLog, TagsMap.publishChannel]
},
{
name: '对话管理',

View File

@ -13,6 +13,9 @@ export const TagsMap = {
pluginToolTag: '工具标签',
pluginTeam: '团队插件管理',
// Publish Channel
publishChannel: '发布渠道',
/* Support */
// Wallet
walletBill: '订单',

View File

@ -108,16 +108,11 @@ export type OutLinkEditType<T = undefined> = {
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<typeof PlaygroundVisibilityConfigSchema>;

View File

@ -259,7 +259,7 @@ const ChatItem = (props: Props) => {
setCiteModalData({
rawSearch: quoteList,
metadata:
item?.collectionId && (isShowReadRawSource || isShowFullText)
item?.collectionId && isShowFullText
? {
appId: appId,
chatId: chatId,

View File

@ -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
})

View File

@ -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<ResponseType>
): Promise<ResponseType> {
await authCert({ req, authRoot: true });
const queue = getQueue<S3MQJobData>(QueueNames.s3FileDelete);
// Get all failed jobs and retry them
// 1. 处理失败的 S3 删除任务
const queue = getQueue<S3MQJobData>(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
};
}

View File

@ -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);
}

View File

@ -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<PlaygroundVisibilityConfigResponse> {
const { appId } = PlaygroundVisibilityConfigQuerySchema.parse(req.query);
await authApp({
req,
authToken: true,
appId,
per: WritePermissionVal
});
const existingConfig = await MongoOutLink.findOne(
{
appId,

View File

@ -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<UpdatePlaygroundVisibilityConfigBody
req,
authToken: true,
appId,
per: ManagePermissionVal
per: WritePermissionVal
});
await MongoOutLink.updateOne(

View File

@ -0,0 +1,175 @@
import type { PlaygroundVisibilityConfigResponse } 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 configApi from '@/pages/api/support/outLink/playground/config';
describe('Playground Visibility Config 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 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<PlaygroundVisibilityConfigResponse>(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<PlaygroundVisibilityConfigResponse>(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<PlaygroundVisibilityConfigResponse>(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<PlaygroundVisibilityConfigResponse>(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<PlaygroundVisibilityConfigResponse>(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<PlaygroundVisibilityConfigResponse>(configApi.default, {
query: {
appId: testApp._id
}
});
expect(res.code).toBe(500);
expect(res.error).toBeDefined();
});
});

View File

@ -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 });
});
});