feat: 优化工作流字段化参数配置

- 开始节点固定 user_input 并区分系统入口与自定义参数

- LLM 与知识库节点切换为字段值加上游引用配置

- 单节点调试改为字段预览与上游引用输入模式
This commit is contained in:
2026-04-12 20:31:02 +08:00
parent 47655a728b
commit 8cfe5400fe
24 changed files with 2785 additions and 792 deletions

View File

@@ -1,15 +1,16 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { Position } from '@element-plus/icons-vue';
import { ElButton, ElForm, ElFormItem, ElMessage } from 'element-plus';
import { ElAlert, ElButton, ElForm, ElFormItem, ElMessage } from 'element-plus';
import { api } from '#/api/request';
import ShowJson from '#/components/json/ShowJson.vue';
import { $t } from '#/locales';
import WorkflowFormItem from '#/views/ai/workflow/components/WorkflowFormItem.vue';
import { buildSingleRunModel } from '../../../../../../packages/tinyflow-ui/src/utils/workflowNodeFields';
interface Props {
workflowId: any;
@@ -22,6 +23,52 @@ const singleRunForm = ref<FormInstance>();
const runParams = ref<any>({});
const submitLoading = ref(false);
const result = ref<any>('');
const singleRunModel = computed(() => buildSingleRunModel(props.node));
const isFieldMode = computed(() => singleRunModel.value.mode === 'fields');
const singleRunParameters = computed(() => singleRunModel.value.parameters);
const singleRunFields = computed(() => singleRunModel.value.fields);
const parameterDisplayNameMap = computed(() => {
return new Map(
singleRunParameters.value.map((parameter: any) => [
String(parameter.name || ''),
String(parameter.displayName || parameter.formLabel || parameter.name || ''),
]),
);
});
function buildFieldSegments(value: string) {
const source = String(value || '');
const segments: Array<{ text: string; token: boolean }> = [];
const regex = /\{\{\s*([^{}]+?)\s*}}/g;
let lastIndex = 0;
for (const match of source.matchAll(regex)) {
const matchedText = match[0] || '';
const tokenKey = String(match[1] || '').trim();
const start = match.index ?? 0;
if (start > lastIndex) {
segments.push({
text: source.slice(lastIndex, start),
token: false,
});
}
segments.push({
text: parameterDisplayNameMap.value.get(tokenKey) || tokenKey,
token: true,
});
lastIndex = start + matchedText.length;
}
if (lastIndex < source.length) {
segments.push({
text: source.slice(lastIndex),
token: false,
});
}
return segments.length > 0 ? segments : [{ text: '', token: false }];
}
function submit() {
singleRunForm.value?.validate((valid) => {
if (valid) {
@@ -48,9 +95,75 @@ function submit() {
<template>
<div>
<ElForm label-position="top" ref="singleRunForm" :model="runParams">
<template v-if="isFieldMode">
<div class="single-run-section">
<div class="single-run-section__title">字段值</div>
<ElAlert
type="info"
:closable="false"
show-icon
title="当前节点字段按正式配置展示;下方只需填写这些字段引用到的上游值。"
/>
<div class="single-run-field-list">
<div
v-for="field in singleRunFields"
:key="field.key"
class="single-run-field-card"
>
<div class="single-run-field-card__label">
{{ field.label }}
</div>
<div
class="single-run-field-card__value"
:class="{ 'is-multiline': field.multiline }"
>
<template v-if="field.value">
<template
v-for="(segment, index) in buildFieldSegments(field.value)"
:key="`${field.key}-${index}`"
>
<span
v-if="segment.token"
class="single-run-token-chip"
>
{{ segment.text }}
</span>
<span
v-else
class="single-run-field-card__text"
>
{{ segment.text }}
</span>
</template>
</template>
<span v-else class="single-run-field-card__placeholder">
{{ field.placeholder || '未设置' }}
</span>
</div>
</div>
</div>
</div>
<div class="single-run-section">
<div class="single-run-section__title">上游引用</div>
<ElAlert
v-if="singleRunParameters.length === 0"
type="success"
:closable="false"
show-icon
title="当前字段没有引用上游参数,可直接运行。"
/>
<WorkflowFormItem
v-else
v-model:run-params="runParams"
:parameters="singleRunParameters"
/>
</div>
</template>
<WorkflowFormItem
v-else
v-model:run-params="runParams"
:parameters="node?.data.parameters || []"
:parameters="singleRunParameters"
/>
<ElFormItem>
<ElButton
@@ -68,4 +181,71 @@ function submit() {
</div>
</template>
<style scoped></style>
<style scoped>
.single-run-section + .single-run-section {
margin-top: 20px;
}
.single-run-section__title {
margin-bottom: 10px;
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.single-run-field-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
}
.single-run-field-card {
border: 1px solid var(--el-border-color-light);
border-radius: 10px;
padding: 12px;
background: var(--el-fill-color-blank);
}
.single-run-field-card__label {
margin-bottom: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.single-run-field-card__value {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
min-height: 20px;
line-height: 1.7;
color: var(--el-text-color-primary);
white-space: nowrap;
}
.single-run-field-card__value.is-multiline {
white-space: pre-wrap;
}
.single-run-field-card__text {
white-space: inherit;
}
.single-run-field-card__placeholder {
color: var(--el-text-color-placeholder);
}
.single-run-token-chip {
display: inline-flex;
align-items: center;
max-width: 100%;
padding: 1px 8px;
border-radius: 999px;
border: 1px solid var(--el-color-primary-light-5);
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-size: 12px;
line-height: 20px;
}
</style>