Browse Source

修复for循环以及参数解析

yichael 2 months ago
parent
commit
7a485bcfa1

+ 9 - 1
nodejs/ef-compiler/actions/adb/click.js

@@ -1,10 +1,18 @@
 /**
  * adb method: click — 按坐标或变量位置点击
+ * 入参由 variable-parser 在调用前解析,inVars[0] 可能已是解析后的坐标值(JSON 字符串、"x,y" 或对象),也可能仍是变量名需从 variableContext 读取。
  */
 async function run(action, ctx) {
   const { device, variableContext, api, extractVarName, resolveValue } = ctx
   const inVars = action.inVars || []
-  let position = inVars.length > 0 ? variableContext[extractVarName(inVars[0])] : null
+  let position = null
+  if (inVars.length > 0) {
+    const raw = inVars[0]
+    // 已是解析后的坐标值(variable-parser 已替换):对象、JSON 串或 "x,y"
+    if (typeof raw === 'object' && raw !== null && (raw.x !== undefined || (Array.isArray(raw) && raw.length >= 2))) position = raw
+    else if (typeof raw === 'string' && raw !== '' && (raw.trim().startsWith('{') || /^\s*-?\d+\s*,\s*-?\d+\s*$/.test(raw.trim()))) position = raw
+    else position = variableContext[extractVarName(raw)]
+  }
   if (!position && action.target) position = resolveValue(action.target, variableContext)
   if (!position) return { success: false, error: 'click 操作缺少位置参数' }
   if (typeof position === 'string') {

+ 1 - 0
nodejs/ef-compiler/actions/for-parser.js

@@ -7,6 +7,7 @@ function parse(action, parseContext) {
   const parsed = {
     type: 'for',
     variable: action.variable,
+    indexVariable: action.indexVariable,
     times: action.times,
     items: action.items != null ? resolveValue(action.items, variableContext) : null,
     body: action.body ? parseActions(action.body) : [],

+ 13 - 13
nodejs/ef-compiler/actions/fun/fun-parser.js

@@ -299,7 +299,7 @@ async function run(actionType, action, ctx, device, folderPath) {
       if (screenshotPath === null && !device) return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' }
       const result = await executeImgBoundingBoxLocation({ device, screenshot: screenshotPath, region: regionPath, folderPath })
       if (!result.success) return { success: false, error: `图像区域定位失败: ${result.error}` }
-      const outputVarName = action.outVars?.[0] != null ? String(action.outVars[0]).trim() : (action.variable ? extractVarName(action.variable) : null)
+      const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
       if (outputVarName) {
         variableContext[outputVarName] = result.corners && typeof result.corners === 'object' ? JSON.stringify(result.corners) : ''
         await logOutVars(action, variableContext, folderPath)
@@ -319,7 +319,7 @@ async function run(actionType, action, ctx, device, folderPath) {
       if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' }
       const result = await executeImgCenterPointLocation({ device, template: templatePath, folderPath })
       if (!result.success) return { success: false, error: `图像中心点定位失败: ${result.error}` }
-      const outputVarName = action.outVars?.[0] != null ? String(action.outVars[0]).trim() : (action.variable ? extractVarName(action.variable) : null)
+      const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
       if (outputVarName) {
         variableContext[outputVarName] = result.center && typeof result.center === 'object' && result.center.x !== undefined && result.center.y !== undefined
           ? JSON.stringify({ x: result.center.x, y: result.center.y }) : ''
@@ -341,7 +341,7 @@ async function run(actionType, action, ctx, device, folderPath) {
       const result = await executeImgCropping({ area, savePath, folderPath, device })
       if (!result.success) return { success: false, error: result.error }
       if (action.outVars?.[0] != null) {
-        const outputVarName = String(action.outVars[0]).trim()
+        const outputVarName = extractVarName(String(action.outVars[0]).trim())
         if (outputVarName) variableContext[outputVarName] = result.success ? '1' : '0'
       }
       await logOutVars(action, variableContext, folderPath)
@@ -352,8 +352,8 @@ async function run(actionType, action, ctx, device, folderPath) {
       const { executeReadLastMessage } = get(funcDir, 'io')
       const inputVars = action.inVars || action.inputVars || []
       const outputVars = action.outVars || action.outputVars || []
-      let textVar = outputVars.length > 0 ? String(outputVars[0]).trim() : action.textVariable
-      let senderVar = outputVars.length > 1 ? String(outputVars[1]).trim() : action.senderVariable
+      let textVar = outputVars.length > 0 ? extractVarName(String(outputVars[0]).trim()) : (action.textVariable ? extractVarName(action.textVariable) : null)
+      let senderVar = outputVars.length > 1 ? extractVarName(String(outputVars[1]).trim()) : (action.senderVariable ? extractVarName(action.senderVariable) : null)
       let inputDataString = inputVars.length > 0 ? (inputVars[0] != null ? (typeof inputVars[0] === 'string' ? inputVars[0] : (Array.isArray(inputVars[0]) || typeof inputVars[0] === 'object' ? JSON.stringify(inputVars[0]) : String(inputVars[0]))) : null) : null
       if (!textVar && !senderVar) return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' }
       const result = await executeReadLastMessage({ folderPath, inputData: inputDataString, textVariable: textVar, senderVariable: senderVar })
@@ -370,7 +370,7 @@ async function run(actionType, action, ctx, device, folderPath) {
       let filePath = action.filePath
       let varName = action.variable
       if (action.inVars?.length > 0) filePath = action.inVars[0]
-      if (action.outVars?.length > 0) varName = String(action.outVars[0]).trim()
+      if (action.outVars?.length > 0) varName = extractVarName(String(action.outVars[0]).trim())
       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 参数' }
@@ -398,7 +398,7 @@ async function run(actionType, action, ctx, device, folderPath) {
         current: typeof current === 'string' ? current : String(current),
       })
       if (!result.success) return { success: false, error: result.error }
-      const outputVarName = action.outVars?.[0] != null ? String(action.outVars[0]).trim() : (action.variable ? extractVarName(action.variable) : null)
+      const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
       if (outputVarName && result.result) variableContext[outputVarName] = result.result
       return { success: true, result: result.result }
     }
@@ -417,7 +417,7 @@ async function run(actionType, action, ctx, device, folderPath) {
       const result = await executeSaveTxt({ filePath, content, folderPath })
       if (!result.success) return { success: false, error: result.error }
       if (action.outVars?.[0] != null) {
-        const outputVarName = String(action.outVars[0]).trim()
+        const outputVarName = extractVarName(String(action.outVars[0]).trim())
         if (outputVarName) variableContext[outputVarName] = result.success ? '1' : '0'
       }
       await logOutVars(action, variableContext, folderPath)
@@ -481,7 +481,7 @@ async function run(actionType, action, ctx, device, folderPath) {
       const chatResult = await executeOcrChat({ device, avatar1: avatar1Path, avatar2: avatar2Path, folderPath, region: regionParam, friendRgb, myRgb })
       if (!chatResult.success) return { success: false, error: `提取消息记录失败: ${chatResult.error}` }
 
-      const outputVarName = action.outVars?.[0] != null ? String(action.outVars[0]).trim() : (action.variable ? extractVarName(action.variable) : null)
+      const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
       if (outputVarName) {
         variableContext[outputVarName] = chatResult.messagesJson || JSON.stringify(chatResult.messages || [])
         await logOutVars(action, variableContext, folderPath)
@@ -570,11 +570,11 @@ async function run(actionType, action, ctx, device, folderPath) {
 
         if (action.outVars?.length > 0) {
           if (action.outVars.length > 0) {
-            const outputVarName = action.outVars?.[0] != null ? String(action.outVars[0]).trim() : null
+            const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null
             if (outputVarName) variableContext[outputVarName] = result
           }
           if (action.outVars.length > 1) {
-            const callbackVarName = action.outVars?.[1] != null ? String(action.outVars[1]).trim() : null
+            const callbackVarName = action.outVars?.[1] != null ? extractVarName(String(action.outVars[1]).trim()) : null
             if (callbackVarName) variableContext[callbackVarName] = 1
           }
           await logOutVars(action, variableContext, folderPath)
@@ -584,7 +584,7 @@ async function run(actionType, action, ctx, device, folderPath) {
         }
         if (!action.outVars || action.outVars.length <= 1) {
           if (action.inVars?.length > 1 && action.inVars[1] != null) {
-            const callbackVarName = String(action.inVars[1]).trim()
+            const callbackVarName = extractVarName(String(action.inVars[1]).trim())
             if (callbackVarName) variableContext[callbackVarName] = 1
           }
         }
@@ -645,7 +645,7 @@ async function run(actionType, action, ctx, device, folderPath) {
         if (!result || !result.success) {
           return { success: false, error: (result && result.error) || 'execute failed' }
         }
-        const outputVarName = action.outVars?.[0] != null ? String(action.outVars[0]).trim() : null
+        const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null
         if (outputVarName && result != null) {
           const outVal = result.path ?? result.value ?? result.result
           if (outVal !== undefined && outVal !== null) variableContext[outputVarName] = typeof outVal === 'string' ? outVal : String(outVal)

+ 4 - 1
nodejs/ef-compiler/actions/fun/img-center-point-location.js

@@ -71,7 +71,10 @@ async function executeImgCenterPointLocation({ device, template, folderPath }) {
   if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' }
   if (!template || typeof template !== 'string') return { success: false, error: '缺少模板图片路径' }
   const baseDir = folderPath && typeof folderPath === 'string' ? folderPath : projectRoot
-  const templatePath = template.startsWith('/') || template.includes(':') ? template : path.join(baseDir, 'resources', template)
+  // 绝对路径或带盘符的保持原样;已含子路径(如 tmp/pic0.png)相对 baseDir;否则视为 resources 下文件名
+  const isAbsoluteOrDrive = template.startsWith('/') || template.includes(':')
+  const hasSubPath = template.includes('/') || template.includes(path.sep)
+  const templatePath = isAbsoluteOrDrive ? template : (hasSubPath ? path.join(baseDir, template) : path.join(baseDir, 'resources', template))
   const result = matchImageAndGetCoordinate(device, templatePath)
   if (!result.success) return { success: false, error: result.error }
   const center = result.clickPosition || { x: result.coordinate.x + result.coordinate.width / 2, y: result.coordinate.y + result.coordinate.height / 2 }

+ 5 - 2
nodejs/ef-compiler/sequence-runner.js

@@ -101,9 +101,12 @@ async function executeActionSequence(
         }
       } else {
         const items = Array.isArray(action.items) ? action.items : []
-        for (const item of items) {
+        const indexKey = action.indexVariable != null ? String(action.indexVariable).replace(/^\{|\}$/g, '').trim() : null
+        const variableKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null
+        for (let i = 0; i < items.length; i++) {
           if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
-          if (action.variable) variableContext[action.variable.replace(/^\{|\}$/g, '').trim()] = item
+          if (indexKey !== null) variableContext[indexKey] = i
+          if (variableKey !== null) variableContext[variableKey] = items[i]
           if (action.body && action.body.length > 0) {
             const result = await executeActionSequence(action.body, device, folderPath, resolution, interval, onStepComplete, shouldStop, depth + 1, ctx)
             if (!result.success) return result

+ 48 - 4
nodejs/ef-compiler/variable-parser.js

@@ -1,13 +1,14 @@
 /**
  * 统一解析结点入参、出参:将 action 中的变量引用用 variableContext 解析为实际值。
- * 规则:{var}、{{var}} 为变量(替换为变量值),"hello" 为字符串字面量,"hello{var}" 为字符串+变量拼接。
+ * 规则:{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 由各结点按需解析(因部分结点将 inVars 某项作为输出变量名)。 */
+/** 视为入参的字段(会被解析);inVars 由本脚本统一解析后传给各结点。 */
 const INPUT_KEYS = [
   'value', 'target', 'template', 'area', 'savePath', 'condition', 'delay', 'interval',
   'items', 'screenshot', 'region', 'method', 'clear', 'timeout', 'retry',
@@ -18,12 +19,53 @@ const INPUT_KEYS = [
 ]
 
 /**
- * 解析单值:先对字符串做 {{var}} 替换,再对整体做 {var} 引用解析
+ * 将数组下标取值结果转为可嵌入字符串的形式
+ */
+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} 替换,最后对整体做引用解析。
  */
 function resolveInputValue(val, variableContext) {
   if (variableContext == null) return val
   if (typeof val === 'string') {
-    const replaced = replaceVariablesInString(val, variableContext)
+    const afterIndex = replaceArrayIndexInString(val, variableContext)
+    const replaced = replaceVariablesInString(afterIndex, variableContext)
     let result = resolveValue(replaced, variableContext)
     if (result === val && /^[\w-]+$/.test(val) && variableContext[val] !== undefined) result = variableContext[val]
     return result
@@ -74,4 +116,6 @@ function resolveActionInputs(action, variableContext) {
 module.exports = {
   resolveActionInputs,
   resolveInputValue,
+  replaceArrayIndexInString,
+  extractVarName,
 }

+ 2 - 0
nodejs/ef-compiler/workflow-json-parser.js

@@ -150,6 +150,7 @@ function parseWorkflow(workflow, state) {
         else if (typeof value === 'boolean') variableContext[key] = value ? '1' : '0'
         else if (typeof value === 'number') variableContext[key] = value
         else if (typeof value === 'string') variableContext[key] = value
+        else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) variableContext[key] = value
         else variableContext[key] = String(value)
       }
     }
@@ -163,6 +164,7 @@ function parseWorkflow(workflow, state) {
         else if (typeof value === 'boolean') variableContext[key] = value ? '1' : '0'
         else if (typeof value === 'number') variableContext[key] = value
         else if (typeof value === 'string') variableContext[key] = value
+        else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) variableContext[key] = value
         else variableContext[key] = String(value)
       }
     }

+ 200 - 0
static/process/GenerateNote/bp.json

@@ -0,0 +1,200 @@
+{
+	"nodePositions": {
+		"node_begin_1768681618551": {
+			"x": 150,
+			"y": 100
+		},
+		"node_0": {
+			"x": 150,
+			"y": 300
+		},
+		"node_1": {
+			"x": 470,
+			"y": 100
+		},
+		"node_2": {
+			"x": 470,
+			"y": 300
+		},
+		"node_3": {
+			"x": 470,
+			"y": 500
+		},
+		"node_4": {
+			"x": 470,
+			"y": 700
+		},
+		"node_5": {
+			"x": 470,
+			"y": 900
+		},
+		"node_6": {
+			"x": 470,
+			"y": 1100
+		},
+		"node_7": {
+			"x": 470,
+			"y": 1300
+		},
+		"node_8": {
+			"x": 470,
+			"y": 1500
+		},
+		"node_9": {
+			"x": 470,
+			"y": 1700
+		},
+		"node_10": {
+			"x": 150,
+			"y": 1900
+		},
+		"node_11": {
+			"x": 790,
+			"y": 100
+		},
+		"node_12": {
+			"x": 790,
+			"y": 300
+		},
+		"node_13": {
+			"x": 150,
+			"y": 2100
+		},
+		"node_14": {
+			"x": 150,
+			"y": 2300
+		},
+		"node_15": {
+			"x": 150,
+			"y": 2500
+		},
+		"node_16": {
+			"x": 150,
+			"y": 2700
+		},
+		"node_17": {
+			"x": 150,
+			"y": 2900
+		},
+		"node_18": {
+			"x": 150,
+			"y": 3100
+		},
+		"node_19": {
+			"x": 150,
+			"y": 3300
+		},
+		"node_20": {
+			"x": 150,
+			"y": 500
+		},
+		"node_21": {
+			"x": 150,
+			"y": 700
+		},
+		"node_22": {
+			"x": 150,
+			"y": 900
+		},
+		"node_23": {
+			"x": 1110,
+			"y": 100
+		},
+		"node_24": {
+			"x": 1110,
+			"y": 300
+		},
+		"node_25": {
+			"x": 1430,
+			"y": 100
+		},
+		"node_26": {
+			"x": 1430,
+			"y": 300
+		},
+		"node_27": {
+			"x": 790,
+			"y": 500
+		},
+		"node_28": {
+			"x": 790,
+			"y": 700
+		},
+		"node_29": {
+			"x": 790,
+			"y": 900
+		},
+		"node_30": {
+			"x": 150,
+			"y": 1100
+		},
+		"node_31": {
+			"x": 150,
+			"y": 1300
+		},
+		"node_32": {
+			"x": 150,
+			"y": 1500
+		},
+		"node_33": {
+			"x": 150,
+			"y": 1700
+		},
+		"var_turn_1768681618551": {
+			"x": 50,
+			"y": 200
+		},
+		"var_relationBg_1768681618551": {
+			"x": 50,
+			"y": 320
+		},
+		"var_chatArea_1768681618551": {
+			"x": 50,
+			"y": 440
+		},
+		"var_chatHistoryMessage_1768681618551": {
+			"x": 50,
+			"y": 560
+		},
+		"var_currentChatMessage_1768681618551": {
+			"x": 50,
+			"y": 680
+		},
+		"var_lastHistoryMessage_1768681618551": {
+			"x": 50,
+			"y": 800
+		},
+		"var_lastChatMessage_1768681618551": {
+			"x": 50,
+			"y": 920
+		},
+		"var_lastChatRole_1768681618551": {
+			"x": 50,
+			"y": 1040
+		},
+		"var_lastHistoryChatMessage_1768681618551": {
+			"x": 50,
+			"y": 1160
+		},
+		"var_lastHistoryChatRole_1768681618551": {
+			"x": 50,
+			"y": 1280
+		},
+		"var_aiReply_1768681618551": {
+			"x": 50,
+			"y": 1400
+		},
+		"var_aiCallBack_1768681618551": {
+			"x": 50,
+			"y": 1520
+		},
+		"var_sendBtnPos_1768681618551": {
+			"x": 50,
+			"y": 1640
+		},
+		"var_newChatMessage_1768681618551": {
+			"x": 50,
+			"y": 1760
+		}
+	}
+}

+ 181 - 0
static/process/GenerateNote/process.json

@@ -0,0 +1,181 @@
+{
+  "name": "GenerateNote",
+  "description": "生成小红书图文笔记",
+  "variables": {
+    "send-btn-pos": "",
+    "prompt": "健康减脂:科学饮食与运动习惯,适合做小红书笔记",
+    "img-prompt-arr": [
+      "健康减脂餐 轻食沙拉 低卡高蛋白 摆盘",
+      "居家有氧运动 女生健身 燃脂操"
+    ],
+    "download-path": "tmp",
+    "img-url-arr": [],
+    "idx": 0,
+    "pos": "",
+    "thumbRect": ""
+  },
+  "execute": [
+    {
+      "type": "echo",
+      "inVars": ["开始生成小红书图文笔记"]
+    },
+    {
+      "type": "ai",
+      "method": "text2text",
+      "inVars": ["根据以下主题写一篇小红书风格的图文稿件,要求:长文,至少 500 字,分段清晰、吸引人、适当使用 emoji、适合发笔记。只输出稿件正文,不要标题。主题:{{prompt}}", ""],
+      "outVars": ["{article}"]
+    },
+    {
+      "type": "save-txt",
+      "inVars": ["{article}", "tmp/article.txt"],
+      "outVars": []
+    },
+    {
+      "type": "echo",
+      "inVars": ["开始生图"]
+    },
+    {
+      "type": "for",
+      "indexVariable": "{idx}",
+      "items": "{img-prompt-arr}",
+      "body": [
+        {
+          "type": "echo",
+          "inVars": ["请根据描述词找一张确认可下载的图片 URL。描述词:{img-prompt-arr}[{idx}]"]
+        },
+        {
+          "type": "try",
+          "try": [
+            {
+              "type": "ai",
+              "method": "text2text",
+              "inVars": [
+                "请根据以下描述词找一张图片,只返回一张确认过可以正常下载的图片 URL(必须是当前可访问、能直接 GET 下载的地址),不要任何说明、markdown 或换行。描述词:{img-prompt-arr}[{idx}]",
+                "doubao"
+              ],
+              "outVars": ["{img-url-arr}"]
+            },
+            {
+              "type": "download",
+              "inVars": ["{img-url-arr}", "tmp/pic{idx}.png"],
+              "outVars": []
+            }
+          ],
+          "fail": [
+            {
+              "type": "try",
+              "try": [
+                {
+                  "type": "ai",
+                  "method": "text2text",
+                  "inVars": [
+                    "请根据描述词找一张图片,只返回一个当前可访问、能直接 GET 下载的图片 URL(不要说明、markdown 或换行)。描述词:{img-prompt-arr}[{idx}]",
+                    "doubao"
+                  ],
+                  "outVars": ["{img-url-arr}"]
+                },
+                {
+                  "type": "download",
+                  "inVars": ["{img-url-arr}", "tmp/pic{idx}.png"],
+                  "outVars": []
+                }
+              ],
+              "fail": [
+                {
+                  "type": "ai",
+                  "method": "text2text",
+                  "inVars": [
+                    "请根据描述词找一张图片,只返回一个可访问的图片 URL(仅 URL,无说明)。描述词:{img-prompt-arr}[{idx}]",
+                    "doubao"
+                  ],
+                  "outVars": ["{img-url-arr}"]
+                },
+                {
+                  "type": "download",
+                  "inVars": ["{img-url-arr}", "tmp/pic{idx}.png"],
+                  "outVars": []
+                }
+              ]
+            }
+          ]
+        },
+        {
+          "type": "echo",
+          "inVars": ["豆包返回链接原文:", "{img-url-arr}"]
+        },
+        {
+          "type": "echo",
+          "inVars": ["第{{idx}}张图片下载结束"]
+        },
+        {
+          "type": "adb",
+          "method": "send-img-to-device",
+          "inVars": ["tmp/pic{idx}.png"],
+          "outVars": []
+        }
+      ]
+    },
+    {
+      "type": "img-center-point-location",
+      "inVars": ["添加笔记.png"],
+      "outVars": ["{send-btn-pos}"]
+    },
+    {
+      "type": "adb",
+      "method": "click",
+      "inVars": ["{send-btn-pos}"]
+    },
+    {
+      "type": "img-center-point-location",
+      "inVars": ["从相册选择.png"],
+      "outVars": ["{send-btn-pos}"]
+    },
+    {
+      "type": "adb",
+      "method": "click",
+      "inVars": ["{send-btn-pos}"]
+    },
+    {
+      "type": "for",
+      "indexVariable": "{idx}",
+      "items": "{img-prompt-arr}",
+      "body": [
+        {
+          "type": "echo",
+          "inVars": ["tmp/pic{idx}.png"]
+        },
+        {
+          "type": "img-center-point-location",
+          "method": "template",
+          "inVars": ["tmp/pic{idx}.png"],
+          "outVars": ["{pos}"]
+        },
+        {
+          "type": "adb",
+          "method": "click",
+          "inVars": ["{pos}"]
+        },
+        {
+          "type": "img-center-point-location",
+          "inVars": ["选中图片.png"],
+          "outVars": ["{pos}"]
+        },
+        {
+          "type": "adb",
+          "method": "click",
+          "inVars": ["{pos}"]
+        },
+        {
+					"type": "adb",
+					"method": "keyevent",
+					"inVars": ["4"],
+					"outVars": []
+				}
+      ]
+    },
+    {
+      "type": "echo",
+      "inVars": ["流程结束"]
+    }
+  ]
+}

+ 1 - 0
static/process/GenerateNote/readme.md

@@ -0,0 +1 @@
+生成小红书图文笔记

BIN
static/process/GenerateNote/resources/从相册选择.png


BIN
static/process/GenerateNote/resources/添加笔记.png


BIN
static/process/GenerateNote/resources/选中图片.png


BIN
static/process/GenerateNote/resources/选择图片.png


BIN
static/process/GenerateNote/tmp/pic0.png


BIN
static/process/GenerateNote/tmp/pic1.png