From 89824b5b9b3a894b718ca38eff01a3cfb2ccb209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Mon, 3 Aug 2026 14:50:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=87=E6=BB=A4=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E8=BE=93=E5=87=BA=E4=B8=AD=E7=9A=84=E6=80=9D=E8=80=83?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一清洗运行结果、详情、预览与复制文本 - 支持嵌套、混合及未闭合思考标签 --- .../workflow/components/WorkflowChatPage.vue | 2 +- .../components/WorkflowFinalOutput.vue | 4 + .../workflowExecutionDetails.test.ts | 11 +++ .../components/workflowExecutionDetails.ts | 18 +++- .../components/workflowFinalOutput.test.ts | 73 ++++++++++++++- .../components/workflowFinalOutput.ts | 91 +++++++++++++++++-- 6 files changed, 182 insertions(+), 17 deletions(-) diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue index 63d80af8..bb0c4381 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowChatPage.vue @@ -1154,7 +1154,7 @@ function executionTraceText( 输出 - {{ formatExecutionValue(step.output) }} + {{ formatExecutionValue(step.output, true) }} 错误 diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.vue index 2522d96a..edfe8552 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.vue @@ -21,6 +21,7 @@ import { copyTextWithFeedback } from '#/utils/clipboard-feedback'; import { buildWorkflowFinalOutputView, formatWorkflowOutputPreview, + sanitizeWorkflowOutputTextForDisplay, serializeWorkflowOutput, } from './workflowFinalOutput'; @@ -118,6 +119,9 @@ function cellText(value: unknown) { if (value === null) { return '空值'; } + if (typeof value === 'string') { + return sanitizeWorkflowOutputTextForDisplay(value); + } return String(value); } diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowExecutionDetails.test.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowExecutionDetails.test.ts index c01155f8..b5ada4d0 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowExecutionDetails.test.ts +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowExecutionDetails.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { finalizeWorkflowExecutionSteps, + formatExecutionValue, hydrateWorkflowExecutionSteps, reduceWorkflowExecutionSteps, } from './workflowExecutionDetails'; @@ -121,4 +122,14 @@ describe('workflowExecutionDetails', () => { status: 'cancelled', }); }); + + it('filters thinking blocks only when formatting node outputs', () => { + const value = { + text: '内部推理最终答案', + }; + + expect(formatExecutionValue(value)).toContain('内部推理'); + expect(formatExecutionValue(value, true)).toContain('最终答案'); + expect(formatExecutionValue(value, true)).not.toContain('内部推理'); + }); }); diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowExecutionDetails.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowExecutionDetails.ts index 8b786151..2da15d62 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowExecutionDetails.ts +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowExecutionDetails.ts @@ -1,3 +1,5 @@ +import { sanitizeWorkflowOutputForDisplay } from './workflowFinalOutput'; + export interface WorkflowExecutionTrace { id: string; outcome: 'matched' | 'skipped'; @@ -192,15 +194,21 @@ export function finalizeWorkflowExecutionSteps( /** * 格式化节点输入或输出。 + * + * @param value 节点输入或输出 + * @param sanitizeThinking 是否移除旧思考标签内容块 */ -export function formatExecutionValue(value: unknown) { - if (typeof value === 'string') { - return value; +export function formatExecutionValue(value: unknown, sanitizeThinking = false) { + const displayValue = sanitizeThinking + ? sanitizeWorkflowOutputForDisplay(value) + : value; + if (typeof displayValue === 'string') { + return displayValue; } try { - return JSON.stringify(value ?? null, null, 2); + return JSON.stringify(displayValue ?? null, null, 2); } catch { - return String(value ?? ''); + return String(displayValue ?? ''); } } diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFinalOutput.test.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFinalOutput.test.ts index 01c368d7..146dea2c 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFinalOutput.test.ts +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFinalOutput.test.ts @@ -3,6 +3,8 @@ import { describe, expect, it } from 'vitest'; import { buildWorkflowFinalOutputView, formatWorkflowOutputPreview, + sanitizeWorkflowOutputForDisplay, + serializeWorkflowOutput, } from './workflowFinalOutput'; describe('workflowFinalOutput', () => { @@ -68,12 +70,12 @@ describe('workflowFinalOutput', () => { const view = buildWorkflowFinalOutputView({ image: { contentType: 'image/png', - fileName: 'result.png', + fileName: '隐藏图片名result.png', filePath: '/files/result.png', }, report: { contentType: 'application/pdf', - fileName: 'report.pdf', + fileName: '隐藏文件名report.pdf', filePath: '/files/report.pdf', }, }); @@ -98,4 +100,71 @@ describe('workflowFinalOutput', () => { sections: [], }); }); + + it('removes complete and unclosed thinking blocks from nested outputs', () => { + const sanitized = sanitizeWorkflowOutputForDisplay({ + answer: '内部推理最终答案', + items: ['前缀隐藏内容后缀', '未闭合内容'], + nested: { + text: '可见内容', + }, + }); + + expect(sanitized).toEqual({ + answer: '最终答案', + items: ['前缀后缀', ''], + nested: { + text: '可见内容', + }, + }); + }); + + it('removes nested and mixed thinking blocks without exposing outer content', () => { + const sanitized = sanitizeWorkflowOutputForDisplay({ + mixed: '外层内层仍属外层最终答案', + same: '前缀第一层第二层第一层结尾后缀', + unclosed: '隐藏继续隐藏仍未闭合', + }); + + expect(sanitized).toEqual({ + mixed: '最终答案', + same: '前缀后缀', + unclosed: '', + }); + }); + + it('uses sanitized values for rendering previews and copy text', () => { + const output = { + answer: '\n推理过程\n\n最终答案', + }; + const view = buildWorkflowFinalOutputView(output); + + expect(view.sections[0]?.scalarText).toBe('\n最终答案'); + expect(formatWorkflowOutputPreview(output)).not.toContain('推理过程'); + expect(serializeWorkflowOutput(output)).not.toContain('推理过程'); + expect(serializeWorkflowOutput(output)).not.toContain(''); + }); + + it('sanitizes only values reached by the bounded preview', () => { + const nested: Record = { + visible: '隐藏内容展示内容', + }; + for (let index = 1; index < 20; index += 1) { + Object.defineProperty(nested, `field${index}`, { + enumerable: true, + get() { + if (index >= 16) { + throw new Error('不应访问预览范围外的数据'); + } + return index; + }, + }); + } + + const preview = formatWorkflowOutputPreview(nested); + + expect(preview).toContain('展示内容'); + expect(preview).not.toContain('隐藏内容'); + expect(preview).toContain('另有 4 个字段'); + }); }); diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFinalOutput.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFinalOutput.ts index 325fed2e..682bb389 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFinalOutput.ts +++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/workflowFinalOutput.ts @@ -41,6 +41,70 @@ const IMAGE_EXTENSIONS = new Set([ 'webp', ]); const TABLE_COLUMN_LIMIT = 8; +const THINKING_TAG_PATTERN = /<\s*(?:(\/)\s*)?(?:thinking|think)\b[^>]*>/gi; + +/** + * 移除工作流展示文本中的旧思考标签内容块。 + */ +export function sanitizeWorkflowOutputTextForDisplay(value: string): string { + let cursor = 0; + let thinkingDepth = 0; + let sanitized = ''; + + for (const match of value.matchAll(THINKING_TAG_PATTERN)) { + const matchIndex = match.index; + if (thinkingDepth === 0) { + sanitized += value.slice(cursor, matchIndex); + } + if (match[1]) { + thinkingDepth = Math.max(0, thinkingDepth - 1); + } else { + thinkingDepth += 1; + } + cursor = matchIndex + match[0].length; + } + + if (thinkingDepth === 0) { + sanitized += value.slice(cursor); + } + return sanitized; +} + +/** + * 递归移除工作流展示值中的旧思考标签内容块。 + */ +export function sanitizeWorkflowOutputForDisplay( + value: unknown, + seen = new WeakMap(), +): unknown { + if (typeof value === 'string') { + return sanitizeWorkflowOutputTextForDisplay(value); + } + if (!value || typeof value !== 'object') { + return value; + } + const existing = seen.get(value); + if (existing !== undefined) { + return existing; + } + if (Array.isArray(value)) { + const sanitized: unknown[] = []; + seen.set(value, sanitized); + value.forEach((item) => { + sanitized.push(sanitizeWorkflowOutputForDisplay(item, seen)); + }); + return sanitized; + } + if (!isPlainObject(value)) { + return value; + } + const sanitized: Record = {}; + seen.set(value, sanitized); + Object.entries(value).forEach(([key, item]) => { + sanitized[key] = sanitizeWorkflowOutputForDisplay(item, seen); + }); + return sanitized; +} /** * 将工作流顶级最终输出整理为稳定的展示分区。 @@ -89,13 +153,14 @@ export function formatWorkflowOutputPreview( * 将完整输出序列化为复制文本。 */ export function serializeWorkflowOutput(value: unknown): string { - if (typeof value === 'string') { - return value; + const sanitizedValue = sanitizeWorkflowOutputForDisplay(value); + if (typeof sanitizedValue === 'string') { + return sanitizedValue; } try { - return JSON.stringify(value, null, 2); + return JSON.stringify(sanitizedValue, null, 2); } catch { - return String(value ?? ''); + return String(sanitizedValue ?? ''); } } @@ -199,7 +264,9 @@ function resolveMedia(value: unknown) { const source = mediaSource(candidate); images.push({ mimeType: stringValue(candidate.contentType || candidate.mimeType), - name: mediaName(candidate, source, '图片'), + name: sanitizeWorkflowOutputTextForDisplay( + mediaName(candidate, source, '图片'), + ), previewUrl: source, size: numberValue(candidate.size), status: 'ready', @@ -211,7 +278,9 @@ function resolveMedia(value: unknown) { attachmentRef: stringValue(candidate.attachmentRef) || undefined, downloadUrl: source, mimeType: stringValue(candidate.contentType || candidate.mimeType), - name: mediaName(candidate, source, '文件'), + name: sanitizeWorkflowOutputTextForDisplay( + mediaName(candidate, source, '文件'), + ), size: numberValue(candidate.size), status: 'ready', }); @@ -277,6 +346,9 @@ function formatScalar(value: unknown): string { if (value === null) { return '空值'; } + if (typeof value === 'string') { + return sanitizeWorkflowOutputTextForDisplay(value); + } return String(value); } @@ -314,9 +386,10 @@ function boundedPreview( seen = new WeakSet(), ): unknown { if (typeof value === 'string') { - return value.length <= limits.stringLimit - ? value - : `${value.slice(0, limits.stringLimit)}…`; + const sanitizedValue = sanitizeWorkflowOutputTextForDisplay(value); + return sanitizedValue.length <= limits.stringLimit + ? sanitizedValue + : `${sanitizedValue.slice(0, limits.stringLimit)}…`; } if (isScalar(value) || value === undefined) { return value;
{{ formatExecutionValue(step.output) }}
{{ formatExecutionValue(step.output, true) }}