variable-parser.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. /**
  2. * 统一解析结点入参、出参:将 action 中的变量引用用 variableContext 解析为实际值。
  3. * 规则:{var}、{{var}} 为变量(替换为变量值);{arr}[{idx}]、{arr}[数字] 为数组下标,解析为对应元素;"hello{var}" 为字符串+变量拼接。
  4. * 所有函数的入参、出参都经本脚本解析后再传给对应结点。
  5. */
  6. const setParser = require('./actions/set-parser.js')
  7. const resolveValue = setParser.resolveValue
  8. const replaceVariablesInString = setParser.replaceVariablesInString
  9. const extractVarName = setParser.extractVarName
  10. /** 视为入参的字段(会被解析);inVars 由本脚本统一解析后传给各结点。 */
  11. const INPUT_KEYS = [
  12. 'value', 'target', 'template', 'area', 'savePath', 'condition', 'delay', 'interval',
  13. 'items', 'screenshot', 'region', 'method', 'clear', 'timeout', 'retry',
  14. 'min', 'max', 'avatar1', 'avatar2', 'friendAvatar', 'avatar', 'path', 'filePath',
  15. 'inputDataString', 'textVariable', 'senderVariable', 'appendMode',
  16. 'summaryPrompt', 'historyPrompt', 'model', 'prompt', 'systemPrompt',
  17. 'regionArea', 'saveDir', 'url', 'filename', 'imageUrl',
  18. 'imagePath', 'squareSpec', 'scale', 'method',
  19. 'recursive',
  20. 'stateKey', 'stateValue',
  21. 'key',
  22. ]
  23. /**
  24. * 将数组下标取值结果转为可嵌入字符串的形式
  25. */
  26. function toEmbedString(v) {
  27. if (v === undefined || v === null) return ''
  28. if (typeof v === 'string') return v
  29. if (typeof v === 'number' || typeof v === 'boolean') return String(v)
  30. try {
  31. return JSON.stringify(v)
  32. } catch (e) {
  33. return String(v)
  34. }
  35. }
  36. /**
  37. * 解析字符串中的数组下标:{arr}[{idx}]、{arr}[数字],替换为实际元素值。
  38. * 先于普通变量替换执行,以便 {img-prompt-arr}[{idx}] 能解析为当前项。
  39. */
  40. function replaceArrayIndexInString(str, variableContext) {
  41. if (typeof str !== 'string' || !variableContext) return str
  42. // {var}[{indexVar}]
  43. let out = str.replace(/\{([\w-]+)\}\s*\[\s*\{([\w-]+)\}\s*\]/g, (_, arrName, idxName) => {
  44. const arr = variableContext[arrName]
  45. const idxVal = variableContext[idxName]
  46. if (!Array.isArray(arr)) return toEmbedString(arr)
  47. const i = typeof idxVal === 'number' ? idxVal : parseInt(idxVal, 10)
  48. if (Number.isNaN(i) || i < 0 || i >= arr.length) return ''
  49. return toEmbedString(arr[i])
  50. })
  51. // {var}[数字]
  52. out = out.replace(/\{([\w-]+)\}\s*\[\s*(\d+)\s*\]/g, (_, arrName, numStr) => {
  53. const arr = variableContext[arrName]
  54. if (!Array.isArray(arr)) return toEmbedString(arr)
  55. const i = parseInt(numStr, 10)
  56. if (i < 0 || i >= arr.length) return ''
  57. return toEmbedString(arr[i])
  58. })
  59. return out
  60. }
  61. /**
  62. * 解析单值:先解析数组下标 {arr}[{idx}] / {arr}[n],再做 {{var}}、{var} 替换,最后对整体做引用解析。
  63. */
  64. /**
  65. * @param {{ skipBareWordLookup?: boolean }} [opts]
  66. * persist-read / persist-save 的 inVars[0] 为 config 键名,可能与 variables 同名;
  67. * 若整串为纯标识符且与变量名相同,不应替换成变量值,否则 key 会变成空或错值。
  68. */
  69. function resolveInputValue (val, variableContext, opts) {
  70. if (variableContext == null) return val
  71. if (typeof val === 'string') {
  72. const afterIndex = replaceArrayIndexInString(val, variableContext)
  73. const replaced = replaceVariablesInString(afterIndex, variableContext)
  74. let result = resolveValue(replaced, variableContext)
  75. if (!opts || !opts.skipBareWordLookup) {
  76. if (result === val && /^[\w-]+$/.test(val) && variableContext[val] !== undefined) result = variableContext[val]
  77. }
  78. return result
  79. }
  80. if (Array.isArray(val)) return val.map((item) => resolveInputValue(item, variableContext, opts))
  81. if (typeof val === 'object' && val !== null) {
  82. const out = {}
  83. for (const k in val) out[k] = resolveInputValue(val[k], variableContext, opts)
  84. return out
  85. }
  86. return val
  87. }
  88. /**
  89. * 解析整条 action 的入参,返回新对象(不修改原 action)
  90. * @param {object} action - 原始或已 parse 的 action
  91. * @param {object} variableContext - 变量表
  92. * @returns {object} 入参解析后的 action 副本
  93. */
  94. function resolveActionInputs(action, variableContext) {
  95. if (!action || typeof action !== 'object') return action
  96. if (!variableContext || typeof variableContext !== 'object') return Object.assign({}, action)
  97. const resolved = Object.assign({}, action)
  98. for (const key of INPUT_KEYS) {
  99. if (key in resolved && resolved[key] !== undefined && resolved[key] !== null) {
  100. resolved[key] = resolveInputValue(resolved[key], variableContext)
  101. }
  102. }
  103. if (resolved.inVars && Array.isArray(resolved.inVars)) {
  104. const isPersistKeySlot = resolved.type === 'fun' && (resolved.method === 'persist-read' || resolved.method === 'persist-save')
  105. resolved.inVars = resolved.inVars.map((v, i) => resolveInputValue(v, variableContext, isPersistKeySlot && i === 0 ? { skipBareWordLookup: true } : undefined))
  106. }
  107. if (resolved.outVars && Array.isArray(resolved.outVars)) {
  108. resolved.outVars = resolved.outVars.map((v) => (typeof v === 'string' ? extractVarName(v) : v))
  109. }
  110. if (resolved.condition && typeof resolved.condition === 'object' && !Array.isArray(resolved.condition)) {
  111. const c = resolved.condition
  112. if (c.interval != null) resolved.condition = Object.assign({}, c, { interval: resolveInputValue(c.interval, variableContext) })
  113. if (c.repeat != null) resolved.condition = Object.assign({}, resolved.condition, { repeat: resolveInputValue(c.repeat, variableContext) })
  114. }
  115. return resolved
  116. }
  117. /**
  118. * 从字符串或数组中提取所有变量引用名({var}、{{var}}、{arr}[{idx}] 中的 var/arr/idx)
  119. * @returns {string[]} 变量名列表(可能重复)
  120. */
  121. function extractVarNamesFromValue(val) {
  122. const names = []
  123. function collect(str) {
  124. if (typeof str !== 'string') return
  125. const doubleBrace = /\{\{([\w-]+)\}\}/g
  126. const singleBrace = /\{([\w-]+)\}/g
  127. let m
  128. while ((m = doubleBrace.exec(str)) !== null) names.push(m[1])
  129. while ((m = singleBrace.exec(str)) !== null) names.push(m[1])
  130. }
  131. if (typeof val === 'string') {
  132. collect(val)
  133. return names
  134. }
  135. if (Array.isArray(val)) {
  136. val.forEach((v) => names.push(...extractVarNamesFromValue(v)))
  137. return names
  138. }
  139. if (typeof val === 'object' && val !== null) {
  140. Object.keys(val).forEach((k) => names.push(...extractVarNamesFromValue(val[k])))
  141. return names
  142. }
  143. return names
  144. }
  145. /**
  146. * 校验 action 的 inVars/outVars 中引用的变量是否均在 declaredVariableNames 中声明
  147. * @param {object} action - 当前 action(含 inVars、outVars)
  148. * @param {Set|string[]} declaredVariableNames - 在 workflow.variables 中声明的变量名集合
  149. * @returns {{ valid: boolean, undeclared: string[] }}
  150. */
  151. function validateInOutVars(action, declaredVariableNames) {
  152. const declared = declaredVariableNames instanceof Set ? declaredVariableNames : new Set(declaredVariableNames || [])
  153. const undeclared = []
  154. function check(names) {
  155. names.forEach((n) => {
  156. if (n && typeof n === 'string' && !declared.has(n)) undeclared.push(n)
  157. })
  158. }
  159. if (action.inVars && Array.isArray(action.inVars)) {
  160. action.inVars.forEach((v) => check(extractVarNamesFromValue(v)))
  161. }
  162. if (action.outVars && Array.isArray(action.outVars)) {
  163. action.outVars.forEach((v) => {
  164. const name = typeof v === 'string' ? extractVarName(v) : v
  165. if (name) check([name])
  166. })
  167. }
  168. const unique = [...new Set(undeclared)]
  169. return { valid: unique.length === 0, undeclared: unique }
  170. }
  171. module.exports = {
  172. resolveActionInputs,
  173. resolveInputValue,
  174. replaceArrayIndexInString,
  175. extractVarName,
  176. extractVarNamesFromValue,
  177. validateInOutVars,
  178. }