1. 上游节点输出引用(核心功能)

- 新增 upstreamNodes computed:遍历当前选中节点的前驱链路,仅 model / http / form 三类节点被识别为有输出的节点
  - 新增 getNodeOutputFields,按节点类型提取可引用字段:
    - form → formConfig 的字段
    - http → response schema 的叶子字段
    - model → modelResponseBodyMapping(模型返回参数)的 key
    - 开始节点 → runFormFields(运行表单字段),从而让下游节点能引用"用户填的运行表单"作为输出

  2. 开始节点运行表单字段聚合
  - syncRunFormFields + 深监听:开始节点 formConfig 变化时自动聚合字段,存为 runFormFields,作为开始节点自身可被下游引用的输出

  3. 模型返回参数
  - handleModelConfirm 用 stripReadonlyFields 清理 requestBodyMapping 冗余字段,同时保存 modelResponseBodyMapping 供下游引用
  - 保存时 collectExposedFields 提取对外暴露字段存 modelFormFields

  4. 提示词 / 反向提示词
  - 保存 prompt、negativePrompt、runFormFields 到节点 DSL

  5. 其他
  - nodeTypes 用 markRaw 包裹 FlowNode,修复 Vue 警告
  - buildNodeFormConfigFromDsl 的 responseType 匹配改为兼容 key/value 两种取值
