diff --git a/document/content/docs/upgrading/4-14/4145.mdx b/document/content/docs/upgrading/4-14/4145.mdx index 494087bb7208..3d9d990074c0 100644 --- a/document/content/docs/upgrading/4-14/4145.mdx +++ b/document/content/docs/upgrading/4-14/4145.mdx @@ -36,5 +36,11 @@ description: 'FastGPT V4.14.5 更新说明' 5. 加载默认模型时,maxTokens 字段未赋值,导致模型最大响应值配置为空。 6. S3 文件清理队列因网络稳定问题出现阻塞,导致删除任务不再执行。 7. 对话日志接口适配 mongo4.x 语法。 +8. 变量更新节点将文件 URL 字符串数组错误转换为对象数组。 +9. 多个表单输入节点共享 sessionStorage 导致默认值不显示。 +10. 代码运行节点切换语言后,AI 仍使用旧语言生成代码。 +11. 多个自定义反馈节点并发写入触发数据库写入冲突。 +12. 交互节点后续的自定义反馈节点写入失败。 + ## 插件 diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 315e4521f8f9..4c9c1630ab1f 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -104,7 +104,7 @@ "document/content/docs/protocol/terms.en.mdx": "2025-12-15T23:36:54+08:00", "document/content/docs/protocol/terms.mdx": "2025-12-15T23:36:54+08:00", "document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00", - "document/content/docs/toc.mdx": "2026-01-06T18:19:42+08:00", + "document/content/docs/toc.mdx": "2026-01-06T13:25:46+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", @@ -204,4 +204,4 @@ "document/content/docs/use-cases/external-integration/openapi.mdx": "2025-09-29T11:34:11+08:00", "document/content/docs/use-cases/external-integration/wecom.mdx": "2025-12-10T20:07:05+08:00", "document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00" -} \ No newline at end of file +} diff --git a/packages/global/core/workflow/runtime/constants.ts b/packages/global/core/workflow/runtime/constants.ts index 2f7740501754..ad94b9de5818 100644 --- a/packages/global/core/workflow/runtime/constants.ts +++ b/packages/global/core/workflow/runtime/constants.ts @@ -31,7 +31,8 @@ export enum DispatchNodeResponseKeyEnum { interactive = 'INTERACTIVE', // is interactive runTimes = 'runTimes', // run times newVariables = 'newVariables', // new variables - memories = 'system_memories' // memories + memories = 'system_memories', // memories + customFeedbacks = 'customFeedbacks' // custom feedbacks } export const needReplaceReferenceInputTypeList = [ diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index 0900795dd68f..aa2e6096db45 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -68,7 +68,6 @@ export type ChatDispatchProps = { responseChatItemId?: string; histories: ChatItemType[]; variables: Record; // global variable - cloneVariables: Record; query: UserChatItemValueItemType[]; // trigger query chatConfig: AppSchema['chatConfig']; lastInteractive?: WorkflowInteractiveResponseType; // last interactive response @@ -274,6 +273,7 @@ export type DispatchNodeResultType; [DispatchNodeResponseKeyEnum.memories]?: Record; [DispatchNodeResponseKeyEnum.interactive]?: InteractiveNodeResponseType; + [DispatchNodeResponseKeyEnum.customFeedbacks]?: string[]; data?: T; error?: ERR; diff --git a/packages/service/core/chat/controller.ts b/packages/service/core/chat/controller.ts index 1ef39b10c033..3f33cc2ad175 100644 --- a/packages/service/core/chat/controller.ts +++ b/packages/service/core/chat/controller.ts @@ -194,47 +194,6 @@ export async function getChatItems({ return { histories, total, hasMorePrev, hasMoreNext }; } -export const addCustomFeedbacks = async ({ - appId, - chatId, - dataId, - feedbacks -}: { - appId: string; - chatId?: string; - dataId?: string; - feedbacks: string[]; -}) => { - if (!chatId || !dataId) return; - - try { - await mongoSessionRun(async (session) => { - // Add custom feedbacks to ChatItem - await MongoChatItem.updateOne( - { - appId, - chatId, - dataId - }, - { - $push: { customFeedbacks: { $each: feedbacks } } - }, - { session } - ); - - // Update ChatLog feedback statistics - await updateChatFeedbackCount({ - appId, - chatId, - session - }); - }); - } catch (error) { - addLog.error('addCustomFeedbacks error', error); - throw error; - } -}; - /** * Update feedback count statistics for a chat in Chat table * This method aggregates feedback data from chatItems and updates the Chat table diff --git a/packages/service/core/workflow/dispatch/child/runApp.ts b/packages/service/core/workflow/dispatch/child/runApp.ts index 4c2d4773d3a9..b5d90abc86ce 100644 --- a/packages/service/core/workflow/dispatch/child/runApp.ts +++ b/packages/service/core/workflow/dispatch/child/runApp.ts @@ -131,7 +131,8 @@ export const dispatchRunAppNode = async (props: Props): Promise => { assistantResponses, runTimes, workflowInteractiveResponse, - system_memories + system_memories, + customFeedbacks } = await runWorkflow({ ...props, usageId: undefined, @@ -205,7 +206,8 @@ export const dispatchRunAppNode = async (props: Props): Promise => { totalPoints: usagePoints } ], - [DispatchNodeResponseKeyEnum.toolResponses]: text + [DispatchNodeResponseKeyEnum.toolResponses]: text, + [DispatchNodeResponseKeyEnum.customFeedbacks]: customFeedbacks }; } catch (error) { return getNodeErrResponse({ error }); diff --git a/packages/service/core/workflow/dispatch/index.ts b/packages/service/core/workflow/dispatch/index.ts index a2a35d2bd85c..6be99b9f0d83 100644 --- a/packages/service/core/workflow/dispatch/index.ts +++ b/packages/service/core/workflow/dispatch/index.ts @@ -24,7 +24,7 @@ import type { } from '@fastgpt/global/core/workflow/runtime/type'; import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type.d'; import { getErrText, UserError } from '@fastgpt/global/common/error/utils'; -import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; +import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils'; import { checkNodeRunStatus, @@ -57,13 +57,12 @@ import { addPreviewUrlToChatItems, presignVariablesFileUrls } from '../../chat/u import type { MCPClient } from '../../app/mcp'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { i18nT } from '../../../../web/i18n/utils'; -import { clone } from 'lodash'; import { validateFileUrlDomain } from '../../../common/security/fileUrlValidator'; import { delAgentRuntimeStopSign, shouldWorkflowStop } from './workflowStatus'; type Props = Omit< ChatDispatchProps, - 'checkIsStopping' | 'workflowDispatchDeep' | 'timezone' | 'externalProvider' | 'cloneVariables' + 'checkIsStopping' | 'workflowDispatchDeep' | 'timezone' | 'externalProvider' > & { runtimeNodes: RuntimeNodeItemType[]; runtimeEdges: RuntimeEdgeItemType[]; @@ -182,7 +181,6 @@ export async function dispatchWorkFlow({ } // Get default variables - const cloneVariables = clone(data.variables); const defaultVariables = { ...externalProvider.externalWorkflowVariables, ...(await getSystemVariables({ @@ -228,8 +226,7 @@ export async function dispatchWorkFlow({ workflowDispatchDeep: 0, usageId: newUsageId, concatUsage, - mcpClientMemory, - cloneVariables + mcpClientMemory }).finally(async () => { if (streamCheckTimer) { clearInterval(streamCheckTimer); @@ -273,8 +270,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise = {}; // Workflow node memories + customFeedbackList: string[] = []; // Custom feedbacks collected from nodes // Debug debugNextStepRunNodes: RuntimeNodeItemType[] = []; // 记录 Debug 模式下,下一个阶段需要执行的节点。 @@ -720,7 +716,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise { // Add run times this.workflowRunTimes += runTimes; @@ -737,6 +734,11 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise 0 ? workflowQueue.system_memories : undefined, + [DispatchNodeResponseKeyEnum.customFeedbacks]: + workflowQueue.customFeedbackList.length > 0 ? workflowQueue.customFeedbackList : undefined, durationSeconds }; }; diff --git a/packages/service/core/workflow/dispatch/loop/runLoop.ts b/packages/service/core/workflow/dispatch/loop/runLoop.ts index 0b019e5caae2..164dec897d30 100644 --- a/packages/service/core/workflow/dispatch/loop/runLoop.ts +++ b/packages/service/core/workflow/dispatch/loop/runLoop.ts @@ -51,6 +51,7 @@ export const dispatchLoop = async (props: Props): Promise => { const outputValueArr = interactiveData ? interactiveData.loopResult : []; const loopResponseDetail: ChatHistoryItemResType[] = []; let assistantResponses: AIChatItemValueItemType[] = []; + const customFeedbacks: string[] = []; let totalPoints = 0; let newVariables: Record = props.variables; let interactiveResponse: WorkflowInteractiveResponseType | undefined = undefined; @@ -116,6 +117,11 @@ export const dispatchLoop = async (props: Props): Promise => { assistantResponses.push(...response.assistantResponses); totalPoints += response.flowUsages.reduce((acc, usage) => acc + usage.totalPoints, 0); + // Collect custom feedbacks + if (response[DispatchNodeResponseKeyEnum.customFeedbacks]) { + customFeedbacks.push(...response[DispatchNodeResponseKeyEnum.customFeedbacks]); + } + // Concat new variables newVariables = { ...newVariables, @@ -163,6 +169,8 @@ export const dispatchLoop = async (props: Props): Promise => { } ] : [], - [DispatchNodeResponseKeyEnum.newVariables]: newVariables + [DispatchNodeResponseKeyEnum.newVariables]: newVariables, + [DispatchNodeResponseKeyEnum.customFeedbacks]: + customFeedbacks.length > 0 ? customFeedbacks : undefined }; }; diff --git a/packages/service/core/workflow/dispatch/plugin/run.ts b/packages/service/core/workflow/dispatch/plugin/run.ts index defaf4dbea94..0c776b0ad3a2 100644 --- a/packages/service/core/workflow/dispatch/plugin/run.ts +++ b/packages/service/core/workflow/dispatch/plugin/run.ts @@ -132,35 +132,41 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise item.moduleType === FlowNodeTypeEnum.pluginOutput); const usagePoints = await computedAppToolUsage({ @@ -200,7 +206,8 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise; export const dispatchCustomFeedback = (props: Record): Response => { const { - runningAppInfo: { id: appId }, - chatId, - responseChatItemId: dataId, - stream, - workflowStreamResponse, params: { system_textareaInput: feedbackText = '' } } = props as Props; - setTimeout(() => { - addCustomFeedbacks({ - appId, - chatId, - dataId, - feedbacks: [feedbackText] - }); - }, 60000); - - if (stream) { - if (!chatId || !dataId) { - workflowStreamResponse?.({ - event: SseResponseEventEnum.fastAnswer, - data: textAdaptGptResponse({ - text: `\n\n**自定义反馈成功: (仅调试模式下展示该内容)**: "${feedbackText}"\n\n` - }) - }); - } - } - return { [DispatchNodeResponseKeyEnum.nodeResponse]: { textOutput: feedbackText - } + }, + [DispatchNodeResponseKeyEnum.customFeedbacks]: [feedbackText] }; }; diff --git a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts index cb16b4c0d5fc..c0fc0373956a 100644 --- a/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts +++ b/packages/service/core/workflow/dispatch/tools/runUpdateVar.ts @@ -14,8 +14,6 @@ import { type ModuleDispatchProps } from '@fastgpt/global/core/workflow/runtime/ import { runtimeSystemVar2StoreType } from '../utils'; import { isValidReferenceValue } from '@fastgpt/global/core/workflow/utils'; import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils'; -import { parseUrlToFileType } from '@fastgpt/global/common/file/tools'; -import { z } from 'zod'; type Props = ModuleDispatchProps<{ [NodeInputKeyEnum.updateList]: TUpdateListItem[]; @@ -27,7 +25,6 @@ export const dispatchUpdateVariable = async (props: Props): Promise => chatConfig, params, variables, - cloneVariables, runtimeNodes, workflowStreamResponse, externalProvider, @@ -36,7 +33,6 @@ export const dispatchUpdateVariable = async (props: Props): Promise => const { updateList } = params; const nodeIds = runtimeNodes.map((node) => node.nodeId); - const urlSchema = z.string().url(); const result = updateList.map((item) => { const variable = item.variable; @@ -66,19 +62,11 @@ export const dispatchUpdateVariable = async (props: Props): Promise => return valueTypeFormat(val, item.valueType); } else { - const val = getReferenceVariableValue({ + return getReferenceVariableValue({ value: item.value, variables, nodes: runtimeNodes }); - - if ( - Array.isArray(val) && - val.every((url) => typeof url === 'string' && urlSchema.safeParse(url).success) - ) { - return val.map((url) => parseUrlToFileType(url)).filter(Boolean); - } - return val; } })(); @@ -106,7 +94,6 @@ export const dispatchUpdateVariable = async (props: Props): Promise => event: SseResponseEventEnum.updateVariables, data: runtimeSystemVar2StoreType({ variables, - cloneVariables, removeObj: externalProvider.externalWorkflowVariables, userVariablesConfigs: chatConfig?.variables }) diff --git a/packages/service/core/workflow/dispatch/type.d.ts b/packages/service/core/workflow/dispatch/type.d.ts index 2bf3280e856b..aafc0d78eeef 100644 --- a/packages/service/core/workflow/dispatch/type.d.ts +++ b/packages/service/core/workflow/dispatch/type.d.ts @@ -37,6 +37,7 @@ export type DispatchFlowResponse = { [DispatchNodeResponseKeyEnum.assistantResponses]: AIChatItemValueItemType[]; [DispatchNodeResponseKeyEnum.runTimes]: number; [DispatchNodeResponseKeyEnum.memories]?: Record; + [DispatchNodeResponseKeyEnum.customFeedbacks]?: string[]; [DispatchNodeResponseKeyEnum.newVariables]: Record; durationSeconds: number; }; diff --git a/packages/service/core/workflow/dispatch/utils.ts b/packages/service/core/workflow/dispatch/utils.ts index ec66dd67722e..03a432ecdfe0 100644 --- a/packages/service/core/workflow/dispatch/utils.ts +++ b/packages/service/core/workflow/dispatch/utils.ts @@ -1,9 +1,11 @@ +import path from 'path'; import { getErrText } from '@fastgpt/global/common/error/utils'; -import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; +import { ChatRoleEnum, ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants'; import type { ChatItemType, UserChatItemFileItemType } from '@fastgpt/global/core/chat/type.d'; import { NodeOutputKeyEnum, VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import type { VariableItemType } from '@fastgpt/global/core/app/type'; import { encryptSecret } from '../../../common/secret/aes256gcm'; +import { imageFileType } from '@fastgpt/global/common/file/constants'; import { type RuntimeEdgeItemType, type RuntimeNodeItemType, @@ -120,12 +122,10 @@ export const checkQuoteQAValue = (quoteQA?: SearchDataResponseItemType[]) => { /* remove system variable */ export const runtimeSystemVar2StoreType = ({ variables, - cloneVariables, removeObj = {}, userVariablesConfigs = [] }: { variables: Record; - cloneVariables: Record; removeObj?: Record; userVariablesConfigs?: VariableItemType[]; }) => { @@ -155,9 +155,33 @@ export const runtimeSystemVar2StoreType = ({ }; } } - // Remove URL from file variables + // Handle file variables else if (item.type === VariableInputEnum.file) { - copyVariables[item.key] = cloneVariables[item.key]; + const currentValue = copyVariables[item.key]; + + copyVariables[item.key] = currentValue + .map((url: string) => { + try { + const urlObj = new URL(url); + // Extract key: remove bucket prefix (e.g., "/fastgpt-private/") + const key = decodeURIComponent(urlObj.pathname.replace(/^\/[^/]+\//, '')); + const filename = path.basename(key) || 'file'; + const extname = path.extname(key).toLowerCase(); // includes the dot, e.g., ".jpg" + + // Check if it's an image type + const isImage = extname && imageFileType.includes(extname); + + return { + id: path.basename(key, path.extname(key)), // filename without extension + key, + name: filename, + type: isImage ? ChatFileTypeEnum.image : ChatFileTypeEnum.file + }; + } catch { + return null; + } + }) + .filter((file: any) => file !== null); } }); diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx index 1a05f0692f4f..6c7fc314aef3 100644 --- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx +++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx @@ -217,17 +217,15 @@ const RenderUserSelectInteractive = React.memo(function RenderInteractive({ ); }); const RenderUserFormInteractive = React.memo(function RenderFormInput({ - interactive, - chatItemDataId + interactive }: { interactive: InteractiveBasicType & UserInputInteractive; - chatItemDataId: string; }) { const { t } = useTranslation(); const defaultValues = useMemo(() => { if (interactive.type === 'userInput') { - return interactive.params.inputForm?.reduce((acc: Record, item, index) => { + return interactive.params.inputForm?.reduce((acc: Record, item) => { // 使用 ?? 运算符,只有 undefined 或 null 时才使用 defaultValue acc[item.key] = item.value ?? item.defaultValue; return acc; @@ -239,42 +237,18 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({ const handleFormSubmit = useCallback( (data: Record) => { const finalData: Record = {}; - interactive.params.inputForm?.forEach((item, index) => { + interactive.params.inputForm?.forEach((item) => { if (item.key in data) { finalData[item.key] = data[item.key]; } }); - if (typeof window !== 'undefined') { - const dataToSave = { ...data }; - interactive.params.inputForm?.forEach((item) => { - if ( - item.type === 'fileSelect' && - Array.isArray(dataToSave[item.key]) && - dataToSave[item.key].length > 0 - ) { - const files = dataToSave[item.key]; - if (files[0]?.url !== undefined) { - dataToSave[item.key] = files - .map((file: any) => ({ - url: file.url, - key: file.key, - name: file.name, - type: file.type - })) - .filter((file: any) => file.url); - } - } - }); - sessionStorage.setItem(`interactiveForm_${chatItemDataId}`, JSON.stringify(dataToSave)); - } - onSendPrompt({ text: JSON.stringify(finalData), isInteractivePrompt: true }); }, - [interactive.params.inputForm, chatItemDataId] + [interactive.params.inputForm] ); return ( @@ -282,7 +256,6 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({ (