feat: 完成工作流多文件文档解析闭环

- 支持文档解析节点批量解析并收口为 documents 轻量输出

- 收口引用树、节点输出展示与旧工作流固定输出兼容

- 修复共享按钮点击事件,恢复多个节点加号交互
This commit is contained in:
2026-04-19 16:05:40 +08:00
parent a5aab86de2
commit 1d8b9d9662
15 changed files with 496 additions and 48 deletions

View File

@@ -0,0 +1,53 @@
import { flushSync, mount, unmount } from 'svelte';
import { afterEach, describe, expect, it, vi } from 'vitest';
import Button from './button.svelte';
import MenuButton from './menu-button.svelte';
describe('Button', () => {
afterEach(() => {
document.body.innerHTML = '';
});
it('转发 onclick 到原生按钮', async () => {
const handleClick = vi.fn();
const host = document.createElement('div');
document.body.appendChild(host);
const app = mount(Button, {
target: host,
props: {
onclick: handleClick,
},
});
flushSync();
(host.querySelector('button') as HTMLButtonElement).click();
expect(handleClick).toHaveBeenCalledTimes(1);
await unmount(app);
host.remove();
});
it('通过 MenuButton 转发 onclick 到原生按钮', async () => {
const handleClick = vi.fn();
const host = document.createElement('div');
document.body.appendChild(host);
const app = mount(MenuButton, {
target: host,
props: {
onclick: handleClick,
},
});
flushSync();
(host.querySelector('button') as HTMLButtonElement).click();
expect(handleClick).toHaveBeenCalledTimes(1);
await unmount(app);
host.remove();
});
});

View File

@@ -3,11 +3,16 @@
import type {MyHTMLButtonAttributes} from './types';
import type {Snippet} from 'svelte';
const { children, primary, ...rest }: MyHTMLButtonAttributes & {
const { children, primary, onclick, ...rest }: MyHTMLButtonAttributes & {
children?: Snippet;
primary?: boolean;
} = $props();
</script>
<button type="button" {...rest} class="tf-btn {primary?'tf-btn-primary':''} nopan nodrag {rest.class}">
<button
type="button"
{...rest}
onclick={onclick}
class="tf-btn {primary?'tf-btn-primary':''} nopan nodrag {rest.class}"
>
{@render children?.()}
</button>

View File

@@ -182,11 +182,14 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"></polyline></svg>
</span>
{/if}
<span class="tf-parameter-name">{item.label}</span>
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
</div>
{#if item.dataType}
<span class="tf-parameter-type">{item.dataType}</span>
{/if}
{#if item.itemTypeLabel}
<span class="tf-parameter-meta">{item.itemTypeLabel}</span>
{/if}
</div>
</button>
</div>
@@ -768,4 +771,11 @@
white-space: nowrap;
flex-shrink: 0;
}
.tf-parameter-meta {
color: var(--tf-text-muted);
font-size: 11px;
white-space: nowrap;
flex-shrink: 0;
}
</style>

View File

@@ -40,6 +40,13 @@
});
const { updateNodeData } = useSvelteFlow();
const displayParameterName = $derived.by(() => {
const baseName = currentParameter.name || '';
if (!baseName) {
return '';
}
return baseName;
});
const updateAttribute = (key: string, value: any) => {
updateNodeData(currentNodeId, (node) => {
@@ -138,11 +145,13 @@
<div class="input-item">
{#if (position.length > 1)}
{#each position as p} &nbsp;{/each}
{/if}
<Input style="width: 100%;" value={currentParameter.name} placeholder={placeholder}
oninput={(e)=>{updateByEvent('name',e)}} disabled={currentParameter.nameDisabled === true} />
<div class="output-name-shell" class:output-name-shell--child={position.length > 1}>
{#if position.length > 1}
<span class="output-branch-marker"></span>
{/if}
<Input style="width: 100%;" value={displayParameterName} placeholder={placeholder}
oninput={(e)=>{updateByEvent('name',e)}} disabled={currentParameter.nameDisabled === true} />
</div>
</div>
<div class="input-item">
<Select items={currentParameter.dataTypeItems || parameterDataTypes} style="width: 100%" defaultValue={["String"]}
@@ -200,6 +209,27 @@
gap: 2px;
}
.output-name-shell {
display: flex;
align-items: center;
width: 100%;
gap: 8px;
&--child {
padding-left: 8px;
}
}
.output-branch-marker {
width: 10px;
height: 10px;
flex-shrink: 0;
border-left: 1px solid var(--tf-border-color);
border-bottom: 1px solid var(--tf-border-color);
border-bottom-left-radius: 6px;
opacity: 0.9;
}
.input-more-setting {
display: flex;
flex-direction: column;
@@ -221,4 +251,3 @@
}
</style>

View File

@@ -0,0 +1,22 @@
import type { Parameter } from '#types';
/**
* 解析开始节点参数在引用选择器中的展示名。
*
* 系统主问题字段始终保持“用户问题”口径;普通自定义字段优先展示参数名,
* 避免文件类型字段退化为默认“文件字段”标签。
*/
export const getStartNodeParameterLabel = (parameter: Parameter) => {
const name = String(parameter?.name || '').trim();
if (name === 'user_input') {
return (
String(parameter?.formLabel || parameter?.displayName || '用户问题').trim()
|| '用户问题'
);
}
return (
name
|| String(parameter?.formLabel || parameter?.displayName || '参数').trim()
|| '参数'
);
};

View File

@@ -1,6 +1,7 @@
import { type Edge, type Node, useNodesData, useStore } from '@xyflow/svelte';
import type { Parameter } from '#types';
import { getCurrentNodeId, getOptions } from '#components/utils/NodeUtils';
import { getStartNodeParameterLabel } from '#components/utils/startNodeParameterLabel';
import { nodeIcons } from '../../consts';
const fillRefNodeIds = (
@@ -21,24 +22,39 @@ const getChildren = (
parentId: string,
nodeIsChildren: boolean,
nodeType: string,
parentPathLabel = '',
parentIsCollection = false,
) => {
if (!params || params.length === 0) return [];
return params.map((param: any) => {
const isCollection = param.dataType === 'Array' && param.children && param.children.length > 0;
const childBaseLabel = param.formLabel || param.displayName || param.name;
const normalizedChildLabel = String(childBaseLabel || '').trim();
const pathLabel = !parentPathLabel
? normalizedChildLabel
: parentIsCollection
? `${parentPathLabel}.[].${normalizedChildLabel}`
: `${parentPathLabel}.${normalizedChildLabel}`;
const dataType = nodeIsChildren
? `Array<${param.dataType || 'String'}>`
: param.dataType || 'String';
const label = param.formLabel || param.displayName || param.name;
return {
label,
label: pathLabel,
dataType: dataType,
value: parentId + '.' + param.name,
selectable: true,
nodeType: nodeType,
displayLabel: pathLabel,
pathLabel,
itemTypeLabel: parentIsCollection ? '数组项字段' : undefined,
isCollection,
children: getChildren(
param.children,
parentId + '.' + param.name,
nodeIsChildren,
nodeType,
pathLabel,
isCollection,
),
};
});
@@ -72,8 +88,7 @@ const nodeToOptions = (
const dataType = nodeIsChildren
? `Array<${parameter.dataType || 'String'}>`
: parameter.dataType || 'String';
const label =
parameter.formLabel || parameter.displayName || parameter.name;
const label = getStartNodeParameterLabel(parameter);
children.push({
label,
dataType: dataType,