/** * fun 解析与执行 + 执行入口:registry/executeAction 由 ctx 传入(来自 workflow-json-parser),本模块负责 parse/runAction/run/supports * 简易结点由 fun-node-registry.js 配置,只需新建脚本 + 在注册表加一条即可。 */ const path = require('path') const variableParser = require('../../variable-parser.js') const FUN_NODE_REGISTRY = require('./fun-node-registry.js') const LEGACY_FUN_TYPES = [ 'fun', 'ai', 'read-txt', 'read-text', 'save-txt', 'save-text', 'img-bounding-box-location', 'img-center-point-location', 'img-cropping', 'ocr', '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 REGISTERED_TYPES = (FUN_NODE_REGISTRY && Array.isArray(FUN_NODE_REGISTRY)) ? FUN_NODE_REGISTRY.map((r) => r.type) : [] const FUN_REGISTRY_TYPES = LEGACY_FUN_TYPES.concat(REGISTERED_TYPES) const types = FUN_REGISTRY_TYPES const REGISTRY_BY_TYPE = new Map((FUN_NODE_REGISTRY || []).map((r) => [r.type, r])) const scriptCache = new Map() function getRegistryScript(funcDir, type) { const def = REGISTRY_BY_TYPE.get(type) if (!def) return null const key = `${funcDir}:${type}` if (!scriptCache.has(key)) { try { const scriptPath = path.join(funcDir, def.script || def.type + '.js') scriptCache.set(key, require(scriptPath)) } catch (e) { scriptCache.set(key, null) } } return scriptCache.get(key) } 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) const regDef = REGISTRY_BY_TYPE.get(action.type) if (regDef) { const funcDir = (parseContext.compilerConfig && parseContext.compilerConfig.funcDir) || __dirname if (regDef.customParse) { const script = getRegistryScript(funcDir, action.type) if (script && typeof script.parseNode === 'function') return script.parseNode(action, parseContext) } parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map((v) => extractVarName(v)) : [] const inKeys = regDef.in || [] const inAlt = regDef.inAlt || {} inKeys.forEach((key, i) => { const altKey = inAlt[key] parsed[key] = action.inVars?.[i] ?? action[key] ?? (altKey ? action[altKey] : undefined) }) parsed.variable = action.outVars?.[0] ? extractVarName(action.outVars[0]) : undefined return parsed } 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, i) => (i === 1 && Array.isArray(v) ? v : extractVarName(v))) : [] parsed.template = action.inVars?.[0] ?? action.template parsed.scaleRange = Array.isArray(action.inVars?.[1]) ? action.inVars[1] : undefined 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 case 'ocr': parsed.inVars = action.inVars && Array.isArray(action.inVars) ? action.inVars.map(v => extractVarName(v)) : [] parsed.image = action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0 ? action.inVars[0] : action.image parsed.variable = action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0 ? extractVarName(action.outVars[0]) : (action.variable ? extractVarName(action.variable) : undefined) 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 } } const resolvedAction = variableParser.resolveActionInputs(action, variableContext) if (resolvedAction.type === 'fun' && resolvedAction.method) { return run(resolvedAction.method, resolvedAction, ctx, device, folderPath) } if (resolvedAction.type === 'ai' && resolvedAction.method) { return run(resolvedAction.method, resolvedAction, ctx, device, folderPath) } if (supports(resolvedAction.type)) { return run(resolvedAction.type, resolvedAction, ctx, device, folderPath) } if (registry && registry[resolvedAction.type]) { const execCtx = { device, folderPath, resolution, variableContext, compilerConfig: ctx.compilerConfig, 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(resolvedAction.type, resolvedAction, execCtx) } return { success: false, error: `未知的操作类型: ${resolvedAction.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, executeOcr: require(path.join(funcDir, 'ocr.js')).executeOcr, executeOcrFindText: require(path.join(funcDir, 'ocr.js')).executeOcrFindText, } 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, } ;(FUN_NODE_REGISTRY || []).filter((r) => r.category === 'io').forEach((def) => { const scriptPath = path.join(funcDir, def.script || def.type + '.js') try { const m = require(scriptPath) if (m[def.execute]) mod[def.execute] = m[def.execute] } catch (e) { /* skip missing script */ } }) 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, logMessage } = 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 firstValue = action.inVars[0] regionPath = firstValue != null && typeof firstValue === 'string' && !String(firstValue).includes('{') ? firstValue : action.inVars[0] screenshotPath = null } else if (action.inVars.length >= 2) { const sv = action.inVars[0] screenshotPath = sv != null && typeof sv === 'string' && !sv.includes('{') ? sv : action.inVars[0] const rv = action.inVars[1] regionPath = rv != null && 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?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (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 templateValue = action.inVars[0] templatePath = templateValue != null && typeof templateValue === 'string' && !String(templateValue).includes('{') ? templateValue : action.inVars[0] } if (!templatePath) templatePath = action.template if (!templatePath) return { success: false, error: '缺少模板图片路径' } let scaleRange = action.inVars?.[1] ?? action.scaleRange if (!Array.isArray(scaleRange) || scaleRange.length < 2) return { success: false, error: 'img-center-point-location 必须填写 inVars[1] 缩放比范围 [min, max],如 [0.2, 1.6]' } const minS = Number(scaleRange[0]) const maxS = Number(scaleRange[1]) if (Number.isNaN(minS) || Number.isNaN(maxS) || minS >= maxS) return { success: false, error: 'img-center-point-location inVars[1] 缩放比范围无效,需为两个数字且 min < max' } if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' } const result = await executeImgCenterPointLocation({ device, template: templatePath, folderPath, scaleRange: [minS, maxS] }) if (!result.success) return { success: false, error: `图像中心点定位失败: ${result.error}` } const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null) if (outputVarName) { if (result.center && typeof result.center === 'object' && result.center.x !== undefined && result.center.y !== undefined) { variableContext[outputVarName] = { x: Math.round(Number(result.center.x)), y: Math.round(Number(result.center.y)) } } else { variableContext[outputVarName] = null } 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) area = action.inVars[0] if (action.inVars.length > 1) savePath = 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?.[0] != null) { const outputVarName = extractVarName(String(action.outVars[0]).trim()) if (outputVarName) variableContext[outputVarName] = result.success ? '1' : '0' } await logOutVars(action, variableContext, folderPath) return { success: true } } case 'ocr': { const { executeOcr, executeOcrFindText } = get(funcDir, 'img') let imageOrText = action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0 ? action.inVars[0] : action.image if (imageOrText == null || String(imageOrText).trim() === '') return { success: false, error: 'ocr 缺少参数:图片路径或要查找的文字(inVars[0] / image)' } const baseDir = folderPath && typeof folderPath === 'string' ? folderPath : (ctx.compilerConfig && ctx.compilerConfig.projectRoot) || process.cwd() const fs = require('fs') const isAbsoluteOrDrive = String(imageOrText).startsWith('/') || String(imageOrText).includes(':') const hasSubPath = String(imageOrText).includes('/') || String(imageOrText).includes(path.sep) const resolvedPath = isAbsoluteOrDrive ? imageOrText : (hasSubPath ? path.join(baseDir, imageOrText) : path.join(baseDir, 'resources', imageOrText)) const isImagePath = fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile() const outputVarName = action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0 ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null) if (isImagePath) { const result = await executeOcr({ imagePath: imageOrText, folderPath }) if (!result.success) return { success: false, error: result.error } if (outputVarName) { variableContext[outputVarName] = result.text != null ? String(result.text) : '' await logOutVars(action, variableContext, folderPath) } return { success: true, result: result.text } } if (!device) return { success: false, error: 'ocr 按文字查找需设备截图,当前无设备' } const findResult = await executeOcrFindText({ device, findText: String(imageOrText).trim(), folderPath }) if (!findResult.success) return { success: false, error: findResult.error } if (outputVarName) { variableContext[outputVarName] = findResult.center && typeof findResult.center === 'object' ? JSON.stringify({ x: findResult.center.x, y: findResult.center.y }) : '' await logOutVars(action, variableContext, folderPath) } return { success: true, result: findResult.center } } 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(String(outputVars[0]).trim()) : (action.textVariable ? extractVarName(action.textVariable) : null) let senderVar = outputVars.length > 1 ? extractVarName(String(outputVars[1]).trim()) : (action.senderVariable ? extractVarName(action.senderVariable) : null) 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 if (!textVar && !senderVar) return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' } 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) filePath = action.inVars[0] if (action.outVars?.length > 0) varName = extractVarName(String(action.outVars[0]).trim()) 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) history = action.inVars[0] if (action.inVars.length > 1) current = 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?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (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) content = action.inVars[0] if (action.inVars.length > 1) filePath = 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?.[0] != null) { const outputVarName = extractVarName(String(action.outVars[0]).trim()) 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 = action.inVars[0] const param2 = action.inVars[1] if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) && typeof param2 === 'string' && rgbPattern.test(param2.trim())) { friendRgb = param1.trim() myRgb = param2.trim() regionArea = action.inVars[2] } else { avatar1Name = action.inVars[0] avatar2Name = action.inVars[1] regionArea = action.inVars[2] } regionArea = parseRegion(regionArea) } else if (action.inVars.length >= 2) { const param1 = action.inVars[0] const param2 = 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 = param1 avatar2Name = param2 } } 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, variableContext) if (resolved) avatar1Path = `${folderName}/resources/${resolved}` } if (avatar2Name) { const resolved = resolveValue(avatar2Name, variableContext) 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?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (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 (prompt && 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 = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null if (outputVarName) variableContext[outputVarName] = result } if (action.outVars.length > 1) { const callbackVarName = action.outVars?.[1] != null ? extractVarName(String(action.outVars[1]).trim()) : null 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 && action.inVars[1] != null) { const callbackVarName = extractVarName(String(action.inVars[1]).trim()) 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 || [] const targetText = inVars.length > 0 ? (inVars[0] ?? action.value) : (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: { const regDef = REGISTRY_BY_TYPE.get(actionType) if (regDef) { if (regDef.customRun) { const script = getRegistryScript(funcDir, actionType) if (script && typeof script.runNode === 'function') { const runCtx = { ...ctx, get, funcDir, folderPath, device } const result = await script.runNode(action, runCtx) if (result && result.success !== false) await logOutVars(action, variableContext, folderPath) return result != null ? result : { success: true } } } const inKeys = regDef.in || [] const inAlt = regDef.inAlt || {} const input = {} inKeys.forEach((key, i) => { let val = action[key] if (action.inVars && action.inVars[i] !== undefined) val = action.inVars[i] if (val === undefined && inAlt[key]) val = action[inAlt[key]] input[key] = val != null ? String(val).trim() : val }) input.folderPath = folderPath const mod = get(funcDir, regDef.category) const fn = mod[regDef.execute] if (!fn) { return { success: false, error: `fun-parser: ${regDef.execute} not found` } } let result try { result = await fn(input) } catch (e) { return { success: false, error: (e && (e.message || String(e))) || 'execute threw' } } if (!result || !result.success) { return { success: false, error: (result && result.error) || 'execute failed' } } const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null if (outputVarName && result != null) { const outVal = result.path ?? result.value ?? result.result if (outVal !== undefined && outVal !== null) variableContext[outputVarName] = typeof outVal === 'string' ? outVal : String(outVal) } await logOutVars(action, variableContext, folderPath) return { success: true, ...result } } return { success: false, error: `fun-parser 不支持的 type: ${actionType}` } } } } const FUN_TYPES = new Set([ 'ai', 'img-bounding-box-location', 'img-center-point-location', 'img-cropping', 'ocr', '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', ].concat(REGISTERED_TYPES)) function supports(type) { return FUN_TYPES.has(type) } module.exports = { types, parse, execute, runAction, get, run, supports }