fun-parser.js 32 KB

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