Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ui/src/workflow-canvas/config/node-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ export const variableAggregationNode = {
type: WorkflowNodeType.VariableAggregationNode,
text: '按聚合策略聚合每组的变量',
label: '变量聚合',
properties: { stepName: '变量聚合', config: { fields: [] } },
// 分支汇合:各分支互斥执行,需“任一”前置完成即执行,否则会一直等待未执行的分支
properties: { stepName: '变量聚合', condition: 'OR', config: { fields: [] } },
}

/* 变量拆分 */
Expand Down
30 changes: 26 additions & 4 deletions ui/src/workflow-canvas/nodes/document-extract-node/agent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node'
import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas'
import { argReference, finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node'
import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result'
import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type'

/** document-extract-node(文档内容提取)的 AI 生成配置:只有入参,无需额外配置 */
/** document-extract-node(文档内容提取)的 AI 生成配置:只有入参文档(documents),无其他可配置项 */

const params: JsonObjectSchema = {
type: 'object',
Expand All @@ -24,8 +26,28 @@ const returns: JsonObjectSchema = {

function useAgent(context: NodeAgentContext) {
return useNodeAgent(context, {
prompt: ['document-extract-node(文档内容提取)配置要点:', '- 文档已由入参 documents 绑定,无需其他配置,直接调用 update 完成校验。'].join('\n'),
tools: [createUpdateTool(context, config, () => null)],
prompt: [
'document-extract-node(文档内容提取)配置要点:',
'- 文档来自入参 documents(引用上游的文档列表),确认它能解析出要提取的文档;无需其他配置,直接调用 update 完成校验。',
].join('\n'),
tools: [
{
name: 'update',
description:
'配置当前文档内容提取节点:把入参 documents 绑定为待提取文档(document_list),其余字段无需配置。' +
'写入后自动校验,通过即配置完成并交付本节点的输出字段 content 与 document_list;不通过返回 error 继续修。',
parameters: config,
apply: async () => {
const node = getNode(context.lf, context.nodeId)
if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`)
const reference = argReference(context, 'documents')
if (!reference) return new ErrorResult('缺少入参 documents:需要上游传入选中的文档列表,请先把 documents 绑定到文档来源节点')
const nodeData = readNodeData<Record<string, unknown>>(node)
writeNodeProperties(node, { node_data: { ...nodeData, document_list: [...reference] } })
return finishWithValidate(context)
},
},
],
})
}

Expand Down
168 changes: 133 additions & 35 deletions ui/src/workflow-canvas/nodes/form-node/agent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { randomId } from '@/utils/common'
import { getNode, readNodeData, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas'
import { createUpdateTool, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node'
import { finishWithValidate, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node'
import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result'
import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type'
import type { VisibilityCompareOperator, VisibilityCondition, VisibilityRules } from '@/components/mk-dynamics-form'

/** 可由 AI 生成的字段类型,数据结构与 mk-dynamics-form 各字段配置器的 getData 一致 */
const INPUT_TYPES = [
Expand All @@ -23,6 +26,57 @@ const OPTION_TYPES: FormInputType[] = ['SingleSelect', 'MultiSelect', 'RadioCard
const MULTI_OPTION_TYPES: FormInputType[] = ['MultiSelect', 'MultiRow']
const DATE_FORMATS: Record<string, string> = { year: 'YYYY', month: 'YYYY-MM', date: 'YYYY-MM-DD', datetime: 'YYYY-MM-DD HH:mm:ss' }

/** 显隐条件的比较运算符,与 VisibilityCompareOperator 一致 */
const COMPARE_OPS: Array<{ label: string; value: VisibilityCompareOperator }> = [
{ label: '等于', value: 'eq' },
{ label: '不等于', value: 'not_eq' },
{ label: '包含', value: 'contain' },
{ label: '不包含', value: 'not_contain' },
{ label: '为真', value: 'is_true' },
{ label: '为假', value: 'is_not_true' },
{ label: '大于', value: 'gt' },
{ label: '大于等于', value: 'ge' },
{ label: '小于', value: 'lt' },
{ label: '小于等于', value: 'le' },
]
const COMPARE_VALUES = COMPARE_OPS.map((op) => op.value)

/** 一条显隐条件的 AI 入参:self=true 时基于本表单另一字段,否则基于上游节点字段 */
interface VisibilityConditionArgs {
self: boolean
field: string
node_id?: string
compare: VisibilityCompareOperator
value?: unknown
}

interface VisibilityRulesArgs {
action: 'show' | 'hide'
condition: 'and' | 'or'
conditions: VisibilityConditionArgs[]
}

/** 把 AI 给出的显隐条件构建为节点持久化结构;self 引用指向本表单另一字段,self_ref = [本节点ID, 字段名] */
function toVisibilityRules(rules: VisibilityRulesArgs | undefined, nodeId: string): VisibilityRules | null {
if (!rules) return null
const conditions: VisibilityCondition[] = []
for (const condition of rules.conditions) {
if (!condition || !condition.field || !condition.compare) continue
const fieldRef: [string, string] = condition.self ? [nodeId, condition.field] : [condition.node_id ?? '', condition.field]
if (!fieldRef[0] || !fieldRef[1]) continue
conditions.push({
id: randomId(),
field: fieldRef,
self: condition.self === true,
compare: condition.compare,
value: condition.compare === 'is_true' || condition.compare === 'is_not_true' ? true : (condition.value ?? ''),
})
}
return conditions.length
? { action: rules.action === 'hide' ? 'hide' : 'show', condition: rules.condition === 'or' ? 'or' : 'and', conditions }
: null
}

interface FormFieldArgs {
field: string
label: string
Expand All @@ -37,6 +91,7 @@ interface FormFieldArgs {
max?: number
step?: number
date_type?: keyof typeof DATE_FORMATS
visibility?: VisibilityRulesArgs
}

/** 文本类字段的长度校验规则,与文本配置器一致 */
Expand All @@ -47,15 +102,15 @@ function textRules(field: FormFieldArgs, required: boolean, minLength: number, m
return [requiredRule, lengthRule]
}

function toFormField(field: FormFieldArgs) {
function toFormField(field: FormFieldArgs, nodeId: string) {
const required = field.required !== false
const base = {
field: field.field,
label: { label: field.label, input_type: 'TooltipLabel', attrs: { tooltip: field.tooltip ?? '' }, props_info: {} },
input_type: field.input_type,
required,
show_default_value: true,
visibility_rules: null,
visibility_rules: toVisibilityRules(field.visibility, nodeId),
}
if (TEXT_TYPES.includes(field.input_type)) {
const minLength = Math.max(0, Number(field.min_length ?? 0))
Expand Down Expand Up @@ -123,6 +178,29 @@ const config: JsonObjectSchema = {
max: { type: 'number', description: '滑块最大值,默认 20' },
step: { type: 'number', description: '滑块步长,默认 1' },
date_type: { type: 'string', enum: Object.keys(DATE_FORMATS), description: '日期粒度,默认 datetime' },
visibility: {
type: 'object',
description: '显隐规则(可选):根据其他字段的值控制本字段是否显示',
properties: {
action: { type: 'string', enum: ['show', 'hide'], description: 'show=满足条件时显示本字段(否则隐藏);hide=满足条件时隐藏本字段(否则显示)' },
condition: { type: 'string', enum: ['and', 'or'], description: '多条条件组合方式:and 全部满足 / or 任一满足' },
conditions: {
type: 'array',
items: {
type: 'object',
properties: {
self: { type: 'boolean', description: 'true=基于本表单另一字段;false=基于上游节点字段' },
field: { type: 'string', description: '被引用的字段名:self=true 时为本表单另一字段名,否则为上游字段名' },
node_id: { type: 'string', description: 'self=false 时上游节点 id' },
compare: { type: 'string', enum: [...COMPARE_VALUES], description: '比较方式' },
value: { description: '比较值:is_true / is_not_true 时可不填,其余按字段类型给值' },
},
required: ['self', 'field', 'compare'],
},
},
},
required: ['action', 'condition', 'conditions'],
},
},
required: ['field', 'label', 'input_type'],
},
Expand Down Expand Up @@ -151,42 +229,62 @@ function useAgent(context: NodeAgentContext) {
'- fields:要收集的字段,field 为英文变量名(字母开头,下划线分隔),label 为中文显示名。',
'- input_type:文本 TextInput / 多行文本 TextareaInput / 密码 PasswordInput;单选下拉 SingleSelect / 多选下拉 MultiSelect / 选项卡 RadioCard / 单行选项卡 RadioRow / 单行多选卡 MultiRow(选项类需给 options);开关 SwitchInput;滑块 Slider(min / max / step);日期 DatePicker(date_type)。',
'- 可选:required(默认必填)、tooltip 提示、default_value 默认值、文本类 min_length / max_length。',
'- visibility:显隐规则(可选)。action:show=满足条件时显示本字段,hide=满足条件时隐藏本字段。想“满足某条件就把字段藏起来”用 hide;想“满足某条件才显示”用 show。',
'- form_content_format:展示给用户的引导文字,必须包含占位符 {{form}}(表单渲染位置)。',
'- 下游可用 [本节点ID, 字段名] 或 {{节点名称.字段名}} 引用各字段。',
].join('\n'),
tools: [
createUpdateTool(context, config, (args) => {
const node = getNode(context.lf, context.nodeId)!
const fields = (Array.isArray(args.fields) ? args.fields : []) as FormFieldArgs[]
if (!fields.length) return 'fields 至少一个字段'
const names = new Set<string>()
for (const field of fields) {
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(String(field.field))) return `字段名不合法:${field.field}`
if (names.has(field.field)) return `字段名重复:${field.field}`
if (!INPUT_TYPES.includes(field.input_type)) return `input_type 不支持:${field.input_type}`
if (OPTION_TYPES.includes(field.input_type) && !field.options?.length) return `选项字段 ${field.field} 需要 options`
if (field.input_type === 'Slider' && Number(field.max ?? 20) <= Number(field.min ?? 0)) return `滑块字段 ${field.field} 的 max 需大于 min`
names.add(field.field)
}
const format =
typeof args.form_content_format === 'string' && args.form_content_format.includes('{{form}}')
? args.form_content_format
: '你好,请先填写下面表单内容:\n{{form}}\n填写后请点击【提交】按钮进行提交。'
writeNodeProperties(node, {
node_data: {
...readNodeData<Record<string, unknown>>(node),
is_result: true,
form_field_list: fields.map(toFormField),
form_content_format: format,
},
config: {
...(node.properties.config as Record<string, unknown>),
fields: [{ label: '表单全部内容', value: 'form_data' }, ...fields.map((field) => ({ label: field.label, value: field.field }))],
},
})
node.clearNextNodeField(true)
return null
}),
{
name: 'update',
description:
'配置当前表单收集节点:字段列表 fields(含显隐 visibility)、引导文字 form_content_format。只传要设置的字段,其余保持不变。' +
'写入后自动校验,通过即配置完成并交付本节点输出字段;不通过返回 error 继续修。',
parameters: config,
apply: async (args) => {
const node = getNode(context.lf, context.nodeId)
if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`)
const fields = (Array.isArray(args.fields) ? args.fields : []) as FormFieldArgs[]
if (!fields.length) return new ErrorResult('fields 至少一个字段')
const names = new Set<string>()
for (const field of fields) {
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(String(field.field))) return new ErrorResult(`字段名不合法:${field.field}`)
if (names.has(field.field)) return new ErrorResult(`字段名重复:${field.field}`)
if (!INPUT_TYPES.includes(field.input_type)) return new ErrorResult(`input_type 不支持:${field.input_type}`)
if (OPTION_TYPES.includes(field.input_type) && !field.options?.length) return new ErrorResult(`选项字段 ${field.field} 需要 options`)
if (field.input_type === 'Slider' && Number(field.max ?? 20) <= Number(field.min ?? 0)) {
return new ErrorResult(`滑块字段 ${field.field} 的 max 需大于 min`)
}
names.add(field.field)
}
for (const field of fields) {
for (const condition of field.visibility?.conditions ?? []) {
if (!COMPARE_VALUES.includes(condition.compare)) return new ErrorResult(`字段 ${field.field} 显隐比较符不支持:${condition.compare}`)
if (condition.self !== true && !condition.node_id) return new ErrorResult(`字段 ${field.field} 的显隐条件 self=false 需给 node_id`)
if (condition.self === true && !names.has(condition.field)) {
return new ErrorResult(`字段 ${field.field} 的显隐 self 字段不存在:${condition.field}(只能引用本表单已定义字段)`)
}
}
}
const format =
typeof args.form_content_format === 'string' && args.form_content_format.includes('{{form}}')
? args.form_content_format
: '你好,请先填写下面表单内容:\n{{form}}\n填写后请点击【提交】按钮进行提交。'
writeNodeProperties(node, {
node_data: {
...readNodeData<Record<string, unknown>>(node),
is_result: true,
form_field_list: fields.map((field) => toFormField(field, node.id)),
form_content_format: format,
},
config: {
...(node.properties.config as Record<string, unknown>),
fields: [{ label: '表单全部内容', value: 'form_data' }, ...fields.map((field) => ({ label: field.label, value: field.field }))],
},
})
node.clearNextNodeField(true)
return finishWithValidate(context)
},
},
],
})
}
Expand Down
55 changes: 32 additions & 23 deletions ui/src/workflow-canvas/nodes/image-generate-node/agent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getNode, readNodeData, validatePromptVariables, writeNodeProperties } from '@/workflow-canvas/ai-generate/tools/canvas'
import { createUpdateTool, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node'
import { finishWithValidate, getModelsTool, MODEL_FIELD_SCHEMA, modelSlot, resolveModelFields, useNodeAgent } from '@/workflow-canvas/ai-generate/tools/node'
import { ErrorResult } from '@/workflow-canvas/ai-generate/tools/result'
import type { JsonObjectSchema, NodeAgentContext, NodeAgentModule } from '@/workflow-canvas/ai-generate/type'

/** image-generate-node(图片生成)的 AI 生成配置;node_data 协议以节点视图 index.vue 的 defaultForm 为准 */
Expand Down Expand Up @@ -44,28 +45,36 @@ function useAgent(context: NodeAgentContext) {
'- is_result:结果直接给用户时为 true。',
].join('\n'),
tools: [
createUpdateTool(context, config, (args) => {
const node = getNode(context.lf, context.nodeId)!
const modelFields = resolveModelFields(context, args.model, MODEL)
if (typeof modelFields === 'string') return modelFields
const error =
validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ??
validatePromptVariables(context.lf, context.nodeId, args.negative_prompt, 'negative_prompt')
if (error) return error
const nodeData = readNodeData<Record<string, unknown>>(node)
writeNodeProperties(node, {
node_data: {
...nodeData,
...modelFields,
prompt: String(args.prompt ?? nodeData.prompt ?? ''),
negative_prompt: typeof args.negative_prompt === 'string' ? args.negative_prompt : (nodeData.negative_prompt ?? ''),
dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0),
dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'),
is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true),
},
})
return null
}),
{
name: 'update',
description:
'配置当前图片生成节点:模型、正向/反向提示词、是否输出给用户等。只传要设置的字段,其余保持不变。' +
'写入后自动校验,通过即配置完成并交付本节点的输出字段;不通过返回 error 继续修。',
parameters: config,
apply: async (args) => {
const node = getNode(context.lf, context.nodeId)
if (!node) return new ErrorResult(`节点不存在: ${context.nodeId}`)
const modelFields = resolveModelFields(context, args.model, MODEL)
if (typeof modelFields === 'string') return new ErrorResult(modelFields)
const error =
validatePromptVariables(context.lf, context.nodeId, args.prompt, 'prompt') ??
validatePromptVariables(context.lf, context.nodeId, args.negative_prompt, 'negative_prompt')
if (error) return new ErrorResult(error)
const nodeData = readNodeData<Record<string, unknown>>(node)
writeNodeProperties(node, {
node_data: {
...nodeData,
...modelFields,
prompt: String(args.prompt ?? nodeData.prompt ?? ''),
negative_prompt: typeof args.negative_prompt === 'string' ? args.negative_prompt : (nodeData.negative_prompt ?? ''),
dialogue_number: Number(args.dialogue_number ?? nodeData.dialogue_number ?? 0),
dialogue_type: args.dialogue_type === 'NODE' || args.dialogue_type === 'WORKFLOW' ? args.dialogue_type : (nodeData.dialogue_type ?? 'NODE'),
is_result: typeof args.is_result === 'boolean' ? args.is_result : (nodeData.is_result ?? true),
},
})
return finishWithValidate(context)
},
},
getModelsTool(context, MODEL),
],
})
Expand Down
Loading
Loading