Commit 870ea89b by Archer Committed by GitHub

perf: invalid quote filter (#7139)

* perf: invalid quote filter

* doc

* perf: user edit

* fxi: template

* fix: tempalte init
parent 9796796b
......@@ -28,6 +28,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initSandboxArchive' \
2. 优化应用、知识库、文件和文件夹等长名称展示:超出宽度时自动省略,并在 hover 名称时展示完整内容。
3. 移除所有内置 LLM 请求中的 `temperature` 和 `max_tokens`,避免部分模型不兼容。
4. 知识库训练出现错误时的提示,同时支持一键全部重试。
5. 过滤掉无效的知识库引用角标。
## 🐛 修复
......
......@@ -25,8 +25,8 @@
"content/guide/build/publish/mcp_server.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/publish/official_account.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/publish/official_account.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/publish/openapi.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/publish/openapi.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/publish/openapi.en.mdx": "2026-06-18T10:43:42+08:00",
"content/guide/build/publish/openapi.mdx": "2026-06-18T10:43:42+08:00",
"content/guide/build/publish/wechat.en.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/publish/wechat.mdx": "2026-05-07T15:06:40+08:00",
"content/guide/build/publish/wecom.en.mdx": "2026-05-07T15:06:40+08:00",
......@@ -139,14 +139,14 @@
"content/guide/workspace/team/team_roles_permissions.mdx": "2026-05-07T15:06:40+08:00",
"content/openapi/app.en.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/app.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/chat.en.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/chat.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/chat.en.mdx": "2026-06-18T10:43:42+08:00",
"content/openapi/chat.mdx": "2026-06-18T10:43:42+08:00",
"content/openapi/dataset.en.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/dataset.mdx": "2026-05-29T19:31:16+08:00",
"content/openapi/index.en.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/index.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/intro.en.mdx": "2026-05-29T21:03:14+08:00",
"content/openapi/intro.mdx": "2026-05-29T21:03:14+08:00",
"content/openapi/intro.en.mdx": "2026-06-18T10:43:42+08:00",
"content/openapi/intro.mdx": "2026-06-18T10:43:42+08:00",
"content/openapi/share.en.mdx": "2026-04-26T21:08:47+08:00",
"content/openapi/share.mdx": "2026-04-26T21:08:47+08:00",
"content/plugin/index.en.mdx": "2026-06-04T16:10:15+08:00",
......@@ -285,7 +285,7 @@
"content/self-host/upgrading/4-15/41504.en.mdx": "2026-06-10T19:02:59+08:00",
"content/self-host/upgrading/4-15/41504.mdx": "2026-06-15T23:34:43+08:00",
"content/self-host/upgrading/4-15/41505.en.mdx": "2026-06-12T20:47:04+08:00",
"content/self-host/upgrading/4-15/41505.mdx": "2026-06-17T17:02:34+08:00",
"content/self-host/upgrading/4-15/41505.mdx": "2026-06-17T18:19:09+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",
......
......@@ -9,11 +9,48 @@ const getFileTemplates = async (): Promise<AppTemplateSchemaType[]> => {
return (await pluginClient.listWorkflows()) as AppTemplateSchemaType[];
};
const formatTemplateAvatar = (avatar?: string | null) => {
if (!avatar) {
return '';
}
if (avatar.startsWith('/') || avatar.startsWith('http://') || avatar.startsWith('https://')) {
return avatar;
}
return `/${avatar}`;
};
/**
* 提取系统模板允许由数据库覆盖的运营字段。
*
* 系统模板来自 plugin 服务,workflow、userGuide、type 等内容字段必须始终以 plugin 返回为准。
* 数据库只作为展示卡片、上下线、推荐和标签等运营配置层,避免后台保存的旧 workflow 覆盖新版模板。
*/
const pickPluginSystemTemplateEditableConfig = (config: AppTemplateSchemaType) => ({
...Object.fromEntries(
Object.entries({
name: config.name,
intro: config.intro,
avatar: formatTemplateAvatar(config.avatar),
tags: config.tags,
isActive: config.isActive,
isPromoted: config.isPromoted,
promoteTags: config.promoteTags,
hideTags: config.hideTags,
recommendText: config.recommendText,
isQuickTemplate: config.isQuickTemplate,
order: config.order
}).filter(([, value]) => value !== undefined)
)
});
const getAppTemplates = async () => {
const originCommunityTemplates = await getFileTemplates();
const communityTemplates = originCommunityTemplates.map((template) => {
return {
...template,
avatar: formatTemplateAvatar(template.avatar),
templateId: `${AppToolSourceEnum.community}-${template.templateId.split('.')[0]}`
};
});
......@@ -26,8 +63,9 @@ const getAppTemplates = async () => {
if (config) {
return {
...config,
...template,
...config
...pickPluginSystemTemplateEditableConfig(config)
};
}
......@@ -72,5 +110,13 @@ export const getAppTemplatesAndLoadThem = async (refresh = false) => {
};
export const isCommercialTemaplte = (templateId: string) => {
return templateId.startsWith(AppToolSourceEnum.commercial);
return templateId.startsWith(`${AppToolSourceEnum.commercial}-`);
};
/**
* 判断模板是否来自 fastgpt-plugin 的系统模板列表。
* 系统模板的 workflow 和 userGuide 由 plugin 服务维护,本地数据库只保存展示和上下线等覆盖配置。
*/
export const isPluginSystemTemplate = (templateId: string) => {
return templateId.startsWith(`${AppToolSourceEnum.community}-`);
};
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
const mocks = vi.hoisted(() => ({
listWorkflows: vi.fn(),
findTemplates: vi.fn()
}));
vi.mock('@fastgpt/service/thirdProvider/fastgptPlugin', () => ({
pluginClient: {
listWorkflows: mocks.listWorkflows
}
}));
vi.mock('@fastgpt/service/core/app/templates/templateSchema', () => ({
MongoAppTemplate: {
find: mocks.findTemplates
}
}));
import { getAppTemplatesAndLoadThem } from '@fastgpt/service/core/app/templates/register';
beforeEach(() => {
vi.clearAllMocks();
global.appTemplates = [];
global.templatesRefreshTime = 0;
});
describe('getAppTemplatesAndLoadThem', () => {
it('keeps plugin workflow fields authoritative while applying editable database config', async () => {
const pluginWorkflow = {
nodes: [
{
nodeId: 'plugin-start',
flowNodeType: FlowNodeTypeEnum.workflowStart,
name: 'Plugin start',
inputs: [],
outputs: []
}
],
edges: [],
chatConfig: {
welcomeText: 'plugin welcome'
}
};
const staleDbWorkflow = {
nodes: [
{
nodeId: 'stale-db-node',
flowNodeType: FlowNodeTypeEnum.workflowStart,
name: 'Stale DB node',
inputs: [
{
key: 'questionGuide',
valueType: 'hidden',
renderTypeList: ['hidden'],
label: 'core.app.Question Guide'
}
],
outputs: []
}
],
edges: [],
chatConfig: {
welcomeText: 'stale db welcome'
}
};
mocks.listWorkflows.mockResolvedValue([
{
templateId: 'githubIssue',
name: 'Plugin name',
intro: 'Plugin intro',
avatar: 'core/app/templates/githubIssue',
tags: ['plugin-tag'],
type: AppTypeEnum.workflow,
author: 'plugin author',
userGuide: {
type: 'link',
content: 'https://plugin.example.com'
},
workflow: pluginWorkflow,
order: 10
}
]);
mocks.findTemplates.mockReturnValue({
lean: vi.fn().mockResolvedValue([
{
templateId: `${AppToolSourceEnum.community}-githubIssue`,
name: 'DB name',
intro: 'DB intro',
avatar: 'db-avatar',
tags: ['db-tag'],
type: AppTypeEnum.simple,
isActive: false,
isPromoted: true,
recommendText: 'DB recommend',
userGuide: {
type: 'link',
content: 'https://db.example.com'
},
workflow: staleDbWorkflow,
order: 1
}
])
});
const [template] = await getAppTemplatesAndLoadThem(true);
expect(template).toMatchObject({
templateId: `${AppToolSourceEnum.community}-githubIssue`,
name: 'DB name',
intro: 'DB intro',
avatar: '/db-avatar',
tags: ['db-tag'],
type: AppTypeEnum.workflow,
isActive: false,
isPromoted: true,
recommendText: 'DB recommend',
order: 1
});
expect(template?.workflow).toEqual(pluginWorkflow);
expect(template?.userGuide).toEqual({
type: 'link',
content: 'https://plugin.example.com'
});
});
});
Subproject commit 860cc780847d747e7fdd24433e95887cdb341812
Subproject commit 465869779095ca30d14a2852c220dd2d896a6c67
......@@ -30,6 +30,7 @@ export type AProps = {
chatId: string;
chatItemDataId: string;
} & OutLinkChatAuthProps;
allowedCitationIds?: Set<string>;
onOpenCiteModal?: (e?: {
collectionId?: string;
sourceId?: string;
......@@ -202,6 +203,7 @@ const CiteLink = React.memo(function CiteLink({
const A = ({
children,
chatAuthData,
allowedCitationIds,
onOpenCiteModal,
showAnimation,
...props
......@@ -222,6 +224,11 @@ const A = ({
(props.href?.startsWith('CITE') || props.href?.startsWith('QUOTE')) &&
typeof content === 'string'
) {
console.log(allowedCitationIds, allowedCitationIds?.has(content));
if (allowedCitationIds && !allowedCitationIds.has(content)) {
return null;
}
return (
<CiteLink
id={content}
......
......@@ -51,7 +51,7 @@ function MarkdownCodeRenderer(props: any) {
}
function MarkdownLinkRenderer(props: any) {
const { showAnimation, chatAuthData, onOpenCiteModal } = useContext(
const { showAnimation, chatAuthData, allowedCitationIds, onOpenCiteModal } = useContext(
MarkdownRendererRuntimeContext
);
......@@ -60,6 +60,7 @@ function MarkdownLinkRenderer(props: any) {
{...props}
showAnimation={showAnimation}
chatAuthData={chatAuthData}
allowedCitationIds={allowedCitationIds}
onOpenCiteModal={onOpenCiteModal}
/>
);
......@@ -100,6 +101,7 @@ const MarkdownRender = ({
autoPreviewHtmlCodeBlock,
chatAuthData,
allowedCitationIds,
onOpenCiteModal
}: Props) => {
const renderContextValue = useMemo(
......@@ -108,9 +110,17 @@ const MarkdownRender = ({
autoPreviewHtmlCodeBlock,
markdownClassName: className,
chatAuthData,
allowedCitationIds,
onOpenCiteModal
}),
[autoPreviewHtmlCodeBlock, chatAuthData, className, onOpenCiteModal, showAnimation]
[
allowedCitationIds,
autoPreviewHtmlCodeBlock,
chatAuthData,
className,
onOpenCiteModal,
showAnimation
]
);
const formatSource = useMemo(() => {
......
......@@ -6,6 +6,7 @@ export type MarkdownRendererRuntimeContextValue = {
autoPreviewHtmlCodeBlock?: boolean;
markdownClassName?: string;
chatAuthData?: AProps['chatAuthData'];
allowedCitationIds?: AProps['allowedCitationIds'];
onOpenCiteModal?: AProps['onOpenCiteModal'];
};
......
......@@ -24,6 +24,7 @@ type AIChatBubbleContentProps = {
responseData?: ChatHistoryItemResType[];
isLastChild: boolean;
isChatting: boolean;
allowedCitationIds?: Set<string>;
onOpenCiteModal: (e?: OnOpenCiteModalProps) => void;
};
......@@ -33,6 +34,7 @@ const AIChatBubbleContent = ({
dataId,
isLastChild,
isChatting,
allowedCitationIds,
onOpenCiteModal
}: AIChatBubbleContentProps) => {
const renderValue = ({
......@@ -67,6 +69,7 @@ const AIChatBubbleContent = ({
isLastChild={isLastChild}
isChatting={isChatting}
onOpenCiteModal={onOpenCiteModal}
allowedCitationIds={allowedCitationIds}
wrapProcessing={wrapProcessing}
showProcessing={showProcessing}
showFoldableProcessing={showFoldableProcessing}
......
......@@ -28,6 +28,7 @@ type AIChatBubbleProps = {
isChatting: boolean;
loadingText?: string;
questionGuides: string[];
allowedCitationIds?: Set<string>;
onOpenCiteModal: (e?: OnOpenCiteModalProps) => void;
chatControllerProps: ChatControllerProps;
children?: React.ReactNode;
......@@ -42,6 +43,7 @@ const AIChatBubble = ({
isChatting,
loadingText,
questionGuides,
allowedCitationIds,
onOpenCiteModal,
chatControllerProps,
children
......@@ -79,6 +81,7 @@ const AIChatBubble = ({
dataId={chat.dataId}
isLastChild={isLastChild && isLastValueGroup}
isChatting={isChatting}
allowedCitationIds={allowedCitationIds}
onOpenCiteModal={onOpenCiteModal}
/>
{showNoOutputTip && (
......
......@@ -80,6 +80,12 @@ const ChatItem = (props: Props) => {
const statisticalChatItem = useMemoEnhance(() => addStatisticalDataToHistoryItem(chat), [chat]);
const quoteList: SearchDataResponseQuoteListItemType[] = statisticalChatItem.totalQuoteList ?? [];
const allowedCitationIds = useMemoEnhance(() => {
const sourceQuoteList = statisticalChatItem.totalQuoteList;
if (!sourceQuoteList) return;
return new Set(sourceQuoteList.map((item) => item.id).filter((id): id is string => !!id));
}, [statisticalChatItem.totalQuoteList]);
const { errorText } = statisticalChatItem;
const inlineErrorInfo = useMemo(() => {
if (!chat.errorMsg && !errorText) return;
......@@ -315,6 +321,7 @@ const ChatItem = (props: Props) => {
<HumanChatBubble
chatValue={value as UserChatItemValueItemType[]}
chatTime={i === splitAiResponseResults.length - 1 ? chat.time : undefined}
canEdit={!isChatting}
onEditSubmit={onEditSubmit}
>
{renderCommonFooter()}
......@@ -341,6 +348,7 @@ const ChatItem = (props: Props) => {
isChatting={isChatting}
loadingText={showRunningStatus ? statusBoxData?.name : undefined}
questionGuides={questionGuides}
allowedCitationIds={allowedCitationIds}
onOpenCiteModal={onOpenCiteModal}
chatControllerProps={{
...props,
......
......@@ -10,6 +10,7 @@ type HumanChatBubbleActionsProps = {
chatText: string;
chatTime?: Date;
onEdit: () => void;
canEdit?: boolean;
isAlwaysVisible?: boolean;
};
......@@ -17,6 +18,7 @@ const HumanChatBubbleActions = ({
chatText,
chatTime,
onEdit,
canEdit = true,
isAlwaysVisible = false
}: HumanChatBubbleActionsProps) => {
const { t } = useTranslation();
......@@ -58,17 +60,19 @@ const HumanChatBubbleActions = ({
onClick={() => copyData(chatText)}
/>
</MyTooltip>
<MyTooltip label={t('common:Edit')}>
<MyIcon
w={'16px'}
p={'4px'}
cursor="pointer"
name={'edit'}
color={'myGray.400'}
_hover={{ color: 'primary.600' }}
onClick={onEdit}
/>
</MyTooltip>
{canEdit && (
<MyTooltip label={t('common:Edit')}>
<MyIcon
w={'16px'}
p={'4px'}
cursor="pointer"
name={'edit'}
color={'myGray.400'}
_hover={{ color: 'primary.600' }}
onClick={onEdit}
/>
</MyTooltip>
)}
</Flex>
</Flex>
);
......
......@@ -14,6 +14,7 @@ type HumanChatBubbleProps = {
chatValue: UserChatItemValueItemType[];
chatTime?: Date;
onEditSubmit?: (input: ChatBoxInputType) => void | Promise<void>;
canEdit?: boolean;
children?: React.ReactNode;
};
......@@ -23,7 +24,13 @@ type HumanChatBubbleProps = {
* 用户气泡的宽度、附件单列展示和 hover 操作区与 AI 气泡完全隔离,避免影响流式
* AI 回复的原有宽度计算。编辑按钮当前只提供 UI 入口,后续需要接入消息编辑接口。
*/
const HumanChatBubble = ({ chatValue, chatTime, onEditSubmit, children }: HumanChatBubbleProps) => {
const HumanChatBubble = ({
chatValue,
chatTime,
onEditSubmit,
canEdit = true,
children
}: HumanChatBubbleProps) => {
const { isPc } = useSystem();
const { text: chatText = '', files = [] } = useMemo(
() => formatChatValue2InputType(chatValue),
......@@ -79,6 +86,7 @@ const HumanChatBubble = ({ chatValue, chatTime, onEditSubmit, children }: HumanC
chatText={chatText}
chatTime={chatTime}
isAlwaysVisible={!isPc}
canEdit={canEdit}
onEdit={() => setIsEditing(true)}
/>
</Box>
......
......@@ -15,12 +15,14 @@ const RenderText = React.memo(function RenderText({
text,
chatItemDataId,
onOpenCiteModal,
allowedCitationIds,
isDisabled
}: {
showAnimation: boolean;
text: string;
chatItemDataId: string;
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
allowedCitationIds?: Set<string>;
isDisabled?: boolean;
}) {
const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId);
......@@ -47,6 +49,7 @@ const RenderText = React.memo(function RenderText({
source={source}
showAnimation={showAnimation}
chatAuthData={chatAuthData}
allowedCitationIds={allowedCitationIds}
onOpenCiteModal={onOpenCiteModal}
isDisabled={isDisabled}
autoPreviewHtmlCodeBlock
......
......@@ -30,6 +30,7 @@ const AIResponseBox = ({
isLastChild,
isChatting,
onOpenCiteModal,
allowedCitationIds,
wrapProcessing = true,
showProcessing = true,
showFoldableProcessing: showFoldableProcessingProp = true,
......@@ -45,6 +46,7 @@ const AIResponseBox = ({
isLastChild: boolean;
isChatting: boolean;
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
allowedCitationIds?: Set<string>;
wrapProcessing?: boolean;
showProcessing?: boolean;
showFoldableProcessing?: boolean;
......@@ -146,6 +148,7 @@ const AIResponseBox = ({
showAnimation={isChatting && isLastResponseValue}
text={textContent}
onOpenCiteModal={onOpenCiteModal}
allowedCitationIds={allowedCitationIds}
isDisabled={disableStreamingInteraction}
/>
);
......
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