fun-parser.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. /**
  2. * fun 解析与执行 + 执行入口:registry/executeAction 由 ctx 传入(来自 workflow-json-parser),本模块负责 parse/runAction/run/supports
  3. * 简易结点由 fun-node-registry.js 配置,只需新建脚本 + 在注册表加一条即可。
  4. */
  5. const path = require('path')
  6. const variableParser = require('../../variable-parser.js')
  7. const FUN_NODE_REGISTRY = require('./fun-node-registry.js')
  8. const funAdbJsonBridge = require('./fun-adb-json-bridge.js')
  9. /** type: io + method 与常见拼写 → 注册表 type */
  10. function normalizeRegistryMethodName (name) {
  11. if (name == null || name === '') return name
  12. const key = String(name).trim().toLowerCase().replace(/_/g, '-')
  13. const map = {
  14. 'remov-folder': 'remove-folder',
  15. 'remove-forder': 'remove-folder',
  16. 'creat-folder': 'create-folder',
  17. 'create-forder': 'create-folder',
  18. }
  19. return map[key] || String(name).trim()
  20. }
  21. const LEGACY_FUN_TYPES = [
  22. 'fun', 'ai', 'io',
  23. 'read-txt', 'read-text', 'save-txt', 'save-text',
  24. 'img-bounding-box-location', 'img-center-point-location', 'img-cropping',
  25. 'ocr',
  26. 'read-last-message', 'smart-chat-append',
  27. 'extract-messages', 'ocr-chat', 'ocr-chat-history', 'extract-chat-history',
  28. 'save-messages', 'generate-summary', 'generate-history-summary',
  29. 'ai-generate', 'string-press',
  30. ]
  31. const REGISTERED_TYPES = (FUN_NODE_REGISTRY && Array.isArray(FUN_NODE_REGISTRY)) ? FUN_NODE_REGISTRY.map((r) => r.type) : []
  32. const FUN_REGISTRY_TYPES = LEGACY_FUN_TYPES.concat(REGISTERED_TYPES)
  33. const types = FUN_REGISTRY_TYPES
  34. const REGISTRY_BY_TYPE = new Map((FUN_NODE_REGISTRY || []).map((r) => [r.type, r]))
  35. const scriptCache = new Map()
  36. function getRegistryScript(funcDir, type) {
  37. const def = REGISTRY_BY_TYPE.get(type)
  38. if (!def) return null
  39. const key = `${funcDir}:${type}`
  40. if (!scriptCache.has(key)) {
  41. try {
  42. const scriptPath = path.join(funcDir, def.script || def.type + '.js')
  43. scriptCache.set(key, require(scriptPath))
  44. } catch (e) {
  45. scriptCache.set(key, null)
  46. }
  47. }
  48. return scriptCache.get(key)
  49. }
  50. function parse(action, parseContext) {
  51. const { extractVarName, resolveValue } = parseContext
  52. const variableContext = parseContext.variableContext || {}
  53. const parsed = {
  54. type: action.type,
  55. method: action.method,
  56. target: action.target,
  57. value: action.value,
  58. variable: action.variable,
  59. condition: action.condition,
  60. delay: action.delay || '',
  61. timeout: action.timeout,
  62. retry: action.retry,
  63. }
  64. Object.assign(parsed, action)
  65. const regDef = REGISTRY_BY_TYPE.get(action.type)
  66. if (regDef) {
  67. const funcDir = (parseContext.compilerConfig && parseContext.compilerConfig.funcDir) || __dirname
  68. if (regDef.customParse) {
  69. const script = getRegistryScript(funcDir, action.type)
  70. if (script && typeof script.parseNode === 'function') return script.parseNode(action, parseContext)
  71. }
  72. parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map((v) => extractVarName(v)) : []
  73. const inKeys = regDef.in || []
  74. const inAlt = regDef.inAlt || {}
  75. inKeys.forEach((key, i) => {
  76. const altKey = inAlt[key]
  77. parsed[key] = action.inVars?.[i] ?? action[key] ?? (altKey ? action[altKey] : undefined)
  78. })
  79. parsed.variable = action.outVars?.[0] ? extractVarName(action.outVars[0]) : undefined
  80. return parsed
  81. }
  82. switch (action.type) {
  83. case 'fun':
  84. parsed.method = action.method
  85. parsed.inVars = Array.isArray(action.inVars) ? action.inVars.map((v) => extractVarName(v)) : []
  86. parsed.outVars = Array.isArray(action.outVars) ? action.outVars.map((v) => extractVarName(v)) : []
  87. break
  88. case 'extract-messages':
  89. case 'ocr-chat':
  90. case 'ocr-chat-history':
  91. case 'extract-chat-history':
  92. parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
  93. parsed.avatar1 = action.inVars && action.inVars.length >= 2 ? action.inVars[0] : action.inVars?.[0]
  94. parsed.avatar2 = action.inVars && action.inVars.length >= 2 ? action.inVars[1] : action.avatar2
  95. parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
  96. if (parsed.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
  97. else if (action.variable) parsed.variable = extractVarName(action.variable)
  98. if (action.friendAvatar && !parsed.avatar1) parsed.avatar1 = action.friendAvatar
  99. if (action.myAvatar && !parsed.avatar2) parsed.avatar2 = action.myAvatar
  100. break
  101. case 'save-messages':
  102. break
  103. case 'generate-summary':
  104. case 'generate-history-summary':
  105. parsed.summaryVariable = action.summaryVariable
  106. break
  107. case 'ai-generate':
  108. parsed.prompt = resolveValue(action.prompt, variableContext)
  109. parsed.model = action.model
  110. parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
  111. parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
  112. if (parsed.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
  113. else if (action.variable) parsed.variable = extractVarName(action.variable)
  114. break
  115. case 'read-last-message': {
  116. const inputVars = action.inVars || action.inputVars || []
  117. const outputVars = action.outVars || action.outputVars || []
  118. parsed.inVars = inputVars.map(v => extractVarName(v))
  119. parsed.outVars = outputVars.map(v => extractVarName(v))
  120. if (inputVars.length > 0) parsed.inputVar = extractVarName(inputVars[0])
  121. if (outputVars.length > 0) parsed.textVariable = extractVarName(outputVars[0])
  122. if (outputVars.length > 1) parsed.senderVariable = extractVarName(outputVars[1])
  123. if (!parsed.textVariable) parsed.textVariable = action.textVariable
  124. if (!parsed.senderVariable) parsed.senderVariable = action.senderVariable
  125. break
  126. }
  127. case 'read-txt':
  128. case 'read-text':
  129. parsed.inVars = action.inVars && action.inVars.length > 0 ? action.inVars.map(v => extractVarName(v)) : []
  130. parsed.filePath = action.inVars && action.inVars.length > 0 ? action.inVars[0] : action.filePath
  131. parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
  132. break
  133. case 'save-txt':
  134. case 'save-text':
  135. if (action.inVars && Array.isArray(action.inVars)) {
  136. parsed.inVars = action.inVars.map(v => extractVarName(v))
  137. parsed.content = action.inVars[0]
  138. parsed.filePath = action.inVars.length > 1 ? action.inVars[1] : action.filePath
  139. } else {
  140. parsed.inVars = []
  141. parsed.filePath = action.filePath
  142. parsed.content = action.content
  143. }
  144. if (action.outVars && action.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
  145. break
  146. case 'img-bounding-box-location':
  147. parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
  148. parsed.screenshot = action.inVars?.[0]
  149. parsed.region = action.inVars?.[1] ?? action.region
  150. parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
  151. break
  152. case 'img-center-point-location': {
  153. parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map((v, i) => (i === 1 && Array.isArray(v) ? v : extractVarName(v))) : []
  154. parsed.template = action.inVars?.[0] ?? action.template
  155. parsed.scaleRange = Array.isArray(action.inVars?.[1]) ? action.inVars[1] : undefined
  156. parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
  157. break
  158. }
  159. case 'img-cropping':
  160. parsed.inVars = action.inVars && Array.isArray(action.inVars)
  161. ? action.inVars.map((v, i) => (i === 2 && Array.isArray(v) ? v : extractVarName(v)))
  162. : []
  163. parsed.imagePath = action.inVars?.[0] ?? action.imagePath
  164. parsed.savePath = action.inVars?.[1] ?? action.savePath
  165. parsed.squareSpec = action.inVars?.[2] ?? action.squareSpec
  166. if (action.outVars && action.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
  167. break
  168. case 'ocr':
  169. parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
  170. parsed.image = action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0 ? action.inVars[0] : action.image
  171. parsed.variable = action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined)
  172. break
  173. default:
  174. parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : []
  175. parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : []
  176. break
  177. }
  178. return parsed
  179. }
  180. async function execute(action, ctx) {
  181. return { success: true }
  182. }
  183. async function runAction(action, device, folderPath, resolution, ctx) {
  184. const { variableContext, evaluateCondition, registry, executeAction } = ctx
  185. if (action.condition && !evaluateCondition(action.condition, variableContext)) {
  186. return { success: true, skipped: true }
  187. }
  188. const resolvedAction = variableParser.resolveActionInputs(action, variableContext)
  189. ctx.resolution = resolution || ctx.resolution || { width: 1080, height: 1920 }
  190. if (resolvedAction.type === 'fun') {
  191. const m = resolvedAction.method != null ? String(resolvedAction.method).trim() : ''
  192. if (!m) return { success: false, error: 'fun 结点缺少 method(如 adb-click、json-to-arr)' }
  193. return run(m, resolvedAction, ctx, device, folderPath)
  194. }
  195. if (resolvedAction.type === 'ai' && resolvedAction.method) {
  196. return run(resolvedAction.method, resolvedAction, ctx, device, folderPath)
  197. }
  198. if (resolvedAction.type === 'io' && resolvedAction.method) {
  199. const m = normalizeRegistryMethodName(resolvedAction.method)
  200. return run(m, resolvedAction, ctx, device, folderPath)
  201. }
  202. if (supports(resolvedAction.type)) {
  203. return run(resolvedAction.type, resolvedAction, ctx, device, folderPath)
  204. }
  205. if (registry && registry[resolvedAction.type]) {
  206. const execCtx = {
  207. device,
  208. folderPath,
  209. resolution,
  210. variableContext,
  211. compilerConfig: ctx.compilerConfig,
  212. api: ctx.electronAPI,
  213. extractVarName: ctx.extractVarName,
  214. resolveValue: ctx.resolveValue,
  215. replaceVariablesInString: ctx.replaceVariablesInString,
  216. evaluateCondition: ctx.evaluateCondition,
  217. evaluateExpression: ctx.evaluateExpression,
  218. getActionName: ctx.getActionName,
  219. logMessage: ctx.logMessage,
  220. logOutVars: ctx.logOutVars,
  221. parseDelayString: ctx.parseDelayString,
  222. calculateWaitTime: ctx.calculateWaitTime,
  223. DEFAULT_SCROLL_DISTANCE: ctx.DEFAULT_SCROLL_DISTANCE,
  224. }
  225. return await executeAction(resolvedAction.type, resolvedAction, execCtx)
  226. }
  227. return { success: false, error: `未知的操作类型: ${resolvedAction.type}` }
  228. }
  229. const cache = new Map()
  230. function get(funcDir, category) {
  231. if (!funcDir) throw new Error('fun-parser: funcDir 未提供')
  232. const key = `${funcDir}:${category}`
  233. if (cache.has(key)) return cache.get(key)
  234. let mod
  235. switch (category) {
  236. case 'img':
  237. mod = {
  238. executeImgBoundingBoxLocation: require(path.join(funcDir, 'img', 'img-bounding-box-location.js')).executeImgBoundingBoxLocation,
  239. executeImgCenterPointLocation: require(path.join(funcDir, 'img', 'img-center-point-location.js')).executeImgCenterPointLocation,
  240. executeImgCropping: require(path.join(funcDir, 'img', 'img-cropping.js')).executeImgCropping,
  241. executeOcr: require(path.join(funcDir, 'ocr.js')).executeOcr,
  242. executeOcrFindText: require(path.join(funcDir, 'ocr.js')).executeOcrFindText,
  243. }
  244. break
  245. case 'io':
  246. mod = {
  247. executeReadLastMessage: require(path.join(funcDir, 'chat', 'read-last-message.js')).executeReadLastMessage,
  248. executeReadTxt: require(path.join(funcDir, 'IO', 'read-txt.js')).executeReadTxt,
  249. executeSmartChatAppend: require(path.join(funcDir, 'chat', 'smart-chat-append.js')).executeSmartChatAppend,
  250. executeSaveTxt: require(path.join(funcDir, 'IO', 'save-txt.js')).executeSaveTxt,
  251. }
  252. ;(FUN_NODE_REGISTRY || []).filter((r) => r.category === 'io').forEach((def) => {
  253. const scriptPath = path.join(funcDir, def.script || def.type + '.js')
  254. try {
  255. const m = require(scriptPath)
  256. if (m[def.execute]) mod[def.execute] = m[def.execute]
  257. } catch (e) { /* skip missing script */ }
  258. })
  259. break
  260. case 'chat':
  261. mod = (() => {
  262. const chatHistory = require(path.join(funcDir, 'chat', 'chat-history.js'))
  263. const ocrChat = require(path.join(funcDir, 'chat', 'ocr-chat.js'))
  264. return {
  265. executeOcrChat: ocrChat.executeOcrChat,
  266. generateHistorySummary: chatHistory.generateHistorySummary,
  267. getHistorySummary: chatHistory.getHistorySummary,
  268. }
  269. })()
  270. break
  271. default:
  272. throw new Error(`fun-parser: 未知分类 ${category}`)
  273. }
  274. cache.set(key, mod)
  275. return mod
  276. }
  277. const rgbPattern = /^\((\d+),(\d+),(\d+)\)$/
  278. function parseRegion(regionArea) {
  279. if (!regionArea) return null
  280. if (typeof regionArea === 'string') {
  281. try {
  282. regionArea = JSON.parse(regionArea)
  283. } catch (e) {
  284. return null
  285. }
  286. }
  287. if (regionArea && typeof regionArea === 'object' && (!regionArea.topLeft || !regionArea.bottomRight)) return null
  288. return regionArea
  289. }
  290. async function run(actionType, action, ctx, device, folderPath) {
  291. const { variableContext, extractVarName, resolveValue, replaceVariablesInString, logOutVars, logMessage } = ctx
  292. const funcDir = ctx.compilerConfig && ctx.compilerConfig.funcDir
  293. if (!funcDir) return { success: false, error: 'compilerConfig.funcDir 未提供' }
  294. const bridged = await funAdbJsonBridge.runFunBridgedMethod(actionType, action, ctx, device, folderPath)
  295. if (bridged != null) return bridged
  296. switch (actionType) {
  297. case 'img-bounding-box-location': {
  298. const { executeImgBoundingBoxLocation } = get(funcDir, 'img')
  299. let screenshotPath = action.screenshot
  300. let regionPath = action.region
  301. if (action.inVars && Array.isArray(action.inVars)) {
  302. if (action.inVars.length === 1) {
  303. const firstValue = action.inVars[0]
  304. regionPath = firstValue != null && typeof firstValue === 'string' && !String(firstValue).includes('{') ? firstValue : action.inVars[0]
  305. screenshotPath = null
  306. } else if (action.inVars.length >= 2) {
  307. const sv = action.inVars[0]
  308. screenshotPath = sv != null && typeof sv === 'string' && !sv.includes('{') ? sv : action.inVars[0]
  309. const rv = action.inVars[1]
  310. regionPath = rv != null && typeof rv === 'string' && !rv.includes('{') ? rv : action.inVars[1]
  311. }
  312. }
  313. if (screenshotPath !== null && !screenshotPath) screenshotPath = action.screenshot
  314. if (!regionPath) regionPath = action.region
  315. if (!regionPath) return { success: false, error: '缺少区域截图路径' }
  316. if (screenshotPath === null && !device) return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' }
  317. const result = await executeImgBoundingBoxLocation({ device, screenshot: screenshotPath, region: regionPath, folderPath })
  318. if (!result.success) return { success: false, error: `图像区域定位失败: ${result.error}` }
  319. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  320. if (outputVarName) {
  321. variableContext[outputVarName] = result.corners && typeof result.corners === 'object' ? JSON.stringify(result.corners) : ''
  322. await logOutVars(action, variableContext, folderPath)
  323. }
  324. return { success: true, result: result.corners }
  325. }
  326. case 'img-center-point-location': {
  327. const { executeImgCenterPointLocation } = get(funcDir, 'img')
  328. let templatePath = action.template
  329. if (action.inVars?.length > 0) {
  330. const templateValue = action.inVars[0]
  331. templatePath = templateValue != null && typeof templateValue === 'string' && !String(templateValue).includes('{') ? templateValue : action.inVars[0]
  332. }
  333. if (!templatePath) templatePath = action.template
  334. if (!templatePath) return { success: false, error: '缺少模板图片路径' }
  335. if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
  336. return { success: false, error: 'img-center-point-location 至少填写 inVars[0]=模板路径' }
  337. }
  338. if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' }
  339. const result = await executeImgCenterPointLocation({ device, template: templatePath, folderPath })
  340. if (!result.success) return { success: false, error: `图像中心点定位失败: ${result.error}` }
  341. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  342. if (outputVarName) {
  343. if (result.center && typeof result.center === 'object' && result.center.x !== undefined && result.center.y !== undefined) {
  344. variableContext[outputVarName] = { x: Math.round(Number(result.center.x)), y: Math.round(Number(result.center.y)) }
  345. } else {
  346. variableContext[outputVarName] = null
  347. }
  348. await logOutVars(action, variableContext, folderPath)
  349. }
  350. return { success: true, result: result.center }
  351. }
  352. case 'img-cropping': {
  353. const { executeImgCropping } = get(funcDir, 'img')
  354. let imagePath = action.imagePath
  355. let squareSpec = action.squareSpec
  356. let savePath = action.savePath
  357. if (action.inVars && Array.isArray(action.inVars)) {
  358. if (action.inVars.length > 0) imagePath = action.inVars[0]
  359. if (action.inVars.length > 1) savePath = action.inVars[1]
  360. if (action.inVars.length > 2) squareSpec = action.inVars[2]
  361. }
  362. if (!imagePath) return { success: false, error: 'img-cropping 缺少 imagePath(inVars[0])' }
  363. if (!savePath) return { success: false, error: 'img-cropping 缺少 savePath(inVars[1])' }
  364. if (squareSpec === undefined || squareSpec === null || squareSpec === '') {
  365. return { success: false, error: 'img-cropping 缺少 squareSpec(inVars[2],如 [0.8,"w"])' }
  366. }
  367. const result = await executeImgCropping({ imagePath, squareSpec, savePath, folderPath })
  368. if (!result.success) return { success: false, error: result.error }
  369. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null
  370. if (outputVarName) {
  371. const outVal = result.path ?? result.value ?? result.result
  372. if (outVal !== undefined && outVal !== null) variableContext[outputVarName] = String(outVal)
  373. }
  374. await logOutVars(action, variableContext, folderPath)
  375. return { success: true, ...result }
  376. }
  377. case 'ocr': {
  378. const { executeOcr, executeOcrFindText } = get(funcDir, 'img')
  379. let imageOrText = action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0 ? action.inVars[0] : action.image
  380. if (imageOrText == null || String(imageOrText).trim() === '') return { success: false, error: 'ocr 缺少参数:图片路径或要查找的文字(inVars[0] / image)' }
  381. const baseDir = folderPath && typeof folderPath === 'string' ? folderPath : (ctx.compilerConfig && ctx.compilerConfig.projectRoot) || process.cwd()
  382. const fs = require('fs')
  383. const isAbsoluteOrDrive = String(imageOrText).startsWith('/') || String(imageOrText).includes(':')
  384. const hasSubPath = String(imageOrText).includes('/') || String(imageOrText).includes(path.sep)
  385. const resolvedPath = isAbsoluteOrDrive ? imageOrText : (hasSubPath ? path.join(baseDir, imageOrText) : path.join(baseDir, 'resources', imageOrText))
  386. const isImagePath = fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()
  387. const outputVarName = action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0 ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  388. if (isImagePath) {
  389. const result = await executeOcr({ imagePath: imageOrText, folderPath })
  390. if (!result.success) return { success: false, error: result.error }
  391. if (outputVarName) {
  392. variableContext[outputVarName] = result.text != null ? String(result.text) : ''
  393. await logOutVars(action, variableContext, folderPath)
  394. }
  395. return { success: true, result: result.text }
  396. }
  397. if (!device) return { success: false, error: 'ocr 按文字查找需设备截图,当前无设备' }
  398. const findResult = await executeOcrFindText({ device, findText: String(imageOrText).trim(), folderPath })
  399. if (!findResult.success) return { success: false, error: findResult.error }
  400. if (outputVarName) {
  401. variableContext[outputVarName] = findResult.center && typeof findResult.center === 'object'
  402. ? JSON.stringify({ x: findResult.center.x, y: findResult.center.y }) : ''
  403. await logOutVars(action, variableContext, folderPath)
  404. }
  405. return { success: true, result: findResult.center }
  406. }
  407. case 'read-last-message': {
  408. const { executeReadLastMessage } = get(funcDir, 'io')
  409. const inputVars = action.inVars || action.inputVars || []
  410. const outputVars = action.outVars || action.outputVars || []
  411. let textVar = outputVars.length > 0 ? extractVarName(String(outputVars[0]).trim()) : (action.textVariable ? extractVarName(action.textVariable) : null)
  412. let senderVar = outputVars.length > 1 ? extractVarName(String(outputVars[1]).trim()) : (action.senderVariable ? extractVarName(action.senderVariable) : null)
  413. let inputDataString = inputVars.length > 0 ? (inputVars[0] != null ? (typeof inputVars[0] === 'string' ? inputVars[0] : (Array.isArray(inputVars[0]) || typeof inputVars[0] === 'object' ? JSON.stringify(inputVars[0]) : String(inputVars[0]))) : null) : null
  414. if (!textVar && !senderVar) return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' }
  415. const result = await executeReadLastMessage({ folderPath, inputData: inputDataString, textVariable: textVar, senderVariable: senderVar })
  416. if (!result.success) return { success: false, error: result.error }
  417. if (textVar) variableContext[textVar] = result.text
  418. if (senderVar) variableContext[senderVar] = result.sender
  419. await logOutVars(action, variableContext, folderPath)
  420. return { success: true, text: result.text, sender: result.sender }
  421. }
  422. case 'read-txt':
  423. case 'read-text': {
  424. const { executeReadTxt } = get(funcDir, 'io')
  425. let filePath = action.filePath
  426. let varName = action.variable
  427. if (action.inVars?.length > 0) filePath = action.inVars[0]
  428. if (action.outVars?.length > 0) varName = extractVarName(String(action.outVars[0]).trim())
  429. else if (action.variable) varName = extractVarName(action.variable)
  430. if (!filePath) return { success: false, error: 'read-txt 缺少 filePath 参数' }
  431. if (!varName) return { success: false, error: 'read-txt 缺少 variable 参数' }
  432. const result = await executeReadTxt({ filePath, folderPath })
  433. if (!result.success) return { success: false, error: result.error }
  434. const content = result.content || ''
  435. variableContext[varName] = typeof content === 'string' ? content : String(content)
  436. if (variableContext[varName] === undefined || variableContext[varName] === null) variableContext[varName] = ''
  437. await logOutVars(action, variableContext, folderPath)
  438. return { success: true, content: result.content }
  439. }
  440. case 'smart-chat-append': {
  441. const { executeSmartChatAppend } = get(funcDir, 'io')
  442. let history = action.history
  443. let current = action.current
  444. if (action.inVars && Array.isArray(action.inVars)) {
  445. if (action.inVars.length > 0) history = action.inVars[0]
  446. if (action.inVars.length > 1) current = action.inVars[1]
  447. }
  448. if (history === undefined || history === null) history = ''
  449. if (current === undefined || current === null) current = ''
  450. const result = await executeSmartChatAppend({
  451. history: typeof history === 'string' ? history : String(history),
  452. current: typeof current === 'string' ? current : String(current),
  453. })
  454. if (!result.success) return { success: false, error: result.error }
  455. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  456. if (outputVarName && result.result) variableContext[outputVarName] = result.result
  457. return { success: true, result: result.result }
  458. }
  459. case 'save-txt':
  460. case 'save-text': {
  461. const { executeSaveTxt } = get(funcDir, 'io')
  462. let filePath = action.filePath
  463. let content = action.content
  464. if (action.inVars && Array.isArray(action.inVars)) {
  465. if (action.inVars.length > 0) content = action.inVars[0]
  466. if (action.inVars.length > 1) filePath = action.inVars[1]
  467. }
  468. if (!filePath) return { success: false, error: 'save-txt 缺少 filePath 参数' }
  469. if (content === undefined || content === null) return { success: false, error: 'save-txt 缺少 content 参数' }
  470. const result = await executeSaveTxt({ filePath, content, folderPath })
  471. if (!result.success) return { success: false, error: result.error }
  472. if (action.outVars?.[0] != null) {
  473. const outputVarName = extractVarName(String(action.outVars[0]).trim())
  474. if (outputVarName) variableContext[outputVarName] = result.success ? '1' : '0'
  475. }
  476. await logOutVars(action, variableContext, folderPath)
  477. return { success: true }
  478. }
  479. case 'extract-messages':
  480. case 'ocr-chat':
  481. case 'ocr-chat-history':
  482. case 'extract-chat-history': {
  483. const { executeOcrChat } = get(funcDir, 'chat')
  484. const folderName = folderPath.split(/[/\\]/).pop()
  485. let avatar1Path = null
  486. let avatar2Path = null
  487. let avatar1Name, avatar2Name, regionArea = null
  488. let friendRgb = null, myRgb = null
  489. if (action.inVars && Array.isArray(action.inVars)) {
  490. if (action.inVars.length >= 3) {
  491. const param1 = action.inVars[0]
  492. const param2 = action.inVars[1]
  493. if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
  494. friendRgb = param1.trim()
  495. myRgb = param2.trim()
  496. regionArea = action.inVars[2]
  497. } else {
  498. avatar1Name = action.inVars[0]
  499. avatar2Name = action.inVars[1]
  500. regionArea = action.inVars[2]
  501. }
  502. regionArea = parseRegion(regionArea)
  503. } else if (action.inVars.length >= 2) {
  504. const param1 = action.inVars[0]
  505. const param2 = action.inVars[1]
  506. if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
  507. friendRgb = param1.trim()
  508. myRgb = param2.trim()
  509. } else {
  510. avatar1Name = param1
  511. avatar2Name = param2
  512. }
  513. } else if (action.inVars.length === 1) {
  514. avatar1Name = action.inVars[0]
  515. avatar2Name = action.avatar2 || action.myAvatar
  516. }
  517. } else {
  518. avatar1Name = action.avatar1 || action.friendAvatar
  519. avatar2Name = action.avatar2 || action.myAvatar
  520. }
  521. if (avatar1Name) {
  522. const resolved = resolveValue(avatar1Name, variableContext)
  523. if (resolved) avatar1Path = `${folderName}/resources/${resolved}`
  524. }
  525. if (avatar2Name) {
  526. const resolved = resolveValue(avatar2Name, variableContext)
  527. if (resolved) avatar2Path = `${folderName}/resources/${resolved}`
  528. }
  529. const regionParam = regionArea && typeof regionArea === 'string' ? (() => { try { return JSON.parse(regionArea) } catch (e) { return null } })() : regionArea
  530. const chatResult = await executeOcrChat({ device, avatar1: avatar1Path, avatar2: avatar2Path, folderPath, region: regionParam, friendRgb, myRgb })
  531. if (!chatResult.success) return { success: false, error: `提取消息记录失败: ${chatResult.error}` }
  532. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  533. if (outputVarName) {
  534. variableContext[outputVarName] = chatResult.messagesJson || JSON.stringify(chatResult.messages || [])
  535. await logOutVars(action, variableContext, folderPath)
  536. }
  537. return {
  538. success: true,
  539. messages: chatResult.messages || [],
  540. messagesJson: chatResult.messagesJson || JSON.stringify(chatResult.messages || []),
  541. lastMessage: chatResult.messages?.length > 0 ? chatResult.messages[chatResult.messages.length - 1] : null,
  542. }
  543. }
  544. case 'save-messages':
  545. case 'generate-summary':
  546. case 'generate-history-summary': {
  547. const { generateHistorySummary } = get(funcDir, 'chat')
  548. if (!action.variable) return { success: false, error: '缺少变量名' }
  549. const messages = variableContext[action.variable]
  550. if (!messages) return { success: false, error: `变量 ${action.variable} 不存在或为空` }
  551. const modelName = action.model || 'gpt-5-nano-ca'
  552. const result = await generateHistorySummary(messages, folderPath, modelName)
  553. if (!result.success) return { success: false, error: `生成消息记录总结失败: ${result.error}` }
  554. if (action.summaryVariable) variableContext[action.summaryVariable] = result.summary
  555. return { success: true, summary: result.summary }
  556. }
  557. case 'ai-generate': {
  558. const { getHistorySummary } = get(funcDir, 'chat')
  559. let prompt = resolveValue(action.prompt, variableContext)
  560. if (prompt && prompt.includes('{historySummary}') && getHistorySummary) {
  561. let historySummary = variableContext['historySummary'] || ''
  562. if (!historySummary) {
  563. historySummary = await getHistorySummary(folderPath)
  564. if (historySummary) variableContext['historySummary'] = historySummary
  565. }
  566. prompt = prompt.replace(/{historySummary}/g, historySummary)
  567. }
  568. prompt = replaceVariablesInString(prompt, variableContext)
  569. try {
  570. const response = await fetch('https://ai-anim.com/api/text2textByModel', {
  571. method: 'POST',
  572. headers: { 'Content-Type': 'application/json' },
  573. body: JSON.stringify({ prompt, modelName: action.model || 'gpt-5-nano-ca' }),
  574. })
  575. if (!response.ok) return { success: false, error: `AI请求失败: ${response.statusText}` }
  576. const data = await response.json()
  577. let rawResult = ''
  578. if (data.data?.output_text) rawResult = data.data.output_text
  579. else if (data.output_text) rawResult = data.output_text
  580. else if (data.text) rawResult = data.text
  581. else if (data.content) rawResult = data.content
  582. else if (typeof data.data === 'string') rawResult = data.data
  583. else rawResult = JSON.stringify(data)
  584. rawResult = rawResult ? String(rawResult) : ''
  585. let result = rawResult
  586. try {
  587. try {
  588. const jsonResult = JSON.parse(rawResult.trim())
  589. if (jsonResult.reply) result = jsonResult.reply
  590. } catch (e) {
  591. const codeBlockMatch = rawResult.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/)
  592. if (codeBlockMatch) {
  593. try {
  594. const jsonResult = JSON.parse(codeBlockMatch[1])
  595. if (jsonResult.reply) result = jsonResult.reply
  596. } catch (e2) {}
  597. } else {
  598. const jsonMatch = rawResult.match(/\{\s*"reply"\s*:\s*"([^"]+)"\s*\}/)
  599. if (jsonMatch) result = jsonMatch[1]
  600. else {
  601. const lines = rawResult.split('\n').map((l) => l.trim()).filter(Boolean)
  602. for (const line of lines) {
  603. if (line.startsWith('{') && line.includes('"reply"')) {
  604. try {
  605. const jsonResult = JSON.parse(line)
  606. if (jsonResult.reply) { result = jsonResult.reply; break }
  607. } catch (e3) {}
  608. }
  609. }
  610. }
  611. }
  612. }
  613. } catch (parseError) {}
  614. if (action.outVars?.length > 0) {
  615. if (action.outVars.length > 0) {
  616. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null
  617. if (outputVarName) variableContext[outputVarName] = result
  618. }
  619. if (action.outVars.length > 1) {
  620. const callbackVarName = action.outVars?.[1] != null ? extractVarName(String(action.outVars[1]).trim()) : null
  621. if (callbackVarName) variableContext[callbackVarName] = 1
  622. }
  623. await logOutVars(action, variableContext, folderPath)
  624. } else if (action.variable) {
  625. const outputVarName = extractVarName(action.variable)
  626. if (outputVarName) variableContext[outputVarName] = result
  627. }
  628. if (!action.outVars || action.outVars.length <= 1) {
  629. if (action.inVars?.length > 1 && action.inVars[1] != null) {
  630. const callbackVarName = extractVarName(String(action.inVars[1]).trim())
  631. if (callbackVarName) variableContext[callbackVarName] = 1
  632. }
  633. }
  634. return { success: true, result }
  635. } catch (error) {
  636. return { success: false, error: `AI生成失败: ${error.message}` }
  637. }
  638. }
  639. case 'string-press': {
  640. const api = ctx.electronAPI
  641. const inVars = action.inVars || []
  642. const targetText = inVars.length > 0 ? (inVars[0] ?? action.value) : (action.value ?? '')
  643. if (!targetText) return { success: false, error: 'string-press 操作缺少文字内容' }
  644. if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
  645. const matchResult = await api.findTextAndGetCoordinate(device, targetText)
  646. if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
  647. const { x, y } = matchResult.clickPosition
  648. if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
  649. const tapResult = await api.sendTap(device, x, y)
  650. if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
  651. return { success: true }
  652. }
  653. default: {
  654. const regDef = REGISTRY_BY_TYPE.get(actionType)
  655. if (regDef) {
  656. if (regDef.customRun) {
  657. const script = getRegistryScript(funcDir, actionType)
  658. if (script && typeof script.runNode === 'function') {
  659. const runCtx = { ...ctx, get, funcDir, folderPath, device }
  660. const result = await script.runNode(action, runCtx)
  661. if (result && result.success !== false) await logOutVars(action, variableContext, folderPath)
  662. return result != null ? result : { success: true }
  663. }
  664. }
  665. const inKeys = regDef.in || []
  666. const inAlt = regDef.inAlt || {}
  667. const input = {}
  668. inKeys.forEach((key, i) => {
  669. let val = action[key]
  670. if (action.inVars && action.inVars[i] !== undefined) val = action.inVars[i]
  671. if (val === undefined && inAlt[key]) val = action[inAlt[key]]
  672. input[key] = val != null ? String(val).trim() : val
  673. })
  674. input.folderPath = folderPath
  675. if (actionType === 'remove-folder') {
  676. let rec = action.recursive
  677. if ((rec === undefined || rec === null || rec === '') && action.inVars && action.inVars.length > 1 && action.inVars[1] !== undefined) {
  678. rec = action.inVars[1]
  679. }
  680. if (rec !== undefined && rec !== null && rec !== '') {
  681. input.recursive = typeof rec === 'string' ? rec.trim() : String(rec).trim()
  682. }
  683. }
  684. const mod = get(funcDir, regDef.category)
  685. const fn = mod[regDef.execute]
  686. if (!fn) {
  687. return { success: false, error: `fun-parser: ${regDef.execute} not found` }
  688. }
  689. let result
  690. try {
  691. result = await fn(input)
  692. } catch (e) {
  693. return { success: false, error: (e && (e.message || String(e))) || 'execute threw' }
  694. }
  695. if (!result || !result.success) {
  696. return { success: false, error: (result && result.error) || 'execute failed' }
  697. }
  698. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null
  699. if (outputVarName && result != null) {
  700. const outVal = result.path ?? result.value ?? result.result
  701. if (outVal !== undefined && outVal !== null) {
  702. if (actionType === 'json' && Array.isArray(outVal)) variableContext[outputVarName] = outVal
  703. else variableContext[outputVarName] = typeof outVal === 'string' ? outVal : String(outVal)
  704. }
  705. }
  706. await logOutVars(action, variableContext, folderPath)
  707. return { success: true, ...result }
  708. }
  709. return { success: false, error: `fun-parser 不支持的 type: ${actionType}` }
  710. }
  711. }
  712. }
  713. const FUN_TYPES = new Set([
  714. 'ai',
  715. 'img-bounding-box-location', 'img-center-point-location', 'img-cropping', 'ocr',
  716. 'read-last-message', 'read-txt', 'read-text', 'smart-chat-append', 'save-txt', 'save-text',
  717. 'extract-messages', 'ocr-chat', 'ocr-chat-history', 'extract-chat-history',
  718. 'save-messages', 'generate-summary', 'generate-history-summary',
  719. 'ai-generate',
  720. 'string-press',
  721. ].concat(REGISTERED_TYPES))
  722. function supports(type) {
  723. return FUN_TYPES.has(type)
  724. }
  725. module.exports = { types, parse, execute, runAction, get, run, supports }