Commit e927b94c by Archer Committed by GitHub

perf: quote code (#6977)

* perf: quote code

* fix: type

* doc

* doc

* add zod schema
parent 22ebfacb
......@@ -24,6 +24,7 @@ description: 'FastGPT V4.15.0-beta2 更新说明'
1. 支持 Skill 编辑,Agent 支持 Skill 使用,目前仅支持静态 Skill,无法反向调用系统工具。
2. 重写 agentV2 loop 逻辑。
3. 知识库搜索支持原生多模态 embedding 模型以及图搜图。
4. Chat API dataId 校验:`/v1/chat/completions`、`/v2/chat/completions` 与 `chatTest` 在工作流执行前校验本轮 `dataId` 是否与请求内或当前会话已有记录重复;重复时直接返回业务错误,避免脏数据进入工作流与流恢复合并逻辑。
## ⚙️ 优化
......@@ -44,13 +45,14 @@ description: 'FastGPT V4.15.0-beta2 更新说明'
1. 工作流,单节点调试,存在异常默认值。
2. 模型配置,defaultConfig 覆盖异常。
3. 切换团队时,清除本地 chat 缓存。
4. 流恢复表单输入:刷新或断线续传后,已提交的表单输入值(含 `fileSelect` 文件列表)能正确回填到交互节点内,不再出现空表单或文件消失。
5. 流恢复内容保留:自动续传开始时保留已加载的 AI 输出与节点响应;completed 记录覆盖时不再丢失已恢复的交互表单值与 flow 节点响应。
6. 流恢复交互状态:已提交表单后不再重复追加过期未提交交互;恢复过程中表单默认值能随 `formInputResult` 同步更新。
7. 流恢复历史标题:新对话发起后,侧栏临时历史项优先展示用户输入生成的标题,服务端标题落库后再覆盖,避免长时间显示「新对话」。
8. 切换应用历史串线:修复切换不同应用时,侧栏或会话内容短暂展示其他应用聊天记录的问题。
9. 停止对话提示:移除停止时的 warning toast,改为与后端生成态同步的状态提示。
10. Chat API dataId 校验:`/v1/chat/completions`、`/v2/chat/completions` 与 `chatTest` 在工作流执行前校验本轮 `dataId` 是否与请求内或当前会话已有记录重复;重复时直接返回业务错误,避免脏数据进入工作流与流恢复合并逻辑。
4. 对话流恢复:
- 刷新或断线续传后,已提交的表单输入值(含 `fileSelect` 文件列表)能正确回填到交互节点内,不再出现空表单或文件消失。
- 自动续传开始时保留已加载的 AI 输出与节点响应;completed 记录覆盖时不再丢失已恢复的交互表单值与 flow 节点响应。
- 已提交表单后不再重复追加过期未提交交互;恢复过程中表单默认值能随 `formInputResult` 同步更新。
- 新对话发起后,侧栏临时历史项优先展示用户输入生成的标题,服务端标题落库后再覆盖,避免长时间显示「新对话」。
- 切换不同应用时,侧栏或会话内容短暂展示其他应用聊天记录的问题。
5. 停止对话提示:移除停止时的 warning toast,改为与后端生成态同步的状态提示。
6. v1/completions 接口,nodeResponse 中,quoteList 未返回 `q` , `a`。
## 代码优化
......
......@@ -187,8 +187,8 @@
"content/self-host/custom-models/ollama.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/custom-models/xinference.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/custom-models/xinference.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/deploy/docker.en.mdx": "2026-05-07T15:08:21+08:00",
"content/self-host/deploy/docker.mdx": "2026-05-07T15:08:21+08:00",
"content/self-host/deploy/docker.en.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/deploy/docker.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/deploy/sealos.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/deploy/sealos.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/design/dataset.en.mdx": "2026-04-26T21:08:47+08:00",
......@@ -233,8 +233,8 @@
"content/self-host/upgrading/4-14/4140.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4141.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4141.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/41410.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/41410.mdx": "2026-05-23T22:47:02+08:00",
"content/self-host/upgrading/4-14/41410.en.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/4-14/41410.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/4-14/41411.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/41411.mdx": "2026-04-26T21:28:27+08:00",
"content/self-host/upgrading/4-14/41412.en.mdx": "2026-04-26T21:08:47+08:00",
......@@ -278,13 +278,13 @@
"content/self-host/upgrading/4-14/4149.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4149.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-15/4150.mdx": "2026-05-20T17:52:26+08:00",
"content/self-host/upgrading/4-15/41502.mdx": "2026-05-23T23:33:02+08:00",
"content/self-host/upgrading/4-15/41502.mdx": "2026-05-24T17:01:11+08:00",
"content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/41.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4100.en.mdx": "2026-05-07T15:06:40+08:00",
"content/self-host/upgrading/outdated/4100.mdx": "2026-05-07T15:06:40+08:00",
"content/self-host/upgrading/outdated/4100.en.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/outdated/4100.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/outdated/4101.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4101.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4110.en.mdx": "2026-04-26T21:08:47+08:00",
......@@ -363,8 +363,8 @@
"content/self-host/upgrading/outdated/4818.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4819.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4819.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/482.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/482.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/482.en.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/outdated/482.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/outdated/4820.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4820.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4821.en.mdx": "2026-04-26T21:08:47+08:00",
......@@ -373,8 +373,8 @@
"content/self-host/upgrading/outdated/4822.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4823.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4823.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/483.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/483.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/483.en.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/outdated/483.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/outdated/484.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/484.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/485.en.mdx": "2026-04-26T21:08:47+08:00",
......@@ -387,8 +387,8 @@
"content/self-host/upgrading/outdated/488.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/489.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/489.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/490.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/490.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/490.en.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/outdated/490.mdx": "2026-05-24T00:53:50+08:00",
"content/self-host/upgrading/outdated/491.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/491.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/4910.en.mdx": "2026-04-26T21:08:47+08:00",
......
......@@ -22,19 +22,19 @@ const datasetErr = [
},
{
statusText: CommonErrEnum.fileNotFound,
message: 'error.fileNotFound'
message: i18nT('common:error.fileNotFound')
},
{
statusText: CommonErrEnum.unAuthFile,
message: 'error.unAuthFile'
message: i18nT('common:error.unAuthFile')
},
{
statusText: CommonErrEnum.missingParams,
message: 'error.missingParams'
message: i18nT('common:error.missingParams')
},
{
statusText: CommonErrEnum.inheritPermissionError,
message: 'error.inheritPermissionError'
message: i18nT('common:error.inheritPermissionError')
}
];
export default datasetErr.reduce((acc, cur, index) => {
......
......@@ -20,11 +20,11 @@ export enum DatasetErrEnum {
const datasetErr = [
{
statusText: DatasetErrEnum.sameApiCollection,
message: i18nT('dataset:same_api_collection')
message: i18nT('common:core.dataset.error.sameApiCollection')
},
{
statusText: DatasetErrEnum.notSupportSync,
message: i18nT('dataset:collection_not_support_sync')
message: i18nT('common:core.dataset.error.notSupportSync')
},
{
statusText: DatasetErrEnum.unExist,
......@@ -36,35 +36,39 @@ const datasetErr = [
},
{
statusText: DatasetErrEnum.unAuthDataset,
message: 'core.dataset.error.unAuthDataset'
message: i18nT('common:core.dataset.error.unAuthDataset')
},
{
statusText: DatasetErrEnum.unAuthDatasetCollection,
message: 'core.dataset.error.unAuthDatasetCollection'
message: i18nT('common:core.dataset.error.unAuthDatasetCollection')
},
{
statusText: DatasetErrEnum.unAuthDatasetData,
message: 'core.dataset.error.unAuthDatasetData'
message: i18nT('common:core.dataset.error.unAuthDatasetData')
},
{
statusText: DatasetErrEnum.unAuthDatasetFile,
message: 'core.dataset.error.unAuthDatasetFile'
message: i18nT('common:core.dataset.error.unAuthDatasetFile')
},
{
statusText: DatasetErrEnum.unCreateCollection,
message: 'core.dataset.error.unCreateCollection'
message: i18nT('common:core.dataset.error.unCreateCollection')
},
{
statusText: DatasetErrEnum.unLinkCollection,
message: 'core.dataset.error.unLinkCollection'
message: i18nT('common:core.dataset.error.unLinkCollection')
},
{
statusText: DatasetErrEnum.invalidVectorModelOrQAModel,
message: 'core.dataset.error.invalidVectorModelOrQAModel'
message: i18nT('common:core.dataset.error.invalidVectorModelOrQAModel')
},
{
statusText: DatasetErrEnum.canNotEditAdminPermission,
message: 'core.dataset.error.canNotEditAdminPermission'
message: i18nT('common:core.dataset.error.canNotEditAdminPermission')
},
{
statusText: DatasetErrEnum.noApiServer,
message: i18nT('common:core.dataset.error.noApiServer')
}
];
export default datasetErr.reduce((acc, cur, index) => {
......
import { SearchDataResponseItemSchema } from '../dataset/type';
import { SearchDataResponseQuoteListItemSchema } from '../dataset/type';
import {
ChatFileTypeEnum,
ChatGenerateStatusEnum,
ChatRoleEnum,
type ChatSourceEnum
ChatSourceEnum
} from './constants';
import { FlowNodeTypeEnum } from '../workflow/node/constant';
import { DispatchNodeResponseKeyEnum } from '../workflow/runtime/constants';
import { AppSchemaTypeSchema, type AppSchemaType, type VariableItemType } from '../app/type';
import { AppSchemaTypeSchema, VariableItemTypeSchema } from '../app/type';
import { DispatchNodeResponseSchema } from '../workflow/runtime/type';
import { WorkflowInteractiveResponseTypeSchema } from '../workflow/template/system/interactive/type';
import type { FlowNodeInputItemType } from '../workflow/type/io';
import { FlowNodeInputItemTypeSchema } from '../workflow/type/io';
import z from 'zod';
import {
AgentLoopAskSchema,
......@@ -19,6 +19,7 @@ import {
AgentPlanSchema,
AgentPlanStatusSchema
} from '../ai/agent/type';
import { ObjectIdSchema } from '../../common/type/mongo';
export const ChatHistoryItemResSchema = DispatchNodeResponseSchema.extend({
nodeId: z.string(),
......@@ -80,49 +81,56 @@ export const SkillModuleResponseItemSchema = z.object({
export type SkillModuleResponseItemType = z.infer<typeof SkillModuleResponseItemSchema>;
/* --------- chat ---------- */
export type ChatSchemaType = {
_id: string;
chatId: string;
userId: string;
teamId: string;
tmbId: string;
appId: string;
appVersionId?: string;
createTime: Date;
updateTime: Date;
title: string;
customTitle: string;
top: boolean;
source: `${ChatSourceEnum}`;
sourceName?: string;
shareId?: string;
outLinkUid?: string;
variableList?: VariableItemType[];
welcomeText?: string;
variables: Record<string, any>;
pluginInputs?: FlowNodeInputItemType[];
metadata?: Record<string, any>;
export const ChatSchema = z.object({
_id: ObjectIdSchema,
chatId: z.string(),
userId: ObjectIdSchema,
teamId: ObjectIdSchema,
tmbId: ObjectIdSchema,
appId: ObjectIdSchema.meta({
description: '目前已经变成 sourceId,可能是 app 的,也可能是 skill 的'
}),
appVersionId: ObjectIdSchema.optional().meta({ description: 'appId 为 app 时候才有' }),
createTime: z.coerce.date(),
updateTime: z.coerce.date(),
title: z.string(),
customTitle: z.string().optional(),
top: z.boolean().default(false),
source: z.enum(ChatSourceEnum),
sourceName: z.string().optional(),
shareId: z.string().optional(),
outLinkUid: z.string().optional(),
variableList: z.array(VariableItemTypeSchema).optional(),
welcomeText: z.string().optional(),
variables: z.record(z.string(), z.any()),
pluginInputs: z.array(FlowNodeInputItemTypeSchema).optional(),
metadata: z.record(z.string(), z.any()).optional(),
// Boolean flags for efficient filtering
hasGoodFeedback?: boolean;
hasBadFeedback?: boolean;
hasUnreadGoodFeedback?: boolean;
hasUnreadBadFeedback?: boolean;
hasGoodFeedback: z.boolean().optional(),
hasBadFeedback: z.boolean().optional(),
hasUnreadGoodFeedback: z.boolean().optional(),
hasUnreadBadFeedback: z.boolean().optional(),
// Error count (redundant field for performance)
errorCount?: number;
errorCount: z.number().optional(),
/** 旧数据可能无此字段;业务上按 done 处理 */
chatGenerateStatus?: ChatGenerateStatusEnum;
hasBeenRead: boolean;
chatGenerateStatus: z
.enum(ChatGenerateStatusEnum)
.default(ChatGenerateStatusEnum.done)
.meta({ description: '生成状态' }),
hasBeenRead: z.boolean().default(true),
deleteTime?: Date | null;
};
deleteTime: z.coerce.date().nullish()
});
export type ChatSchemaType = z.infer<typeof ChatSchema>;
export type ChatWithAppSchema = Omit<ChatSchemaType, 'appId'> & {
appId: AppSchemaType;
};
export const ChatWithAppSchema = ChatSchema.omit({ appId: true }).extend({
appId: AppSchemaTypeSchema
});
export type ChatWithAppSchema = z.infer<typeof ChatWithAppSchema>;
/* --------- chat item ---------- */
// User
......@@ -320,7 +328,7 @@ export type ToolCiteLinksType = z.infer<typeof ToolCiteLinksSchema>;
export const ResponseTagItemSchema = z.object({
useAgentSandbox: z.boolean().optional(),
totalQuoteList: z.array(SearchDataResponseItemSchema).optional(),
totalQuoteList: z.array(SearchDataResponseQuoteListItemSchema).optional(),
toolCiteLinks: z.array(ToolCiteLinksSchema).optional(),
errorText: ErrorTextItemSchema.optional(),
llmModuleAccount: z.number().optional().meta({ deprecated: true }),
......
......@@ -428,6 +428,25 @@ export const SearchDataResponseItemSchema = DatasetDataItemSchema.omit({
.meta({ description: '搜索数据响应项' });
export type SearchDataResponseItemType = z.infer<typeof SearchDataResponseItemSchema>;
export const SearchDataResponseQuoteItemSchema = SearchDataResponseItemSchema.pick({
id: true,
chunkIndex: true,
datasetId: true,
collectionId: true,
sourceId: true,
sourceName: true,
score: true
}).meta({ description: '搜索数据引用响应项(精简)' });
export type SearchDataResponseQuoteItemType = z.infer<typeof SearchDataResponseQuoteItemSchema>;
export const SearchDataResponseQuoteListItemSchema = z.union([
SearchDataResponseItemSchema,
SearchDataResponseQuoteItemSchema
]);
export type SearchDataResponseQuoteListItemType = z.infer<
typeof SearchDataResponseQuoteListItemSchema
>;
export const DatasetCiteItemSchema = z
.object({
_id: ObjectIdSchema.meta({ description: '数据 ID' }),
......
......@@ -24,7 +24,7 @@ import type {
InteractiveNodeResponseType,
WorkflowInteractiveResponseType
} from '../template/system/interactive/type';
import { SearchDataResponseItemSchema } from '../../dataset/type';
import { SearchDataResponseQuoteListItemSchema } from '../../dataset/type';
import type { localeType } from '../../../common/i18n/type';
import { type ChatFileStoreValue, type UserChatItemValueItemType } from '../../chat/type';
import { DatasetSearchModeEnum } from '../../dataset/constants';
......@@ -198,7 +198,7 @@ export const DispatchNodeResponseSchema = z
temperature: z.number().optional().meta({ description: '温度' }),
maxToken: z.number().optional().meta({ description: '最大 token' }),
quoteList: z
.array(SearchDataResponseItemSchema)
.array(SearchDataResponseQuoteListItemSchema)
.optional()
.meta({ description: '知识库引用列表' }),
reasoningText: z.string().optional().meta({ description: '思考文本' }),
......
import { describe, expect, it } from 'vitest';
import {
ChatSchema,
ToolModuleResponseItemSchema,
UserChatItemFileItemSchema,
UserChatItemValueItemSchema,
......@@ -12,7 +13,66 @@ import {
ToolCiteLinksSchema,
RuntimeUserPromptSchema
} from '@fastgpt/global/core/chat/type';
import { ChatRoleEnum, ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import {
ChatRoleEnum,
ChatFileTypeEnum,
ChatGenerateStatusEnum,
ChatSourceEnum
} from '@fastgpt/global/core/chat/constants';
describe('ChatSchema', () => {
const chatId = 'chat-1';
const objectId = '68ee0bd23d17260b7829b137';
it('should validate chat document and coerce date fields', () => {
const result = ChatSchema.safeParse({
_id: objectId,
chatId,
userId: objectId,
teamId: objectId,
tmbId: objectId,
appId: objectId,
createTime: '2026-05-24T00:00:00.000Z',
updateTime: new Date('2026-05-24T00:00:01.000Z'),
title: '历史记录',
customTitle: '',
top: false,
source: ChatSourceEnum.online,
variables: {},
errorCount: 0,
chatGenerateStatus: ChatGenerateStatusEnum.done,
hasBeenRead: false,
deleteTime: null
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.createTime).toBeInstanceOf(Date);
expect(result.data.deleteTime).toBeNull();
}
});
it('should reject invalid chat source', () => {
const result = ChatSchema.safeParse({
_id: objectId,
chatId,
userId: objectId,
teamId: objectId,
tmbId: objectId,
appId: objectId,
createTime: new Date(),
updateTime: new Date(),
title: '历史记录',
customTitle: '',
top: false,
source: 'invalid',
variables: {},
hasBeenRead: false
});
expect(result.success).toBe(false);
});
});
describe('ToolModuleResponseItemSchema', () => {
it('should validate valid tool response', () => {
......
......@@ -38,6 +38,7 @@ import { getFlatAppResponses } from '@fastgpt/global/core/chat/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { normalizeChatFileStoreValues } from './fileStoreValue';
import { cloneDeep } from 'lodash';
export type Props = {
chatId: string;
......@@ -178,10 +179,10 @@ const formatAiContent = ({
const { responseData, ...aiResponse } = aiContent;
const citeCollectionIds = new Set<string>();
const cloneResponseData = cloneDeep(responseData || []);
const dealResponseData = (responseItem: ChatHistoryItemResType) => {
getFlatAppResponses(cloneResponseData).forEach((responseItem: ChatHistoryItemResType) => {
if (responseItem.moduleType === FlowNodeTypeEnum.datasetSearchNode && responseItem.quoteList) {
// @ts-ignore
responseItem.quoteList = responseItem.quoteList.map((quote) => {
citeCollectionIds.add(quote.collectionId);
return {
......@@ -195,10 +196,9 @@ const formatAiContent = ({
};
});
}
};
getFlatAppResponses(responseData || []).forEach(dealResponseData);
});
const errorCount = responseData?.filter((item) => item.errorText).length ?? 0;
const errorCount = cloneResponseData?.filter((item) => item.errorText).length ?? 0;
return {
aiResponse: {
......@@ -207,7 +207,7 @@ const formatAiContent = ({
errorMsg,
citeCollectionIds: Array.from(citeCollectionIds)
},
nodeResponses: responseData,
nodeResponses: cloneResponseData,
citeCollectionIds,
errorCount
};
......
......@@ -349,6 +349,11 @@ describe('pushChatRecords', () => {
await pushChatRecords(props);
expect(props.aiContent.responseData?.[0]?.quoteList?.[0]).toMatchObject({
q: quote.q,
a: quote.a
});
const responses = await MongoChatItemResponse.find({
appId: testAppId,
chatId: props.chatId
......
......@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
import { type LinkedListResponse, type LinkedPaginationProps } from '@fastgpt/global/openapi/api';
import { Box, type BoxProps } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useScroll, useMemoizedFn, useDebounceEffect, useLatest } from 'ahooks';
import { useScroll, useDebounceEffect } from 'ahooks';
import MyBox from '../components/common/MyBox';
import { useRequest } from './useRequest';
......@@ -42,37 +42,44 @@ export function useLinkedScroll<
const isInit = useRef(false);
const scrollToItem = useCallback(
async (id?: string) => {
if (!id) {
id = defaultScroll === 'top' ? dataList[0]?.id : dataList[dataList.length - 1]?.id;
}
(id?: string) => {
const targetId =
id || (defaultScroll === 'top' ? dataList[0]?.id : dataList[dataList.length - 1]?.id);
const itemIndex = dataList.findIndex((item) => item.id === id);
if (itemIndex === -1) {
if (!targetId) {
return;
}
const element = itemRefs.current.get(id);
if (!element || !containerRef.current) {
requestAnimationFrame(() => scrollToItem(id));
const itemIndex = dataList.findIndex((item) => item.id === targetId);
if (itemIndex === -1) {
return;
}
const elementRect = element.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const tryScroll = () => {
const element = itemRefs.current.get(targetId);
if (!element || !containerRef.current) {
requestAnimationFrame(tryScroll);
return;
}
const elementRect = element.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const scrollTop = containerRef.current.scrollTop + elementRect.top - containerRect.top;
const scrollTop = containerRef.current.scrollTop + elementRect.top - containerRect.top;
containerRef.current.scrollTo({
top: scrollTop
});
};
containerRef.current.scrollTo({
top: scrollTop
});
tryScroll();
},
[dataList, defaultScroll]
);
const { runAsync: callApi, loading: isLoading } = useRequest(api);
const { runAsync: callApi, loading: isLoading } = useRequest(api, { errorToast: '' });
let scrollSign = useRef(false);
const scrollSign = useRef(false);
const { runAsync: loadInitData } = useRequest(
async ({ scrollWhenFinish, refresh } = { scrollWhenFinish: true, refresh: false }) => {
if (isLoading) return;
......
......@@ -137,28 +137,8 @@
"code_error.app_error.invalid_owner": "Unauthorized Application Owner",
"code_error.app_error.not_exist": "Application Does Not Exist",
"code_error.app_error.un_auth_app": "Unauthorized to Operate This Application",
"code_error.skill_error.not_exist": "Skill Does Not Exist",
"code_error.skill_error.un_auth_skill": "Unauthorized to Operate This Skill",
"code_error.skill_error.can_not_edit_admin_permission": "Can not edit admin permission",
"code_error.skill_error.name_exists": "Skill name already exists in this directory",
"code_error.skill_error.invalid_name": "Skill name must be a non-empty string",
"code_error.skill_error.skill_name_too_long": "Skill name must be 50 characters or fewer",
"code_error.skill_error.invalid_description": "Description must be less than 500 characters",
"code_error.skill_error.invalid_category": "Invalid category value",
"code_error.skill_error.invalid_config": "Config exceeds maximum allowed size (50KB)",
"code_error.skill_error.missing_model": "Model is required when requirements is provided",
"code_error.skill_error.requirements_too_long": "Requirements must be less than 8000 characters",
"code_error.skill_error.no_storage": "Skill has no storage, cannot copy",
"code_error.skill_error.no_fields_to_update": "No fields to update",
"code_error.skill_error.invalid_archive_format": "Only ZIP, TAR, and TAR.GZ files are supported",
"code_error.skill_error.invalid_package": "Invalid skill package structure",
"code_error.skill_error.invalid_skill_id": "Invalid skill ID",
"code_error.skill_error.archive_empty": "Archive is empty",
"code_error.skill_error.archive_extraction_failed": "Failed to extract archive",
"code_error.skill_error.archive_too_large": "Archive file size exceeds the maximum allowed limit",
"code_error.skill_error.missing_image_repository": "image.repository is required when image is provided",
"code_error.chat_error.un_auth": "Unauthorized to Operate This Chat Record",
"code_error.chat_error.chat_generating": "This chat is still generating. Please wait until it finishes before sending another request.",
"code_error.chat_error.un_auth": "Unauthorized to Operate This Chat Record",
"code_error.error_code.400": "Request Failed",
"code_error.error_code.401": "No Access Permission",
"code_error.error_code.403": "Access Forbidden",
......@@ -184,6 +164,26 @@
"code_error.outlink_error.link_not_exist": "Share Link Does Not Exist",
"code_error.outlink_error.un_auth_user": "Identity Verification Failed",
"code_error.plugin_error.un_auth": "No permission to operate the tool",
"code_error.skill_error.archive_empty": "Archive is empty",
"code_error.skill_error.archive_extraction_failed": "Failed to extract archive",
"code_error.skill_error.archive_too_large": "Archive file size exceeds the maximum allowed limit",
"code_error.skill_error.can_not_edit_admin_permission": "Can not edit admin permission",
"code_error.skill_error.invalid_archive_format": "Only ZIP, TAR, and TAR.GZ files are supported",
"code_error.skill_error.invalid_category": "Invalid category value",
"code_error.skill_error.invalid_config": "Config exceeds maximum allowed size (50KB)",
"code_error.skill_error.invalid_description": "Description must be less than 500 characters",
"code_error.skill_error.invalid_name": "Skill name must be a non-empty string",
"code_error.skill_error.invalid_package": "Invalid skill package structure",
"code_error.skill_error.invalid_skill_id": "Invalid skill ID",
"code_error.skill_error.missing_image_repository": "image.repository is required when image is provided",
"code_error.skill_error.missing_model": "Model is required when requirements is provided",
"code_error.skill_error.name_exists": "Skill name already exists in this directory",
"code_error.skill_error.no_fields_to_update": "No fields to update",
"code_error.skill_error.no_storage": "Skill has no storage, cannot copy",
"code_error.skill_error.not_exist": "Skill Does Not Exist",
"code_error.skill_error.requirements_too_long": "Requirements must be less than 8000 characters",
"code_error.skill_error.skill_name_too_long": "Skill name must be 50 characters or fewer",
"code_error.skill_error.un_auth_skill": "Unauthorized to Operate This Skill",
"code_error.system_error.community_version_num_limit": "Exceeded Open Source Version Limit, Please Upgrade to Commercial Version: https://fastgpt.io",
"code_error.system_error.license_app_amount_limit": "Exceed the maximum number of applications in the system",
"code_error.system_error.license_dataset_amount_limit": "Exceed the maximum number of knowledge bases in the system",
......@@ -210,12 +210,12 @@
"code_error.team_error.over_size": "Team members exceed limit",
"code_error.team_error.plugin_amount_not_enough": "Plugin Limit Reached",
"code_error.team_error.re_rank_not_enough": "Search rearrangement cannot be used in the free version~",
"code_error.team_error.sandbox_not_support": "The current team package does not support using sandbox/VM.",
"code_error.team_error.ticket_not_available": "The current package does not support work order services. You can go to the community to get help for free ~",
"code_error.team_error.too_many_invitations": "You have reached the maximum number of active invitation links, please clean up some links first",
"code_error.team_error.un_auth": "Unauthorized to Operate This Team",
"code_error.team_error.user_not_active": "The user did not accept or has left the team",
"code_error.team_error.website_sync_not_enough": "The free version cannot be synchronized with the web site ~",
"code_error.team_error.sandbox_not_support": "The current team package does not support using sandbox/VM.",
"code_error.team_error.you_have_been_in_the_team": "You are already in this team",
"code_error.token_error_code.403": "Invalid Login Status, Please Re-login",
"comfirm_import": "Confirm import",
......@@ -242,10 +242,10 @@
"core.ai.Prompt": "Prompt",
"core.ai.Support tool": "Tool call",
"core.ai.model.Dataset Agent Model": "File read model",
"core.ai.model.multimodal": "Multimodal",
"core.ai.model.multimodal_tip": "Multimodal embedding models can generate vectors for images.",
"core.ai.model.Vector Model": "Index model",
"core.ai.model.doc_index_and_dialog": "Document Index & Dialog Index",
"core.ai.model.multimodal": "Multimodal",
"core.ai.model.multimodal_tip": "Multimodal embedding models can generate vectors for images.",
"core.app.Api request": "API Request",
"core.app.Api request desc": "Connect to existing systems via API",
"core.app.App intro": "App Introduction",
......@@ -288,7 +288,6 @@
"core.app.feedback.Custom feedback": "Custom Feedback",
"core.app.feedback.close custom feedback": "Close Feedback",
"core.app.have_saved": "Saved",
"core.app.saving": "Saving",
"core.app.no_app": "No Apps Yet, Create One Now!",
"core.app.not_saved": "Not Saved",
"core.app.outLink.Can Drag": "Icon Can Be Dragged",
......@@ -302,6 +301,7 @@
"core.app.outLink.Select Using Way": "Select Usage Method",
"core.app.outLink.Show History": "Show Chat History",
"core.app.publish.Fei shu bot publish": "Publish to Feishu Bot",
"core.app.saving": "Saving",
"core.app.schedule.Default prompt": "Default Question",
"core.app.schedule.Default prompt placeholder": "Default question when executing the app",
"core.app.schedule.Every day": "Every day at {{hour}}:00",
......@@ -396,15 +396,12 @@
"core.chat.response.Tool call tokens": "Tool Call Tokens Consumption",
"core.chat.response.context total length": "Total Context Length",
"core.chat.response.loop_input": "Loop Input Array",
"core.chat.response.parallel_input": "Parallel Input Array",
"core.chat.response.parallel_output": "Parallel Success Results",
"core.chat.response.parallel_run_detail": "Parallel Run Details",
"core.chat.response.loop_run_input": "Loop Input",
"core.chat.response.loop_run_iterations": "Iterations",
"core.chat.response.loop_run_history": "Loop History",
"core.chat.response.loop_input_element": "Loop Input Element",
"core.chat.response.loop_output": "Loop Output Array",
"core.chat.response.loop_output_element": "Loop Output Element",
"core.chat.response.loop_run_history": "Loop History",
"core.chat.response.loop_run_input": "Loop Input",
"core.chat.response.loop_run_iterations": "Iterations",
"core.chat.response.module cq": "Question Classification List",
"core.chat.response.module cq result": "Classification Result",
"core.chat.response.module extract description": "Extract Background Description",
......@@ -418,6 +415,9 @@
"core.chat.response.module query": "Question/Search Term",
"core.chat.response.module similarity": "Similarity",
"core.chat.response.module temperature": "Temperature",
"core.chat.response.parallel_input": "Parallel Input Array",
"core.chat.response.parallel_output": "Parallel Success Results",
"core.chat.response.parallel_run_detail": "Parallel Run Details",
"core.chat.response.search using reRank": "Result Re-Rank",
"core.chat.response.text output": "Text Output",
"core.chat.response.update_var_result": "Variable Update Result (Displays Multiple Variable Update Results in Order)",
......@@ -464,9 +464,20 @@
"core.dataset.data.Updated": "Updated",
"core.dataset.data.group": " Groups",
"core.dataset.embedding model tip": "The index model converts knowledge base content into vectors for semantic search. Note that knowledge bases using different index models cannot be queried together. Switching the index model requires rebuilding all vector indexes, so choose carefully.",
"core.dataset.error.canNotEditAdminPermission": "Can not edit admin permission",
"core.dataset.error.Data not found": "Data Not Found or Deleted",
"core.dataset.error.invalidVectorModelOrQAModel": "Invalid index model or QA model",
"core.dataset.error.noApiServer": "API server does not exist",
"core.dataset.error.notSupportSync": "This collection does not support synchronization",
"core.dataset.error.sameApiCollection": "The same API collection already exists",
"core.dataset.error.Start Sync Failed": "Failed to Start Sync",
"core.dataset.error.unAuthDataset": "Unauthorized to operate this dataset",
"core.dataset.error.unAuthDatasetCollection": "Unauthorized to operate this dataset collection",
"core.dataset.error.unAuthDatasetData": "Unauthorized to operate this dataset data",
"core.dataset.error.unAuthDatasetFile": "Knowledge base file authentication failed",
"core.dataset.error.unCreateCollection": "Unauthorized to create dataset collection",
"core.dataset.error.unExistDataset": "The knowledge base does not exist",
"core.dataset.error.unLinkCollection": "Unauthorized to operate this external dataset collection",
"core.dataset.externalFile": "External File Library",
"core.dataset.file": "File",
"core.dataset.folder": "Directory",
......@@ -527,18 +538,18 @@
"core.dataset.test.Test Result": "Test Result",
"core.dataset.test.Test Text": "Single Text Test",
"core.dataset.test.Test Text Placeholder": "Enter the text to be tested",
"core.dataset.test.Test params": "Test Parameters",
"core.dataset.test.delete test history": "Delete This Test Result",
"core.dataset.test.image_expired": "Image expired",
"core.dataset.test.image_search_disabled_tip": "Configure an image understanding model or multimodal embedding model first.",
"core.dataset.test.image_token": "[Image]",
"core.dataset.test.input_title": "Test input",
"core.dataset.test.max_images_tip": "Up to 10 images are supported",
"core.dataset.test.search_config": "Search configuration",
"core.dataset.test.Test params": "Test Parameters",
"core.dataset.test.upload_image": "Upload image",
"core.dataset.test.delete test history": "Delete This Test Result",
"core.dataset.test.test history": "Test History",
"core.dataset.test.test result placeholder": "Test results will be displayed here",
"core.dataset.test.test result tip": "Sort based on the similarity between the Dataset content and the test text. You can adjust the corresponding text based on the test results.\nNote: The data in the test records may have been modified. Clicking on a test data will display the latest data.",
"core.dataset.test.upload_image": "Upload image",
"core.dataset.training.Auto mode": "Auto index",
"core.dataset.training.Auto mode Tip": "Increase the semantic richness of data blocks by generating related questions and summaries through sub-indexes and calling models, making it more conducive to retrieval. Requires more storage space and increases AI call times.",
"core.dataset.training.Chunk mode": "Chunk",
......@@ -738,8 +749,8 @@
"dataset.data.Can not edit": "No Edit Permission",
"dataset.data.Index Placeholder": "Enter Index Text Content",
"dataset.data.Input Success Tip": "Data Imported Successfully",
"dataset.data.Update Success Tip": "Data Updated Successfully",
"dataset.data.Update Index Timeout Tip": "Updating indexes timed out. Please refresh the data later to check the result.",
"dataset.data.Update Success Tip": "Data Updated Successfully",
"dataset.data.edit.Index": "Data Index ({{amount}})",
"dataset.dataset_name": "Dataset Name",
"dataset.deleteFolderTips": "Confirm to Delete This Folder and All Its Contained Datasets? Data Cannot Be Recovered After Deletion, Please Confirm!",
......@@ -779,10 +790,11 @@
"error.s3_upload_timeout": "Upload timed out",
"error.send_auth_code_too_frequently": "Please do not obtain verification code frequently",
"error.too_many_request": "Too many request",
"error.upload_file_interval_limit": "Too many uploads in a short time or the current round limit was reached. Please try again later.",
"error.tool_not_exist": "Tool deleted",
"error.unKnow": "An Unexpected Error Occurred",
"error.unAuthFile": "Unauthorized to read this file",
"error.upload_file_error_filename": "{{name}} Upload Failed",
"error.upload_file_interval_limit": "Too many uploads in a short time or the current round limit was reached. Please try again later.",
"error.username_empty": "Account cannot be empty",
"error_collection_not_exist": "The collection does not exist",
"error_embedding_not_config": "Unconfigured index model",
......@@ -1026,7 +1038,7 @@
"support.user.auth.get_code_again": "s Get Again",
"support.user.captcha_placeholder": "Please enter the verification code",
"support.user.info.bind_notification_error": "Abnormal binding notification account",
"support.user.info.bind_notification_hint": "Please bind the notification receiving account to ensure that you can receive notifications such as package expiration reminders, etc., to ensure the normal operation of your service.",
"support.user.info.bind_notification_hint": "Please bind the notification receiving account to ensure that you can receive important reminders such as account information and package information in a timely manner.",
"support.user.info.bind_notification_success": "Binding notification account successful",
"support.user.info.code_required": "Verification code cannot be empty",
"support.user.info.notification_receiving_hint": "Notification reception",
......@@ -1117,10 +1129,10 @@
"support.wallet.subscription.type.extraDatasetSize": "Dataset Expansion",
"support.wallet.subscription.type.extraPoints": "AI Points Package",
"support.wallet.subscription.type.standard": "Package Subscription",
"support.wallet.usage.Assist Generate Skill": "Skill Assistance Generation",
"support.wallet.usage.Audio Speech": "Voice Playback",
"support.wallet.usage.Code Copilot": "Code Copilot",
"support.wallet.usage.Optimize Prompt": "Prompt word optimization",
"support.wallet.usage.Assist Generate Skill": "Skill Assistance Generation",
"support.wallet.usage.Source": "Source",
"support.wallet.usage.Text Length": "Text Length",
"support.wallet.usage.Time": "Generation Time",
......
......@@ -137,28 +137,8 @@
"code_error.app_error.invalid_owner": "非法的应用所有者",
"code_error.app_error.not_exist": "应用不存在",
"code_error.app_error.un_auth_app": "无权操作该应用",
"code_error.skill_error.not_exist": "技能不存在",
"code_error.skill_error.un_auth_skill": "无权操作该技能",
"code_error.skill_error.can_not_edit_admin_permission": "不能编辑管理员权限",
"code_error.skill_error.name_exists": "该目录下已存在同名技能",
"code_error.skill_error.invalid_name": "技能名称不能为空",
"code_error.skill_error.skill_name_too_long": "技能名称不能超过 50 字符",
"code_error.skill_error.invalid_description": "描述不能超过 500 字符",
"code_error.skill_error.invalid_category": "无效的技能分类",
"code_error.skill_error.invalid_config": "配置超过最大限制(50KB)",
"code_error.skill_error.missing_model": "提供需求描述时必须指定 model",
"code_error.skill_error.requirements_too_long": "需求描述不能超过 8000 字符",
"code_error.skill_error.no_storage": "技能没有存储,无法复制",
"code_error.skill_error.no_fields_to_update": "没有需要更新的字段",
"code_error.skill_error.invalid_archive_format": "仅支持 ZIP、TAR、TAR.GZ 格式",
"code_error.skill_error.invalid_package": "无效的技能包结构",
"code_error.skill_error.invalid_skill_id": "无效的技能 ID",
"code_error.skill_error.archive_empty": "压缩包内容为空",
"code_error.skill_error.archive_extraction_failed": "压缩包解压失败",
"code_error.skill_error.archive_too_large": "压缩包大小超过最大限制",
"code_error.skill_error.missing_image_repository": "提供 image 时必须指定 image.repository",
"code_error.chat_error.un_auth": "没有权限操作此对话记录",
"code_error.chat_error.chat_generating": "当前对话正在生成中,请等待完成后再发起新的请求",
"code_error.chat_error.un_auth": "没有权限操作此对话记录",
"code_error.error_code.400": "请求失败",
"code_error.error_code.401": "无访问权限",
"code_error.error_code.403": "紧张访问",
......@@ -184,6 +164,26 @@
"code_error.outlink_error.link_not_exist": "分享链接不存在",
"code_error.outlink_error.un_auth_user": "身份校验失败",
"code_error.plugin_error.un_auth": "无权操作该工具",
"code_error.skill_error.archive_empty": "压缩包内容为空",
"code_error.skill_error.archive_extraction_failed": "压缩包解压失败",
"code_error.skill_error.archive_too_large": "压缩包大小超过最大限制",
"code_error.skill_error.can_not_edit_admin_permission": "不能编辑管理员权限",
"code_error.skill_error.invalid_archive_format": "仅支持 ZIP、TAR、TAR.GZ 格式",
"code_error.skill_error.invalid_category": "无效的技能分类",
"code_error.skill_error.invalid_config": "配置超过最大限制(50KB)",
"code_error.skill_error.invalid_description": "描述不能超过 500 字符",
"code_error.skill_error.invalid_name": "技能名称不能为空",
"code_error.skill_error.invalid_package": "无效的技能包结构",
"code_error.skill_error.invalid_skill_id": "无效的技能 ID",
"code_error.skill_error.missing_image_repository": "提供 image 时必须指定 image.repository",
"code_error.skill_error.missing_model": "提供需求描述时必须指定 model",
"code_error.skill_error.name_exists": "该目录下已存在同名技能",
"code_error.skill_error.no_fields_to_update": "没有需要更新的字段",
"code_error.skill_error.no_storage": "技能没有存储,无法复制",
"code_error.skill_error.not_exist": "技能不存在",
"code_error.skill_error.requirements_too_long": "需求描述不能超过 8000 字符",
"code_error.skill_error.skill_name_too_long": "技能名称不能超过 50 字符",
"code_error.skill_error.un_auth_skill": "无权操作该技能",
"code_error.system_error.community_version_num_limit": "超出社区版数量限制,请升级商业版: https://fastgpt.in",
"code_error.system_error.license_app_amount_limit": "超出系统最大应用数量",
"code_error.system_error.license_dataset_amount_limit": "超出系统最大知识库数量",
......@@ -210,12 +210,12 @@
"code_error.team_error.over_size": "团队成员超出限制",
"code_error.team_error.plugin_amount_not_enough": "插件数量已达上限~",
"code_error.team_error.re_rank_not_enough": "免费版无法使用检索重排~",
"code_error.team_error.sandbox_not_support": "当前团队套餐不支持使用虚拟机/沙箱功能~",
"code_error.team_error.ticket_not_available": "当前套餐暂不支持工单服务,可以前往社区免费获取帮助~",
"code_error.team_error.too_many_invitations": "您的有效邀请链接数已达上限,请先清理链接",
"code_error.team_error.un_auth": "无权操作该团队",
"code_error.team_error.user_not_active": "用户未接受或已离开团队",
"code_error.team_error.website_sync_not_enough": "免费版无法使用Web站点同步~",
"code_error.team_error.sandbox_not_support": "当前团队套餐不支持使用虚拟机/沙箱功能~",
"code_error.team_error.you_have_been_in_the_team": "你已经在该团队中",
"code_error.token_error_code.403": "登录状态无效,请重新登录",
"comfirm_import": "确认导入",
......@@ -242,10 +242,10 @@
"core.ai.Prompt": "提示词",
"core.ai.Support tool": "工具调用",
"core.ai.model.Dataset Agent Model": "文本理解模型",
"core.ai.model.multimodal": "多模态",
"core.ai.model.multimodal_tip": "多模态索引模型可以给图片生成向量。",
"core.ai.model.Vector Model": "索引模型",
"core.ai.model.doc_index_and_dialog": "文档索引 & 对话索引",
"core.ai.model.multimodal": "多模态",
"core.ai.model.multimodal_tip": "多模态索引模型可以给图片生成向量。",
"core.app.Api request": "API 访问",
"core.app.Api request desc": "通过 API 接入已有系统",
"core.app.App intro": "应用介绍",
......@@ -288,7 +288,6 @@
"core.app.feedback.Custom feedback": "自定义反馈",
"core.app.feedback.close custom feedback": "关闭反馈",
"core.app.have_saved": "已保存",
"core.app.saving": "保存中",
"core.app.no_app": "还没有应用,快去创建一个吧!",
"core.app.not_saved": "未保存",
"core.app.outLink.Can Drag": "图标可拖拽",
......@@ -302,6 +301,7 @@
"core.app.outLink.Select Using Way": "选择使用方式",
"core.app.outLink.Show History": "展示历史对话",
"core.app.publish.Fei shu bot publish": "发布到飞书机器人",
"core.app.saving": "保存中",
"core.app.schedule.Default prompt": "默认问题",
"core.app.schedule.Default prompt placeholder": "执行应用时的默认问题",
"core.app.schedule.Every day": "每天 {{hour}}:00",
......@@ -396,15 +396,12 @@
"core.chat.response.Tool call tokens": "工具调用 tokens 消耗",
"core.chat.response.context total length": "上下文总长度",
"core.chat.response.loop_input": "输入数组",
"core.chat.response.parallel_input": "并行输入数组",
"core.chat.response.parallel_output": "并行成功结果",
"core.chat.response.parallel_run_detail": "并行执行明细",
"core.chat.response.loop_run_input": "循环输入",
"core.chat.response.loop_run_iterations": "循环轮数",
"core.chat.response.loop_run_history": "循环历史",
"core.chat.response.loop_input_element": "输入数组元素",
"core.chat.response.loop_output": "输出数组",
"core.chat.response.loop_output_element": "输出数组元素",
"core.chat.response.loop_run_history": "循环历史",
"core.chat.response.loop_run_input": "循环输入",
"core.chat.response.loop_run_iterations": "循环轮数",
"core.chat.response.module cq": "问题分类列表",
"core.chat.response.module cq result": "分类结果",
"core.chat.response.module extract description": "提取背景描述",
......@@ -418,6 +415,9 @@
"core.chat.response.module query": "问题/检索词",
"core.chat.response.module similarity": "相似度",
"core.chat.response.module temperature": "温度",
"core.chat.response.parallel_input": "并行输入数组",
"core.chat.response.parallel_output": "并行成功结果",
"core.chat.response.parallel_run_detail": "并行执行明细",
"core.chat.response.search using reRank": "结果重排",
"core.chat.response.text output": "文本输出",
"core.chat.response.update_var_result": "变量更新结果(按顺序展示多个变量更新结果)",
......@@ -464,9 +464,20 @@
"core.dataset.data.Updated": "已更新",
"core.dataset.data.group": "组",
"core.dataset.embedding model tip": "索引模型可以将知识库内容转成向量,用于进行语义检索。注意,不同索引模型的知识库无法同时查询,切换索引模型需重建全量向量索引,请慎重选择。",
"core.dataset.error.canNotEditAdminPermission": "不能编辑管理员权限",
"core.dataset.error.Data not found": "数据不存在或已被删除",
"core.dataset.error.invalidVectorModelOrQAModel": "索引模型或 QA 模型无效",
"core.dataset.error.noApiServer": "API 服务不存在",
"core.dataset.error.notSupportSync": "该集合不支持同步",
"core.dataset.error.sameApiCollection": "存在相同的 API 集合",
"core.dataset.error.Start Sync Failed": "开始同步失败",
"core.dataset.error.unAuthDataset": "无权操作该知识库",
"core.dataset.error.unAuthDatasetCollection": "无权操作该知识库集合",
"core.dataset.error.unAuthDatasetData": "无权操作该知识库数据",
"core.dataset.error.unAuthDatasetFile": "知识库文件鉴权失败",
"core.dataset.error.unCreateCollection": "无权创建知识库集合",
"core.dataset.error.unExistDataset": "知识库不存在",
"core.dataset.error.unLinkCollection": "无权操作该外部知识库集合",
"core.dataset.externalFile": "外部文件库",
"core.dataset.file": "文件",
"core.dataset.folder": "目录",
......@@ -527,18 +538,18 @@
"core.dataset.test.Test Result": "测试结果",
"core.dataset.test.Test Text": "单个文本测试",
"core.dataset.test.Test Text Placeholder": "输入需要测试的内容",
"core.dataset.test.Test params": "测试参数",
"core.dataset.test.delete test history": "删除该测试结果",
"core.dataset.test.image_expired": "图片已过期",
"core.dataset.test.image_search_disabled_tip": "请配置图片理解模型或多模态索引模型",
"core.dataset.test.image_token": "[图片]",
"core.dataset.test.input_title": "输入测试内容",
"core.dataset.test.max_images_tip": "最多支持上传10张图片",
"core.dataset.test.search_config": "搜索配置",
"core.dataset.test.Test params": "测试参数",
"core.dataset.test.upload_image": "上传图片",
"core.dataset.test.delete test history": "删除该测试结果",
"core.dataset.test.test history": "测试历史",
"core.dataset.test.test result placeholder": "测试结果将在这里展示",
"core.dataset.test.test result tip": "根据知识库内容与测试文本的相似度进行排序,你可以根据测试结果调整对应的文本。\n注意:测试记录中的数据可能已经被修改过,点击某条测试数据后将展示最新的数据。",
"core.dataset.test.upload_image": "上传图片",
"core.dataset.training.Auto mode": "补充索引",
"core.dataset.training.Auto mode Tip": "通过子索引以及调用模型生成相关问题与摘要,来增加数据块的语义丰富度,更利于检索。需要消耗更多的存储空间和增加 AI 调用次数。",
"core.dataset.training.Chunk mode": "分块存储",
......@@ -738,8 +749,8 @@
"dataset.data.Can not edit": "无编辑权限",
"dataset.data.Index Placeholder": "输入索引文本内容",
"dataset.data.Input Success Tip": "导入数据成功",
"dataset.data.Update Success Tip": "更新数据成功",
"dataset.data.Update Index Timeout Tip": "更新索引超时,请稍后刷新数据查看结果",
"dataset.data.Update Success Tip": "更新数据成功",
"dataset.data.edit.Index": "数据索引({{amount}})",
"dataset.dataset_name": "知识库名称",
"dataset.deleteFolderTips": "确认删除该文件夹及其包含的所有知识库?删除后数据无法恢复,请确认!",
......@@ -779,10 +790,11 @@
"error.s3_upload_timeout": "上传超时",
"error.send_auth_code_too_frequently": "请勿频繁获取验证码",
"error.too_many_request": "请求太频繁了,请稍后重试",
"error.upload_file_interval_limit": "短时间内上传次数过多或已达本轮上限,请稍后再试",
"error.tool_not_exist": "工具已删除",
"error.unKnow": "出现了点意外~",
"error.unAuthFile": "无权读取该文件",
"error.upload_file_error_filename": "{{name}} 上传失败",
"error.upload_file_interval_limit": "短时间内上传次数过多或已达本轮上限,请稍后再试",
"error.username_empty": "账号不能为空",
"error_collection_not_exist": "集合不存在",
"error_embedding_not_config": "未配置索引模型",
......@@ -1026,7 +1038,7 @@
"support.user.auth.get_code_again": "s后重新获取",
"support.user.captcha_placeholder": "请输入验证码",
"support.user.info.bind_notification_error": "绑定通知账号异常",
"support.user.info.bind_notification_hint": "请绑定通知接收账号,以确保您能正常接收套餐过期提醒等通知,保障您的服务正常运行。",
"support.user.info.bind_notification_hint": "请绑定通知接收账号,以确保您能及时正常接收账号信息、套餐信息等重要提醒。",
"support.user.info.bind_notification_success": "绑定通知账号成功",
"support.user.info.code_required": "验证码不能为空",
"support.user.info.notification_receiving_hint": "通知接收",
......@@ -1117,16 +1129,16 @@
"support.wallet.subscription.type.extraDatasetSize": "知识库扩容",
"support.wallet.subscription.type.extraPoints": "AI 积分套餐",
"support.wallet.subscription.type.standard": "套餐订阅",
"support.wallet.usage.Assist Generate Skill": "协助生成Skill",
"support.wallet.usage.Audio Speech": "语音播放",
"support.wallet.usage.Code Copilot": "代码助手",
"support.wallet.usage.Optimize Prompt": "提示词优化",
"support.wallet.usage.Total points": "积分总消耗",
"support.wallet.usage.Assist Generate Skill": "协助生成Skill",
"support.wallet.usage.Source": "来源",
"support.wallet.usage.Text Length": "文本长度",
"support.wallet.usage.Time": "生成时间",
"support.wallet.usage.Token Length": "token 长度",
"support.wallet.usage.Total": "总金额",
"support.wallet.usage.Total points": "积分总消耗",
"support.wallet.usage.Usage Detail": "使用详情",
"support.wallet.usage.Whisper": "语音输入",
"sync_link": "同步链接",
......
......@@ -136,28 +136,8 @@
"code_error.app_error.invalid_owner": "非法的應用程式擁有者",
"code_error.app_error.not_exist": "應用程式不存在",
"code_error.app_error.un_auth_app": "無權操作此應用程式",
"code_error.skill_error.not_exist": "技能不存在",
"code_error.skill_error.un_auth_skill": "無權操作該技能",
"code_error.skill_error.can_not_edit_admin_permission": "不能編輯管理員權限",
"code_error.skill_error.name_exists": "該目錄下已存在同名技能",
"code_error.skill_error.invalid_name": "技能名稱不能為空",
"code_error.skill_error.skill_name_too_long": "技能名稱不能超過 50 字元",
"code_error.skill_error.invalid_description": "描述不能超過 500 字元",
"code_error.skill_error.invalid_category": "無效的技能分類",
"code_error.skill_error.invalid_config": "配置超過最大限制(50KB)",
"code_error.skill_error.missing_model": "提供需求描述時必須指定 model",
"code_error.skill_error.requirements_too_long": "需求描述不能超過 8000 字元",
"code_error.skill_error.no_storage": "技能沒有存儲,無法複製",
"code_error.skill_error.no_fields_to_update": "沒有需要更新的欄位",
"code_error.skill_error.invalid_archive_format": "僅支援 ZIP、TAR、TAR.GZ 格式",
"code_error.skill_error.invalid_package": "無效的技能包結構",
"code_error.skill_error.invalid_skill_id": "無效的技能 ID",
"code_error.skill_error.archive_empty": "壓縮包內容為空",
"code_error.skill_error.archive_extraction_failed": "壓縮包解壓失敗",
"code_error.skill_error.archive_too_large": "壓縮包大小超過最大限制",
"code_error.skill_error.missing_image_repository": "提供 image 時必須指定 image.repository",
"code_error.chat_error.un_auth": "沒有權限操作此對話記錄",
"code_error.chat_error.chat_generating": "目前對話仍在生成中,請等待完成後再發起新的請求",
"code_error.chat_error.un_auth": "沒有權限操作此對話記錄",
"code_error.error_code.400": "請求失敗",
"code_error.error_code.401": "無存取權限",
"code_error.error_code.403": "禁止存取",
......@@ -182,6 +162,26 @@
"code_error.outlink_error.link_not_exist": "分享連結不存在",
"code_error.outlink_error.un_auth_user": "身份驗證失敗",
"code_error.plugin_error.un_auth": "無權操作該工具",
"code_error.skill_error.archive_empty": "壓縮包內容為空",
"code_error.skill_error.archive_extraction_failed": "壓縮包解壓失敗",
"code_error.skill_error.archive_too_large": "壓縮包大小超過最大限制",
"code_error.skill_error.can_not_edit_admin_permission": "不能編輯管理員權限",
"code_error.skill_error.invalid_archive_format": "僅支援 ZIP、TAR、TAR.GZ 格式",
"code_error.skill_error.invalid_category": "無效的技能分類",
"code_error.skill_error.invalid_config": "配置超過最大限制(50KB)",
"code_error.skill_error.invalid_description": "描述不能超過 500 字元",
"code_error.skill_error.invalid_name": "技能名稱不能為空",
"code_error.skill_error.invalid_package": "無效的技能包結構",
"code_error.skill_error.invalid_skill_id": "無效的技能 ID",
"code_error.skill_error.missing_image_repository": "提供 image 時必須指定 image.repository",
"code_error.skill_error.missing_model": "提供需求描述時必須指定 model",
"code_error.skill_error.name_exists": "該目錄下已存在同名技能",
"code_error.skill_error.no_fields_to_update": "沒有需要更新的欄位",
"code_error.skill_error.no_storage": "技能沒有存儲,無法複製",
"code_error.skill_error.not_exist": "技能不存在",
"code_error.skill_error.requirements_too_long": "需求描述不能超過 8000 字元",
"code_error.skill_error.skill_name_too_long": "技能名稱不能超過 50 字元",
"code_error.skill_error.un_auth_skill": "無權操作該技能",
"code_error.system_error.community_version_num_limit": "超出開源版數量限制,請升級商業版:https://fastgpt.io",
"code_error.system_error.license_app_amount_limit": "超出系統最大應用數量",
"code_error.system_error.license_dataset_amount_limit": "超出系統最大知識庫數量",
......@@ -239,10 +239,10 @@
"core.ai.Prompt": "提示詞",
"core.ai.Support tool": "工具調用",
"core.ai.model.Dataset Agent Model": "檔案處理模型",
"core.ai.model.multimodal": "多模態",
"core.ai.model.multimodal_tip": "多模態索引模型可以給圖片生成向量。",
"core.ai.model.Vector Model": "索引模型",
"core.ai.model.doc_index_and_dialog": "文件索引與對話索引",
"core.ai.model.multimodal": "多模態",
"core.ai.model.multimodal_tip": "多模態索引模型可以給圖片生成向量。",
"core.app.Api request": "API 存取",
"core.app.Api request desc": "通過 API 接入已有系統",
"core.app.App intro": "應用程式介紹",
......@@ -285,7 +285,6 @@
"core.app.feedback.Custom feedback": "自訂回饋",
"core.app.feedback.close custom feedback": "關閉回饋",
"core.app.have_saved": "已儲存",
"core.app.saving": "儲存中",
"core.app.no_app": "還沒有應用程式,快來建立一個吧!",
"core.app.not_saved": "未儲存",
"core.app.outLink.Can Drag": "圖示可拖曳",
......@@ -299,6 +298,7 @@
"core.app.outLink.Select Using Way": "選擇使用方式",
"core.app.outLink.Show History": "顯示歷史對話",
"core.app.publish.Fei shu bot publish": "發布到飛書機器人",
"core.app.saving": "儲存中",
"core.app.schedule.Default prompt": "預設問題",
"core.app.schedule.Default prompt placeholder": "執行應用程式時的預設問題",
"core.app.schedule.Every day": "每天 {{hour}}:00",
......@@ -392,15 +392,12 @@
"core.chat.response.Tool call tokens": "工具呼叫 Token 消耗",
"core.chat.response.context total length": "上下文總長度",
"core.chat.response.loop_input": "輸入陣列",
"core.chat.response.parallel_input": "並行輸入陣列",
"core.chat.response.parallel_output": "並行成功結果",
"core.chat.response.parallel_run_detail": "並行執行明細",
"core.chat.response.loop_run_input": "迴圈輸入",
"core.chat.response.loop_run_iterations": "迴圈輪數",
"core.chat.response.loop_run_history": "迴圈歷史",
"core.chat.response.loop_input_element": "輸入陣列元素",
"core.chat.response.loop_output": "輸出陣列",
"core.chat.response.loop_output_element": "輸出陣列元素",
"core.chat.response.loop_run_history": "迴圈歷史",
"core.chat.response.loop_run_input": "迴圈輸入",
"core.chat.response.loop_run_iterations": "迴圈輪數",
"core.chat.response.module cq": "問題分類列表",
"core.chat.response.module cq result": "分類結果",
"core.chat.response.module extract description": "提取背景描述",
......@@ -414,6 +411,9 @@
"core.chat.response.module query": "問題/搜尋詞",
"core.chat.response.module similarity": "相似度",
"core.chat.response.module temperature": "溫度",
"core.chat.response.parallel_input": "並行輸入陣列",
"core.chat.response.parallel_output": "並行成功結果",
"core.chat.response.parallel_run_detail": "並行執行明細",
"core.chat.response.search using reRank": "結果重新排名",
"core.chat.response.text output": "文字輸出",
"core.chat.response.update_var_result": "變數更新結果(依序顯示多個變數更新結果)",
......@@ -459,9 +459,20 @@
"core.dataset.data.Updated": "已更新",
"core.dataset.data.group": "組",
"core.dataset.embedding model tip": "索引模型可以將知識庫內容轉成向量,用於進行語意檢索。注意,不同索引模型的知識庫無法同時查詢,切換索引模型需重建全量向量索引,請慎重選擇。",
"core.dataset.error.canNotEditAdminPermission": "不能編輯管理員權限",
"core.dataset.error.Data not found": "資料不存在或已被刪除",
"core.dataset.error.invalidVectorModelOrQAModel": "索引模型或 QA 模型無效",
"core.dataset.error.noApiServer": "API 服務不存在",
"core.dataset.error.notSupportSync": "該集合不支援同步",
"core.dataset.error.sameApiCollection": "存在相同的 API 集合",
"core.dataset.error.Start Sync Failed": "開始同步失敗",
"core.dataset.error.unAuthDataset": "無權操作該知識庫",
"core.dataset.error.unAuthDatasetCollection": "無權操作該知識庫集合",
"core.dataset.error.unAuthDatasetData": "無權操作該知識庫資料",
"core.dataset.error.unAuthDatasetFile": "知識庫文件辨識失敗",
"core.dataset.error.unCreateCollection": "無權建立知識庫集合",
"core.dataset.error.unExistDataset": "知識庫不存在",
"core.dataset.error.unLinkCollection": "無權操作該外部知識庫集合",
"core.dataset.externalFile": "外部檔案庫",
"core.dataset.file": "檔案",
"core.dataset.folder": "目錄",
......@@ -522,18 +533,18 @@
"core.dataset.test.Test Result": "測試結果",
"core.dataset.test.Test Text": "單一文字測試",
"core.dataset.test.Test Text Placeholder": "輸入需要測試的內容",
"core.dataset.test.Test params": "測試參數",
"core.dataset.test.delete test history": "刪除此測試結果",
"core.dataset.test.image_expired": "圖片已過期",
"core.dataset.test.image_search_disabled_tip": "請配置圖片理解模型或多模態索引模型",
"core.dataset.test.image_token": "[圖片]",
"core.dataset.test.input_title": "輸入測試內容",
"core.dataset.test.max_images_tip": "最多支援上傳10張圖片",
"core.dataset.test.search_config": "搜尋設定",
"core.dataset.test.Test params": "測試參數",
"core.dataset.test.upload_image": "上傳圖片",
"core.dataset.test.delete test history": "刪除此測試結果",
"core.dataset.test.test history": "測試歷史",
"core.dataset.test.test result placeholder": "測試結果將顯示在這裡",
"core.dataset.test.test result tip": "根據知識庫內容與測試文字的相似度進行排序。您可以根據測試結果調整相應的文字。\n注意:測試記錄中的資料可能已經被修改。點選某筆測試資料後將顯示最新資料。",
"core.dataset.test.upload_image": "上傳圖片",
"core.dataset.training.Auto mode": "補充索引",
"core.dataset.training.Auto mode Tip": "透過子索引以及呼叫模型產生相關問題與摘要,來增加資料區塊的語意豐富度,更有利於檢索。需要消耗更多的儲存空間並增加 AI 呼叫次數。",
"core.dataset.training.Chunk mode": "分塊儲存",
......@@ -733,8 +744,8 @@
"dataset.data.Can not edit": "無編輯權限",
"dataset.data.Index Placeholder": "輸入索引文字內容",
"dataset.data.Input Success Tip": "匯入資料成功",
"dataset.data.Update Success Tip": "更新資料成功",
"dataset.data.Update Index Timeout Tip": "更新索引逾時,請稍後重新整理資料查看結果",
"dataset.data.Update Success Tip": "更新資料成功",
"dataset.data.edit.Index": "資料索引({{amount}})",
"dataset.dataset_name": "知識庫名稱",
"dataset.deleteFolderTips": "確認刪除此資料夾及其包含的所有知識庫?刪除後資料無法復原,請確認!",
......@@ -773,10 +784,11 @@
"error.s3_upload_timeout": "上傳超時",
"error.send_auth_code_too_frequently": "請勿頻繁取得驗證碼",
"error.too_many_request": "請求太頻繁了,請稍後重試",
"error.upload_file_interval_limit": "短時間內上傳次數過多或已達本輪上限,請稍後再試",
"error.tool_not_exist": "工具已刪除",
"error.unKnow": "發生未預期的錯誤",
"error.unAuthFile": "無權讀取該文件",
"error.upload_file_error_filename": "{{name}} 上傳失敗",
"error.upload_file_interval_limit": "短時間內上傳次數過多或已達本輪上限,請稍後再試",
"error.username_empty": "帳號不能為空",
"error_collection_not_exist": "集合不存在",
"error_embedding_not_config": "未設定索引模型",
......@@ -1015,7 +1027,7 @@
"support.user.auth.get_code_again": "秒後重新取得",
"support.user.captcha_placeholder": "請輸入驗證碼",
"support.user.info.bind_notification_error": "綁定通知帳號異常",
"support.user.info.bind_notification_hint": "請綁定通知接收帳號,確保您能正常接收套餐過期提醒等通知,保障您的服務正常運作。",
"support.user.info.bind_notification_hint": "請綁定通知接收帳號,以確保您能及時正常接收帳號資訊、套餐資訊等重要提醒。",
"support.user.info.bind_notification_success": "綁定通知帳號成功",
"support.user.info.code_required": "驗證碼不能為空",
"support.user.info.notification_receiving_hint": "通知接收",
......@@ -1106,16 +1118,16 @@
"support.wallet.subscription.type.extraDatasetSize": "知識庫擴充容量",
"support.wallet.subscription.type.extraPoints": "AI 點數方案",
"support.wallet.subscription.type.standard": "方案訂閱",
"support.wallet.usage.Assist Generate Skill": "協助生成skill",
"support.wallet.usage.Audio Speech": "語音播放",
"support.wallet.usage.Code Copilot": "代碼助手",
"support.wallet.usage.Optimize Prompt": "提示詞優化",
"support.wallet.usage.Total points": "積分總消耗",
"support.wallet.usage.Assist Generate Skill": "協助生成skill",
"support.wallet.usage.Source": "來源",
"support.wallet.usage.Text Length": "文字長度",
"support.wallet.usage.Time": "產生時間",
"support.wallet.usage.Token Length": "Token 長度",
"support.wallet.usage.Total": "總金額",
"support.wallet.usage.Total points": "積分總消耗",
"support.wallet.usage.Usage Detail": "使用詳細資訊",
"support.wallet.usage.Whisper": "語音輸入",
"sync_link": "同步連結",
......
import React, { useMemo } from 'react';
import { Box, useTheme } from '@chakra-ui/react';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import type { SearchDataResponseQuoteListItemType } from '@fastgpt/global/core/dataset/type';
import QuoteItem, { formatScore } from '@/components/core/dataset/QuoteItem';
import { useContextSelector } from 'use-context-selector';
import { WorkflowRuntimeContext } from '../../context/workflowRuntimeContext';
......@@ -15,7 +15,7 @@ const QuoteList = React.memo(function QuoteList({
rawSearch = []
}: {
chatItemDataId?: string;
rawSearch: SearchDataResponseItemType[];
rawSearch: SearchDataResponseQuoteListItemType[];
}) {
const theme = useTheme();
const { appId, outLinkAuthData } = useChatStore();
......@@ -62,7 +62,7 @@ const QuoteList = React.memo(function QuoteList({
};
}
return { ...item, q: item.q || '' };
return { ...item, q: 'q' in item ? item.q : '' };
});
return processedData.sort((a, b) => {
......
import React, { useMemo, useState } from 'react';
import { Flex, useDisclosure, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import type { SearchDataResponseQuoteListItemType } from '@fastgpt/global/core/dataset/type';
import dynamic from 'next/dynamic';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
......@@ -95,7 +95,7 @@ const ResponseTags = ({
// Dataset citations
const datasetItems = Object.values(
quoteList.reduce((acc: Record<string, SearchDataResponseItemType[]>, cur) => {
quoteList.reduce((acc: Record<string, SearchDataResponseQuoteListItemType[]>, cur) => {
if (!acc[cur.collectionId]) {
acc[cur.collectionId] = [cur];
}
......@@ -107,9 +107,10 @@ const ResponseTags = ({
type: 'dataset' as const,
key: item.collectionId,
displayText: item.sourceName,
icon: item.imageId
? 'core/dataset/imageFill'
: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }),
icon:
'imageId' in item && item.imageId
? 'core/dataset/imageFill'
: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }),
onClick: () => {
onOpenCiteModal({
collectionId: item.collectionId,
......
import React, { useMemo, useState } from 'react';
import { Box, Flex, Link, Progress } from '@chakra-ui/react';
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import type { SearchDataResponseQuoteItemType } from '@fastgpt/global/core/dataset/type';
import NextLink from 'next/link';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
......@@ -85,7 +85,11 @@ const QuoteItem = ({
canEditDataset,
...RawSourceBoxProps
}: {
quoteItem: SearchDataResponseItemType;
quoteItem: SearchDataResponseQuoteItemType & {
q: string;
a?: string;
imagePreivewUrl?: string;
};
canDownloadSource?: boolean;
canEditData?: boolean;
canEditDataset?: boolean;
......
......@@ -5,7 +5,7 @@ import type {
ToolCiteLinksType,
ErrorTextItemType
} from '@fastgpt/global/core/chat/type';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import type { SearchDataResponseQuoteListItemType } from '@fastgpt/global/core/dataset/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getFlatAppResponses } from '@fastgpt/global/core/chat/utils';
import { sandboxToolMap } from '@fastgpt/global/core/ai/sandbox/tools';
......@@ -32,8 +32,8 @@ export function transformPreviewHistories(
const extractCitationIdsFromText = (text: string): string[] => {
if (!text) return [];
// Match [24-bit hexadecimal ID](CITE) format
const citeRegex = /\[([a-f0-9]{24})\]\(CITE\)/gi;
// Match [24-bit hexadecimal ID](CITE|QUOTE) format. Markdown rendering supports both.
const citeRegex = /\[([a-f0-9]{24})\]\((?:CITE|QUOTE)\)/gi;
const matches = text.match(citeRegex);
if (!matches) return [];
......@@ -111,7 +111,7 @@ export function addStatisticalDataToHistoryItem(historyItem: ChatItemMiniType) {
},
{
useAgentSandbox: false,
totalQuoteList: [] as SearchDataResponseItemType[],
totalQuoteList: [] as SearchDataResponseQuoteListItemType[],
toolCiteLinks: [] as ToolCiteLinksType[],
linkDedupe: new Set<string>(),
errorText: undefined as ErrorTextItemType | undefined,
......
import Markdown from '@/components/Markdown';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { type Dispatch, type MutableRefObject, type SetStateAction, useState } from 'react';
import { type MutableRefObject, useState } from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
......@@ -23,7 +23,7 @@ const CollectionQuoteItem = ({
}: {
quoteRefs: MutableRefObject<Map<string, HTMLDivElement | null>>;
quoteIndex: number;
setQuoteIndex: Dispatch<SetStateAction<number>>;
setQuoteIndex: (quoteIndex: number) => void;
refreshList: () => void;
canEdit: boolean;
......
import { Box, Flex, HStack } from '@chakra-ui/react';
import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { type SearchDataResponseQuoteListItemType } from '@fastgpt/global/core/dataset/type';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
......@@ -28,7 +28,7 @@ const CollectionReader = ({
metadata,
onClose
}: {
rawSearch: SearchDataResponseItemType[];
rawSearch: SearchDataResponseQuoteListItemType[];
metadata: GetCollectionQuoteDataProps;
onClose: () => void;
}) => {
......@@ -39,7 +39,7 @@ const CollectionReader = ({
const canDownloadSource = useContextSelector(ChatItemContext, (v) => v.canDownloadSource);
const { collectionId, datasetId, chatItemDataId, sourceId, sourceName, quoteId } = metadata;
const [quoteIndex, setQuoteIndex] = useState(0);
const [selectedQuote, setSelectedQuote] = useState<{ sourceQuoteId?: string; id: string }>();
// Get dataset permission
const { data: datasetData } = useRequest(async () => await getDatasetPermission(datasetId), {
......@@ -48,7 +48,7 @@ const CollectionReader = ({
});
const filterResults = useMemo(() => {
const res = rawSearch
return rawSearch
.filter((item) => item.collectionId === collectionId)
.sort((a, b) => {
const chunkDiff = (a.chunkIndex || 0) - (b.chunkIndex || 0);
......@@ -56,15 +56,28 @@ const CollectionReader = ({
return a.id.localeCompare(b.id);
});
}, [collectionId, rawSearch]);
if (quoteId) {
setQuoteIndex(res.findIndex((item) => item.id === quoteId));
} else {
setQuoteIndex(0);
}
const quoteIndex = useMemo(() => {
const selectedQuoteId =
selectedQuote && selectedQuote.sourceQuoteId === quoteId ? selectedQuote.id : quoteId;
if (!selectedQuoteId) return 0;
return Math.max(
filterResults.findIndex((item) => item.id === selectedQuoteId),
0
);
}, [filterResults, quoteId, selectedQuote]);
return res;
}, [collectionId, quoteId, rawSearch]);
const setQuoteIndex = (index: number) => {
const nextQuote = filterResults[index];
if (!nextQuote) return;
setSelectedQuote({
sourceQuoteId: quoteId,
id: nextQuote.id
});
};
const currentQuoteItem = useMemo(() => {
const item = filterResults[quoteIndex];
......@@ -281,7 +294,7 @@ const CollectionReader = ({
{isLoading || datasetDataList.length > 0 ? (
<ScrollData flex={'1 0 0'} mt={2} px={5} py={1}>
<Flex flexDir={'column'}>
{formatedDataList.map((item, index) => (
{formatedDataList.map((item) => (
<CollectionQuoteItem
key={item._id}
quoteRefs={itemRefs as React.MutableRefObject<Map<string, HTMLDivElement | null>>}
......
import { Box, Flex } from '@chakra-ui/react';
import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { type SearchDataResponseQuoteListItemType } from '@fastgpt/global/core/dataset/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useTranslation } from 'next-i18next';
......@@ -16,7 +16,7 @@ const QuoteReader = ({
metadata,
onClose
}: {
rawSearch: SearchDataResponseItemType[];
rawSearch: SearchDataResponseQuoteListItemType[];
metadata: GetAllQuoteDataProps;
onClose: () => void;
}) => {
......
import React from 'react';
import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { type SearchDataResponseQuoteListItemType } from '@fastgpt/global/core/dataset/type';
import { type GetQuoteProps } from '@/web/core/chat/context/chatItemContext';
import CollectionQuoteReader from './CollectionQuoteReader';
import QuoteReader from './QuoteReader';
......@@ -9,7 +9,7 @@ const ChatQuoteList = ({
metadata,
onClose
}: {
rawSearch: SearchDataResponseItemType[];
rawSearch: SearchDataResponseQuoteListItemType[];
metadata: GetQuoteProps;
onClose: () => void;
}) => {
......
......@@ -50,7 +50,7 @@ async function handler(req: ApiRequestProps): Promise<GetCollectionQuoteResType>
teamToken
}),
MongoChatItem.findOne({ appId, chatId, dataId: chatItemDataId }, 'time').lean(),
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
authCollectionInChat({ appId, chatId, collectionIds: [collectionId] })
]);
if (!showFullText || !chat || !chatItem || initialAnchor === undefined) {
......
......@@ -38,7 +38,7 @@ async function handler(req: ApiRequestProps): Promise<GetQuoteResponseType> {
teamToken
}),
MongoChatItem.findOne({ appId, chatId, dataId: chatItemDataId }, 'time').lean(),
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: collectionIdList })
authCollectionInChat({ appId, chatId, collectionIds: collectionIdList })
]);
if (!chat || !chatItem || !showCite) return Promise.reject(ChatErrEnum.unAuthChat);
......
......@@ -41,8 +41,7 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) {
};
}
const { appId, chatId, chatItemDataId, shareId, outLinkUid, teamId, teamToken, chatTime } =
parseBody;
const { appId, chatId, shareId, outLinkUid, teamId, teamToken, chatTime } = parseBody;
/*
1. auth chat read permission
2. auth collection quote in chat
......@@ -60,7 +59,7 @@ async function handler(req: ApiRequestProps, res: NextApiResponse) {
teamToken
}),
getCollectionWithDataset(collectionId),
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
authCollectionInChat({ appId, chatId, collectionIds: [collectionId] })
]);
if (!authRes.canDownloadSource) {
......
......@@ -48,7 +48,7 @@ async function handler(req: ApiRequestProps): Promise<ReadCollectionSourceRespon
teamToken
}),
getCollectionWithDataset(collectionId),
authCollectionInChat({ appId, chatId, chatItemDataId, collectionIds: [collectionId] })
authCollectionInChat({ appId, chatId, collectionIds: [collectionId] })
]);
if (!authRes.canDownloadSource) {
......
......@@ -23,7 +23,7 @@ async function handler(req: ApiRequestProps): Promise<GetQuoteDataResponse> {
// Auth
const { collection, q, a } = await (async () => {
if (body.chatId && body.appId && body.chatItemDataId) {
const { appId, chatId, shareId, outLinkUid, teamId, teamToken, chatItemDataId } = body;
const { appId, chatId, shareId, outLinkUid, teamId, teamToken } = body;
await authChatCrud({
req,
authToken: true,
......@@ -55,7 +55,6 @@ async function handler(req: ApiRequestProps): Promise<GetQuoteDataResponse> {
authCollectionInChat({
appId,
chatId,
chatItemDataId,
collectionIds: [datasetData.collectionId]
})
]);
......
import { type ChatHistoryItemResType, type ChatSchemaType } from '@fastgpt/global/core/chat/type';
import { type ChatSchemaType } from '@fastgpt/global/core/chat/type';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { type AuthModeType } from '@fastgpt/service/support/permission/type';
import { authOutLink } from './outLink';
......@@ -8,13 +8,12 @@ import { AuthUserTypeEnum, ReadPermissionVal } from '@fastgpt/global/support/per
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import { getFlatAppResponses } from '@fastgpt/global/core/chat/utils';
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import type { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { MongoHelperBotChat } from '@fastgpt/service/core/chat/HelperBot/chatSchema';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { Types } from 'mongoose';
/*
检查chat的权限:
......@@ -237,69 +236,54 @@ export async function authChatCrud({
return Promise.reject(ChatErrEnum.unAuthChat);
}
/**
* 校验文档是否来自当前会话引用。
*
* 只依赖 ChatItem 上的 citeCollectionIds 判断 collection 是否在当前会话中被引用,
* 避免读取和解析完整 responseData。
*/
export const authCollectionInChat = async ({
collectionIds,
appId,
chatId,
chatItemDataId
chatId
}: {
collectionIds: string[];
appId: string;
chatId: string;
chatItemDataId: string;
}) => {
try {
// 1. 使用 citeCollectionIds 字段来判断
const chatItems = await MongoChatItem.find(
{
appId,
const appObjectId = Types.ObjectId.isValid(String(appId))
? new Types.ObjectId(String(appId))
: appId;
const targetCollectionIds = collectionIds.map(String);
const [authResult] = await MongoChatItem.aggregate<{ isAuthorized: boolean }>([
{
$match: {
appId: appObjectId,
chatId,
obj: ChatRoleEnum.AI
},
'citeCollectionIds'
)
.sort({ _id: -1 })
.limit(50)
.lean();
const citeCollectionIds = new Set(
chatItems.map((item) => ('citeCollectionIds' in item ? item.citeCollectionIds : [])).flat()
);
if (collectionIds.every((id) => citeCollectionIds.has(id))) {
return;
}
// Adapt <=4.13.0
const chatItem = (await MongoChatItem.findOne(
{
appId,
chatId,
dataId: chatItemDataId
},
'responseData'
).lean()) as { time: Date; responseData?: ChatHistoryItemResType[] };
if (!chatItem) return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
// Concat response data
if (!chatItem.responseData || chatItem.responseData.length === 0) {
const chatItemResponses = await MongoChatItemResponse.find(
{ appId, chatId, chatItemDataId },
{ data: 1 }
).lean();
chatItem.responseData = chatItemResponses.map((item) => item.data);
}
},
{ $sort: { _id: -1 } },
{ $limit: 50 },
{ $unwind: '$citeCollectionIds' },
{
$group: {
_id: null,
citeCollectionIds: { $addToSet: { $toString: '$citeCollectionIds' } }
}
},
{
$project: {
_id: 0,
isAuthorized: { $setIsSubset: [targetCollectionIds, '$citeCollectionIds'] }
}
}
]);
// 找 responseData 里,是否有该文档 id
const flatResData = getFlatAppResponses(chatItem.responseData || []);
const quoteListSet = new Set(
flatResData.map((item) => item.quoteList?.map((quote) => quote.collectionId) || []).flat()
);
if (collectionIds.every((id) => quoteListSet.has(id))) {
return;
}
} catch (error) {}
if (authResult?.isAuthorized) {
return;
}
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
};
......
import { type ChatBoxInputFormType } from '@/components/core/chat/ChatContainer/ChatBox/type';
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
import React, { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { type ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import { createContext } from 'use-context-selector';
import { type ComponentRef as ChatComponentRef } from '@/components/core/chat/ChatContainer/ChatBox/type';
import { useForm, type UseFormReturn } from 'react-hook-form';
......@@ -8,7 +8,7 @@ import { defaultChatData } from '@/global/core/chat/constants';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { type AppChatConfigType, type VariableItemType } from '@fastgpt/global/core/app/type';
import { type FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { type SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { type SearchDataResponseQuoteListItemType } from '@fastgpt/global/core/dataset/type';
import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import type { ChatGenerateStatusEnum } from '@fastgpt/global/core/chat/constants';
......@@ -65,7 +65,7 @@ export type GetAllQuoteDataProps = GetQuoteDataBasicProps & {
};
export type GetQuoteProps = GetAllQuoteDataProps | GetCollectionQuoteDataProps;
export type QuoteDataType = {
rawSearch: SearchDataResponseItemType[];
rawSearch: SearchDataResponseQuoteListItemType[];
metadata: GetQuoteProps;
};
export type OnOpenCiteModalProps = {
......@@ -131,7 +131,7 @@ export const ChatItemContext = createContext<ChatItemContextType>({
}
});
/*
/*
Chat 对象的上下文
*/
const ChatItemContextProvider = ({
......
......@@ -201,7 +201,6 @@ describe('getQuoteData handler', () => {
expect(authCollectionInChatMock).toHaveBeenCalledWith({
appId: VALID_APP_ID,
chatId: 'chat_123',
chatItemDataId: 'item_456',
collectionIds: ['col_1']
});
expect(authDatasetDataMock).not.toHaveBeenCalled();
......
......@@ -85,4 +85,38 @@ describe('addStatisticalDataToHistoryItem', () => {
expect(addStatisticalDataToHistoryItem(historyItem).useAgentSandbox).toBe(false);
});
it('includes dataset quote tags that use QUOTE markdown links', () => {
const quoteId = '507f1f77bcf86cd799439011';
const historyItem: ChatItemMiniType = {
obj: ChatRoleEnum.AI,
value: [
{
text: {
content: `done [${quoteId}](QUOTE)`
}
}
],
responseData: [
{
id: 'dataset-response',
nodeId: 'dataset-node',
moduleName: 'Dataset Search',
moduleType: FlowNodeTypeEnum.datasetSearchNode,
quoteList: [
{
id: quoteId,
chunkIndex: 0,
datasetId: 'dataset-1',
collectionId: 'collection-1',
sourceName: 'doc.pdf',
score: [{ type: 'embedding', value: 0.9, index: 0 }]
}
]
}
]
};
expect(addStatisticalDataToHistoryItem(historyItem).totalQuoteList).toHaveLength(1);
});
});
......@@ -2,7 +2,6 @@ import { describe, expect, it, vi, beforeEach } from 'vitest';
import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
......@@ -10,12 +9,10 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { getFlatAppResponses } from '@fastgpt/global/core/chat/utils';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { OutLinkSchemaType } from '@fastgpt/global/support/outLink/type';
import { Types } from 'mongoose';
vi.mock('@fastgpt/service/core/chat/chatSchema', () => ({
MongoChat: {
......@@ -25,14 +22,7 @@ vi.mock('@fastgpt/service/core/chat/chatSchema', () => ({
vi.mock('@fastgpt/service/core/chat/chatItemSchema', () => ({
MongoChatItem: {
findOne: vi.fn(),
find: vi.fn()
}
}));
vi.mock('@fastgpt/service/core/chat/chatItemResponseSchema', () => ({
MongoChatItemResponse: {
find: vi.fn()
aggregate: vi.fn()
}
}));
......@@ -49,9 +39,6 @@ vi.mock('@fastgpt/service/core/app/schema', async (importOriginal) => {
vi.mock('@fastgpt/service/support/permission/app/auth');
vi.mock('@/service/support/permission/auth/outLink');
vi.mock('@/service/support/permission/auth/team');
vi.mock('@fastgpt/global/core/chat/utils', () => ({
getFlatAppResponses: vi.fn()
}));
const buildOutLinkConfig = (
overrides: Partial<OutLinkSchemaType> = {},
......@@ -84,22 +71,6 @@ const buildOutLinkConfig = (
return config;
};
const buildQuoteList = (...ids: (string | undefined)[]): any[] =>
ids.map((id) => (id ? { collectionId: id } : {}));
const buildResponse = (
overrides: Partial<ChatHistoryItemResType> = {}
): ChatHistoryItemResType => ({
nodeId: 'node1',
id: 'response1',
moduleType: FlowNodeTypeEnum.appModule,
moduleName: 'module',
...overrides
});
const buildQuoteResponse = (...ids: (string | undefined)[]): ChatHistoryItemResType =>
buildResponse({ quoteList: buildQuoteList(...ids) });
describe('authChatCrud', () => {
beforeEach(() => {
vi.clearAllMocks();
......@@ -715,334 +686,81 @@ describe('authChatCrud', () => {
describe('authCollectionInChat', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(MongoChatItem.find).mockReturnValue({
sort: () => ({
limit: () => ({
lean: () => Promise.resolve([])
})
})
} as any);
vi.mocked(MongoChatItem.aggregate).mockResolvedValue([]);
});
describe('validation', () => {
it('should reject if chat item not found', async () => {
// Mock the find method to return empty array (no cite collection ids found)
vi.mocked(MongoChatItem.find).mockReturnValue({
sort: () => ({
limit: () => ({
lean: () => Promise.resolve([])
})
})
} as any);
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(null)
} as any);
await expect(
authCollectionInChat({
collectionIds: ['col1'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
});
it('should reject with empty collectionIds array', async () => {
// Mock the find method to return empty array (no cite collection ids found)
vi.mocked(MongoChatItem.find).mockReturnValue({
sort: () => ({
limit: () => ({
lean: () => Promise.resolve([])
})
})
} as any);
const mockChatItem = {
time: new Date(),
responseData: []
};
it('should authorize when aggregation confirms all collection ids are cited', async () => {
vi.mocked(MongoChatItem.aggregate).mockResolvedValue([{ isAuthorized: true }]);
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(MongoChatItemResponse.find).mockReturnValue({
lean: () => Promise.resolve([])
} as any);
vi.mocked(getFlatAppResponses).mockReturnValue([]);
const result = await authCollectionInChat({
collectionIds: [],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
});
expect(result).toEqual(undefined);
const result = await authCollectionInChat({
collectionIds: ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012'],
appId: '507f1f77bcf86cd799439010',
chatId: 'chat1'
});
it('should handle missing appId, chatId, or chatItemDataId', async () => {
await expect(
authCollectionInChat({
collectionIds: ['col1'],
appId: '',
chatId: 'chat1',
chatItemDataId: 'item1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
});
expect(result).toBeUndefined();
});
describe('response data handling', () => {
it('should auth collection ids in chat item with existing responseData', async () => {
// Mock the find method to return empty array (no cite collection ids found)
vi.mocked(MongoChatItem.find).mockReturnValue({
sort: () => ({
limit: () => ({
lean: () => Promise.resolve([])
})
})
} as any);
const mockChatItem = {
time: new Date(),
citeCollectionIds: ['col1', 'col2']
};
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(getFlatAppResponses).mockReturnValue([buildQuoteResponse('col1', 'col2')]);
const result = await authCollectionInChat({
collectionIds: ['col1', 'col2'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
});
expect(result).toEqual(undefined);
});
it('should fetch responseData from MongoChatItemResponse when missing', async () => {
const mockChatItem: { time: Date; citeCollectionIds: string[]; responseData?: any[] } = {
time: new Date(),
citeCollectionIds: ['col1', 'col2']
};
const mockChatItemResponses = [
{ data: buildQuoteResponse('col1') },
{ data: buildQuoteResponse('col2') }
];
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(MongoChatItemResponse.find).mockReturnValue({
lean: () => Promise.resolve(mockChatItemResponses)
} as any);
vi.mocked(getFlatAppResponses).mockReturnValue([
buildQuoteResponse('col1'),
buildQuoteResponse('col2')
]);
const result = await authCollectionInChat({
collectionIds: ['col1', 'col2'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
});
expect(mockChatItem.responseData).toEqual([
buildQuoteResponse('col1'),
buildQuoteResponse('col2')
]);
expect(result).toEqual(undefined);
});
it('should reject when aggregation does not confirm all collection ids', async () => {
vi.mocked(MongoChatItem.aggregate).mockResolvedValue([{ isAuthorized: false }]);
it('should handle empty responseData array', async () => {
const mockChatItem = {
time: new Date(),
responseData: []
};
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(MongoChatItemResponse.find).mockReturnValue({
lean: () => Promise.resolve([])
} as any);
vi.mocked(getFlatAppResponses).mockReturnValue([]);
await expect(
authCollectionInChat({
collectionIds: ['col1'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
});
it('should handle plugin, tool and loop details in response data', async () => {
const mockChatItem = {
time: new Date(),
responseData: [
buildResponse({
quoteList: buildQuoteList('col1'),
pluginDetail: [buildQuoteResponse('col2')],
toolDetail: [buildQuoteResponse('col3')],
loopDetail: [buildQuoteResponse('col4')]
})
]
};
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(getFlatAppResponses).mockReturnValue([
buildQuoteResponse('col1'),
buildQuoteResponse('col2'),
buildQuoteResponse('col3'),
buildQuoteResponse('col4')
]);
const result = await authCollectionInChat({
collectionIds: ['col1', 'col2', 'col3', 'col4'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
});
expect(result).toEqual(undefined);
});
it('should reject if collection ids not found in quotes', async () => {
const mockChatItem = {
time: new Date(),
responseData: [buildQuoteResponse('col1')]
};
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(getFlatAppResponses).mockReturnValue([buildQuoteResponse('col1')]);
await expect(
authCollectionInChat({
collectionIds: ['col2'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
});
it('should reject if only some collection ids are found', async () => {
const mockChatItem = {
time: new Date(),
responseData: [buildQuoteResponse('col1', 'col2')]
};
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(getFlatAppResponses).mockReturnValue([buildQuoteResponse('col1', 'col2')]);
await expect(
authCollectionInChat({
collectionIds: ['col1', 'col2', 'col3'], // col3 not found
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
});
it('should handle quotes with missing collectionId', async () => {
const mockChatItem = {
time: new Date(),
responseData: [buildQuoteResponse('col1', undefined)]
};
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(getFlatAppResponses).mockReturnValue([buildQuoteResponse('col1', undefined)]);
const result = await authCollectionInChat({
collectionIds: ['col1'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
});
expect(result).toEqual(undefined);
});
it('should handle missing quoteList in response data', async () => {
const mockChatItem = {
time: new Date(),
responseData: [
{
// no quoteList
}
]
};
await expect(
authCollectionInChat({
collectionIds: ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012'],
appId: '507f1f77bcf86cd799439010',
chatId: 'chat1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
});
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(getFlatAppResponses).mockReturnValue([]);
it('should reject when aggregation returns no result', async () => {
vi.mocked(MongoChatItem.aggregate).mockResolvedValue([]);
await expect(
authCollectionInChat({
collectionIds: ['col1'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
});
await expect(
authCollectionInChat({
collectionIds: ['507f1f77bcf86cd799439011'],
appId: '507f1f77bcf86cd799439010',
chatId: 'chat1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
});
describe('error handling', () => {
it('should reject if database query throws error', async () => {
vi.mocked(MongoChatItem.findOne).mockImplementation(() => {
throw new Error('Database error');
});
it('should cast appId and compare stored citeCollectionIds as strings', async () => {
vi.mocked(MongoChatItem.aggregate).mockResolvedValue([{ isAuthorized: true }]);
const appId = '507f1f77bcf86cd799439010';
const collectionIds = ['507f1f77bcf86cd799439011', '507f1f77bcf86cd799439012'];
await expect(
authCollectionInChat({
collectionIds: ['col1'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
await authCollectionInChat({
collectionIds,
appId,
chatId: 'chat1'
});
it('should reject if getFlatAppResponses throws error', async () => {
const mockChatItem = {
time: new Date(),
responseData: [{}]
};
vi.mocked(MongoChatItem.findOne).mockReturnValue({
lean: () => Promise.resolve(mockChatItem)
} as any);
vi.mocked(getFlatAppResponses).mockImplementation(() => {
throw new Error('Processing error');
});
const pipeline = vi.mocked(MongoChatItem.aggregate).mock.calls[0][0];
await expect(
authCollectionInChat({
collectionIds: ['col1'],
appId: 'app1',
chatId: 'chat1',
chatItemDataId: 'item1'
})
).rejects.toBe(DatasetErrEnum.unAuthDatasetFile);
expect(pipeline[0]).toEqual({
$match: {
appId: new Types.ObjectId(appId),
chatId: 'chat1',
obj: 'AI'
}
});
expect(pipeline).toContainEqual({ $sort: { _id: -1 } });
expect(pipeline).toContainEqual({ $limit: 50 });
expect(pipeline).toContainEqual({ $unwind: '$citeCollectionIds' });
expect(pipeline).toContainEqual({
$group: {
_id: null,
citeCollectionIds: { $addToSet: { $toString: '$citeCollectionIds' } }
}
});
expect(pipeline).toContainEqual({
$project: {
_id: 0,
isAuthorized: {
$setIsSubset: [collectionIds, '$citeCollectionIds']
}
}
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment