yichael 2 місяців тому
батько
коміт
185c875991

+ 22 - 0
configs/config.js

@@ -69,3 +69,25 @@ module.exports = {
   npmCmdPath: path.join(nodeDir, isWin ? 'npm.cmd' : 'npm'),
   npmCliPath: path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js')
 }
+
+// 被直接执行时:供 bat / PowerShell 读取路径。 node config.js        → 输出 set "NODE_EXE=..." 等(bat); node config.js --json → 输出 JSON
+if (typeof require !== 'undefined' && require.main === module) {
+  const args = (process.argv || []).slice(2)
+  const wantJson = args.includes('--json')
+  const nodeExe = path.join(nodeDir, isWin ? 'node.exe' : 'node')
+  const adbPathVal = (module.exports.adbPath && module.exports.adbPath.path) ? module.exports.adbPath.path : ''
+  const safe = (s) => String(s || '').replace(/"/g, '')
+  if (wantJson) {
+    console.log(JSON.stringify({
+      arch,
+      nodeDir,
+      npmCmdPath: path.join(nodeDir, isWin ? 'npm.cmd' : 'npm'),
+      npmCliPath: path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
+      pythonDir,
+      pythonVenvPath
+    }))
+  } else {
+    console.log('set "NODE_EXE=' + safe(nodeExe) + '"')
+    console.log('set "ADB_PATH=' + safe(adbPathVal) + '"')
+  }
+}

+ 0 - 16
configs/get-node-paths.js

@@ -1,16 +0,0 @@
-#!/usr/bin/env node
-/**
- * 输出 Node.js/npm/Python 路径(供 PowerShell / bat 等脚本读取 config.js)
- * 用法: node configs/get-node-paths.js
- * 输出 JSON: { arch, nodeDir, npmCmdPath, npmCliPath, pythonDir, pythonVenvPath }
- */
-const path = require('path');
-const config = require(path.join(__dirname, 'config.js'));
-console.log(JSON.stringify({
-  arch: config.arch,
-  nodeDir: config.nodeDir,
-  npmCmdPath: config.npmCmdPath,
-  npmCliPath: config.npmCliPath,
-  pythonDir: config.pythonDir || config.pythonPath?.path,
-  pythonVenvPath: config.pythonVenvPath
-}));

+ 8 - 2
doc/ef-compiler新增结点说明.md

@@ -48,8 +48,12 @@ module.exports = { executeTestFun }
 约第 6~14 行,在 `FUN_REGISTRY_TYPES` 数组里加 `'test-fun'`(可加在 `'string-press',` 后)。
 
 **3. 解析**  
-脚本:`fun-parser.js`  
-约第 34~118 行,在 `parse()` 的 `switch (action.type)` 里、`default:` 之前加:
+脚本:`nodejs/ef-compiler/actions/fun-parser.js`  
+在 `parse()` 里找到 `switch (action.type)`(约第 34 行),在**最后一个 `case`(如 `case 'img-cropping':`)的 `break` 之后、`default:` 之前**插入新分支,用于解析该 fun 结点的输出变量到 `parsed.variable`(工作流里通过 `variable` 或 `outVars[0]` 指定)。  
+
+插入位置示例:第 113 行 `break` 与第 115 行 `default:` 之间。  
+
+插入内容:
 
 ```js
 case 'test-fun':
@@ -57,6 +61,8 @@ case 'test-fun':
   break
 ```
 
+若结点需要输入参数(如模板、路径),可仿照 `img-cropping` 等 case,给 `parsed` 增加 `parsed.inVars`、`parsed.template` 等字段。
+
 **4. 挂载**  
 脚本:`fun-parser.js`  
 约第 183~189 行,在 `get()` 的 `case 'io':` 的 mod 对象里加一行(例如在 `executeSaveTxt` 那行后面):  

+ 3 - 3
enviroment-check.ps1

@@ -18,11 +18,11 @@ foreach ($tryNode in @(
 )) {
     if (Test-Path $tryNode) { $nodeExeBootstrap = $tryNode; break }
 }
-$getPathsScript = Join-Path $scriptRoot 'configs\get-node-paths.js'
-if ($nodeExeBootstrap -and (Test-Path $getPathsScript)) {
+$configScript = Join-Path $scriptRoot 'configs\config.js'
+if ($nodeExeBootstrap -and (Test-Path $configScript)) {
     try {
         Push-Location $scriptRoot
-        $cfgJson = & $nodeExeBootstrap $getPathsScript 2>$null
+        $cfgJson = & $nodeExeBootstrap $configScript --json 2>$null
         if ($cfgJson) {
             $cfg = $cfgJson | ConvertFrom-Json
             $nodeDir = $cfg.nodeDir

+ 43 - 70
nodejs/ef-compiler/actions/fun-parser.js

@@ -2,6 +2,7 @@
  * fun 解析与执行 + 执行入口:registry/executeAction 由 ctx 传入(来自 workflow-json-parser),本模块负责 parse/runAction/run/supports
  */
 const path = require('path')
+const variableParser = require('../variable-parser.js')
 
 const FUN_REGISTRY_TYPES = [
   'fun',
@@ -10,7 +11,7 @@ const FUN_REGISTRY_TYPES = [
   'read-last-message', 'smart-chat-append',
   'extract-messages', 'ocr-chat', 'ocr-chat-history', 'extract-chat-history',
   'save-messages', 'generate-summary', 'generate-history-summary',
-  'ai-generate', 'string-press',
+  'ai-generate', 'string-press','download',
 ]
 
 const types = FUN_REGISTRY_TYPES
@@ -112,6 +113,9 @@ function parse(action, parseContext) {
       parsed.savePath = action.inVars?.[1] ?? action.savePath
       if (action.outVars && action.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
       break
+    case 'download':
+      parsed.variable = action.outVars?.[0] ? extractVarName(action.outVars[0]) : extractVarName(action.variable)
+      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)) : []
@@ -130,16 +134,17 @@ async function runAction(action, device, folderPath, resolution, ctx) {
     return { success: true, skipped: true }
   }
 
-  if (action.type === 'fun' && action.method) {
-    return run(action.method, action, ctx, device, folderPath)
+  const resolvedAction = variableParser.resolveActionInputs(action, variableContext)
+
+  if (resolvedAction.type === 'fun' && resolvedAction.method) {
+    return run(resolvedAction.method, resolvedAction, ctx, device, folderPath)
   }
 
-  // fun 类 type(如 img-center-point-location)必须走 run() 才会执行逻辑并写变量,不能走 registry 的 stub execute
-  if (supports(action.type)) {
-    return run(action.type, action, ctx, device, folderPath)
+  if (supports(resolvedAction.type)) {
+    return run(resolvedAction.type, resolvedAction, ctx, device, folderPath)
   }
 
-  if (registry && registry[action.type]) {
+  if (registry && registry[resolvedAction.type]) {
     const execCtx = {
       device,
       folderPath,
@@ -158,10 +163,10 @@ async function runAction(action, device, folderPath, resolution, ctx) {
       calculateWaitTime: ctx.calculateWaitTime,
       DEFAULT_SCROLL_DISTANCE: ctx.DEFAULT_SCROLL_DISTANCE,
     }
-    return await executeAction(action.type, action, execCtx)
+    return await executeAction(resolvedAction.type, resolvedAction, execCtx)
   }
 
-  return { success: false, error: `未知的操作类型: ${action.type}` }
+  return { success: false, error: `未知的操作类型: ${resolvedAction.type}` }
 }
 
 const cache = new Map()
@@ -233,15 +238,14 @@ async function run(actionType, action, ctx, device, folderPath) {
       let regionPath = action.region
       if (action.inVars && Array.isArray(action.inVars)) {
         if (action.inVars.length === 1) {
-          const firstVar = extractVarName(action.inVars[0])
-          const firstValue = variableContext[firstVar]
-          regionPath = firstValue && typeof firstValue === 'string' && !firstValue.includes('{') ? firstValue : action.inVars[0]
+          const firstValue = variableContext[extractVarName(action.inVars[0])] ?? resolveValue(action.inVars[0], variableContext)
+          regionPath = firstValue != null && typeof firstValue === 'string' && !String(firstValue).includes('{') ? firstValue : action.inVars[0]
           screenshotPath = null
         } else if (action.inVars.length >= 2) {
-          const sv = variableContext[extractVarName(action.inVars[0])]
-          screenshotPath = sv && typeof sv === 'string' && !sv.includes('{') ? sv : action.inVars[0]
-          const rv = variableContext[extractVarName(action.inVars[1])]
-          regionPath = rv && typeof rv === 'string' && !rv.includes('{') ? rv : action.inVars[1]
+          const sv = variableContext[extractVarName(action.inVars[0])] ?? resolveValue(action.inVars[0], variableContext)
+          screenshotPath = sv != null && typeof sv === 'string' && !sv.includes('{') ? sv : action.inVars[0]
+          const rv = variableContext[extractVarName(action.inVars[1])] ?? resolveValue(action.inVars[1], variableContext)
+          regionPath = rv != null && typeof rv === 'string' && !rv.includes('{') ? rv : action.inVars[1]
         }
       }
       if (screenshotPath !== null && !screenshotPath) screenshotPath = action.screenshot
@@ -262,9 +266,8 @@ async function run(actionType, action, ctx, device, folderPath) {
       const { executeImgCenterPointLocation } = get(funcDir, 'img')
       let templatePath = action.template
       if (action.inVars?.length > 0) {
-        const templateVar = extractVarName(action.inVars[0])
-        const templateValue = variableContext[templateVar]
-        templatePath = templateValue && typeof templateValue === 'string' && !templateValue.includes('{') ? templateValue : action.inVars[0]
+        const templateValue = variableContext[extractVarName(action.inVars[0])] ?? resolveValue(action.inVars[0], variableContext)
+        templatePath = templateValue != null && typeof templateValue === 'string' && !String(templateValue).includes('{') ? templateValue : action.inVars[0]
       }
       if (!templatePath) templatePath = action.template
       if (!templatePath) return { success: false, error: '缺少模板图片路径' }
@@ -285,14 +288,8 @@ async function run(actionType, action, ctx, device, folderPath) {
       let area = action.area
       let savePath = action.savePath
       if (action.inVars && Array.isArray(action.inVars)) {
-        if (action.inVars.length > 0) {
-          const areaValue = variableContext[extractVarName(action.inVars[0])]
-          area = areaValue !== undefined ? areaValue : resolveValue(action.inVars[0])
-        }
-        if (action.inVars.length > 1) {
-          const savePathValue = variableContext[extractVarName(action.inVars[1])]
-          savePath = savePathValue !== undefined ? savePathValue : resolveValue(action.inVars[1])
-        }
+        if (action.inVars.length > 0) area = variableContext[extractVarName(action.inVars[0])] ?? resolveValue(action.inVars[0], variableContext)
+        if (action.inVars.length > 1) savePath = variableContext[extractVarName(action.inVars[1])] ?? resolveValue(action.inVars[1], variableContext)
       }
       if (!area) return { success: false, error: 'img-cropping 缺少 area 参数' }
       if (!savePath) return { success: false, error: 'img-cropping 缺少 savePath 参数' }
@@ -334,12 +331,10 @@ async function run(actionType, action, ctx, device, folderPath) {
       const { executeReadTxt } = get(funcDir, 'io')
       let filePath = action.filePath
       let varName = action.variable
-      if (action.inVars?.length > 0) {
-        const filePathValue = variableContext[extractVarName(action.inVars[0])]
-        filePath = filePathValue !== undefined ? filePathValue : resolveValue(action.inVars[0])
-      }
+      if (action.inVars?.length > 0) filePath = variableContext[extractVarName(action.inVars[0])] ?? resolveValue(action.inVars[0], variableContext)
       if (action.outVars?.length > 0) varName = extractVarName(action.outVars[0])
       else if (action.variable) varName = extractVarName(action.variable)
+      else if (action.variable) varName = extractVarName(action.variable)
       if (!filePath) return { success: false, error: 'read-txt 缺少 filePath 参数' }
       if (!varName) return { success: false, error: 'read-txt 缺少 variable 参数' }
       const result = await executeReadTxt({ filePath, folderPath })
@@ -356,14 +351,8 @@ async function run(actionType, action, ctx, device, folderPath) {
       let history = action.history
       let current = action.current
       if (action.inVars && Array.isArray(action.inVars)) {
-        if (action.inVars.length > 0) {
-          const historyValue = variableContext[extractVarName(action.inVars[0])]
-          history = historyValue !== undefined ? historyValue : resolveValue(action.inVars[0])
-        }
-        if (action.inVars.length > 1) {
-          const currentValue = variableContext[extractVarName(action.inVars[1])]
-          current = currentValue !== undefined ? currentValue : resolveValue(action.inVars[1])
-        }
+        if (action.inVars.length > 0) history = variableContext[extractVarName(action.inVars[0])] ?? resolveValue(action.inVars[0], variableContext)
+        if (action.inVars.length > 1) current = variableContext[extractVarName(action.inVars[1])] ?? resolveValue(action.inVars[1], variableContext)
       }
       if (history === undefined || history === null) history = ''
       if (current === undefined || current === null) current = ''
@@ -383,14 +372,8 @@ async function run(actionType, action, ctx, device, folderPath) {
       let filePath = action.filePath
       let content = action.content
       if (action.inVars && Array.isArray(action.inVars)) {
-        if (action.inVars.length > 0) {
-          const contentValue = variableContext[extractVarName(action.inVars[0])]
-          content = contentValue !== undefined ? contentValue : resolveValue(action.inVars[0])
-        }
-        if (action.inVars.length > 1) {
-          const filePathValue = variableContext[extractVarName(action.inVars[1])]
-          filePath = filePathValue !== undefined ? filePathValue : resolveValue(action.inVars[1])
-        }
+        if (action.inVars.length > 0) content = variableContext[extractVarName(action.inVars[0])] ?? resolveValue(action.inVars[0], variableContext)
+        if (action.inVars.length > 1) filePath = variableContext[extractVarName(action.inVars[1])] ?? resolveValue(action.inVars[1], variableContext)
       }
       if (!filePath) return { success: false, error: 'save-txt 缺少 filePath 参数' }
       if (content === undefined || content === null) return { success: false, error: 'save-txt 缺少 content 参数' }
@@ -417,37 +400,27 @@ async function run(actionType, action, ctx, device, folderPath) {
 
       if (action.inVars && Array.isArray(action.inVars)) {
         if (action.inVars.length >= 3) {
-          const param1 = resolveValue(action.inVars[0])
-          const param2 = resolveValue(action.inVars[1])
+          const param1 = resolveValue(action.inVars[0], variableContext)
+          const param2 = resolveValue(action.inVars[1], variableContext)
           if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
             friendRgb = param1.trim()
             myRgb = param2.trim()
-            const regionVar = extractVarName(action.inVars[2])
-            regionArea = variableContext[regionVar]
-            if (regionArea === undefined) {
-              const regionResolved = resolveValue(action.inVars[2])
-              if (regionResolved && typeof regionResolved === 'object') regionArea = regionResolved
-            }
+            regionArea = variableContext[extractVarName(action.inVars[2])] ?? resolveValue(action.inVars[2], variableContext)
           } else {
             avatar1Name = action.inVars[0]
             avatar2Name = action.inVars[1]
-            const regionVar = extractVarName(action.inVars[2])
-            regionArea = variableContext[regionVar]
-            if (regionArea === undefined) {
-              const regionResolved = resolveValue(action.inVars[2])
-              if (regionResolved && typeof regionResolved === 'object') regionArea = regionResolved
-            }
+            regionArea = variableContext[extractVarName(action.inVars[2])] ?? resolveValue(action.inVars[2], variableContext)
           }
           regionArea = parseRegion(regionArea)
         } else if (action.inVars.length >= 2) {
-          const param1 = resolveValue(action.inVars[0])
-          const param2 = resolveValue(action.inVars[1])
+          const param1 = resolveValue(action.inVars[0], variableContext)
+          const param2 = resolveValue(action.inVars[1], variableContext)
           if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
             friendRgb = param1.trim()
             myRgb = param2.trim()
           } else {
-            avatar1Name = action.inVars[0]
-            avatar2Name = action.inVars[1]
+            avatar1Name = param1
+            avatar2Name = param2
           }
         } else if (action.inVars.length === 1) {
           avatar1Name = action.inVars[0]
@@ -459,11 +432,11 @@ async function run(actionType, action, ctx, device, folderPath) {
       }
 
       if (avatar1Name) {
-        const resolved = resolveValue(avatar1Name)
+        const resolved = resolveValue(avatar1Name, variableContext)
         if (resolved) avatar1Path = `${folderName}/resources/${resolved}`
       }
       if (avatar2Name) {
-        const resolved = resolveValue(avatar2Name)
+        const resolved = resolveValue(avatar2Name, variableContext)
         if (resolved) avatar2Path = `${folderName}/resources/${resolved}`
       }
 
@@ -513,11 +486,11 @@ async function run(actionType, action, ctx, device, folderPath) {
                 if (Array.isArray(parsed) && parsed.length === 0) replaceValue = ''
               } catch (e) {}
             }
-            prompt = prompt.replace(new RegExp(`{${varName}}`.replace(/[{}]/g, '\\$&'), 'g'), replaceValue)
+            prompt = (prompt || '').replace(new RegExp(`\\{${varName.replace(/[{}]/g, '\\$&')}\\}`, 'g'), replaceValue)
           }
         }
       }
-      if (prompt.includes('{historySummary}') && getHistorySummary) {
+      if (prompt && prompt.includes('{historySummary}') && getHistorySummary) {
         let historySummary = variableContext['historySummary'] || ''
         if (!historySummary) {
           historySummary = await getHistorySummary(folderPath)
@@ -603,7 +576,7 @@ async function run(actionType, action, ctx, device, folderPath) {
     case 'string-press': {
       const api = ctx.electronAPI
       const inVars = action.inVars || []
-      let targetText = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || action.value) : (resolveValue(action.value, variableContext) || action.value)
+      const targetText = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] ?? resolveValue(inVars[0], variableContext) ?? action.value) : (action.value ?? '')
       if (!targetText) return { success: false, error: 'string-press 操作缺少文字内容' }
       if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
       const matchResult = await api.findTextAndGetCoordinate(device, targetText)

+ 4 - 0
nodejs/ef-compiler/actions/fun/download.js

@@ -0,0 +1,4 @@
+async function executeDownload() {
+    return { success: true, value: 'ok' }
+  }
+  module.exports = { executeDownload }

+ 67 - 0
nodejs/ef-compiler/variable-parser.js

@@ -0,0 +1,67 @@
+/**
+ * 统一解析结点入参:将 action 中的变量引用({var}、{{var}})用 variableContext 解析为实际值,
+ * 再传给各 action 的 parse/execute,避免各结点脚本重复写解析逻辑。
+ */
+const setParser = require('./actions/set-parser.js')
+const resolveValue = setParser.resolveValue
+const replaceVariablesInString = setParser.replaceVariablesInString
+
+/** 视为入参的字段(会被解析);outVars/variable 为输出,不在此解析。inVars 由各结点按需解析(因部分结点将 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',
+]
+
+/**
+ * 解析单值:先对字符串做 {{var}} 替换,再对整体做 {var} 引用解析
+ */
+function resolveInputValue(val, variableContext) {
+  if (variableContext == null) return val
+  if (typeof val === 'string') {
+    const replaced = replaceVariablesInString(val, variableContext)
+    return resolveValue(replaced, variableContext)
+  }
+  if (Array.isArray(val)) return val.map(item => resolveInputValue(item, variableContext))
+  if (typeof val === 'object' && val !== null) {
+    const out = {}
+    for (const k in val) out[k] = resolveInputValue(val[k], variableContext)
+    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.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
+}
+
+module.exports = {
+  resolveActionInputs,
+  resolveInputValue,
+}