From 518627ca2cffa3bbde599bdbf331fba03b073d8d Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 21 Apr 2026 23:00:10 +0800 Subject: [PATCH] fix: getquote api auth error (#6792) * deploy doc * perf: completions api support null * fix: getquote api auth error --- deploy/docker/cn/docker-compose.milvus.yml | 5 + deploy/docker/cn/docker-compose.oceanbase.yml | 5 + deploy/docker/cn/docker-compose.opengauss.yml | 5 + deploy/docker/cn/docker-compose.pg.yml | 5 + deploy/docker/cn/docker-compose.seekdb.yml | 5 + deploy/docker/cn/docker-compose.zilliz.yml | 5 + .../docker/global/docker-compose.milvus.yml | 5 + .../global/docker-compose.oceanbase.yml | 5 + .../global/docker-compose.opengauss.yml | 5 + deploy/docker/global/docker-compose.pg.yml | 5 + .../docker/global/docker-compose.seekdb.yml | 5 + .../docker/global/docker-compose.zilliz.yml | 5 + deploy/templates/docker-compose.prod.yml | 5 + .../docker/cn/docker-compose.milvus.yml | 5 + .../docker/cn/docker-compose.oceanbase.yml | 5 + .../docker/cn/docker-compose.opengauss.yml | 5 + .../deploy/docker/cn/docker-compose.pg.yml | 5 + .../docker/cn/docker-compose.seekdb.yml | 5 + .../docker/cn/docker-compose.zilliz.yml | 5 + .../docker/global/docker-compose.milvus.yml | 5 + .../global/docker-compose.oceanbase.yml | 5 + .../global/docker-compose.opengauss.yml | 5 + .../docker/global/docker-compose.pg.yml | 5 + .../docker/global/docker-compose.seekdb.yml | 5 + .../docker/global/docker-compose.zilliz.yml | 5 + .../openapi/core/chat/completion/api.ts | 26 +- .../global/openapi/core/dataset/data/api.ts | 46 ++- projects/app/next.config.ts | 11 +- .../api/core/dataset/data/getQuoteData.ts | 2 +- .../core/dataset/data/getQuoteData.test.ts | 304 ++++++++++++++++++ 30 files changed, 474 insertions(+), 40 deletions(-) create mode 100644 projects/app/test/api/core/dataset/data/getQuoteData.test.ts diff --git a/deploy/docker/cn/docker-compose.milvus.yml b/deploy/docker/cn/docker-compose.milvus.yml index 0d6912d98c..0fdd7cd477 100644 --- a/deploy/docker/cn/docker-compose.milvus.yml +++ b/deploy/docker/cn/docker-compose.milvus.yml @@ -497,10 +497,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/cn/docker-compose.oceanbase.yml b/deploy/docker/cn/docker-compose.oceanbase.yml index b04910ef8c..bef660b450 100644 --- a/deploy/docker/cn/docker-compose.oceanbase.yml +++ b/deploy/docker/cn/docker-compose.oceanbase.yml @@ -475,10 +475,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/cn/docker-compose.opengauss.yml b/deploy/docker/cn/docker-compose.opengauss.yml index 4f39fef45b..5842abd415 100644 --- a/deploy/docker/cn/docker-compose.opengauss.yml +++ b/deploy/docker/cn/docker-compose.opengauss.yml @@ -457,10 +457,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/cn/docker-compose.pg.yml b/deploy/docker/cn/docker-compose.pg.yml index 16f3ffd1ad..ce928f2e00 100644 --- a/deploy/docker/cn/docker-compose.pg.yml +++ b/deploy/docker/cn/docker-compose.pg.yml @@ -456,10 +456,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/cn/docker-compose.seekdb.yml b/deploy/docker/cn/docker-compose.seekdb.yml index 9831ce9aff..84cdac605d 100644 --- a/deploy/docker/cn/docker-compose.seekdb.yml +++ b/deploy/docker/cn/docker-compose.seekdb.yml @@ -462,10 +462,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/cn/docker-compose.zilliz.yml b/deploy/docker/cn/docker-compose.zilliz.yml index b6f1c85d5f..ee10156d36 100644 --- a/deploy/docker/cn/docker-compose.zilliz.yml +++ b/deploy/docker/cn/docker-compose.zilliz.yml @@ -439,10 +439,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/global/docker-compose.milvus.yml b/deploy/docker/global/docker-compose.milvus.yml index b6600f4bb6..9be12726b0 100644 --- a/deploy/docker/global/docker-compose.milvus.yml +++ b/deploy/docker/global/docker-compose.milvus.yml @@ -497,10 +497,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/global/docker-compose.oceanbase.yml b/deploy/docker/global/docker-compose.oceanbase.yml index 7ef26594ee..82f4d58832 100644 --- a/deploy/docker/global/docker-compose.oceanbase.yml +++ b/deploy/docker/global/docker-compose.oceanbase.yml @@ -475,10 +475,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/global/docker-compose.opengauss.yml b/deploy/docker/global/docker-compose.opengauss.yml index d4a34014c0..e9657dee91 100644 --- a/deploy/docker/global/docker-compose.opengauss.yml +++ b/deploy/docker/global/docker-compose.opengauss.yml @@ -457,10 +457,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/global/docker-compose.pg.yml b/deploy/docker/global/docker-compose.pg.yml index ba43157648..ce149eb024 100644 --- a/deploy/docker/global/docker-compose.pg.yml +++ b/deploy/docker/global/docker-compose.pg.yml @@ -456,10 +456,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/global/docker-compose.seekdb.yml b/deploy/docker/global/docker-compose.seekdb.yml index c62b16b61d..d2a9274f9c 100644 --- a/deploy/docker/global/docker-compose.seekdb.yml +++ b/deploy/docker/global/docker-compose.seekdb.yml @@ -462,10 +462,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/docker/global/docker-compose.zilliz.yml b/deploy/docker/global/docker-compose.zilliz.yml index c66f0a2f4f..5bca5ee3e4 100644 --- a/deploy/docker/global/docker-compose.zilliz.yml +++ b/deploy/docker/global/docker-compose.zilliz.yml @@ -439,10 +439,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/deploy/templates/docker-compose.prod.yml b/deploy/templates/docker-compose.prod.yml index 8d57205069..ecf82560b6 100644 --- a/deploy/templates/docker-compose.prod.yml +++ b/deploy/templates/docker-compose.prod.yml @@ -438,10 +438,15 @@ ${{vec.db}} retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/cn/docker-compose.milvus.yml b/document/public/deploy/docker/cn/docker-compose.milvus.yml index 0d6912d98c..0fdd7cd477 100644 --- a/document/public/deploy/docker/cn/docker-compose.milvus.yml +++ b/document/public/deploy/docker/cn/docker-compose.milvus.yml @@ -497,10 +497,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/cn/docker-compose.oceanbase.yml b/document/public/deploy/docker/cn/docker-compose.oceanbase.yml index b04910ef8c..bef660b450 100644 --- a/document/public/deploy/docker/cn/docker-compose.oceanbase.yml +++ b/document/public/deploy/docker/cn/docker-compose.oceanbase.yml @@ -475,10 +475,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/cn/docker-compose.opengauss.yml b/document/public/deploy/docker/cn/docker-compose.opengauss.yml index 4f39fef45b..5842abd415 100644 --- a/document/public/deploy/docker/cn/docker-compose.opengauss.yml +++ b/document/public/deploy/docker/cn/docker-compose.opengauss.yml @@ -457,10 +457,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/cn/docker-compose.pg.yml b/document/public/deploy/docker/cn/docker-compose.pg.yml index 16f3ffd1ad..ce928f2e00 100644 --- a/document/public/deploy/docker/cn/docker-compose.pg.yml +++ b/document/public/deploy/docker/cn/docker-compose.pg.yml @@ -456,10 +456,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/cn/docker-compose.seekdb.yml b/document/public/deploy/docker/cn/docker-compose.seekdb.yml index 9831ce9aff..84cdac605d 100644 --- a/document/public/deploy/docker/cn/docker-compose.seekdb.yml +++ b/document/public/deploy/docker/cn/docker-compose.seekdb.yml @@ -462,10 +462,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/cn/docker-compose.zilliz.yml b/document/public/deploy/docker/cn/docker-compose.zilliz.yml index b6f1c85d5f..ee10156d36 100644 --- a/document/public/deploy/docker/cn/docker-compose.zilliz.yml +++ b/document/public/deploy/docker/cn/docker-compose.zilliz.yml @@ -439,10 +439,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/global/docker-compose.milvus.yml b/document/public/deploy/docker/global/docker-compose.milvus.yml index b6600f4bb6..9be12726b0 100644 --- a/document/public/deploy/docker/global/docker-compose.milvus.yml +++ b/document/public/deploy/docker/global/docker-compose.milvus.yml @@ -497,10 +497,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/global/docker-compose.oceanbase.yml b/document/public/deploy/docker/global/docker-compose.oceanbase.yml index 7ef26594ee..82f4d58832 100644 --- a/document/public/deploy/docker/global/docker-compose.oceanbase.yml +++ b/document/public/deploy/docker/global/docker-compose.oceanbase.yml @@ -475,10 +475,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/global/docker-compose.opengauss.yml b/document/public/deploy/docker/global/docker-compose.opengauss.yml index d4a34014c0..e9657dee91 100644 --- a/document/public/deploy/docker/global/docker-compose.opengauss.yml +++ b/document/public/deploy/docker/global/docker-compose.opengauss.yml @@ -457,10 +457,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/global/docker-compose.pg.yml b/document/public/deploy/docker/global/docker-compose.pg.yml index ba43157648..ce149eb024 100644 --- a/document/public/deploy/docker/global/docker-compose.pg.yml +++ b/document/public/deploy/docker/global/docker-compose.pg.yml @@ -456,10 +456,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/global/docker-compose.seekdb.yml b/document/public/deploy/docker/global/docker-compose.seekdb.yml index c62b16b61d..d2a9274f9c 100644 --- a/document/public/deploy/docker/global/docker-compose.seekdb.yml +++ b/document/public/deploy/docker/global/docker-compose.seekdb.yml @@ -462,10 +462,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/document/public/deploy/docker/global/docker-compose.zilliz.yml b/document/public/deploy/docker/global/docker-compose.zilliz.yml index c66f0a2f4f..5bca5ee3e4 100644 --- a/document/public/deploy/docker/global/docker-compose.zilliz.yml +++ b/document/public/deploy/docker/global/docker-compose.zilliz.yml @@ -439,10 +439,15 @@ services: retries: 10 networks: data: + name: fastgpt_data app: + name: fastgpt_app codesandbox: + name: fastgpt_codesandbox opensandbox: + name: fastgpt_opensandbox aiproxy: + name: fastgpt_aiproxy volumes: fastgpt-pg: diff --git a/packages/global/openapi/core/chat/completion/api.ts b/packages/global/openapi/core/chat/completion/api.ts index 2d9bc1db53..d9ed35a327 100644 --- a/packages/global/openapi/core/chat/completion/api.ts +++ b/packages/global/openapi/core/chat/completion/api.ts @@ -12,19 +12,19 @@ const WebCompletionsSchema = z.object({ chatId: z .string() .max(1024) - .optional() + .nullish() .meta({ description: '聊天ID, 传入的话会自动获取历史记录,不传入则认为是新对话' }), - appId: ObjectIdSchema.optional(), - customUid: z.string().max(1024).optional().meta({ description: '自定义用户ID(分享链接)' }), - metadata: z.record(z.string(), z.any()).optional().meta({ description: '元数据' }) + appId: ObjectIdSchema.nullish(), + customUid: z.string().max(1024).nullish().meta({ description: '自定义用户ID(分享链接)' }), + metadata: z.record(z.string(), z.any()).nullish().meta({ description: '元数据' }) }); // completions 接口实际上并没有用完所有字段,所以这里就取局部即可 const ChatCompletionCreateParamsSchema = z.object({ - messages: z.array(ChatCompletionMessageParamSchema).optional().default([]).meta({ + messages: z.array(ChatCompletionMessageParamSchema).nullish().default([]).meta({ description: '消息列表' }), - stream: z.boolean().optional().default(false).meta({ + stream: z.boolean().nullish().default(false).meta({ description: '是否流式返回' }) }); @@ -32,23 +32,23 @@ const ChatCompletionCreateParamsSchema = z.object({ export const CompletionsPropsSchema = OutLinkChatAuthSchema.extend(WebCompletionsSchema.shape) .extend(ChatCompletionCreateParamsSchema.shape) .extend({ - variables: z.record(z.string(), z.any()).optional().default({}).meta({ + variables: z.record(z.string(), z.any()).nullish().default({}).meta({ description: '全局变量或插件输入' }), responseChatItemId: z .string() - .optional() + .nullish() .default(() => getNanoid()) .meta({ description: '自定义响应的 assistant 的消息 ID,如果不传入,则自动生成一个' }), - detail: z.boolean().optional().default(false).meta({ + detail: z.boolean().nullish().default(false).meta({ description: '是否返回详细信息,包括 reasoning_content, tool_calls, usage 等' }), - retainDatasetCite: z.boolean().optional().default(false).meta({ + retainDatasetCite: z.boolean().nullish().default(false).meta({ description: '是否保留数据集引用' }), - showSkillReferences: z.boolean().optional().default(false).meta({ + showSkillReferences: z.boolean().nullish().default(false).meta({ description: '是否显示技能引用' }) }); @@ -129,12 +129,12 @@ export const ChatTestPropsSchema = z.object({ messages: z.array(ChatCompletionMessageParamSchema).meta({ description: '消息列表' }), responseChatItemId: z .string() - .optional() + .nullish() .meta({ description: '自定义响应的 assistant 的消息 ID,如果不传入,则自动生成一个' }), nodes: z.array(OpenAPIStoreNodeItemTypeSchema).meta({ description: '节点列表' }), edges: z.array(StoreEdgeItemTypeSchema).meta({ description: '边列表' }), chatConfig: AppChatConfigTypeSchema.meta({ description: '聊天配置' }), - variables: z.record(z.string(), z.any()).optional().default({}).meta({ + variables: z.record(z.string(), z.any()).nullish().default({}).meta({ description: '全局变量或插件输入' }), appId: ObjectIdSchema.meta({ description: '应用ID' }), diff --git a/packages/global/openapi/core/dataset/data/api.ts b/packages/global/openapi/core/dataset/data/api.ts index 2a6b32cf8d..67af86712b 100644 --- a/packages/global/openapi/core/dataset/data/api.ts +++ b/packages/global/openapi/core/dataset/data/api.ts @@ -70,33 +70,29 @@ export type DeleteDatasetDataQuery = z.infer + (!!d.chatId && !!d.appId && !!d.chatItemDataId) || (!d.chatId && !d.appId && !d.chatItemDataId), + { message: '对话模式下 appId / chatId / chatItemDataId 必须同时提供' } +); export type GetQuoteDataBody = z.infer; export const GetQuoteDataResponseSchema = z.object({ diff --git a/projects/app/next.config.ts b/projects/app/next.config.ts index 667a385f65..681fdb0225 100644 --- a/projects/app/next.config.ts +++ b/projects/app/next.config.ts @@ -136,8 +136,17 @@ const nextConfig: NextConfig = { } if (isServer) { + // 这些包只在服务端运行,且内部使用动态 import / 原生可选依赖(ws 的 bufferutil、 + // utf-8-validate,pi-ai 的 node:os/provider dynamicImport 等),让 webpack 直接 + // externalize,避免扫描源码产生 Critical dependency / Module not found 警告。 config.externals.push({ - '@node-rs/jieba': '@node-rs/jieba' + '@node-rs/jieba': '@node-rs/jieba', + '@mariozechner/pi-ai': 'commonjs @mariozechner/pi-ai', + '@mariozechner/pi-agent-core': 'commonjs @mariozechner/pi-agent-core', + '@google/genai': 'commonjs @google/genai', + ws: 'commonjs ws', + bufferutil: 'commonjs bufferutil', + 'utf-8-validate': 'commonjs utf-8-validate' }); } diff --git a/projects/app/src/pages/api/core/dataset/data/getQuoteData.ts b/projects/app/src/pages/api/core/dataset/data/getQuoteData.ts index 4b8b8bfd02..f88b80beb3 100644 --- a/projects/app/src/pages/api/core/dataset/data/getQuoteData.ts +++ b/projects/app/src/pages/api/core/dataset/data/getQuoteData.ts @@ -21,7 +21,7 @@ async function handler(req: ApiRequestProps): Promise { // Auth const { collection, q, a } = await (async () => { - if ('chatId' in body) { + if (body.chatId && body.appId && body.chatItemDataId) { const { appId, chatId, shareId, outLinkUid, teamId, teamToken, chatItemDataId } = body; await authChatCrud({ req, diff --git a/projects/app/test/api/core/dataset/data/getQuoteData.test.ts b/projects/app/test/api/core/dataset/data/getQuoteData.test.ts new file mode 100644 index 0000000000..93ad7cef06 --- /dev/null +++ b/projects/app/test/api/core/dataset/data/getQuoteData.test.ts @@ -0,0 +1,304 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { GetQuoteDataBodySchema } from '@fastgpt/global/openapi/core/dataset/data/api'; + +const VALID_ID = '68ad85a7463006c963799a05'; +const VALID_APP_ID = '68ad85a7463006c963799a10'; + +/** + * 回归测试:历史上 GetQuoteDataBodySchema 使用 z.union([简单, 复杂]), + * 由于 z.object 默认会剥离未知字段 + union 按顺序匹配第一个成功分支, + * 导致当请求体包含 appId/chatId/shareId 等字段时被静默丢弃,走到错误的鉴权分支。 + * 本套 schema 测试锁死:"对话模式字段必须被完整保留"。 + */ +describe('GetQuoteDataBodySchema', () => { + describe('API 模式(仅 id)', () => { + it('should parse with only id', () => { + const result = GetQuoteDataBodySchema.parse({ id: VALID_ID }); + expect(result.id).toBe(VALID_ID); + expect(result.appId).toBeUndefined(); + expect(result.chatId).toBeUndefined(); + expect(result.chatItemDataId).toBeUndefined(); + expect(result.shareId).toBeUndefined(); + }); + + it('should reject invalid ObjectId', () => { + expect(() => GetQuoteDataBodySchema.parse({ id: 'not-an-object-id' })).toThrow(); + }); + + it('should reject when id is missing', () => { + expect(() => GetQuoteDataBodySchema.parse({})).toThrow(); + }); + }); + + describe('对话模式(id + appId + chatId + chatItemDataId)', () => { + it('should preserve all chat fields', () => { + const body = { + id: VALID_ID, + appId: VALID_APP_ID, + chatId: 'chat_123', + chatItemDataId: 'item_456' + }; + const result = GetQuoteDataBodySchema.parse(body); + expect(result.id).toBe(VALID_ID); + expect(result.appId).toBe(VALID_APP_ID); + expect(result.chatId).toBe('chat_123'); + expect(result.chatItemDataId).toBe('item_456'); + }); + + // 这是核心回归用例 —— 以前 shareId/outLinkUid 会被静默剥离 + it('should preserve shareId / outLinkUid in chat mode', () => { + const body = { + id: VALID_ID, + appId: VALID_APP_ID, + chatId: 'chat_123', + chatItemDataId: 'item_456', + shareId: 'share_abc', + outLinkUid: 'uid_xyz' + }; + const result = GetQuoteDataBodySchema.parse(body); + expect(result.shareId).toBe('share_abc'); + expect(result.outLinkUid).toBe('uid_xyz'); + }); + + it('should preserve teamId / teamToken in chat mode', () => { + const body = { + id: VALID_ID, + appId: VALID_APP_ID, + chatId: 'chat_123', + chatItemDataId: 'item_456', + teamId: 'team_abc', + teamToken: 'token_xyz' + }; + const result = GetQuoteDataBodySchema.parse(body); + expect(result.teamId).toBe('team_abc'); + expect(result.teamToken).toBe('token_xyz'); + }); + }); + + describe('对话模式字段一致性校验(refine)', () => { + it('should reject when only chatId is provided', () => { + expect(() => GetQuoteDataBodySchema.parse({ id: VALID_ID, chatId: 'chat_123' })).toThrow(); + }); + + it('should reject when only appId is provided', () => { + expect(() => GetQuoteDataBodySchema.parse({ id: VALID_ID, appId: VALID_APP_ID })).toThrow(); + }); + + it('should reject when only chatItemDataId is provided', () => { + expect(() => + GetQuoteDataBodySchema.parse({ id: VALID_ID, chatItemDataId: 'item_456' }) + ).toThrow(); + }); + + it('should reject missing chatItemDataId when appId + chatId present', () => { + expect(() => + GetQuoteDataBodySchema.parse({ + id: VALID_ID, + appId: VALID_APP_ID, + chatId: 'chat_123' + }) + ).toThrow(); + }); + }); +}); + +/** + * Handler 分支测试:mock 所有数据层依赖,确认 + * - 对话模式:走 authChatCrud / authCollectionInChat + * - API 模式:走 authDatasetData + * 并且字段会完整传给下游鉴权函数(防止上游 schema 剥字段后神不知鬼不觉)。 + */ +const authChatCrudMock = vi.fn(); +const authCollectionInChatMock = vi.fn(); +const authDatasetDataMock = vi.fn(); +const findByIdDatasetDataMock = vi.fn(); +const findByIdCollectionMock = vi.fn(); +const formatDatasetDataValueMock = vi.fn(); + +vi.mock('@/service/support/permission/auth/chat', () => ({ + authChatCrud: (props: any) => authChatCrudMock(props), + authCollectionInChat: (props: any) => authCollectionInChatMock(props) +})); + +vi.mock('@fastgpt/service/support/permission/dataset/auth', () => ({ + authDatasetData: (props: any) => authDatasetDataMock(props) +})); + +vi.mock('@fastgpt/service/core/dataset/data/schema', () => ({ + MongoDatasetData: { + findById: (id: string) => ({ lean: () => findByIdDatasetDataMock(id) }) + } +})); + +vi.mock('@fastgpt/service/core/dataset/collection/schema', () => ({ + MongoDatasetCollection: { + findById: (id: string) => ({ lean: () => findByIdCollectionMock(id) }) + } +})); + +vi.mock('@fastgpt/service/core/dataset/data/controller', () => ({ + formatDatasetDataValue: (props: any) => formatDatasetDataValueMock(props) +})); + +import handler from '@/pages/api/core/dataset/data/getQuoteData'; +import { Call } from '@test/utils/request'; + +const makeCollection = (overrides: Record = {}) => ({ + _id: '68ad85a7463006c963799a06', + teamId: '68ad85a7463006c963799a07', + tmbId: '68ad85a7463006c963799a08', + datasetId: '68ad85a7463006c963799a09', + parentId: null, + name: 'col', + type: 'file', + createTime: new Date(), + updateTime: new Date(), + ...overrides +}); + +describe('getQuoteData handler', () => { + beforeEach(() => { + vi.clearAllMocks(); + formatDatasetDataValueMock.mockImplementation(({ q, a }: any) => ({ q, a })); + }); + + it('chat mode: forwards shareId / outLinkUid / teamId / teamToken to authChatCrud', async () => { + findByIdDatasetDataMock.mockResolvedValue({ + _id: VALID_ID, + collectionId: 'col_1', + q: 'Q', + a: 'A', + imageId: undefined + }); + findByIdCollectionMock.mockResolvedValue(makeCollection()); + authChatCrudMock.mockResolvedValue({ showCite: true }); + authCollectionInChatMock.mockResolvedValue(undefined); + + const res = await Call(handler, { + body: { + id: VALID_ID, + appId: VALID_APP_ID, + chatId: 'chat_123', + chatItemDataId: 'item_456', + shareId: 'share_abc', + outLinkUid: 'uid_xyz', + teamId: 'team_abc', + teamToken: 'token_xyz' + } + }); + + expect(res.code).toBe(200); + // 核心断言:对话模式字段被完整透传给鉴权层 + expect(authChatCrudMock).toHaveBeenCalled(); + const call = authChatCrudMock.mock.calls[0][0]; + expect(call.appId).toBe(VALID_APP_ID); + expect(call.chatId).toBe('chat_123'); + expect(call.shareId).toBe('share_abc'); + expect(call.outLinkUid).toBe('uid_xyz'); + expect(call.teamId).toBe('team_abc'); + expect(call.teamToken).toBe('token_xyz'); + + expect(authCollectionInChatMock).toHaveBeenCalledWith({ + appId: VALID_APP_ID, + chatId: 'chat_123', + chatItemDataId: 'item_456', + collectionIds: ['col_1'] + }); + expect(authDatasetDataMock).not.toHaveBeenCalled(); + }); + + it('chat mode: rejects when showCite is false', async () => { + findByIdDatasetDataMock.mockResolvedValue({ + _id: VALID_ID, + collectionId: 'col_1', + q: 'Q', + a: 'A' + }); + findByIdCollectionMock.mockResolvedValue(makeCollection()); + authChatCrudMock.mockResolvedValue({ showCite: false }); + authCollectionInChatMock.mockResolvedValue(undefined); + + const res = await Call(handler, { + body: { + id: VALID_ID, + appId: VALID_APP_ID, + chatId: 'chat_123', + chatItemDataId: 'item_456' + } + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('chat mode: rejects when dataset data not found', async () => { + findByIdDatasetDataMock.mockResolvedValue(null); + authChatCrudMock.mockResolvedValue({ showCite: true }); + + const res = await Call(handler, { + body: { + id: VALID_ID, + appId: VALID_APP_ID, + chatId: 'chat_123', + chatItemDataId: 'item_456' + } + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('chat mode: rejects when collection not found', async () => { + findByIdDatasetDataMock.mockResolvedValue({ + _id: VALID_ID, + collectionId: 'col_1', + q: 'Q', + a: 'A' + }); + findByIdCollectionMock.mockResolvedValue(null); + authChatCrudMock.mockResolvedValue({ showCite: true }); + authCollectionInChatMock.mockResolvedValue(undefined); + + const res = await Call(handler, { + body: { + id: VALID_ID, + appId: VALID_APP_ID, + chatId: 'chat_123', + chatItemDataId: 'item_456' + } + }); + + expect(res.code).toBe(500); + expect(res.error).toBeDefined(); + }); + + it('api mode: goes to authDatasetData when only id is provided', async () => { + authDatasetDataMock.mockResolvedValue({ + datasetData: { q: 'Q', a: 'A', imageId: undefined }, + collection: makeCollection() + }); + + const res = await Call(handler, { + body: { id: VALID_ID } + }); + + expect(res.code).toBe(200); + expect(authDatasetDataMock).toHaveBeenCalledWith(expect.objectContaining({ dataId: VALID_ID })); + expect(authChatCrudMock).not.toHaveBeenCalled(); + expect(authCollectionInChatMock).not.toHaveBeenCalled(); + }); + + it('returns schema-validated response payload', async () => { + authDatasetDataMock.mockResolvedValue({ + datasetData: { q: 'Hello', a: 'World', imageId: undefined }, + collection: makeCollection({ name: 'my-col' }) + }); + + const res = await Call(handler, { body: { id: VALID_ID } }); + + expect(res.code).toBe(200); + expect(res.data?.q).toBe('Hello'); + expect(res.data?.a).toBe('World'); + expect(res.data?.collection?.name).toBe('my-col'); + }); +});