/** * fun 解析与执行 + 执行入口:registry/executeAction 由 ctx 传入(来自 workflow-json-parser),本模块负责 parse/runAction/run/supports */ const path = require('path') const FUN_REGISTRY_TYPES = [ 'fun', 'read-txt', 'read-text', 'save-txt', 'save-text', 'img-bounding-box-location', 'img-center-point-location', 'img-cropping', 'read-last-message', 'smart-chat-append', 'extract-messages', 'ocr-chat', 'ocr-chat-history', 'extract-chat-history', 'save-messages', 'generate-summary', 'generate-history-summary', 'ai-generate', 'string-press', ] const types = FUN_REGISTRY_TYPES function parse(action, parseContext) { const { extractVarName, resolveValue } = parseContext const variableContext = parseContext.variableContext || {} const parsed = { type: action.type, method: action.method, target: action.target, value: action.value, variable: action.variable, condition: action.condition, delay: action.delay || '', timeout: action.timeout, retry: action.retry, } Object.assign(parsed, action) switch (action.type) { case 'fun': parsed.method = action.method parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : [] parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : [] break case 'extract-messages': case 'ocr-chat': case 'ocr-chat-history': case 'extract-chat-history': parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : [] parsed.avatar1 = action.inVars && action.inVars.length >= 2 ? action.inVars[0] : action.inVars?.[0] parsed.avatar2 = action.inVars && action.inVars.length >= 2 ? action.inVars[1] : action.avatar2 parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : [] if (parsed.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0]) else if (action.variable) parsed.variable = extractVarName(action.variable) if (action.friendAvatar && !parsed.avatar1) parsed.avatar1 = action.friendAvatar if (action.myAvatar && !parsed.avatar2) parsed.avatar2 = action.myAvatar break case 'save-messages': break case 'generate-summary': case 'generate-history-summary': parsed.summaryVariable = action.summaryVariable break case 'ai-generate': parsed.prompt = resolveValue(action.prompt, variableContext) parsed.model = action.model parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : [] parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : [] if (parsed.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0]) else if (action.variable) parsed.variable = extractVarName(action.variable) break case 'read-last-message': { const inputVars = action.inVars || action.inputVars || [] const outputVars = action.outVars || action.outputVars || [] parsed.inVars = inputVars.map(v => extractVarName(v)) parsed.outVars = outputVars.map(v => extractVarName(v)) if (inputVars.length > 0) parsed.inputVar = extractVarName(inputVars[0]) if (outputVars.length > 0) parsed.textVariable = extractVarName(outputVars[0]) if (outputVars.length > 1) parsed.senderVariable = extractVarName(outputVars[1]) if (!parsed.textVariable) parsed.textVariable = action.textVariable if (!parsed.senderVariable) parsed.senderVariable = action.senderVariable break } case 'read-txt': case 'read-text': parsed.inVars = action.inVars && action.inVars.length > 0 ? action.inVars.map(v => extractVarName(v)) : [] parsed.filePath = action.inVars && action.inVars.length > 0 ? action.inVars[0] : action.filePath parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined) break case 'save-txt': case 'save-text': if (action.inVars && Array.isArray(action.inVars)) { parsed.inVars = action.inVars.map(v => extractVarName(v)) parsed.content = action.inVars[0] parsed.filePath = action.inVars.length > 1 ? action.inVars[1] : action.filePath } else { parsed.inVars = [] parsed.filePath = action.filePath parsed.content = action.content } if (action.outVars && action.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0]) break case 'img-bounding-box-location': parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : [] parsed.screenshot = action.inVars?.[0] parsed.region = action.inVars?.[1] ?? action.region parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined) break case 'img-center-point-location': parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : [] parsed.template = action.inVars?.[0] ?? action.template parsed.variable = action.outVars && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined) break case 'img-cropping': parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : [] parsed.area = action.inVars?.[0] ?? action.area parsed.savePath = action.inVars?.[1] ?? action.savePath if (action.outVars && action.outVars.length > 0) parsed.variable = extractVarName(action.outVars[0]) break default: parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : [] parsed.outVars = action.outVars && Array.isArray(action.outVars) ? action.outVars.map(v => extractVarName(v)) : [] break } return parsed } async function execute(action, ctx) { return { success: true } } async function runAction(action, device, folderPath, resolution, ctx) { const { variableContext, evaluateCondition, registry, executeAction } = ctx if (action.condition && !evaluateCondition(action.condition, variableContext)) { return { success: true, skipped: true } } if (action.type === 'fun' && action.method) { return run(action.method, action, ctx, device, folderPath) } // fun 类 type(如 img-center-point-location)必须走 run() 才会执行逻辑并写变量,不能走 registry 的 stub execute if (supports(action.type)) { return run(action.type, action, ctx, device, folderPath) } if (registry && registry[action.type]) { const execCtx = { device, folderPath, resolution, variableContext, api: ctx.electronAPI, extractVarName: ctx.extractVarName, resolveValue: ctx.resolveValue, replaceVariablesInString: ctx.replaceVariablesInString, evaluateCondition: ctx.evaluateCondition, evaluateExpression: ctx.evaluateExpression, getActionName: ctx.getActionName, logMessage: ctx.logMessage, logOutVars: ctx.logOutVars, parseDelayString: ctx.parseDelayString, calculateWaitTime: ctx.calculateWaitTime, DEFAULT_SCROLL_DISTANCE: ctx.DEFAULT_SCROLL_DISTANCE, } return await executeAction(action.type, action, execCtx) } return { success: false, error: `未知的操作类型: ${action.type}` } } const cache = new Map() function get(funcDir, category) { if (!funcDir) throw new Error('fun-parser: funcDir 未提供') const key = `${funcDir}:${category}` if (cache.has(key)) return cache.get(key) let mod switch (category) { case 'img': mod = { executeImgBoundingBoxLocation: require(path.join(funcDir, 'img-bounding-box-location.js')).executeImgBoundingBoxLocation, executeImgCenterPointLocation: require(path.join(funcDir, 'img-center-point-location.js')).executeImgCenterPointLocation, executeImgCropping: require(path.join(funcDir, 'img-cropping.js')).executeImgCropping, } break case 'io': mod = { executeReadLastMessage: require(path.join(funcDir, 'chat', 'read-last-message.js')).executeReadLastMessage, executeReadTxt: require(path.join(funcDir, 'read-txt.js')).executeReadTxt, executeSmartChatAppend: require(path.join(funcDir, 'chat', 'smart-chat-append.js')).executeSmartChatAppend, executeSaveTxt: require(path.join(funcDir, 'save-txt.js')).executeSaveTxt, } break case 'chat': mod = (() => { const chatHistory = require(path.join(funcDir, 'chat', 'chat-history.js')) const ocrChat = require(path.join(funcDir, 'chat', 'ocr-chat.js')) return { executeOcrChat: ocrChat.executeOcrChat, generateHistorySummary: chatHistory.generateHistorySummary, getHistorySummary: chatHistory.getHistorySummary, } })() break default: throw new Error(`fun-parser: 未知分类 ${category}`) } cache.set(key, mod) return mod } const rgbPattern = /^\((\d+),(\d+),(\d+)\)$/ function parseRegion(regionArea) { if (!regionArea) return null if (typeof regionArea === 'string') { try { regionArea = JSON.parse(regionArea) } catch (e) { return null } } if (regionArea && typeof regionArea === 'object' && (!regionArea.topLeft || !regionArea.bottomRight)) return null return regionArea } async function run(actionType, action, ctx, device, folderPath) { const { variableContext, extractVarName, resolveValue, replaceVariablesInString, logOutVars } = ctx const funcDir = ctx.compilerConfig && ctx.compilerConfig.funcDir if (!funcDir) return { success: false, error: 'compilerConfig.funcDir 未提供' } switch (actionType) { case 'img-bounding-box-location': { const { executeImgBoundingBoxLocation } = get(funcDir, 'img') let screenshotPath = action.screenshot let regionPath = action.region if (action.inVars && Array.isArray(action.inVars)) { if (action.inVars.length === 1) { const firstVar = extractVarName(action.inVars[0]) const firstValue = variableContext[firstVar] regionPath = firstValue && typeof firstValue === 'string' && !firstValue.includes('{') ? firstValue : action.inVars[0] screenshotPath = null } else if (action.inVars.length >= 2) { const sv = variableContext[extractVarName(action.inVars[0])] screenshotPath = sv && typeof sv === 'string' && !sv.includes('{') ? sv : action.inVars[0] const rv = variableContext[extractVarName(action.inVars[1])] regionPath = rv && typeof rv === 'string' && !rv.includes('{') ? rv : action.inVars[1] } } if (screenshotPath !== null && !screenshotPath) screenshotPath = action.screenshot if (!regionPath) regionPath = action.region if (!regionPath) return { success: false, error: '缺少区域截图路径' } if (screenshotPath === null && !device) return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' } const result = await executeImgBoundingBoxLocation({ device, screenshot: screenshotPath, region: regionPath, folderPath }) if (!result.success) return { success: false, error: `图像区域定位失败: ${result.error}` } const outputVarName = action.outVars?.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : null) if (outputVarName) { variableContext[outputVarName] = result.corners && typeof result.corners === 'object' ? JSON.stringify(result.corners) : '' await logOutVars(action, variableContext, folderPath) } return { success: true, result: result.corners } } case 'img-center-point-location': { const { executeImgCenterPointLocation } = get(funcDir, 'img') let templatePath = action.template if (action.inVars?.length > 0) { const templateVar = extractVarName(action.inVars[0]) const templateValue = variableContext[templateVar] templatePath = templateValue && typeof templateValue === 'string' && !templateValue.includes('{') ? templateValue : action.inVars[0] } if (!templatePath) templatePath = action.template if (!templatePath) return { success: false, error: '缺少模板图片路径' } if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' } const result = await executeImgCenterPointLocation({ device, template: templatePath, folderPath }) if (!result.success) return { success: false, error: `图像中心点定位失败: ${result.error}` } const outputVarName = action.outVars?.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : null) if (outputVarName) { variableContext[outputVarName] = result.center && typeof result.center === 'object' && result.center.x !== undefined && result.center.y !== undefined ? JSON.stringify({ x: result.center.x, y: result.center.y }) : '' await logOutVars(action, variableContext, folderPath) } return { success: true, result: result.center } } case 'img-cropping': { const { executeImgCropping } = get(funcDir, 'img') let area = action.area let savePath = action.savePath if (action.inVars && Array.isArray(action.inVars)) { if (action.inVars.length > 0) { const areaValue = variableContext[extractVarName(action.inVars[0])] area = areaValue !== undefined ? areaValue : resolveValue(action.inVars[0]) } if (action.inVars.length > 1) { const savePathValue = variableContext[extractVarName(action.inVars[1])] savePath = savePathValue !== undefined ? savePathValue : resolveValue(action.inVars[1]) } } if (!area) return { success: false, error: 'img-cropping 缺少 area 参数' } if (!savePath) return { success: false, error: 'img-cropping 缺少 savePath 参数' } const result = await executeImgCropping({ area, savePath, folderPath, device }) if (!result.success) return { success: false, error: result.error } if (action.outVars?.length > 0) { const outputVarName = extractVarName(action.outVars[0]) if (outputVarName) variableContext[outputVarName] = result.success ? '1' : '0' } await logOutVars(action, variableContext, folderPath) return { success: true } } case 'read-last-message': { const { executeReadLastMessage } = get(funcDir, 'io') const inputVars = action.inVars || action.inputVars || [] const outputVars = action.outVars || action.outputVars || [] let textVar = outputVars.length > 0 ? extractVarName(outputVars[0]) : action.textVariable let senderVar = outputVars.length > 1 ? extractVarName(outputVars[1]) : action.senderVariable const inputVar = inputVars.length > 0 ? extractVarName(inputVars[0]) : null if (!textVar && !senderVar) return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' } let inputDataString = null if (inputVar && variableContext[inputVar] !== undefined) { const inputData = variableContext[inputVar] if (typeof inputData === 'string') inputDataString = inputData else if (Array.isArray(inputData) || typeof inputData === 'object') inputDataString = JSON.stringify(inputData) else inputDataString = String(inputData) } const result = await executeReadLastMessage({ folderPath, inputData: inputDataString, textVariable: textVar, senderVariable: senderVar }) if (!result.success) return { success: false, error: result.error } if (textVar) variableContext[textVar] = result.text if (senderVar) variableContext[senderVar] = result.sender await logOutVars(action, variableContext, folderPath) return { success: true, text: result.text, sender: result.sender } } case 'read-txt': case 'read-text': { const { executeReadTxt } = get(funcDir, 'io') let filePath = action.filePath let varName = action.variable if (action.inVars?.length > 0) { const filePathValue = variableContext[extractVarName(action.inVars[0])] filePath = filePathValue !== undefined ? filePathValue : resolveValue(action.inVars[0]) } if (action.outVars?.length > 0) varName = extractVarName(action.outVars[0]) else if (action.variable) varName = extractVarName(action.variable) if (!filePath) return { success: false, error: 'read-txt 缺少 filePath 参数' } if (!varName) return { success: false, error: 'read-txt 缺少 variable 参数' } const result = await executeReadTxt({ filePath, folderPath }) if (!result.success) return { success: false, error: result.error } const content = result.content || '' variableContext[varName] = typeof content === 'string' ? content : String(content) if (variableContext[varName] === undefined || variableContext[varName] === null) variableContext[varName] = '' await logOutVars(action, variableContext, folderPath) return { success: true, content: result.content } } case 'smart-chat-append': { const { executeSmartChatAppend } = get(funcDir, 'io') let history = action.history let current = action.current if (action.inVars && Array.isArray(action.inVars)) { if (action.inVars.length > 0) { const historyValue = variableContext[extractVarName(action.inVars[0])] history = historyValue !== undefined ? historyValue : resolveValue(action.inVars[0]) } if (action.inVars.length > 1) { const currentValue = variableContext[extractVarName(action.inVars[1])] current = currentValue !== undefined ? currentValue : resolveValue(action.inVars[1]) } } if (history === undefined || history === null) history = '' if (current === undefined || current === null) current = '' const result = await executeSmartChatAppend({ history: typeof history === 'string' ? history : String(history), current: typeof current === 'string' ? current : String(current), }) if (!result.success) return { success: false, error: result.error } const outputVarName = action.outVars?.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : null) if (outputVarName && result.result) variableContext[outputVarName] = result.result return { success: true, result: result.result } } case 'save-txt': case 'save-text': { const { executeSaveTxt } = get(funcDir, 'io') let filePath = action.filePath let content = action.content if (action.inVars && Array.isArray(action.inVars)) { if (action.inVars.length > 0) { const contentValue = variableContext[extractVarName(action.inVars[0])] content = contentValue !== undefined ? contentValue : resolveValue(action.inVars[0]) } if (action.inVars.length > 1) { const filePathValue = variableContext[extractVarName(action.inVars[1])] filePath = filePathValue !== undefined ? filePathValue : resolveValue(action.inVars[1]) } } if (!filePath) return { success: false, error: 'save-txt 缺少 filePath 参数' } if (content === undefined || content === null) return { success: false, error: 'save-txt 缺少 content 参数' } const result = await executeSaveTxt({ filePath, content, folderPath }) if (!result.success) return { success: false, error: result.error } if (action.outVars?.length > 0) { const outputVarName = extractVarName(action.outVars[0]) if (outputVarName) variableContext[outputVarName] = result.success ? '1' : '0' } await logOutVars(action, variableContext, folderPath) return { success: true } } case 'extract-messages': case 'ocr-chat': case 'ocr-chat-history': case 'extract-chat-history': { const { executeOcrChat } = get(funcDir, 'chat') const folderName = folderPath.split(/[/\\]/).pop() let avatar1Path = null let avatar2Path = null let avatar1Name, avatar2Name, regionArea = null let friendRgb = null, myRgb = null if (action.inVars && Array.isArray(action.inVars)) { if (action.inVars.length >= 3) { const param1 = resolveValue(action.inVars[0]) const param2 = resolveValue(action.inVars[1]) if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) { friendRgb = param1.trim() myRgb = param2.trim() const regionVar = extractVarName(action.inVars[2]) regionArea = variableContext[regionVar] if (regionArea === undefined) { const regionResolved = resolveValue(action.inVars[2]) if (regionResolved && typeof regionResolved === 'object') regionArea = regionResolved } } else { avatar1Name = action.inVars[0] avatar2Name = action.inVars[1] const regionVar = extractVarName(action.inVars[2]) regionArea = variableContext[regionVar] if (regionArea === undefined) { const regionResolved = resolveValue(action.inVars[2]) if (regionResolved && typeof regionResolved === 'object') regionArea = regionResolved } } regionArea = parseRegion(regionArea) } else if (action.inVars.length >= 2) { const param1 = resolveValue(action.inVars[0]) const param2 = resolveValue(action.inVars[1]) if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) { friendRgb = param1.trim() myRgb = param2.trim() } else { avatar1Name = action.inVars[0] avatar2Name = action.inVars[1] } } else if (action.inVars.length === 1) { avatar1Name = action.inVars[0] avatar2Name = action.avatar2 || action.myAvatar } } else { avatar1Name = action.avatar1 || action.friendAvatar avatar2Name = action.avatar2 || action.myAvatar } if (avatar1Name) { const resolved = resolveValue(avatar1Name) if (resolved) avatar1Path = `${folderName}/resources/${resolved}` } if (avatar2Name) { const resolved = resolveValue(avatar2Name) if (resolved) avatar2Path = `${folderName}/resources/${resolved}` } const regionParam = regionArea && typeof regionArea === 'string' ? (() => { try { return JSON.parse(regionArea) } catch (e) { return null } })() : regionArea const chatResult = await executeOcrChat({ device, avatar1: avatar1Path, avatar2: avatar2Path, folderPath, region: regionParam, friendRgb, myRgb }) if (!chatResult.success) return { success: false, error: `提取消息记录失败: ${chatResult.error}` } const outputVarName = action.outVars?.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : null) if (outputVarName) { variableContext[outputVarName] = chatResult.messagesJson || JSON.stringify(chatResult.messages || []) await logOutVars(action, variableContext, folderPath) } return { success: true, messages: chatResult.messages || [], messagesJson: chatResult.messagesJson || JSON.stringify(chatResult.messages || []), lastMessage: chatResult.messages?.length > 0 ? chatResult.messages[chatResult.messages.length - 1] : null, } } case 'save-messages': case 'generate-summary': case 'generate-history-summary': { const { generateHistorySummary } = get(funcDir, 'chat') if (!action.variable) return { success: false, error: '缺少变量名' } const messages = variableContext[action.variable] if (!messages) return { success: false, error: `变量 ${action.variable} 不存在或为空` } const modelName = action.model || 'gpt-5-nano-ca' const result = await generateHistorySummary(messages, folderPath, modelName) if (!result.success) return { success: false, error: `生成消息记录总结失败: ${result.error}` } if (action.summaryVariable) variableContext[action.summaryVariable] = result.summary return { success: true, summary: result.summary } } case 'ai-generate': { const { getHistorySummary } = get(funcDir, 'chat') let prompt = resolveValue(action.prompt, variableContext) if (action.inVars && Array.isArray(action.inVars)) { for (let i = 0; i < action.inVars.length; i++) { const varName = extractVarName(action.inVars[i]) const varValue = variableContext[varName] if (varValue !== undefined && varValue !== null) { let replaceValue = String(varValue) if (typeof varValue === 'string' && varValue.trim() === '[]') { try { const parsed = JSON.parse(varValue) if (Array.isArray(parsed) && parsed.length === 0) replaceValue = '' } catch (e) {} } prompt = prompt.replace(new RegExp(`{${varName}}`.replace(/[{}]/g, '\\$&'), 'g'), replaceValue) } } } if (prompt.includes('{historySummary}') && getHistorySummary) { let historySummary = variableContext['historySummary'] || '' if (!historySummary) { historySummary = await getHistorySummary(folderPath) if (historySummary) variableContext['historySummary'] = historySummary } prompt = prompt.replace(/{historySummary}/g, historySummary) } prompt = replaceVariablesInString(prompt, variableContext) try { const response = await fetch('https://ai-anim.com/api/text2textByModel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, modelName: action.model || 'gpt-5-nano-ca' }), }) if (!response.ok) return { success: false, error: `AI请求失败: ${response.statusText}` } const data = await response.json() let rawResult = '' if (data.data?.output_text) rawResult = data.data.output_text else if (data.output_text) rawResult = data.output_text else if (data.text) rawResult = data.text else if (data.content) rawResult = data.content else if (typeof data.data === 'string') rawResult = data.data else rawResult = JSON.stringify(data) rawResult = rawResult ? String(rawResult) : '' let result = rawResult try { try { const jsonResult = JSON.parse(rawResult.trim()) if (jsonResult.reply) result = jsonResult.reply } catch (e) { const codeBlockMatch = rawResult.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/) if (codeBlockMatch) { try { const jsonResult = JSON.parse(codeBlockMatch[1]) if (jsonResult.reply) result = jsonResult.reply } catch (e2) {} } else { const jsonMatch = rawResult.match(/\{\s*"reply"\s*:\s*"([^"]+)"\s*\}/) if (jsonMatch) result = jsonMatch[1] else { const lines = rawResult.split('\n').map((l) => l.trim()).filter(Boolean) for (const line of lines) { if (line.startsWith('{') && line.includes('"reply"')) { try { const jsonResult = JSON.parse(line) if (jsonResult.reply) { result = jsonResult.reply; break } } catch (e3) {} } } } } } } catch (parseError) {} if (action.outVars?.length > 0) { if (action.outVars.length > 0) { const outputVarName = extractVarName(action.outVars[0]) if (outputVarName) variableContext[outputVarName] = result } if (action.outVars.length > 1) { const callbackVarName = extractVarName(action.outVars[1]) if (callbackVarName) variableContext[callbackVarName] = 1 } await logOutVars(action, variableContext, folderPath) } else if (action.variable) { const outputVarName = extractVarName(action.variable) if (outputVarName) variableContext[outputVarName] = result } if (!action.outVars || action.outVars.length <= 1) { if (action.inVars?.length > 1) { const callbackVarName = extractVarName(action.inVars[1]) if (callbackVarName) variableContext[callbackVarName] = 1 } } return { success: true, result } } catch (error) { return { success: false, error: `AI生成失败: ${error.message}` } } } case 'string-press': { const api = ctx.electronAPI const inVars = action.inVars || [] let targetText = inVars.length > 0 ? (variableContext[extractVarName(inVars[0])] || action.value) : (resolveValue(action.value, variableContext) || action.value) if (!targetText) return { success: false, error: 'string-press 操作缺少文字内容' } if (!api?.findTextAndGetCoordinate) return { success: false, error: '文字识别 API 不可用' } const matchResult = await api.findTextAndGetCoordinate(device, targetText) if (!matchResult.success) return { success: false, error: `文字识别失败: ${matchResult.error}` } const { x, y } = matchResult.clickPosition if (!api?.sendTap) return { success: false, error: '点击 API 不可用' } const tapResult = await api.sendTap(device, x, y) if (!tapResult.success) return { success: false, error: `点击失败: ${tapResult.error}` } return { success: true } } default: return { success: false, error: `fun-parser 不支持的 type: ${actionType}` } } } const FUN_TYPES = new Set([ 'img-bounding-box-location', 'img-center-point-location', 'img-cropping', 'read-last-message', 'read-txt', 'read-text', 'smart-chat-append', 'save-txt', 'save-text', 'extract-messages', 'ocr-chat', 'ocr-chat-history', 'extract-chat-history', 'save-messages', 'generate-summary', 'generate-history-summary', 'ai-generate', 'string-press', ]) function supports(type) { return FUN_TYPES.has(type) } module.exports = { types, parse, execute, runAction, get, run, supports }