mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-07 01:02:55 +08:00
fix: getquote api auth error (#6792)
* deploy doc * perf: completions api support null * fix: getquote api auth error
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
@@ -70,33 +70,29 @@ export type DeleteDatasetDataQuery = z.infer<typeof DeleteDatasetDataQuerySchema
|
||||
* API: 获取引用数据
|
||||
* Route: POST /api/core/dataset/data/getQuoteData
|
||||
* ============================================================================ */
|
||||
export const GetQuoteDataBodySchema = z.union([
|
||||
z.object({
|
||||
id: ObjectIdSchema.meta({
|
||||
example: '68ad85a7463006c963799a05',
|
||||
description: '数据 ID'
|
||||
})
|
||||
export const GetQuoteDataBodySchema = OutLinkChatAuthSchema.extend({
|
||||
id: ObjectIdSchema.meta({
|
||||
example: '68ad85a7463006c963799a05',
|
||||
description: '数据 ID'
|
||||
}),
|
||||
OutLinkChatAuthSchema.extend({
|
||||
id: ObjectIdSchema.meta({
|
||||
example: '68ad85a7463006c963799a05',
|
||||
description: '数据 ID'
|
||||
}),
|
||||
// 对话模式下的额外字段(可选)
|
||||
appId: ObjectIdSchema.meta({
|
||||
example: '68ad85a7463006c963799a10',
|
||||
description: '应用 ID(对话模式必填)'
|
||||
}),
|
||||
chatId: z.string().meta({
|
||||
example: '68ad85a7463006c963799a11',
|
||||
description: '对话 ID(对话模式必填)'
|
||||
}),
|
||||
chatItemDataId: z.string().meta({
|
||||
example: '68ad85a7463006c963799a12',
|
||||
description: '对话条目数据 ID(对话模式必填)'
|
||||
})
|
||||
// 对话模式下的额外字段(三者必须同时提供,否则走 API 模式)
|
||||
appId: ObjectIdSchema.optional().meta({
|
||||
example: '68ad85a7463006c963799a10',
|
||||
description: '应用 ID(对话模式必填)'
|
||||
}),
|
||||
chatId: z.string().optional().meta({
|
||||
example: '68ad85a7463006c963799a11',
|
||||
description: '对话 ID(对话模式必填)'
|
||||
}),
|
||||
chatItemDataId: z.string().optional().meta({
|
||||
example: '68ad85a7463006c963799a12',
|
||||
description: '对话条目数据 ID(对话模式必填)'
|
||||
})
|
||||
]);
|
||||
}).refine(
|
||||
(d) =>
|
||||
(!!d.chatId && !!d.appId && !!d.chatItemDataId) || (!d.chatId && !d.appId && !d.chatItemDataId),
|
||||
{ message: '对话模式下 appId / chatId / chatItemDataId 必须同时提供' }
|
||||
);
|
||||
export type GetQuoteDataBody = z.infer<typeof GetQuoteDataBodySchema>;
|
||||
|
||||
export const GetQuoteDataResponseSchema = z.object({
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ async function handler(req: ApiRequestProps): Promise<GetQuoteDataResponse> {
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -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<string, any> = {}) => ({
|
||||
_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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user