fix: var render (#5857)
Some checks are pending
Document deploy / sync-images (push) Waiting to run
Document deploy / generate-timestamp (push) Blocked by required conditions
Document deploy / build-images (map[domain:https://fastgpt.cn suffix:cn]) (push) Blocked by required conditions
Document deploy / build-images (map[domain:https://fastgpt.io suffix:io]) (push) Blocked by required conditions
Document deploy / update-images (map[deployment:fastgpt-docs domain:https://fastgpt.cn kube_config:KUBE_CONFIG_CN suffix:cn]) (push) Blocked by required conditions
Document deploy / update-images (map[deployment:fastgpt-docs domain:https://fastgpt.io kube_config:KUBE_CONFIG_IO suffix:io]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / get-vars (push) Waiting to run
Build FastGPT images in Personal warehouse / build-fastgpt-images (map[arch:amd64 runs-on:ubuntu-24.04]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / build-fastgpt-images (map[arch:arm64 runs-on:ubuntu-24.04-arm]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / release-fastgpt-images (push) Blocked by required conditions

* fix: timeselector ui error

* var update node

* fix: var render

* fix: prompt editor

* perf: init

* fix: retry input

* fix: prompt editor

* fix: editor
This commit is contained in:
Archer 2025-11-04 22:15:47 +08:00 committed by GitHub
parent a499d05a02
commit 44e87e3053
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 205 additions and 80 deletions

View File

@ -52,11 +52,12 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4140' \
## 🐛 修复
1. Claude 工具调用,如果下标从 1 开始会导致参数异常。
2. S3 删除头像,如果 key 为空时,会抛错,导致流程阻塞。
3. 工作流前置IO 变更时,依赖未及时刷新。
4. 导出对话日志,缺少反馈记录。
5. 工作流欢迎语输入框输入时,光标会偏移到最后一位。
6. 存在交互节点和连续批量执行时,会导致工作流运行逻辑错误。
7. 工作流 Redo 操作后,编辑记录无法再继续推送快照。
1. Prompt 编辑器存在特殊语法时候,无法解析正确内容。
2. Claude 工具调用,如果下标从 1 开始会导致参数异常。
3. S3 删除头像,如果 key 为空时,会抛错,导致流程阻塞。
4. 工作流前置IO 变更时,依赖未及时刷新。
5. 导出对话日志,缺少反馈记录。
6. 工作流欢迎语输入框输入时,光标会偏移到最后一位。
7. 存在交互节点和连续批量执行时,会导致工作流运行逻辑错误。
8. 工作流 Redo 操作后,编辑记录无法再继续推送快照。

View File

@ -19,7 +19,7 @@
"document/content/docs/introduction/development/custom-models/ollama.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/introduction/development/custom-models/xinference.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/introduction/development/design/dataset.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/development/design/design_plugin.mdx": "2025-10-30T22:14:07+08:00",
"document/content/docs/introduction/development/design/design_plugin.mdx": "2025-11-04T16:58:12+08:00",
"document/content/docs/introduction/development/docker.mdx": "2025-09-29T11:34:11+08:00",
"document/content/docs/introduction/development/faq.mdx": "2025-08-12T22:22:18+08:00",
"document/content/docs/introduction/development/intro.mdx": "2025-09-29T11:34:11+08:00",
@ -84,11 +84,11 @@
"document/content/docs/introduction/guide/knowledge_base/websync.mdx": "2025-08-05T23:20:39+08:00",
"document/content/docs/introduction/guide/knowledge_base/yuque_dataset.mdx": "2025-09-17T22:29:56+08:00",
"document/content/docs/introduction/guide/plugins/bing_search_plugin.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/plugins/dev_system_tool.mdx": "2025-10-30T22:14:07+08:00",
"document/content/docs/introduction/guide/plugins/dev_system_tool.mdx": "2025-11-04T16:58:12+08:00",
"document/content/docs/introduction/guide/plugins/doc2x_plugin_guide.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/plugins/google_search_plugin_guide.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/plugins/searxng_plugin_guide.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/plugins/upload_system_tool.mdx": "2025-10-30T22:14:07+08:00",
"document/content/docs/introduction/guide/plugins/upload_system_tool.mdx": "2025-11-04T16:58:12+08:00",
"document/content/docs/introduction/guide/team_permissions/invitation_link.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/team_permissions/team_roles_permissions.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/index.en.mdx": "2025-07-23T21:35:03+08:00",
@ -101,7 +101,7 @@
"document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00",
"document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00",
"document/content/docs/toc.mdx": "2025-10-23T19:11:11+08:00",
"document/content/docs/toc.mdx": "2025-11-04T16:58:12+08:00",
"document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00",
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
@ -111,10 +111,10 @@
"document/content/docs/upgrading/4-12/4122.mdx": "2025-09-07T14:41:48+08:00",
"document/content/docs/upgrading/4-12/4123.mdx": "2025-09-07T20:55:14+08:00",
"document/content/docs/upgrading/4-12/4124.mdx": "2025-09-17T22:29:56+08:00",
"document/content/docs/upgrading/4-13/4130.mdx": "2025-09-30T16:00:10+08:00",
"document/content/docs/upgrading/4-13/4130.mdx": "2025-11-04T15:06:39+08:00",
"document/content/docs/upgrading/4-13/4131.mdx": "2025-09-30T15:47:06+08:00",
"document/content/docs/upgrading/4-13/4132.mdx": "2025-10-21T11:46:53+08:00",
"document/content/docs/upgrading/4-14/4140.mdx": "2025-11-03T12:13:10+08:00",
"document/content/docs/upgrading/4-14/4140.mdx": "2025-11-04T16:58:12+08:00",
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",

View File

@ -46,6 +46,54 @@ const { LOG_LEVEL, STORE_LOG_LEVEL, SIGNOZ_STORE_LEVEL } = (() => {
};
})();
/**
* Sanitize object to prevent circular references for BSON serialization
* Remove properties that may contain circular references
*/
const sanitizeObjectForBSON = (obj: Record<string, any>): Record<string, any> => {
try {
// Use JSON stringify with replacer to handle circular references
const seen = new WeakSet();
const sanitized = JSON.parse(
JSON.stringify(obj, (key, value) => {
// Handle circular references
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
}
// Remove known problematic properties from axios config
if (key === 'config' && value && typeof value === 'object') {
return {
method: value.method,
url: value.url,
baseURL: value.baseURL,
headers: value.headers,
timeout: value.timeout,
responseType: value.responseType
};
}
// Remove functions and other non-serializable values
if (typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
return value;
})
);
return sanitized;
} catch (error) {
// If sanitization fails, return a safe fallback
return {
error: 'Failed to sanitize object',
originalKeys: Object.keys(obj)
};
}
};
/* add logger */
export const addLog = {
log(level: LogLevelEnum, msg: string, obj: Record<string, any> = {}) {
@ -77,10 +125,13 @@ export const addLog = {
if (level >= STORE_LOG_LEVEL && connectionMongo.connection.readyState === 1) {
(async () => {
try {
// Sanitize metadata to prevent circular reference errors
const safeMetadata = sanitizeObjectForBSON(obj);
await getMongoLog().create({
text: msg,
level,
metadata: obj
metadata: safeMetadata
});
} catch (error) {
console.error('store log error', error);

View File

@ -163,7 +163,7 @@ export const loadRequestMessages = async ({
// If imgUrl is a local path, load image from local, and set url to base64
if (
imgUrl.startsWith('/') ||
process.env.MULTIPLE_DATA_TO_BASE64 === 'true' ||
process.env.MULTIPLE_DATA_TO_BASE64 !== 'false' ||
isInternalAddress(imgUrl)
) {
const url = await (async () => {

View File

@ -42,28 +42,11 @@ export type TextEditorNode = BaseEditorNode & {
export type LineBreakEditorNode = BaseEditorNode & {
type: 'linebreak';
};
export type VariableLabelEditorNode = BaseEditorNode & {
type: 'variableLabel';
variableKey: string;
};
export type VariableEditorNode = BaseEditorNode & {
type: 'Variable';
variableKey: string;
};
export type TabEditorNode = BaseEditorNode & {
type: 'tab';
};
export type ChildEditorNode =
| TextEditorNode
| LineBreakEditorNode
| VariableLabelEditorNode
| VariableEditorNode
| TabEditorNode;
// Rich text
export type ParagraphEditorNode = BaseEditorNode & {
type: 'paragraph';
children: ChildEditorNode[];
@ -72,15 +55,33 @@ export type ParagraphEditorNode = BaseEditorNode & {
indent: number;
};
// ListItem 节点的 children 可以包含嵌套的 list 节点
export type ListItemChildEditorNode =
| TextEditorNode
| LineBreakEditorNode
| TabEditorNode
| VariableLabelEditorNode
| VariableEditorNode;
export type ListItemEditorNode = BaseEditorNode & {
type: 'listitem';
children: Array<ChildEditorNode | ListEditorNode>;
children: (ListItemChildEditorNode | ListEditorNode)[];
direction: string | null;
format: string;
indent: number;
value: number;
};
// Custom variable node types
export type VariableLabelEditorNode = BaseEditorNode & {
type: 'variableLabel';
variableKey: string;
};
export type VariableEditorNode = BaseEditorNode & {
type: 'Variable';
variableKey: string;
};
export type ListEditorNode = BaseEditorNode & {
type: 'list';
children: ListItemEditorNode[];
@ -92,10 +93,20 @@ export type ListEditorNode = BaseEditorNode & {
tag: 'ul' | 'ol';
};
export type ChildEditorNode =
| TextEditorNode
| LineBreakEditorNode
| TabEditorNode
| ParagraphEditorNode
| ListEditorNode
| ListItemEditorNode
| VariableLabelEditorNode
| VariableEditorNode;
export type EditorState = {
root: {
type: 'root';
children: Array<ParagraphEditorNode | ListEditorNode>;
children: ChildEditorNode[];
direction: string;
format: string;
indent: number;

View File

@ -17,7 +17,8 @@ import type {
ListEditorNode,
ParagraphEditorNode,
EditorState,
ListItemInfo
ListItemInfo,
ChildEditorNode
} from './type';
export function registerLexicalTextEntity<T extends TextNode | VariableLabelNode | VariableNode>(
@ -472,6 +473,62 @@ export const editorStateToText = (editor: LexicalEditor) => {
const editorState = editor.getEditorState().toJSON() as EditorState;
const paragraphs = editorState.root.children;
const extractText = (node: ChildEditorNode): string => {
if (!node) return '';
// Handle line break nodes
if (node.type === 'linebreak') {
return '\n';
}
// Handle tab nodes
if (node.type === 'tab') {
return ' ';
}
// Handle text nodes
if (node.type === 'text') {
return node.text || '';
}
// Handle custom variable nodes
if (node.type === 'variableLabel' || node.type === 'Variable') {
return node.variableKey || '';
}
// Handle paragraph nodes - recursively process children
if (node.type === 'paragraph') {
if (!node.children || node.children.length === 0) {
return '';
}
return node.children.map(extractText).join('');
}
// Handle list item nodes - recursively process children (excluding nested lists)
if (node.type === 'listitem') {
if (!node.children || node.children.length === 0) {
return '';
}
// Filter out nested list nodes as they are handled separately
return node.children
.filter((child) => child.type !== 'list')
.map(extractText)
.join('');
}
// Handle list nodes - recursively process children
if (node.type === 'list') {
if (!node.children || node.children.length === 0) {
return '';
}
return node.children.map(extractText).join('');
}
// Unknown node type
console.warn('Unknown node type in extractText:', (node as any).type, node);
return '';
};
paragraphs.forEach((paragraph) => {
if (paragraph.type === 'list') {
const listResults = processList({ list: paragraph });
@ -483,19 +540,15 @@ export const editorStateToText = (editor: LexicalEditor) => {
const indentSpaces = ' '.repeat(paragraph.indent || 0);
children.forEach((child) => {
if (child.type === 'linebreak') {
paragraphText.push('\n');
} else if (child.type === 'text') {
paragraphText.push(child.text);
} else if (child.type === 'tab') {
paragraphText.push(' ');
} else if (child.type === 'variableLabel' || child.type === 'Variable') {
paragraphText.push(child.variableKey);
}
const val = extractText(child);
paragraphText.push(val);
});
const finalText = paragraphText.join('');
editorStateTextString.push(indentSpaces + finalText);
} else {
const text = extractText(paragraph);
editorStateTextString.push(text);
}
});
return editorStateTextString.join('\n');

View File

@ -651,11 +651,12 @@ const ChatBox = ({
// retry input
const onDelMessage = useCallback(
(contentId: string) => {
(contentId: string, delFile = true) => {
return delChatRecordById({
appId,
chatId,
contentId,
delFile,
...outLinkAuthData
});
},
@ -672,7 +673,7 @@ const ChatBox = ({
await Promise.all(
delHistory.map((item) => {
if (item.dataId) {
return onDelMessage(item.dataId);
return onDelMessage(item.dataId, false);
}
})
);

View File

@ -31,7 +31,8 @@ export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): Chat
type: item.file.type,
name: item.file.name,
icon: getFileIcon(item.file.name),
url: item.file.url
url: item.file.url,
key: item.file.key
}
: undefined
)

View File

@ -88,6 +88,7 @@ export type DeleteChatItemProps = OutLinkChatAuthProps & {
appId: string;
chatId: string;
contentId?: string;
delFile?: boolean;
};
export type AdminUpdateFeedbackParams = AdminFbkType & {

View File

@ -34,6 +34,7 @@ import {
import { InputTypeEnum } from '@/components/core/app/formRender/constant';
import { WorkflowActionsContext } from '../../context/workflowActionsContext';
import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance';
import { useMemoizedFn } from 'ahooks';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data;
@ -105,7 +106,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
[inputs, nodeId, onChangeNode]
);
const ValueRender = useCallback(
const ValueRender = useMemoizedFn(
({ updateItem, index }: { updateItem: TUpdateListItem; index: number }) => {
const { inputType, formParams = {} } = (() => {
const value = updateItem.variable;
@ -279,7 +280,6 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
return (
<Box minW={'250px'} maxW={'400px'} borderRadius={'sm'}>
<InputRender
// @ts-ignore
inputType={inputType}
{...formParams}
isRichText={false}
@ -294,18 +294,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
</Flex>
</Container>
);
},
[
appDetail.chatConfig,
externalProviderWorkflowVariables,
getNodeById,
nodeId,
onUpdateList,
systemConfigNode,
t,
updateList,
variables
]
}
);
const Render = useMemo(() => {

View File

@ -174,7 +174,7 @@ async function migrateSystemPluginsToTools(typeToGroupMap: Map<string, string>):
pluginId: plugin.pluginId?.startsWith(AppToolSourceEnum.community)
? plugin.pluginId.replace(AppToolSourceEnum.community, AppToolSourceEnum.systemTool)
: plugin.pluginId,
status: !plugin.isActive ? PluginStatusEnum.Offline : PluginStatusEnum.Normal,
status: plugin.isActive === false ? PluginStatusEnum.Offline : PluginStatusEnum.Normal,
defaultInstalled: false,
originCost: plugin.originCost || 0,
currentCost: plugin.currentCost || 0,
@ -182,7 +182,7 @@ async function migrateSystemPluginsToTools(typeToGroupMap: Map<string, string>):
pluginOrder: plugin.pluginOrder,
systemKeyCost: plugin.systemKeyCost || 0,
customConfig: plugin.customConfig ? { ...plugin.customConfig } : {},
inputListVal: plugin.inputListVal || {}
inputListVal: plugin.inputListVal
};
// 迁移 templateType → tags

View File

@ -9,8 +9,8 @@ import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemRespon
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat';
async function handler(req: ApiRequestProps<{}, DeleteChatItemProps>, res: NextApiResponse) {
const { appId, chatId, contentId } = req.query;
async function handler(req: ApiRequestProps<DeleteChatItemProps>, res: NextApiResponse) {
const { appId, chatId, contentId, delFile = true } = req.body;
if (!contentId || !chatId) {
return Promise.reject('contentId or chatId is empty');
@ -20,7 +20,7 @@ async function handler(req: ApiRequestProps<{}, DeleteChatItemProps>, res: NextA
req,
authToken: true,
authApiKey: true,
...req.query
...req.body
});
await mongoSessionRun(async (session) => {
@ -36,7 +36,7 @@ async function handler(req: ApiRequestProps<{}, DeleteChatItemProps>, res: NextA
dataId: contentId
}).session(session);
if (item?.obj === ChatRoleEnum.Human) {
if (item?.obj === ChatRoleEnum.Human && delFile) {
const s3ChatSource = getS3ChatSource();
for (const value of item.value) {
if (value.type === ChatItemValueTypeEnum.file && value.file?.key) {

View File

@ -2,7 +2,7 @@ import { NextAPI } from '@/service/middleware/entry';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoTeamInstalledPlugin } from '@fastgpt/service/core/plugin/schema/teamInstalledPluginSchema';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import type { ToggleInstallPluginBodyType } from '@fastgpt/global/openapi/core/plugin/team/api';
export type ToggleInstallPluginBody = ToggleInstallPluginBodyType;
@ -18,7 +18,7 @@ async function handler(
const { teamId } = await authUserPer({
req,
authToken: true,
per: WritePermissionVal
per: ReadPermissionVal
});
await MongoTeamInstalledPlugin.findOneAndUpdate(

View File

@ -75,7 +75,7 @@ export const delClearChatHistories = (data: ClearHistoriesProps) =>
* delete one chat record
*/
export const delChatRecordById = (data: DeleteChatItemProps) =>
DELETE(`/core/chat/item/delete`, data);
POST(`/core/chat/item/delete`, data);
/**
* 修改历史记录: 标题/

View File

@ -411,8 +411,11 @@ describe('loadRequestMessages function tests', () => {
const result = await loadRequestMessages({ messages, useVision: true });
expect(result).toHaveLength(1);
expect(typeof result[0].content).toBe('string');
expect(result[0].content).toBe('https://example.com/image.png');
// When useVision is true and text contains image URL, it returns array format
expect(Array.isArray(result[0].content)).toBe(true);
const content = result[0].content as any[];
expect(content.some((item: any) => item.type === 'image_url')).toBe(true);
expect(content.some((item: any) => item.type === 'text')).toBe(true);
});
it('should not extract images when useVision is false', async () => {
@ -474,9 +477,10 @@ describe('loadRequestMessages function tests', () => {
const result = await loadRequestMessages({ messages, useVision: true });
expect(result).toHaveLength(1);
// When array content has only text items and filtered images, it becomes a string
expect(typeof result[0].content).toBe('string');
expect(result[0].content).toBe('Hello');
// When array content has text and image_url, remains as array
expect(Array.isArray(result[0].content)).toBe(true);
const content = result[0].content as any[];
expect(content.some((item: any) => item.type === 'text')).toBe(true);
});
it('should filter out empty text items from array content', async () => {
@ -539,6 +543,9 @@ describe('loadRequestMessages function tests', () => {
});
it('should handle invalid remote images gracefully', async () => {
const originalEnv = process.env.MULTIPLE_DATA_TO_BASE64;
process.env.MULTIPLE_DATA_TO_BASE64 = 'false'; // Disable base64 conversion
const messages: ChatCompletionMessageParam[] = [
{
role: ChatCompletionRequestMessageRoleEnum.User,
@ -554,9 +561,17 @@ describe('loadRequestMessages function tests', () => {
const result = await loadRequestMessages({ messages, useVision: true });
expect(result).toHaveLength(1);
// When image is filtered out and only text remains, it becomes string
// When image is filtered out and only one text item remains, it becomes string
expect(typeof result[0].content).toBe('string');
expect(result[0].content).toBe('Text');
// Restore original environment
if (originalEnv !== undefined) {
process.env.MULTIPLE_DATA_TO_BASE64 = originalEnv;
} else {
// @ts-ignore
delete process.env.MULTIPLE_DATA_TO_BASE64;
}
});
it('should handle 405 status as valid image', async () => {
@ -577,9 +592,11 @@ describe('loadRequestMessages function tests', () => {
const result = await loadRequestMessages({ messages, useVision: true });
expect(result).toHaveLength(1);
// The function processes images from array content differently, expects text to remain
expect(typeof result[0].content).toBe('string');
expect(result[0].content).toBe('Check this image:');
// 405 status is treated as valid, so image is kept and content is array
expect(Array.isArray(result[0].content)).toBe(true);
const content = result[0].content as any[];
expect(content.some((item: any) => item.type === 'text')).toBe(true);
expect(content.some((item: any) => item.type === 'image_url')).toBe(true);
});
it('should remove origin from image URLs when provided', async () => {