mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-08 01:08:43 +08:00
fix: parent datasetId type;fix: v1 completions; (#6775)
* fix: parent datasetId type * doc * fix(chat): keep stream resume out of v1 completions (#6774) * fix(chat): avoid duplicate v1 completion history items * fix(chat): restore v1 completion persistence flow * fix(chat): keep stream resume out of v1 completions * fix(chat): revert pushChatRecords append flow * Mobile UI (#6776) * doc * perf: review * fix: review --------- Co-authored-by: Ryo <whoeverimf5@gmail.com> Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: 'V4.14.12'
|
||||
description: 'FastGPT V4.14.12 更新说明'
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 🐛 修复
|
||||
|
||||
1. 知识库三级目录 path 接口报 zod 校验出错。
|
||||
2. v1/completions 接口 dataId 异常,导致 api 调用时候,对话日志里无法获取到运行详情。
|
||||
|
||||
## 🚀 新增内容
|
||||
|
||||
1. 响应值允许自定义 HttpStatus 状态码。
|
||||
2. Agent 调度器支持 PI Agent 模式(beta功能)。
|
||||
|
||||
## ⚙️ 优化
|
||||
|
||||
1. skill 接口错误处理。
|
||||
|
||||
@@ -115,6 +115,7 @@ description: FastGPT 文档目录
|
||||
- [/docs/self-host/upgrading/4-14/4141](/docs/self-host/upgrading/4-14/4141)
|
||||
- [/docs/self-host/upgrading/4-14/41410](/docs/self-host/upgrading/4-14/41410)
|
||||
- [/docs/self-host/upgrading/4-14/41411](/docs/self-host/upgrading/4-14/41411)
|
||||
- [/docs/self-host/upgrading/4-14/41412](/docs/self-host/upgrading/4-14/41412)
|
||||
- [/docs/self-host/upgrading/4-14/4142](/docs/self-host/upgrading/4-14/4142)
|
||||
- [/docs/self-host/upgrading/4-14/4143](/docs/self-host/upgrading/4-14/4143)
|
||||
- [/docs/self-host/upgrading/4-14/4144](/docs/self-host/upgrading/4-14/4144)
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
"document/content/docs/self-host/upgrading/4-14/41410.en.mdx": "2026-03-31T23:15:29+08:00",
|
||||
"document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-04-18T20:47:39+08:00",
|
||||
"document/content/docs/self-host/upgrading/4-14/41411.mdx": "2026-04-18T20:47:39+08:00",
|
||||
"document/content/docs/self-host/upgrading/4-14/41412.mdx": "2026-04-20T15:54:50+08:00",
|
||||
"document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00",
|
||||
"document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00",
|
||||
"document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00",
|
||||
@@ -384,9 +385,9 @@
|
||||
"document/content/docs/self-host/upgrading/outdated/499.en.mdx": "2026-03-03T17:39:47+08:00",
|
||||
"document/content/docs/self-host/upgrading/outdated/499.mdx": "2026-03-03T17:39:47+08:00",
|
||||
"document/content/docs/self-host/upgrading/upgrade-intruction.en.mdx": "2026-03-03T17:39:47+08:00",
|
||||
"document/content/docs/self-host/upgrading/upgrade-intruction.mdx": "2026-03-03T17:39:47+08:00",
|
||||
"document/content/docs/self-host/upgrading/upgrade-intruction.mdx": "2026-04-20T13:51:34+08:00",
|
||||
"document/content/docs/toc.en.mdx": "2026-04-17T23:28:43+08:00",
|
||||
"document/content/docs/toc.mdx": "2026-04-17T23:28:43+08:00",
|
||||
"document/content/docs/toc.mdx": "2026-04-20T15:24:07+08:00",
|
||||
"document/content/docs/use-cases/app-cases/dalle3.en.mdx": "2026-02-26T22:14:30+08:00",
|
||||
"document/content/docs/use-cases/app-cases/dalle3.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-02-26T22:14:30+08:00",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import z from 'zod';
|
||||
|
||||
export const ParentIdSchema = z.string().nullish();
|
||||
export const ParentIdSchema = z
|
||||
.preprocess(
|
||||
(value) => (value !== null && typeof value === 'object' ? String(value) : value),
|
||||
z.string().regex(/^([0-9a-fA-F]{24})?$/)
|
||||
)
|
||||
.nullish();
|
||||
export type ParentIdType = string | null | undefined;
|
||||
|
||||
export const GetPathPropsSchema = z.object({
|
||||
|
||||
@@ -387,72 +387,6 @@ export const prepareChatRound = async (params: PrepareChatRoundParams) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const ensurePendingChatRoundItems = async (params: EnsurePendingChatRoundParams) => {
|
||||
const { chatId, appId, teamId, tmbId, responseChatItemId } = params;
|
||||
if (!chatId || chatId === 'NO_RECORD_HISTORIES') return;
|
||||
|
||||
const humanDataId = params.userContent.dataId ?? responseChatItemId;
|
||||
|
||||
const existingAi = await MongoChatItem.findOne({
|
||||
appId,
|
||||
chatId,
|
||||
dataId: responseChatItemId,
|
||||
obj: ChatRoleEnum.AI
|
||||
})
|
||||
.select('_id')
|
||||
.lean();
|
||||
|
||||
if (existingAi) return;
|
||||
|
||||
const userPayload: UserChatItemType & { dataId: string; obj: typeof ChatRoleEnum.Human } = {
|
||||
...params.userContent,
|
||||
dataId: humanDataId,
|
||||
obj: ChatRoleEnum.Human
|
||||
};
|
||||
|
||||
userPayload.value?.forEach((item) => {
|
||||
if ('file' in item && item.file?.key) {
|
||||
item.file.url = '';
|
||||
}
|
||||
});
|
||||
|
||||
const aiPlaceholder: AIChatItemType & { dataId: string } = {
|
||||
dataId: responseChatItemId,
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: []
|
||||
};
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
const upsertOpts = { session, upsert: true };
|
||||
await MongoChatItem.updateOne(
|
||||
{ appId, chatId, dataId: humanDataId, obj: ChatRoleEnum.Human },
|
||||
{
|
||||
$setOnInsert: {
|
||||
teamId,
|
||||
tmbId,
|
||||
chatId,
|
||||
appId,
|
||||
...userPayload
|
||||
}
|
||||
},
|
||||
upsertOpts
|
||||
);
|
||||
await MongoChatItem.updateOne(
|
||||
{ appId, chatId, dataId: responseChatItemId, obj: ChatRoleEnum.AI },
|
||||
{
|
||||
$setOnInsert: {
|
||||
teamId,
|
||||
tmbId,
|
||||
chatId,
|
||||
appId,
|
||||
...aiPlaceholder
|
||||
}
|
||||
},
|
||||
upsertOpts
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const finalizeChatRound = async (props: Props) => {
|
||||
beforeProcess(props);
|
||||
|
||||
@@ -771,96 +705,30 @@ export const pushChatRecords = async (props: Props) => {
|
||||
errorMsg
|
||||
});
|
||||
const processedContent = [userContent, aiResponse];
|
||||
const humanRoundDataId = (processedContent[0] as { dataId?: string }).dataId as string;
|
||||
const aiRoundDataId = (processedContent[1] as { dataId?: string }).dataId as string;
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
const humanExisting = await MongoChatItem.findOne({
|
||||
appId,
|
||||
chatId,
|
||||
dataId: humanRoundDataId,
|
||||
obj: ChatRoleEnum.Human
|
||||
}).session(session);
|
||||
const aiExisting = await MongoChatItem.findOne({
|
||||
appId,
|
||||
chatId,
|
||||
dataId: aiRoundDataId,
|
||||
obj: ChatRoleEnum.AI
|
||||
}).session(session);
|
||||
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi, dataId }] = await MongoChatItem.create(
|
||||
processedContent.map((item) => ({
|
||||
chatId,
|
||||
teamId,
|
||||
tmbId,
|
||||
appId,
|
||||
...item
|
||||
})),
|
||||
{ session, ordered: true, ...writePrimary }
|
||||
);
|
||||
|
||||
let chatItemIdHuman: unknown;
|
||||
let chatItemIdAi: unknown;
|
||||
let responseDataId = aiRoundDataId;
|
||||
|
||||
if (humanExisting && aiExisting) {
|
||||
await MongoChatItem.updateOne(
|
||||
{ _id: humanExisting._id },
|
||||
{
|
||||
$set: {
|
||||
...(processedContent[0] as Record<string, unknown>),
|
||||
obj: ChatRoleEnum.Human
|
||||
}
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
await MongoChatItem.updateOne(
|
||||
{ _id: aiExisting._id },
|
||||
{
|
||||
$set: {
|
||||
...(processedContent[1] as Record<string, unknown>),
|
||||
obj: ChatRoleEnum.AI
|
||||
}
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
|
||||
await MongoChatItemResponse.deleteMany(
|
||||
{ appId, chatId, chatItemDataId: aiRoundDataId },
|
||||
{ session }
|
||||
);
|
||||
|
||||
if (nodeResponses?.length) {
|
||||
await MongoChatItemResponse.create(
|
||||
nodeResponses.map((item) => ({
|
||||
teamId,
|
||||
appId,
|
||||
chatId,
|
||||
chatItemDataId: aiRoundDataId,
|
||||
data: item
|
||||
})),
|
||||
{ session, ordered: true }
|
||||
);
|
||||
}
|
||||
|
||||
chatItemIdHuman = humanExisting._id;
|
||||
chatItemIdAi = aiExisting._id;
|
||||
} else {
|
||||
const [humanCreated, aiCreated] = await MongoChatItem.create(
|
||||
processedContent.map((item) => ({
|
||||
chatId,
|
||||
if (nodeResponses) {
|
||||
await MongoChatItemResponse.create(
|
||||
nodeResponses.map((item) => ({
|
||||
teamId,
|
||||
tmbId,
|
||||
appId,
|
||||
...item
|
||||
chatId,
|
||||
chatItemDataId: dataId,
|
||||
data: item
|
||||
})),
|
||||
{ session, ordered: true }
|
||||
{ session, ordered: true, ...writePrimary }
|
||||
);
|
||||
chatItemIdHuman = humanCreated._id;
|
||||
chatItemIdAi = aiCreated._id;
|
||||
responseDataId = aiCreated.dataId;
|
||||
|
||||
if (nodeResponses) {
|
||||
await MongoChatItemResponse.create(
|
||||
nodeResponses.map((item) => ({
|
||||
teamId,
|
||||
appId,
|
||||
chatId,
|
||||
chatItemDataId: responseDataId,
|
||||
data: item
|
||||
})),
|
||||
{ session, ordered: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await MongoChat.updateOne(
|
||||
@@ -894,7 +762,8 @@ export const pushChatRecords = async (props: Props) => {
|
||||
},
|
||||
{
|
||||
session,
|
||||
upsert: true
|
||||
upsert: true,
|
||||
...writePrimary
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -217,34 +217,50 @@ const ModelTable = ({ permissionConfig = false }: { permissionConfig?: boolean }
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Flex>
|
||||
<HStack flexShrink={0}>
|
||||
<Box fontSize={'sm'} color={'myGray.900'}>
|
||||
<Flex flexDirection={'column'} h={'100%'} minW={0}>
|
||||
<Flex flexDirection={['column', 'row']} gap={[3, 0]} alignItems={['stretch', 'center']}>
|
||||
<Flex flexShrink={0} w={['100%', 'auto']} alignItems={'center'} gap={2}>
|
||||
<Box
|
||||
w={['84px', 'auto']}
|
||||
flexShrink={0}
|
||||
fontSize={'sm'}
|
||||
color={'myGray.900'}
|
||||
textAlign={'left'}
|
||||
>
|
||||
{t('common:model.provider')}
|
||||
</Box>
|
||||
<MySelect
|
||||
w={'200px'}
|
||||
bg={'myGray.50'}
|
||||
value={provider}
|
||||
onChange={setProvider}
|
||||
list={filterProviderList}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack flexShrink={0} ml={6}>
|
||||
<Box fontSize={'sm'} color={'myGray.900'}>
|
||||
<Box flex={1} minW={0} w={['100%', '200px']}>
|
||||
<MySelect
|
||||
w={'100%'}
|
||||
bg={'myGray.50'}
|
||||
value={provider}
|
||||
onChange={setProvider}
|
||||
list={filterProviderList}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex flexShrink={0} ml={[0, 6]} w={['100%', 'auto']} alignItems={'center'} gap={2}>
|
||||
<Box
|
||||
w={['84px', 'auto']}
|
||||
flexShrink={0}
|
||||
fontSize={'sm'}
|
||||
color={'myGray.900'}
|
||||
textAlign={'left'}
|
||||
>
|
||||
{t('common:model.model_type')}
|
||||
</Box>
|
||||
<MySelect
|
||||
w={'150px'}
|
||||
bg={'myGray.50'}
|
||||
value={modelType}
|
||||
onChange={setModelType}
|
||||
list={selectModelTypeList.current}
|
||||
/>
|
||||
</HStack>
|
||||
<Box flex={1} />
|
||||
<Box flex={'0 0 250px'}>
|
||||
<Box flex={1} minW={0} w={['100%', '150px']}>
|
||||
<MySelect
|
||||
w={'100%'}
|
||||
bg={'myGray.50'}
|
||||
value={modelType}
|
||||
onChange={setModelType}
|
||||
list={selectModelTypeList.current}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box flex={1} display={['none', 'block']} />
|
||||
<Box w={['100%', '250px']} flex={['none', '0 0 250px']}>
|
||||
<SearchInput
|
||||
bg={'myGray.50'}
|
||||
value={search}
|
||||
@@ -253,7 +269,15 @@ const ModelTable = ({ permissionConfig = false }: { permissionConfig?: boolean }
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<TableContainer mt={5} flex={'1 0 0'} h={0} overflowY={'auto'}>
|
||||
<TableContainer
|
||||
mt={5}
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
w={'100%'}
|
||||
maxW={'100%'}
|
||||
overflowY={'auto'}
|
||||
overflowX={'auto'}
|
||||
>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr color={'myGray.600'}>
|
||||
|
||||
@@ -20,7 +20,15 @@ const Points = () => {
|
||||
<Link href="https://tiktokenizer.vercel.app/" target="_blank" mb={['30px', 10]}>
|
||||
{t('common:support.wallet.subscription.token_compute')}
|
||||
</Link>
|
||||
<Box p={5} w={'100%'} h={'666px'} bg={'white'} borderRadius={'lg'} boxShadow={'md'}>
|
||||
<Box
|
||||
p={[3, 5]}
|
||||
w={'100%'}
|
||||
h={'666px'}
|
||||
bg={'white'}
|
||||
borderRadius={'lg'}
|
||||
boxShadow={'md'}
|
||||
overflow={'hidden'}
|
||||
>
|
||||
<ModelTable />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -271,36 +271,41 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
) : (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
onClick={handleFileSelectorOpen}
|
||||
>
|
||||
<MyTooltip label={t('account_info:choose_avatar')}>
|
||||
<Box
|
||||
w={['44px', '54px']}
|
||||
h={['44px', '54px']}
|
||||
borderRadius={'50%'}
|
||||
border={theme.borders.base}
|
||||
overflow={'hidden'}
|
||||
p={'2px'}
|
||||
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
|
||||
mb={2}
|
||||
>
|
||||
<Avatar src={userInfo?.avatar} borderRadius={'50%'} w={'100%'} h={'100%'} />
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
<Flex mt={4} alignItems={'center'}>
|
||||
<Box {...labelStyles}>{t('account_info:avatar')} </Box>
|
||||
<Flex
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
alignItems={'center'}
|
||||
gap={2}
|
||||
cursor={'pointer'}
|
||||
onClick={handleFileSelectorOpen}
|
||||
>
|
||||
<MyTooltip label={t('account_info:choose_avatar')}>
|
||||
<Box
|
||||
w={'40px'}
|
||||
h={'40px'}
|
||||
borderRadius={'50%'}
|
||||
border={'1px solid'}
|
||||
borderColor={'borderColor.base'}
|
||||
overflow={'hidden'}
|
||||
p={'2px'}
|
||||
bg={'white'}
|
||||
>
|
||||
<Avatar src={userInfo?.avatar} borderRadius={'50%'} w={'100%'} h={'100%'} />
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
|
||||
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'}>
|
||||
<MyIcon mr={1} name={'edit'} w={'14px'} />
|
||||
{t('account_info:change')}
|
||||
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'}>
|
||||
<MyIcon mr={1} name={'edit'} w={'14px'} />
|
||||
{t('account_info:change')}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{feConfigs?.isPlus && (
|
||||
<Flex mt={[0, 4]} alignItems={'center'}>
|
||||
<Flex mt={[4, 4]} alignItems={'center'}>
|
||||
<Box {...labelStyles}>{t('account_info:member_name')} </Box>
|
||||
<Input
|
||||
flex={'1 0 0'}
|
||||
@@ -308,7 +313,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
|
||||
defaultValue={userInfo?.team?.memberName || 'Member'}
|
||||
title={t('account_info:click_modify_nickname')}
|
||||
borderColor={'transparent'}
|
||||
transform={'translateX(-11px)'}
|
||||
transform={['none', 'translateX(-11px)']}
|
||||
maxLength={100}
|
||||
onBlur={async (e) => {
|
||||
const val = e.target.value;
|
||||
|
||||
@@ -18,7 +18,6 @@ import { GPTMessages2Chats, chatValue2RuntimePrompt } from '@fastgpt/global/core
|
||||
import { getChatItems } from '@fastgpt/service/core/chat/controller';
|
||||
import {
|
||||
type Props as SaveChatProps,
|
||||
ensurePendingChatRoundItems,
|
||||
pushChatRecords,
|
||||
updateInteractiveChat
|
||||
} from '@fastgpt/service/core/chat/saveChat';
|
||||
@@ -65,11 +64,6 @@ import { formatTime2YMDHM } from '@fastgpt/global/common/string/time';
|
||||
import { LimitTypeEnum, teamFrequencyLimit } from '@fastgpt/service/common/api/frequencyLimit';
|
||||
import { getIpFromRequest } from '@fastgpt/service/common/geo';
|
||||
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
|
||||
import {
|
||||
ensureGenerateChat,
|
||||
updateChatGenerateStatus
|
||||
} from '@fastgpt/service/core/chat/chatGenerateStatus';
|
||||
import { ChatGenerateStatusEnum } from '@fastgpt/global/core/chat/constants';
|
||||
|
||||
const logger = getLogger(LogCategories.MODULE.CHAT.ITEM);
|
||||
|
||||
@@ -96,8 +90,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
} = CompletionsPropsSchema.parse(req.body);
|
||||
|
||||
const startTime = Date.now();
|
||||
let runningChatId: string | undefined;
|
||||
let runningAppId: string | undefined;
|
||||
|
||||
const originIp = getIpFromRequest(req);
|
||||
|
||||
@@ -263,36 +255,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
return ChatSourceEnum.online;
|
||||
})();
|
||||
|
||||
runningChatId = saveChatId;
|
||||
runningAppId = String(app._id);
|
||||
|
||||
await ensureGenerateChat({
|
||||
appId: runningAppId,
|
||||
chatId: runningChatId,
|
||||
teamId,
|
||||
tmbId: tmbId,
|
||||
source,
|
||||
sourceName: sourceName || '',
|
||||
shareId,
|
||||
outLinkUid: outLinkUserId
|
||||
});
|
||||
|
||||
// 流式 + 站内 online:工作流 dispatch 走 v2 管道;与 HTTP 路径是 /v1 还是 /v2 无关
|
||||
const shouldUseWorkflowStreamV2 = stream && source === ChatSourceEnum.online;
|
||||
const workflowApiVersion = shouldUseWorkflowStreamV2 ? 'v2' : 'v1';
|
||||
// OpenAI 兼容 /v1/chat/completions 不镜像 SSE 到 Redis;断线续传由 /api/v2/chat/completions + /api/core/chat/resume 承担
|
||||
|
||||
if (!interactive) {
|
||||
await ensurePendingChatRoundItems({
|
||||
chatId: saveChatId,
|
||||
appId: runningAppId,
|
||||
teamId,
|
||||
tmbId: String(tmbId),
|
||||
userContent: userQuestion,
|
||||
responseChatItemId
|
||||
});
|
||||
}
|
||||
|
||||
const workflowResponseWrite = getWorkflowResponseWrite({
|
||||
res,
|
||||
detail,
|
||||
@@ -313,7 +275,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
} = await (async () => {
|
||||
if (app.version === 'v2') {
|
||||
return dispatchWorkFlow({
|
||||
apiVersion: workflowApiVersion,
|
||||
apiVersion: 'v1',
|
||||
res,
|
||||
lang: getLocale(req),
|
||||
requestOrigin: req.headers.origin,
|
||||
@@ -390,12 +352,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
await pushChatRecords(params);
|
||||
}
|
||||
|
||||
await updateChatGenerateStatus({
|
||||
appId: runningAppId,
|
||||
chatId: runningChatId,
|
||||
status: ChatGenerateStatusEnum.done
|
||||
});
|
||||
|
||||
const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId);
|
||||
if (isOwnerUse && source === ChatSourceEnum.online) {
|
||||
await recordAppUsage({
|
||||
@@ -540,13 +496,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (runningAppId && runningChatId) {
|
||||
await updateChatGenerateStatus({
|
||||
appId: runningAppId,
|
||||
chatId: runningChatId,
|
||||
status: ChatGenerateStatusEnum.error
|
||||
});
|
||||
}
|
||||
if (stream) {
|
||||
sseErrRes(res, err);
|
||||
res.end();
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getParents } from '@/pages/api/core/dataset/paths';
|
||||
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
|
||||
import { GetDatasetPathsResponseSchema } from '@fastgpt/global/openapi/core/dataset/api';
|
||||
import { ParentIdSchema } from '@fastgpt/global/common/parentFolder/type';
|
||||
|
||||
class FakeObjectId {
|
||||
constructor(private readonly hex: string) {}
|
||||
toString() {
|
||||
return this.hex;
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@fastgpt/service/core/dataset/schema', () => ({
|
||||
MongoDataset: {
|
||||
@@ -62,6 +71,34 @@ describe('getParents', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should coerce ObjectId-like values through the response schema', async () => {
|
||||
const childHex = '69e5ca9ce4f63f23d53848da';
|
||||
const parentHex = '69e5ca9ce4f63f23d53848db';
|
||||
const childObjectId = new FakeObjectId(childHex);
|
||||
const parentObjectId = new FakeObjectId(parentHex);
|
||||
|
||||
vi.mocked(MongoDataset.findById)
|
||||
.mockResolvedValueOnce({
|
||||
name: 'Child',
|
||||
parentId: parentObjectId
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
name: 'Parent',
|
||||
parentId: null
|
||||
});
|
||||
|
||||
const result = await getParents(childObjectId as unknown as string);
|
||||
const parsed = GetDatasetPathsResponseSchema.parse(result);
|
||||
|
||||
expect(parsed).toEqual([
|
||||
{ parentId: parentHex, parentName: 'Parent' },
|
||||
{ parentId: childHex, parentName: 'Child' }
|
||||
]);
|
||||
for (const item of parsed) {
|
||||
expect(typeof item.parentId).toBe('string');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle circular references gracefully', async () => {
|
||||
vi.mocked(MongoDataset.findById)
|
||||
.mockResolvedValueOnce({
|
||||
@@ -81,3 +118,80 @@ describe('getParents', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ParentIdSchema', () => {
|
||||
const validHex = '69e5ca9ce4f63f23d53848da';
|
||||
|
||||
describe('accepts', () => {
|
||||
it('24-char lowercase hex string (ObjectId shape)', () => {
|
||||
expect(ParentIdSchema.parse(validHex)).toBe(validHex);
|
||||
});
|
||||
|
||||
it('24-char uppercase / mixed-case hex string', () => {
|
||||
const upper = 'ABCDEF0123456789ABCDEF01';
|
||||
const mixed = 'AbCdEf0123456789abcdef01';
|
||||
expect(ParentIdSchema.parse(upper)).toBe(upper);
|
||||
expect(ParentIdSchema.parse(mixed)).toBe(mixed);
|
||||
});
|
||||
|
||||
it('empty string (root sentinel)', () => {
|
||||
expect(ParentIdSchema.parse('')).toBe('');
|
||||
});
|
||||
|
||||
it('null', () => {
|
||||
expect(ParentIdSchema.parse(null)).toBeNull();
|
||||
});
|
||||
|
||||
it('undefined', () => {
|
||||
expect(ParentIdSchema.parse(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ObjectId-like object with toString', () => {
|
||||
const oid = new FakeObjectId(validHex);
|
||||
expect(ParentIdSchema.parse(oid)).toBe(validHex);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejects', () => {
|
||||
it('arbitrary non-hex string', () => {
|
||||
expect(() => ParentIdSchema.parse('parent1-id')).toThrow();
|
||||
expect(() => ParentIdSchema.parse('not-an-id')).toThrow();
|
||||
});
|
||||
|
||||
it('hex string with wrong length', () => {
|
||||
// 23 chars
|
||||
expect(() => ParentIdSchema.parse('69e5ca9ce4f63f23d53848d')).toThrow();
|
||||
// 25 chars
|
||||
expect(() => ParentIdSchema.parse('69e5ca9ce4f63f23d53848daa')).toThrow();
|
||||
});
|
||||
|
||||
it('24-char string containing non-hex character', () => {
|
||||
expect(() => ParentIdSchema.parse('69e5ca9ce4f63f23d53848dz')).toThrow();
|
||||
});
|
||||
|
||||
it('number', () => {
|
||||
expect(() => ParentIdSchema.parse(123)).toThrow();
|
||||
});
|
||||
|
||||
it('boolean', () => {
|
||||
expect(() => ParentIdSchema.parse(true)).toThrow();
|
||||
expect(() => ParentIdSchema.parse(false)).toThrow();
|
||||
});
|
||||
|
||||
it('plain object whose toString produces non-hex', () => {
|
||||
// String({}) -> "[object Object]" -> regex fails
|
||||
expect(() => ParentIdSchema.parse({})).toThrow();
|
||||
expect(() => ParentIdSchema.parse({ foo: 'bar' })).toThrow();
|
||||
});
|
||||
|
||||
it('array', () => {
|
||||
// String([1, 2]) -> "1,2" -> regex fails
|
||||
expect(() => ParentIdSchema.parse([1, 2])).toThrow();
|
||||
});
|
||||
|
||||
it('ObjectId-like object whose toString produces invalid hex', () => {
|
||||
const bad = new FakeObjectId('not-hex');
|
||||
expect(() => ParentIdSchema.parse(bad)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user