mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-16 01:09:01 +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:
@@ -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