From 765006747adc48ffabe6fa356dd9920684521926 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com>
Date: Tue, 11 Aug 2026 21:41:58 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=86=85=E5=AE=B9?=
=?UTF-8?q?=E6=A8=A1=E6=9D=BF=E5=8F=98=E9=87=8F=E5=BC=95=E7=94=A8=E4=B8=8E?=
=?UTF-8?q?=E6=8D=A2=E8=A1=8C=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 内容模板直接引用上游参数并自动维护运行参数
- 支持 Enjoy 变量标签编辑及中文变量显示
- 保留试运行与聊天运行页的模板输出换行
---
.../app/src/components/json/ShowJson.vue | 6 +-
.../ai/workflow/components/ExecResultItem.vue | 2 +-
.../WorkflowFinalOutput.component.test.ts | 18 ++++
.../__tests__/ExecResultItem.test.ts | 21 ++++
.../__tests__/ChatTimeMarkdown.test.ts | 22 +++-
.../components/core/ParamTokenEditor.svelte | 72 +++++++++----
.../components/core/ParamTokenEditor.test.ts | 39 +++++++
.../src/components/nodes/TemplateNode.svelte | 71 +++++++-----
.../src/components/utils/paramToken.test.ts | 21 ++++
.../src/components/utils/paramToken.ts | 42 ++++++--
.../src/utils/workflowNodeFields.test.ts | 70 ++++++++++++
.../src/utils/workflowNodeFields.ts | 102 +++++++++++++++++-
12 files changed, 419 insertions(+), 67 deletions(-)
create mode 100644 easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/ExecResultItem.test.ts
diff --git a/easyflow-ui-admin/app/src/components/json/ShowJson.vue b/easyflow-ui-admin/app/src/components/json/ShowJson.vue
index 21bc9145..c9cdfac5 100644
--- a/easyflow-ui-admin/app/src/components/json/ShowJson.vue
+++ b/easyflow-ui-admin/app/src/components/json/ShowJson.vue
@@ -12,8 +12,8 @@ import 'vue3-json-viewer/dist/vue3-json-viewer.css';
withDefaults(
defineProps<{
- value: unknown;
plain?: boolean;
+ value: unknown;
}>(),
{
plain: false,
@@ -49,4 +49,8 @@ watch(
border: none;
border-radius: 0;
}
+
+.res-container :deep(.jv-item.jv-string) {
+ white-space: pre-wrap;
+}
diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/ExecResultItem.vue b/easyflow-ui-admin/app/src/views/ai/workflow/components/ExecResultItem.vue
index 2ac35632..3ccdbe8e 100644
--- a/easyflow-ui-admin/app/src/views/ai/workflow/components/ExecResultItem.vue
+++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/ExecResultItem.vue
@@ -69,7 +69,7 @@ function makeItem(item: any, index: number) {
:src="`${item}`"
class="h-3/5 w-full"
>
-
+
{{ typeof item === 'string' ? item : JSON.stringify(item) }}
diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.component.test.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.component.test.ts
index d6c98d5c..269c7802 100644
--- a/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.component.test.ts
+++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/WorkflowFinalOutput.component.test.ts
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { describe, expect, it, vi } from 'vitest';
@@ -44,4 +45,21 @@ describe('workflow final output', () => {
expect(wrapper.text()).toContain('复制全部');
expect(wrapper.findAll('button')).toHaveLength(3);
});
+
+ it('renders scalar line breaks in the workflow chat result', async () => {
+ const wrapper = mount(WorkflowFinalOutput, {
+ props: {
+ output: {
+ output: '问题:你是谁\n答案:你好',
+ },
+ },
+ });
+
+ await nextTick();
+
+ const textOutput = wrapper.get('.workflow-final-output__text');
+ expect(textOutput.text()).toContain('问题:你是谁');
+ expect(textOutput.text()).toContain('答案:你好');
+ expect(textOutput.find('br').exists()).toBe(true);
+ });
});
diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/ExecResultItem.test.ts b/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/ExecResultItem.test.ts
new file mode 100644
index 00000000..70ddc5e2
--- /dev/null
+++ b/easyflow-ui-admin/app/src/views/ai/workflow/components/__tests__/ExecResultItem.test.ts
@@ -0,0 +1,21 @@
+import { mount } from '@vue/test-utils';
+
+import { describe, expect, it } from 'vitest';
+
+import ExecResultItem from '../ExecResultItem.vue';
+
+describe('execResultItem', () => {
+ it('preserves line breaks for text results', () => {
+ const wrapper = mount(ExecResultItem, {
+ props: {
+ contentType: 'text',
+ defName: 'output',
+ result: ['问题:你是谁\n答案:你好'],
+ resultCount: 1,
+ },
+ });
+
+ const textResult = wrapper.get('.whitespace-pre-wrap');
+ expect(textResult.text()).toContain('问题:你是谁\n答案:你好');
+ });
+});
diff --git a/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-markdown/__tests__/ChatTimeMarkdown.test.ts b/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-markdown/__tests__/ChatTimeMarkdown.test.ts
index eba1279b..cea25d90 100644
--- a/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-markdown/__tests__/ChatTimeMarkdown.test.ts
+++ b/easyflow-ui-admin/packages/effects/common-ui/src/components/chat-markdown/__tests__/ChatTimeMarkdown.test.ts
@@ -1,7 +1,7 @@
-import {mount} from '@vue/test-utils';
-import {nextTick} from 'vue';
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
-import {describe, expect, it, vi} from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
import ChatTimeMarkdown from '../ChatTimeMarkdown.vue';
@@ -11,7 +11,21 @@ vi.mock('@easyflow-core/preferences', () => ({
}),
}));
-describe('ChatTimeMarkdown', () => {
+describe('chatTimeMarkdown', () => {
+ it('renders a single source line break as a visible line break', async () => {
+ const wrapper = mount(ChatTimeMarkdown, {
+ props: {
+ content: '问题:你是谁\n答案:你好',
+ },
+ });
+
+ await nextTick();
+
+ expect(wrapper.text()).toContain('问题:你是谁');
+ expect(wrapper.text()).toContain('答案:你好');
+ expect(wrapper.find('br').exists()).toBe(true);
+ });
+
it('renders mermaid code fences with the shared mermaid block', async () => {
const wrapper = mount(ChatTimeMarkdown, {
props: {
diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.svelte
index 54ab0c96..c1df5de7 100644
--- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.svelte
+++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.svelte
@@ -20,7 +20,9 @@
findBackspaceTokenRange,
findTokenRangeAtCursor,
flattenParameterCandidates,
+ formatParamToken,
getTokenRanges,
+ type TokenSyntax,
} from '../utils/paramToken';
const {
@@ -30,6 +32,7 @@
rows = 3,
placeholder = '',
disabled = false,
+ tokenSyntax = 'mustache',
class: className = '',
style = '',
...rest
@@ -40,6 +43,7 @@
rows?: number;
placeholder?: string;
disabled?: boolean;
+ tokenSyntax?: TokenSyntax;
class?: string;
style?: string;
[key: string]: any;
@@ -169,10 +173,13 @@
});
}
- function createTokenDecorations(metaMap: Map): Extension {
+ function createTokenDecorations(
+ metaMap: Map,
+ syntax: TokenSyntax
+ ): Extension {
const buildDecorations = (source: string) => {
const decorations = [];
- for (const range of getTokenRanges(source)) {
+ for (const range of getTokenRanges(source, syntax)) {
const tokenMeta = metaMap.get(range.key);
const displayName =
!tokenMeta || tokenMeta.variant === 'invalid'
@@ -299,12 +306,15 @@
if (!editorView || disabled) {
return;
}
- insertTextAtSelection(`{{${paramName}}}`);
+ insertTextAtSelection(formatParamToken(paramName, tokenSyntax));
emitChange(editorView.state.doc.toString());
triggerObject?.hide?.();
}
- function moveCursorAcrossToken(direction: 'left' | 'right') {
+ function moveCursorAcrossToken(
+ direction: 'left' | 'right',
+ syntax: TokenSyntax
+ ) {
return (view: EditorView) => {
const selection = view.state.selection.main;
if (!selection.empty) {
@@ -317,11 +327,13 @@
direction === 'left'
? findTokenRangeAtCursor(source, cursor, {
includeStart: false,
- includeEnd: true
+ includeEnd: true,
+ syntax
})
: findTokenRangeAtCursor(source, cursor, {
includeStart: true,
- includeEnd: false
+ includeEnd: false,
+ syntax
});
if (!range) {
@@ -341,13 +353,17 @@
};
}
- function deleteTokenBackward(view: EditorView) {
+ function deleteTokenBackward(view: EditorView, syntax: TokenSyntax) {
const selection = view.state.selection.main;
if (!selection.empty) {
return false;
}
- const range = findBackspaceTokenRange(view.state.doc.toString(), selection.head);
+ const range = findBackspaceTokenRange(
+ view.state.doc.toString(),
+ selection.head,
+ syntax
+ );
if (!range) {
return false;
}
@@ -366,16 +382,21 @@
return true;
}
- function deleteTokenForward(view: EditorView) {
+ function deleteTokenForward(view: EditorView, syntax: TokenSyntax) {
const selection = view.state.selection.main;
if (!selection.empty) {
return false;
}
- const range = findTokenRangeAtCursor(view.state.doc.toString(), selection.head, {
- includeStart: true,
- includeEnd: false
- });
+ const range = findTokenRangeAtCursor(
+ view.state.doc.toString(),
+ selection.head,
+ {
+ includeStart: true,
+ includeEnd: false,
+ syntax
+ }
+ );
if (!range) {
return false;
}
@@ -398,23 +419,26 @@
return text.replace(/\r\n|\r|\n/g, ' ');
}
- function createEditorKeymap(singleLine: boolean) {
+ function createEditorKeymap(
+ singleLine: boolean,
+ syntax: TokenSyntax
+ ) {
const entries = [
{
key: 'ArrowLeft',
- run: moveCursorAcrossToken('left')
+ run: moveCursorAcrossToken('left', syntax)
},
{
key: 'ArrowRight',
- run: moveCursorAcrossToken('right')
+ run: moveCursorAcrossToken('right', syntax)
},
{
key: 'Backspace',
- run: deleteTokenBackward
+ run: (view: EditorView) => deleteTokenBackward(view, syntax)
},
{
key: 'Delete',
- run: deleteTokenForward
+ run: (view: EditorView) => deleteTokenForward(view, syntax)
},
...defaultKeymap,
...historyKeymap
@@ -528,11 +552,11 @@
}
}),
eventCompartment.of(createDomEventHandlers(isSingleLine)),
- keymapCompartment.of(createEditorKeymap(isSingleLine)),
+ keymapCompartment.of(createEditorKeymap(isSingleLine, tokenSyntax)),
editableCompartment.of([EditorView.editable.of(!disabled), EditorState.readOnly.of(disabled)]),
placeholderCompartment.of(placeholder ? cmPlaceholder(placeholder) : []),
lineModeCompartment.of(createLineModeExtension(isSingleLine)),
- tokenCompartment.of(createTokenDecorations(tokenMetaMap)),
+ tokenCompartment.of(createTokenDecorations(tokenMetaMap, tokenSyntax)),
themeCompartment.of(createEditorTheme(isSingleLine))
]
})
@@ -585,7 +609,9 @@
return;
}
editorView.dispatch({
- effects: keymapCompartment.reconfigure(createEditorKeymap(isSingleLine))
+ effects: keymapCompartment.reconfigure(
+ createEditorKeymap(isSingleLine, tokenSyntax)
+ )
});
});
@@ -603,7 +629,9 @@
return;
}
editorView.dispatch({
- effects: tokenCompartment.reconfigure(createTokenDecorations(tokenMetaMap))
+ effects: tokenCompartment.reconfigure(
+ createTokenDecorations(tokenMetaMap, tokenSyntax)
+ )
});
});
diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.test.ts b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.test.ts
index 62a02187..8fbce075 100644
--- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.test.ts
+++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/ParamTokenEditor.test.ts
@@ -170,4 +170,43 @@ describe('ParamTokenEditor', () => {
await destroy();
});
+
+ it('should render and insert Enjoy parameter tokens', async () => {
+ const onchange = vi.fn();
+ const { host, view, destroy } = await renderEditor({
+ tokenSyntax: 'enjoy',
+ value: '申请人:#(申请人)',
+ parameters: [
+ {
+ name: '申请人',
+ refType: 'ref',
+ ref: 'llm.output',
+ },
+ ],
+ onchange,
+ });
+
+ expect(host.querySelector('.param-token-chip-text')?.textContent).toBe(
+ '申请人',
+ );
+
+ view.dispatch({
+ changes: {
+ from: 0,
+ to: view.state.doc.length,
+ insert: '',
+ },
+ });
+ (host.querySelector('.param-token-item') as HTMLButtonElement).click();
+ flushSync();
+
+ expect(view.state.doc.toString()).toBe('#(申请人)');
+ expect(onchange).toHaveBeenCalledWith({
+ target: {
+ value: '#(申请人)',
+ },
+ });
+
+ await destroy();
+ });
});
diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/TemplateNode.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/TemplateNode.svelte
index 36bf8dc7..eb9fac9e 100644
--- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/TemplateNode.svelte
+++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/TemplateNode.svelte
@@ -2,14 +2,17 @@