/** * 统一解析结点入参、出参:将 action 中的变量引用用 variableContext 解析为实际值。 * 规则:{var}、{{var}} 为变量(替换为变量值);{arr}[{idx}]、{arr}[数字] 为数组下标,解析为对应元素;"hello{var}" 为字符串+变量拼接。 * 所有函数的入参、出参都经本脚本解析后再传给对应结点。 */ const setParser = require('./actions/set-parser.js') const resolveValue = setParser.resolveValue const replaceVariablesInString = setParser.replaceVariablesInString const extractVarName = setParser.extractVarName /** 视为入参的字段(会被解析);inVars 由本脚本统一解析后传给各结点。 */ const INPUT_KEYS = [ 'value', 'target', 'template', 'area', 'savePath', 'condition', 'delay', 'interval', 'items', 'screenshot', 'region', 'method', 'clear', 'timeout', 'retry', 'min', 'max', 'avatar1', 'avatar2', 'friendAvatar', 'avatar', 'path', 'filePath', 'inputDataString', 'textVariable', 'senderVariable', 'appendMode', 'summaryPrompt', 'historyPrompt', 'model', 'prompt', 'systemPrompt', 'regionArea', 'saveDir', 'url', 'filename', 'imageUrl', 'imagePath', 'squareSpec', 'scale', 'method', 'recursive', 'stateKey', 'stateValue', 'key', ] /** * 将数组下标取值结果转为可嵌入字符串的形式 */ function toEmbedString(v) { if (v === undefined || v === null) return '' if (typeof v === 'string') return v if (typeof v === 'number' || typeof v === 'boolean') return String(v) try { return JSON.stringify(v) } catch (e) { return String(v) } } /** * 解析字符串中的数组下标:{arr}[{idx}]、{arr}[数字],替换为实际元素值。 * 先于普通变量替换执行,以便 {img-prompt-arr}[{idx}] 能解析为当前项。 */ function replaceArrayIndexInString(str, variableContext) { if (typeof str !== 'string' || !variableContext) return str // {var}[{indexVar}] let out = str.replace(/\{([\w-]+)\}\s*\[\s*\{([\w-]+)\}\s*\]/g, (_, arrName, idxName) => { const arr = variableContext[arrName] const idxVal = variableContext[idxName] if (!Array.isArray(arr)) return toEmbedString(arr) const i = typeof idxVal === 'number' ? idxVal : parseInt(idxVal, 10) if (Number.isNaN(i) || i < 0 || i >= arr.length) return '' return toEmbedString(arr[i]) }) // {var}[数字] out = out.replace(/\{([\w-]+)\}\s*\[\s*(\d+)\s*\]/g, (_, arrName, numStr) => { const arr = variableContext[arrName] if (!Array.isArray(arr)) return toEmbedString(arr) const i = parseInt(numStr, 10) if (i < 0 || i >= arr.length) return '' return toEmbedString(arr[i]) }) return out } /** * 解析单值:先解析数组下标 {arr}[{idx}] / {arr}[n],再做 {{var}}、{var} 替换,最后对整体做引用解析。 */ /** * @param {{ skipBareWordLookup?: boolean }} [opts] * persist-read / persist-save 的 inVars[0] 为 config 键名,可能与 variables 同名; * 若整串为纯标识符且与变量名相同,不应替换成变量值,否则 key 会变成空或错值。 */ function resolveInputValue (val, variableContext, opts) { if (variableContext == null) return val if (typeof val === 'string') { const afterIndex = replaceArrayIndexInString(val, variableContext) const replaced = replaceVariablesInString(afterIndex, variableContext) let result = resolveValue(replaced, variableContext) if (!opts || !opts.skipBareWordLookup) { if (result === val && /^[\w-]+$/.test(val) && variableContext[val] !== undefined) result = variableContext[val] } return result } if (Array.isArray(val)) return val.map((item) => resolveInputValue(item, variableContext, opts)) if (typeof val === 'object' && val !== null) { const out = {} for (const k in val) out[k] = resolveInputValue(val[k], variableContext, opts) return out } return val } /** * 解析整条 action 的入参,返回新对象(不修改原 action) * @param {object} action - 原始或已 parse 的 action * @param {object} variableContext - 变量表 * @returns {object} 入参解析后的 action 副本 */ function resolveActionInputs(action, variableContext) { if (!action || typeof action !== 'object') return action if (!variableContext || typeof variableContext !== 'object') return Object.assign({}, action) const resolved = Object.assign({}, action) for (const key of INPUT_KEYS) { if (key in resolved && resolved[key] !== undefined && resolved[key] !== null) { resolved[key] = resolveInputValue(resolved[key], variableContext) } } if (resolved.inVars && Array.isArray(resolved.inVars)) { const isPersistKeySlot = resolved.type === 'fun' && (resolved.method === 'persist-read' || resolved.method === 'persist-save') resolved.inVars = resolved.inVars.map((v, i) => resolveInputValue(v, variableContext, isPersistKeySlot && i === 0 ? { skipBareWordLookup: true } : undefined)) } if (resolved.outVars && Array.isArray(resolved.outVars)) { resolved.outVars = resolved.outVars.map((v) => (typeof v === 'string' ? extractVarName(v) : v)) } if (resolved.condition && typeof resolved.condition === 'object' && !Array.isArray(resolved.condition)) { const c = resolved.condition if (c.interval != null) resolved.condition = Object.assign({}, c, { interval: resolveInputValue(c.interval, variableContext) }) if (c.repeat != null) resolved.condition = Object.assign({}, resolved.condition, { repeat: resolveInputValue(c.repeat, variableContext) }) } return resolved } /** * 从字符串或数组中提取所有变量引用名({var}、{{var}}、{arr}[{idx}] 中的 var/arr/idx) * @returns {string[]} 变量名列表(可能重复) */ function extractVarNamesFromValue(val) { const names = [] function collect(str) { if (typeof str !== 'string') return const doubleBrace = /\{\{([\w-]+)\}\}/g const singleBrace = /\{([\w-]+)\}/g let m while ((m = doubleBrace.exec(str)) !== null) names.push(m[1]) while ((m = singleBrace.exec(str)) !== null) names.push(m[1]) } if (typeof val === 'string') { collect(val) return names } if (Array.isArray(val)) { val.forEach((v) => names.push(...extractVarNamesFromValue(v))) return names } if (typeof val === 'object' && val !== null) { Object.keys(val).forEach((k) => names.push(...extractVarNamesFromValue(val[k]))) return names } return names } /** * 校验 action 的 inVars/outVars 中引用的变量是否均在 declaredVariableNames 中声明 * @param {object} action - 当前 action(含 inVars、outVars) * @param {Set|string[]} declaredVariableNames - 在 workflow.variables 中声明的变量名集合 * @returns {{ valid: boolean, undeclared: string[] }} */ function validateInOutVars(action, declaredVariableNames) { const declared = declaredVariableNames instanceof Set ? declaredVariableNames : new Set(declaredVariableNames || []) const undeclared = [] function check(names) { names.forEach((n) => { if (n && typeof n === 'string' && !declared.has(n)) undeclared.push(n) }) } if (action.inVars && Array.isArray(action.inVars)) { action.inVars.forEach((v) => check(extractVarNamesFromValue(v))) } if (action.outVars && Array.isArray(action.outVars)) { action.outVars.forEach((v) => { const name = typeof v === 'string' ? extractVarName(v) : v if (name) check([name]) }) } const unique = [...new Set(undeclared)] return { valid: unique.length === 0, undeclared: unique } } module.exports = { resolveActionInputs, resolveInputValue, replaceArrayIndexInString, extractVarName, extractVarNamesFromValue, validateInOutVars, }