Bläddra i källkod

小红书发笔记流程第一版

yichael 2 månader sedan
förälder
incheckning
3618d6ed53

+ 4 - 1
nodejs/ef-compiler/ef-compiler.js

@@ -51,6 +51,7 @@ const state = {
   variableContextInitialized: false,
   globalStepCounter: 0,
   currentWorkflowFolderPath: null,
+  declaredVariableNames: [], // workflow.variables 的 key,用于校验 inVars/outVars 引用是否已声明
 }
 
 // --- 从各组件抽出的工具方法(供本文件与 ctx 使用)---
@@ -89,7 +90,9 @@ function parseWorkflow(workflow) {
     getInitialized: () => state.variableContextInitialized,
     setInitialized: (v) => { state.variableContextInitialized = v },
   }
-  return workflowJsonParser.parseWorkflow(workflow, loaderState)
+  const result = workflowJsonParser.parseWorkflow(workflow, loaderState)
+  state.declaredVariableNames = workflow && typeof workflow.variables === 'object' ? Object.keys(workflow.variables) : []
+  return result
 }
 
 // --- 对外入口 2:仅解析动作列表 ---

+ 12 - 0
nodejs/ef-compiler/sequence-runner.js

@@ -1,4 +1,5 @@
 const { logActionError } = require('./actions/echo-parser.js')
+const variableParser = require('./variable-parser.js')
 
 /**
  * 执行操作序列(schedule/if/for/while + 普通步骤)
@@ -183,6 +184,17 @@ async function executeActionSequence(
       state.globalStepCounter++
       const typeName = getActionName(action)
 
+      // inVars/outVars 引用的变量必须在 workflow.variables 中声明,否则写 log 并失败
+      if ((action.inVars && action.inVars.length > 0) || (action.outVars && action.outVars.length > 0)) {
+        const declared = state.declaredVariableNames || []
+        const validation = variableParser.validateInOutVars(action, declared)
+        if (!validation.valid) {
+          const errMsg = `inVars/outVars 引用了未在 variables 中声明的变量: ${validation.undeclared.join(', ')}`
+          await logActionError(action, { success: false, error: errMsg }, { getActionName, logMessage, folderPath }).catch(() => {})
+          return { success: false, error: errMsg, completedSteps: i }
+        }
+      }
+
       const result = await executeAction(action, device, folderPath, resolution)
 
       if (result.success && result.skipped) { /* 步骤跳过不写 log */ }

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

@@ -113,9 +113,67 @@ function resolveActionInputs(action, variableContext) {
   return resolved
 }
 
+/**
+ * 从字符串或数组中提取所有变量引用名({var}、{{var}}、{arr}[{idx}] 中的 var/arr/idx)
+ * @returns {string[]} 变量名列表(可能重复)
+ */
+function extractVarNamesFromValue(val) {
+  const names = []
+  function collect(str) {
+    if (typeof str !== 'string') return
+    const doubleBrace = /\{\{([\w-]+)\}\}/g
+    const singleBrace = /\{([\w-]+)\}/g
+    let m
+    while ((m = doubleBrace.exec(str)) !== null) names.push(m[1])
+    while ((m = singleBrace.exec(str)) !== null) names.push(m[1])
+  }
+  if (typeof val === 'string') {
+    collect(val)
+    return names
+  }
+  if (Array.isArray(val)) {
+    val.forEach((v) => names.push(...extractVarNamesFromValue(v)))
+    return names
+  }
+  if (typeof val === 'object' && val !== null) {
+    Object.keys(val).forEach((k) => names.push(...extractVarNamesFromValue(val[k])))
+    return names
+  }
+  return names
+}
+
+/**
+ * 校验 action 的 inVars/outVars 中引用的变量是否均在 declaredVariableNames 中声明
+ * @param {object} action - 当前 action(含 inVars、outVars)
+ * @param {Set|string[]} declaredVariableNames - 在 workflow.variables 中声明的变量名集合
+ * @returns {{ valid: boolean, undeclared: string[] }}
+ */
+function validateInOutVars(action, declaredVariableNames) {
+  const declared = declaredVariableNames instanceof Set ? declaredVariableNames : new Set(declaredVariableNames || [])
+  const undeclared = []
+  function check(names) {
+    names.forEach((n) => {
+      if (n && typeof n === 'string' && !declared.has(n)) undeclared.push(n)
+    })
+  }
+  if (action.inVars && Array.isArray(action.inVars)) {
+    action.inVars.forEach((v) => check(extractVarNamesFromValue(v)))
+  }
+  if (action.outVars && Array.isArray(action.outVars)) {
+    action.outVars.forEach((v) => {
+      const name = typeof v === 'string' ? extractVarName(v) : v
+      if (name) check([name])
+    })
+  }
+  const unique = [...new Set(undeclared)]
+  return { valid: unique.length === 0, undeclared: unique }
+}
+
 module.exports = {
   resolveActionInputs,
   resolveInputValue,
   replaceArrayIndexInString,
   extractVarName,
+  extractVarNamesFromValue,
+  validateInOutVars,
 }

+ 62 - 39
static/process/GenerateNote/process.json

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

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


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