فهرست منبع

极速版小红书发文

yichael 1 ماه پیش
والد
کامیت
6783a3c745
75فایلهای تغییر یافته به همراه2478 افزوده شده و 1518 حذف شده
  1. 5 4
      doc/工作流语法.md
  2. 31 0
      nodejs/ef-compiler/action-schema.js
  3. 10 5
      nodejs/ef-compiler/actions/delay-parser.js
  4. 18 23
      nodejs/ef-compiler/actions/echo-parser.js
  5. 24 7
      nodejs/ef-compiler/actions/for-parser.js
  6. 88 26
      nodejs/ef-compiler/actions/fun/adb/adb-parser.js
  7. 19 1
      nodejs/ef-compiler/actions/fun/fun-node-registry.js
  8. 326 121
      nodejs/ef-compiler/actions/fun/fun-parser.js
  9. 31 5
      nodejs/ef-compiler/actions/fun/json/json-to-arr.js
  10. 72 0
      nodejs/ef-compiler/actions/fun/persist-read.js
  11. 82 0
      nodejs/ef-compiler/actions/fun/persist-save.js
  12. 14 7
      nodejs/ef-compiler/actions/if-parser.js
  13. 20 13
      nodejs/ef-compiler/actions/random-parser.js
  14. 17 9
      nodejs/ef-compiler/actions/schedule-parser.js
  15. 8 3
      nodejs/ef-compiler/actions/set-parser.js
  16. 20 9
      nodejs/ef-compiler/actions/try-parser.js
  17. 10 6
      nodejs/ef-compiler/actions/while-parser.js
  18. 107 10
      nodejs/ef-compiler/expression-evaluator.js
  19. 3 3
      nodejs/ef-compiler/sequence-runner.js
  20. 15 5
      nodejs/ef-compiler/variable-parser.js
  21. 38 11
      nodejs/ef-compiler/workflow-json-parser.js
  22. 230 101
      package/pack-resources/static/process/GenerateNote/process.json
  23. 36 22
      package/pack-resources/static/process/RedNoteAIThumbsUp/process.json
  24. 196 150
      package/pack-resources/static/process/RedNoteBrowsingAndThumbsUp/process.json
  25. 21 24
      package/pack-resources/static/process/Test/process.json
  26. 4 1
      static/process/CreateNote/process.json
  27. 371 144
      static/process/GenerateNote/process.json
  28. 0 1
      static/process/GenerateNote/readme.md
  29. BIN
      static/process/GenerateNote/resources/添加笔记_crop.png
  30. BIN
      static/process/GenerateNote/resources/添加笔记_imgcenter_crop.png
  31. BIN
      static/process/GenerateNote/resources/添加笔记_imgcenter_scale.png
  32. BIN
      static/process/GenerateNote/resources/添加笔记_scale.png
  33. 10 0
      static/process/GenerateNote/save/config.json
  34. 0 32
      static/process/GenerateNote/tmp/img-center-1774173318186/openai_raw.json
  35. 0 38
      static/process/GenerateNote/tmp/img-center-1774173318186/openai_raw_attempt_0.json
  36. BIN
      static/process/GenerateNote/tmp/img-center-1774173318186/screenshot.png
  37. BIN
      static/process/GenerateNote/tmp/img-center-1774173318186/template.png
  38. 0 17
      static/process/GenerateNote/tmp/img-center-1774173318186/vlm_center_model_attempts.json
  39. 0 4
      static/process/GenerateNote/tmp/img-center-1774173318186/vlm_center_parsed.json
  40. 0 10
      static/process/GenerateNote/tmp/img-center-1774173318186/vlm_center_result.json
  41. 0 32
      static/process/GenerateNote/tmp/img-center-1774173370331/openai_raw.json
  42. 0 38
      static/process/GenerateNote/tmp/img-center-1774173370331/openai_raw_attempt_0.json
  43. BIN
      static/process/GenerateNote/tmp/img-center-1774173370331/screenshot.png
  44. BIN
      static/process/GenerateNote/tmp/img-center-1774173370331/template.png
  45. 0 17
      static/process/GenerateNote/tmp/img-center-1774173370331/vlm_center_model_attempts.json
  46. 0 4
      static/process/GenerateNote/tmp/img-center-1774173370331/vlm_center_parsed.json
  47. 0 10
      static/process/GenerateNote/tmp/img-center-1774173370331/vlm_center_result.json
  48. 0 32
      static/process/GenerateNote/tmp/img-center-1774173408104/openai_raw.json
  49. 0 38
      static/process/GenerateNote/tmp/img-center-1774173408104/openai_raw_attempt_0.json
  50. BIN
      static/process/GenerateNote/tmp/img-center-1774173408104/screenshot.png
  51. BIN
      static/process/GenerateNote/tmp/img-center-1774173408104/template.png
  52. 0 17
      static/process/GenerateNote/tmp/img-center-1774173408104/vlm_center_model_attempts.json
  53. 0 4
      static/process/GenerateNote/tmp/img-center-1774173408104/vlm_center_parsed.json
  54. 0 10
      static/process/GenerateNote/tmp/img-center-1774173408104/vlm_center_result.json
  55. 0 32
      static/process/GenerateNote/tmp/img-center-1774173430585/openai_raw.json
  56. 0 38
      static/process/GenerateNote/tmp/img-center-1774173430585/openai_raw_attempt_0.json
  57. BIN
      static/process/GenerateNote/tmp/img-center-1774173430585/screenshot.png
  58. BIN
      static/process/GenerateNote/tmp/img-center-1774173430585/template.png
  59. 0 17
      static/process/GenerateNote/tmp/img-center-1774173430585/vlm_center_model_attempts.json
  60. 0 4
      static/process/GenerateNote/tmp/img-center-1774173430585/vlm_center_parsed.json
  61. 0 10
      static/process/GenerateNote/tmp/img-center-1774173430585/vlm_center_result.json
  62. 0 32
      static/process/GenerateNote/tmp/img-center-1774173460899/openai_raw.json
  63. 0 38
      static/process/GenerateNote/tmp/img-center-1774173460899/openai_raw_attempt_0.json
  64. BIN
      static/process/GenerateNote/tmp/img-center-1774173460899/screenshot.png
  65. BIN
      static/process/GenerateNote/tmp/img-center-1774173460899/template.png
  66. 0 17
      static/process/GenerateNote/tmp/img-center-1774173460899/vlm_center_model_attempts.json
  67. 0 4
      static/process/GenerateNote/tmp/img-center-1774173460899/vlm_center_parsed.json
  68. 0 10
      static/process/GenerateNote/tmp/img-center-1774173460899/vlm_center_result.json
  69. BIN
      static/process/GenerateNote/tmp/pic0.png
  70. BIN
      static/process/GenerateNote/tmp/pic1.png
  71. 79 10
      static/process/RedNoteAIThumbsUp/process.json
  72. 112 29
      static/process/RedNoteBrowsingAndThumbsUp/process.json
  73. 138 30
      static/process/RedNoteBrowsingAndThumbsUpTest/process.json
  74. 4 2
      static/process/Test/process.json
  75. 319 231
      static/process/WeChatAIChating/process.json

+ 5 - 4
doc/工作流语法.md

@@ -146,9 +146,10 @@
 
 各扩展标签基础结构示例:
 
-- **fun**(method 为 func 目录下脚本名)
+- **fun**(`method` 为结点名;**仅允许**下列字段,语义一律用 `inVars` / `outVars` 表达,禁止另写 `path`、`key`、`value`、`prompt` 等)
+  - 允许:`type`、`method`、`inVars`、`outVars`,以及可选的流程字段 `condition`、`delay`、`times`、`timeout`、`retry`、`data`、`model`。
 ```json
-{ "type": "fun", "method": "脚本文件名", "inVars": [], "outVars": [] }
+{ "type": "fun", "method": "脚本名", "inVars": [], "outVars": [] }
 ```
 
 - **ocr-chat** / **extract-messages** / **ocr-chat-history** / **extract-chat-history**
@@ -191,9 +192,9 @@
 { "type": "img-cropping", "inVars": ["{areaJson}", "history/crop.png"], "outVars": [] }
 ```
 
-- **ai-generate**
+- **ai-generate**(须用 `type: fun`,prompt 只能写在 `inVars[0]`)
 ```json
-{ "type": "ai-generate", "prompt": "请总结:{{input}}", "inVars": ["{input}"], "outVars": ["{result}"] }
+{ "type": "fun", "method": "ai-generate", "inVars": ["请总结:{{input}}"], "outVars": ["{result}"] }
 ```
 
 - **string-press**

+ 31 - 0
nodejs/ef-compiler/action-schema.js

@@ -0,0 +1,31 @@
+/**
+ * 工作流结点严格格式:除各 type 专有字段外,仅允许下列可选流程字段。
+ */
+const FLOW_OPTIONAL_KEYS = new Set([
+  'condition',
+  'delay',
+  'times',
+  'timeout',
+  'retry',
+  'data',
+  'model',
+])
+
+/**
+ * @param {object} action
+ * @param {string[]} baseKeys - 允许的专有字段(须含 type)
+ * @param {string} path - 如 execute[0]
+ */
+function assertStrictKeys (action, baseKeys, path) {
+  if (!action || typeof action !== 'object') return
+  const allowed = new Set(baseKeys)
+  FLOW_OPTIONAL_KEYS.forEach((k) => allowed.add(k))
+  const extra = Object.keys(action).filter((k) => !allowed.has(k))
+  if (extra.length > 0) {
+    throw new Error(
+      `${path}: 不允许的字段: ${extra.sort().join(', ')}。允许: ${[...allowed].sort().join(', ')}`,
+    )
+  }
+}
+
+module.exports = { FLOW_OPTIONAL_KEYS, assertStrictKeys }

+ 10 - 5
nodejs/ef-compiler/actions/delay-parser.js

@@ -1,13 +1,18 @@
-/** 语句:delay 延迟(value 支持变量如 "{{stay-duration}}s";单位 ms/s/m/h/d/w/mon/月/y,可组合,纯数字按秒) */
+/** 语句:delay 延迟(仅 value;单位 ms/s/m/h/d/w/mon/月/y,可组合,纯数字按秒) */
+const { assertStrictKeys } = require('../action-schema.js')
 const types = ['delay']
 
-function parse(action, parseContext) {
-  const parsed = { type: 'delay', value: action.value || action.delay || '0s' }
-  return Object.assign({}, action, parsed)
+function parse (action, parseContext) {
+  const path = parseContext.actionPath || 'delay'
+  assertStrictKeys(action, ['type', 'value'], path)
+  if (action.value === undefined || action.value === null || String(action.value).trim() === '') {
+    throw new Error(`${path}: delay 须使用字段 value 指定时长`)
+  }
+  return { type: 'delay', value: action.value }
 }
 
 async function execute(action, ctx) {
-  const raw = action.value || action.delay || '0s'
+  const raw = action.value != null ? action.value : '0s'
   const resolved = ctx.replaceVariablesInString && ctx.variableContext
     ? ctx.replaceVariablesInString(String(raw), ctx.variableContext)
     : raw

+ 18 - 23
nodejs/ef-compiler/actions/echo-parser.js

@@ -1,4 +1,5 @@
-/** 语句:echo 打印信息(写入 log + UI);统一结点报错打印逻辑 */
+/** 语句:echo 打印信息(写入 log + UI);仅 inVars:字符串列表,支持 {{var}} / {var},由执行阶段替换 */
+const { assertStrictKeys } = require('../action-schema.js')
 const types = ['echo']
 
 /**
@@ -23,14 +24,20 @@ async function logActionError(action, result, ctx) {
   }
 }
 
-function parse(action, parseContext) {
-  const { extractVarName } = parseContext
-  const parsed = { type: 'echo' }
-  if (action.inVars && Array.isArray(action.inVars)) {
-    parsed.inVars = action.inVars.map(v => extractVarName(v))
-  } else parsed.inVars = []
-  if (action.value) parsed.value = action.value
-  return Object.assign({}, action, parsed)
+function parse (action, parseContext) {
+  const path = parseContext.actionPath || 'echo'
+  assertStrictKeys(action, ['type', 'inVars'], path)
+  if (!Array.isArray(action.inVars)) {
+    throw new Error(`${path}: echo 须使用 inVars 数组(无内容写 [])`)
+  }
+  return {
+    type: 'echo',
+    inVars: action.inVars.map((v) => {
+      if (v == null) return ''
+      if (typeof v === 'object' && v !== null) return JSON.stringify(v)
+      return String(v)
+    }),
+  }
 }
 
 async function execute(action, ctx) {
@@ -40,21 +47,9 @@ async function execute(action, ctx) {
     message = action.inVars.map((v) => {
       if (v == null) return ''
       if (typeof v === 'object' && v !== null) return JSON.stringify(v)
-      return String(v)
+      const s = String(v)
+      return replaceVariablesInString ? replaceVariablesInString(s, variableContext) : s
     }).join(' ')
-  } else if (action.value) {
-    message = replaceVariablesInString(action.value, variableContext)
-    const doubleBracePattern = /\{\{([\w-]+)\}\}/g
-    let match
-    const variablesInValue = []
-    while ((match = doubleBracePattern.exec(action.value)) !== null) variablesInValue.push(match[1])
-    if (!message || message === action.value) {
-      const missingVars = variablesInValue.filter(vn => {
-        const v = variableContext[vn]
-        return v === undefined || v === null || v === ''
-      })
-      if (missingVars.length > 0) message = `${action.value} [vars undefined or empty: ${missingVars.join(', ')}]`
-    }
   }
   const now = new Date()
   const timeStr = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`

+ 24 - 7
nodejs/ef-compiler/actions/for-parser.js

@@ -1,17 +1,34 @@
 /** 语句:for 循环(解析在此,执行在 sequence-runner) */
+const { assertStrictKeys } = require('../action-schema.js')
 const types = ['for']
 
-function parse(action, parseContext) {
-  const { parseActions } = parseContext
-  const parsed = {
+function parse (action, parseContext) {
+  const path = parseContext.actionPath || 'for'
+  assertStrictKeys(action, ['type', 'variable', 'indexVariable', 'times', 'array', 'body'], path)
+  if (!Array.isArray(action.body)) {
+    throw new Error(`${path}: for 须包含数组 body`)
+  }
+  const hasTimes = action.times != null && action.times !== ''
+  const hasArray = action.array != null && action.array !== ''
+  if (hasTimes === hasArray) {
+    throw new Error(`${path}: for 须二选一:填写 times+variable,或填写 array+indexVariable+variable`)
+  }
+  if (hasTimes && (action.variable === undefined || action.variable === null || action.variable === '')) {
+    throw new Error(`${path}: for 使用 times 时须提供 variable`)
+  }
+  if (hasArray) {
+    if (!action.indexVariable) {
+      throw new Error(`${path}: for 使用 array 时须提供 indexVariable`)
+    }
+  }
+  return {
     type: 'for',
     variable: action.variable,
     indexVariable: action.indexVariable,
-    times: action.times,
-    array: action.array != null ? action.array : null,
-    body: action.body ? parseActions(action.body) : [],
+    times: hasTimes ? action.times : null,
+    array: hasArray ? action.array : null,
+    body: parseContext.parseActions(action.body, 'body'),
   }
-  return Object.assign({}, action, parsed)
 }
 
 async function execute() {

+ 88 - 26
nodejs/ef-compiler/actions/fun/adb/adb-parser.js

@@ -4,6 +4,7 @@
  */
 
 const { calculateSwipeCoordinates } = require('./utils.js')
+const { assertStrictKeys } = require('../../../action-schema.js')
 
 const types = ['adb', 'keyevent', 'scroll', 'swipe', 'locate', 'click', 'press', 'input', 'ocr']
 
@@ -20,33 +21,97 @@ const METHOD_HANDLERS = {
 }
 
 /* ========== 解析入口 ========== */
-function parse(action, parseContext) {
+function parse (action, parseContext) {
+  const path = parseContext.actionPath || 'adb'
   const type = action.type || 'adb'
   const { extractVarName, resolveValue } = parseContext
   const variableContext = parseContext.variableContext || {}
-  const parsed = Object.assign({}, action, { type })
 
   if (type === 'adb') {
-    parsed.method = action.method
-    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)) : []
-    parsed.target = action.target
-    parsed.value = action.value
-    parsed.variable = action.variable
-    parsed.clear = action.clear || false
-    return parsed
-  }
-  if (type === 'locate' || type === 'click') return parsed
+    assertStrictKeys(action, ['type', 'method', 'inVars', 'outVars', 'target', 'value', 'variable', 'clear'], path)
+    if (action.method == null || String(action.method).trim() === '') {
+      throw new Error(`${path}: adb 须包含 method`)
+    }
+    if (!Array.isArray(action.inVars)) {
+      throw new Error(`${path}: adb 须包含 inVars 数组`)
+    }
+    if (!Array.isArray(action.outVars)) {
+      throw new Error(`${path}: adb 须包含 outVars 数组`)
+    }
+    return {
+      type: 'adb',
+      method: action.method,
+      inVars: action.inVars.map((v) => extractVarName(v)),
+      outVars: action.outVars.map((v) => extractVarName(v)),
+      target: action.target,
+      value: action.value,
+      variable: action.variable,
+      clear: action.clear === true,
+    }
+  }
+  if (type === 'locate' || type === 'click') {
+    assertStrictKeys(action, ['type', 'method', 'target', 'variable'], path)
+    return {
+      type,
+      method: action.method,
+      target: action.target,
+      variable: action.variable,
+    }
+  }
   if (type === 'input') {
-    parsed.clear = action.clear || false
-    return parsed
+    assertStrictKeys(action, ['type', 'value', 'target', 'clear'], path)
+    if (action.value === undefined || action.value === null || action.value === '') {
+      throw new Error(`${path}: input 须包含 value`)
+    }
+    return {
+      type: 'input',
+      value: action.value,
+      target: action.target,
+      clear: action.clear === true,
+    }
+  }
+  if (type === 'press') {
+    assertStrictKeys(action, ['type', 'value'], path)
+    if (action.value === undefined || action.value === null || action.value === '') {
+      throw new Error(`${path}: press 须包含 value`)
+    }
+    return { type: 'press', value: action.value }
   }
   if (type === 'ocr') {
-    parsed.area = action.area
-    parsed.avatar = resolveValue(action.avatar, variableContext)
-    return parsed
+    assertStrictKeys(action, ['type', 'method', 'area', 'avatar', 'variable'], path)
+    return {
+      type: 'ocr',
+      method: action.method,
+      area: action.area,
+      avatar: action.avatar != null ? resolveValue(action.avatar, variableContext) : undefined,
+      variable: action.variable,
+    }
+  }
+  if (type === 'keyevent') {
+    assertStrictKeys(action, ['type', 'inVars'], path)
+    if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
+      throw new Error(`${path}: keyevent 须使用 inVars 且至少 1 项为键码`)
+    }
+    return {
+      type: 'keyevent',
+      inVars: action.inVars.map((v) => extractVarName(v)),
+    }
   }
-  return parsed
+  if (type === 'scroll') {
+    assertStrictKeys(action, ['type', 'value'], path)
+    if (action.value === undefined || action.value === null || action.value === '') {
+      throw new Error(`${path}: scroll 须包含 value(方向)`)
+    }
+    return { type: 'scroll', value: action.value }
+  }
+  if (type === 'swipe') {
+    assertStrictKeys(action, ['type', 'value'], path)
+    if (action.value === undefined || action.value === null || action.value === '') {
+      throw new Error(`${path}: swipe 须包含 value`)
+    }
+    return { type: 'swipe', value: action.value }
+  }
+  throw new Error(`${path}: 未知的 adb-parser type: ${type}`)
 }
 
 /* ========== 执行入口 ========== */
