fun-parser.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974
  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 { assertStrictKeys } = require('../../action-schema.js')
  8. const FUN_NODE_REGISTRY = require('./fun-node-registry.js')
  9. const funAdbJsonBridge = require('./fun-adb-json-bridge.js')
  10. /** fun 结点在工作流 JSON 中仅允许这些字段(外加 method 对应的语义全在 inVars/outVars 中表达) */
  11. const FUN_STANDARD_KEYS = new Set([
  12. 'type',
  13. 'method',
  14. 'inVars',
  15. 'outVars',
  16. 'condition',
  17. 'delay',
  18. 'times',
  19. 'timeout',
  20. 'retry',
  21. 'data',
  22. 'model',
  23. ])
  24. function validateFunStandardFormat (action) {
  25. if (!action || action.type !== 'fun') return { ok: true }
  26. const extra = Object.keys(action).filter((k) => !FUN_STANDARD_KEYS.has(k))
  27. if (extra.length > 0) {
  28. return {
  29. ok: false,
  30. error: `fun 结点仅允许字段: ${[...FUN_STANDARD_KEYS].sort().join(', ')}。禁止使用: ${extra.join(', ')}`,
  31. }
  32. }
  33. if (action.method == null || String(action.method).trim() === '') {
  34. return { ok: false, error: 'fun 结点缺少 method' }
  35. }
  36. if (!Array.isArray(action.inVars)) {
  37. return { ok: false, error: 'fun 结点必须包含 inVars 数组(无入参写 [])' }
  38. }
  39. if (!Array.isArray(action.outVars)) {
  40. return { ok: false, error: 'fun 结点必须包含 outVars 数组(无出参写 [])' }
  41. }
  42. return { ok: true }
  43. }
  44. /** fun.method 须与注册表/脚本一致,不做别名兼容 */
  45. function normalizeRegistryMethodName (name) {
  46. if (name == null || name === '') return name
  47. return String(name).trim()
  48. }
  49. const LEGACY_FUN_TYPES = [
  50. 'fun', 'ai', 'io',
  51. 'read-txt', 'read-text', 'save-txt', 'save-text',
  52. 'img-bounding-box-location', 'img-center-point-location', 'img-cropping',
  53. 'ocr',
  54. 'read-last-message', 'smart-chat-append',
  55. 'extract-messages', 'ocr-chat', 'ocr-chat-history', 'extract-chat-history',
  56. 'save-messages', 'generate-summary', 'generate-history-summary',
  57. 'ai-generate', 'string-press',
  58. ]
  59. const REGISTERED_TYPES = (FUN_NODE_REGISTRY && Array.isArray(FUN_NODE_REGISTRY)) ? FUN_NODE_REGISTRY.map((r) => r.type) : []
  60. const FUN_REGISTRY_TYPES = LEGACY_FUN_TYPES.concat(REGISTERED_TYPES)
  61. const types = FUN_REGISTRY_TYPES
  62. const REGISTRY_BY_TYPE = new Map((FUN_NODE_REGISTRY || []).map((r) => [r.type, r]))
  63. const scriptCache = new Map()
  64. function getRegistryScript(funcDir, type) {
  65. const def = REGISTRY_BY_TYPE.get(type)
  66. if (!def) return null
  67. const key = `${funcDir}:${type}`
  68. if (!scriptCache.has(key)) {
  69. try {
  70. const scriptPath = path.join(funcDir, def.script || def.type + '.js')
  71. scriptCache.set(key, require(scriptPath))
  72. } catch (e) {
  73. scriptCache.set(key, null)
  74. }
  75. }
  76. return scriptCache.get(key)
  77. }
  78. function pickFlowFields (action) {
  79. const out = {}
  80. for (const k of ['condition', 'delay', 'times', 'timeout', 'retry', 'data', 'model']) {
  81. if (Object.prototype.hasOwnProperty.call(action, k)) out[k] = action[k]
  82. }
  83. return out
  84. }
  85. function parse (action, parseContext) {
  86. const { extractVarName } = parseContext
  87. const path = parseContext.actionPath || 'fun'
  88. const regDef = REGISTRY_BY_TYPE.get(action.type)
  89. if (regDef) {
  90. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  91. if (!Array.isArray(action.inVars) || !Array.isArray(action.outVars)) {
  92. throw new Error(`${path}: 注册表结点 ${action.type} 须含 inVars、outVars 数组`)
  93. }
  94. const funcDir = (parseContext.compilerConfig && parseContext.compilerConfig.funcDir) || __dirname
  95. if (regDef.customParse) {
  96. const script = getRegistryScript(funcDir, action.type)
  97. if (script && typeof script.parseNode === 'function') return script.parseNode(action, parseContext)
  98. }
  99. const inSpecLen = (regDef.in && regDef.in.length) || 0
  100. if (!regDef.customParse && inSpecLen > 0 && action.inVars.length < inSpecLen) {
  101. throw new Error(`${path}: ${action.type} 至少需要 ${inSpecLen} 个 inVars`)
  102. }
  103. const inVars = action.inVars.map((v) => extractVarName(v))
  104. const outVars = action.outVars.map((v) => extractVarName(v))
  105. const parsed = { type: action.type, inVars, outVars, ...pickFlowFields(action) }
  106. const inKeys = regDef.in || []
  107. inKeys.forEach((key, i) => {
  108. parsed[key] = inVars[i]
  109. })
  110. if (outVars.length > 0) parsed.variable = extractVarName(action.outVars[0])
  111. return parsed
  112. }
  113. switch (action.type) {
  114. case 'fun': {
  115. const vf = validateFunStandardFormat(action)
  116. if (!vf.ok) throw new Error(`${path}: ${vf.error}`)
  117. return {
  118. type: 'fun',
  119. method: action.method,
  120. inVars: action.inVars.map((v) => extractVarName(v)),
  121. outVars: action.outVars.map((v) => extractVarName(v)),
  122. ...pickFlowFields(action),
  123. }
  124. }
  125. case 'extract-messages':
  126. case 'ocr-chat':
  127. case 'ocr-chat-history':
  128. case 'extract-chat-history': {
  129. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  130. if (!Array.isArray(action.inVars) || action.inVars.length < 3) {
  131. throw new Error(`${path}: ${action.type} 须 inVars 至少 3 项(如好友 RGB、我的 RGB、区域)`)
  132. }
  133. if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
  134. throw new Error(`${path}: ${action.type} 须 outVars 至少 1 项`)
  135. }
  136. const inVars = action.inVars.map((v) => extractVarName(v))
  137. const outVars = action.outVars.map((v) => extractVarName(v))
  138. return {
  139. type: action.type,
  140. inVars,
  141. outVars,
  142. variable: extractVarName(action.outVars[0]),
  143. ...pickFlowFields(action),
  144. }
  145. }
  146. case 'save-messages':
  147. case 'generate-summary':
  148. case 'generate-history-summary': {
  149. assertStrictKeys(action, ['type', 'variable', 'summaryVariable', 'model'], path)
  150. if (action.variable === undefined || action.variable === null || action.variable === '') {
  151. throw new Error(`${path}: ${action.type} 须提供 variable(消息来源)`)
  152. }
  153. return {
  154. type: action.type,
  155. variable: extractVarName(action.variable),
  156. summaryVariable: action.summaryVariable != null ? extractVarName(action.summaryVariable) : undefined,
  157. ...pickFlowFields(action),
  158. }
  159. }
  160. case 'ai-generate': {
  161. assertStrictKeys(action, ['type', 'inVars', 'outVars', 'model'], path)
  162. if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
  163. throw new Error(`${path}: ai-generate 须在 inVars[0] 填写 prompt`)
  164. }
  165. if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
  166. throw new Error(`${path}: ai-generate 须至少 1 个 outVars`)
  167. }
  168. const inVars = action.inVars.map((v) => extractVarName(v))
  169. const outVars = action.outVars.map((v) => extractVarName(v))
  170. return {
  171. type: 'ai-generate',
  172. inVars,
  173. outVars,
  174. variable: extractVarName(action.outVars[0]),
  175. ...pickFlowFields(action),
  176. }
  177. }
  178. case 'read-last-message': {
  179. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  180. if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
  181. throw new Error(`${path}: read-last-message 须至少 1 个 inVars`)
  182. }
  183. if (!Array.isArray(action.outVars) || action.outVars.length < 2) {
  184. throw new Error(`${path}: read-last-message 须至少 2 个 outVars(文本、发送者)`)
  185. }
  186. const inVars = action.inVars.map((v) => extractVarName(v))
  187. const outVars = action.outVars.map((v) => extractVarName(v))
  188. return {
  189. type: 'read-last-message',
  190. inVars,
  191. outVars,
  192. inputVar: inVars[0],
  193. textVariable: outVars[0],
  194. senderVariable: outVars[1],
  195. ...pickFlowFields(action),
  196. }
  197. }
  198. case 'read-txt':
  199. case 'read-text': {
  200. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  201. if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
  202. throw new Error(`${path}: ${action.type} 须至少 1 个 inVars(文件路径)`)
  203. }
  204. if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
  205. throw new Error(`${path}: ${action.type} 须至少 1 个 outVars`)
  206. }
  207. const inVars = action.inVars.map((v) => extractVarName(v))
  208. const outVars = action.outVars.map((v) => extractVarName(v))
  209. return {
  210. type: action.type,
  211. inVars,
  212. outVars,
  213. filePath: inVars[0],
  214. variable: outVars[0],
  215. ...pickFlowFields(action),
  216. }
  217. }
  218. case 'save-txt':
  219. case 'save-text': {
  220. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  221. if (!Array.isArray(action.inVars) || action.inVars.length < 2) {
  222. throw new Error(`${path}: ${action.type} 须 inVars[0]=内容、inVars[1]=路径`)
  223. }
  224. if (!Array.isArray(action.outVars)) {
  225. throw new Error(`${path}: ${action.type} 须含 outVars 数组(可无输出写 [])`)
  226. }
  227. const inVars = action.inVars.map((v) => extractVarName(v))
  228. const outVars = action.outVars.map((v) => extractVarName(v))
  229. const parsed = {
  230. type: action.type,
  231. inVars,
  232. outVars,
  233. content: inVars[0],
  234. filePath: inVars[1],
  235. ...pickFlowFields(action),
  236. }
  237. if (outVars.length > 0) parsed.variable = outVars[0]
  238. return parsed
  239. }
  240. case 'img-bounding-box-location': {
  241. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  242. if (!Array.isArray(action.inVars) || action.inVars.length < 2) {
  243. throw new Error(`${path}: img-bounding-box-location 须 inVars[0]=截图、inVars[1]=区域模板`)
  244. }
  245. if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
  246. throw new Error(`${path}: img-bounding-box-location 须至少 1 个 outVars`)
  247. }
  248. const inVars = action.inVars.map((v) => extractVarName(v))
  249. const outVars = action.outVars.map((v) => extractVarName(v))
  250. return {
  251. type: 'img-bounding-box-location',
  252. inVars,
  253. outVars,
  254. screenshot: inVars[0],
  255. region: inVars[1],
  256. variable: outVars[0],
  257. ...pickFlowFields(action),
  258. }
  259. }
  260. case 'img-center-point-location': {
  261. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  262. if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
  263. throw new Error(`${path}: img-center-point-location 须至少 inVars[0]=模板路径`)
  264. }
  265. if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
  266. throw new Error(`${path}: img-center-point-location 须至少 1 个 outVars`)
  267. }
  268. const inVars = action.inVars.map((v, i) => (i === 1 && Array.isArray(v) ? v : extractVarName(v)))
  269. const outVars = action.outVars.map((v) => extractVarName(v))
  270. return {
  271. type: 'img-center-point-location',
  272. inVars,
  273. outVars,
  274. template: inVars[0],
  275. scaleRange: Array.isArray(action.inVars[1]) ? action.inVars[1] : undefined,
  276. variable: outVars[0],
  277. ...pickFlowFields(action),
  278. }
  279. }
  280. case 'img-cropping': {
  281. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  282. if (!Array.isArray(action.inVars) || action.inVars.length < 3) {
  283. throw new Error(`${path}: img-cropping 须 inVars[0]=图、inVars[1]=保存路径、inVars[2]=裁剪规格`)
  284. }
  285. if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
  286. throw new Error(`${path}: img-cropping 须至少 1 个 outVars`)
  287. }
  288. const inVars = action.inVars.map((v, i) => (i === 2 && Array.isArray(v) ? v : extractVarName(v)))
  289. const outVars = action.outVars.map((v) => extractVarName(v))
  290. return {
  291. type: 'img-cropping',
  292. inVars,
  293. outVars,
  294. imagePath: inVars[0],
  295. savePath: inVars[1],
  296. squareSpec: inVars[2],
  297. variable: outVars[0],
  298. ...pickFlowFields(action),
  299. }
  300. }
  301. case 'ocr': {
  302. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  303. if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
  304. throw new Error(`${path}: ocr 须至少 1 个 inVars(图片路径)`)
  305. }
  306. if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
  307. throw new Error(`${path}: ocr 须至少 1 个 outVars`)
  308. }
  309. const inVars = action.inVars.map((v) => extractVarName(v))
  310. const outVars = action.outVars.map((v) => extractVarName(v))
  311. return {
  312. type: 'ocr',
  313. inVars,
  314. outVars,
  315. image: inVars[0],
  316. variable: outVars[0],
  317. ...pickFlowFields(action),
  318. }
  319. }
  320. case 'smart-chat-append': {
  321. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  322. if (!Array.isArray(action.inVars) || action.inVars.length < 2) {
  323. throw new Error(`${path}: smart-chat-append 须 2 个 inVars`)
  324. }
  325. if (!Array.isArray(action.outVars) || action.outVars.length < 1) {
  326. throw new Error(`${path}: smart-chat-append 须至少 1 个 outVars`)
  327. }
  328. return {
  329. type: 'smart-chat-append',
  330. inVars: action.inVars.map((v) => extractVarName(v)),
  331. outVars: action.outVars.map((v) => extractVarName(v)),
  332. ...pickFlowFields(action),
  333. }
  334. }
  335. default: {
  336. assertStrictKeys(action, ['type', 'inVars', 'outVars'], path)
  337. if (!Array.isArray(action.inVars) || !Array.isArray(action.outVars)) {
  338. throw new Error(`${path}: ${action.type} 须含 inVars、outVars 数组`)
  339. }
  340. return {
  341. type: action.type,
  342. inVars: action.inVars.map((v) => extractVarName(v)),
  343. outVars: action.outVars.map((v) => extractVarName(v)),
  344. ...pickFlowFields(action),
  345. }
  346. }
  347. }
  348. }
  349. async function execute(action, ctx) {
  350. return { success: true }
  351. }
  352. async function runAction(action, device, folderPath, resolution, ctx) {
  353. const { variableContext, evaluateCondition, registry, executeAction } = ctx
  354. if (action.condition && !evaluateCondition(action.condition, variableContext)) {
  355. return { success: true, skipped: true }
  356. }
  357. if (action.type === 'fun') {
  358. const vf = validateFunStandardFormat(action)
  359. if (!vf.ok) return { success: false, error: vf.error }
  360. }
  361. const resolvedAction = variableParser.resolveActionInputs(action, variableContext)
  362. ctx.resolution = resolution || ctx.resolution || { width: 1080, height: 1920 }
  363. if (resolvedAction.type === 'fun') {
  364. const m = resolvedAction.method != null ? String(resolvedAction.method).trim() : ''
  365. if (!m) return { success: false, error: 'fun 结点缺少 method(如 adb-click、json-to-arr)' }
  366. return run(normalizeRegistryMethodName(m), resolvedAction, ctx, device, folderPath)
  367. }
  368. if (resolvedAction.type === 'ai' && resolvedAction.method) {
  369. return run(resolvedAction.method, resolvedAction, ctx, device, folderPath)
  370. }
  371. if (resolvedAction.type === 'io' && resolvedAction.method) {
  372. const m = normalizeRegistryMethodName(resolvedAction.method)
  373. return run(m, resolvedAction, ctx, device, folderPath)
  374. }
  375. if (supports(resolvedAction.type)) {
  376. return run(resolvedAction.type, resolvedAction, ctx, device, folderPath)
  377. }
  378. if (registry && registry[resolvedAction.type]) {
  379. const execCtx = {
  380. device,
  381. folderPath,
  382. resolution,
  383. variableContext,
  384. compilerConfig: ctx.compilerConfig,
  385. api: ctx.electronAPI,
  386. extractVarName: ctx.extractVarName,
  387. resolveValue: ctx.resolveValue,
  388. replaceVariablesInString: ctx.replaceVariablesInString,
  389. evaluateCondition: ctx.evaluateCondition,
  390. evaluateExpression: ctx.evaluateExpression,
  391. getActionName: ctx.getActionName,
  392. logMessage: ctx.logMessage,
  393. logOutVars: ctx.logOutVars,
  394. parseDelayString: ctx.parseDelayString,
  395. calculateWaitTime: ctx.calculateWaitTime,
  396. DEFAULT_SCROLL_DISTANCE: ctx.DEFAULT_SCROLL_DISTANCE,
  397. }
  398. return await executeAction(resolvedAction.type, resolvedAction, execCtx)
  399. }
  400. return { success: false, error: `未知的操作类型: ${resolvedAction.type}` }
  401. }
  402. const cache = new Map()
  403. function get(funcDir, category) {
  404. if (!funcDir) throw new Error('fun-parser: funcDir 未提供')
  405. const key = `${funcDir}:${category}`
  406. if (cache.has(key)) return cache.get(key)
  407. let mod
  408. switch (category) {
  409. case 'img':
  410. mod = {
  411. executeImgBoundingBoxLocation: require(path.join(funcDir, 'img', 'img-bounding-box-location.js')).executeImgBoundingBoxLocation,
  412. executeImgCenterPointLocation: require(path.join(funcDir, 'img', 'img-center-point-location.js')).executeImgCenterPointLocation,
  413. executeImgCropping: require(path.join(funcDir, 'img', 'img-cropping.js')).executeImgCropping,
  414. executeOcr: require(path.join(funcDir, 'ocr.js')).executeOcr,
  415. executeOcrFindText: require(path.join(funcDir, 'ocr.js')).executeOcrFindText,
  416. }
  417. break
  418. case 'io':
  419. mod = {
  420. executeReadLastMessage: require(path.join(funcDir, 'chat', 'read-last-message.js')).executeReadLastMessage,
  421. executeReadTxt: require(path.join(funcDir, 'IO', 'read-txt.js')).executeReadTxt,
  422. executeSmartChatAppend: require(path.join(funcDir, 'chat', 'smart-chat-append.js')).executeSmartChatAppend,
  423. executeSaveTxt: require(path.join(funcDir, 'IO', 'save-txt.js')).executeSaveTxt,
  424. }
  425. ;(FUN_NODE_REGISTRY || []).filter((r) => r.category === 'io').forEach((def) => {
  426. const scriptPath = path.join(funcDir, def.script || def.type + '.js')
  427. try {
  428. const m = require(scriptPath)
  429. if (m[def.execute]) mod[def.execute] = m[def.execute]
  430. } catch (e) { /* skip missing script */ }
  431. })
  432. break
  433. case 'fun':
  434. mod = {}
  435. ;(FUN_NODE_REGISTRY || []).filter((r) => r.category === 'fun').forEach((def) => {
  436. const scriptPath = path.join(funcDir, def.script || def.type + '.js')
  437. try {
  438. const m = require(scriptPath)
  439. if (m[def.execute]) mod[def.execute] = m[def.execute]
  440. } catch (e) { /* skip missing script */ }
  441. })
  442. break
  443. case 'chat':
  444. mod = (() => {
  445. const chatHistory = require(path.join(funcDir, 'chat', 'chat-history.js'))
  446. const ocrChat = require(path.join(funcDir, 'chat', 'ocr-chat.js'))
  447. return {
  448. executeOcrChat: ocrChat.executeOcrChat,
  449. generateHistorySummary: chatHistory.generateHistorySummary,
  450. getHistorySummary: chatHistory.getHistorySummary,
  451. }
  452. })()
  453. break
  454. default:
  455. throw new Error(`fun-parser: 未知分类 ${category}`)
  456. }
  457. cache.set(key, mod)
  458. return mod
  459. }
  460. const rgbPattern = /^\((\d+),(\d+),(\d+)\)$/
  461. function parseRegion(regionArea) {
  462. if (!regionArea) return null
  463. if (typeof regionArea === 'string') {
  464. try {
  465. regionArea = JSON.parse(regionArea)
  466. } catch (e) {
  467. return null
  468. }
  469. }
  470. if (regionArea && typeof regionArea === 'object' && (!regionArea.topLeft || !regionArea.bottomRight)) return null
  471. return regionArea
  472. }
  473. async function run(actionType, action, ctx, device, folderPath) {
  474. const { variableContext, extractVarName, resolveValue, replaceVariablesInString, logOutVars, logMessage } = ctx
  475. const funcDir = ctx.compilerConfig && ctx.compilerConfig.funcDir
  476. if (!funcDir) return { success: false, error: 'compilerConfig.funcDir 未提供' }
  477. const bridged = await funAdbJsonBridge.runFunBridgedMethod(actionType, action, ctx, device, folderPath)
  478. if (bridged != null) return bridged
  479. switch (actionType) {
  480. case 'img-bounding-box-location': {
  481. const { executeImgBoundingBoxLocation } = get(funcDir, 'img')
  482. let screenshotPath = action.screenshot
  483. let regionPath = action.region
  484. if (action.inVars && Array.isArray(action.inVars)) {
  485. if (action.inVars.length === 1) {
  486. const firstValue = action.inVars[0]
  487. regionPath = firstValue != null && typeof firstValue === 'string' && !String(firstValue).includes('{') ? firstValue : action.inVars[0]
  488. screenshotPath = null
  489. } else if (action.inVars.length >= 2) {
  490. const sv = action.inVars[0]
  491. screenshotPath = sv != null && typeof sv === 'string' && !sv.includes('{') ? sv : action.inVars[0]
  492. const rv = action.inVars[1]
  493. regionPath = rv != null && typeof rv === 'string' && !rv.includes('{') ? rv : action.inVars[1]
  494. }
  495. }
  496. if (screenshotPath !== null && !screenshotPath) screenshotPath = action.screenshot
  497. if (!regionPath) regionPath = action.region
  498. if (!regionPath) return { success: false, error: '缺少区域截图路径' }
  499. if (screenshotPath === null && !device) return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' }
  500. const result = await executeImgBoundingBoxLocation({ device, screenshot: screenshotPath, region: regionPath, folderPath })
  501. if (!result.success) return { success: false, error: `图像区域定位失败: ${result.error}` }
  502. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  503. if (outputVarName) {
  504. variableContext[outputVarName] = result.corners && typeof result.corners === 'object' ? JSON.stringify(result.corners) : ''
  505. await logOutVars(action, variableContext, folderPath)
  506. }
  507. return { success: true, result: result.corners }
  508. }
  509. case 'img-center-point-location': {
  510. const { executeImgCenterPointLocation } = get(funcDir, 'img')
  511. let templatePath = action.template
  512. if (action.inVars?.length > 0) {
  513. const templateValue = action.inVars[0]
  514. templatePath = templateValue != null && typeof templateValue === 'string' && !String(templateValue).includes('{') ? templateValue : action.inVars[0]
  515. }
  516. if (!templatePath) templatePath = action.template
  517. if (!templatePath) return { success: false, error: '缺少模板图片路径' }
  518. if (!Array.isArray(action.inVars) || action.inVars.length < 1) {
  519. return { success: false, error: 'img-center-point-location 至少填写 inVars[0]=模板路径' }
  520. }
  521. if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' }
  522. const result = await executeImgCenterPointLocation({ device, template: templatePath, folderPath })
  523. if (!result.success) return { success: false, error: `图像中心点定位失败: ${result.error}` }
  524. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  525. if (outputVarName) {
  526. if (result.center && typeof result.center === 'object' && result.center.x !== undefined && result.center.y !== undefined) {
  527. variableContext[outputVarName] = { x: Math.round(Number(result.center.x)), y: Math.round(Number(result.center.y)) }
  528. } else {
  529. variableContext[outputVarName] = null
  530. }
  531. await logOutVars(action, variableContext, folderPath)
  532. }
  533. return { success: true, result: result.center }
  534. }
  535. case 'img-cropping': {
  536. const { executeImgCropping } = get(funcDir, 'img')
  537. let imagePath = action.imagePath
  538. let squareSpec = action.squareSpec
  539. let savePath = action.savePath
  540. if (action.inVars && Array.isArray(action.inVars)) {
  541. if (action.inVars.length > 0) imagePath = action.inVars[0]
  542. if (action.inVars.length > 1) savePath = action.inVars[1]
  543. if (action.inVars.length > 2) squareSpec = action.inVars[2]
  544. }
  545. if (!imagePath) return { success: false, error: 'img-cropping 缺少 imagePath(inVars[0])' }
  546. if (!savePath) return { success: false, error: 'img-cropping 缺少 savePath(inVars[1])' }
  547. if (squareSpec === undefined || squareSpec === null || squareSpec === '') {
  548. return { success: false, error: 'img-cropping 缺少 squareSpec(inVars[2],如 [0.8,"w"])' }
  549. }
  550. const result = await executeImgCropping({ imagePath, squareSpec, savePath, folderPath })
  551. if (!result.success) return { success: false, error: result.error }
  552. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null
  553. if (outputVarName) {
  554. const outVal = result.path ?? result.value ?? result.result
  555. if (outVal !== undefined && outVal !== null) variableContext[outputVarName] = String(outVal)
  556. }
  557. await logOutVars(action, variableContext, folderPath)
  558. return { success: true, ...result }
  559. }
  560. case 'ocr': {
  561. const { executeOcr, executeOcrFindText } = get(funcDir, 'img')
  562. let imageOrText = action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0 ? action.inVars[0] : action.image
  563. if (imageOrText == null || String(imageOrText).trim() === '') return { success: false, error: 'ocr 缺少参数:图片路径或要查找的文字(inVars[0] / image)' }
  564. const baseDir = folderPath && typeof folderPath === 'string' ? folderPath : (ctx.compilerConfig && ctx.compilerConfig.projectRoot) || process.cwd()
  565. const fs = require('fs')
  566. const isAbsoluteOrDrive = String(imageOrText).startsWith('/') || String(imageOrText).includes(':')
  567. const hasSubPath = String(imageOrText).includes('/') || String(imageOrText).includes(path.sep)
  568. const resolvedPath = isAbsoluteOrDrive ? imageOrText : (hasSubPath ? path.join(baseDir, imageOrText) : path.join(baseDir, 'resources', imageOrText))
  569. const isImagePath = fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()
  570. const outputVarName = action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0 ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  571. if (isImagePath) {
  572. const result = await executeOcr({ imagePath: imageOrText, folderPath })
  573. if (!result.success) return { success: false, error: result.error }
  574. if (outputVarName) {
  575. variableContext[outputVarName] = result.text != null ? String(result.text) : ''
  576. await logOutVars(action, variableContext, folderPath)
  577. }
  578. return { success: true, result: result.text }
  579. }
  580. if (!device) return { success: false, error: 'ocr 按文字查找需设备截图,当前无设备' }
  581. const findResult = await executeOcrFindText({ device, findText: String(imageOrText).trim(), folderPath })
  582. if (!findResult.success) return { success: false, error: findResult.error }
  583. if (outputVarName) {
  584. variableContext[outputVarName] = findResult.center && typeof findResult.center === 'object'
  585. ? JSON.stringify({ x: findResult.center.x, y: findResult.center.y }) : ''
  586. await logOutVars(action, variableContext, folderPath)
  587. }
  588. return { success: true, result: findResult.center }
  589. }
  590. case 'read-last-message': {
  591. const { executeReadLastMessage } = get(funcDir, 'io')
  592. const inputVars = action.inVars || action.inputVars || []
  593. const outputVars = action.outVars || action.outputVars || []
  594. let textVar = outputVars.length > 0 ? extractVarName(String(outputVars[0]).trim()) : (action.textVariable ? extractVarName(action.textVariable) : null)
  595. let senderVar = outputVars.length > 1 ? extractVarName(String(outputVars[1]).trim()) : (action.senderVariable ? extractVarName(action.senderVariable) : null)
  596. 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
  597. if (!textVar && !senderVar) return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' }
  598. const result = await executeReadLastMessage({ folderPath, inputData: inputDataString, textVariable: textVar, senderVariable: senderVar })
  599. if (!result.success) return { success: false, error: result.error }
  600. if (textVar) variableContext[textVar] = result.text
  601. if (senderVar) variableContext[senderVar] = result.sender
  602. await logOutVars(action, variableContext, folderPath)
  603. return { success: true, text: result.text, sender: result.sender }
  604. }
  605. case 'read-txt':
  606. case 'read-text': {
  607. const { executeReadTxt } = get(funcDir, 'io')
  608. let filePath = action.filePath
  609. let varName = action.variable
  610. if (action.inVars?.length > 0) filePath = action.inVars[0]
  611. if (action.outVars?.length > 0) varName = extractVarName(String(action.outVars[0]).trim())
  612. else if (action.variable) varName = extractVarName(action.variable)
  613. if (!filePath) return { success: false, error: 'read-txt 缺少 filePath 参数' }
  614. if (!varName) return { success: false, error: 'read-txt 缺少 variable 参数' }
  615. const result = await executeReadTxt({ filePath, folderPath })
  616. if (!result.success) return { success: false, error: result.error }
  617. const content = result.content || ''
  618. variableContext[varName] = typeof content === 'string' ? content : String(content)
  619. if (variableContext[varName] === undefined || variableContext[varName] === null) variableContext[varName] = ''
  620. await logOutVars(action, variableContext, folderPath)
  621. return { success: true, content: result.content }
  622. }
  623. case 'smart-chat-append': {
  624. const { executeSmartChatAppend } = get(funcDir, 'io')
  625. let history = action.history
  626. let current = action.current
  627. if (action.inVars && Array.isArray(action.inVars)) {
  628. if (action.inVars.length > 0) history = action.inVars[0]
  629. if (action.inVars.length > 1) current = action.inVars[1]
  630. }
  631. if (history === undefined || history === null) history = ''
  632. if (current === undefined || current === null) current = ''
  633. const result = await executeSmartChatAppend({
  634. history: typeof history === 'string' ? history : String(history),
  635. current: typeof current === 'string' ? current : String(current),
  636. })
  637. if (!result.success) return { success: false, error: result.error }
  638. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  639. if (outputVarName && result.result) variableContext[outputVarName] = result.result
  640. return { success: true, result: result.result }
  641. }
  642. case 'save-txt':
  643. case 'save-text': {
  644. const { executeSaveTxt } = get(funcDir, 'io')
  645. let filePath = action.filePath
  646. let content = action.content
  647. if (action.inVars && Array.isArray(action.inVars)) {
  648. if (action.inVars.length > 0) content = action.inVars[0]
  649. if (action.inVars.length > 1) filePath = action.inVars[1]
  650. }
  651. if (!filePath) return { success: false, error: 'save-txt 缺少 filePath 参数' }
  652. if (content === undefined || content === null) return { success: false, error: 'save-txt 缺少 content 参数' }
  653. const result = await executeSaveTxt({ filePath, content, folderPath })
  654. if (!result.success) return { success: false, error: result.error }
  655. if (action.outVars?.[0] != null) {
  656. const outputVarName = extractVarName(String(action.outVars[0]).trim())
  657. if (outputVarName) variableContext[outputVarName] = result.success ? '1' : '0'
  658. }
  659. await logOutVars(action, variableContext, folderPath)
  660. return { success: true }
  661. }
  662. case 'extract-messages':
  663. case 'ocr-chat':
  664. case 'ocr-chat-history':
  665. case 'extract-chat-history': {
  666. const { executeOcrChat } = get(funcDir, 'chat')
  667. const folderName = folderPath.split(/[/\\]/).pop()
  668. let avatar1Path = null
  669. let avatar2Path = null
  670. let avatar1Name, avatar2Name, regionArea = null
  671. let friendRgb = null, myRgb = null
  672. if (action.inVars && Array.isArray(action.inVars)) {
  673. if (action.inVars.length >= 3) {
  674. const param1 = action.inVars[0]
  675. const param2 = action.inVars[1]
  676. if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
  677. friendRgb = param1.trim()
  678. myRgb = param2.trim()
  679. regionArea = action.inVars[2]
  680. } else {
  681. avatar1Name = action.inVars[0]
  682. avatar2Name = action.inVars[1]
  683. regionArea = action.inVars[2]
  684. }
  685. regionArea = parseRegion(regionArea)
  686. } else if (action.inVars.length >= 2) {
  687. const param1 = action.inVars[0]
  688. const param2 = action.inVars[1]
  689. if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
  690. friendRgb = param1.trim()
  691. myRgb = param2.trim()
  692. } else {
  693. avatar1Name = param1
  694. avatar2Name = param2
  695. }
  696. } else if (action.inVars.length === 1) {
  697. avatar1Name = action.inVars[0]
  698. avatar2Name = action.avatar2 || action.myAvatar
  699. }
  700. } else {
  701. avatar1Name = action.avatar1 || action.friendAvatar
  702. avatar2Name = action.avatar2 || action.myAvatar
  703. }
  704. if (avatar1Name) {
  705. const resolved = resolveValue(avatar1Name, variableContext)
  706. if (resolved) avatar1Path = `${folderName}/resources/${resolved}`
  707. }
  708. if (avatar2Name) {
  709. const resolved = resolveValue(avatar2Name, variableContext)
  710. if (resolved) avatar2Path = `${folderName}/resources/${resolved}`
  711. }
  712. const regionParam = regionArea && typeof regionArea === 'string' ? (() => { try { return JSON.parse(regionArea) } catch (e) { return null } })() : regionArea
  713. const chatResult = await executeOcrChat({ device, avatar1: avatar1Path, avatar2: avatar2Path, folderPath, region: regionParam, friendRgb, myRgb })
  714. if (!chatResult.success) return { success: false, error: `提取消息记录失败: ${chatResult.error}` }
  715. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
  716. if (outputVarName) {
  717. variableContext[outputVarName] = chatResult.messagesJson || JSON.stringify(chatResult.messages || [])
  718. await logOutVars(action, variableContext, folderPath)
  719. }
  720. return {
  721. success: true,
  722. messages: chatResult.messages || [],
  723. messagesJson: chatResult.messagesJson || JSON.stringify(chatResult.messages || []),
  724. lastMessage: chatResult.messages?.length > 0 ? chatResult.messages[chatResult.messages.length - 1] : null,
  725. }
  726. }
  727. case 'save-messages':
  728. case 'generate-summary':
  729. case 'generate-history-summary': {
  730. const { generateHistorySummary } = get(funcDir, 'chat')
  731. if (!action.variable) return { success: false, error: '缺少变量名' }
  732. const messages = variableContext[action.variable]
  733. if (!messages) return { success: false, error: `变量 ${action.variable} 不存在或为空` }
  734. const modelName = action.model || 'gpt-5-nano-ca'
  735. const result = await generateHistorySummary(messages, folderPath, modelName)
  736. if (!result.success) return { success: false, error: `生成消息记录总结失败: ${result.error}` }
  737. if (action.summaryVariable) variableContext[action.summaryVariable] = result.summary
  738. return { success: true, summary: result.summary }
  739. }
  740. case 'ai-generate': {
  741. const { getHistorySummary } = get(funcDir, 'chat')
  742. if (!action.inVars || action.inVars.length < 1 || action.inVars[0] === undefined || action.inVars[0] === null) {
  743. return { success: false, error: 'ai-generate 须在 inVars[0] 填写 prompt 文本' }
  744. }
  745. let prompt = action.inVars[0]
  746. if (typeof prompt !== 'string') prompt = String(prompt)
  747. if (prompt && prompt.includes('{historySummary}') && getHistorySummary) {
  748. let historySummary = variableContext['historySummary'] || ''
  749. if (!historySummary) {
  750. historySummary = await getHistorySummary(folderPath)
  751. if (historySummary) variableContext['historySummary'] = historySummary
  752. }
  753. prompt = prompt.replace(/{historySummary}/g, historySummary)
  754. }
  755. prompt = replaceVariablesInString(prompt, variableContext)
  756. try {
  757. const response = await fetch('https://ai-anim.com/api/text2textByModel', {
  758. method: 'POST',
  759. headers: { 'Content-Type': 'application/json' },
  760. body: JSON.stringify({ prompt, modelName: action.model || 'gpt-5-nano-ca' }),
  761. })
  762. if (!response.ok) return { success: false, error: `AI请求失败: ${response.statusText}` }
  763. const data = await response.json()
  764. let rawResult = ''
  765. if (data.data?.output_text) rawResult = data.data.output_text
  766. else if (data.output_text) rawResult = data.output_text
  767. else if (data.text) rawResult = data.text
  768. else if (data.content) rawResult = data.content
  769. else if (typeof data.data === 'string') rawResult = data.data
  770. else rawResult = JSON.stringify(data)
  771. rawResult = rawResult ? String(rawResult) : ''
  772. let result = rawResult
  773. try {
  774. try {
  775. const jsonResult = JSON.parse(rawResult.trim())
  776. if (jsonResult.reply) result = jsonResult.reply
  777. } catch (e) {
  778. const codeBlockMatch = rawResult.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/)
  779. if (codeBlockMatch) {
  780. try {
  781. const jsonResult = JSON.parse(codeBlockMatch[1])
  782. if (jsonResult.reply) result = jsonResult.reply
  783. } catch (e2) {}
  784. } else {
  785. const jsonMatch = rawResult.match(/\{\s*"reply"\s*:\s*"([^"]+)"\s*\}/)
  786. if (jsonMatch) result = jsonMatch[1]
  787. else {
  788. const lines = rawResult.split('\n').map((l) => l.trim()).filter(Boolean)
  789. for (const line of lines) {
  790. if (line.startsWith('{') && line.includes('"reply"')) {
  791. try {
  792. const jsonResult = JSON.parse(line)
  793. if (jsonResult.reply) { result = jsonResult.reply; break }
  794. } catch (e3) {}
  795. }
  796. }
  797. }
  798. }
  799. }
  800. } catch (parseError) {}
  801. if (action.outVars?.length > 0) {
  802. if (action.outVars.length > 0) {
  803. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null
  804. if (outputVarName) variableContext[outputVarName] = result
  805. }
  806. if (action.outVars.length > 1) {
  807. const callbackVarName = action.outVars?.[1] != null ? extractVarName(String(action.outVars[1]).trim()) : null
  808. if (callbackVarName) variableContext[callbackVarName] = 1
  809. }
  810. await logOutVars(action, variableContext, folderPath)
  811. } else if (action.variable) {
  812. const outputVarName = extractVarName(action.variable)
  813. if (outputVarName) variableContext[outputVarName] = result
  814. }
  815. if (!action.outVars || action.outVars.length <= 1) {
  816. if (action.inVars?.length > 1 && action.inVars[1] != null) {
  817. const callbackVarName = extractVarName(String(action.inVars[1]).trim())
  818. if (callbackVarName) variableContext[callbackVarName] = 1
  819. }
  820. }
  821. return { success: true, result }
  822. } catch (error) {
  823. return { success: false, error: `AI生成失败: ${error.message}` }
  824. }
  825. }
  826. case 'string-press': {
  827. const api = ctx.electronAPI
  828. const inVars = action.inVars || []
  829. const targetText = inVars.length > 0 ? (inVars[0] ?? action.value) : (action.value ?? '')
  830. if (!targetText) return { success: false, error: 'string-press 操作缺少文字内容' }
  831. if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' }
  832. const matchResult = await api.findTextAndGetCoordinate(device, targetText)
  833. if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` }
  834. const { x, y } = matchResult.clickPosition
  835. if (!api?.sendTap) return { success: false, error: '点击 API 不可用' }
  836. const tapResult = await api.sendTap(device, x, y)
  837. if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` }
  838. return { success: true }
  839. }
  840. default: {
  841. const regDef = REGISTRY_BY_TYPE.get(actionType)
  842. if (regDef) {
  843. if (regDef.customRun) {
  844. const script = getRegistryScript(funcDir, actionType)
  845. if (script && typeof script.runNode === 'function') {
  846. const runCtx = { ...ctx, get, funcDir, folderPath, device }
  847. const result = await script.runNode(action, runCtx)
  848. if (result && result.success !== false) await logOutVars(action, variableContext, folderPath)
  849. return result != null ? result : { success: true }
  850. }
  851. }
  852. const inKeys = regDef.in || []
  853. const inAlt = regDef.inAlt || {}
  854. const input = {}
  855. if (actionType === 'persist-save') {
  856. let sk = action.stateKey
  857. let sv = action.stateValue
  858. if (action.inVars && action.inVars[0] !== undefined) sk = action.inVars[0]
  859. if (action.inVars && action.inVars[1] !== undefined) sv = action.inVars[1]
  860. if (sk === undefined && inAlt.stateKey && action[inAlt.stateKey] !== undefined) sk = action[inAlt.stateKey]
  861. if (sv === undefined && inAlt.stateValue && action[inAlt.stateValue] !== undefined) sv = action[inAlt.stateValue]
  862. input.stateKey = sk != null && typeof sk === 'string' ? sk.trim() : sk
  863. input.stateValue = sv
  864. input.folderPath = folderPath
  865. } else {
  866. inKeys.forEach((key, i) => {
  867. let val = action[key]
  868. if (action.inVars && action.inVars[i] !== undefined) val = action.inVars[i]
  869. if (val === undefined && inAlt[key]) val = action[inAlt[key]]
  870. input[key] = val != null ? String(val).trim() : val
  871. })
  872. input.folderPath = folderPath
  873. }
  874. if (actionType === 'remove-folder') {
  875. let rec = action.recursive
  876. if ((rec === undefined || rec === null || rec === '') && action.inVars && action.inVars.length > 1 && action.inVars[1] !== undefined) {
  877. rec = action.inVars[1]
  878. }
  879. if (rec !== undefined && rec !== null && rec !== '') {
  880. input.recursive = typeof rec === 'string' ? rec.trim() : String(rec).trim()
  881. }
  882. }
  883. const mod = get(funcDir, regDef.category)
  884. const fn = mod[regDef.execute]
  885. if (!fn) {
  886. return { success: false, error: `fun-parser: ${regDef.execute} not found` }
  887. }
  888. let result
  889. try {
  890. result = await fn(input)
  891. } catch (e) {
  892. return { success: false, error: (e && (e.message || String(e))) || 'execute threw' }
  893. }
  894. if (!result || !result.success) {
  895. return { success: false, error: (result && result.error) || 'execute failed' }
  896. }
  897. const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null
  898. if (outputVarName && result != null) {
  899. const outVal = result.path ?? result.value ?? result.result
  900. if (outVal !== undefined && outVal !== null) {
  901. if (actionType === 'json' && Array.isArray(outVal)) variableContext[outputVarName] = outVal
  902. else if (actionType === 'persist-read' && (typeof outVal === 'string' || typeof outVal === 'number')) {
  903. variableContext[outputVarName] = outVal
  904. } else variableContext[outputVarName] = typeof outVal === 'string' ? outVal : String(outVal)
  905. }
  906. }
  907. await logOutVars(action, variableContext, folderPath)
  908. return { success: true, ...result }
  909. }
  910. return { success: false, error: `fun-parser 不支持的 type: ${actionType}` }
  911. }
  912. }
  913. }
  914. const FUN_TYPES = new Set([
  915. 'ai',
  916. 'img-bounding-box-location', 'img-center-point-location', 'img-cropping', 'ocr',
  917. 'read-last-message', 'read-txt', 'read-text', 'smart-chat-append', 'save-txt', 'save-text',
  918. 'extract-messages', 'ocr-chat', 'ocr-chat-history', 'extract-chat-history',
  919. 'save-messages', 'generate-summary', 'generate-history-summary',
  920. 'ai-generate',
  921. 'string-press',
  922. ].concat(REGISTERED_TYPES))
  923. function supports(type) {
  924. return FUN_TYPES.has(type)
  925. }
  926. module.exports = { types, parse, execute, runAction, get, run, supports }