fun-parser.js 30 KB

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