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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user