feat: 工作流适配数据中枢查询节点
- 新增查询数据与写入数据节点并移除旧数据中心节点入口 - 将查询数据节点切换为连接服务加 SQL 的执行模型 - 同步更新工作流校验、提示词上下文与设计器交互
This commit is contained in:
@@ -77,6 +77,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isMarkupIcon(icon?: string) {
|
||||
return typeof icon === 'string' && icon.trim().startsWith('<');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#snippet renderDefaultItems(items: SelectItem[], depth = 0)}
|
||||
@@ -97,7 +101,13 @@
|
||||
{#if group.selectable === false}
|
||||
<div class="tf-select-model-group-title">
|
||||
{#if group.icon}
|
||||
<span class="tf-select-model-group-icon">{@html group.icon}</span>
|
||||
<span class="tf-select-model-group-icon">
|
||||
{#if isMarkupIcon(group.icon)}
|
||||
{@html group.icon}
|
||||
{:else}
|
||||
<img src={group.icon} alt="" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span>{group.label}</span>
|
||||
</div>
|
||||
@@ -105,7 +115,11 @@
|
||||
<button class="tf-select-model-item {value.includes(model.value) ? 'active' : ''}" onclick={(e) => { e.stopPropagation(); handlerOnSelect(model); }}>
|
||||
<div class="tf-select-model-icon">
|
||||
{#if model.icon}
|
||||
{@html model.icon}
|
||||
{#if isMarkupIcon(model.icon)}
|
||||
{@html model.icon}
|
||||
{:else}
|
||||
<img src={model.icon} alt="" />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="tf-select-model-avatar">{group.label ? group.label.toString().charAt(0) : 'M'}</div>
|
||||
{/if}
|
||||
@@ -129,7 +143,11 @@
|
||||
<button class="tf-select-model-item {value.includes(group.value) ? 'active' : ''}" onclick={(e) => { e.stopPropagation(); handlerOnSelect(group); }}>
|
||||
<div class="tf-select-model-icon">
|
||||
{#if group.icon}
|
||||
{@html group.icon}
|
||||
{#if isMarkupIcon(group.icon)}
|
||||
{@html group.icon}
|
||||
{:else}
|
||||
<img src={group.icon} alt="" />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="tf-select-model-avatar">{group.label ? group.label.toString().charAt(0) : 'M'}</div>
|
||||
{/if}
|
||||
@@ -200,7 +218,11 @@
|
||||
</span>
|
||||
{:else if variant === 'model' && item.icon}
|
||||
<span class="tf-select-item-icon-input-model">
|
||||
{@html item.icon}
|
||||
{#if isMarkupIcon(item.icon)}
|
||||
{@html item.icon}
|
||||
{:else}
|
||||
<img src={item.icon} alt="" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
|
||||
@@ -217,7 +239,11 @@
|
||||
</span>
|
||||
{:else if variant === 'model' && item.icon}
|
||||
<span class="tf-select-item-icon-input-model">
|
||||
{@html item.icon}
|
||||
{#if isMarkupIcon(item.icon)}
|
||||
{@html item.icon}
|
||||
{:else}
|
||||
<img src={item.icon} alt="" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
|
||||
@@ -305,6 +331,12 @@
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
:global(.tf-select > div:first-child) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tf-select-default-wrapper {
|
||||
display: flex;
|
||||
@@ -525,13 +557,15 @@
|
||||
box-sizing: border-box;
|
||||
max-height: 480px;
|
||||
z-index: 99999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tf-select-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
|
||||
&.tf-select-primary-list {
|
||||
width: 100%; /* Default fills the wrapper, which is minWidth-constrained by the input */
|
||||
@@ -543,13 +577,13 @@
|
||||
.tf-select-wrapper:has(.tf-select-secondary-list) &.tf-select-primary-list {
|
||||
/* Let it take the width of the input minus borders/paddings if needed, but minWidth handles it mostly */
|
||||
width: auto;
|
||||
min-width: 180px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
&.tf-select-secondary-list {
|
||||
min-width: 220px;
|
||||
min-width: 188px;
|
||||
background: var(--tf-bg-surface);
|
||||
padding: 12px;
|
||||
padding: 10px;
|
||||
border-left: 1px solid var(--tf-bg-muted);
|
||||
animation: slideIn 0.2s ease-out;
|
||||
box-sizing: border-box;
|
||||
@@ -561,6 +595,8 @@
|
||||
|
||||
.tf-select-item-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tf-select-item-children {
|
||||
@@ -646,8 +682,10 @@
|
||||
.tf-parameter-label-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-right: 4px;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.tf-select-item-icon-input {
|
||||
width: 18px;
|
||||
@@ -692,12 +730,16 @@
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tf-parameter-name-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tf-parameter-expand-icon {
|
||||
@@ -709,16 +751,21 @@
|
||||
|
||||
.tf-parameter-name {
|
||||
color: inherit;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tf-parameter-type {
|
||||
background: var(--tf-bg-tag);
|
||||
color: var(--tf-text-secondary);
|
||||
padding: 2px 8px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -49,9 +49,13 @@
|
||||
<style lang="less">
|
||||
.input-container {
|
||||
display: grid;
|
||||
grid-template-columns: 40% 50% 10%;
|
||||
row-gap: 5px;
|
||||
column-gap: 3px;
|
||||
grid-template-columns: 124px minmax(0, 1fr) 22px;
|
||||
row-gap: 6px;
|
||||
column-gap: 4px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 318px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.none-params {
|
||||
font-size: 12px;
|
||||
@@ -61,16 +65,17 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
width: calc(100% - 5px);
|
||||
width: 100%;
|
||||
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-header {
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-secondary);
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -148,6 +148,8 @@
|
||||
.input-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.input-more-setting {
|
||||
@@ -171,4 +173,3 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -41,9 +41,13 @@
|
||||
<style lang="less">
|
||||
.input-container {
|
||||
display: grid;
|
||||
grid-template-columns: 40% 50% 10%;
|
||||
row-gap: 5px;
|
||||
column-gap: 3px;
|
||||
grid-template-columns: 124px minmax(0, 1fr) 22px;
|
||||
row-gap: 6px;
|
||||
column-gap: 4px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 318px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.none-params {
|
||||
font-size: 12px;
|
||||
@@ -53,16 +57,16 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
width: calc(100% - 5px);
|
||||
width: 100%;
|
||||
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-header {
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-secondary);
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
{/if}
|
||||
|
||||
|
||||
{#if customNode.renderFirst}
|
||||
<div bind:this={container} style={customNode.rootStyle||""} class={customNode.rootClass}></div>
|
||||
{/if}
|
||||
|
||||
{#if forms}
|
||||
{#each forms as form}
|
||||
{#if form.type === 'input'}
|
||||
@@ -201,7 +205,9 @@
|
||||
{/if}
|
||||
|
||||
|
||||
<div bind:this={container} style={customNode.rootStyle||""} class={customNode.rootClass}></div>
|
||||
{#if !customNode.renderFirst}
|
||||
<div bind:this={container} style={customNode.rootStyle||""} class={customNode.rootClass}></div>
|
||||
{/if}
|
||||
|
||||
|
||||
{#if customNode.outputDefsEnable !== false}
|
||||
@@ -279,4 +285,3 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||
import {type NodeProps, useNodesData, useStore, useSvelteFlow} from '@xyflow/svelte';
|
||||
import {Button, FloatingTrigger, Heading, Select} from '../base';
|
||||
import {MenuButton} from '../base/index.js';
|
||||
import RefParameterList from '../core/RefParameterList.svelte';
|
||||
@@ -20,8 +20,42 @@
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
let currentNode = useNodesData(currentNodeId);
|
||||
const { addParameter } = useAddParameter();
|
||||
const { nodes } = $derived(useStore());
|
||||
const editorParameters = $derived.by(() => {
|
||||
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
||||
const parameters = [
|
||||
...(((currentNode?.current?.data?.parameters as Array<any>) || data.parameters || []) as Array<any>)
|
||||
];
|
||||
if (queryContextNodeIds.length > 0) {
|
||||
parameters.push({
|
||||
id: 'queryDataContext',
|
||||
name: 'queryDataContext',
|
||||
dataType: 'String',
|
||||
description: '数据查询规则与连接表摘要',
|
||||
required: false,
|
||||
nameDisabled: true,
|
||||
dataTypeDisabled: true,
|
||||
deleteDisabled: true
|
||||
});
|
||||
}
|
||||
return parameters;
|
||||
});
|
||||
const queryContextOptions = $derived.by(() => {
|
||||
return (nodes || [])
|
||||
.filter((node: any) => node?.id !== currentNodeId && node?.type === 'search-dataset-node')
|
||||
.map((node: any) => ({
|
||||
label: node?.data?.title || '查询数据',
|
||||
value: node.id,
|
||||
displayLabel: node?.data?.title || '查询数据',
|
||||
description: node?.data?.sourceName ? '连接服务:' + node.data.sourceName : '未选择连接服务',
|
||||
}));
|
||||
});
|
||||
const queryContextNodeIds = $derived.by(() => {
|
||||
const ids = (currentNode?.current?.data?.queryContextNodeIds as Array<any>) || data.queryContextNodeIds || [];
|
||||
return Array.isArray(ids)
|
||||
? ids
|
||||
.map((item: any) => (item == null ? '' : String(item)))
|
||||
.filter((item: string) => item.trim().length > 0)
|
||||
: [];
|
||||
});
|
||||
|
||||
const options = getOptions();
|
||||
@@ -29,33 +63,29 @@
|
||||
let llmArray = $state<SelectItem[]>([]);
|
||||
onMount(async () => {
|
||||
const newLLMs = await options.provider?.llm?.();
|
||||
|
||||
|
||||
const isFlat = newLLMs?.every(item => !item.children);
|
||||
|
||||
|
||||
if (isFlat && newLLMs && newLLMs.length > 0) {
|
||||
const grouped = new Map<string, SelectItem[]>();
|
||||
for (const llm of newLLMs) {
|
||||
// If it still has a slash, parse it; otherwise, check if there's a custom logic we can infer brand.
|
||||
// In WorkflowDesign we pass `item.modelProvider?.providerName` via some other way, but here it's flat.
|
||||
// Actually, the label is just the title now (e.g. 'deepseek-chat').
|
||||
// Wait, LLMNode doesn't know the brand unless it's in the string or we modify WorkflowDesign to pass `brand`.
|
||||
// Let's modify WorkflowDesign to pass `brand` instead.
|
||||
let brand = (llm as any).brand || '其他';
|
||||
let modelName = llm.label;
|
||||
|
||||
const modelName = typeof llm.label === 'string'
|
||||
? llm.label
|
||||
: ((llm.displayLabel as string | undefined) || '模型');
|
||||
|
||||
if (!grouped.has(brand)) {
|
||||
grouped.set(brand, []);
|
||||
}
|
||||
grouped.get(brand)!.push({
|
||||
...llm,
|
||||
label: modelName,
|
||||
displayLabel: modelName // 外部选中时也只显示模型名称
|
||||
displayLabel: modelName
|
||||
});
|
||||
}
|
||||
|
||||
const treeArray: SelectItem[] = [];
|
||||
for (const [brand, models] of grouped) {
|
||||
// Try to get a representative icon for the brand from its children
|
||||
let groupIcon = undefined;
|
||||
if (models.length > 0) {
|
||||
const modelWithIcon = models.find(m => m.icon);
|
||||
@@ -72,9 +102,9 @@
|
||||
children: models
|
||||
});
|
||||
}
|
||||
llmArray.push(...treeArray);
|
||||
llmArray = treeArray;
|
||||
} else {
|
||||
llmArray.push(...(newLLMs || []));
|
||||
llmArray = [...(newLLMs || [])];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,6 +151,26 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const validIds = new Set(queryContextOptions.map((item) => String(item.value)));
|
||||
const normalized = queryContextNodeIds.filter((item) => validIds.has(item));
|
||||
if (normalized.length !== queryContextNodeIds.length) {
|
||||
updateNodeData(currentNodeId, {
|
||||
queryContextNodeIds: normalized
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const toggleQueryContextNode = (nodeId: string) => {
|
||||
const currentIds = [...queryContextNodeIds];
|
||||
const exists = currentIds.includes(nodeId);
|
||||
updateNodeData(currentNodeId, {
|
||||
queryContextNodeIds: exists
|
||||
? currentIds.filter((item) => item !== nodeId)
|
||||
: [...currentIds, nodeId]
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@@ -159,6 +209,20 @@
|
||||
|
||||
<RefParameterList dataKeyName="images" noneParameterText="无图片参数" />
|
||||
|
||||
<Heading level={3} mt="10px">查询数据信息</Heading>
|
||||
<div class="setting-item">
|
||||
<Select
|
||||
items={queryContextOptions}
|
||||
multiple={true}
|
||||
style="width: 100%"
|
||||
placeholder="请选择查询数据节点"
|
||||
onSelect={(item)=>{
|
||||
toggleQueryContextNode(String(item.value));
|
||||
}}
|
||||
value={queryContextNodeIds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Heading level={3} mt="10px">模型设置</Heading>
|
||||
<div class="setting-title">模型</div>
|
||||
<div class="setting-item">
|
||||
|
||||
@@ -69,6 +69,10 @@
|
||||
|
||||
|
||||
.tf-select {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
&-input {
|
||||
display: flex;
|
||||
border: 1px solid var(--tf-border-color);
|
||||
@@ -95,7 +99,8 @@
|
||||
|
||||
&-value {
|
||||
height: 100%;
|
||||
min-width: 10px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user