This commit is contained in:
2026-08-12 17:26:58 +08:00
parent 4e1cd284e9
commit 0e6381d2b3
9 changed files with 1661 additions and 50 deletions
@@ -0,0 +1,227 @@
// 模型参数表单共用工具(workflow 独立实现,不依赖内容创作)
// 与项目一致的深拷贝方式
export function deepClone<T>(val: T): T {
return JSON.parse(JSON.stringify(val ?? null)) as T;
}
// 按类型生成叶子字段空值
export function defaultLeafValue(def: any): any {
const t = def?.type || 'string';
const ft = def?.fieldType || '';
if (t === 'boolean' || ft === 'switch') return false;
if (t === 'number' || ft === 'number') return 0;
return '';
}
// 归一化数组元素:元素为 { type:'object', attrs } 包装时,递归预填 attrs 子字段
export function normalizeArrayItem(item: any): any {
if (item && typeof item === 'object') {
if (item.type === 'object' && item.attrs && typeof item.attrs === 'object' && !Array.isArray(item.attrs)) {
Object.values(item.attrs).forEach(normalizeFieldDef);
}
}
return item;
}
// 归一化字段定义:用 defaultValue 预填 value;递归处理 object/array 子树
export function normalizeFieldDef(def: any): any {
if (!def || typeof def !== 'object') return def;
const t = def.type;
if (t === 'object') {
const attrs = def.attrs;
if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) {
Object.values(attrs).forEach(normalizeFieldDef);
}
if (def.value === undefined || def.value === null) {
def.value = def.defaultValue !== undefined && def.defaultValue !== null ? def.defaultValue : {};
}
} else if (t === 'array') {
if (Array.isArray(def.value)) {
def.value.forEach(normalizeArrayItem);
} else if (Array.isArray(def.defaultValue)) {
def.value = def.defaultValue.map((it: any) => normalizeArrayItem(deepClone(it)));
} else {
def.value = [];
}
} else {
if (def.value === undefined || def.value === null) {
def.value = def.defaultValue !== undefined && def.defaultValue !== null ? def.defaultValue : defaultLeafValue(def);
}
}
return def;
}
// 归一化整个 requestBodyMapping 参数模板对象
export function normalizeModelParams(params: any): Record<string, any> {
if (!params || typeof params !== 'object') return {};
Object.values(params).forEach(normalizeFieldDef);
return params;
}
// 剔除 isForm === false(只读)字段:不在节点表单中显示,且不随工作流保存。
// 级联规则:容器若内部无可编辑子级则无意义,一并剔除
// 1. 字段自身 isForm === false → 整体剔除
// 2. object:剔除子字段后 attrs 为空(无可编辑子字段)→ object 一并剔除
// 3. array:过滤「子字段全部只读」的元素模板;无可编辑模板 → array 一并剔除
// 4. array 已添加元素内部被清空 → 一并移除
export function stripReadonlyFields(params: any): any {
if (!params || typeof params !== 'object' || Array.isArray(params)) return params;
for (const key of Object.keys(params)) {
const def = params[key];
if (!def || typeof def !== 'object') continue;
// 自身标记为只读 → 整体删除
if (def.isForm === false) {
delete params[key];
continue;
}
if (def.type === 'object') {
const attrs = def.attrs;
if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) {
stripReadonlyFields(attrs);
// 子字段全部只读 → object 无意义,一并剔除
if (Object.keys(attrs).length === 0) {
delete params[key];
}
}
} else if (def.type === 'array') {
let hasEditable = false;
// 单一元素模板(attrs 为 object
if (def.attrs && typeof def.attrs === 'object' && !Array.isArray(def.attrs)) {
stripReadonlyFields(def.attrs);
if (Object.keys(def.attrs).length > 0) hasEditable = true;
}
// 多元素模板:优先 enumValues(与 ModelField.arrayTemplates 一致),其次 attrs 数组;过滤子字段全部只读的空模板
const isEnumValues = Array.isArray(def.enumValues) && def.enumValues.length > 0;
const isAttrsArray = Array.isArray(def.attrs) && def.attrs.length > 0;
const templates = isEnumValues ? def.enumValues : isAttrsArray ? def.attrs : [];
const keptTemplates = templates.filter((t: any) => {
if (t && typeof t === 'object' && t.attrs && typeof t.attrs === 'object' && !Array.isArray(t.attrs)) {
stripReadonlyFields(t.attrs);
return Object.keys(t.attrs).length > 0;
}
return true;
});
if (isEnumValues) def.enumValues = keptTemplates;
if (isAttrsArray) def.attrs = keptTemplates;
if (keptTemplates.length > 0) hasEditable = true;
// 已添加元素内部清理后为空的 → 一并移除
if (Array.isArray(def.value)) {
def.value.forEach((it: any) => {
if (it && typeof it === 'object' && it.attrs && typeof it.attrs === 'object' && !Array.isArray(it.attrs)) {
stripReadonlyFields(it.attrs);
}
});
def.value = def.value.filter((it: any) => {
if (it && typeof it === 'object' && it.attrs && typeof it.attrs === 'object' && !Array.isArray(it.attrs)) {
return Object.keys(it.attrs).length > 0;
}
return true;
});
}
// 无可编辑内容 → array 无意义,一并剔除
if (!hasEditable) {
delete params[key];
}
}
}
return params;
}
// ===== 暴露清单:在工作流表单中展示的勾选字段 =====
// 单个暴露叶子字段(路径带实例索引,如 "messages.value[0].attrs.content"
export interface ExposedField {
path: string;
label: string;
fieldType: string;
type: string;
required: boolean;
options?: any[];
value?: any;
refNodeId?: string; // 预留:引用其他节点功能(后续启用)
}
// 收集 runtimeShow === true 的叶子字段,生成暴露清单
// 路径文法(与 restoreRuntimeShow 共用):
// object 子字段 → 父路径 + ".attrs." + 子key
// array 实例元素 → 父路径 + ".value[i]"
// 原始值数组元素(非 object 包装)不可勾选,跳过
export function collectExposedFields(params: any, prefix = ''): ExposedField[] {
if (!params || typeof params !== 'object') return [];
const result: ExposedField[] = [];
for (const key of Object.keys(params)) {
const def = params[key];
if (!def || typeof def !== 'object') continue;
const path = prefix ? `${prefix}.${key}` : key;
const t = def.type;
if (t === 'object') {
const attrs = def.attrs;
if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) {
result.push(...collectExposedFields(attrs, `${path}.attrs`));
}
} else if (t === 'array') {
// 仅遍历实例数组;元素为 { type:'object', attrs } 包装时才递归
if (Array.isArray(def.value)) {
def.value.forEach((item: any, i: number) => {
if (item && typeof item === 'object' && item.attrs && typeof item.attrs === 'object' && !Array.isArray(item.attrs)) {
result.push(...collectExposedFields(item.attrs, `${path}.value[${i}].attrs`));
}
});
}
} else if (def.runtimeShow === true) {
// 叶子且已勾选
result.push({
path,
label: def.label || key,
fieldType: def.fieldType || 'string',
type: t || 'string',
required: !!def.required,
options: Array.isArray(def.options) ? def.options : undefined,
value: def.value,
});
}
}
return result;
}
// 按暴露清单的 path 反解,把对应叶子 def 的 runtimeShow 置 true(加载时还原勾选)
export function restoreRuntimeShow(params: any, fields: ExposedField[] | null | undefined): void {
if (!params || typeof params !== 'object' || !Array.isArray(fields) || fields.length === 0) return;
for (const f of fields) {
if (!f || typeof f.path !== 'string') continue;
const cur = resolvePath(params, f.path);
if (cur && typeof cur === 'object' && !Array.isArray(cur)) {
cur.runtimeShow = true;
}
}
}
// 按路径走回 params 树;解析失败(schema 变更导致字段缺失)返回 undefined
function resolvePath(params: any, path: string): any {
if (!path) return undefined;
const segments = path.split('.');
let cur = params;
for (const seg of segments) {
if (cur === undefined || cur === null || typeof cur !== 'object' || Array.isArray(cur)) return undefined;
if (seg === 'attrs') {
cur = cur.attrs;
if (!cur || typeof cur !== 'object') return undefined;
} else {
const m = seg.match(/^value\[(\d+)\]$/);
if (m) {
const idx = Number(m[1]);
if (!Array.isArray(cur.value) || idx >= cur.value.length) return undefined;
cur = cur.value[idx];
} else {
cur = cur[seg];
}
}
}
return cur;
}
// 深度比较(JSON 序列化方式),供 index.vue 的"仅变更才写"守卫使用
export function isEqual(a: any, b: any): boolean {
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
}