@@ -164,15 +229,12 @@ async function execute(action, ctx) {
 
   /* --- type: keyevent --- */
   if (action.type === 'keyevent') {
-    let keyCode = null
     const inVars = action.inVars || []
-    if (inVars.length > 0) {
-      const keyVar = extractVarName(inVars[0])
-      keyCode = variableContext[keyVar] || keyVar
-    } else if (action.value) {
-      keyCode = resolveValue(action.value, variableContext)
-    }
-    if (!keyCode) return { success: false, error: 'keyevent 操作缺少按键代码参数' }
+    const keyVar = extractVarName(inVars[0])
+    const keyCode = variableContext[keyVar] != null && variableContext[keyVar] !== ''
+      ? variableContext[keyVar]
+      : keyVar
+    if (!keyCode && keyCode !== 0) return { success: false, error: 'keyevent 操作缺少按键代码参数' }
     if (keyCode === 'KEYCODE_BACK') keyCode = '4'
     const keyResult = api.sendSystemKey(device, String(keyCode))
     if (!keyResult.success) return { success: false, error: `按键失败: ${keyResult.error != null ? keyResult.error : 'unknown'}` }

+ 19 - 1
nodejs/ef-compiler/actions/fun/fun-node-registry.js

@@ -1,7 +1,7 @@
 /**
  * fun 结点注册表:新增结点只需 1)在 actions/fun/ 新建脚本(只实现 executeXxx(input),入参由 in 规定、出参由 out 规定)2)在此添加一条配置。
  * 通用逻辑(解析 inVars、取值、写回 variableContext、logOutVars)由 fun-parser 统一处理,脚本不写 parseNode/runNode。
- * 配置项:type, category, in(入参名数组), inAlt?(字段别名), execute(脚本导出的函数名), script?(默认 type+'.js'), displayName?。出参由工作流 action.outVars[0] 指定。
+ * 配置项:type, category(io=下载/AI 等;fun=actions/fun 根目录脚本,工作流 type:fun + method), in, inAlt?, execute, script?, displayName?。出参由工作流 action.outVars[0] 指定。
  * 仅当需要完全自定义解析或执行时,才配 customParse/customRun 并在脚本中导出 parseNode、runNode。
  */
 module.exports = [
@@ -46,4 +46,22 @@ module.exports = [
     script: 'IO/remove-forder.js',
     displayName: 'remove folder',
   },
+  {
+    type: 'persist-save',
+    category: 'fun',
+    in: ['stateKey', 'stateValue'],
+    inAlt: { stateKey: 'key', stateValue: 'value' },
+    execute: 'executePersistSave',
+    script: 'persist-save.js',
+    displayName: 'persist save',
+  },
+  {
+    type: 'persist-read',
+    category: 'fun',
+    in: ['stateKey'],
+    inAlt: { stateKey: 'key' },
+    execute: 'executePersistRead',
+    script: 'persist-read.js',
+    displayName: 'persist read',
+  },
 ]

+ 326 - 121
nodejs/ef-compiler/actions/fun/fun-parser.js

@@ -4,20 +4,50 @@
  */
 const path = require('path')
 const variableParser = require('../../variable-parser.js')
+const { assertStrictKeys } = require('../../action-schema.js')
 const FUN_NODE_REGISTRY = require('./fun-node-registry.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) {
   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 = [
@@ -51,137 +81,279 @@ function getRegistryScript(funcDir, type) {
   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)
   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
     if (regDef.customParse) {
       const script = getRegistryScript(funcDir, action.type)
       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 inAlt = regDef.inAlt || {}
     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
   }
 
   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 'ocr-chat':
     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':
-      break
     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': {
-      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-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-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': {
-      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) {
@@ -194,13 +366,18 @@ async function runAction(action, device, folderPath, resolution, ctx) {
     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)
   ctx.resolution = resolution || ctx.resolution || { width: 1080, height: 1920 }
 
   if (resolvedAction.type === 'fun') {
     const m = resolvedAction.method != null ? String(resolvedAction.method).trim() : ''
     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) {
     return run(resolvedAction.method, resolvedAction, ctx, device, folderPath)
@@ -274,6 +451,16 @@ function get(funcDir, category) {
         } catch (e) { /* skip missing script */ }
       })
       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':
       mod = (() => {
         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': {
       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) {
         let historySummary = variableContext['historySummary'] || ''
         if (!historySummary) {
@@ -706,13 +897,25 @@ async function run(actionType, action, ctx, device, folderPath) {
         const inKeys = regDef.in || []
         const inAlt = regDef.inAlt || {}
         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') {
           let rec = action.recursive
           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
           if (outVal !== undefined && outVal !== null) {
             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)

+ 31 - 5
nodejs/ef-compiler/actions/fun/json/json-to-arr.js

@@ -9,17 +9,36 @@
  * @param {{ jsonString: string }} input - jsonString 为变量取值后的 JSON 字符串
  * @returns {{ success: boolean, result?: any[], error?: string }}
  */
+function normalizeAiJsonText(raw) {
+  let s = typeof raw === 'string' ? raw.trim() : String(raw).trim()
+  if (!s) return ''
+  // 去掉行首行尾 markdown 围栏(含仅开头 ```json 的情况)
+  s = s.replace(/^```(?:json)?\s*\r?\n?/i, '')
+  s = s.replace(/\r?\n?```\s*$/i, '')
+  s = s.trim()
+  // 再取第一个 ```...``` 内层(模型有时前后还有说明文字)
+  const inner = s.match(/```(?:json)?\s*([\s\S]*?)```/i)
+  if (inner) s = inner[1].trim()
+  // 弯引号、直角引号 → 标准双引号(避免 Unterminated string)
+  s = s.replace(/[\u201c\u201d\u201e\u201f\u2033\u2036\u00ab\u00bb]/g, '"')
+  s = s.replace(/[「」『』]/g, '"')
+  // 截取从首个 [ 到最后一个 ],忽略模型输出的前后废话
+  const lb = s.indexOf('[')
+  const rb = s.lastIndexOf(']')
+  if (lb !== -1 && rb > lb) s = s.slice(lb, rb + 1)
+  return s.trim()
+}
+
 async function executeJsonToArr({ jsonString }) {
   if (jsonString == null) return { success: false, error: 'json-to-arr 缺少输入(如 inVars[0])' }
-  let str = typeof jsonString === 'string' ? jsonString.trim() : String(jsonString).trim()
-  // 去掉可能的 markdown 代码块包裹
-  const codeBlockMatch = str.match(/^```(?:json)?\s*([\s\S]*?)```\s*$/m)
-  if (codeBlockMatch) str = codeBlockMatch[1].trim()
+  let str = normalizeAiJsonText(jsonString)
   if (!str) return { success: false, error: 'json-to-arr 输入为空' }
   let parsed
+  let lastErr
   try {
     parsed = JSON.parse(str)
   } catch (e) {
+    lastErr = e
     // AI 可能截断:尝试补全后再解析
     if (str.startsWith('[')) {
       if (/^\[\s*"[^"]*$/.test(str)) {
@@ -28,9 +47,16 @@ async function executeJsonToArr({ jsonString }) {
       if (parsed == null && str.endsWith('"')) {
         try { parsed = JSON.parse(str + ']') } catch (_) {}
       }
+      // 常见:只输出了一个带引号的串和逗号,第二个串未闭合
+      if (parsed == null && /^\[\s*"[^"]*",\s*"[^"]*$/.test(str)) {
+        try { parsed = JSON.parse(str + '"]') } catch (_) {}
+      }
     }
     if (parsed == null) {
-      return { success: false, error: `JSON 解析失败: ${e && e.message ? e.message : String(e)}` }
+      return {
+        success: false,
+        error: `JSON 解析失败: ${lastErr && lastErr.message ? lastErr.message : String(lastErr)}(原始前 120 字: ${String(jsonString).slice(0, 120).replace(/\s+/g, ' ')})`,
+      }
     }
   }
   const arr = Array.isArray(parsed) ? parsed : (parsed != null ? [parsed] : [])

+ 72 - 0
nodejs/ef-compiler/actions/fun/persist-read.js

@@ -0,0 +1,72 @@
+/**
+ * 持久化读取:从当前流程目录 save/config.json 按 key 读取(与 persist-save 成对,仅 string / number)
+ */
+
+const path = require('path')
+const fs = require('fs')
+
+/** 确保 save 目录存在;若无 config.json 则写入 {} */
+function ensurePersistConfigFile (configPath) {
+  const saveDir = path.dirname(configPath)
+  try {
+    if (!fs.existsSync(saveDir)) {
+      fs.mkdirSync(saveDir, { recursive: true })
+    }
+    if (!fs.existsSync(configPath)) {
+      fs.writeFileSync(configPath, '{}\n', 'utf8')
+    }
+  } catch (e) {
+    return { ok: false, error: `persist-read 无法创建 save/config: ${e.message || e}` }
+  }
+  return { ok: true }
+}
+
+function isAllowedPersistValue (val) {
+  if (val === null || val === undefined) return { ok: false, error: 'persist-read 不支持 null / undefined' }
+  if (typeof val === 'string' || typeof val === 'number') {
+    if (typeof val === 'number' && !Number.isFinite(val)) {
+      return { ok: false, error: 'persist-read 不支持 NaN / Infinity' }
+    }
+    return { ok: true }
+  }
+  if (typeof val === 'boolean') {
+    return { ok: false, error: 'persist-read 仅支持读取 string 与 number,当前为 boolean' }
+  }
+  return { ok: false, error: 'persist-read 仅支持读取 string 与 number,不支持 object/array' }
+}
+
+async function executePersistRead ({ stateKey, folderPath }) {
+  if (!folderPath || typeof folderPath !== 'string') {
+    return { success: false, error: 'persist-read 缺少流程目录 folderPath' }
+  }
+  if (stateKey == null || String(stateKey).trim() === '') {
+    return { success: false, error: 'persist-read 缺少 key(JSON 键名)' }
+  }
+  const key = String(stateKey).trim()
+  const configPath = path.join(folderPath, 'save', 'config.json')
+  const ensured = ensurePersistConfigFile(configPath)
+  if (!ensured.ok) {
+    return { success: false, error: ensured.error }
+  }
+  let data
+  try {
+    const raw = fs.readFileSync(configPath, 'utf8')
+    data = JSON.parse(raw)
+  } catch (e) {
+    return { success: false, error: `persist-read 解析 config.json 失败: ${e.message || e}` }
+  }
+  if (!data || typeof data !== 'object' || Array.isArray(data)) {
+    return { success: false, error: 'persist-read config.json 必须为 JSON 对象' }
+  }
+  if (!Object.prototype.hasOwnProperty.call(data, key)) {
+    return { success: true, value: '', result: '' }
+  }
+  const val = data[key]
+  const check = isAllowedPersistValue(val)
+  if (!check.ok) {
+    return { success: false, error: check.error }
+  }
+  return { success: true, value: val, result: val }
+}
+
+module.exports = { executePersistRead }

+ 82 - 0
nodejs/ef-compiler/actions/fun/persist-save.js

@@ -0,0 +1,82 @@
+/**
+ * 持久化保存:将单个键值写入当前流程目录 save/config.json
+ * 支持:string、number;普通 object(如 {x,y})会 JSON.stringify 后以字符串写入
+ * 路径:folderPath/save/config.json(folderPath 一般为 static/process/<流程名>)
+ */
+
+const path = require('path')
+const fs = require('fs')
+
+function normalizeStoredValue (value) {
+  if (value === undefined) {
+    return { ok: false, error: 'persist-save 缺少 value' }
+  }
+  if (value === null) {
+    return { ok: false, error: 'persist-save 不支持 null' }
+  }
+  if (typeof value === 'number') {
+    if (!Number.isFinite(value)) {
+      return { ok: false, error: 'persist-save 不支持 NaN / Infinity' }
+    }
+    return { ok: true, stored: value }
+  }
+  if (typeof value === 'string') {
+    return { ok: true, stored: value }
+  }
+  if (typeof value === 'object') {
+    if (Array.isArray(value)) {
+      return { ok: false, error: 'persist-save 不支持 array' }
+    }
+    try {
+      return { ok: true, stored: JSON.stringify(value) }
+    } catch (e) {
+      return { ok: false, error: 'persist-save 对象无法序列化为 JSON' }
+    }
+  }
+  if (typeof value === 'boolean') {
+    return { ok: false, error: 'persist-save 仅支持 string 与 number,当前为 boolean' }
+  }
+  return { ok: false, error: `persist-save 仅支持 string 与 number,当前类型: ${typeof value}` }
+}
+
+async function executePersistSave ({ stateKey, stateValue, folderPath }) {
+  if (!folderPath || typeof folderPath !== 'string') {
+    return { success: false, error: 'persist-save 缺少流程目录 folderPath' }
+  }
+  if (stateKey == null || String(stateKey).trim() === '') {
+    return { success: false, error: 'persist-save 缺少 key(JSON 键名)' }
+  }
+  const key = String(stateKey).trim()
+  const norm = normalizeStoredValue(stateValue)
+  if (!norm.ok) {
+    return { success: false, error: norm.error }
+  }
+
+  const saveDir = path.join(folderPath, 'save')
+  const configPath = path.join(saveDir, 'config.json')
+  let data = {}
+  try {
+    if (fs.existsSync(configPath)) {
+      const raw = fs.readFileSync(configPath, 'utf8')
+      const parsed = JSON.parse(raw)
+      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
+        data = parsed
+      }
+    }
+  } catch (_) {
+    data = {}
+  }
+
+  data[key] = norm.stored
+
+  try {
+    fs.mkdirSync(saveDir, { recursive: true })
+    fs.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf8')
+  } catch (e) {
+    return { success: false, error: `persist-save 写入失败: ${e.message || e}` }
+  }
+
+  return { success: true, path: configPath, key, value: norm.stored }
+}
+
+module.exports = { executePersistSave }

+ 14 - 7
nodejs/ef-compiler/actions/if-parser.js

@@ -1,15 +1,22 @@
-/** 语句:if 条件判断(解析在此,执行在 sequence-runner) */
+/** 语句:if 条件判断(解析在此,执行在 sequence-runner);须同时含 then、else 数组(可无分支写 []) */
+const { assertStrictKeys } = require('../action-schema.js')
 const types = ['if']
 
-function parse(action, parseContext) {
-  const { parseActions } = parseContext
-  const parsed = {
+function parse (action, parseContext) {
+  const path = parseContext.actionPath || 'if'
+  assertStrictKeys(action, ['type', 'condition', 'then', 'else'], path)
+  if (!Array.isArray(action.then)) {
+    throw new Error(`${path}: if 须包含数组 then`)
+  }
+  if (!Array.isArray(action.else)) {
+    throw new Error(`${path}: if 须包含数组 else(无 else 分支写 [])`)
+  }
+  return {
     type: 'if',
     condition: action.condition,
-    then: (action.then || action.ture) ? parseActions(action.then || action.ture) : [],
-    else: action.else ? parseActions(action.else) : [],
+    then: parseContext.parseActions(action.then, 'then'),
+    else: parseContext.parseActions(action.else, 'else'),
   }
-  return Object.assign({}, action, parsed)
 }
 
 async function execute() {

+ 20 - 13
nodejs/ef-compiler/actions/random-parser.js

@@ -1,20 +1,27 @@
-/** 语句:random 生成随机数 */
+/** 语句:random 生成随机数(仅 inVars[0]=min、inVars[1]=max,outVars[0]=目标变量名) */
+const { assertStrictKeys } = require('../action-schema.js')
 const types = ['random']
 
-function parse(action, parseContext) {
+function parse (action, parseContext) {
   const { extractVarName } = parseContext
-  const parsed = { type: 'random', integer: action.integer !== undefined ? action.integer : true }
-  if (action.inVars && Array.isArray(action.inVars) && action.inVars.length >= 2) {
-    parsed.min = action.inVars[0]
-    parsed.max = action.inVars[1]
-  } else {
-    parsed.min = action.min
-    parsed.max = action.max
+  const path = parseContext.actionPath || 'random'
+  assertStrictKeys(action, ['type', 'inVars', 'outVars', 'integer'], path)
+  if (!Array.isArray(action.inVars) || action.inVars.length !== 2) {
+    throw new Error(`${path}: random 须 inVars 恰好 2 项 [min, max]`)
+  }
+  if (!Array.isArray(action.outVars) || action.outVars.length !== 1) {
+    throw new Error(`${path}: random 须 outVars 恰好 1 项(写入变量名,如 "{x}")`)
+  }
+  const integer = action.integer !== undefined ? action.integer : true
+  return {
+    type: 'random',
+    integer,
+    min: action.inVars[0],
+    max: action.inVars[1],
+    inVars: action.inVars.map((v) => extractVarName(v)),
+    outVars: [extractVarName(action.outVars[0])],
+    variable: extractVarName(action.outVars[0]),
   }
-  if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
-    parsed.variable = action.outVars[0]
-  } else parsed.variable = action.variable
-  return Object.assign({}, action, parsed)
 }
 
 async function execute(action, ctx) {

+ 17 - 9
nodejs/ef-compiler/actions/schedule-parser.js

@@ -1,17 +1,25 @@
 /** 语句:schedule 定时执行(解析在此,执行在 sequence-runner) */
+const { assertStrictKeys } = require('../action-schema.js')
 const types = ['schedule']
 
-function parse(action, parseContext) {
-  const { parseActions } = parseContext
-  const parsed = {
-    type: 'schedule',
-    condition: action.condition || {},
-    interval: action.interval && Array.isArray(action.interval) ? parseActions(action.interval) : [],
+function parse (action, parseContext) {
+  const path = parseContext.actionPath || 'schedule'
+  assertStrictKeys(action, ['type', 'condition', 'interval'], path)
+  if (action.condition == null || typeof action.condition !== 'object' || Array.isArray(action.condition)) {
+    throw new Error(`${path}: schedule 须包含对象 condition`)
+  }
+  if (!Array.isArray(action.interval)) {
+    throw new Error(`${path}: schedule 须包含数组 interval`)
   }
-  if (parsed.condition.repeat === 'forever' || parsed.condition.repeat === 'Forever') {
-    parsed.condition.repeat = -1
+  const condition = { ...action.condition }
+  if (condition.repeat === 'forever' || condition.repeat === 'Forever') {
+    condition.repeat = -1
+  }
+  return {
+    type: 'schedule',
+    condition,
+    interval: parseContext.parseActions(action.interval, 'interval'),
   }
-  return Object.assign({}, action, parsed)
 }
 
 async function execute() {

+ 8 - 3
nodejs/ef-compiler/actions/set-parser.js

@@ -1,6 +1,7 @@
 /**
  * 语句:set 设置变量 + 变量与值解析(原 value-resolver:extractVarName、replaceVariablesInString、resolveValue、parseValue、时间/延迟)
  */
+const { assertStrictKeys } = require('../action-schema.js')
 const types = ['set']
 
 function extractVarName(varName) {
@@ -135,14 +136,18 @@ function parseValue(str) {
   return str
 }
 
-function parse(action, parseContext) {
+function parse (action, parseContext) {
+  const path = parseContext.actionPath || 'set'
+  assertStrictKeys(action, ['type', 'variable', 'value'], path)
+  if (action.variable === undefined || action.variable === null || action.variable === '') {
+    throw new Error(`${path}: set 须提供 variable`)
+  }
   const variableContext = parseContext.variableContext || {}
-  const parsed = {
+  return {
     type: 'set',
     variable: action.variable,
     value: resolveValue(action.value, variableContext),
   }
-  return Object.assign({}, action, parsed)
 }
 
 async function execute(action, ctx) {

+ 20 - 9
nodejs/ef-compiler/actions/try-parser.js

@@ -1,16 +1,27 @@
-/** 语句:try 尝试执行,成功走 success、失败走 fail(解析在此,执行在 sequence-runner) */
+/** 语句:try:须含 try、success、fail 三个数组(可为 []);可选 continueAfterFail */
+const { assertStrictKeys } = require('../action-schema.js')
 const types = ['try']
 
-function parse(action, parseContext) {
-  const { parseActions } = parseContext
-  const failActions = action.fail || action.catch
-  const parsed = {
+function parse (action, parseContext) {
+  const path = parseContext.actionPath || 'try'
+  assertStrictKeys(action, ['type', 'try', 'success', 'fail', 'continueAfterFail'], path)
+  if (!Array.isArray(action.try)) {
+    throw new Error(`${path}: try 须包含数组 try`)
+  }
+  if (!Array.isArray(action.success)) {
+    throw new Error(`${path}: try 须包含数组 success(无成功后续写 [])`)
+  }
+  if (!Array.isArray(action.fail)) {
+    throw new Error(`${path}: try 须包含数组 fail(无失败处理写 [])`)
+  }
+  const out = {
     type: 'try',
-    try: (action.try || action.body) ? parseActions(action.try || action.body) : [],
-    success: action.success ? parseActions(action.success) : [],
-    fail: failActions ? parseActions(failActions) : [],
+    try: parseContext.parseActions(action.try, 'try'),
+    success: parseContext.parseActions(action.success, 'success'),
+    fail: parseContext.parseActions(action.fail, 'fail'),
   }
-  return Object.assign({}, action, parsed)
+  if (action.continueAfterFail === true) out.continueAfterFail = true
+  return out
 }
 
 async function execute() {

+ 10 - 6
nodejs/ef-compiler/actions/while-parser.js

@@ -1,14 +1,18 @@
-/** 语句:while 循环(解析在此,执行在 sequence-runner) */
+/** 语句:while 循环(解析在此,执行在 sequence-runner);仅 body,不用 ture */
+const { assertStrictKeys } = require('../action-schema.js')
 const types = ['while']
 
-function parse(action, parseContext) {
-  const { parseActions } = parseContext
-  const parsed = {
+function parse (action, parseContext) {
+  const path = parseContext.actionPath || 'while'
+  assertStrictKeys(action, ['type', 'condition', 'body'], path)
+  if (!Array.isArray(action.body)) {
+    throw new Error(`${path}: while 须包含数组 body`)
+  }
+  return {
     type: 'while',
     condition: action.condition,
-    body: (action.body || action.ture) ? parseActions(action.body || action.ture) : [],
+    body: parseContext.parseActions(action.body, 'body'),
   }
-  return Object.assign({}, action, parsed)
 }
 
 async function execute() {

+ 107 - 10
nodejs/ef-compiler/expression-evaluator.js

@@ -3,6 +3,99 @@
  */
 const { parseValue, resolveValue } = require('./actions/set-parser.js')
 
+/** 在 str 中查找 needle 的起始下标,忽略单/双引号字符串内的匹配(支持 \\ 转义) */
+function findOutsideQuotes(str, needle, fromIndex = 0) {
+  let i = fromIndex
+  let inSingle = false
+  let inDouble = false
+  while (i < str.length) {
+    const c = str[i]
+    if (inSingle) {
+      if (c === '\\' && i + 1 < str.length) {
+        i += 2
+        continue
+      }
+      if (c === "'") inSingle = false
+      i++
+      continue
+    }
+    if (inDouble) {
+      if (c === '\\' && i + 1 < str.length) {
+        i += 2
+        continue
+      }
+      if (c === '"') inDouble = false
+      i++
+      continue
+    }
+    if (c === "'") {
+      inSingle = true
+      i++
+      continue
+    }
+    if (c === '"') {
+      inDouble = true
+      i++
+      continue
+    }
+    if (str.startsWith(needle, i)) return i
+    i++
+  }
+  return -1
+}
+
+function splitTopLevelFirst(expr, delimiter) {
+  const idx = findOutsideQuotes(expr, delimiter)
+  if (idx === -1) return null
+  return [expr.slice(0, idx).trim(), expr.slice(idx + delimiter.length).trim()]
+}
+
+/**
+ * 在引号外查找第一个比较运算符(长运算符优先,避免 >= 被拆成 >)
+ */
+function findBinaryComparisonOp(expr, operators) {
+  const sorted = [...operators].sort((a, b) => b.op.length - a.op.length)
+  let i = 0
+  let inSingle = false
+  let inDouble = false
+  while (i < expr.length) {
+    const c = expr[i]
+    if (inSingle) {
+      if (c === '\\' && i + 1 < expr.length) {
+        i += 2
+        continue
+      }
+      if (c === "'") inSingle = false
+      i++
+      continue
+    }
+    if (inDouble) {
+      if (c === '\\' && i + 1 < expr.length) {
+        i += 2
+        continue
+      }
+      if (c === '"') inDouble = false
+      i++
+      continue
+    }
+    if (c === "'") {
+      inSingle = true
+      i++
+      continue
+    }
+    if (c === '"') {
+      inDouble = true
+      i++
+      continue
+    }
+    for (const row of sorted) {
+      if (expr.startsWith(row.op, i)) return { index: i, op: row.op, fn: row.fn }
+    }
+    i++
+  }
+  return null
+}
+
 function parseArithmeticExpression(expr) {
   let index = 0
   const skipWhitespace = () => {
@@ -115,11 +208,13 @@ function evaluateExpression(expression, context) {
 
 function parseConditionExpression(expr) {
   expr = expr.trim()
-  if (expr.includes('||')) {
-    return expr.split('||').map(p => p.trim()).some(part => parseConditionExpression(part))
+  const orPart = splitTopLevelFirst(expr, '||')
+  if (orPart) {
+    return parseConditionExpression(orPart[0]) || parseConditionExpression(orPart[1])
   }
-  if (expr.includes('&&')) {
-    return expr.split('&&').map(p => p.trim()).every(part => parseConditionExpression(part))
+  const andPart = splitTopLevelFirst(expr, '&&')
+  if (andPart) {
+    return parseConditionExpression(andPart[0]) && parseConditionExpression(andPart[1])
   }
   const operators = [
     { op: '!=', fn: (a, b) => a != b },
@@ -140,11 +235,11 @@ function parseConditionExpression(expr) {
     { op: '>', fn: (a, b) => Number(a) > Number(b) },
     { op: '<', fn: (a, b) => Number(a) < Number(b) },
   ]
-  for (const { op, fn } of operators) {
-    if (expr.includes(op)) {
-      const parts = expr.split(op).map(p => p.trim())
-      if (parts.length === 2) return fn(parseValue(parts[0]), parseValue(parts[1]))
-    }
+  const binary = findBinaryComparisonOp(expr, operators)
+  if (binary) {
+    const left = expr.slice(0, binary.index).trim()
+    const right = expr.slice(binary.index + binary.op.length).trim()
+    return binary.fn(parseValue(left), parseValue(right))
   }
   const value = parseValue(expr)
   if (typeof value === 'boolean') return value
@@ -170,8 +265,10 @@ function evaluateCondition(condition, context) {
       const v = context[varName]
       if (v === undefined || v === null || v === '' || v === 'undefined' || v === 'null') return '""'
       if (typeof v === 'string') {
+        const trimmed = v.trim()
+        if (trimmed === '' || trimmed === 'undefined' || trimmed === 'null') return '""'
         try {
-          const p = JSON.parse(v)
+          const p = JSON.parse(trimmed)
           if (Array.isArray(p)) return `"${v.replace(/"/g, '\\"')}"`
         } catch (e) {}
         return `"${v.replace(/"/g, '\\"')}"`

+ 3 - 3
nodejs/ef-compiler/sequence-runner.js

@@ -83,7 +83,7 @@ async function executeActionSequence(
 
     if (action.type === 'if') {
       const conditionResult = evaluateCondition(action.condition, variableContext)
-      const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || [])
+      const actionsToExecute = conditionResult ? (action.then || []) : (action.else || [])
       if (actionsToExecute.length > 0) {
         const result = await executeActionSequence(actionsToExecute, device, folderPath, resolution, innerInterval, onStepComplete, shouldStop, depth + 1, ctx)
         if (!isOk(result)) {
@@ -164,9 +164,9 @@ async function executeActionSequence(
     }
 
     if (action.type === 'try') {
-      const tryActions = action.try || action.body || []
+      const tryActions = action.try || []
       const successActions = action.success || []
-      const failActions = action.fail || action.catch || []
+      const failActions = action.fail || []
       /** try 主路径失败后:即使 fail 分支执行成功,默认仍向上返回失败,避免 for 进入下一轮或继续执行后续兄弟步骤。需继续时请设 continueAfterFail: true */
       const continueAfterFail = action.continueAfterFail === true
       const result = tryActions.length > 0

+ 15 - 5
nodejs/ef-compiler/variable-parser.js

@@ -18,6 +18,8 @@ const INPUT_KEYS = [
   'regionArea', 'saveDir', 'url', 'filename', 'imageUrl',
   'imagePath', 'squareSpec', 'scale', 'method',
   'recursive',
+  'stateKey', 'stateValue',
+  'key',
 ]
 
 /**
@@ -63,19 +65,26 @@ function replaceArrayIndexInString(str, variableContext) {
 /**
  * 解析单值:先解析数组下标 {arr}[{idx}] / {arr}[n],再做 {{var}}、{var} 替换,最后对整体做引用解析。
  */
-function resolveInputValue(val, variableContext) {
+/**
+ * @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 (result === val && /^[\w-]+$/.test(val) && variableContext[val] !== undefined) result = variableContext[val]
+    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))
+  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)
+    for (const k in val) out[k] = resolveInputValue(val[k], variableContext, opts)
     return out
   }
   return val
@@ -100,7 +109,8 @@ function resolveActionInputs(action, variableContext) {
   }
 
   if (resolved.inVars && Array.isArray(resolved.inVars)) {
-    resolved.inVars = resolved.inVars.map((v) => resolveInputValue(v, variableContext))
+    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))

+ 38 - 11
nodejs/ef-compiler/workflow-json-parser.js

@@ -96,6 +96,12 @@ function getActionName(action) {
     const repeatText = repeat === -1 ? 'infinite' : `repeat ${repeat}`
     return `${typeName}: ${interval}, ${repeatText}`
   }
+  if (action.type === 'echo') {
+    const parts = (action.inVars && Array.isArray(action.inVars)) ? action.inVars.map((v) => (v == null ? '' : String(v))) : []
+    const joined = parts.join(' ').trim()
+    const d = joined.length > 40 ? joined.substring(0, 40) + '...' : joined
+    return `${typeName}: ${d || '(empty)'}`
+  }
   if (action.type === 'input') {
     return `${typeName}: ${displayValue.length > 20 ? displayValue.substring(0, 20) + '...' : displayValue}`
   }
@@ -116,27 +122,48 @@ function getActionName(action) {
 }
 
 /**
- * 解析操作数组:只按 type 分派,由各 action 脚本自己解析
+ * 解析操作数组:只按 type 分派,由各 action 脚本自己解析。
+ * 非法结点、未知 type、缺 type 均抛错并中止解析(不静默跳过、不原样透传)。
  */
-function parseActions(actions, state) {
-  if (!Array.isArray(actions)) return []
+function parseActions (actions, state, pathPrefix = 'execute') {
+  if (!Array.isArray(actions)) {
+    throw new Error(`[${pathPrefix}] 必须为数组`)
+  }
   const parsedActions = []
-  const parseActionsFn = (arr) => parseActions(arr, state)
-  for (const action of actions) {
-    if (typeof action !== 'object' || action === null) continue
-    if (!action.type) continue
+  for (let i = 0; i < actions.length; i++) {
+    const itemPath = `${pathPrefix}[${i}]`
+    const action = actions[i]
+    if (action === null || typeof action !== 'object' || Array.isArray(action)) {
+      throw new Error(`[${itemPath}] 结点必须为对象`)
+    }
+    if (!action.type || typeof action.type !== 'string') {
+      throw new Error(`[${itemPath}] 缺少字符串字段 type`)
+    }
     const entry = registry[action.type]
     if (!entry || !entry.parse) {
-      parsedActions.push(Object.assign({}, action, { type: action.type }))
-      continue
+      throw new Error(`[${itemPath}] 未知的 type: "${action.type}"`)
+    }
+    const parseActionsChild = (childArr, segment) => {
+      if (!Array.isArray(childArr)) {
+        throw new Error(`[${itemPath}.${segment}] 必须为数组`)
+      }
+      return parseActions(childArr, state, `${itemPath}.${segment}`)
     }
     const parseContext = {
       extractVarName,
       resolveValue,
       variableContext: state.variableContext,
-      parseActions: parseActionsFn,
+      state,
+      parseActions: parseActionsChild,
+      actionPath: itemPath,
+    }
+    try {
+      parsedActions.push(entry.parse(action, parseContext))
+    } catch (e) {
+      const msg = e && e.message ? String(e.message) : String(e)
+      if (msg.includes(itemPath)) throw e
+      throw new Error(`[${itemPath}] ${msg}`)
     }
-    parsedActions.push(entry.parse(action, parseContext))
   }
   return parsedActions
 }

+ 230 - 101
package/pack-resources/static/process/GenerateNote/process.json

@@ -1,8 +1,7 @@
 {
   "name": "GenerateNote",
   "description": "生成小红书图文笔记",
-  "variables": 
-  {
+  "variables": {
     "pos": "",
     "article-prompt": "健康减脂:科学饮食与运动习惯,适合做小红书笔记",
     "article": "",
@@ -12,109 +11,180 @@
   },
   "execute": [
     {
-      "type": "echo",
-      "inVars": ["开始生成小红书图文笔记"]
-    },
-    {
-      "type": "ai",
-      "method": "text2text",
-      "inVars": ["根据以下主题写一篇小红书风格的图文稿件,要求:长文,至少 500 字,分段清晰、吸引人、适当使用 emoji、适合发笔记。只输出稿件正文,不要标题。主题:{{article-prompt}}", ""],
-      "outVars": ["{article}"]
+      "type": "fun",
+      "method": "remove-folder",
+      "inVars": [
+        "tmp"
+      ],
+      "outVars": []
     },
     {
       "type": "echo",
-      "inVars": ["开始生成小红书配图 prompt"]
+      "inVars": [
+        "开始生成小红书图文笔记"
+      ]
     },
     {
-      "type": "ai",
+      "type": "fun",
       "method": "text2text",
-      "inVars": ["根据:{article}的内容,为配图给出两个图片的 prompt(简短关键词或短语,用于搜图),不要 URL。只输出一个 JSON 数组,两个字符串,不要任何说明和 markdown。示例格式:[\"健康饮食 沙拉\",\"运动 瑜伽\"]"],
-      "outVars": ["{img-prompt-json}"]
+      "inVars": [
+        "根据以下主题写一篇小红书风格的图文稿件,要求:长文,至少 500 字,绝对不能超过900字。分段清晰、吸引人、适当使用 emoji、适合发笔记。只输出稿件正文,不要标题。主题:{{article-prompt}}",
+        ""
+      ],
+      "outVars": [
+        "{article}"
+      ]
     },
     {
-      "type": "echo",
-      "inVars": ["{img-prompt-json}"]
+      "type": "fun",
+      "method": "text2text",
+      "inVars": [
+        "根据:{article}的内容,为配图给出两个图片的 prompt(简短关键词或短语,用于搜图),不要 URL。只输出一行、完整且合法的 JSON 数组且仅包含两个字符串,不要任何说明、换行或 markdown。示例:[\"健康饮食 沙拉\",\"运动 瑜伽\"]"
+      ],
+      "outVars": [
+        "{img-prompt-json}"
+      ]
     },
     {
-      "type": "json",
+      "type": "fun",
       "method": "json-to-arr",
-      "inVars": ["{img-prompt-json}"],
-      "outVars": ["{img-prompt-arr}"]
-    },
-    {
-      "type": "echo",
-      "inVars": ["{img-prompt-arr}[0]"]
+      "inVars": [
+        "{img-prompt-json}"
+      ],
+      "outVars": [
+        "{img-prompt-arr}"
+      ]
     },
     {
       "type": "echo",
-      "inVars": ["开始下载配图"]
+      "inVars": [
+        "开始下载配图"
+      ]
     },
     {
       "type": "for",
       "indexVariable": "{idx}",
       "array": "{img-prompt-arr}",
-      "body": 
-      [
+      "body": [
+        {
+          "type": "echo",
+          "inVars": [
+            "[配图 {idx}] 尝试按关键词下载 tmp/pic{idx}.png"
+          ]
+        },
         {
           "type": "try",
-          "try": 
-          [
+          "continueAfterFail": true,
+          "try": [
+            {
+              "type": "echo",
+              "inVars": [
+                "[配图 {idx}] download-img:关键词 {img-prompt-arr}[{idx}]"
+              ]
+            },
             {
-              "type": "download-img",
-              "inVars": ["{img-prompt-arr}[{idx}]", "tmp/pic{idx}.png"],
+              "type": "fun",
+              "method": "download-img",
+              "inVars": [
+                "{img-prompt-arr}[{idx}]",
+                "tmp/pic{idx}.png"
+              ],
               "outVars": []
             }
           ],
-          "fail": 
-          [
+          "fail": [
+            {
+              "type": "echo",
+              "inVars": [
+                "[配图 {idx}] 下载失败,AI 生成备用 prompt"
+              ]
+            },
             {
-              "type": "ai",
+              "type": "fun",
               "method": "text2text",
-              "inVars": ["根据:{article}的内容,为配图给出一个图片的 prompt(简短关键词或短语),不要 URL、不要 JSON、不要引号,只输出一个 prompt。"],
-              "outVars": ["{img-prompt}"]
+              "inVars": [
+                "根据:{article}的内容,为配图给出一个图片的 prompt(简短关键词或短语),不要 URL、不要 JSON、不要引号,只输出一个 prompt。"
+              ],
+              "outVars": [
+                "{img-prompt}"
+              ]
             },
             {
-              "type": "download-img",
-              "inVars": ["{img-prompt}", "tmp/pic{idx}.png"],
+              "type": "echo",
+              "inVars": [
+                "[配图 {idx}] download-img:备用 prompt"
+              ]
+            },
+            {
+              "type": "fun",
+              "method": "download-img",
+              "inVars": [
+                "{img-prompt}",
+                "tmp/pic{idx}.png"
+              ],
               "outVars": []
             }
-          ]
+          ],
+          "success": []
         },
         {
           "type": "echo",
-          "inVars": ["pic{idx}.png 下载成功"]
+          "inVars": [
+            "[配图 {idx}] adb:发送图片到设备"
+          ]
         },
         {
-          "type": "adb",
-          "method": "send-img-to-device",
-          "inVars": ["tmp/pic{idx}.png"],
+          "type": "fun",
+          "method": "adb-send-img-to-device",
+          "inVars": [
+            "tmp/pic{idx}.png"
+          ],
           "outVars": []
         }
       ]
     },
     {
       "type": "echo",
-      "inVars": ["开始添加笔记"]
+      "inVars": [
+        "步骤:点击添加笔记"
+      ]
     },
     {
-      "type": "img-center-point-location",
-      "inVars": ["添加笔记.png", [0.2, 1.6]],
-      "outVars": ["{pos}"]
+      "type": "fun",
+      "method": "img-center-point-location",
+      "inVars": [
+        "添加笔记.png"
+      ],
+      "outVars": [
+        "{pos}"
+      ]
     },
     {
-      "type": "adb",
-      "method": "click",
-      "inVars": ["{pos}"]
+      "type": "fun",
+      "method": "persist-save",
+      "inVars": [
+        "add_note_btn_center",
+        "{pos}"
+      ],
+      "outVars": []
     },
     {
-      "type": "ocr",
-      "inVars": ["从相册选择"],
-      "outVars": ["{pos}"]
+      "type": "fun",
+      "method": "ocr",
+      "inVars": [
+        "从相册选择"
+      ],
+      "outVars": [
+        "{pos}"
+      ]
     },
     {
-      "type": "adb",
-      "method": "click",
-      "inVars": ["{pos}"]
+      "type": "fun",
+      "method": "adb-click",
+      "inVars": [
+        "{pos}"
+      ],
+      "outVars": []
     },
     {
       "type": "for",
@@ -123,82 +193,141 @@
       "body": [
         {
           "type": "echo",
-          "inVars": ["开始先择配图pic{idx}.png"]
+          "inVars": [
+            "开始选择配图 pic{idx}.png"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "img-center-point-location",
+          "inVars": [
+            "tmp/pic{idx}.png"
+          ],
+          "outVars": [
+            "{pos}"
+          ]
         },
         {
-          "type": "img-center-point-location",
-          "inVars": ["tmp/pic{idx}.png", [0.1, 2.0], [0.8, "w"]],
-          "outVars": ["{pos}"]
+          "type": "fun",
+          "method": "adb-click",
+          "inVars": [
+            "{pos}"
+          ],
+          "outVars": []
         },
         {
-          "type": "adb",
-          "method": "click",
-          "inVars": ["{pos}"]
+          "type": "delay",
+          "value": "1s"
         },
         {
-          "type": "img-center-point-location",
-          "inVars": ["选中图片.png", [0.2, 1.6]],
-          "outVars": ["{pos}"]
+          "type": "fun",
+          "method": "img-center-point-location",
+          "inVars": [
+            "选中图片.png"
+          ],
+          "outVars": [
+            "{pos}"
+          ]
         },
         {
-          "type": "adb",
-          "method": "click",
-          "inVars": ["{pos}"]
+          "type": "echo",
+          "inVars": [
+            "步骤:点击确认选中"
+          ]
         },
         {
-          "type": "adb",
-          "method": "keyevent",
-          "inVars": ["4"],
+          "type": "fun",
+          "method": "adb-click",
+          "inVars": [
+            "{pos}"
+          ],
           "outVars": []
+        },
+        {
+          "type": "delay",
+          "value": "1s"
+        },
+        {
+          "type": "fun",
+          "method": "adb-keyevent",
+          "inVars": [
+            "4"
+          ],
+          "outVars": []
+        },
+        {
+          "type": "delay",
+          "value": "1s"
         }
       ]
     },
     {
-      "type": "ocr",
-      "inVars": ["下一步"],
-      "outVars": ["{pos}"]
-    },
-    {
-      "type": "adb",
-      "method": "click",
-      "inVars": ["{pos}"]
-    },
-    {
-      "type": "ocr",
-      "inVars": ["下一步"],
-      "outVars": ["{pos}"]
+      "type": "fun",
+      "method": "ocr",
+      "inVars": [
+        "下一步"
+      ],
+      "outVars": [
+        "{pos}"
+      ]
     },
     {
-      "type": "adb",
-      "method": "click",
-      "inVars": ["{pos}"]
+      "type": "fun",
+      "method": "adb-click",
+      "inVars": [
+        "{pos}"
+      ],
+      "outVars": []
     },
     {
-      "type": "adb",
-      "method": "input",
-      "inVars": ["{article}"]
+      "type": "fun",
+      "method": "ocr",
+      "inVars": [
+        "下一步"
+      ],
+      "outVars": [
+        "{pos}"
+      ]
     },
     {
-      "type": "echo",
-      "inVars": ["开始查找并点击发布"]
+      "type": "fun",
+      "method": "adb-click",
+      "inVars": [
+        "{pos}"
+      ],
+      "outVars": []
     },
     {
-      "type": "ocr",
-      "inVars": ["发布"],
-      "outVars": ["{pos}"]
+      "type": "fun",
+      "method": "adb-input",
+      "inVars": [
+        "{article}"
+      ],
+      "outVars": []
     },
     {
-      "type": "adb",
-      "method": "click",
-      "inVars": ["{pos}"]
+      "type": "fun",
+      "method": "ocr",
+      "inVars": [
+        "发布"
+      ],
+      "outVars": [
+        "{pos}"
+      ]
     },
     {
-      "type": "echo",
-      "inVars": ["已点击发布"]
+      "type": "fun",
+      "method": "adb-click",
+      "inVars": [
+        "{pos}"
+      ],
+      "outVars": []
     },
     {
       "type": "echo",
-      "inVars": ["流程结束"]
+      "inVars": [
+        "流程结束"
+      ]
     }
   ]
 }

+ 36 - 22
package/pack-resources/static/process/RedNoteAIThumbsUp/process.json

@@ -1,36 +1,50 @@
 {
   "name": "RedNoteAIThumbsUp",
   "description": "小红书自动点赞",
-  "variables": { "sendBtnPos": "" },
-  "execute": 
-  [
+  "variables": {
+    "sendBtnPos": ""
+  },
+  "execute": [
     {
       "type": "try",
-      "try": 
-	    [
+      "try": [
         {
           "type": "img-center-point-location",
-          "inVars": ["视频点赞.png", [0.2, 1.6]],
-          "outVars": ["{sendBtnPos}"]
+          "inVars": [
+            "视频点赞.png",
+            [
+              0.2,
+              1.6
+            ]
+          ],
+          "outVars": [
+            "{sendBtnPos}"
+          ]
         }
       ],
-      "success": 
-	    [
-        
-      ],
-      "fail": 
-	    [
+      "success": [],
+      "fail": [
         {
-			    "type": "img-center-point-location",
-			    "inVars": ["图文点赞.png", [0.2, 1.6]],
-			    "outVars": ["{sendBtnPos}"]
-		    }
+          "type": "img-center-point-location",
+          "inVars": [
+            "图文点赞.png",
+            [
+              0.2,
+              1.6
+            ]
+          ],
+          "outVars": [
+            "{sendBtnPos}"
+          ]
+        }
       ]
     },
-    { 
-		  "type": "adb", 
-		  "method": "click", 
-		  "inVars": ["{sendBtnPos}"] 
-	  }
+    {
+      "type": "adb",
+      "method": "click",
+      "inVars": [
+        "{sendBtnPos}"
+      ]
+    }
   ]
 }

+ 196 - 150
package/pack-resources/static/process/RedNoteBrowsingAndThumbsUp/process.json

@@ -1,152 +1,198 @@
 {
-	"name": "RedNoteBrowsingAndThumbsUp",
-        "description": "小红书随机浏览和点赞",
-	"variables": {
-		"page-index": 0,
-		"up-or-down": 0,
-		"swipe-count": 0,
-		"click-x": 0,
-		"click-y": 0,
-		"random-click-pos": "",
-		"stay-duration": 0,
-		"send-btn-pos": "",
-		"b-like-click": 0
-	},
-	"execute": 
-	[
-		{
-			"type": "while",
-			"condition": "1",
-			"body": 
-			[
-				{
-					"type": "random",
-					"inVars": ["1", "3"],
-					"outVars": ["{swipe-count}"]
-				},
-				{
-					"type": "schedule",
-					"condition": 
-					{
-						"interval": "7s",
-						"repeat": "{swipe-count}"
-					},
-					"interval": 
-					[
-						{
-							"type": "adb",
-							"method": "swipe",
-							"inVars": ["up-down"],
-							"outVars": []
-						}
-					]
-				},
-				{
-					"type": "random",
-					"inVars": ["200", "780"],
-					"outVars": ["{click-x}"]
-				},
-				{
-					"type": "random",
-					"inVars": ["400", "1500"],
-					"outVars": ["{click-y}"]
-				},
-				{
-					"type": "set",
-					"variable": "{random-click-pos}",
-					"value": "{click-x},{click-y}"
-				},
-				{
-					
-					"type": "adb",
-					"method": "click",
-					"inVars": ["{random-click-pos}"]
-				},
-				{
-					"type": "random",
-					"inVars": [10, 2000],
-					"outVars": ["{stay-duration}"]
-				},
-				{
-					"type": "try",
-					"try": 
-					[
-						{
-							"type": "img-center-point-location",
-							"inVars": ["视频点赞.png", [0.2, 1.6]],
-							"outVars": ["{send-btn-pos}"]
-						}
-					],
-					"success": 
-					[
-						{
-							"type": "random",
-							"inVars": ["2", "10"],
-							"outVars": ["{swipe-count}"]
-						},
-						{
-							"type": "for",
-							"times": "{swipe-count}",
-							"variable": "{i}",
-							"body": 
-							[
-								{
-									"type": "random",
-									"inVars": [5,30],
-									"outVars": ["{stay-duration}"]
-								},
-								{
-									"type": "delay",
-									"value": "{{stay-duration}}s"
-								},
-								{
-									"type": "adb",
-									"method": "swipe",
-									"inVars": ["down-up"],
-									"outVars": []
-								},
-								{
-									"type": "if",
-									"condition": "{stay-duration} > 25",
-									"ture": 
-									[
-										{
-											"type": "random",
-											"inVars": [0, 1],
-											"outVars": ["{b-like-click}"]
-										},
-										{
-											"type": "if",
-											"condition": "{b-like-click} == 1",
-											"ture": 
-											[
-												{
-													"type": "adb",
-													"method": "click",
-													"inVars": ["{send-btn-pos}"]
-												}
-											]
-										}
-									]
-								}	
-							]
-						}
-					],
-					"fail": 
-					[
-						{
-							"type": "echo",
-							"value": "点赞图标匹配失败"
-						}
-					]
-			    },
-				{
-					"type": "adb",
-					"method": "keyevent",
-					"inVars": ["4"],
-					"outVars": []
-				}
-			]
-		}		
-	]
+  "name": "RedNoteBrowsingAndThumbsUp",
+  "description": "小红书随机浏览和点赞",
+  "variables": {
+    "page-index": 0,
+    "up-or-down": 0,
+    "swipe-count": 0,
+    "click-x": 0,
+    "click-y": 0,
+    "random-click-pos": "",
+    "stay-duration": 0,
+    "send-btn-pos": "",
+    "b-like-click": 0
+  },
+  "execute": [
+    {
+      "type": "while",
+      "condition": "1",
+      "body": [
+        {
+          "type": "random",
+          "inVars": [
+            "1",
+            "3"
+          ],
+          "outVars": [
+            "{swipe-count}"
+          ]
+        },
+        {
+          "type": "schedule",
+          "condition": {
+            "interval": "7s",
+            "repeat": "{swipe-count}"
+          },
+          "interval": [
+            {
+              "type": "adb",
+              "method": "swipe",
+              "inVars": [
+                "up-down"
+              ],
+              "outVars": []
+            }
+          ]
+        },
+        {
+          "type": "random",
+          "inVars": [
+            "200",
+            "780"
+          ],
+          "outVars": [
+            "{click-x}"
+          ]
+        },
+        {
+          "type": "random",
+          "inVars": [
+            "400",
+            "1500"
+          ],
+          "outVars": [
+            "{click-y}"
+          ]
+        },
+        {
+          "type": "set",
+          "variable": "{random-click-pos}",
+          "value": "{click-x},{click-y}"
+        },
+        {
+          "type": "adb",
+          "method": "click",
+          "inVars": [
+            "{random-click-pos}"
+          ]
+        },
+        {
+          "type": "random",
+          "inVars": [
+            10,
+            2000
+          ],
+          "outVars": [
+            "{stay-duration}"
+          ]
+        },
+        {
+          "type": "try",
+          "try": [
+            {
+              "type": "img-center-point-location",
+              "inVars": [
+                "视频点赞.png",
+                [
+                  0.2,
+                  1.6
+                ]
+              ],
+              "outVars": [
+                "{send-btn-pos}"
+              ]
+            }
+          ],
+          "success": [
+            {
+              "type": "random",
+              "inVars": [
+                "2",
+                "10"
+              ],
+              "outVars": [
+                "{swipe-count}"
+              ]
+            },
+            {
+              "type": "for",
+              "times": "{swipe-count}",
+              "variable": "{i}",
+              "body": [
+                {
+                  "type": "random",
+                  "inVars": [
+                    5,
+                    30
+                  ],
+                  "outVars": [
+                    "{stay-duration}"
+                  ]
+                },
+                {
+                  "type": "delay",
+                  "value": "{{stay-duration}}s"
+                },
+                {
+                  "type": "adb",
+                  "method": "swipe",
+                  "inVars": [
+                    "down-up"
+                  ],
+                  "outVars": []
+                },
+                {
+                  "type": "if",
+                  "condition": "{stay-duration} > 25",
+                  "then": [
+                    {
+                      "type": "random",
+                      "inVars": [
+                        0,
+                        1
+                      ],
+                      "outVars": [
+                        "{b-like-click}"
+                      ]
+                    },
+                    {
+                      "type": "if",
+                      "condition": "{b-like-click} == 1",
+                      "then": [
+                        {
+                          "type": "adb",
+                          "method": "click",
+                          "inVars": [
+                            "{send-btn-pos}"
+                          ]
+                        }
+                      ],
+                      "else": []
+                    }
+                  ],
+                  "else": []
+                }
+              ]
+            }
+          ],
+          "fail": [
+            {
+              "type": "echo",
+              "inVars": [
+                "点赞图标匹配失败"
+              ]
+            }
+          ]
+        },
+        {
+          "type": "adb",
+          "method": "keyevent",
+          "inVars": [
+            "4"
+          ],
+          "outVars": []
+        }
+      ]
+    }
+  ]
 }

+ 21 - 24
package/pack-resources/static/process/Test/process.json

@@ -1,27 +1,24 @@
 {
-    "name": "Test",
-    "description": "测试",
-    "variables": [
-      
-    ],
-    "execute": 
-    [
+  "name": "Test",
+  "description": "测试",
+  "variables": {},
+  "execute": [
+    {
+      "type": "schedule",
+      "condition": {
+        "interval": "1s",
+        "repeat": -1
+      },
+      "interval": [
         {
-            "type": "schedule",
-			"condition": 
-			{
-				"interval": "1s",
-				"repeat": -1
-			},
-			"interval": 
-			[
-				{
-                    "type": "adb",
-                    "method": "swipe",
-                    "inVars": ["right-left"],
-                    "outVars": []
-                }
-            ]
+          "type": "adb",
+          "method": "swipe",
+          "inVars": [
+            "right-left"
+          ],
+          "outVars": []
         }
-    ]
-}
+      ]
+    }
+  ]
+}

+ 4 - 1
static/process/CreateNote/process.json

@@ -8,7 +8,10 @@
     {
       "type": "fun",
       "method": "adb-input",
-      "inVars": ["创建小红书笔记"]
+      "inVars": [
+        "创建小红书笔记"
+      ],
+      "outVars": []
     }
   ]
 }

+ 371 - 144
static/process/GenerateNote/process.json

@@ -2,7 +2,13 @@
   "name": "GenerateNote",
   "description": "生成小红书图文笔记",
   "variables": {
-    "pos": "",
+    "add_note_img_center_pos": "",
+    "pic_thumb_img_center_pos": "",
+    "select_pic_img_center_pos": "",
+    "ocr_pos_pick_from_album": "",
+    "ocr_pos_next_1": "",
+    "ocr_pos_next_2": "",
+    "ocr_pos_publish": "",
     "article-prompt": "健康减脂:科学饮食与运动习惯,适合做小红书笔记",
     "article": "",
     "img-prompt-json": "",
@@ -13,63 +19,113 @@
     {
       "type": "fun",
       "method": "remove-folder",
-      "path": "tmp",
+      "inVars": [
+        "tmp"
+      ],
       "outVars": []
     },
     {
-      "type": "echo",
-      "value": "开始生成小红书图文笔记"
+      "type": "fun",
+      "method": "persist-read",
+      "inVars": [
+        "add_note_img_center_pos"
+      ],
+      "outVars": [
+        "{add_note_img_center_pos}"
+      ]
     },
     {
       "type": "fun",
-      "method": "text2text",
+      "method": "persist-read",
       "inVars": [
-        "根据以下主题写一篇小红书风格的图文稿件,要求:长文,至少 500 字,绝对不能超过900字。分段清晰、吸引人、适当使用 emoji、适合发笔记。只输出稿件正文,不要标题。主题:{{article-prompt}}",
-        ""
+        "ocr_pos_pick_from_album"
       ],
-      "outVars": ["{article}"]
+      "outVars": [
+        "{ocr_pos_pick_from_album}"
+      ]
     },
     {
-      "type": "echo",
-      "value": "开始生成小红书配图 prompt"
+      "type": "fun",
+      "method": "persist-read",
+      "inVars": [
+        "select_pic_img_center_pos"
+      ],
+      "outVars": [
+        "{select_pic_img_center_pos}"
+      ]
     },
     {
-      "type": "echo",
-      "value": "步骤:AI 根据文章生成配图关键词 JSON"
+      "type": "fun",
+      "method": "persist-read",
+      "inVars": [
+        "ocr_pos_next_1"
+      ],
+      "outVars": [
+        "{ocr_pos_next_1}"
+      ]
     },
     {
       "type": "fun",
-      "method": "text2text",
+      "method": "persist-read",
       "inVars": [
-        "根据:{article}的内容,为配图给出两个图片的 prompt(简短关键词或短语,用于搜图),不要 URL。只输出一行、完整且合法的 JSON 数组且仅包含两个字符串,不要任何说明、换行或 markdown。示例:[\"健康饮食 沙拉\",\"运动 瑜伽\"]"
+        "ocr_pos_next_2"
       ],
-      "outVars": ["{img-prompt-json}"]
+      "outVars": [
+        "{ocr_pos_next_2}"
+      ]
     },
     {
-      "type": "echo",
-      "value": "配图 prompt JSON:{{img-prompt-json}}"
+      "type": "fun",
+      "method": "persist-read",
+      "inVars": [
+        "ocr_pos_publish"
+      ],
+      "outVars": [
+        "{ocr_pos_publish}"
+      ]
     },
     {
       "type": "echo",
-      "value": "步骤:将 JSON 解析为配图关键词数组"
+      "inVars": [
+        "开始生成小红书图文笔记"
+      ]
     },
     {
       "type": "fun",
-      "method": "json-to-arr",
-      "inVars": ["{img-prompt-json}"],
-      "outVars": ["{img-prompt-arr}"]
+      "method": "text2text",
+      "inVars": [
+        "根据以下主题写一篇小红书风格的图文稿件,要求:长文,至少 500 字,绝对不能超过900字。分段清晰、吸引人、适当使用 emoji、适合发笔记。只输出稿件正文,不要标题。主题:{{article-prompt}}",
+        ""
+      ],
+      "outVars": [
+        "{article}"
+      ]
     },
     {
-      "type": "echo",
-      "value": "配图数组首项:{img-prompt-arr}[0]"
+      "type": "fun",
+      "method": "text2text",
+      "inVars": [
+        "根据:{article}的内容,为配图给出两个搜图用关键词(短语即可)。严格只输出一行 JSON 数组,元素恰好 2 个字符串,除此之外不要任何字符。规则:只用英文半角双引号 \" 包裹每个元素,元素内不要用双引号;不要用中文引号「」或 “”;不要 markdown、不要代码块、不要换行。示例:[\"健康饮食 沙拉\",\"运动 瑜伽\"]"
+      ],
+      "outVars": [
+        "{img-prompt-json}"
+      ]
     },
     {
-      "type": "echo",
-      "value": "开始下载配图"
+      "type": "fun",
+      "method": "json-to-arr",
+      "inVars": [
+        "{img-prompt-json}"
+      ],
+      "outVars": [
+        "{img-prompt-arr}"
+      ]
     },
     {
       "type": "echo",
-      "value": "步骤:循环下载每张配图并发送到设备"
+      "inVars": [
+        "开始下载配图"
+      ]
     },
     {
       "type": "for",
@@ -78,7 +134,9 @@
       "body": [
         {
           "type": "echo",
-          "value": "[配图 {idx}] 尝试按关键词下载 tmp/pic{idx}.png"
+          "inVars": [
+            "[配图 {idx}] 尝试按关键词下载 tmp/pic{idx}.png"
+          ]
         },
         {
           "type": "try",
@@ -86,19 +144,26 @@
           "try": [
             {
               "type": "echo",
-              "value": "[配图 {idx}] download-img:关键词 {img-prompt-arr}[{idx}]"
+              "inVars": [
+                "[配图 {idx}] download-img:关键词 {img-prompt-arr}[{idx}]"
+              ]
             },
             {
               "type": "fun",
               "method": "download-img",
-              "inVars": ["{img-prompt-arr}[{idx}]", "tmp/pic{idx}.png"],
+              "inVars": [
+                "{img-prompt-arr}[{idx}]",
+                "tmp/pic{idx}.png"
+              ],
               "outVars": []
             }
           ],
           "fail": [
             {
               "type": "echo",
-              "value": "[配图 {idx}] 下载失败,AI 生成备用 prompt"
+              "inVars": [
+                "[配图 {idx}] 下载失败,AI 生成备用 prompt"
+              ]
             },
             {
               "type": "fun",
@@ -106,103 +171,189 @@
               "inVars": [
                 "根据:{article}的内容,为配图给出一个图片的 prompt(简短关键词或短语),不要 URL、不要 JSON、不要引号,只输出一个 prompt。"
               ],
-              "outVars": ["{img-prompt}"]
+              "outVars": [
+                "{img-prompt}"
+              ]
             },
             {
               "type": "echo",
-              "value": "[配图 {idx}] download-img:备用 prompt"
+              "inVars": [
+                "[配图 {idx}] download-img:备用 prompt"
+              ]
             },
             {
               "type": "fun",
               "method": "download-img",
-              "inVars": ["{img-prompt}", "tmp/pic{idx}.png"],
+              "inVars": [
+                "{img-prompt}",
+                "tmp/pic{idx}.png"
+              ],
               "outVars": []
             }
-          ]
-        },
-        {
-          "type": "echo",
-          "value": "pic{idx}.png 下载成功"
+          ],
+          "success": []
         },
         {
           "type": "echo",
-          "value": "[配图 {idx}] adb:发送图片到设备"
+          "inVars": [
+            "[配图 {idx}] adb:发送图片到设备"
+          ]
         },
         {
           "type": "fun",
           "method": "adb-send-img-to-device",
-          "inVars": ["tmp/pic{idx}.png"],
+          "inVars": [
+            "tmp/pic{idx}.png"
+          ],
           "outVars": []
         }
       ]
     },
     {
       "type": "echo",
-      "value": "步骤:图匹配定位「添加笔记」按钮(仅匹配一次,失败则整流程停止)"
-    },
-    {
-      "type": "fun",
-      "method": "img-center-point-location",
-      "inVars": ["添加笔记.png"],
-      "outVars": ["{pos}"]
+      "inVars": [
+        "步骤:点击添加笔记"
+      ]
     },
     {
-      "type": "echo",
-      "value": "步骤:点击添加笔记"
+      "type": "if",
+      "condition": "{add_note_img_center_pos} == ''",
+      "then": [
+        {
+          "type": "echo",
+          "inVars": [
+            "步骤:无缓存,ai计算图片中心点"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "img-center-point-location",
+          "inVars": [
+            "添加笔记.png"
+          ],
+          "outVars": [
+            "{add_note_img_center_pos}"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "persist-save",
+          "inVars": [
+            "add_note_img_center_pos",
+            "{add_note_img_center_pos}"
+          ],
+          "outVars": []
+        }
+      ],
+      "else": []
     },
     {
       "type": "fun",
       "method": "adb-click",
-      "inVars": ["{pos}"],
+      "inVars": [
+        "{add_note_img_center_pos}"
+      ],
       "outVars": []
     },
     {
-      "type": "echo",
-      "value": "步骤:OCR 查找「从相册选择」"
-    },
-    {
-      "type": "fun",
-      "method": "ocr",
-      "inVars": ["从相册选择"],
-      "outVars": ["{pos}"]
-    },
-    {
-      "type": "echo",
-      "value": "步骤:点击从相册选择"
+      "type": "if",
+      "condition": "{ocr_pos_pick_from_album} == ''",
+      "then": [
+        {
+          "type": "echo",
+          "inVars": [
+            "步骤:无缓存,OCR 识别「从相册选择」"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "ocr",
+          "inVars": [
+            "从相册选择"
+          ],
+          "outVars": [
+            "{ocr_pos_pick_from_album}"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "persist-save",
+          "inVars": [
+            "ocr_pos_pick_from_album",
+            "{ocr_pos_pick_from_album}"
+          ],
+          "outVars": []
+        }
+      ],
+      "else": []
     },
     {
       "type": "fun",
       "method": "adb-click",
-      "inVars": ["{pos}"],
+      "inVars": [
+        "{ocr_pos_pick_from_album}"
+      ],
       "outVars": []
     },
-    {
-      "type": "echo",
-      "value": "步骤:循环在相册中选择每张配图"
-    },
     {
       "type": "for",
       "indexVariable": "{idx}",
       "array": "{img-prompt-arr}",
       "body": [
-        {
-          "type": "echo",
-          "value": "开始选择配图 pic{idx}.png"
-        },
         {
           "type": "fun",
-          "method": "img-center-point-location",
-          "inVars": ["tmp/pic{idx}.png"],
-          "outVars": ["{pos}"]
+          "method": "persist-read",
+          "inVars": [
+            "img_center_tmp_pic_{idx}"
+          ],
+          "outVars": [
+            "{pic_thumb_img_center_pos}"
+          ]
         },
         {
           "type": "echo",
-          "value": "步骤:点击配图缩略图"
+          "inVars": [
+            "开始选择配图 pic{idx}.png"
+          ]
+        },
+        {
+          "type": "if",
+          "condition": "{pic_thumb_img_center_pos} == ''",
+          "then": [
+            {
+              "type": "echo",
+              "inVars": [
+                "[配图 {idx}] 无缓存,识图计算缩略图中心"
+              ]
+            },
+            {
+              "type": "fun",
+              "method": "img-center-point-location",
+              "inVars": [
+                "tmp/pic{idx}.png"
+              ],
+              "outVars": [
+                "{pic_thumb_img_center_pos}"
+              ]
+            },
+            {
+              "type": "fun",
+              "method": "persist-save",
+              "inVars": [
+                "img_center_tmp_pic_{idx}",
+                "{pic_thumb_img_center_pos}"
+              ],
+              "outVars": []
+            }
+          ],
+          "else": []
         },
         {
           "type": "fun",
           "method": "adb-click",
-          "inVars": ["{pos}"],
+          "inVars": [
+            "{pic_thumb_img_center_pos}"
+          ],
           "outVars": []
         },
         {
@@ -210,37 +361,61 @@
           "value": "1s"
         },
         {
-          "type": "echo",
-          "value": "步骤:图匹配定位「选中图片」"
-        },
-        {
-          "type": "fun",
-          "method": "img-center-point-location",
-          "inVars": ["选中图片.png"],
-          "outVars": ["{pos}"]
+          "type": "if",
+          "condition": "{select_pic_img_center_pos} == ''",
+          "then": [
+            {
+              "type": "echo",
+              "inVars": [
+                "[配图 {idx}] 无缓存,识图计算「选中图片」中心"
+              ]
+            },
+            {
+              "type": "fun",
+              "method": "img-center-point-location",
+              "inVars": [
+                "选中图片.png"
+              ],
+              "outVars": [
+                "{select_pic_img_center_pos}"
+              ]
+            },
+            {
+              "type": "fun",
+              "method": "persist-save",
+              "inVars": [
+                "select_pic_img_center_pos",
+                "{select_pic_img_center_pos}"
+              ],
+              "outVars": []
+            }
+          ],
+          "else": []
         },
         {
           "type": "echo",
-          "value": "步骤:点击确认选中"
+          "inVars": [
+            "步骤:点击确认选中"
+          ]
         },
         {
           "type": "fun",
           "method": "adb-click",
-          "inVars": ["{pos}"],
+          "inVars": [
+            "{select_pic_img_center_pos}"
+          ],
           "outVars": []
         },
         {
           "type": "delay",
           "value": "1s"
         },
-        {
-          "type": "echo",
-          "value": "步骤:返回键(keyevent 4)"
-        },
         {
           "type": "fun",
           "method": "adb-keyevent",
-          "inVars": ["4"],
+          "inVars": [
+            "4"
+          ],
           "outVars": []
         },
         {
@@ -250,86 +425,138 @@
       ]
     },
     {
-      "type": "echo",
-      "value": "步骤:OCR 查找第一次「下一步」"
-    },
-    {
-      "type": "fun",
-      "method": "ocr",
-      "inVars": ["下一步"],
-      "outVars": ["{pos}"]
-    },
-    {
-      "type": "echo",
-      "value": "步骤:点击第一次下一步"
+      "type": "if",
+      "condition": "{ocr_pos_next_1} == ''",
+      "then": [
+        {
+          "type": "echo",
+          "inVars": [
+            "步骤:无缓存,OCR 识别「下一步」(1)"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "ocr",
+          "inVars": [
+            "下一步"
+          ],
+          "outVars": [
+            "{ocr_pos_next_1}"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "persist-save",
+          "inVars": [
+            "ocr_pos_next_1",
+            "{ocr_pos_next_1}"
+          ],
+          "outVars": []
+        }
+      ],
+      "else": []
     },
     {
       "type": "fun",
       "method": "adb-click",
-      "inVars": ["{pos}"],
+      "inVars": [
+        "{ocr_pos_next_1}"
+      ],
       "outVars": []
     },
     {
-      "type": "echo",
-      "value": "步骤:OCR 查找第二次「下一步」"
-    },
-    {
-      "type": "fun",
-      "method": "ocr",
-      "inVars": ["下一步"],
-      "outVars": ["{pos}"]
-    },
-    {
-      "type": "echo",
-      "value": "步骤:点击第二次下一步"
+      "type": "if",
+      "condition": "{ocr_pos_next_2} == ''",
+      "then": [
+        {
+          "type": "echo",
+          "inVars": [
+            "步骤:无缓存,OCR 识别「下一步」(2)"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "ocr",
+          "inVars": [
+            "下一步"
+          ],
+          "outVars": [
+            "{ocr_pos_next_2}"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "persist-save",
+          "inVars": [
+            "ocr_pos_next_2",
+            "{ocr_pos_next_2}"
+          ],
+          "outVars": []
+        }
+      ],
+      "else": []
     },
     {
       "type": "fun",
       "method": "adb-click",
-      "inVars": ["{pos}"],
+      "inVars": [
+        "{ocr_pos_next_2}"
+      ],
       "outVars": []
     },
-    {
-      "type": "echo",
-      "value": "步骤:adb 输入文章正文"
-    },
     {
       "type": "fun",
       "method": "adb-input",
-      "inVars": ["{article}"],
+      "inVars": [
+        "{article}"
+      ],
       "outVars": []
     },
     {
-      "type": "echo",
-      "value": "开始查找并点击发布"
-    },
-    {
-      "type": "echo",
-      "value": "步骤:OCR 查找「发布」"
-    },
-    {
-      "type": "fun",
-      "method": "ocr",
-      "inVars": ["发布"],
-      "outVars": ["{pos}"]
-    },
-    {
-      "type": "echo",
-      "value": "步骤:点击发布"
+      "type": "if",
+      "condition": "{ocr_pos_publish} == ''",
+      "then": [
+        {
+          "type": "echo",
+          "inVars": [
+            "步骤:无缓存,OCR 识别「发布」"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "ocr",
+          "inVars": [
+            "发布"
+          ],
+          "outVars": [
+            "{ocr_pos_publish}"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "persist-save",
+          "inVars": [
+            "ocr_pos_publish",
+            "{ocr_pos_publish}"
+          ],
+          "outVars": []
+        }
+      ],
+      "else": []
     },
     {
       "type": "fun",
       "method": "adb-click",
-      "inVars": ["{pos}"],
+      "inVars": [
+        "{ocr_pos_publish}"
+      ],
       "outVars": []
     },
     {
       "type": "echo",
-      "value": "已点击发布"
-    },
-    {
-      "type": "echo",
-      "value": "流程结束"
+      "inVars": [
+        "流程结束"
+      ]
     }
   ]
 }

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

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

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


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


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


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


+ 10 - 0
static/process/GenerateNote/save/config.json

@@ -0,0 +1,10 @@
+{
+  "add_note_img_center_pos": "{\"x\":539,\"y\":2230}",
+  "ocr_pos_pick_from_album": "{\"x\":541,\"y\":1617}",
+  "img_center_tmp_pic_0": "{\"x\":538,\"y\":578}",
+  "select_pic_img_center_pos": "{\"x\":874,\"y\":127}",
+  "img_center_tmp_pic_1": "{\"x\":177,\"y\":569}",
+  "ocr_pos_next_1": "{\"x\":796,\"y\":2193}",
+  "ocr_pos_next_2": "{\"x\":911,\"y\":2191}",
+  "ocr_pos_publish": "{\"x\":955,\"y\":181}"
+}

+ 0 - 32
static/process/GenerateNote/tmp/img-center-1774173318186/openai_raw.json

@@ -1,32 +0,0 @@
-{
-  "id": "chatcmpl-DM9ot0Yk182DKoaEsUnXMLt35nkf9",
-  "choices": [
-    {
-      "index": 0,
-      "message": {
-        "role": "assistant",
-        "content": "{\"center_rx\":0.499,\"center_ry\":0.927}",
-        "annotations": []
-      },
-      "logprobs": null,
-      "finish_reason": "stop"
-    }
-  ],
-  "created": 1774173335,
-  "model": "gpt-5.4-2026-03-05-short-api-ev3",
-  "object": "chat.completion",
-  "usage": {
-    "prompt_tokens": 1856,
-    "completion_tokens": 20,
-    "total_tokens": 1876,
-    "completion_tokens_details": {
-      "audio_tokens": 0,
-      "reasoning_tokens": 0
-    },
-    "prompt_tokens_details": {
-      "audio_tokens": 0,
-      "cached_tokens": 0
-    }
-  },
-  "system_fingerprint": null
-}

+ 0 - 38
static/process/GenerateNote/tmp/img-center-1774173318186/openai_raw_attempt_0.json

@@ -1,38 +0,0 @@
-{
-  "model": "gpt-5.4",
-  "attemptIndex": 0,
-  "httpSuccess": true,
-  "httpError": null,
-  "response": {
-    "id": "chatcmpl-DM9ot0Yk182DKoaEsUnXMLt35nkf9",
-    "choices": [
-      {
-        "index": 0,
-        "message": {
-          "role": "assistant",
-          "content": "{\"center_rx\":0.499,\"center_ry\":0.927}",
-          "annotations": []
-        },
-        "logprobs": null,
-        "finish_reason": "stop"
-      }
-    ],
-    "created": 1774173335,
-    "model": "gpt-5.4-2026-03-05-short-api-ev3",
-    "object": "chat.completion",
-    "usage": {
-      "prompt_tokens": 1856,
-      "completion_tokens": 20,
-      "total_tokens": 1876,
-      "completion_tokens_details": {
-        "audio_tokens": 0,
-        "reasoning_tokens": 0
-      },
-      "prompt_tokens_details": {
-        "audio_tokens": 0,
-        "cached_tokens": 0
-      }
-    },
-    "system_fingerprint": null
-  }
-}

BIN
static/process/GenerateNote/tmp/img-center-1774173318186/screenshot.png


BIN
static/process/GenerateNote/tmp/img-center-1774173318186/template.png


+ 0 - 17
static/process/GenerateNote/tmp/img-center-1774173318186/vlm_center_model_attempts.json

@@ -1,17 +0,0 @@
-{
-  "model_chain": [
-    "gpt-5.4",
-    "claude-opus-4-6",
-    "gemini-3.1-pro-preview"
-  ],
-  "success_model": "gpt-5.4",
-  "attempts": [
-    {
-      "index": 0,
-      "model": "gpt-5.4",
-      "requestOk": true,
-      "error": null,
-      "hasValidCenter": true
-    }
-  ]
-}

+ 0 - 4
static/process/GenerateNote/tmp/img-center-1774173318186/vlm_center_parsed.json

@@ -1,4 +0,0 @@
-{
-  "center_rx": 0.499,
-  "center_ry": 0.927
-}

+ 0 - 10
static/process/GenerateNote/tmp/img-center-1774173318186/vlm_center_result.json

@@ -1,10 +0,0 @@
-{
-  "center_rx": 0.499,
-  "center_ry": 0.927,
-  "pixel_x": 539,
-  "pixel_y": 2225,
-  "screenshot_width": 1080,
-  "screenshot_height": 2400,
-  "marked_screenshot": null,
-  "marked_screenshot_error": "未找到 C:\\Users\\liuyu\\Desktop\\test\\AndroidRemoteController\\python\\scripts\\img-center-mark-center.py"
-}

+ 0 - 32
static/process/GenerateNote/tmp/img-center-1774173370331/openai_raw.json

@@ -1,32 +0,0 @@
-{
-  "id": "chatcmpl-DM9pyKUl4rGl59oluMRjbGyGI6R4y",
-  "choices": [
-    {
-      "index": 0,
-      "message": {
-        "role": "assistant",
-        "content": "{\"center_rx\":0.499,\"center_ry\":0.260}",
-        "annotations": []
-      },
-      "logprobs": null,
-      "finish_reason": "stop"
-    }
-  ],
-  "created": 1774173402,
-  "model": "gpt-5.4-2026-03-05-short-api-ev3",
-  "object": "chat.completion",
-  "usage": {
-    "prompt_tokens": 2694,
-    "completion_tokens": 20,
-    "total_tokens": 2714,
-    "completion_tokens_details": {
-      "audio_tokens": 0,
-      "reasoning_tokens": 0
-    },
-    "prompt_tokens_details": {
-      "audio_tokens": 0,
-      "cached_tokens": 0
-    }
-  },
-  "system_fingerprint": null
-}

+ 0 - 38
static/process/GenerateNote/tmp/img-center-1774173370331/openai_raw_attempt_0.json

@@ -1,38 +0,0 @@
-{
-  "model": "gpt-5.4",
-  "attemptIndex": 0,
-  "httpSuccess": true,
-  "httpError": null,
-  "response": {
-    "id": "chatcmpl-DM9pyKUl4rGl59oluMRjbGyGI6R4y",
-    "choices": [
-      {
-        "index": 0,
-        "message": {
-          "role": "assistant",
-          "content": "{\"center_rx\":0.499,\"center_ry\":0.260}",
-          "annotations": []
-        },
-        "logprobs": null,
-        "finish_reason": "stop"
-      }
-    ],
-    "created": 1774173402,
-    "model": "gpt-5.4-2026-03-05-short-api-ev3",
-    "object": "chat.completion",
-    "usage": {
-      "prompt_tokens": 2694,
-      "completion_tokens": 20,
-      "total_tokens": 2714,
-      "completion_tokens_details": {
-        "audio_tokens": 0,
-        "reasoning_tokens": 0
-      },
-      "prompt_tokens_details": {
-        "audio_tokens": 0,
-        "cached_tokens": 0
-      }
-    },
-    "system_fingerprint": null
-  }
-}

BIN
static/process/GenerateNote/tmp/img-center-1774173370331/screenshot.png


BIN
static/process/GenerateNote/tmp/img-center-1774173370331/template.png


+ 0 - 17
static/process/GenerateNote/tmp/img-center-1774173370331/vlm_center_model_attempts.json

@@ -1,17 +0,0 @@
-{
-  "model_chain": [
-    "gpt-5.4",
-    "claude-opus-4-6",
-    "gemini-3.1-pro-preview"
-  ],
-  "success_model": "gpt-5.4",
-  "attempts": [
-    {
-      "index": 0,
-      "model": "gpt-5.4",
-      "requestOk": true,
-      "error": null,
-      "hasValidCenter": true
-    }
-  ]
-}

+ 0 - 4
static/process/GenerateNote/tmp/img-center-1774173370331/vlm_center_parsed.json

@@ -1,4 +0,0 @@
-{
-  "center_rx": 0.499,
-  "center_ry": 0.26
-}

+ 0 - 10
static/process/GenerateNote/tmp/img-center-1774173370331/vlm_center_result.json

@@ -1,10 +0,0 @@
-{
-  "center_rx": 0.499,
-  "center_ry": 0.26,
-  "pixel_x": 539,
-  "pixel_y": 624,
-  "screenshot_width": 1080,
-  "screenshot_height": 2400,
-  "marked_screenshot": null,
-  "marked_screenshot_error": "未找到 C:\\Users\\liuyu\\Desktop\\test\\AndroidRemoteController\\python\\scripts\\img-center-mark-center.py"
-}

+ 0 - 32
static/process/GenerateNote/tmp/img-center-1774173408104/openai_raw.json

@@ -1,32 +0,0 @@
-{
-  "id": "chatcmpl-DM9qKoB9kYx1ALy7m03zt1FlyLopW",
-  "choices": [
-    {
-      "index": 0,
-      "message": {
-        "role": "assistant",
-        "content": "{\"center_rx\":0.840,\"center_ry\":0.070}",
-        "annotations": []
-      },
-      "logprobs": null,
-      "finish_reason": "stop"
-    }
-  ],
-  "created": 1774173424,
-  "model": "gpt-5.4-2026-03-05-short-api-ev3",
-  "object": "chat.completion",
-  "usage": {
-    "prompt_tokens": 1845,
-    "completion_tokens": 20,
-    "total_tokens": 1865,
-    "completion_tokens_details": {
-      "audio_tokens": 0,
-      "reasoning_tokens": 0
-    },
-    "prompt_tokens_details": {
-      "audio_tokens": 0,
-      "cached_tokens": 0
-    }
-  },
-  "system_fingerprint": null
-}

+ 0 - 38
static/process/GenerateNote/tmp/img-center-1774173408104/openai_raw_attempt_0.json

@@ -1,38 +0,0 @@
-{
-  "model": "gpt-5.4",
-  "attemptIndex": 0,
-  "httpSuccess": true,
-  "httpError": null,
-  "response": {
-    "id": "chatcmpl-DM9qKoB9kYx1ALy7m03zt1FlyLopW",
-    "choices": [
-      {
-        "index": 0,
-        "message": {
-          "role": "assistant",
-          "content": "{\"center_rx\":0.840,\"center_ry\":0.070}",
-          "annotations": []
-        },
-        "logprobs": null,
-        "finish_reason": "stop"
-      }
-    ],
-    "created": 1774173424,
-    "model": "gpt-5.4-2026-03-05-short-api-ev3",
-    "object": "chat.completion",
-    "usage": {
-      "prompt_tokens": 1845,
-      "completion_tokens": 20,
-      "total_tokens": 1865,
-      "completion_tokens_details": {
-        "audio_tokens": 0,
-        "reasoning_tokens": 0
-      },
-      "prompt_tokens_details": {
-        "audio_tokens": 0,
-        "cached_tokens": 0
-      }
-    },
-    "system_fingerprint": null
-  }
-}

BIN
static/process/GenerateNote/tmp/img-center-1774173408104/screenshot.png


BIN
static/process/GenerateNote/tmp/img-center-1774173408104/template.png


+ 0 - 17
static/process/GenerateNote/tmp/img-center-1774173408104/vlm_center_model_attempts.json

@@ -1,17 +0,0 @@
-{
-  "model_chain": [
-    "gpt-5.4",
-    "claude-opus-4-6",
-    "gemini-3.1-pro-preview"
-  ],
-  "success_model": "gpt-5.4",
-  "attempts": [
-    {
-      "index": 0,
-      "model": "gpt-5.4",
-      "requestOk": true,
-      "error": null,
-      "hasValidCenter": true
-    }
-  ]
-}

+ 0 - 4
static/process/GenerateNote/tmp/img-center-1774173408104/vlm_center_parsed.json

@@ -1,4 +0,0 @@
-{
-  "center_rx": 0.84,
-  "center_ry": 0.07
-}

+ 0 - 10
static/process/GenerateNote/tmp/img-center-1774173408104/vlm_center_result.json

@@ -1,10 +0,0 @@
-{
-  "center_rx": 0.84,
-  "center_ry": 0.07,
-  "pixel_x": 907,
-  "pixel_y": 168,
-  "screenshot_width": 1080,
-  "screenshot_height": 2400,
-  "marked_screenshot": null,
-  "marked_screenshot_error": "未找到 C:\\Users\\liuyu\\Desktop\\test\\AndroidRemoteController\\python\\scripts\\img-center-mark-center.py"
-}

+ 0 - 32
static/process/GenerateNote/tmp/img-center-1774173430585/openai_raw.json

@@ -1,32 +0,0 @@
-{
-  "id": "chatcmpl-DM9qpVEXjLXmWoDfbnljSlUkM4r28",
-  "choices": [
-    {
-      "index": 0,
-      "message": {
-        "role": "assistant",
-        "content": "{\"center_rx\":0.164,\"center_ry\":0.248}",
-        "annotations": []
-      },
-      "logprobs": null,
-      "finish_reason": "stop"
-    }
-  ],
-  "created": 1774173455,
-  "model": "gpt-5.4-2026-03-05-short-api-ev3",
-  "object": "chat.completion",
-  "usage": {
-    "prompt_tokens": 2828,
-    "completion_tokens": 20,
-    "total_tokens": 2848,
-    "completion_tokens_details": {
-      "audio_tokens": 0,
-      "reasoning_tokens": 0
-    },
-    "prompt_tokens_details": {
-      "audio_tokens": 0,
-      "cached_tokens": 0
-    }
-  },
-  "system_fingerprint": null
-}

+ 0 - 38
static/process/GenerateNote/tmp/img-center-1774173430585/openai_raw_attempt_0.json

@@ -1,38 +0,0 @@
-{
-  "model": "gpt-5.4",
-  "attemptIndex": 0,
-  "httpSuccess": true,
-  "httpError": null,
-  "response": {
-    "id": "chatcmpl-DM9qpVEXjLXmWoDfbnljSlUkM4r28",
-    "choices": [
-      {
-        "index": 0,
-        "message": {
-          "role": "assistant",
-          "content": "{\"center_rx\":0.164,\"center_ry\":0.248}",
-          "annotations": []
-        },
-        "logprobs": null,
-        "finish_reason": "stop"
-      }
-    ],
-    "created": 1774173455,
-    "model": "gpt-5.4-2026-03-05-short-api-ev3",
-    "object": "chat.completion",
-    "usage": {
-      "prompt_tokens": 2828,
-      "completion_tokens": 20,
-      "total_tokens": 2848,
-      "completion_tokens_details": {
-        "audio_tokens": 0,
-        "reasoning_tokens": 0
-      },
-      "prompt_tokens_details": {
-        "audio_tokens": 0,
-        "cached_tokens": 0
-      }
-    },
-    "system_fingerprint": null
-  }
-}

BIN
static/process/GenerateNote/tmp/img-center-1774173430585/screenshot.png


BIN
static/process/GenerateNote/tmp/img-center-1774173430585/template.png


+ 0 - 17
static/process/GenerateNote/tmp/img-center-1774173430585/vlm_center_model_attempts.json

@@ -1,17 +0,0 @@
-{
-  "model_chain": [
-    "gpt-5.4",
-    "claude-opus-4-6",
-    "gemini-3.1-pro-preview"
-  ],
-  "success_model": "gpt-5.4",
-  "attempts": [
-    {
-      "index": 0,
-      "model": "gpt-5.4",
-      "requestOk": true,
-      "error": null,
-      "hasValidCenter": true
-    }
-  ]
-}

+ 0 - 4
static/process/GenerateNote/tmp/img-center-1774173430585/vlm_center_parsed.json

@@ -1,4 +0,0 @@
-{
-  "center_rx": 0.164,
-  "center_ry": 0.248
-}

+ 0 - 10
static/process/GenerateNote/tmp/img-center-1774173430585/vlm_center_result.json

@@ -1,10 +0,0 @@
-{
-  "center_rx": 0.164,
-  "center_ry": 0.248,
-  "pixel_x": 177,
-  "pixel_y": 595,
-  "screenshot_width": 1080,
-  "screenshot_height": 2400,
-  "marked_screenshot": null,
-  "marked_screenshot_error": "未找到 C:\\Users\\liuyu\\Desktop\\test\\AndroidRemoteController\\python\\scripts\\img-center-mark-center.py"
-}

+ 0 - 32
static/process/GenerateNote/tmp/img-center-1774173460899/openai_raw.json

@@ -1,32 +0,0 @@
-{
-  "id": "chatcmpl-DM9r7xYGlGqQ8KBvguoiVmBjP9rwX",
-  "choices": [
-    {
-      "index": 0,
-      "message": {
-        "role": "assistant",
-        "content": "{\"center_rx\":0.8105,\"center_ry\":0.0719}",
-        "annotations": []
-      },
-      "logprobs": null,
-      "finish_reason": "stop"
-    }
-  ],
-  "created": 1774173473,
-  "model": "gpt-5.4-2026-03-05-short-api-ev3",
-  "object": "chat.completion",
-  "usage": {
-    "prompt_tokens": 1845,
-    "completion_tokens": 22,
-    "total_tokens": 1867,
-    "completion_tokens_details": {
-      "audio_tokens": 0,
-      "reasoning_tokens": 0
-    },
-    "prompt_tokens_details": {
-      "audio_tokens": 0,
-      "cached_tokens": 0
-    }
-  },
-  "system_fingerprint": null
-}

+ 0 - 38
static/process/GenerateNote/tmp/img-center-1774173460899/openai_raw_attempt_0.json

@@ -1,38 +0,0 @@
-{
-  "model": "gpt-5.4",
-  "attemptIndex": 0,
-  "httpSuccess": true,
-  "httpError": null,
-  "response": {
-    "id": "chatcmpl-DM9r7xYGlGqQ8KBvguoiVmBjP9rwX",
-    "choices": [
-      {
-        "index": 0,
-        "message": {
-          "role": "assistant",
-          "content": "{\"center_rx\":0.8105,\"center_ry\":0.0719}",
-          "annotations": []
-        },
-        "logprobs": null,
-        "finish_reason": "stop"
-      }
-    ],
-    "created": 1774173473,
-    "model": "gpt-5.4-2026-03-05-short-api-ev3",
-    "object": "chat.completion",
-    "usage": {
-      "prompt_tokens": 1845,
-      "completion_tokens": 22,
-      "total_tokens": 1867,
-      "completion_tokens_details": {
-        "audio_tokens": 0,
-        "reasoning_tokens": 0
-      },
-      "prompt_tokens_details": {
-        "audio_tokens": 0,
-        "cached_tokens": 0
-      }
-    },
-    "system_fingerprint": null
-  }
-}

BIN
static/process/GenerateNote/tmp/img-center-1774173460899/screenshot.png


BIN
static/process/GenerateNote/tmp/img-center-1774173460899/template.png


+ 0 - 17
static/process/GenerateNote/tmp/img-center-1774173460899/vlm_center_model_attempts.json

@@ -1,17 +0,0 @@
-{
-  "model_chain": [
-    "gpt-5.4",
-    "claude-opus-4-6",
-    "gemini-3.1-pro-preview"
-  ],
-  "success_model": "gpt-5.4",
-  "attempts": [
-    {
-      "index": 0,
-      "model": "gpt-5.4",
-      "requestOk": true,
-      "error": null,
-      "hasValidCenter": true
-    }
-  ]
-}

+ 0 - 4
static/process/GenerateNote/tmp/img-center-1774173460899/vlm_center_parsed.json

@@ -1,4 +0,0 @@
-{
-  "center_rx": 0.8105,
-  "center_ry": 0.0719
-}

+ 0 - 10
static/process/GenerateNote/tmp/img-center-1774173460899/vlm_center_result.json

@@ -1,10 +0,0 @@
-{
-  "center_rx": 0.8105,
-  "center_ry": 0.0719,
-  "pixel_x": 875,
-  "pixel_y": 173,
-  "screenshot_width": 1080,
-  "screenshot_height": 2400,
-  "marked_screenshot": null,
-  "marked_screenshot_error": "未找到 C:\\Users\\liuyu\\Desktop\\test\\AndroidRemoteController\\python\\scripts\\img-center-mark-center.py"
-}

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


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


+ 79 - 10
static/process/RedNoteAIThumbsUp/process.json

@@ -1,33 +1,102 @@
 {
   "name": "RedNoteAIThumbsUp",
   "description": "小红书自动点赞",
-  "variables": { "sendBtnPos": "" },
+  "variables": {
+    "sendBtnPos": "",
+    "video_like_btn_center_pos": "",
+    "note_text_like_btn_center_pos": ""
+  },
   "execute": [
     {
       "type": "try",
       "continueAfterFail": true,
       "try": [
         {
-          "type": "fun",
-          "method": "img-center-point-location",
-          "inVars": ["视频点赞.png"],
-          "outVars": ["{sendBtnPos}"]
+          "type": "if",
+          "condition": "{video_like_btn_center_pos} != \"\"",
+          "then": [
+            {
+              "type": "fun",
+              "method": "img-center-point-location",
+              "inVars": [
+                "视频点赞.png"
+              ],
+              "outVars": [
+                "{sendBtnPos}"
+              ]
+            },
+            {
+              "type": "fun",
+              "method": "persist-save",
+              "inVars": [
+                "rednote_video_like_btn",
+                "{sendBtnPos}"
+              ],
+              "outVars": []
+            }
+          ],
+          "else": [
+            {
+              "type": "fun",
+              "method": "persist-read",
+              "inVars": [
+                "rednote_video_like_btn"
+              ],
+              "outVars": [
+                "{sendBtnPos}"
+              ]
+            }
+          ]
         }
       ],
       "success": [],
       "fail": [
         {
-          "type": "fun",
-          "method": "img-center-point-location",
-          "inVars": ["图文点赞.png"],
-          "outVars": ["{sendBtnPos}"]
+          "type": "if",
+          "condition": "{note_text_like_btn_center_pos} != \"\"",
+          "then": [
+            {
+              "type": "fun",
+              "method": "img-center-point-location",
+              "inVars": [
+                "图文点赞.png"
+              ],
+              "outVars": [
+                "{sendBtnPos}"
+              ]
+            },
+            {
+              "type": "fun",
+              "method": "persist-save",
+              "inVars": [
+                "rednote_note_text_like_btn",
+                "{sendBtnPos}"
+              ],
+              "outVars": []
+            }
+          ],
+          "else": [
+            {
+              "type": "fun",
+              "method": "persist-read",
+              "inVars": [
+                "rednote_note_text_like_btn"
+              ],
+              "outVars": [
+                "{sendBtnPos}"
+              ]
+            }
+          ]
         }
       ]
     },
     {
       "type": "fun",
       "method": "adb-click",
-      "inVars": ["{sendBtnPos}"]
+      "inVars": [
+        "{sendBtnPos}"
+      ],
+      "outVars": []
     }
   ]
 }

+ 112 - 29
static/process/RedNoteBrowsingAndThumbsUp/process.json

@@ -10,7 +10,8 @@
     "random-click-pos": "",
     "stay-duration": 0,
     "send-btn-pos": "",
-    "b-like-click": 0
+    "b-like-click": 0,
+    "video_like_btn_center_pos": ""
   },
   "execute": [
     {
@@ -19,8 +20,13 @@
       "body": [
         {
           "type": "random",
-          "inVars": ["1", "3"],
-          "outVars": ["{swipe-count}"]
+          "inVars": [
+            "1",
+            "3"
+          ],
+          "outVars": [
+            "{swipe-count}"
+          ]
         },
         {
           "type": "schedule",
@@ -32,20 +38,32 @@
             {
               "type": "fun",
               "method": "adb-swipe",
-              "inVars": ["up-down"],
+              "inVars": [
+                "up-down"
+              ],
               "outVars": []
             }
           ]
         },
         {
           "type": "random",
-          "inVars": ["200", "780"],
-          "outVars": ["{click-x}"]
+          "inVars": [
+            "200",
+            "780"
+          ],
+          "outVars": [
+            "{click-x}"
+          ]
         },
         {
           "type": "random",
-          "inVars": ["400", "1500"],
-          "outVars": ["{click-y}"]
+          "inVars": [
+            "400",
+            "1500"
+          ],
+          "outVars": [
+            "{click-y}"
+          ]
         },
         {
           "type": "set",
@@ -55,29 +73,73 @@
         {
           "type": "fun",
           "method": "adb-click",
-          "inVars": ["{random-click-pos}"]
+          "inVars": [
+            "{random-click-pos}"
+          ],
+          "outVars": []
         },
         {
           "type": "random",
-          "inVars": [10, 2000],
-          "outVars": ["{stay-duration}"]
+          "inVars": [
+            10,
+            2000
+          ],
+          "outVars": [
+            "{stay-duration}"
+          ]
         },
         {
           "type": "try",
           "continueAfterFail": true,
           "try": [
             {
-              "type": "fun",
-              "method": "img-center-point-location",
-              "inVars": ["视频点赞.png"],
-              "outVars": ["{send-btn-pos}"]
+              "type": "if",
+              "condition": "{video_like_btn_center_pos} != \"\"",
+              "then": [
+                {
+                  "type": "fun",
+                  "method": "img-center-point-location",
+                  "inVars": [
+                    "视频点赞.png"
+                  ],
+                  "outVars": [
+                    "{send-btn-pos}"
+                  ]
+                },
+                {
+                  "type": "fun",
+                  "method": "persist-save",
+                  "inVars": [
+                    "rednote_browse_video_like_btn",
+                    "{send-btn-pos}"
+                  ],
+                  "outVars": []
+                }
+              ],
+              "else": [
+                {
+                  "type": "fun",
+                  "method": "persist-read",
+                  "inVars": [
+                    "rednote_browse_video_like_btn"
+                  ],
+                  "outVars": [
+                    "{send-btn-pos}"
+                  ]
+                }
+              ]
             }
           ],
           "success": [
             {
               "type": "random",
-              "inVars": ["2", "10"],
-              "outVars": ["{swipe-count}"]
+              "inVars": [
+                "2",
+                "10"
+              ],
+              "outVars": [
+                "{swipe-count}"
+              ]
             },
             {
               "type": "for",
@@ -86,8 +148,13 @@
               "body": [
                 {
                   "type": "random",
-                  "inVars": [5, 30],
-                  "outVars": ["{stay-duration}"]
+                  "inVars": [
+                    5,
+                    30
+                  ],
+                  "outVars": [
+                    "{stay-duration}"
+                  ]
                 },
                 {
                   "type": "delay",
@@ -96,30 +163,42 @@
                 {
                   "type": "fun",
                   "method": "adb-swipe",
-                  "inVars": ["down-up"],
+                  "inVars": [
+                    "down-up"
+                  ],
                   "outVars": []
                 },
                 {
                   "type": "if",
                   "condition": "{stay-duration} > 25",
-                  "ture": [
+                  "then": [
                     {
                       "type": "random",
-                      "inVars": [0, 1],
-                      "outVars": ["{b-like-click}"]
+                      "inVars": [
+                        0,
+                        1
+                      ],
+                      "outVars": [
+                        "{b-like-click}"
+                      ]
                     },
                     {
                       "type": "if",
                       "condition": "{b-like-click} == 1",
-                      "ture": [
+                      "then": [
                         {
                           "type": "fun",
                           "method": "adb-click",
-                          "inVars": ["{send-btn-pos}"]
+                          "inVars": [
+                            "{send-btn-pos}"
+                          ],
+                          "outVars": []
                         }
-                      ]
+                      ],
+                      "else": []
                     }
-                  ]
+                  ],
+                  "else": []
                 }
               ]
             }
@@ -127,14 +206,18 @@
           "fail": [
             {
               "type": "echo",
-              "value": "点赞图标匹配失败"
+              "inVars": [
+                "点赞图标匹配失败"
+              ]
             }
           ]
         },
         {
           "type": "fun",
           "method": "adb-keyevent",
-          "inVars": ["4"],
+          "inVars": [
+            "4"
+          ],
           "outVars": []
         }
       ]

+ 138 - 30
static/process/RedNoteBrowsingAndThumbsUpTest/process.json

@@ -9,7 +9,11 @@
     "click-y": 0,
     "random-click-pos": "",
     "stay-duration": 0,
-    "send-btn-pos": ""
+    "send-btn-pos": "",
+    "b-like-click": 0,
+    "back-duration": 0,
+    "video_like_btn_center_pos": "",
+    "note_text_like_btn_center_pos": ""
   },
   "execute": [
     {
@@ -21,48 +25,73 @@
       "interval": [
         {
           "type": "random",
-          "inVars": ["1", "3"],
-          "outVars": ["{swipeCount}"]
+          "inVars": [
+            "1",
+            "3"
+          ],
+          "outVars": [
+            "{swipe-count}"
+          ]
         },
         {
           "type": "schedule",
           "condition": {
             "interval": "1s",
-            "repeat": "{swipeCount}"
+            "repeat": "{swipe-count}"
           },
           "interval": [
             {
               "type": "fun",
               "method": "adb-swipe",
-              "inVars": ["down-up"],
+              "inVars": [
+                "down-up"
+              ],
               "outVars": []
             }
           ]
         },
         {
           "type": "random",
-          "inVars": ["200", "880"],
-          "outVars": ["{clickX}"]
+          "inVars": [
+            "200",
+            "880"
+          ],
+          "outVars": [
+            "{click-x}"
+          ]
         },
         {
           "type": "random",
-          "inVars": ["400", "2000"],
-          "outVars": ["{clickY}"]
+          "inVars": [
+            "400",
+            "2000"
+          ],
+          "outVars": [
+            "{click-y}"
+          ]
         },
         {
           "type": "set",
           "variable": "{random-click-pos}",
-          "value": "{clickX},{clickY}"
+          "value": "{click-x},{click-y}"
         },
         {
           "type": "fun",
           "method": "adb-click",
-          "inVars": ["{random-click-pos}"]
+          "inVars": [
+            "{random-click-pos}"
+          ],
+          "outVars": []
         },
         {
           "type": "random",
-          "inVars": [10, 2000],
-          "outVars": ["{stay-duration}"]
+          "inVars": [
+            10,
+            2000
+          ],
+          "outVars": [
+            "{stay-duration}"
+          ]
         },
         {
           "type": "delay",
@@ -70,44 +99,121 @@
         },
         {
           "type": "random",
-          "inVars": [0, 1],
-          "outVars": ["{b-like-click}"]
+          "inVars": [
+            0,
+            1
+          ],
+          "outVars": [
+            "{b-like-click}"
+          ]
         },
         {
           "type": "if",
           "condition": "{b-like-click} == 1",
-          "ture": [
+          "then": [
             {
               "type": "try",
               "continueAfterFail": true,
               "try": [
                 {
-                  "type": "fun",
-                  "method": "img-center-point-location",
-                  "inVars": ["视频点赞.png"],
-                  "outVars": ["{send-btn-pos}"]
+                  "type": "if",
+                  "condition": "{video_like_btn_center_pos} != \"\"",
+                  "then": [
+                    {
+                      "type": "fun",
+                      "method": "img-center-point-location",
+                      "inVars": [
+                        "视频点赞.png"
+                      ],
+                      "outVars": [
+                        "{send-btn-pos}"
+                      ]
+                    },
+                    {
+                      "type": "fun",
+                      "method": "persist-save",
+                      "inVars": [
+                        "rednote_test_video_like_btn",
+                        "{send-btn-pos}"
+                      ],
+                      "outVars": []
+                    }
+                  ],
+                  "else": [
+                    {
+                      "type": "fun",
+                      "method": "persist-read",
+                      "inVars": [
+                        "rednote_test_video_like_btn"
+                      ],
+                      "outVars": [
+                        "{send-btn-pos}"
+                      ]
+                    }
+                  ]
                 }
               ],
               "fail": [
                 {
-                  "type": "fun",
-                  "method": "img-center-point-location",
-                  "inVars": ["图文点赞.png"],
-                  "outVars": ["{send-btn-pos}"]
+                  "type": "if",
+                  "condition": "{note_text_like_btn_center_pos} != \"\"",
+                  "then": [
+                    {
+                      "type": "fun",
+                      "method": "img-center-point-location",
+                      "inVars": [
+                        "图文点赞.png"
+                      ],
+                      "outVars": [
+                        "{send-btn-pos}"
+                      ]
+                    },
+                    {
+                      "type": "fun",
+                      "method": "persist-save",
+                      "inVars": [
+                        "rednote_test_note_text_like_btn",
+                        "{send-btn-pos}"
+                      ],
+                      "outVars": []
+                    }
+                  ],
+                  "else": [
+                    {
+                      "type": "fun",
+                      "method": "persist-read",
+                      "inVars": [
+                        "rednote_test_note_text_like_btn"
+                      ],
+                      "outVars": [
+                        "{send-btn-pos}"
+                      ]
+                    }
+                  ]
                 }
-              ]
+              ],
+              "success": []
             },
             {
               "type": "fun",
               "method": "adb-click",
-              "inVars": ["{send-btn-pos}"]
+              "inVars": [
+                "{send-btn-pos}"
+              ],
+              "outVars": []
             }
-          ]
+          ],
+          "else": []
         },
         {
           "type": "random",
-          "inVars": [5, 12],
-          "outVars": ["{back-duration}"]
+          "inVars": [
+            5,
+            12
+          ],
+          "outVars": [
+            "{back-duration}"
+          ]
         },
         {
           "type": "delay",
@@ -116,7 +222,9 @@
         {
           "type": "fun",
           "method": "adb-keyevent",
-          "inVars": ["4"],
+          "inVars": [
+            "4"
+          ],
           "outVars": []
         }
       ]

+ 4 - 2
static/process/Test/process.json

@@ -1,7 +1,7 @@
 {
   "name": "Test",
   "description": "测试",
-  "variables": [],
+  "variables": {},
   "execute": [
     {
       "type": "schedule",
@@ -13,7 +13,9 @@
         {
           "type": "fun",
           "method": "adb-swipe",
-          "inVars": ["right-left"],
+          "inVars": [
+            "right-left"
+          ],
           "outVars": []
         }
       ]

+ 319 - 231
static/process/WeChatAIChating/process.json

@@ -1,233 +1,321 @@
 {
-	"name": "WeChatAIChating",
-    	"description": "微信AI陪聊",
-	"variables": {
-		"turn": 1,
-		"relationBg": "",
-		"chatArea": "",
-		"chatHistoryMessage": "",
-		"currentChatMessage": "",
-		"lastHistoryMessage": "",
-		"lastChatMessage": "",
-		"lastChatRole": "",
-		"lastHistoryChatMessage": "",
-		"lastHistoryChatRole": "",
-		"aiReply": "",
-		"aiCallBack":0,
-		"sendBtnPos": "",
-		"newChatMessage": ""
-	},
-	"execute": [
-		{
-			"type": "schedule",
-			"condition": 
-			{
-				"interval": "1s",
-				"repeat": 2
-			},
-			"interval": 
-			[
-				{
-					"type": "echo",
-					"value": "==第 {{turn}} 轮=="
-				},
-				{
-					"type": "set",
-					"variable": "{turn}",
-					"value": "{turn} + 1"
-				},
-				{
-					"type":"if",
-					"condition": "{chatArea}==\"\"",
-					"ture": 
-					[
-						{
-							"type": "fun",
-							"method": "img-bounding-box-location",
-							"inVars": ["ScreenShot.jpg","ChatArea.png"],
-							"outVars": ["{chatArea}"]
-						},
-						{
-							"type": "echo",
-							"value": "坐标: {{chatArea}}"
-						}
-					]
-				},
-				{
-					"type": "fun",
-					"method": "ocr-chat",
-					"inVars": ["(242,242,242)","(114,220,106)","{chatArea}"],
-					"outVars": ["{currentChatMessage}"]
-				},
-				{
-					"type": "echo",
-					"value": "当前聊天内容:{{currentChatMessage}}"
-				},
-				{
-					"type": "fun",
-					"method": "read-txt",
-					"inVars": ["history/chat-history.txt"],
-					"outVars": ["{chatHistoryMessage}"]
-				},
-				{
-					"type": "fun",
-					"method": "read-txt",
-					"inVars": ["history/bg.txt"],
-					"outVars": ["{relationBg}"]
-				},
-				{
-					"type": "echo",
-					"value": "背景内容:{{relationBg}}"
-				},
-				{
-					"type": "if",
-					"condition": "{relationBg}==\"\"",
-					"ture": 
-					[
-						
-						{
-							"type": "if",
-							"condition": "{chatHistoryMessage}==\"\"",
-							"ture": 
-							[
-								{
-									"type": "fun",
-									"method": "ai-generate",
-									"prompt": "根据聊天记录:{{currentChatMessage}},推理出我与聊天好友的关系背景,用一句话描述出来",
-									"inVars": [],
-									"outVars": ["{relationBg}","{aiCallBack}"]	
-								}
-							],
-							"false": 
-							[
-								{
-									"type": "fun",
-									"method": "ai-generate",
-									"prompt": "根据聊天记录:{{chatHistoryMessage}},推理出我与聊天好友的关系背景,用一句话描述出来",
-									"inVars": [],
-									"outVars": ["{relationBg}","{aiCallBack}"]	
-								}
-							]
-						},
-						{
-							"type": "while",
-							"condition": "1 == aiCallBack",
-							"ture": 
-							[
-								
-							]
-						},
-						{
-							"type": "set",
-							"variable": "{aiCallBack}",
-							"value": "0"
-						},
-						{
-							"type": "echo",
-							"value": "==AI生成关系背景==:{{relationBg}}"
-						},
-						{
-							"type": "fun",
-							"method": "save-txt",
-							"inVars": ["{relationBg}","history/bg.txt"],
-							"outVars": []
-						}
-					]
-				},
-				{
-					"type": "fun",
-					"method": "read-last-message",
-					"inVars": ["{currentChatMessage}"],
-					"outVars": ["{lastChatMessage}","{lastChatRole}"]
-				},
-				{
-					"type": "echo",
-					"value": "当前最后一条消息:{{lastChatMessage}}({{lastChatRole}})"
-				},
-				{
-					"type": "fun",
-					"method": "read-last-message",
-					"inVars": ["{chatHistoryMessage}"],
-					"outVars": ["{lastHistoryChatMessage}","{lastHistoryChatRole}"]
-				},
-				{
-					"type": "echo",
-					"value": "==历史最后一条消息:{{lastHistoryChatMessage}}({{lastHistoryChatRole}})=="
-				},
-				{
-					"type": "if",
-					"condition": "{lastChatMessage} != {lastHistoryChatMessage}",
-					"ture": 
-					[
-						{
-							"type": "echo",
-							"value":"AI自动回复流程开始=="
-						},
-						{
-							"type": "if",
-							"condition": "{lastChatRole} == \"friend\"",
-							"ture": 
-							[
-								{
-									"type": "fun",
-									"method": "ai-generate",
-									"prompt": "根据我们背景关系描述:{{relationBg}},以及我们的聊天记录:{{lastChatMessage}},帮我做出适合回复,不要给建议,直接给一条你认为最好的回复信息,回复信息字数要符合微信聊天习惯",
-									"inVars": [],
-									"outVars": ["{aiReply}","{aiCallBack}"]	
-								},
-								{
-									"type": "while",
-									"condition": "1 == aiCallBack",
-									"ture": 
-									[
-										
-									]
-								},
-								{
-									"type": "set",
-									"variable": "{aiCallBack}",
-									"value": "0"
-								},
-								{
-									"type": "fun",
-									"method": "adb-input",
-									"inVars": ["{aiReply}"]
-								},
-								{
-									"type": "if",
-									"condition": "{sendBtnPos} == \"\"",
-									"ture": 
-									[
-										{
-											"type": "fun",
-											"method": "img-center-point-location",
-											"inVars": ["微信聊天界面的发送按钮定位图.png"],
-											"outVars": ["{sendBtnPos}"]
-										}
-									]
-								},
-								{
-									"type": "fun",
-									"method": "adb-click",
-									"inVars": ["{sendBtnPos}"]
-								}		
-							]	
-						},
-						{
-							"type": "fun",
-							"method": "smart-chat-append",
-							"inVars": ["{chatHistoryMessage}","{currentChatMessage}"],
-							"outVars": ["{newChatMessage}"]
-						},
-						{
-							"type": "fun",
-							"method": "save-txt",
-							"inVars": ["{newChatMessage}","history/chat-history.txt"],
-							"outVars": []
-						}
-					]
-				}
-			]	
-		}		
-	]
+  "name": "WeChatAIChating",
+  "description": "微信AI陪聊",
+  "variables": {
+    "turn": 1,
+    "relationBg": "",
+    "chatArea": "",
+    "chatHistoryMessage": "",
+    "currentChatMessage": "",
+    "lastHistoryMessage": "",
+    "lastChatMessage": "",
+    "lastChatRole": "",
+    "lastHistoryChatMessage": "",
+    "lastHistoryChatRole": "",
+    "aiReply": "",
+    "aiCallBack": 0,
+    "sendBtnPos": "",
+    "wx_send_btn_center_pos": "",
+    "newChatMessage": ""
+  },
+  "execute": [
+    {
+      "type": "schedule",
+      "condition": {
+        "interval": "1s",
+        "repeat": 2
+      },
+      "interval": [
+        {
+          "type": "echo",
+          "inVars": [
+            "==第 {{turn}} 轮=="
+          ]
+        },
+        {
+          "type": "set",
+          "variable": "{turn}",
+          "value": "{turn} + 1"
+        },
+        {
+          "type": "if",
+          "condition": "{chatArea}==\"\"",
+          "then": [
+            {
+              "type": "fun",
+              "method": "img-bounding-box-location",
+              "inVars": [
+                "ScreenShot.jpg",
+                "ChatArea.png"
+              ],
+              "outVars": [
+                "{chatArea}"
+              ]
+            },
+            {
+              "type": "echo",
+              "inVars": [
+                "坐标: {{chatArea}}"
+              ]
+            }
+          ],
+          "else": []
+        },
+        {
+          "type": "fun",
+          "method": "ocr-chat",
+          "inVars": [
+            "(242,242,242)",
+            "(114,220,106)",
+            "{chatArea}"
+          ],
+          "outVars": [
+            "{currentChatMessage}"
+          ]
+        },
+        {
+          "type": "echo",
+          "inVars": [
+            "当前聊天内容:{{currentChatMessage}}"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "read-txt",
+          "inVars": [
+            "history/chat-history.txt"
+          ],
+          "outVars": [
+            "{chatHistoryMessage}"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "read-txt",
+          "inVars": [
+            "history/bg.txt"
+          ],
+          "outVars": [
+            "{relationBg}"
+          ]
+        },
+        {
+          "type": "echo",
+          "inVars": [
+            "背景内容:{{relationBg}}"
+          ]
+        },
+        {
+          "type": "if",
+          "condition": "{relationBg}==\"\"",
+          "then": [
+            {
+              "type": "if",
+              "condition": "{chatHistoryMessage}==\"\"",
+              "then": [
+                {
+                  "type": "fun",
+                  "method": "ai-generate",
+                  "inVars": [
+                    "根据聊天记录:{{currentChatMessage}},推理出我与聊天好友的关系背景,用一句话描述出来"
+                  ],
+                  "outVars": [
+                    "{relationBg}",
+                    "{aiCallBack}"
+                  ]
+                }
+              ],
+              "else": [
+                {
+                  "type": "fun",
+                  "method": "ai-generate",
+                  "inVars": [
+                    "根据聊天记录:{{chatHistoryMessage}},推理出我与聊天好友的关系背景,用一句话描述出来"
+                  ],
+                  "outVars": [
+                    "{relationBg}",
+                    "{aiCallBack}"
+                  ]
+                }
+              ]
+            },
+            {
+              "type": "while",
+              "condition": "1 == aiCallBack",
+              "body": []
+            },
+            {
+              "type": "set",
+              "variable": "{aiCallBack}",
+              "value": "0"
+            },
+            {
+              "type": "echo",
+              "inVars": [
+                "==AI生成关系背景==:{{relationBg}}"
+              ]
+            },
+            {
+              "type": "fun",
+              "method": "save-txt",
+              "inVars": [
+                "{relationBg}",
+                "history/bg.txt"
+              ],
+              "outVars": []
+            }
+          ],
+          "else": []
+        },
+        {
+          "type": "fun",
+          "method": "read-last-message",
+          "inVars": [
+            "{currentChatMessage}"
+          ],
+          "outVars": [
+            "{lastChatMessage}",
+            "{lastChatRole}"
+          ]
+        },
+        {
+          "type": "echo",
+          "inVars": [
+            "当前最后一条消息:{{lastChatMessage}}({{lastChatRole}})"
+          ]
+        },
+        {
+          "type": "fun",
+          "method": "read-last-message",
+          "inVars": [
+            "{chatHistoryMessage}"
+          ],
+          "outVars": [
+            "{lastHistoryChatMessage}",
+            "{lastHistoryChatRole}"
+          ]
+        },
+        {
+          "type": "echo",
+          "inVars": [
+            "==历史最后一条消息:{{lastHistoryChatMessage}}({{lastHistoryChatRole}})=="
+          ]
+        },
+        {
+          "type": "if",
+          "condition": "{lastChatMessage} != {lastHistoryChatMessage}",
+          "then": [
+            {
+              "type": "echo",
+              "inVars": [
+                "AI自动回复流程开始=="
+              ]
+            },
+            {
+              "type": "if",
+              "condition": "{lastChatRole} == \"friend\"",
+              "then": [
+                {
+                  "type": "fun",
+                  "method": "ai-generate",
+                  "inVars": [
+                    "根据我们背景关系描述:{{relationBg}},以及我们的聊天记录:{{lastChatMessage}},帮我做出适合回复,不要给建议,直接给一条你认为最好的回复信息,回复信息字数要符合微信聊天习惯"
+                  ],
+                  "outVars": [
+                    "{aiReply}",
+                    "{aiCallBack}"
+                  ]
+                },
+                {
+                  "type": "while",
+                  "condition": "1 == aiCallBack",
+                  "body": []
+                },
+                {
+                  "type": "set",
+                  "variable": "{aiCallBack}",
+                  "value": "0"
+                },
+                {
+                  "type": "fun",
+                  "method": "adb-input",
+                  "inVars": [
+                    "{aiReply}"
+                  ],
+                  "outVars": []
+                },
+                {
+                  "type": "if",
+                  "condition": "{wx_send_btn_center_pos} != \"\"",
+                  "then": [
+                    {
+                      "type": "fun",
+                      "method": "img-center-point-location",
+                      "inVars": [
+                        "微信聊天界面的发送按钮定位图.png"
+                      ],
+                      "outVars": [
+                        "{sendBtnPos}"
+                      ]
+                    },
+                    {
+                      "type": "fun",
+                      "method": "persist-save",
+                      "inVars": [
+                        "wx_send_btn_center",
+                        "{sendBtnPos}"
+                      ],
+                      "outVars": []
+                    }
+                  ],
+                  "else": [
+                    {
+                      "type": "fun",
+                      "method": "persist-read",
+                      "inVars": [
+                        "wx_send_btn_center"
+                      ],
+                      "outVars": [
+                        "{sendBtnPos}"
+                      ]
+                    }
+                  ]
+                },
+                {
+                  "type": "fun",
+                  "method": "adb-click",
+                  "inVars": [
+                    "{sendBtnPos}"
+                  ],
+                  "outVars": []
+                }
+              ],
+              "else": []
+            },
+            {
+              "type": "fun",
+              "method": "smart-chat-append",
+              "inVars": [
+                "{chatHistoryMessage}",
+                "{currentChatMessage}"
+              ],
+              "outVars": [
+                "{newChatMessage}"
+              ]
+            },
+            {
+              "type": "fun",
+              "method": "save-txt",
+              "inVars": [
+                "{newChatMessage}",
+                "history/chat-history.txt"
+              ],
+              "outVars": []
+            }
+          ],
+          "else": []
+        }
+      ]
+    }
+  ]
 }