|
@@ -4,20 +4,50 @@
|
|
|
*/
|
|
*/
|
|
|
const path = require('path')
|
|
const path = require('path')
|
|
|
const variableParser = require('../../variable-parser.js')
|
|
const variableParser = require('../../variable-parser.js')
|
|
|
|
|
+const { assertStrictKeys } = require('../../action-schema.js')
|
|
|
const FUN_NODE_REGISTRY = require('./fun-node-registry.js')
|
|
const FUN_NODE_REGISTRY = require('./fun-node-registry.js')
|
|
|
const funAdbJsonBridge = require('./fun-adb-json-bridge.js')
|
|
const funAdbJsonBridge = require('./fun-adb-json-bridge.js')
|
|
|
|
|
|
|
|
-/** type: io + method 与常见拼写 → 注册表 type */
|
|
|
|
|
|
|
+/** fun 结点在工作流 JSON 中仅允许这些字段(外加 method 对应的语义全在 inVars/outVars 中表达) */
|
|
|
|
|
+const FUN_STANDARD_KEYS = new Set([
|
|
|
|
|
+ 'type',
|
|
|
|
|
+ 'method',
|
|
|
|
|
+ 'inVars',
|
|
|
|
|
+ 'outVars',
|
|
|
|
|
+ 'condition',
|
|
|
|
|
+ 'delay',
|
|
|
|
|
+ 'times',
|
|
|
|
|
+ 'timeout',
|
|
|
|
|
+ 'retry',
|
|
|
|
|
+ 'data',
|
|
|
|
|
+ 'model',
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+function validateFunStandardFormat (action) {
|
|
|
|
|
+ if (!action || action.type !== 'fun') return { ok: true }
|
|
|
|
|
+ const extra = Object.keys(action).filter((k) => !FUN_STANDARD_KEYS.has(k))
|
|
|
|
|
+ if (extra.length > 0) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ ok: false,
|
|
|
|
|
+ error: `fun 结点仅允许字段: ${[...FUN_STANDARD_KEYS].sort().join(', ')}。禁止使用: ${extra.join(', ')}`,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (action.method == null || String(action.method).trim() === '') {
|
|
|
|
|
+ return { ok: false, error: 'fun 结点缺少 method' }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.inVars)) {
|
|
|
|
|
+ return { ok: false, error: 'fun 结点必须包含 inVars 数组(无入参写 [])' }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars)) {
|
|
|
|
|
+ return { ok: false, error: 'fun 结点必须包含 outVars 数组(无出参写 [])' }
|
|
|
|
|
+ }
|
|
|
|
|
+ return { ok: true }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** fun.method 须与注册表/脚本一致,不做别名兼容 */
|
|
|
function normalizeRegistryMethodName (name) {
|
|
function normalizeRegistryMethodName (name) {
|
|
|
if (name == null || name === '') return name
|
|
if (name == null || name === '') return name
|
|
|
- const key = String(name).trim().toLowerCase().replace(/_/g, '-')
|
|
|
|
|
- const map = {
|
|
|
|
|
- 'remov-folder': 'remove-folder',
|
|
|
|
|
- 'remove-forder': 'remove-folder',
|
|
|
|
|
- 'creat-folder': 'create-folder',
|
|
|
|
|
- 'create-forder': 'create-folder',
|
|
|
|
|
- }
|
|
|
|
|
- return map[key] || String(name).trim()
|
|
|
|
|
|
|
+ return String(name).trim()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const LEGACY_FUN_TYPES = [
|
|
const LEGACY_FUN_TYPES = [
|
|
@@ -51,137 +81,279 @@ function getRegistryScript(funcDir, type) {
|
|
|
return scriptCache.get(key)
|
|
return scriptCache.get(key)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function parse(action, parseContext) {
|
|
|
|
|
- const { extractVarName, resolveValue } = parseContext
|
|
|
|
|
- const variableContext = parseContext.variableContext || {}
|
|
|
|
|
- const parsed = {
|
|
|
|
|
- type: action.type,
|
|
|
|
|
- method: action.method,
|
|
|
|
|
- target: action.target,
|
|
|
|
|
- value: action.value,
|
|
|
|
|
- variable: action.variable,
|
|
|
|
|
- condition: action.condition,
|
|
|
|
|
- delay: action.delay || '',
|
|
|
|
|
- timeout: action.timeout,
|
|
|
|
|
- retry: action.retry,
|
|
|
|
|
|
|
+function pickFlowFields (action) {
|
|
|
|
|
+ const out = {}
|
|
|
|
|
+ for (const k of ['condition', 'delay', 'times', 'timeout', 'retry', 'data', 'model']) {
|
|
|
|
|
+ if (Object.prototype.hasOwnProperty.call(action, k)) out[k] = action[k]
|
|
|
}
|
|
}
|
|
|
- Object.assign(parsed, action)
|
|
|
|
|
|
|
+ return out
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parse (action, parseContext) {
|
|
|
|
|
+ const { extractVarName } = parseContext
|
|
|
|
|
+ const path = parseContext.actionPath || 'fun'
|
|
|
|
|
|
|
|
const regDef = REGISTRY_BY_TYPE.get(action.type)
|
|
const regDef = REGISTRY_BY_TYPE.get(action.type)
|
|
|
if (regDef) {
|
|
if (regDef) {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || !Array.isArray(action.outVars)) {
|
|
|
|
|
+ throw new Error(`${path}: 注册表结点 ${action.type} 须含 inVars、outVars 数组`)
|
|
|
|
|
+ }
|
|
|
const funcDir = (parseContext.compilerConfig && parseContext.compilerConfig.funcDir) || __dirname
|
|
const funcDir = (parseContext.compilerConfig && parseContext.compilerConfig.funcDir) || __dirname
|
|
|
if (regDef.customParse) {
|
|
if (regDef.customParse) {
|
|
|
const script = getRegistryScript(funcDir, action.type)
|
|
const script = getRegistryScript(funcDir, action.type)
|
|
|
if (script && typeof script.parseNode === 'function') return script.parseNode(action, parseContext)
|
|
if (script && typeof script.parseNode === 'function') return script.parseNode(action, parseContext)
|
|
|
}
|
|
}
|
|
|
- parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map((v) => extractVarName(v)) : []
|
|
|
|
|
|
|
+ const inSpecLen = (regDef.in && regDef.in.length) || 0
|
|
|
|
|
+ if (!regDef.customParse && inSpecLen > 0 && action.inVars.length < inSpecLen) {
|
|
|
|
|
+ throw new Error(`${path}: ${action.type} 至少需要 ${inSpecLen} 个 inVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const parsed = { type: action.type, inVars, outVars, ...pickFlowFields(action) }
|
|
|
const inKeys = regDef.in || []
|
|
const inKeys = regDef.in || []
|
|
|
- const inAlt = regDef.inAlt || {}
|
|
|
|
|
inKeys.forEach((key, i) => {
|
|
inKeys.forEach((key, i) => {
|
|
|
- const altKey = inAlt[key]
|
|
|
|
|
- parsed[key] = action.inVars?.[i] ?? action[key] ?? (altKey ? action[altKey] : undefined)
|
|
|
|
|
|
|
+ parsed[key] = inVars[i]
|
|
|
})
|
|
})
|
|
|
- parsed.variable = action.outVars?.[0] ? extractVarName(action.outVars[0]) : undefined
|
|
|
|
|
|
|
+ if (outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
|
|
|
return parsed
|
|
return parsed
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
switch (action.type) {
|
|
switch (action.type) {
|
|
|
- case 'fun':
|
|
|
|
|
- parsed.method = action.method
|
|
|
|
|
- parsed.inVars = Array.isArray(action.inVars) ? action.inVars.map((v) => extractVarName(v)) : []
|
|
|
|
|
- parsed.outVars = Array.isArray(action.outVars) ? action.outVars.map((v) => extractVarName(v)) : []
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ case 'fun': {
|
|
|
|
|
+ const vf = validateFunStandardFormat(action)
|
|
|
|
|
+ if (!vf.ok) throw new Error(`${path}: ${vf.error}`)
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'fun',
|
|
|
|
|
+ method: action.method,
|
|
|
|
|
+ inVars: action.inVars.map((v) => extractVarName(v)),
|
|
|
|
|
+ outVars: action.outVars.map((v) => extractVarName(v)),
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
case 'extract-messages':
|
|
case 'extract-messages':
|
|
|
case 'ocr-chat':
|
|
case 'ocr-chat':
|
|
|
case 'ocr-chat-history':
|
|
case 'ocr-chat-history':
|
|
|
- case 'extract-chat-history':
|
|
|
|
|
- parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
|
|
|
|
|
- parsed.avatar1 = action.inVars && action.inVars.length >= 2 ? action.inVars[0] : action.inVars?.[0]
|
|
|
|
|
- parsed.avatar2 = action.inVars && action.inVars.length >= 2 ? action.inVars[1] : action.avatar2
|
|
|
|
|
- parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
|
|
|
|
|
- if (parsed.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
|
|
|
|
|
- else if (action.variable) parsed.variable = extractVarName(action.variable)
|
|
|
|
|
- if (action.friendAvatar && !parsed.avatar1) parsed.avatar1 = action.friendAvatar
|
|
|
|
|
- if (action.myAvatar && !parsed.avatar2) parsed.avatar2 = action.myAvatar
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ case 'extract-chat-history': {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 3) {
|
|
|
|
|
+ throw new Error(`${path}: ${action.type} 须 inVars 至少 3 项(如好友 RGB、我的 RGB、区域)`)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: ${action.type} 须 outVars 至少 1 项`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: action.type,
|
|
|
|
|
+ inVars,
|
|
|
|
|
+ outVars,
|
|
|
|
|
+ variable: extractVarName(action.outVars[0]),
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
case 'save-messages':
|
|
case 'save-messages':
|
|
|
- break
|
|
|
|
|
case 'generate-summary':
|
|
case 'generate-summary':
|
|
|
- case 'generate-history-summary':
|
|
|
|
|
- parsed.summaryVariable = action.summaryVariable
|
|
|
|
|
- break
|
|
|
|
|
- case 'ai-generate':
|
|
|
|
|
- parsed.prompt = resolveValue(action.prompt, variableContext)
|
|
|
|
|
- parsed.model = action.model
|
|
|
|
|
- parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
|
|
|
|
|
- parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
|
|
|
|
|
- if (parsed.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
|
|
|
|
|
- else if (action.variable) parsed.variable = extractVarName(action.variable)
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ case 'generate-history-summary': {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'variable', 'summaryVariable', 'model'], path)
|
|
|
|
|
+ if (action.variable === undefined || action.variable === null || action.variable === '') {
|
|
|
|
|
+ throw new Error(`${path}: ${action.type} 须提供 variable(消息来源)`)
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: action.type,
|
|
|
|
|
+ variable: extractVarName(action.variable),
|
|
|
|
|
+ summaryVariable: action.summaryVariable != null ? extractVarName(action.summaryVariable) : undefined,
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ case 'ai-generate': {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars', 'model'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: ai-generate 须在 inVars[0] 填写 prompt`)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: ai-generate 须至少 1 个 outVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'ai-generate',
|
|
|
|
|
+ inVars,
|
|
|
|
|
+ outVars,
|
|
|
|
|
+ variable: extractVarName(action.outVars[0]),
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
case 'read-last-message': {
|
|
case 'read-last-message': {
|
|
|
- const inputVars = action.inVars || action.inputVars || []
|
|
|
|
|
- const outputVars = action.outVars || action.outputVars || []
|
|
|
|
|
- parsed.inVars = inputVars.map(v => extractVarName(v))
|
|
|
|
|
- parsed.outVars = outputVars.map(v => extractVarName(v))
|
|
|
|
|
- if (inputVars.length > 0) parsed.inputVar = extractVarName(inputVars[0])
|
|
|
|
|
- if (outputVars.length > 0) parsed.textVariable = extractVarName(outputVars[0])
|
|
|
|
|
- if (outputVars.length > 1) parsed.senderVariable = extractVarName(outputVars[1])
|
|
|
|
|
- if (!parsed.textVariable) parsed.textVariable = action.textVariable
|
|
|
|
|
- if (!parsed.senderVariable) parsed.senderVariable = action.senderVariable
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: read-last-message 须至少 1 个 inVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars) || action.outVars.length < 2) {
|
|
|
|
|
+ throw new Error(`${path}: read-last-message 须至少 2 个 outVars(文本、发送者)`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'read-last-message',
|
|
|
|
|
+ inVars,
|
|
|
|
|
+ outVars,
|
|
|
|
|
+ inputVar: inVars[0],
|
|
|
|
|
+ textVariable: outVars[0],
|
|
|
|
|
+ senderVariable: outVars[1],
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
case 'read-txt':
|
|
case 'read-txt':
|
|
|
- case 'read-text':
|
|
|
|
|
- parsed.inVars = action.inVars && action.inVars.length > 0 ? action.inVars.map(v => extractVarName(v)) : []
|
|
|
|
|
- parsed.filePath = action.inVars && action.inVars.length > 0 ? action.inVars[0] : action.filePath
|
|
|
|
|
- parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ case 'read-text': {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: ${action.type} 须至少 1 个 inVars(文件路径)`)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: ${action.type} 须至少 1 个 outVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: action.type,
|
|
|
|
|
+ inVars,
|
|
|
|
|
+ outVars,
|
|
|
|
|
+ filePath: inVars[0],
|
|
|
|
|
+ variable: outVars[0],
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
case 'save-txt':
|
|
case 'save-txt':
|
|
|
- case 'save-text':
|
|
|
|
|
- if (action.inVars && Array.isArray(action.inVars)) {
|
|
|
|
|
- parsed.inVars = action.inVars.map(v => extractVarName(v))
|
|
|
|
|
- parsed.content = action.inVars[0]
|
|
|
|
|
- parsed.filePath = action.inVars.length > 1 ? action.inVars[1] : action.filePath
|
|
|
|
|
- } else {
|
|
|
|
|
- parsed.inVars = []
|
|
|
|
|
- parsed.filePath = action.filePath
|
|
|
|
|
- parsed.content = action.content
|
|
|
|
|
|
|
+ case 'save-text': {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 2) {
|
|
|
|
|
+ throw new Error(`${path}: ${action.type} 须 inVars[0]=内容、inVars[1]=路径`)
|
|
|
}
|
|
}
|
|
|
- if (action.outVars && action.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
|
|
|
|
|
- break
|
|
|
|
|
- case 'img-bounding-box-location':
|
|
|
|
|
- parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
|
|
|
|
|
- parsed.screenshot = action.inVars?.[0]
|
|
|
|
|
- parsed.region = action.inVars?.[1] ?? action.region
|
|
|
|
|
- parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ if (!Array.isArray(action.outVars)) {
|
|
|
|
|
+ throw new Error(`${path}: ${action.type} 须含 outVars 数组(可无输出写 [])`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const parsed = {
|
|
|
|
|
+ type: action.type,
|
|
|
|
|
+ inVars,
|
|
|
|
|
+ outVars,
|
|
|
|
|
+ content: inVars[0],
|
|
|
|
|
+ filePath: inVars[1],
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ if (outVars.length > 0) parsed.variable = outVars[0]
|
|
|
|
|
+ return parsed
|
|
|
|
|
+ }
|
|
|
|
|
+ case 'img-bounding-box-location': {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 2) {
|
|
|
|
|
+ throw new Error(`${path}: img-bounding-box-location 须 inVars[0]=截图、inVars[1]=区域模板`)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: img-bounding-box-location 须至少 1 个 outVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'img-bounding-box-location',
|
|
|
|
|
+ inVars,
|
|
|
|
|
+ outVars,
|
|
|
|
|
+ screenshot: inVars[0],
|
|
|
|
|
+ region: inVars[1],
|
|
|
|
|
+ variable: outVars[0],
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
case 'img-center-point-location': {
|
|
case 'img-center-point-location': {
|
|
|
- parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map((v, i) => (i === 1 && Array.isArray(v) ? v : extractVarName(v))) : []
|
|
|
|
|
- parsed.template = action.inVars?.[0] ?? action.template
|
|
|
|
|
- parsed.scaleRange = Array.isArray(action.inVars?.[1]) ? action.inVars[1] : undefined
|
|
|
|
|
- parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: img-center-point-location 须至少 inVars[0]=模板路径`)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: img-center-point-location 须至少 1 个 outVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v, i) => (i === 1 && Array.isArray(v) ? v : extractVarName(v)))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'img-center-point-location',
|
|
|
|
|
+ inVars,
|
|
|
|
|
+ outVars,
|
|
|
|
|
+ template: inVars[0],
|
|
|
|
|
+ scaleRange: Array.isArray(action.inVars[1]) ? action.inVars[1] : undefined,
|
|
|
|
|
+ variable: outVars[0],
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ case 'img-cropping': {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 3) {
|
|
|
|
|
+ throw new Error(`${path}: img-cropping 须 inVars[0]=图、inVars[1]=保存路径、inVars[2]=裁剪规格`)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: img-cropping 须至少 1 个 outVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v, i) => (i === 2 && Array.isArray(v) ? v : extractVarName(v)))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'img-cropping',
|
|
|
|
|
+ inVars,
|
|
|
|
|
+ outVars,
|
|
|
|
|
+ imagePath: inVars[0],
|
|
|
|
|
+ savePath: inVars[1],
|
|
|
|
|
+ squareSpec: inVars[2],
|
|
|
|
|
+ variable: outVars[0],
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ case 'ocr': {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: ocr 须至少 1 个 inVars(图片路径)`)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: ocr 须至少 1 个 outVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ const inVars = action.inVars.map((v) => extractVarName(v))
|
|
|
|
|
+ const outVars = action.outVars.map((v) => extractVarName(v))
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'ocr',
|
|
|
|
|
+ inVars,
|
|
|
|
|
+ outVars,
|
|
|
|
|
+ image: inVars[0],
|
|
|
|
|
+ variable: outVars[0],
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ case 'smart-chat-append': {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || action.inVars.length < 2) {
|
|
|
|
|
+ throw new Error(`${path}: smart-chat-append 须 2 个 inVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
|
|
|
|
|
+ throw new Error(`${path}: smart-chat-append 须至少 1 个 outVars`)
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: 'smart-chat-append',
|
|
|
|
|
+ inVars: action.inVars.map((v) => extractVarName(v)),
|
|
|
|
|
+ outVars: action.outVars.map((v) => extractVarName(v)),
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ default: {
|
|
|
|
|
+ assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
|
|
|
|
|
+ if (!Array.isArray(action.inVars) || !Array.isArray(action.outVars)) {
|
|
|
|
|
+ throw new Error(`${path}: ${action.type} 须含 inVars、outVars 数组`)
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ type: action.type,
|
|
|
|
|
+ inVars: action.inVars.map((v) => extractVarName(v)),
|
|
|
|
|
+ outVars: action.outVars.map((v) => extractVarName(v)),
|
|
|
|
|
+ ...pickFlowFields(action),
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- case 'img-cropping':
|
|
|
|
|
- parsed.inVars = action.inVars && Array.isArray(action.inVars)
|
|
|
|
|
- ? action.inVars.map((v, i) => (i === 2 && Array.isArray(v) ? v : extractVarName(v)))
|
|
|
|
|
- : []
|
|
|
|
|
- parsed.imagePath = action.inVars?.[0] ?? action.imagePath
|
|
|
|
|
- parsed.savePath = action.inVars?.[1] ?? action.savePath
|
|
|
|
|
- parsed.squareSpec = action.inVars?.[2] ?? action.squareSpec
|
|
|
|
|
- if (action.outVars && action.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
|
|
|
|
|
- break
|
|
|
|
|
- case 'ocr':
|
|
|
|
|
- parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
|
|
|
|
|
- parsed.image = action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0 ? action.inVars[0] : action.image
|
|
|
|
|
- parsed.variable = action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
|
|
|
|
|
- break
|
|
|
|
|
- default:
|
|
|
|
|
- parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
|
|
|
|
|
- parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
|
|
|
|
|
- break
|
|
|
|
|
}
|
|
}
|
|
|
- return parsed
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function execute(action, ctx) {
|
|
async function execute(action, ctx) {
|
|
@@ -194,13 +366,18 @@ async function runAction(action, device, folderPath, resolution, ctx) {
|
|
|
return { success: true, skipped: true }
|
|
return { success: true, skipped: true }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (action.type === 'fun') {
|
|
|
|
|
+ const vf = validateFunStandardFormat(action)
|
|
|
|
|
+ if (!vf.ok) return { success: false, error: vf.error }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const resolvedAction = variableParser.resolveActionInputs(action, variableContext)
|
|
const resolvedAction = variableParser.resolveActionInputs(action, variableContext)
|
|
|
ctx.resolution = resolution || ctx.resolution || { width: 1080, height: 1920 }
|
|
ctx.resolution = resolution || ctx.resolution || { width: 1080, height: 1920 }
|
|
|
|
|
|
|
|
if (resolvedAction.type === 'fun') {
|
|
if (resolvedAction.type === 'fun') {
|
|
|
const m = resolvedAction.method != null ? String(resolvedAction.method).trim() : ''
|
|
const m = resolvedAction.method != null ? String(resolvedAction.method).trim() : ''
|
|
|
if (!m) return { success: false, error: 'fun 结点缺少 method(如 adb-click、json-to-arr)' }
|
|
if (!m) return { success: false, error: 'fun 结点缺少 method(如 adb-click、json-to-arr)' }
|
|
|
- return run(m, resolvedAction, ctx, device, folderPath)
|
|
|
|
|
|
|
+ return run(normalizeRegistryMethodName(m), resolvedAction, ctx, device, folderPath)
|
|
|
}
|
|
}
|
|
|
if (resolvedAction.type === 'ai' && resolvedAction.method) {
|
|
if (resolvedAction.type === 'ai' && resolvedAction.method) {
|
|
|
return run(resolvedAction.method, resolvedAction, ctx, device, folderPath)
|
|
return run(resolvedAction.method, resolvedAction, ctx, device, folderPath)
|
|
@@ -274,6 +451,16 @@ function get(funcDir, category) {
|
|
|
} catch (e) { /* skip missing script */ }
|
|
} catch (e) { /* skip missing script */ }
|
|
|
})
|
|
})
|
|
|
break
|
|
break
|
|
|
|
|
+ case 'fun':
|
|
|
|
|
+ mod = {}
|
|
|
|
|
+ ;(FUN_NODE_REGISTRY || []).filter((r) => r.category === 'fun').forEach((def) => {
|
|
|
|
|
+ const scriptPath = path.join(funcDir, def.script || def.type + '.js')
|
|
|
|
|
+ try {
|
|
|
|
|
+ const m = require(scriptPath)
|
|
|
|
|
+ if (m[def.execute]) mod[def.execute] = m[def.execute]
|
|
|
|
|
+ } catch (e) { /* skip missing script */ }
|
|
|
|
|
+ })
|
|
|
|
|
+ break
|
|
|
case 'chat':
|
|
case 'chat':
|
|
|
mod = (() => {
|
|
mod = (() => {
|
|
|
const chatHistory = require(path.join(funcDir, 'chat', 'chat-history.js'))
|
|
const chatHistory = require(path.join(funcDir, 'chat', 'chat-history.js'))
|
|
@@ -592,7 +779,11 @@ async function run(actionType, action, ctx, device, folderPath) {
|
|
|
|
|
|
|
|
case 'ai-generate': {
|
|
case 'ai-generate': {
|
|
|
const { getHistorySummary } = get(funcDir, 'chat')
|
|
const { getHistorySummary } = get(funcDir, 'chat')
|
|
|
- let prompt = resolveValue(action.prompt, variableContext)
|
|
|
|
|
|
|
+ if (!action.inVars || action.inVars.length < 1 || action.inVars[0] === undefined || action.inVars[0] === null) {
|
|
|
|
|
+ return { success: false, error: 'ai-generate 须在 inVars[0] 填写 prompt 文本' }
|
|
|
|
|
+ }
|
|
|
|
|
+ let prompt = action.inVars[0]
|
|
|
|
|
+ if (typeof prompt !== 'string') prompt = String(prompt)
|
|
|
if (prompt && prompt.includes('{historySummary}') && getHistorySummary) {
|
|
if (prompt && prompt.includes('{historySummary}') && getHistorySummary) {
|
|
|
let historySummary = variableContext['historySummary'] || ''
|
|
let historySummary = variableContext['historySummary'] || ''
|
|
|
if (!historySummary) {
|
|
if (!historySummary) {
|
|
@@ -706,13 +897,25 @@ async function run(actionType, action, ctx, device, folderPath) {
|
|
|
const inKeys = regDef.in || []
|
|
const inKeys = regDef.in || []
|
|
|
const inAlt = regDef.inAlt || {}
|
|
const inAlt = regDef.inAlt || {}
|
|
|
const input = {}
|
|
const input = {}
|
|
|
- inKeys.forEach((key, i) => {
|
|
|
|
|
- let val = action[key]
|
|
|
|
|
- if (action.inVars && action.inVars[i] !== undefined) val = action.inVars[i]
|
|
|
|
|
- if (val === undefined && inAlt[key]) val = action[inAlt[key]]
|
|
|
|
|
- input[key] = val != null ? String(val).trim() : val
|
|
|
|
|
- })
|
|
|
|
|
- input.folderPath = folderPath
|
|
|
|
|
|
|
+ if (actionType === 'persist-save') {
|
|
|
|
|
+ let sk = action.stateKey
|
|
|
|
|
+ let sv = action.stateValue
|
|
|
|
|
+ if (action.inVars && action.inVars[0] !== undefined) sk = action.inVars[0]
|
|
|
|
|
+ if (action.inVars && action.inVars[1] !== undefined) sv = action.inVars[1]
|
|
|
|
|
+ if (sk === undefined && inAlt.stateKey && action[inAlt.stateKey] !== undefined) sk = action[inAlt.stateKey]
|
|
|
|
|
+ if (sv === undefined && inAlt.stateValue && action[inAlt.stateValue] !== undefined) sv = action[inAlt.stateValue]
|
|
|
|
|
+ input.stateKey = sk != null && typeof sk === 'string' ? sk.trim() : sk
|
|
|
|
|
+ input.stateValue = sv
|
|
|
|
|
+ input.folderPath = folderPath
|
|
|
|
|
+ } else {
|
|
|
|
|
+ inKeys.forEach((key, i) => {
|
|
|
|
|
+ let val = action[key]
|
|
|
|
|
+ if (action.inVars && action.inVars[i] !== undefined) val = action.inVars[i]
|
|
|
|
|
+ if (val === undefined && inAlt[key]) val = action[inAlt[key]]
|
|
|
|
|
+ input[key] = val != null ? String(val).trim() : val
|
|
|
|
|
+ })
|
|
|
|
|
+ input.folderPath = folderPath
|
|
|
|
|
+ }
|
|
|
if (actionType === 'remove-folder') {
|
|
if (actionType === 'remove-folder') {
|
|
|
let rec = action.recursive
|
|
let rec = action.recursive
|
|
|
if ((rec === undefined || rec === null || rec === '') && action.inVars && action.inVars.length > 1 && action.inVars[1] !== undefined) {
|
|
if ((rec === undefined || rec === null || rec === '') && action.inVars && action.inVars.length > 1 && action.inVars[1] !== undefined) {
|
|
@@ -741,7 +944,9 @@ async function run(actionType, action, ctx, device, folderPath) {
|
|
|
const outVal = result.path ?? result.value ?? result.result
|
|
const outVal = result.path ?? result.value ?? result.result
|
|
|
if (outVal !== undefined && outVal !== null) {
|
|
if (outVal !== undefined && outVal !== null) {
|
|
|
if (actionType === 'json' && Array.isArray(outVal)) variableContext[outputVarName] = outVal
|
|
if (actionType === 'json' && Array.isArray(outVal)) variableContext[outputVarName] = outVal
|
|
|
- else variableContext[outputVarName] = typeof outVal === 'string' ? outVal : String(outVal)
|
|
|
|
|
|
|
+ else if (actionType === 'persist-read' && (typeof outVal === 'string' || typeof outVal === 'number')) {
|
|
|
|
|
+ variableContext[outputVarName] = outVal
|
|
|
|
|
+ } else variableContext[outputVarName] = typeof outVal === 'string' ? outVal : String(outVal)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
await logOutVars(action, variableContext, folderPath)
|
|
await logOutVars(action, variableContext, folderPath)
|