fun-parser.js 30 KB

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