diff --git a/document/content/docs/self-host/upgrading/4-14/41412.mdx b/document/content/docs/self-host/upgrading/4-14/41412.mdx new file mode 100644 index 0000000000..e141c6a58b --- /dev/null +++ b/document/content/docs/self-host/upgrading/4-14/41412.mdx @@ -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 接口错误处理。 + diff --git a/document/content/docs/toc.mdx b/document/content/docs/toc.mdx index 4e88b8a632..06265d5806 100644 --- a/document/content/docs/toc.mdx +++ b/document/content/docs/toc.mdx @@ -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) diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 15e7fd8143..6ac822caed 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -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", diff --git a/packages/global/common/parentFolder/type.ts b/packages/global/common/parentFolder/type.ts index a1891cf883..c556edeaeb 100644 --- a/packages/global/common/parentFolder/type.ts +++ b/packages/global/common/parentFolder/type.ts @@ -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({ diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index d7612d1693..4d374ed424 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -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), - obj: ChatRoleEnum.Human - } - }, - { session } - ); - await MongoChatItem.updateOne( - { _id: aiExisting._id }, - { - $set: { - ...(processedContent[1] as Record), - 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 } ); diff --git a/projects/app/src/components/core/ai/ModelTable/index.tsx b/projects/app/src/components/core/ai/ModelTable/index.tsx index 835831f2f2..46fcde1be5 100644 --- a/projects/app/src/components/core/ai/ModelTable/index.tsx +++ b/projects/app/src/components/core/ai/ModelTable/index.tsx @@ -217,34 +217,50 @@ const ModelTable = ({ permissionConfig = false }: { permissionConfig?: boolean } }); return ( - - - - + + + + {t('common:model.provider')} - - - - + + + + + + {t('common:model.model_type')} - - - - + + + + + + - + diff --git a/projects/app/src/pageComponents/price/Points.tsx b/projects/app/src/pageComponents/price/Points.tsx index 6f74189d61..79c9dd12a1 100644 --- a/projects/app/src/pageComponents/price/Points.tsx +++ b/projects/app/src/pageComponents/price/Points.tsx @@ -20,7 +20,15 @@ const Points = () => { {t('common:support.wallet.subscription.token_compute')} - + diff --git a/projects/app/src/pages/account/info/index.tsx b/projects/app/src/pages/account/info/index.tsx index a9e10fd20d..35163f1cfb 100644 --- a/projects/app/src/pages/account/info/index.tsx +++ b/projects/app/src/pages/account/info/index.tsx @@ -271,36 +271,41 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => { ) : ( - - - - - - + + {t('account_info:avatar')}  + + + + + + - - - {t('account_info:change')} + + + {t('account_info:change')} + )} {feConfigs?.isPlus && ( - + {t('account_info:member_name')}  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; diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 83fee713d7..b668b6f250 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -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(); diff --git a/projects/app/test/api/core/dataset/paths.test.ts b/projects/app/test/api/core/dataset/paths.test.ts index f97ef3bc19..9f4d179ae1 100644 --- a/projects/app/test/api/core/dataset/paths.test.ts +++ b/projects/app/test/api/core/dataset/paths.test.ts @@ -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(); + }); + }); +});