ef-compiler.js 123 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481
  1. // EasyFlow 编译器 - 工作流任务解析和执行器
  2. // 配置参数
  3. const DEFAULT_STEP_INTERVAL = 1000; // 默认步骤间隔1秒
  4. const DEFAULT_SCROLL_DISTANCE = 100; // 默认每次滚动距离(像素)
  5. // 变量上下文(用于存储变量值)
  6. let variableContext = {};
  7. // 变量上下文是否已初始化(防止循环中重置变量)
  8. let variableContextInitialized = false;
  9. // 全局步骤计数器(用于连续的步骤编号)
  10. let globalStepCounter = 0;
  11. // 当前工作流文件夹路径(用于日志记录)
  12. let currentWorkflowFolderPath = null;
  13. /**
  14. * 记录日志(同时输出到 console 和文件)
  15. * @param {string} message - 日志消息
  16. * @param {string} folderPath - 工作流文件夹路径(可选,如果不提供则使用 currentWorkflowFolderPath)
  17. */
  18. async function logMessage(message, folderPath = null) {
  19. // 尝试写入日志文件(异步,不阻塞主流程)
  20. // 注意:不输出到 console,只写入日志文件
  21. try {
  22. const targetFolderPath = folderPath || currentWorkflowFolderPath;
  23. if (targetFolderPath && electronAPI.appendLog) {
  24. electronAPI.appendLog(targetFolderPath, message).catch(err => {
  25. // 静默失败,不影响主流程
  26. });
  27. }
  28. } catch (error) {
  29. // 静默失败,不影响主流程
  30. }
  31. }
  32. // 打印所有 outVars 的值
  33. async function logOutVars(action, variableContext, folderPath = null) {
  34. if (!action.outVars || !Array.isArray(action.outVars) || action.outVars.length === 0) {
  35. return;
  36. }
  37. const outVarsInfo = action.outVars.map((varName, index) => {
  38. const varNameClean = extractVarName(varName);
  39. const value = variableContext[varNameClean];
  40. // 如果值太长,截断显示(超过100字符)
  41. let displayValue = value;
  42. if (typeof value === 'string' && value.length > 100) {
  43. displayValue = value.substring(0, 100) + '...';
  44. }
  45. return `${varNameClean}: ${JSON.stringify(displayValue)}`;
  46. });
  47. // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
  48. // const logMsg = `输出变量: ${outVarsInfo.join(', ')}`;
  49. // await logMessage(logMsg, folderPath);
  50. }
  51. const path = require('path')
  52. const funcDir = path.join(__dirname, 'Func')
  53. const { generateHistorySummary, getHistorySummary } = require(path.join(funcDir, 'chat', 'chat-history.js'))
  54. const { executeOcrChat } = require(path.join(funcDir, 'chat', 'ocr-chat.js'))
  55. const { executeImageRegionLocation } = require(path.join(funcDir, 'image-region-location.js'))
  56. const { executeImageCenterLocation } = require(path.join(funcDir, 'image-center-location.js'))
  57. const { executeImageAreaCropping } = require(path.join(funcDir, 'image-area-cropping.js'))
  58. const { executeReadLastMessage } = require(path.join(funcDir, 'chat', 'read-last-message.js'))
  59. const { executeReadTxt } = require(path.join(funcDir, 'read-txt.js'))
  60. const { executeSaveTxt } = require(path.join(funcDir, 'save-txt.js'))
  61. const { executeSmartChatAppend } = require(path.join(funcDir, 'chat', 'smart-chat-append.js'))
  62. const electronAPI = require('./node-api.js')
  63. /**
  64. * 解析时间字符串(格式:2026/1/13 02:09)
  65. * @param {string} timeStr - 时间字符串
  66. * @returns {Date|null} 解析后的日期对象,失败返回null
  67. */
  68. function parseTimeString(timeStr) {
  69. if (!timeStr || timeStr.trim() === '') {
  70. return null;
  71. }
  72. try {
  73. // 支持格式:2026/1/13 02:09 或 2026/01/13 02:09
  74. const parts = timeStr.trim().split(' ');
  75. if (parts.length !== 2) {
  76. return null;
  77. }
  78. const datePart = parts[0].split('/');
  79. const timePart = parts[1].split(':');
  80. if (datePart.length !== 3 || timePart.length !== 2) {
  81. return null;
  82. }
  83. const year = parseInt(datePart[0], 10);
  84. const month = parseInt(datePart[1], 10) - 1; // 月份从0开始
  85. const day = parseInt(datePart[2], 10);
  86. const hour = parseInt(timePart[0], 10);
  87. const minute = parseInt(timePart[1], 10);
  88. const date = new Date(year, month, day, hour, minute, 0, 0);
  89. // 验证日期是否有效
  90. if (isNaN(date.getTime())) {
  91. return null;
  92. }
  93. return date;
  94. } catch (error) {
  95. return null;
  96. }
  97. }
  98. /**
  99. * 解析延迟字符串(格式:10s, 5m, 2h)
  100. * @param {string} delayStr - 延迟字符串
  101. * @returns {number|null} 延迟的毫秒数,失败返回null
  102. */
  103. function parseDelayString(delayStr) {
  104. if (!delayStr || delayStr.trim() === '') {
  105. return 0; // 空字符串表示不延迟
  106. }
  107. try {
  108. const trimmed = delayStr.trim();
  109. const unit = trimmed.slice(-1).toLowerCase();
  110. const value = parseInt(trimmed.slice(0, -1), 10);
  111. if (isNaN(value) || value < 0) {
  112. return null;
  113. }
  114. switch (unit) {
  115. case 's':
  116. return value * 1000; // 秒转毫秒
  117. case 'm':
  118. return value * 60 * 1000; // 分钟转毫秒
  119. case 'h':
  120. return value * 60 * 60 * 1000; // 小时转毫秒
  121. default:
  122. return null;
  123. }
  124. } catch (error) {
  125. return null;
  126. }
  127. }
  128. /**
  129. * 计算需要等待的时间(毫秒)
  130. * @param {string} data - 执行时间字符串(格式:2026/1/13 02:09)
  131. * @param {string} delay - 延迟字符串(格式:10s, 5m, 2h)
  132. * @returns {number} 需要等待的毫秒数
  133. */
  134. function calculateWaitTime(data, delay) {
  135. // 始终返回 0,不等待,立即执行
  136. return 0;
  137. }
  138. /**
  139. * 从变量名中提取变量名(去除 {variable} 格式的包裹)
  140. * @param {string} varName - 变量名(可能包含 {})
  141. * @returns {string} 提取后的变量名
  142. */
  143. function extractVarName(varName) {
  144. if (typeof varName === 'string' && varName.startsWith('{') && varName.endsWith('}')) {
  145. return varName.slice(1, -1);
  146. }
  147. return varName;
  148. }
  149. /**
  150. * 替换字符串中的变量(只支持 {{variable}} 格式,用于字符串拼接)
  151. * @param {string} str - 原始字符串
  152. * @param {Object} context - 变量上下文
  153. * @returns {string} 替换后的字符串
  154. */
  155. function replaceVariablesInString(str, context = variableContext) {
  156. if (typeof str !== 'string') {
  157. return str;
  158. }
  159. let result = str;
  160. // 只替换 {{variable}} 格式的变量(双花括号,用于字符串拼接)
  161. // 支持变量名中包含连字符,如 {{chat-history}}
  162. const doubleBracePattern = /\{\{([\w-]+)\}\}/g;
  163. result = result.replace(doubleBracePattern, (match, varName) => {
  164. const varValue = context[varName];
  165. if (varValue === undefined || varValue === null) {
  166. return '';
  167. }
  168. // 如果值是空字符串,返回空字符串
  169. if (varValue === '') {
  170. return '';
  171. }
  172. // 如果值是字符串 "undefined" 或 "null",视为空
  173. if (varValue === 'undefined' || varValue === 'null') {
  174. return '';
  175. }
  176. // 如果是字符串,尝试判断是否是 JSON 数组字符串
  177. if (typeof varValue === 'string') {
  178. try {
  179. const parsed = JSON.parse(varValue);
  180. if (Array.isArray(parsed)) {
  181. // 如果是空数组,返回空字符串
  182. if (parsed.length === 0) {
  183. return '';
  184. }
  185. // 如果不是空数组,返回原始 JSON 字符串
  186. return varValue;
  187. }
  188. } catch (e) {
  189. // 不是 JSON,按普通字符串处理
  190. }
  191. }
  192. // 如果是数组或对象,转换为 JSON 字符串
  193. if (Array.isArray(varValue) || typeof varValue === 'object') {
  194. try {
  195. return JSON.stringify(varValue);
  196. } catch (e) {
  197. return String(varValue);
  198. }
  199. }
  200. return String(varValue);
  201. });
  202. return result;
  203. }
  204. /**
  205. * 解析变量值(支持 {variable} 格式)
  206. * @param {any} value - 原始值
  207. * @param {Object} context - 变量上下文
  208. * @returns {any} 解析后的值
  209. */
  210. function resolveValue(value, context = variableContext) {
  211. if (typeof value === 'string' && value.startsWith('{') && value.endsWith('}')) {
  212. const varName = value.slice(1, -1);
  213. return context[varName] !== undefined ? context[varName] : value;
  214. }
  215. if (Array.isArray(value)) {
  216. return value.map(item => resolveValue(item, context));
  217. }
  218. if (typeof value === 'object' && value !== null) {
  219. const resolved = {};
  220. for (const key in value) {
  221. resolved[key] = resolveValue(value[key], context);
  222. }
  223. return resolved;
  224. }
  225. return value;
  226. }
  227. /**
  228. * 手动解析并计算算术表达式(不使用 eval 或 Function)
  229. * 支持 +, -, *, / 和括号
  230. * @param {string} expr - 表达式字符串,如 "1+2*3"
  231. * @returns {number} 计算结果
  232. */
  233. function parseArithmeticExpression(expr) {
  234. let index = 0;
  235. // 跳过空格
  236. const skipWhitespace = () => {
  237. while (index < expr.length && /\s/.test(expr[index])) {
  238. index++;
  239. }
  240. };
  241. // 解析数字
  242. const parseNumber = () => {
  243. skipWhitespace();
  244. let numStr = '';
  245. let hasDot = false;
  246. while (index < expr.length) {
  247. const char = expr[index];
  248. if (char >= '0' && char <= '9') {
  249. numStr += char;
  250. index++;
  251. } else if (char === '.' && !hasDot) {
  252. numStr += '.';
  253. hasDot = true;
  254. index++;
  255. } else {
  256. break;
  257. }
  258. }
  259. if (numStr === '') {
  260. throw new Error('期望数字');
  261. }
  262. const num = parseFloat(numStr);
  263. if (isNaN(num)) {
  264. throw new Error(`无效的数字: ${numStr}`);
  265. }
  266. return num;
  267. };
  268. // 解析因子(数字或括号表达式)
  269. const parseFactor = () => {
  270. skipWhitespace();
  271. if (index >= expr.length) {
  272. throw new Error('表达式不完整');
  273. }
  274. // 处理负号(一元运算符)
  275. let isNegative = false;
  276. if (expr[index] === '-') {
  277. isNegative = true;
  278. index++;
  279. skipWhitespace();
  280. } else if (expr[index] === '+') {
  281. // 正号可以忽略
  282. index++;
  283. skipWhitespace();
  284. }
  285. let result;
  286. if (expr[index] === '(') {
  287. index++; // 跳过 '('
  288. result = parseExpression();
  289. skipWhitespace();
  290. if (index >= expr.length || expr[index] !== ')') {
  291. throw new Error('缺少右括号');
  292. }
  293. index++; // 跳过 ')'
  294. } else {
  295. result = parseNumber();
  296. }
  297. return isNegative ? -result : result;
  298. };
  299. // 解析项(处理 * 和 /)
  300. const parseTerm = () => {
  301. let result = parseFactor();
  302. skipWhitespace();
  303. while (index < expr.length) {
  304. const op = expr[index];
  305. if (op === '*') {
  306. index++;
  307. result *= parseFactor();
  308. } else if (op === '/') {
  309. index++;
  310. const divisor = parseFactor();
  311. if (divisor === 0) {
  312. throw new Error('除以零');
  313. }
  314. result /= divisor;
  315. } else {
  316. break;
  317. }
  318. skipWhitespace();
  319. }
  320. return result;
  321. };
  322. // 解析表达式(处理 + 和 -)
  323. const parseExpression = () => {
  324. let result = parseTerm();
  325. skipWhitespace();
  326. while (index < expr.length) {
  327. const op = expr[index];
  328. if (op === '+') {
  329. index++;
  330. result += parseTerm();
  331. } else if (op === '-') {
  332. index++;
  333. result -= parseTerm();
  334. } else {
  335. break;
  336. }
  337. skipWhitespace();
  338. }
  339. return result;
  340. };
  341. try {
  342. const result = parseExpression();
  343. skipWhitespace();
  344. if (index < expr.length) {
  345. throw new Error(`表达式解析不完整,剩余: ${expr.substring(index)}`);
  346. }
  347. return result;
  348. } catch (error) {
  349. throw new Error(`表达式解析失败: ${error.message}`);
  350. }
  351. }
  352. /**
  353. * 评估算术表达式(支持 +, -, *, / 运算)
  354. * 只处理 {variable} 格式(单花括号),用于数字运算
  355. * @param {string} expression - 表达式字符串,如 "{turn} + 1"
  356. * @param {Object} context - 变量上下文
  357. * @returns {any} 计算结果
  358. */
  359. function evaluateExpression(expression, context = variableContext) {
  360. if (typeof expression !== 'string') {
  361. return expression;
  362. }
  363. try {
  364. // 替换变量(只处理 {variable} 格式,单花括号用于数字运算)
  365. let expr = expression.trim();
  366. // 只匹配 {variable} 格式,不匹配 {{variable}} 格式
  367. // 使用负向前瞻确保不会匹配双花括号的开始部分
  368. const varPattern = /\{(\w+)\}(?!\})/g;
  369. const originalExpr = expr;
  370. let hasVariables = false;
  371. expr = expr.replace(varPattern, (match, varName) => {
  372. hasVariables = true;
  373. const value = context[varName];
  374. if (value === undefined || value === null) {
  375. return '0';
  376. }
  377. // 数字类型直接转换
  378. if (typeof value === 'number') {
  379. return String(value);
  380. }
  381. // 布尔类型转换
  382. if (typeof value === 'boolean') {
  383. return value ? '1' : '0';
  384. }
  385. // 尝试将字符串转换为数字
  386. if (typeof value === 'string') {
  387. const numValue = Number(value);
  388. // 如果字符串可以转换为数字,且不是空字符串,使用数字
  389. if (!isNaN(numValue) && value.trim() !== '') {
  390. return String(numValue);
  391. }
  392. // 如果无法转换为数字,返回 0 避免错误
  393. return '0';
  394. }
  395. // 其他类型尝试转换为数字
  396. const numValue = Number(value);
  397. if (!isNaN(numValue)) {
  398. return String(numValue);
  399. }
  400. return '0';
  401. });
  402. // 如果没有变量且没有运算符,直接返回原值
  403. if (!hasVariables && !/[+\-*/]/.test(expr)) {
  404. const numValue = Number(expr);
  405. if (!isNaN(numValue) && expr.trim() !== '') {
  406. return numValue;
  407. }
  408. return expr;
  409. }
  410. // 检查是否包含算术运算符
  411. if (!/[+\-*/]/.test(expr)) {
  412. // 没有运算符,直接返回解析后的值
  413. const numValue = Number(expr);
  414. if (!isNaN(numValue) && expr.trim() !== '') {
  415. return numValue;
  416. }
  417. return expr;
  418. }
  419. // 手动解析简单的算术表达式(只支持 +, -, *, /)
  420. // 移除所有空格
  421. expr = expr.replace(/\s+/g, '');
  422. // 检查是否只包含数字、运算符、小数点和括号
  423. if (!/^[0-9+\-*/().]+$/.test(expr)) {
  424. // 包含不允许的字符,返回原值
  425. return resolveValue(originalExpr, context);
  426. }
  427. // 验证表达式格式(防止注入)
  428. // 确保表达式以数字或括号开头,以数字或括号结尾
  429. if (!/^[0-9(]/.test(expr) || !/[0-9)]$/.test(expr)) {
  430. return resolveValue(originalExpr, context);
  431. }
  432. // 手动解析并计算表达式(不使用 eval 或 Function)
  433. const result = parseArithmeticExpression(expr);
  434. // 如果结果是数字,返回数字类型
  435. if (typeof result === 'number' && !isNaN(result) && isFinite(result)) {
  436. return result;
  437. }
  438. // 如果结果不是有效数字,返回原值
  439. return resolveValue(originalExpr, context);
  440. } catch (error) {
  441. // 如果计算失败,尝试直接解析变量
  442. return resolveValue(expression, context);
  443. }
  444. }
  445. /**
  446. * 评估条件表达式(不使用eval,手动解析)
  447. * @param {string} condition - 条件表达式
  448. * @param {Object} context - 变量上下文
  449. * @returns {boolean} 条件结果
  450. */
  451. function evaluateCondition(condition, context = variableContext) {
  452. if (!condition) return true;
  453. try {
  454. // 替换变量
  455. // 支持变量名中包含连字符,如 {chat-history}
  456. // 使用 [\w-]+ 来匹配字母、数字、下划线和连字符
  457. let expr = condition;
  458. const varPattern = /\{([\w-]+)\}/g;
  459. expr = expr.replace(varPattern, (match, varName) => {
  460. const value = context[varName];
  461. // 如果变量不存在,视为空字符串(所有变量都是 string 或 int 类型)
  462. if (value === undefined || value === null) {
  463. return '""';
  464. }
  465. // 如果值是空字符串,也返回 '""'
  466. if (value === '') {
  467. return '""';
  468. }
  469. // 如果值是字符串 "undefined" 或 "null",也视为空字符串
  470. if (value === 'undefined' || value === 'null') {
  471. return '""';
  472. }
  473. // 确保字符串类型
  474. if (typeof value === 'string') {
  475. // 尝试判断是否是 JSON 字符串
  476. try {
  477. const parsed = JSON.parse(value);
  478. if (Array.isArray(parsed)) {
  479. // 如果是 JSON 数组字符串,转换为 JSON 字符串用于比较(保持原格式)
  480. const escaped = value.replace(/"/g, '\\"');
  481. return `"${escaped}"`;
  482. }
  483. } catch (e) {
  484. // 不是 JSON,按普通字符串处理
  485. }
  486. // 转义字符串中的引号
  487. const escaped = value.replace(/"/g, '\\"');
  488. return `"${escaped}"`;
  489. }
  490. if (Array.isArray(value)) {
  491. // 数组转换为 JSON 字符串(与 chat-history 和 currentMessage 格式一致)
  492. try {
  493. const jsonStr = JSON.stringify(value);
  494. const escaped = jsonStr.replace(/"/g, '\\"');
  495. return `"${escaped}"`;
  496. } catch (e) {
  497. return `"[]"`;
  498. }
  499. }
  500. if (typeof value === 'number') return value;
  501. if (typeof value === 'boolean') return value;
  502. // 其他类型转为字符串
  503. return `"${String(value)}"`;
  504. });
  505. // 手动解析简单的条件表达式(不使用eval)
  506. // 支持: ==, !=, >, <, >=, <=, &&, ||
  507. const result = parseConditionExpression(expr);
  508. return result;
  509. } catch (error) {
  510. return false;
  511. }
  512. }
  513. /**
  514. * 手动解析条件表达式(避免使用eval)
  515. * @param {string} expr - 表达式字符串
  516. * @returns {boolean} 结果
  517. */
  518. function parseConditionExpression(expr) {
  519. // 去除空格
  520. expr = expr.trim();
  521. // 处理逻辑运算符(从低优先级到高优先级)
  522. // 先处理 ||
  523. if (expr.includes('||')) {
  524. const parts = expr.split('||').map(p => p.trim());
  525. return parts.some(part => parseConditionExpression(part));
  526. }
  527. // 处理 &&
  528. if (expr.includes('&&')) {
  529. const parts = expr.split('&&').map(p => p.trim());
  530. return parts.every(part => parseConditionExpression(part));
  531. }
  532. // 处理比较运算符
  533. const operators = [
  534. { op: '!=', fn: (a, b) => a != b },
  535. { op: '==', fn: (a, b) => {
  536. // 确保类型一致:如果一边是字符串,另一边也转为字符串
  537. if (typeof a === 'string' && typeof b !== 'string') {
  538. b = String(b);
  539. } else if (typeof b === 'string' && typeof a !== 'string') {
  540. a = String(a);
  541. }
  542. // 特殊处理:空字符串比较
  543. // 如果两边都是空字符串,直接返回 true
  544. if (a === '' && b === '') {
  545. return true;
  546. }
  547. // 特殊处理:如果一边是空字符串,另一边是 JSON 数组字符串,判断数组是否为空
  548. if ((a === '' && typeof b === 'string') || (b === '' && typeof a === 'string')) {
  549. const jsonStr = a === '' ? b : a;
  550. try {
  551. const parsed = JSON.parse(jsonStr);
  552. if (Array.isArray(parsed)) {
  553. return parsed.length === 0; // 空数组 == 空字符串
  554. }
  555. } catch (e) {
  556. // 不是 JSON,按普通比较
  557. }
  558. }
  559. return a == b;
  560. }},
  561. { op: '>=', fn: (a, b) => Number(a) >= Number(b) },
  562. { op: '<=', fn: (a, b) => Number(a) <= Number(b) },
  563. { op: '>', fn: (a, b) => Number(a) > Number(b) },
  564. { op: '<', fn: (a, b) => Number(a) < Number(b) }
  565. ];
  566. for (const { op, fn } of operators) {
  567. if (expr.includes(op)) {
  568. const parts = expr.split(op).map(p => p.trim());
  569. if (parts.length === 2) {
  570. const left = parseValue(parts[0]);
  571. const right = parseValue(parts[1]);
  572. // 调试:如果是 relationBg 相关的条件,输出调试信息
  573. if (expr.includes('relationBg')) {
  574. // 只在开发时输出,不影响生产
  575. }
  576. return fn(left, right);
  577. }
  578. }
  579. }
  580. // 如果没有运算符,尝试解析为布尔值
  581. const value = parseValue(expr);
  582. if (typeof value === 'boolean') return value;
  583. if (typeof value === 'string') {
  584. // 空字符串为false
  585. if (value === '' || value === 'undefined') return false;
  586. // 尝试解析 JSON 字符串,如果是空数组 "[]",返回 false
  587. try {
  588. const parsed = JSON.parse(value);
  589. if (Array.isArray(parsed)) {
  590. return parsed.length > 0;
  591. }
  592. // 如果是空对象 "{}",返回 false
  593. if (typeof parsed === 'object' && Object.keys(parsed).length === 0) {
  594. return false;
  595. }
  596. } catch (e) {
  597. // 不是 JSON,按普通字符串处理
  598. }
  599. // 非空字符串为true
  600. return true;
  601. }
  602. // 数字:0为false,非0为true
  603. if (typeof value === 'number') return value !== 0;
  604. return Boolean(value);
  605. }
  606. /**
  607. * 解析值(字符串、数字、布尔值)
  608. * @param {string} str - 字符串
  609. * @returns {any} 解析后的值
  610. */
  611. function parseValue(str) {
  612. str = str.trim();
  613. // 布尔值
  614. if (str === 'true') return true;
  615. if (str === 'false') return false;
  616. // 字符串(带引号)
  617. if ((str.startsWith('"') && str.endsWith('"')) ||
  618. (str.startsWith("'") && str.endsWith("'"))) {
  619. return str.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'");
  620. }
  621. // 数字
  622. if (/^-?\d+(\.\d+)?$/.test(str)) {
  623. return parseFloat(str);
  624. }
  625. // undefined
  626. if (str === 'undefined') return undefined;
  627. // 默认返回字符串
  628. return str;
  629. }
  630. /**
  631. * 解析工作流格式(支持 variables, execute)
  632. * @param {Object} workflow - 工作流配置对象
  633. * @returns {Object} 解析后的工作流
  634. */
  635. function parseWorkflow(workflow) {
  636. if (!workflow || typeof workflow !== 'object') {
  637. return null;
  638. }
  639. // 初始化变量上下文(仅在未初始化时初始化,避免循环中重置)
  640. if (!variableContextInitialized) {
  641. variableContext = workflow.variables ? {} : {};
  642. if (workflow.variables) {
  643. // 转换变量类型:只允许 number 或 string
  644. for (const key in workflow.variables) {
  645. const value = workflow.variables[key];
  646. if (value === null || value === undefined) {
  647. variableContext[key] = ''; // null/undefined 转换为空字符串
  648. } else if (typeof value === 'boolean') {
  649. variableContext[key] = value ? '1' : '0'; // boolean 转换为字符串 '1' 或 '0'
  650. } else if (typeof value === 'number') {
  651. variableContext[key] = value; // number 保持不变
  652. } else if (typeof value === 'string') {
  653. variableContext[key] = value; // string 保持不变
  654. } else {
  655. variableContext[key] = String(value); // 其他类型转换为字符串
  656. }
  657. }
  658. }
  659. variableContextInitialized = true;
  660. } else {
  661. // 如果已初始化,只更新新增的变量(保留已有变量值)
  662. if (workflow.variables) {
  663. for (const key in workflow.variables) {
  664. // 如果变量已存在且不为空,则保留原值;否则使用新值
  665. if (variableContext[key] === undefined || variableContext[key] === null || variableContext[key] === '') {
  666. const value = workflow.variables[key];
  667. // 转换变量类型:只允许 number 或 string
  668. if (value === null || value === undefined) {
  669. variableContext[key] = ''; // null/undefined 转换为空字符串
  670. } else if (typeof value === 'boolean') {
  671. variableContext[key] = value ? '1' : '0'; // boolean 转换为字符串 '1' 或 '0'
  672. } else if (typeof value === 'number') {
  673. variableContext[key] = value; // number 保持不变
  674. } else if (typeof value === 'string') {
  675. variableContext[key] = value; // string 保持不变
  676. } else {
  677. variableContext[key] = String(value); // 其他类型转换为字符串
  678. }
  679. }
  680. }
  681. }
  682. }
  683. // 解析 execute 字段
  684. let actions = [];
  685. if (workflow.execute && Array.isArray(workflow.execute)) {
  686. actions = parseActions(workflow.execute);
  687. } else {
  688. // 如果没有 execute 字段,返回空数组
  689. actions = [];
  690. }
  691. // 处理 schedule 字段
  692. let schedule = workflow.schedule || {};
  693. // 如果 schedule 是数组,转换为对象格式(取第一个元素)
  694. if (Array.isArray(schedule)) {
  695. schedule = schedule.length > 0 ? schedule[0] : {};
  696. }
  697. // 如果 schedule 是对象但包含嵌套的 schedule 字段,提取内部对象
  698. if (schedule && typeof schedule === 'object' && schedule.schedule) {
  699. schedule = schedule.schedule;
  700. }
  701. // 处理 repeat 字段:将 "forever" 转换为 -1
  702. if (schedule && typeof schedule === 'object') {
  703. if (schedule.repeat === 'forever' || schedule.repeat === 'Forever') {
  704. schedule.repeat = -1;
  705. }
  706. }
  707. return {
  708. schedule: schedule,
  709. variables: variableContext,
  710. actions: actions
  711. };
  712. }
  713. /**
  714. * 解析操作数组(支持新旧格式)
  715. * @param {Array} actions - 操作数组
  716. * @returns {Array} 解析后的操作列表
  717. */
  718. function parseActions(actions) {
  719. if (!Array.isArray(actions)) {
  720. return [];
  721. }
  722. const parsedActions = [];
  723. for (const action of actions) {
  724. if (typeof action !== 'object' || action === null) {
  725. continue;
  726. }
  727. // 新格式:使用 type 字段
  728. if (action.type) {
  729. parsedActions.push(parseNewFormatAction(action));
  730. }
  731. // 旧格式:向后兼容
  732. else {
  733. parsedActions.push(parseOldFormatAction(action));
  734. }
  735. }
  736. return parsedActions;
  737. }
  738. /**
  739. * 解析新格式操作
  740. * @param {Object} action - 操作对象
  741. * @returns {Object} 解析后的操作
  742. */
  743. function parseNewFormatAction(action) {
  744. const parsed = {
  745. type: action.type,
  746. method: action.method,
  747. // target和value保留原始值,在执行时再解析(因为变量可能在执行时才被赋值)
  748. target: action.target,
  749. value: action.value,
  750. variable: action.variable,
  751. condition: action.condition,
  752. delay: action.delay || '',
  753. timeout: action.timeout,
  754. retry: action.retry
  755. };
  756. // 根据类型添加特定字段
  757. switch (action.type) {
  758. case 'adb':
  759. // 统一 ADB 操作,通过 method 区分
  760. parsed.method = action.method;
  761. // 支持新的 inVars/outVars 格式(都可以为空)
  762. if (action.inVars && Array.isArray(action.inVars)) {
  763. parsed.inVars = action.inVars.map(v => extractVarName(v));
  764. } else {
  765. parsed.inVars = [];
  766. }
  767. if (action.outVars && Array.isArray(action.outVars)) {
  768. parsed.outVars = action.outVars.map(v => extractVarName(v));
  769. } else {
  770. parsed.outVars = [];
  771. }
  772. // 向后兼容:保留旧字段
  773. parsed.target = action.target;
  774. parsed.value = action.value;
  775. parsed.variable = action.variable;
  776. parsed.clear = action.clear || false;
  777. break;
  778. case 'locate':
  779. // locate 操作(向后兼容)
  780. break;
  781. case 'click':
  782. // click 操作(向后兼容)
  783. break;
  784. case 'input':
  785. parsed.clear = action.clear || false;
  786. break;
  787. case 'ocr':
  788. parsed.area = action.area;
  789. parsed.avatar = resolveValue(action.avatar);
  790. break;
  791. case 'extract-messages':
  792. case 'ocr-chat':
  793. case 'ocr-chat-history': // 向后兼容
  794. case 'extract-chat-history': // 向后兼容
  795. // 支持新的 inVars/outVars 格式(都可以为空)
  796. if (action.inVars && Array.isArray(action.inVars)) {
  797. parsed.inVars = action.inVars.map(v => extractVarName(v));
  798. if (action.inVars.length >= 2) {
  799. parsed.avatar1 = action.inVars[0];
  800. parsed.avatar2 = action.inVars[1];
  801. } else if (action.inVars.length === 1) {
  802. parsed.avatar1 = action.inVars[0];
  803. parsed.avatar2 = action.avatar2;
  804. }
  805. } else {
  806. parsed.inVars = [];
  807. parsed.avatar1 = action.avatar1;
  808. parsed.avatar2 = action.avatar2;
  809. }
  810. if (action.outVars && Array.isArray(action.outVars)) {
  811. parsed.outVars = action.outVars.map(v => extractVarName(v));
  812. if (action.outVars.length > 0) {
  813. parsed.variable = extractVarName(action.outVars[0]);
  814. }
  815. } else {
  816. parsed.outVars = [];
  817. if (action.variable) {
  818. parsed.variable = extractVarName(action.variable);
  819. }
  820. }
  821. // 向后兼容:支持旧的 friendAvatar 和 myAvatar
  822. if (action.friendAvatar && !parsed.avatar1) parsed.avatar1 = action.friendAvatar;
  823. if (action.myAvatar && !parsed.avatar2) parsed.avatar2 = action.myAvatar;
  824. break;
  825. case 'save-messages':
  826. // 保存消息记录到文件
  827. break;
  828. case 'generate-summary':
  829. parsed.summaryVariable = action.summaryVariable;
  830. break;
  831. case 'ai-generate':
  832. parsed.prompt = resolveValue(action.prompt);
  833. parsed.model = action.model;
  834. // 支持新的 inVars/outVars 格式(都可以为空)
  835. if (action.inVars && Array.isArray(action.inVars)) {
  836. parsed.inVars = action.inVars.map(v => extractVarName(v));
  837. } else {
  838. parsed.inVars = [];
  839. }
  840. if (action.outVars && Array.isArray(action.outVars)) {
  841. parsed.outVars = action.outVars.map(v => extractVarName(v));
  842. if (action.outVars.length > 0) {
  843. parsed.variable = extractVarName(action.outVars[0]);
  844. }
  845. } else {
  846. parsed.outVars = [];
  847. if (action.variable) {
  848. parsed.variable = extractVarName(action.variable);
  849. }
  850. }
  851. break;
  852. case 'schedule':
  853. // schedule 操作:condition 定义调度条件,interval 定义要执行的动作
  854. parsed.condition = action.condition || {};
  855. // 处理 repeat 字段:将 "forever" 转换为 -1
  856. if (parsed.condition && typeof parsed.condition === 'object') {
  857. if (parsed.condition.repeat === 'forever' || parsed.condition.repeat === 'Forever') {
  858. parsed.condition.repeat = -1;
  859. }
  860. }
  861. // interval 字段包含要执行的动作数组
  862. if (action.interval && Array.isArray(action.interval)) {
  863. parsed.interval = parseActions(action.interval);
  864. } else {
  865. parsed.interval = [];
  866. }
  867. break;
  868. case 'if':
  869. // 支持 ture 作为 then 的别名(可能是拼写错误,但为了兼容性支持)
  870. parsed.then = action.then || action.ture ? parseActions(action.then || action.ture) : [];
  871. parsed.else = action.else ? parseActions(action.else) : [];
  872. break;
  873. case 'for':
  874. parsed.variable = action.variable;
  875. parsed.items = resolveValue(action.items);
  876. parsed.body = action.body ? parseActions(action.body) : [];
  877. break;
  878. case 'while':
  879. // 支持 ture 作为 body 的别名(可能是拼写错误,但为了兼容性支持)
  880. parsed.body = action.body || action.ture ? parseActions(action.body || action.ture) : [];
  881. break;
  882. case 'delay':
  883. parsed.value = action.value || action.delay || '0s';
  884. break;
  885. case 'set':
  886. parsed.variable = action.variable;
  887. parsed.value = resolveValue(action.value);
  888. break;
  889. case 'random':
  890. // 支持新格式:inVars[min, max] 和 outVars[{variable}]
  891. if (action.inVars && Array.isArray(action.inVars) && action.inVars.length >= 2) {
  892. parsed.min = action.inVars[0];
  893. parsed.max = action.inVars[1];
  894. } else {
  895. // 向后兼容旧格式
  896. parsed.min = action.min;
  897. parsed.max = action.max;
  898. }
  899. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  900. parsed.variable = action.outVars[0];
  901. } else {
  902. // 向后兼容旧格式
  903. parsed.variable = action.variable;
  904. }
  905. // integer 默认为 true(随机数总是整数)
  906. parsed.integer = action.integer !== undefined ? action.integer : true;
  907. break;
  908. case 'log':
  909. // log 操作:支持 inVars 或 value 字段
  910. if (action.inVars && Array.isArray(action.inVars)) {
  911. parsed.inVars = action.inVars.map(v => extractVarName(v));
  912. } else {
  913. parsed.inVars = [];
  914. }
  915. // 支持 value 字段作为直接输出内容
  916. if (action.value) {
  917. parsed.value = action.value;
  918. }
  919. break;
  920. case 'read-last-message':
  921. // 支持新的 inVars/outVars 格式,也支持 inputVars/outputVars(大小写不同,都可以为空)
  922. const inputVars = action.inVars || action.inputVars || [];
  923. const outputVars = action.outVars || action.outputVars || [];
  924. parsed.inVars = inputVars.map(v => extractVarName(v));
  925. parsed.outVars = outputVars.map(v => extractVarName(v));
  926. if (inputVars.length > 0) {
  927. parsed.inputVar = extractVarName(inputVars[0]);
  928. }
  929. if (outputVars.length > 0) {
  930. parsed.textVariable = extractVarName(outputVars[0]);
  931. }
  932. if (outputVars.length > 1) {
  933. parsed.senderVariable = extractVarName(outputVars[1]);
  934. }
  935. // 向后兼容:支持旧的 textVariable 和 senderVariable
  936. if (!parsed.textVariable) parsed.textVariable = action.textVariable;
  937. if (!parsed.senderVariable) parsed.senderVariable = action.senderVariable;
  938. break;
  939. case 'read-txt':
  940. case 'read-text': // 向后兼容别名
  941. // 支持新的 inVars/outVars 格式
  942. if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
  943. parsed.inVars = action.inVars.map(v => extractVarName(v));
  944. parsed.filePath = action.inVars[0];
  945. } else {
  946. parsed.inVars = [];
  947. parsed.filePath = action.filePath;
  948. }
  949. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  950. parsed.variable = extractVarName(action.outVars[0]);
  951. } else if (action.variable) {
  952. parsed.variable = extractVarName(action.variable);
  953. }
  954. break;
  955. case 'save-txt':
  956. case 'save-text': // 向后兼容别名
  957. // 支持新的 inVars/outVars 格式
  958. // 入参顺序:第一个参数是内容,第二个参数是文件路径
  959. if (action.inVars && Array.isArray(action.inVars)) {
  960. parsed.inVars = action.inVars.map(v => extractVarName(v));
  961. if (action.inVars.length > 0) {
  962. parsed.content = action.inVars[0]; // 第一个参数是内容
  963. }
  964. if (action.inVars.length > 1) {
  965. parsed.filePath = action.inVars[1]; // 第二个参数是文件路径
  966. }
  967. } else {
  968. parsed.inVars = [];
  969. parsed.filePath = action.filePath;
  970. parsed.content = action.content;
  971. }
  972. // save-txt 通常不需要 outVars,但为了兼容性支持
  973. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  974. parsed.variable = extractVarName(action.outVars[0]);
  975. }
  976. break;
  977. case 'image-region-location':
  978. // 支持新的 inVars/outVars 格式(都可以为空)
  979. if (action.inVars && Array.isArray(action.inVars)) {
  980. parsed.inVars = action.inVars.map(v => extractVarName(v));
  981. // 如果 inVars 有值,从变量中读取 screenshot 和 region
  982. if (action.inVars.length > 0) parsed.screenshot = action.inVars[0];
  983. if (action.inVars.length > 1) parsed.region = action.inVars[1];
  984. } else {
  985. parsed.inVars = [];
  986. parsed.screenshot = action.screenshot;
  987. parsed.region = action.region;
  988. }
  989. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  990. parsed.variable = extractVarName(action.outVars[0]);
  991. } else if (action.variable) {
  992. parsed.variable = extractVarName(action.variable);
  993. }
  994. break;
  995. case 'image-center-location':
  996. // 支持新的 inVars/outVars 格式(都可以为空)
  997. if (action.inVars && Array.isArray(action.inVars)) {
  998. parsed.inVars = action.inVars.map(v => extractVarName(v));
  999. // 如果 inVars 有值,从变量中读取 template
  1000. if (action.inVars.length > 0) parsed.template = action.inVars[0];
  1001. } else {
  1002. parsed.inVars = [];
  1003. parsed.template = action.template;
  1004. }
  1005. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  1006. parsed.variable = extractVarName(action.outVars[0]);
  1007. } else if (action.variable) {
  1008. parsed.variable = extractVarName(action.variable);
  1009. }
  1010. break;
  1011. case 'image-area-cropping':
  1012. // 支持新的 inVars/outVars 格式
  1013. if (action.inVars && Array.isArray(action.inVars)) {
  1014. parsed.inVars = action.inVars.map(v => extractVarName(v));
  1015. // 如果 inVars 有值,从变量中读取 area 和 savePath
  1016. if (action.inVars.length > 0) parsed.area = action.inVars[0];
  1017. if (action.inVars.length > 1) parsed.savePath = action.inVars[1];
  1018. } else {
  1019. parsed.inVars = [];
  1020. parsed.area = action.area;
  1021. parsed.savePath = action.savePath;
  1022. }
  1023. // image-area-cropping 通常不需要 outVars,但为了兼容性支持
  1024. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  1025. parsed.variable = extractVarName(action.outVars[0]);
  1026. }
  1027. break;
  1028. case 'scroll':
  1029. case 'swipe':
  1030. case 'press':
  1031. case 'string-press':
  1032. case 'keyevent':
  1033. // 保持原有逻辑(向后兼容)
  1034. break;
  1035. }
  1036. return parsed;
  1037. }
  1038. /**
  1039. * 解析旧格式操作(向后兼容)
  1040. * @param {Object} action - 操作对象
  1041. * @returns {Object} 解析后的操作
  1042. */
  1043. function parseOldFormatAction(action) {
  1044. const times = action.times && action.times > 0 ? parseInt(action.times, 10) : 1;
  1045. const data = action.data || '';
  1046. const delay = action.delay || '';
  1047. // 检查 press 操作
  1048. if (action.press) {
  1049. return {
  1050. type: 'press',
  1051. value: action.press,
  1052. times: times,
  1053. data: data,
  1054. delay: delay,
  1055. };
  1056. }
  1057. // 检查 input 操作
  1058. else if (action.input !== undefined) {
  1059. return {
  1060. type: 'input',
  1061. value: action.input,
  1062. times: times,
  1063. data: data,
  1064. delay: delay,
  1065. };
  1066. }
  1067. // 检查 swipe 操作
  1068. else if (action.swipe) {
  1069. const swipeValue = action.swipe;
  1070. const validSwipeDirections = ['up-down', 'down-up', 'left-right', 'right-left'];
  1071. if (!validSwipeDirections.includes(swipeValue)) {
  1072. return null;
  1073. }
  1074. return {
  1075. type: 'swipe',
  1076. value: swipeValue,
  1077. times: times,
  1078. data: data,
  1079. delay: delay,
  1080. };
  1081. }
  1082. // 检查 string-press 操作
  1083. else if (action['string-press']) {
  1084. return {
  1085. type: 'string-press',
  1086. value: action['string-press'],
  1087. times: times,
  1088. data: data,
  1089. delay: delay,
  1090. };
  1091. }
  1092. // 检查 scroll 操作
  1093. else if (action.scroll) {
  1094. const scrollValue = action.scroll;
  1095. const validScrollDirections = ['up-down', 'down-up', 'left-right', 'right-left'];
  1096. if (!validScrollDirections.includes(scrollValue)) {
  1097. return null;
  1098. }
  1099. return {
  1100. type: 'scroll',
  1101. value: scrollValue,
  1102. times: times,
  1103. data: data,
  1104. delay: delay,
  1105. };
  1106. }
  1107. else {
  1108. return null;
  1109. }
  1110. }
  1111. /**
  1112. * 获取操作名称(用于显示)
  1113. * @param {Object} action - 操作对象
  1114. * @returns {string} 操作名称
  1115. */
  1116. function getActionName(action) {
  1117. const typeNames = {
  1118. 'schedule': '定时执行',
  1119. 'adb': 'ADB操作',
  1120. 'press': '点击图片',
  1121. 'input': '输入文本',
  1122. 'swipe': '滑动',
  1123. 'string-press': '点击文字',
  1124. 'scroll': '滚动',
  1125. 'locate': '定位',
  1126. 'click': '点击',
  1127. 'ocr': '文字识别',
  1128. 'extract-messages': '提取消息记录',
  1129. 'save-messages': '保存消息记录',
  1130. 'generate-summary': '生成总结',
  1131. 'ocr-chat': 'OCR识别对话',
  1132. // 向后兼容
  1133. 'ocr-chat-history': 'OCR提取消息记录',
  1134. 'extract-chat-history': '提取消息记录', // 向后兼容
  1135. 'generate-history-summary': '生成总结',
  1136. 'image-region-location': '图像区域定位',
  1137. 'image-center-location': '图像中心点定位',
  1138. 'image-area-cropping': '裁剪图片区域',
  1139. 'read-last-message': '读取最后一条消息',
  1140. 'read-txt': '读取文本文件',
  1141. 'read-text': '读取文本文件', // 向后兼容别名
  1142. 'save-txt': '保存文本文件',
  1143. 'save-text': '保存文本文件', // 向后兼容别名
  1144. 'smart-chat-append': '智能合并聊天记录',
  1145. 'ai-generate': 'AI生成',
  1146. 'if': '条件判断',
  1147. 'for': '循环',
  1148. 'while': '循环',
  1149. 'delay': '延迟',
  1150. 'set': '设置变量',
  1151. 'random': '生成随机数',
  1152. 'echo': '打印信息',
  1153. 'log': '打印信息' // 向后兼容
  1154. };
  1155. const typeName = typeNames[action.type] || action.type;
  1156. const value = action.value || action.target || '';
  1157. const displayValue = typeof value === 'string' ? value : JSON.stringify(value);
  1158. if (action.type === 'schedule') {
  1159. const condition = action.condition || {};
  1160. const interval = condition.interval || '0s';
  1161. const repeat = condition.repeat !== undefined ? condition.repeat : 1;
  1162. const repeatText = repeat === -1 ? '无限循环' : `重复${repeat}次`;
  1163. return `${typeName}: ${interval}, ${repeatText}`;
  1164. } else if (action.type === 'input') {
  1165. return `${typeName}: ${displayValue.length > 20 ? displayValue.substring(0, 20) + '...' : displayValue}`;
  1166. } else if (action.type === 'string-press' || action.type === 'click') {
  1167. return `${typeName}: ${displayValue.length > 20 ? displayValue.substring(0, 20) + '...' : displayValue}`;
  1168. } else if (action.type === 'if') {
  1169. return `${typeName}: ${action.condition || ''}`;
  1170. } else if (action.type === 'for') {
  1171. return `${typeName}: ${action.variable || ''}`;
  1172. } else if (action.type === 'set') {
  1173. return `${typeName}: ${action.variable || ''}`;
  1174. } else {
  1175. return `${typeName}: ${displayValue}`;
  1176. }
  1177. }
  1178. /**
  1179. * 计算滑动操作的坐标
  1180. * @param {string} direction - 滑动方向: up-down, down-up, left-right, right-left
  1181. * @param {number} width - 设备宽度
  1182. * @param {number} height - 设备高度
  1183. * @returns {Object} 包含起始和结束坐标的对象 {x1, y1, x2, y2}
  1184. */
  1185. function calculateSwipeCoordinates(direction, width, height) {
  1186. // 滑动距离为屏幕的 70%,起始和结束位置各留 15% 的边距
  1187. const margin = 0.15;
  1188. const swipeDistance = 0.7;
  1189. let x1, y1, x2, y2;
  1190. switch (direction) {
  1191. case 'up-down':
  1192. // 从上往下滑动
  1193. x1 = x2 = Math.round(width / 2);
  1194. y1 = Math.round(height * margin);
  1195. y2 = Math.round(height * (margin + swipeDistance));
  1196. break;
  1197. case 'down-up':
  1198. // 从下往上滑动
  1199. x1 = x2 = Math.round(width / 2);
  1200. y1 = Math.round(height * (margin + swipeDistance));
  1201. y2 = Math.round(height * margin);
  1202. break;
  1203. case 'left-right':
  1204. // 从左往右滑动
  1205. y1 = y2 = Math.round(height / 2);
  1206. x1 = Math.round(width * margin);
  1207. x2 = Math.round(width * (margin + swipeDistance));
  1208. break;
  1209. case 'right-left':
  1210. // 从右往左滑动
  1211. y1 = y2 = Math.round(height / 2);
  1212. x1 = Math.round(width * (margin + swipeDistance));
  1213. x2 = Math.round(width * margin);
  1214. break;
  1215. default:
  1216. throw new Error(`未知的滑动方向: ${direction}`);
  1217. }
  1218. return { x1, y1, x2, y2 };
  1219. }
  1220. /**
  1221. * 执行单个操作
  1222. * @param {Object} action - 操作对象
  1223. * @param {string} device - 设备 ID
  1224. * @param {string} folderPath - 文件夹路径(用于 press 操作查找图片)
  1225. * @param {Object} resolution - 设备分辨率 {width, height}
  1226. * @returns {Promise<Object>} 执行结果 {success, error?, result?}
  1227. */
  1228. async function executeAction(action, device, folderPath, resolution) {
  1229. try {
  1230. // 检查条件
  1231. if (action.condition && !evaluateCondition(action.condition)) {
  1232. return { success: true, skipped: true };
  1233. }
  1234. switch (action.type) {
  1235. case 'adb': {
  1236. // 统一 ADB 操作
  1237. const method = action.method;
  1238. if (!method) {
  1239. return { success: false, error: 'adb 操作缺少 method 参数' };
  1240. }
  1241. // 从 inVars 读取参数
  1242. const inVars = action.inVars || [];
  1243. const outVars = action.outVars || [];
  1244. switch (method) {
  1245. case 'input': {
  1246. // 输入文本:inVars[0] 是输入文本
  1247. let inputValue = null;
  1248. if (inVars.length > 0) {
  1249. const inputVar = extractVarName(inVars[0]);
  1250. inputValue = variableContext[inputVar];
  1251. } else if (action.value) {
  1252. inputValue = resolveValue(action.value);
  1253. }
  1254. if (!inputValue) {
  1255. return { success: false, error: 'input 操作缺少输入内容' };
  1256. }
  1257. // 如果设置了clear,先清空输入框
  1258. if (action.clear) {
  1259. for (let i = 0; i < 200; i++) {
  1260. const clearResult = await electronAPI.sendKeyEvent(device, '67');
  1261. if (!clearResult.success) break;
  1262. await new Promise(resolve => setTimeout(resolve, 10));
  1263. }
  1264. await new Promise(resolve => setTimeout(resolve, 200));
  1265. }
  1266. if (!electronAPI || !electronAPI.sendText) {
  1267. return { success: false, error: '输入 API 不可用' };
  1268. }
  1269. const textResult = await electronAPI.sendText(device, String(inputValue));
  1270. if (!textResult.success) {
  1271. return { success: false, error: `输入失败: ${textResult.error}` };
  1272. }
  1273. return { success: true };
  1274. }
  1275. case 'click': {
  1276. // 点击操作:inVars[0] 是位置(坐标对象或变量名)
  1277. let position = null;
  1278. if (inVars.length > 0) {
  1279. const posVar = extractVarName(inVars[0]);
  1280. position = variableContext[posVar];
  1281. } else if (action.target) {
  1282. position = resolveValue(action.target);
  1283. }
  1284. if (!position) {
  1285. return { success: false, error: 'click 操作缺少位置参数' };
  1286. }
  1287. // 如果 position 是字符串,尝试解析为坐标对象
  1288. if (typeof position === 'string') {
  1289. if (position === '') {
  1290. return { success: false, error: 'click 操作缺少位置参数(位置变量为空)' };
  1291. }
  1292. try {
  1293. // 尝试解析 JSON 字符串(例如:{"x":123,"y":456})
  1294. position = JSON.parse(position);
  1295. } catch (e) {
  1296. // 如果不是 JSON,可能是 "x,y" 格式,尝试解析
  1297. const parts = position.split(',');
  1298. if (parts.length === 2) {
  1299. const x = parseFloat(parts[0].trim());
  1300. const y = parseFloat(parts[1].trim());
  1301. if (!isNaN(x) && !isNaN(y)) {
  1302. position = { x: Math.round(x), y: Math.round(y) };
  1303. } else {
  1304. return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` };
  1305. }
  1306. } else {
  1307. return { success: false, error: `click 操作的位置格式错误,无法解析字符串: ${position}` };
  1308. }
  1309. }
  1310. }
  1311. // 如果 position 是数组 [x, y],转换为对象
  1312. if (Array.isArray(position) && position.length >= 2) {
  1313. position = { x: position[0], y: position[1] };
  1314. }
  1315. // 如果 position 是 corners 对象(四个顶点),计算中心点
  1316. if (position && typeof position === 'object' && position.topLeft && position.bottomRight) {
  1317. const centerX = Math.round((position.topLeft.x + position.bottomRight.x) / 2);
  1318. const centerY = Math.round((position.topLeft.y + position.bottomRight.y) / 2);
  1319. position = { x: centerX, y: centerY };
  1320. }
  1321. if (!position || typeof position !== 'object' || position.x === undefined || position.y === undefined) {
  1322. return { success: false, error: 'click 操作的位置格式错误,需要 {x, y} 对象' };
  1323. }
  1324. if (!electronAPI || !electronAPI.sendTap) {
  1325. return { success: false, error: '点击 API 不可用' };
  1326. }
  1327. const tapResult = await electronAPI.sendTap(device, position.x, position.y);
  1328. if (!tapResult.success) {
  1329. return { success: false, error: `点击失败: ${tapResult.error}` };
  1330. }
  1331. return { success: true };
  1332. }
  1333. case 'locate': {
  1334. // 定位操作:inVars[0] 是目标(图片路径或文字),outVars[0] 保存位置
  1335. // 支持通过 action.method 或 action.targetMethod 指定定位方法
  1336. const locateMethod = action.method || action.targetMethod || 'image'; // image, text, coordinate
  1337. let position = null;
  1338. if (locateMethod === 'image') {
  1339. let imagePath = null;
  1340. if (inVars.length > 0) {
  1341. const imageVar = extractVarName(inVars[0]);
  1342. imagePath = variableContext[imageVar] || imageVar;
  1343. } else if (action.target) {
  1344. imagePath = action.target;
  1345. }
  1346. if (!imagePath) {
  1347. return { success: false, error: 'locate 操作(image)缺少图片路径' };
  1348. }
  1349. // resources 作为根目录
  1350. const fullPath = imagePath.startsWith('/') || imagePath.includes(':')
  1351. ? imagePath
  1352. : `${folderPath}/resources/${imagePath}`;
  1353. if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
  1354. return { success: false, error: '图像匹配 API 不可用' };
  1355. }
  1356. const matchResult = await electronAPI.matchImageAndGetCoordinate(device, fullPath);
  1357. if (!matchResult.success) {
  1358. return { success: false, error: `图像匹配失败: ${matchResult.error}` };
  1359. }
  1360. position = matchResult.clickPosition;
  1361. } else if (locateMethod === 'text') {
  1362. let targetText = null;
  1363. if (inVars.length > 0) {
  1364. const textVar = extractVarName(inVars[0]);
  1365. targetText = variableContext[textVar] || textVar;
  1366. } else if (action.target) {
  1367. targetText = action.target;
  1368. }
  1369. if (!targetText) {
  1370. return { success: false, error: 'locate 操作(text)缺少文字内容' };
  1371. }
  1372. if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
  1373. return { success: false, error: '文字识别 API 不可用' };
  1374. }
  1375. const matchResult = await electronAPI.findTextAndGetCoordinate(device, targetText);
  1376. if (!matchResult.success) {
  1377. return { success: false, error: `文字识别失败: ${matchResult.error}` };
  1378. }
  1379. position = matchResult.clickPosition;
  1380. } else if (locateMethod === 'coordinate') {
  1381. let coord = null;
  1382. if (inVars.length > 0) {
  1383. const coordVar = extractVarName(inVars[0]);
  1384. coord = variableContext[coordVar];
  1385. } else if (action.target) {
  1386. coord = resolveValue(action.target);
  1387. }
  1388. if (!coord) {
  1389. return { success: false, error: 'locate 操作(coordinate)缺少坐标' };
  1390. }
  1391. position = Array.isArray(coord) ? { x: coord[0], y: coord[1] } : coord;
  1392. }
  1393. // 保存到变量
  1394. if (outVars.length > 0) {
  1395. const outputVar = extractVarName(outVars[0]);
  1396. variableContext[outputVar] = position;
  1397. await logOutVars(action, variableContext, folderPath);
  1398. } else if (action.variable) {
  1399. variableContext[action.variable] = position;
  1400. }
  1401. return { success: true, result: position };
  1402. }
  1403. case 'swipe': {
  1404. // 滑动操作:inVars[0] 是方向,inVars[1] 和 inVars[2] 可选(起始和结束位置)
  1405. let direction = null;
  1406. if (inVars.length > 0) {
  1407. const dirVar = extractVarName(inVars[0]);
  1408. direction = variableContext[dirVar] || dirVar;
  1409. } else if (action.value) {
  1410. direction = resolveValue(action.value);
  1411. }
  1412. if (!direction) {
  1413. return { success: false, error: 'swipe 操作缺少方向参数' };
  1414. }
  1415. let x1, y1, x2, y2;
  1416. if (inVars.length >= 3) {
  1417. // 从 inVars 读取起始和结束位置
  1418. const startVar = extractVarName(inVars[1]);
  1419. const endVar = extractVarName(inVars[2]);
  1420. const start = variableContext[startVar];
  1421. const end = variableContext[endVar];
  1422. if (start && end) {
  1423. x1 = start.x || start[0];
  1424. y1 = start.y || start[1];
  1425. x2 = end.x || end[0];
  1426. y2 = end.y || end[1];
  1427. }
  1428. }
  1429. // 如果没有提供具体坐标,使用方向计算坐标
  1430. if (x1 === undefined || y1 === undefined || x2 === undefined || y2 === undefined) {
  1431. const coords = calculateSwipeCoordinates(direction, resolution.width, resolution.height);
  1432. x1 = coords.x1;
  1433. y1 = coords.y1;
  1434. x2 = coords.x2;
  1435. y2 = coords.y2;
  1436. }
  1437. if (!electronAPI || !electronAPI.sendSwipe) {
  1438. return { success: false, error: '滑动 API 不可用' };
  1439. }
  1440. const swipeResult = await electronAPI.sendSwipe(device, x1, y1, x2, y2, 300);
  1441. if (!swipeResult.success) {
  1442. return { success: false, error: `滑动失败: ${swipeResult.error}` };
  1443. }
  1444. return { success: true };
  1445. }
  1446. case 'scroll': {
  1447. // 滚动操作:inVars[0] 是方向
  1448. let direction = null;
  1449. if (inVars.length > 0) {
  1450. const dirVar = extractVarName(inVars[0]);
  1451. direction = variableContext[dirVar] || dirVar;
  1452. } else if (action.value) {
  1453. direction = resolveValue(action.value);
  1454. }
  1455. if (!direction) {
  1456. return { success: false, error: 'scroll 操作缺少方向参数' };
  1457. }
  1458. if (!electronAPI || !electronAPI.sendScroll) {
  1459. return { success: false, error: '滚动 API 不可用' };
  1460. }
  1461. const scrollResult = await electronAPI.sendScroll(
  1462. device,
  1463. direction,
  1464. resolution.width,
  1465. resolution.height,
  1466. DEFAULT_SCROLL_DISTANCE,
  1467. 500
  1468. );
  1469. if (!scrollResult.success) {
  1470. return { success: false, error: `滚动失败: ${scrollResult.error}` };
  1471. }
  1472. return { success: true };
  1473. }
  1474. case 'keyevent': {
  1475. // 按键操作:inVars[0] 是按键代码(如 "4" 表示返回键,"KEYCODE_BACK" 也可以)
  1476. let keyCode = null;
  1477. if (inVars.length > 0) {
  1478. const keyVar = extractVarName(inVars[0]);
  1479. keyCode = variableContext[keyVar] || keyVar;
  1480. } else if (action.value) {
  1481. keyCode = resolveValue(action.value);
  1482. }
  1483. if (!keyCode) {
  1484. return { success: false, error: 'keyevent 操作缺少按键代码参数' };
  1485. }
  1486. // 如果是字符串 "KEYCODE_BACK",转换为 "4"
  1487. if (keyCode === 'KEYCODE_BACK') {
  1488. keyCode = '4';
  1489. }
  1490. if (!electronAPI || !electronAPI.sendSystemKey) {
  1491. return { success: false, error: '系统按键 API 不可用' };
  1492. }
  1493. const keyResult = await electronAPI.sendSystemKey(device, String(keyCode));
  1494. if (!keyResult.success) {
  1495. return { success: false, error: `按键失败: ${keyResult.error}` };
  1496. }
  1497. return { success: true };
  1498. }
  1499. case 'press': {
  1500. // 图像匹配并点击:inVars[0] 是图片路径
  1501. let imagePath = null;
  1502. if (inVars.length > 0) {
  1503. const imageVar = extractVarName(inVars[0]);
  1504. imagePath = variableContext[imageVar] || imageVar;
  1505. } else if (action.value) {
  1506. imagePath = action.value;
  1507. }
  1508. if (!imagePath) {
  1509. return { success: false, error: 'press 操作缺少图片路径' };
  1510. }
  1511. const fullPath = imagePath.startsWith('/') || imagePath.includes(':')
  1512. ? imagePath
  1513. : `${folderPath}/${imagePath}`;
  1514. if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
  1515. return { success: false, error: '图像匹配 API 不可用' };
  1516. }
  1517. const matchResult = await electronAPI.matchImageAndGetCoordinate(device, fullPath);
  1518. if (!matchResult.success) {
  1519. return { success: false, error: `图像匹配失败: ${matchResult.error}` };
  1520. }
  1521. const { clickPosition } = matchResult;
  1522. const { x, y } = clickPosition;
  1523. if (!electronAPI || !electronAPI.sendTap) {
  1524. return { success: false, error: '点击 API 不可用' };
  1525. }
  1526. const tapResult = await electronAPI.sendTap(device, x, y);
  1527. if (!tapResult.success) {
  1528. return { success: false, error: `点击失败: ${tapResult.error}` };
  1529. }
  1530. return { success: true };
  1531. }
  1532. case 'string-press': {
  1533. // 文字识别并点击:inVars[0] 是文字
  1534. let targetText = null;
  1535. if (inVars.length > 0) {
  1536. const textVar = extractVarName(inVars[0]);
  1537. targetText = variableContext[textVar] || textVar;
  1538. } else if (action.value) {
  1539. targetText = action.value;
  1540. }
  1541. if (!targetText) {
  1542. return { success: false, error: 'string-press 操作缺少文字内容' };
  1543. }
  1544. if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
  1545. return { success: false, error: '文字识别 API 不可用' };
  1546. }
  1547. const matchResult = await electronAPI.findTextAndGetCoordinate(device, targetText);
  1548. if (!matchResult.success) {
  1549. return { success: false, error: `文字识别失败: ${matchResult.error}` };
  1550. }
  1551. const { clickPosition } = matchResult;
  1552. const { x, y } = clickPosition;
  1553. if (!electronAPI || !electronAPI.sendTap) {
  1554. return { success: false, error: '点击 API 不可用' };
  1555. }
  1556. const tapResult = await electronAPI.sendTap(device, x, y);
  1557. if (!tapResult.success) {
  1558. return { success: false, error: `点击失败: ${tapResult.error}` };
  1559. }
  1560. return { success: true };
  1561. }
  1562. default:
  1563. return { success: false, error: `未知的 adb method: ${method}` };
  1564. }
  1565. }
  1566. case 'locate': {
  1567. // 定位操作
  1568. const method = action.method || 'image';
  1569. let position = null;
  1570. if (method === 'image') {
  1571. const imagePath = action.target.startsWith('/') || action.target.includes(':')
  1572. ? action.target
  1573. : `${folderPath}/${action.target}`;
  1574. if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
  1575. return { success: false, error: '图像匹配 API 不可用' };
  1576. }
  1577. const matchResult = await electronAPI.matchImageAndGetCoordinate(device, imagePath);
  1578. if (!matchResult.success) {
  1579. return { success: false, error: `图像匹配失败: ${matchResult.error}` };
  1580. }
  1581. position = matchResult.clickPosition;
  1582. } else if (method === 'text') {
  1583. if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
  1584. return { success: false, error: '文字识别 API 不可用' };
  1585. }
  1586. const matchResult = await electronAPI.findTextAndGetCoordinate(device, action.target);
  1587. if (!matchResult.success) {
  1588. return { success: false, error: `文字识别失败: ${matchResult.error}` };
  1589. }
  1590. position = matchResult.clickPosition;
  1591. } else if (method === 'coordinate') {
  1592. position = Array.isArray(action.target)
  1593. ? { x: action.target[0], y: action.target[1] }
  1594. : action.target;
  1595. }
  1596. // 保存到变量
  1597. if (action.variable && position) {
  1598. variableContext[action.variable] = position;
  1599. }
  1600. return { success: true, result: position };
  1601. }
  1602. case 'click': {
  1603. // 点击操作
  1604. const method = action.method || 'position';
  1605. let position = null;
  1606. if (method === 'position') {
  1607. position = resolveValue(action.target);
  1608. } else if (method === 'image') {
  1609. const imagePath = action.target.startsWith('/') || action.target.includes(':')
  1610. ? action.target
  1611. : `${folderPath}/${action.target}`;
  1612. if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
  1613. return { success: false, error: '图像匹配 API 不可用' };
  1614. }
  1615. const matchResult = await electronAPI.matchImageAndGetCoordinate(device, imagePath);
  1616. if (!matchResult.success) {
  1617. return { success: false, error: `图像匹配失败: ${matchResult.error}` };
  1618. }
  1619. position = matchResult.clickPosition;
  1620. } else if (method === 'text') {
  1621. if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
  1622. return { success: false, error: '文字识别 API 不可用' };
  1623. }
  1624. const matchResult = await electronAPI.findTextAndGetCoordinate(device, action.target);
  1625. if (!matchResult.success) {
  1626. return { success: false, error: `文字识别失败: ${matchResult.error}` };
  1627. }
  1628. position = matchResult.clickPosition;
  1629. }
  1630. if (!position || !position.x || !position.y) {
  1631. return { success: false, error: '无法获取点击位置' };
  1632. }
  1633. if (!electronAPI || !electronAPI.sendTap) {
  1634. return { success: false, error: '点击 API 不可用' };
  1635. }
  1636. const tapResult = await electronAPI.sendTap(device, position.x, position.y);
  1637. if (!tapResult.success) {
  1638. return { success: false, error: `点击失败: ${tapResult.error}` };
  1639. }
  1640. return { success: true };
  1641. }
  1642. case 'press': {
  1643. // 向后兼容:图像匹配并点击
  1644. // resources 作为根目录
  1645. const imagePath = `${folderPath}/resources/${action.value}`;
  1646. if (!electronAPI || !electronAPI.matchImageAndGetCoordinate) {
  1647. return { success: false, error: '图像匹配 API 不可用' };
  1648. }
  1649. const matchResult = await electronAPI.matchImageAndGetCoordinate(device, imagePath);
  1650. if (!matchResult.success) {
  1651. return { success: false, error: `图像匹配失败: ${matchResult.error}` };
  1652. }
  1653. const { clickPosition } = matchResult;
  1654. const { x, y } = clickPosition;
  1655. if (!electronAPI || !electronAPI.sendTap) {
  1656. return { success: false, error: '点击 API 不可用' };
  1657. }
  1658. const tapResult = await electronAPI.sendTap(device, x, y);
  1659. if (!tapResult.success) {
  1660. return { success: false, error: `点击失败: ${tapResult.error}` };
  1661. }
  1662. return { success: true };
  1663. }
  1664. case 'input': {
  1665. // 输入文本
  1666. const inputStartTime = Date.now();
  1667. // 先解析value(可能在运行时变量才被赋值,需要重新解析)
  1668. let inputValue = resolveValue(action.value);
  1669. // 如果value为空或undefined,且target存在,使用target(用于向后兼容,target可能也是定位文字)
  1670. if (!inputValue && action.target) {
  1671. // 如果target看起来像定位文字(不是变量引用),就不使用它
  1672. const resolvedTarget = resolveValue(action.target);
  1673. // 只有当target是变量引用时才使用它作为输入值
  1674. if (resolvedTarget !== action.target || !action.target.includes(' ')) {
  1675. inputValue = resolvedTarget;
  1676. }
  1677. }
  1678. // 如果还是没有值,报错
  1679. if (!inputValue) {
  1680. return { success: false, error: '输入内容为空' };
  1681. }
  1682. // 如果target是定位方式,先定位输入框(暂未实现,直接输入文本)
  1683. if (action.method === 'locate' && action.target) {
  1684. // 这里可以添加定位输入框的逻辑
  1685. // 暂时直接使用 sendText
  1686. }
  1687. if (!electronAPI || !electronAPI.sendText) {
  1688. return { success: false, error: '输入 API 不可用' };
  1689. }
  1690. // 如果设置了clear,先清空输入框(通过发送退格键)
  1691. if (action.clear) {
  1692. // 发送退格键清空输入框(假设最多200个字符)
  1693. // 使用Android的KEYCODE_DEL,值为67
  1694. for (let i = 0; i < 200; i++) {
  1695. const clearResult = await electronAPI.sendKeyEvent(device, '67');
  1696. if (!clearResult.success) {
  1697. break;
  1698. }
  1699. await new Promise(resolve => setTimeout(resolve, 10));
  1700. }
  1701. // 等待清空完成
  1702. await new Promise(resolve => setTimeout(resolve, 200));
  1703. }
  1704. const textResult = await electronAPI.sendText(device, inputValue);
  1705. if (!textResult.success) {
  1706. return { success: false, error: `输入失败: ${textResult.error}` };
  1707. }
  1708. // 确保正确显示UTF-8编码的中文
  1709. try {
  1710. const displayValue = Buffer.isBuffer(inputValue)
  1711. ? inputValue.toString('utf8')
  1712. : String(inputValue);
  1713. // 输入成功,不打印日志
  1714. } catch (e) {
  1715. // 输入成功,不打印日志
  1716. }
  1717. return { success: true };
  1718. }
  1719. case 'ocr': {
  1720. // OCR识别
  1721. if (!electronAPI || !electronAPI.ocrLastMessage) {
  1722. return { success: false, error: 'OCR API 不可用' };
  1723. }
  1724. const method = action.method || 'full-screen';
  1725. let avatarPath = null;
  1726. const area = action.area;
  1727. // 如果是by-avatar方法,需要获取头像路径
  1728. if (method === 'by-avatar' && action.avatar) {
  1729. const avatarName = resolveValue(action.avatar);
  1730. if (avatarName) {
  1731. // 头像路径:从工作流文件夹路径中提取文件夹名,然后拼接头像文件名
  1732. // folderPath格式可能是:C:\...\static\processing\工作流名称
  1733. // 我们需要传递:工作流名称/头像文件名
  1734. const folderName = folderPath.split(/[/\\]/).pop();
  1735. avatarPath = `${folderName}/${avatarName}`;
  1736. }
  1737. }
  1738. // 调用OCR API,传递工作流文件夹路径
  1739. const ocrResult = await electronAPI.ocrLastMessage(device, method, avatarPath, area, folderPath);
  1740. if (!ocrResult.success) {
  1741. return { success: false, error: `OCR识别失败: ${ocrResult.error}` };
  1742. }
  1743. // 保存识别结果到变量
  1744. if (action.variable) {
  1745. variableContext[action.variable] = ocrResult.text || '';
  1746. // 确保正确显示UTF-8编码的中文
  1747. const displayText = ocrResult.text || '';
  1748. try {
  1749. // 如果text是Buffer,转换为字符串
  1750. const textStr = Buffer.isBuffer(displayText)
  1751. ? displayText.toString('utf8')
  1752. : String(displayText);
  1753. // OCR识别结果已保存到变量
  1754. } catch (e) {
  1755. // 如果转换失败,直接输出
  1756. // OCR识别结果已保存到变量
  1757. }
  1758. }
  1759. return {
  1760. success: true,
  1761. text: ocrResult.text,
  1762. position: ocrResult.position
  1763. };
  1764. }
  1765. case 'extract-messages':
  1766. case 'ocr-chat':
  1767. case 'ocr-chat-history': // 向后兼容
  1768. case 'extract-chat-history': { // 向后兼容
  1769. // 提取消息记录
  1770. // 获取头像路径(支持新的 inVars 格式,也支持旧参数 avatar1/avatar2 和 friendAvatar/myAvatar)
  1771. const folderName = folderPath.split(/[/\\]/).pop();
  1772. let avatar1Path = null;
  1773. let avatar2Path = null;
  1774. // 优先使用新的 inVars 格式
  1775. let avatar1Name, avatar2Name, regionArea = null;
  1776. let friendRgb = null, myRgb = null;
  1777. if (action.inVars && Array.isArray(action.inVars)) {
  1778. if (action.inVars.length >= 3) {
  1779. // 三个参数:可能是 RGB格式 或 头像+区域格式
  1780. const param1 = resolveValue(action.inVars[0]);
  1781. const param2 = resolveValue(action.inVars[1]);
  1782. // 检查是否是RGB格式(格式:"(r,g,b)")
  1783. const rgbPattern = /^\((\d+),(\d+),(\d+)\)$/;
  1784. if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) &&
  1785. typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
  1786. // RGB格式:第一个是好友RGB,第二个是我的RGB,第三个是区域
  1787. friendRgb = param1.trim();
  1788. myRgb = param2.trim();
  1789. // 第三个参数是区域
  1790. const regionVar = extractVarName(action.inVars[2]);
  1791. regionArea = variableContext[regionVar];
  1792. if (regionArea === undefined) {
  1793. const regionResolved = resolveValue(action.inVars[2]);
  1794. if (regionResolved && typeof regionResolved === 'object') {
  1795. regionArea = regionResolved;
  1796. }
  1797. }
  1798. } else {
  1799. // 头像格式:头像1、头像2、区域
  1800. avatar1Name = action.inVars[0];
  1801. avatar2Name = action.inVars[1];
  1802. const regionVar = extractVarName(action.inVars[2]);
  1803. regionArea = variableContext[regionVar];
  1804. if (regionArea === undefined) {
  1805. const regionResolved = resolveValue(action.inVars[2]);
  1806. if (regionResolved && typeof regionResolved === 'object') {
  1807. regionArea = regionResolved;
  1808. }
  1809. }
  1810. }
  1811. // 验证区域格式
  1812. if (regionArea) {
  1813. if (typeof regionArea === 'string') {
  1814. try {
  1815. regionArea = JSON.parse(regionArea);
  1816. } catch (e) {
  1817. regionArea = null;
  1818. }
  1819. }
  1820. if (regionArea && typeof regionArea === 'object') {
  1821. if (!regionArea.topLeft || !regionArea.bottomRight) {
  1822. regionArea = null;
  1823. }
  1824. }
  1825. }
  1826. } else if (action.inVars.length >= 2) {
  1827. // 两个参数:可能是 RGB格式 或 头像格式
  1828. const param1 = resolveValue(action.inVars[0]);
  1829. const param2 = resolveValue(action.inVars[1]);
  1830. // 检查是否是RGB格式
  1831. const rgbPattern = /^\((\d+),(\d+),(\d+)\)$/;
  1832. if (typeof param1 === 'string' && rgbPattern.test(param1.trim()) &&
  1833. typeof param2 === 'string' && rgbPattern.test(param2.trim())) {
  1834. // RGB格式:第一个是好友RGB,第二个是我的RGB
  1835. friendRgb = param1.trim();
  1836. myRgb = param2.trim();
  1837. } else {
  1838. // 头像格式:头像1、头像2(无区域,使用全屏)
  1839. avatar1Name = action.inVars[0];
  1840. avatar2Name = action.inVars[1];
  1841. }
  1842. } else if (action.inVars.length === 1) {
  1843. // 一个参数:头像1(向后兼容)
  1844. avatar1Name = action.inVars[0];
  1845. avatar2Name = action.avatar2 || action.myAvatar;
  1846. }
  1847. } else {
  1848. // 使用旧参数
  1849. avatar1Name = action.avatar1 || action.friendAvatar;
  1850. avatar2Name = action.avatar2 || action.myAvatar;
  1851. }
  1852. if (avatar1Name) {
  1853. const avatar1Resolved = resolveValue(avatar1Name);
  1854. if (avatar1Resolved) {
  1855. // resources 作为根目录
  1856. avatar1Path = `${folderName}/resources/${avatar1Resolved}`;
  1857. }
  1858. }
  1859. if (avatar2Name) {
  1860. const avatar2Resolved = resolveValue(avatar2Name);
  1861. if (avatar2Resolved) {
  1862. // resources 作为根目录
  1863. avatar2Path = `${folderName}/resources/${avatar2Resolved}`;
  1864. }
  1865. }
  1866. // 调用 Func 目录下的执行函数
  1867. // 确保 regionArea 是对象格式(如果是字符串,尝试解析为 JSON)
  1868. let regionParam = regionArea;
  1869. if (regionArea && typeof regionArea === 'string') {
  1870. try {
  1871. regionParam = JSON.parse(regionArea);
  1872. } catch (e) {
  1873. // 解析失败,使用 null(函数内部会处理)
  1874. regionParam = null;
  1875. }
  1876. }
  1877. const chatResult = await executeOcrChat({
  1878. device,
  1879. avatar1: avatar1Path,
  1880. avatar2: avatar2Path,
  1881. folderPath,
  1882. region: regionParam, // 传递区域参数(对象格式,如果提供)
  1883. friendRgb: friendRgb, // 传递好友RGB(如果提供)
  1884. myRgb: myRgb // 传递我的RGB(如果提供)
  1885. });
  1886. if (!chatResult.success) {
  1887. return { success: false, error: `提取消息记录失败: ${chatResult.error}` };
  1888. }
  1889. // 保存消息记录到变量(支持新的 outVars 格式)
  1890. let outputVarName = null;
  1891. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  1892. outputVarName = extractVarName(action.outVars[0]);
  1893. } else if (action.variable) {
  1894. outputVarName = extractVarName(action.variable);
  1895. }
  1896. if (outputVarName) {
  1897. // 使用JSON字符串格式的消息记录
  1898. const messagesJson = chatResult.messagesJson || JSON.stringify(chatResult.messages || []);
  1899. variableContext[outputVarName] = messagesJson;
  1900. await logOutVars(action, variableContext, folderPath);
  1901. }
  1902. return {
  1903. success: true,
  1904. messages: chatResult.messages || [],
  1905. messagesJson: chatResult.messagesJson || JSON.stringify(chatResult.messages || []),
  1906. lastMessage: chatResult.messages && chatResult.messages.length > 0 ? chatResult.messages[chatResult.messages.length - 1] : null
  1907. };
  1908. }
  1909. case 'ai-generate': {
  1910. // AI生成
  1911. let prompt = resolveValue(action.prompt);
  1912. // 如果提供了 inVars,替换 prompt 中的变量
  1913. if (action.inVars && Array.isArray(action.inVars)) {
  1914. for (let i = 0; i < action.inVars.length; i++) {
  1915. const varName = extractVarName(action.inVars[i]);
  1916. const varValue = variableContext[varName];
  1917. // 替换 prompt 中的变量占位符(例如 {currentMessage})
  1918. if (varValue !== undefined && varValue !== null) {
  1919. const placeholder = `{${varName}}`;
  1920. // 检查是否是空数组 JSON 字符串
  1921. let replaceValue = String(varValue);
  1922. if (typeof varValue === 'string' && varValue.trim() === '[]') {
  1923. try {
  1924. const parsed = JSON.parse(varValue);
  1925. if (Array.isArray(parsed) && parsed.length === 0) {
  1926. replaceValue = '';
  1927. }
  1928. } catch (e) {
  1929. // 不是 JSON,继续使用原值
  1930. }
  1931. }
  1932. prompt = prompt.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), replaceValue);
  1933. }
  1934. }
  1935. }
  1936. // 替换 prompt 中的变量(包括 historySummary 和其他变量)
  1937. // 注意:这里需要先替换 historySummary,因为它可能包含在 prompt 模板中
  1938. if (prompt.includes('{historySummary}')) {
  1939. let historySummary = variableContext['historySummary'] || '';
  1940. // 如果变量中没有历史总结,尝试从文件中读取
  1941. if (!historySummary) {
  1942. historySummary = await getHistorySummary(folderPath);
  1943. // 如果从文件读取成功,也更新变量上下文
  1944. if (historySummary) {
  1945. variableContext['historySummary'] = historySummary;
  1946. }
  1947. }
  1948. prompt = prompt.replace(/{historySummary}/g, historySummary);
  1949. }
  1950. // 替换 prompt 中所有剩余的变量占位符(支持 {{variable}} 和 {variable} 格式)
  1951. prompt = replaceVariablesInString(prompt, variableContext);
  1952. try {
  1953. // 调用AI API(使用现有的GPT API)
  1954. const requestBody = {
  1955. prompt: prompt,
  1956. modelName: action.model || 'gpt-5-nano-ca'
  1957. };
  1958. const response = await fetch('https://ai-anim.com/api/text2textByModel', {
  1959. method: 'POST',
  1960. headers: { 'Content-Type': 'application/json' },
  1961. body: JSON.stringify(requestBody)
  1962. });
  1963. if (!response.ok) {
  1964. const errorText = await response.text();
  1965. return { success: false, error: `AI请求失败: ${response.statusText}` };
  1966. }
  1967. const data = await response.json();
  1968. // API 返回格式:{ success: true, data: { output_text: "..." } }
  1969. // 或者:{ data: { output_text: "..." } }
  1970. // 优先从 data.data.output_text 提取,然后是 data.output_text,最后是其他字段
  1971. let rawResult = '';
  1972. if (data.data && typeof data.data === 'object' && data.data.output_text) {
  1973. rawResult = data.data.output_text;
  1974. } else if (data.output_text) {
  1975. rawResult = data.output_text;
  1976. } else if (data.text) {
  1977. rawResult = data.text;
  1978. } else if (data.content) {
  1979. rawResult = data.content;
  1980. } else if (data.data && typeof data.data === 'string') {
  1981. rawResult = data.data;
  1982. } else {
  1983. // 如果都没有,尝试从整个响应中提取
  1984. rawResult = JSON.stringify(data);
  1985. }
  1986. // 确保 rawResult 是字符串
  1987. rawResult = rawResult ? String(rawResult) : '';
  1988. // 解析AI返回的JSON格式回复
  1989. let result = rawResult;
  1990. try {
  1991. // 尝试从返回文本中提取JSON
  1992. // 方法1: 尝试直接解析整个文本
  1993. try {
  1994. const jsonResult = JSON.parse(rawResult.trim());
  1995. if (jsonResult.reply) {
  1996. result = jsonResult.reply;
  1997. }
  1998. } catch (e) {
  1999. // 方法2: 尝试从代码块中提取JSON
  2000. const codeBlockMatch = rawResult.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
  2001. if (codeBlockMatch) {
  2002. try {
  2003. const jsonResult = JSON.parse(codeBlockMatch[1]);
  2004. if (jsonResult.reply) {
  2005. result = jsonResult.reply;
  2006. }
  2007. } catch (e2) {
  2008. // JSON解析失败,继续使用原始文本
  2009. }
  2010. } else {
  2011. // 方法3: 尝试从文本中查找JSON对象
  2012. const jsonMatch = rawResult.match(/\{\s*"reply"\s*:\s*"([^"]+)"\s*\}/);
  2013. if (jsonMatch) {
  2014. result = jsonMatch[1];
  2015. } else {
  2016. // 方法4: 尝试查找单行的JSON格式
  2017. const lines = rawResult.split('\n').map(line => line.trim()).filter(line => line);
  2018. for (const line of lines) {
  2019. if (line.startsWith('{') && line.includes('"reply"')) {
  2020. try {
  2021. const jsonResult = JSON.parse(line);
  2022. if (jsonResult.reply) {
  2023. result = jsonResult.reply;
  2024. break;
  2025. }
  2026. } catch (e3) {
  2027. // 继续尝试下一行
  2028. }
  2029. }
  2030. }
  2031. }
  2032. }
  2033. }
  2034. } catch (parseError) {
  2035. // 如果解析失败,使用原始文本
  2036. }
  2037. // 保存到变量(支持 outVars 格式)
  2038. // outVars[0] 是 AI 生成的结果,outVars[1](如果有)是 aiCallBack 变量
  2039. if (action.outVars && Array.isArray(action.outVars)) {
  2040. // 第一个输出变量保存 AI 生成结果
  2041. if (action.outVars.length > 0) {
  2042. const outputVarName = extractVarName(action.outVars[0]);
  2043. if (outputVarName) {
  2044. variableContext[outputVarName] = result;
  2045. }
  2046. }
  2047. // 第二个输出变量(如果有)设置为 1 表示 AI 生成完成(aiCallBack)
  2048. if (action.outVars.length > 1) {
  2049. const callbackVarName = extractVarName(action.outVars[1]);
  2050. if (callbackVarName) {
  2051. variableContext[callbackVarName] = 1;
  2052. }
  2053. }
  2054. await logOutVars(action, variableContext, folderPath);
  2055. } else if (action.variable) {
  2056. // 向后兼容:使用旧的 variable 字段
  2057. const outputVarName = extractVarName(action.variable);
  2058. if (outputVarName) {
  2059. variableContext[outputVarName] = result;
  2060. }
  2061. }
  2062. // 向后兼容:如果 aiCallBack 在 inVars 中提供,也设置它(但优先使用 outVars)
  2063. if (!action.outVars || !Array.isArray(action.outVars) || action.outVars.length <= 1) {
  2064. if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 1) {
  2065. const callbackVarName = extractVarName(action.inVars[1]);
  2066. if (callbackVarName) {
  2067. variableContext[callbackVarName] = 1;
  2068. }
  2069. }
  2070. }
  2071. return { success: true, result };
  2072. } catch (error) {
  2073. return { success: false, error: `AI生成失败: ${error.message}` };
  2074. }
  2075. }
  2076. case 'save-messages':
  2077. case 'generate-summary':
  2078. case 'generate-history-summary': { // 向后兼容
  2079. // 生成消息记录的AI总结
  2080. if (!action.variable) {
  2081. return { success: false, error: '缺少变量名' };
  2082. }
  2083. const messages = variableContext[action.variable];
  2084. if (!messages) {
  2085. return { success: false, error: `变量 ${action.variable} 不存在或为空` };
  2086. }
  2087. const modelName = action.model || 'gpt-5-nano-ca';
  2088. const result = await generateHistorySummary(messages, folderPath, modelName);
  2089. if (!result.success) {
  2090. return { success: false, error: `生成消息记录总结失败: ${result.error}` };
  2091. }
  2092. // 保存总结到变量
  2093. if (action.summaryVariable) {
  2094. variableContext[action.summaryVariable] = result.summary;
  2095. // 消息记录总结已保存到变量
  2096. }
  2097. return { success: true, summary: result.summary };
  2098. }
  2099. case 'set': {
  2100. // 设置变量
  2101. if (action.variable) {
  2102. const varName = extractVarName(action.variable);
  2103. // 如果 value 是字符串且包含算术运算符,尝试计算表达式
  2104. let finalValue = action.value;
  2105. if (typeof action.value === 'string' && /[+\-*/]/.test(action.value)) {
  2106. finalValue = evaluateExpression(action.value, variableContext);
  2107. } else if (typeof action.value === 'string') {
  2108. // 如果 value 是字符串,检查是否包含变量引用({variable} 格式)
  2109. // 替换字符串中的所有变量引用
  2110. const varPattern = /\{([\w-]+)\}/g;
  2111. let hasVariables = false;
  2112. finalValue = action.value.replace(varPattern, (match, varName) => {
  2113. hasVariables = true;
  2114. const value = variableContext[varName];
  2115. if (value === undefined || value === null) {
  2116. return match; // 如果变量不存在,保留原样
  2117. }
  2118. return String(value);
  2119. });
  2120. // 如果没有找到变量引用,使用 resolveValue 解析(处理单个变量引用的情况)
  2121. if (!hasVariables) {
  2122. finalValue = resolveValue(action.value, variableContext);
  2123. }
  2124. } else {
  2125. // 否则使用 resolveValue 解析变量引用
  2126. finalValue = resolveValue(action.value, variableContext);
  2127. }
  2128. variableContext[varName] = finalValue;
  2129. }
  2130. return { success: true };
  2131. }
  2132. case 'random': {
  2133. // 生成随机数
  2134. // 支持新格式:inVars[min, max] 和 outVars[{variable}]
  2135. let varName, min, max;
  2136. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  2137. varName = extractVarName(action.outVars[0]);
  2138. } else if (action.variable) {
  2139. // 向后兼容旧格式
  2140. varName = extractVarName(action.variable);
  2141. } else {
  2142. return { success: false, error: 'random 操作缺少 variable 或 outVars 参数' };
  2143. }
  2144. if (action.inVars && Array.isArray(action.inVars) && action.inVars.length >= 2) {
  2145. // 新格式:从 inVars 读取 min 和 max
  2146. min = Number(action.inVars[0]);
  2147. max = Number(action.inVars[1]);
  2148. } else {
  2149. // 向后兼容旧格式
  2150. min = action.min !== undefined ? Number(action.min) : 0;
  2151. max = action.max !== undefined ? Number(action.max) : 100;
  2152. }
  2153. const integer = action.integer !== undefined ? action.integer : true;
  2154. if (isNaN(min) || isNaN(max)) {
  2155. return { success: false, error: 'random 操作的 min 和 max 必须是数字' };
  2156. }
  2157. if (min > max) {
  2158. return { success: false, error: 'random 操作的 min 不能大于 max' };
  2159. }
  2160. let randomValue;
  2161. if (integer) {
  2162. randomValue = Math.floor(Math.random() * (max - min + 1)) + min;
  2163. } else {
  2164. randomValue = Math.random() * (max - min) + min;
  2165. }
  2166. variableContext[varName] = randomValue;
  2167. return { success: true };
  2168. }
  2169. case 'echo': {
  2170. try {
  2171. // 打印信息到 console.log、UI 和 log.txt 文件
  2172. // 支持 inVars 或 value 字段
  2173. let message = '';
  2174. if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
  2175. // 从 inVars 中读取变量值
  2176. const messages = action.inVars.map(varWithBraces => {
  2177. const varName = extractVarName(varWithBraces);
  2178. const varValue = variableContext[varName];
  2179. // 如果变量包含 {variable} 或 {{variable}} 格式,尝试解析为变量名
  2180. if (varWithBraces.startsWith('{') && varWithBraces.endsWith('}')) {
  2181. return varValue !== undefined ? String(varValue) : varWithBraces;
  2182. }
  2183. // 如果不是变量格式,直接使用原值
  2184. return varWithBraces;
  2185. });
  2186. message = messages.join(' ');
  2187. } else if (action.value) {
  2188. // 使用 value 字段,支持变量替换(只支持 {{variable}} 格式,用于字符串拼接)
  2189. // 先提取所有变量名,用于调试
  2190. const doubleBracePattern = /\{\{([\w-]+)\}\}/g;
  2191. const variablesInValue = [];
  2192. let match;
  2193. while ((match = doubleBracePattern.exec(action.value)) !== null) {
  2194. variablesInValue.push(match[1]);
  2195. }
  2196. message = replaceVariablesInString(action.value, variableContext);
  2197. // 如果消息为空或只包含原始变量格式,添加调试信息
  2198. if (!message || message === action.value) {
  2199. const missingVars = variablesInValue.filter(varName => {
  2200. const varValue = variableContext[varName];
  2201. return varValue === undefined || varValue === null || varValue === '';
  2202. });
  2203. if (missingVars.length > 0) {
  2204. message = `${action.value} [变量未定义或为空: ${missingVars.join(', ')}]`;
  2205. }
  2206. }
  2207. } else {
  2208. // 如果没有提供任何内容,输出空字符串
  2209. message = '';
  2210. }
  2211. // 输出到 console.log(仅限小红书随机浏览工作流)
  2212. if (folderPath && folderPath.includes('小红书随机浏览工作流')) {
  2213. console.log(message);
  2214. }
  2215. // 写入 log.txt 文件(只有 echo 类型写入文件)
  2216. // 添加系统时间到消息中
  2217. // 即使 message 为空也记录,以便调试
  2218. const now = new Date();
  2219. const year = now.getFullYear();
  2220. const month = String(now.getMonth() + 1).padStart(2, '0');
  2221. const day = String(now.getDate()).padStart(2, '0');
  2222. const hour = String(now.getHours()).padStart(2, '0');
  2223. const minute = String(now.getMinutes()).padStart(2, '0');
  2224. const second = String(now.getSeconds()).padStart(2, '0');
  2225. const timeStr = `${year}/${month}/${day} ${hour}:${minute}:${second}`;
  2226. const messageWithTime = message ? `${message} [系统时间: ${timeStr}]` : `[空消息] [系统时间: ${timeStr}]`;
  2227. await logMessage(messageWithTime, folderPath);
  2228. // 发送 log 消息事件到 UI
  2229. const logEvent = new CustomEvent('log-message', {
  2230. detail: {
  2231. message: message
  2232. }
  2233. });
  2234. window.dispatchEvent(logEvent);
  2235. return { success: true };
  2236. } catch (error) {
  2237. // 如果 echo 执行出错,记录错误到 log.txt
  2238. const now = new Date();
  2239. const year = now.getFullYear();
  2240. const month = String(now.getMonth() + 1).padStart(2, '0');
  2241. const day = String(now.getDate()).padStart(2, '0');
  2242. const hour = String(now.getHours()).padStart(2, '0');
  2243. const minute = String(now.getMinutes()).padStart(2, '0');
  2244. const second = String(now.getSeconds()).padStart(2, '0');
  2245. const timeStr = `${year}/${month}/${day} ${hour}:${minute}:${second}`;
  2246. const errorMsg = `[错误] echo 执行失败: ${error.message} [系统时间: ${timeStr}]`;
  2247. await logMessage(errorMsg, folderPath);
  2248. return { success: false, error: `echo 执行失败: ${error.message}` };
  2249. }
  2250. }
  2251. case 'log': { // 向后兼容,但不写入 log.txt
  2252. // 打印信息到 console.log 和 UI(不写入 log.txt)
  2253. // 支持 inVars 或 value 字段
  2254. let message = '';
  2255. if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
  2256. // 从 inVars 中读取变量值
  2257. const messages = action.inVars.map(varWithBraces => {
  2258. const varName = extractVarName(varWithBraces);
  2259. const varValue = variableContext[varName];
  2260. // 如果变量包含 {variable} 或 {{variable}} 格式,尝试解析为变量名
  2261. if (varWithBraces.startsWith('{') && varWithBraces.endsWith('}')) {
  2262. return varValue !== undefined ? String(varValue) : varWithBraces;
  2263. }
  2264. // 如果不是变量格式,直接使用原值
  2265. return varWithBraces;
  2266. });
  2267. message = messages.join(' ');
  2268. } else if (action.value) {
  2269. // 使用 value 字段,支持变量替换(只支持 {{variable}} 格式,用于字符串拼接)
  2270. message = replaceVariablesInString(action.value, variableContext);
  2271. } else {
  2272. // 如果没有提供任何内容,输出空字符串
  2273. message = '';
  2274. }
  2275. // 输出到 console.log(仅限小红书随机浏览工作流)
  2276. if (folderPath && folderPath.includes('小红书随机浏览工作流')) {
  2277. console.log(message);
  2278. }
  2279. // 注意:log 类型不写入 log.txt 文件,只发送到 UI
  2280. // 发送 log 消息事件到 UI
  2281. const logEvent = new CustomEvent('log-message', {
  2282. detail: {
  2283. message: message
  2284. }
  2285. });
  2286. window.dispatchEvent(logEvent);
  2287. return { success: true };
  2288. }
  2289. case 'image-region-location': {
  2290. // 图像区域定位
  2291. // 支持新的 inVars/outVars 格式(都可以为空)
  2292. let screenshotPath = action.screenshot;
  2293. let regionPath = action.region;
  2294. // 如果提供了 inVars,从变量中读取或直接使用
  2295. if (action.inVars && Array.isArray(action.inVars)) {
  2296. if (action.inVars.length === 1) {
  2297. // 如果只有一个参数,将其作为 region(区域图片)
  2298. const firstVar = extractVarName(action.inVars[0]);
  2299. const firstValue = variableContext[firstVar];
  2300. if (firstValue && typeof firstValue === 'string' && !firstValue.includes('{')) {
  2301. regionPath = firstValue;
  2302. } else {
  2303. // 如果变量不存在,直接使用 inVars[0] 作为路径
  2304. regionPath = action.inVars[0];
  2305. }
  2306. // screenshot 需要自动从设备获取(传递 null)
  2307. screenshotPath = null;
  2308. } else if (action.inVars.length >= 2) {
  2309. // 如果有两个参数,第一个是 screenshot,第二个是 region
  2310. const screenshotVar = extractVarName(action.inVars[0]);
  2311. const screenshotValue = variableContext[screenshotVar];
  2312. if (screenshotValue && typeof screenshotValue === 'string' && !screenshotValue.includes('{')) {
  2313. screenshotPath = screenshotValue;
  2314. } else {
  2315. screenshotPath = action.inVars[0];
  2316. }
  2317. const regionVar = extractVarName(action.inVars[1]);
  2318. const regionValue = variableContext[regionVar];
  2319. if (regionValue && typeof regionValue === 'string' && !regionValue.includes('{')) {
  2320. regionPath = regionValue;
  2321. } else {
  2322. regionPath = action.inVars[1];
  2323. }
  2324. }
  2325. }
  2326. // 如果没有提供路径,使用默认值或从 action 中读取
  2327. if (screenshotPath !== null && !screenshotPath) screenshotPath = action.screenshot;
  2328. if (!regionPath) regionPath = action.region;
  2329. if (!regionPath) {
  2330. return { success: false, error: '缺少区域截图路径' };
  2331. }
  2332. // 如果 screenshotPath 为 null,需要自动从设备获取
  2333. if (screenshotPath === null && !device) {
  2334. return { success: false, error: '缺少完整截图路径,且无法自动获取设备截图(缺少设备ID)' };
  2335. }
  2336. // 调用 Func 目录下的执行函数
  2337. const result = await executeImageRegionLocation({
  2338. device,
  2339. screenshot: screenshotPath,
  2340. region: regionPath,
  2341. folderPath
  2342. });
  2343. if (!result.success) {
  2344. return { success: false, error: `图像区域定位失败: ${result.error}` };
  2345. }
  2346. // 保存结果到变量(支持 outVars 格式)
  2347. // image-region-location 返回四个顶点坐标
  2348. let outputVarName = null;
  2349. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  2350. outputVarName = extractVarName(action.outVars[0]);
  2351. } else if (action.variable) {
  2352. outputVarName = extractVarName(action.variable);
  2353. }
  2354. if (outputVarName) {
  2355. // 将对象转换为 JSON 字符串(只允许 string 或 int 类型)
  2356. if (result.corners && typeof result.corners === 'object') {
  2357. variableContext[outputVarName] = JSON.stringify(result.corners);
  2358. } else {
  2359. variableContext[outputVarName] = '';
  2360. }
  2361. // 图像区域定位结果已保存到变量
  2362. await logOutVars(action, variableContext, folderPath);
  2363. }
  2364. return { success: true, result: result.corners };
  2365. }
  2366. case 'image-center-location': {
  2367. // 图像中心点定位
  2368. // 支持新的 inVars/outVars 格式(都可以为空)
  2369. let templatePath = action.template;
  2370. // 如果提供了 inVars,从变量中读取或直接使用
  2371. if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
  2372. const templateVar = extractVarName(action.inVars[0]);
  2373. const templateValue = variableContext[templateVar];
  2374. if (templateValue && typeof templateValue === 'string' && !templateValue.includes('{')) {
  2375. templatePath = templateValue;
  2376. } else {
  2377. // 如果变量不存在,直接使用 inVars[0] 作为路径
  2378. templatePath = action.inVars[0];
  2379. }
  2380. }
  2381. // 如果没有提供路径,使用默认值或从 action 中读取
  2382. if (!templatePath) templatePath = action.template;
  2383. if (!templatePath) {
  2384. return { success: false, error: '缺少模板图片路径' };
  2385. }
  2386. if (!device) {
  2387. return { success: false, error: '缺少设备 ID,无法自动获取截图' };
  2388. }
  2389. // 调用 Func 目录下的执行函数
  2390. const result = await executeImageCenterLocation({
  2391. device,
  2392. template: templatePath,
  2393. folderPath
  2394. });
  2395. if (!result.success) {
  2396. return { success: false, error: `图像中心点定位失败: ${result.error}` };
  2397. }
  2398. // 保存结果到变量(支持 outVars 格式)
  2399. let outputVarName = null;
  2400. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  2401. outputVarName = extractVarName(action.outVars[0]);
  2402. } else if (action.variable) {
  2403. outputVarName = extractVarName(action.variable);
  2404. }
  2405. if (outputVarName) {
  2406. // 保存中心点坐标为字符串(JSON 格式):只允许 number 或 string
  2407. if (result.center && typeof result.center === 'object' && result.center.x !== undefined && result.center.y !== undefined) {
  2408. variableContext[outputVarName] = JSON.stringify({ x: result.center.x, y: result.center.y });
  2409. } else {
  2410. variableContext[outputVarName] = '';
  2411. }
  2412. // 图像中心点定位结果已保存到变量
  2413. await logOutVars(action, variableContext, folderPath);
  2414. }
  2415. return { success: true, result: result.center };
  2416. }
  2417. case 'image-area-cropping': {
  2418. // 裁剪图片区域
  2419. // 支持新的 inVars 格式
  2420. let area = action.area;
  2421. let savePath = action.savePath;
  2422. // 如果提供了 inVars,从变量中读取或直接使用
  2423. if (action.inVars && Array.isArray(action.inVars)) {
  2424. if (action.inVars.length > 0) {
  2425. const areaVar = extractVarName(action.inVars[0]);
  2426. const areaValue = variableContext[areaVar];
  2427. if (areaValue !== undefined) {
  2428. area = areaValue;
  2429. } else {
  2430. area = resolveValue(action.inVars[0]);
  2431. }
  2432. }
  2433. if (action.inVars.length > 1) {
  2434. const savePathVar = extractVarName(action.inVars[1]);
  2435. const savePathValue = variableContext[savePathVar];
  2436. if (savePathValue !== undefined) {
  2437. savePath = savePathValue;
  2438. } else {
  2439. savePath = resolveValue(action.inVars[1]);
  2440. }
  2441. }
  2442. }
  2443. if (!area) {
  2444. return { success: false, error: 'image-area-cropping 缺少 area 参数' };
  2445. }
  2446. if (!savePath) {
  2447. return { success: false, error: 'image-area-cropping 缺少 savePath 参数' };
  2448. }
  2449. const result = await executeImageAreaCropping({
  2450. area,
  2451. savePath,
  2452. folderPath,
  2453. device // 传递设备ID,用于获取最新截图
  2454. });
  2455. if (!result.success) {
  2456. // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
  2457. return { success: false, error: result.error };
  2458. }
  2459. // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
  2460. // 如果提供了 outVars,可以将结果保存到变量
  2461. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  2462. const outputVarName = extractVarName(action.outVars[0]);
  2463. if (outputVarName) {
  2464. // 只允许 string 或 int 类型,保存成功标志为 "1" 或 1
  2465. variableContext[outputVarName] = result.success ? '1' : '0';
  2466. }
  2467. }
  2468. await logOutVars(action, variableContext, folderPath);
  2469. return { success: true };
  2470. }
  2471. case 'read-last-message': {
  2472. // 读取最后一条消息
  2473. // 支持新的 inVars/outVars 格式,也支持 inputVars/outputVars
  2474. const inputVars = action.inVars || action.inputVars || [];
  2475. const outputVars = action.outVars || action.outputVars || [];
  2476. let textVar = action.textVariable;
  2477. let senderVar = action.senderVariable;
  2478. let inputVar = null;
  2479. if (outputVars.length > 0) {
  2480. textVar = extractVarName(outputVars[0]);
  2481. }
  2482. if (outputVars.length > 1) {
  2483. senderVar = extractVarName(outputVars[1]);
  2484. }
  2485. if (inputVars.length > 0) {
  2486. inputVar = extractVarName(inputVars[0]);
  2487. }
  2488. if (!textVar && !senderVar) {
  2489. return { success: false, error: 'read-last-message 缺少 textVariable 或 senderVariable 参数' };
  2490. }
  2491. // 如果提供了 inputVar,从变量中读取数据
  2492. let inputData = null;
  2493. if (inputVar && variableContext[inputVar] !== undefined) {
  2494. inputData = variableContext[inputVar];
  2495. }
  2496. // 确保 inputData 是字符串类型(如果是对象或数组,转换为 JSON 字符串)
  2497. let inputDataString = inputData;
  2498. if (inputData !== null && inputData !== undefined) {
  2499. if (typeof inputData === 'string') {
  2500. inputDataString = inputData;
  2501. } else if (Array.isArray(inputData) || typeof inputData === 'object') {
  2502. inputDataString = JSON.stringify(inputData);
  2503. } else {
  2504. inputDataString = String(inputData);
  2505. }
  2506. } else {
  2507. inputDataString = null; // null 表示从文件读取
  2508. }
  2509. const result = await executeReadLastMessage({
  2510. folderPath,
  2511. inputData: inputDataString, // 确保是字符串类型,如果为 null,则从 history 文件夹读取
  2512. textVariable: textVar,
  2513. senderVariable: senderVar
  2514. });
  2515. if (!result.success) {
  2516. return { success: false, error: result.error };
  2517. }
  2518. // 保存消息文本和发送者到变量
  2519. if (textVar) {
  2520. variableContext[textVar] = result.text;
  2521. // 最后一条消息文本已保存到变量
  2522. }
  2523. if (senderVar) {
  2524. variableContext[senderVar] = result.sender;
  2525. // 最后一条消息发送者已保存到变量
  2526. }
  2527. await logOutVars(action, variableContext, folderPath);
  2528. return { success: true, text: result.text, sender: result.sender };
  2529. }
  2530. case 'read-txt':
  2531. case 'read-text': { // 向后兼容别名
  2532. // 读取根目录下的文本文件
  2533. // 支持新的 inVars/outVars 格式
  2534. let filePath = action.filePath;
  2535. let varName = action.variable;
  2536. if (action.inVars && Array.isArray(action.inVars) && action.inVars.length > 0) {
  2537. // 从 inVars 读取文件路径(可能是变量或直接路径)
  2538. const filePathVar = extractVarName(action.inVars[0]);
  2539. const filePathValue = variableContext[filePathVar];
  2540. if (filePathValue !== undefined) {
  2541. filePath = filePathValue;
  2542. } else {
  2543. filePath = resolveValue(action.inVars[0]);
  2544. }
  2545. }
  2546. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  2547. varName = extractVarName(action.outVars[0]);
  2548. } else if (action.variable) {
  2549. varName = extractVarName(action.variable);
  2550. }
  2551. if (!filePath) {
  2552. return { success: false, error: 'read-txt 缺少 filePath 参数' };
  2553. }
  2554. if (!varName) {
  2555. return { success: false, error: 'read-txt 缺少 variable 参数' };
  2556. }
  2557. const result = await executeReadTxt({
  2558. filePath,
  2559. folderPath
  2560. });
  2561. if (!result.success) {
  2562. return { success: false, error: result.error };
  2563. }
  2564. // 保存文件内容到变量(确保始终是字符串类型,即使是空字符串)
  2565. const content = result.content || '';
  2566. variableContext[varName] = typeof content === 'string' ? content : String(content);
  2567. // 确保变量始终是字符串类型(即使是空字符串)
  2568. if (variableContext[varName] === undefined || variableContext[varName] === null) {
  2569. variableContext[varName] = '';
  2570. }
  2571. await logOutVars(action, variableContext, folderPath);
  2572. return { success: true, content: result.content };
  2573. }
  2574. case 'smart-chat-append': {
  2575. // 智能合并历史聊天记录和当前聊天记录,自动检测并去除连续重合部分后返回新的聊天记录字符串
  2576. // 支持新的 inVars/outVars 格式
  2577. let history = action.history;
  2578. let current = action.current;
  2579. if (action.inVars && Array.isArray(action.inVars)) {
  2580. // 从 inVars 读取历史记录和当前记录(可能是变量或直接值)
  2581. if (action.inVars.length > 0) {
  2582. const historyVar = extractVarName(action.inVars[0]);
  2583. const historyValue = variableContext[historyVar];
  2584. if (historyValue !== undefined) {
  2585. history = historyValue;
  2586. } else {
  2587. history = resolveValue(action.inVars[0]);
  2588. }
  2589. }
  2590. if (action.inVars.length > 1) {
  2591. const currentVar = extractVarName(action.inVars[1]);
  2592. const currentValue = variableContext[currentVar];
  2593. if (currentValue !== undefined) {
  2594. current = currentValue;
  2595. } else {
  2596. current = resolveValue(action.inVars[1]);
  2597. }
  2598. }
  2599. }
  2600. if (history === undefined || history === null) {
  2601. history = '';
  2602. }
  2603. if (current === undefined || current === null) {
  2604. current = '';
  2605. }
  2606. const result = await executeSmartChatAppend({
  2607. history: typeof history === 'string' ? history : String(history),
  2608. current: typeof current === 'string' ? current : String(current)
  2609. });
  2610. if (!result.success) {
  2611. return { success: false, error: result.error };
  2612. }
  2613. // 保存结果到变量
  2614. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  2615. const outputVarName = extractVarName(action.outVars[0]);
  2616. if (outputVarName && result.result) {
  2617. variableContext[outputVarName] = result.result;
  2618. }
  2619. } else if (action.variable) {
  2620. const varName = extractVarName(action.variable);
  2621. if (varName && result.result) {
  2622. variableContext[varName] = result.result;
  2623. }
  2624. }
  2625. return { success: true, result: result.result };
  2626. }
  2627. case 'save-txt':
  2628. case 'save-text': { // 向后兼容别名
  2629. // 保存字符串为文本文件
  2630. // 支持新的 inVars/outVars 格式
  2631. let filePath = action.filePath;
  2632. let content = action.content;
  2633. if (action.inVars && Array.isArray(action.inVars)) {
  2634. // 从 inVars 读取内容和文件路径(可能是变量或直接值)
  2635. // 入参顺序:第一个参数是内容,第二个参数是文件路径
  2636. if (action.inVars.length > 0) {
  2637. // 第一个参数是内容
  2638. const contentVar = extractVarName(action.inVars[0]);
  2639. const contentValue = variableContext[contentVar];
  2640. if (contentValue !== undefined) {
  2641. content = contentValue;
  2642. } else {
  2643. content = resolveValue(action.inVars[0]);
  2644. }
  2645. }
  2646. if (action.inVars.length > 1) {
  2647. // 第二个参数是文件路径
  2648. const filePathVar = extractVarName(action.inVars[1]);
  2649. const filePathValue = variableContext[filePathVar];
  2650. if (filePathValue !== undefined) {
  2651. filePath = filePathValue;
  2652. } else {
  2653. filePath = resolveValue(action.inVars[1]);
  2654. }
  2655. }
  2656. }
  2657. if (!filePath) {
  2658. return { success: false, error: 'save-txt 缺少 filePath 参数' };
  2659. }
  2660. if (content === undefined || content === null) {
  2661. return { success: false, error: 'save-txt 缺少 content 参数' };
  2662. }
  2663. const result = await executeSaveTxt({
  2664. filePath,
  2665. content,
  2666. folderPath
  2667. });
  2668. if (!result.success) {
  2669. return { success: false, error: result.error };
  2670. }
  2671. // 如果提供了 outVars,可以将结果保存到变量(虽然通常不需要)
  2672. if (action.outVars && Array.isArray(action.outVars) && action.outVars.length > 0) {
  2673. const outputVarName = extractVarName(action.outVars[0]);
  2674. if (outputVarName) {
  2675. // 只允许 string 或 int 类型,保存成功标志为 "1" 或 1
  2676. variableContext[outputVarName] = result.success ? '1' : '0';
  2677. }
  2678. }
  2679. await logOutVars(action, variableContext, folderPath);
  2680. return { success: true };
  2681. }
  2682. case 'delay': {
  2683. // 延迟
  2684. const delayMs = parseDelayString(action.value || action.delay || '0s');
  2685. if (delayMs > 0) {
  2686. await new Promise(resolve => setTimeout(resolve, delayMs));
  2687. }
  2688. return { success: true };
  2689. }
  2690. case 'swipe': {
  2691. // 滑动操作
  2692. if (!electronAPI || !electronAPI.sendSwipe) {
  2693. return { success: false, error: '滑动 API 不可用' };
  2694. }
  2695. const { x1, y1, x2, y2 } = calculateSwipeCoordinates(
  2696. action.value,
  2697. resolution.width,
  2698. resolution.height
  2699. );
  2700. const swipeResult = await electronAPI.sendSwipe(device, x1, y1, x2, y2, 300);
  2701. if (!swipeResult.success) {
  2702. return { success: false, error: `滑动失败: ${swipeResult.error}` };
  2703. }
  2704. // 成功滑动
  2705. return { success: true };
  2706. }
  2707. case 'string-press': {
  2708. // 向后兼容:文字识别并点击
  2709. if (!electronAPI || !electronAPI.findTextAndGetCoordinate) {
  2710. return { success: false, error: '文字识别 API 不可用' };
  2711. }
  2712. const matchResult = await electronAPI.findTextAndGetCoordinate(device, action.value);
  2713. if (!matchResult.success) {
  2714. return { success: false, error: `文字识别失败: ${matchResult.error}` };
  2715. }
  2716. const { clickPosition } = matchResult;
  2717. const { x, y } = clickPosition;
  2718. if (!electronAPI || !electronAPI.sendTap) {
  2719. return { success: false, error: '点击 API 不可用' };
  2720. }
  2721. const tapResult = await electronAPI.sendTap(device, x, y);
  2722. if (!tapResult.success) {
  2723. return { success: false, error: `点击失败: ${tapResult.error}` };
  2724. }
  2725. // 成功点击文字
  2726. return { success: true };
  2727. }
  2728. case 'scroll': {
  2729. // 滚动操作(小幅度滚动)
  2730. if (!electronAPI || !electronAPI.sendScroll) {
  2731. return { success: false, error: '滚动 API 不可用' };
  2732. }
  2733. const scrollResult = await electronAPI.sendScroll(
  2734. device,
  2735. action.value,
  2736. resolution.width,
  2737. resolution.height,
  2738. DEFAULT_SCROLL_DISTANCE,
  2739. 500
  2740. );
  2741. if (!scrollResult.success) {
  2742. return { success: false, error: `滚动失败: ${scrollResult.error}` };
  2743. }
  2744. // 成功滚动
  2745. return { success: true };
  2746. }
  2747. default:
  2748. return { success: false, error: `未知的操作类型: ${action.type}` };
  2749. }
  2750. } catch (error) {
  2751. // 记录错误到 log.txt
  2752. const now = new Date();
  2753. const year = now.getFullYear();
  2754. const month = String(now.getMonth() + 1).padStart(2, '0');
  2755. const day = String(now.getDate()).padStart(2, '0');
  2756. const hour = String(now.getHours()).padStart(2, '0');
  2757. const minute = String(now.getMinutes()).padStart(2, '0');
  2758. const second = String(now.getSeconds()).padStart(2, '0');
  2759. const timeStr = `${year}/${month}/${day} ${hour}:${minute}:${second}`;
  2760. const errorMsg = `[错误] 操作执行失败: ${error.message} [系统时间: ${timeStr}]`;
  2761. await logMessage(errorMsg, folderPath).catch(() => {
  2762. // 静默失败,不影响主流程
  2763. });
  2764. return { success: false, error: error.message };
  2765. }
  2766. }
  2767. /**
  2768. * 执行操作序列(支持嵌套和条件)
  2769. * @param {Array} actions - 解析后的操作列表
  2770. * @param {string} device - 设备 ID
  2771. * @param {string} folderPath - 文件夹路径
  2772. * @param {Object} resolution - 设备分辨率
  2773. * @param {number} stepInterval - 步骤间隔时间(毫秒),默认1秒
  2774. * @param {Function} onStepComplete - 每步完成后的回调函数
  2775. * @param {Function} shouldStop - 检查是否应该停止的函数
  2776. * @param {number} depth - 嵌套深度(用于递归)
  2777. * @returns {Promise<Object>} 执行结果 {success, error?, completedSteps}
  2778. */
  2779. async function executeActionSequence(
  2780. actions,
  2781. device,
  2782. folderPath,
  2783. resolution,
  2784. stepInterval = DEFAULT_STEP_INTERVAL,
  2785. onStepComplete = null,
  2786. shouldStop = null,
  2787. depth = 0
  2788. ) {
  2789. // 如果是顶层(depth === 0),重置全局步骤计数器和变量初始化标志
  2790. if (depth === 0) {
  2791. globalStepCounter = 0;
  2792. // 重置变量初始化标志,允许在新工作流开始时重新初始化
  2793. variableContextInitialized = false;
  2794. // 保存当前工作流文件夹路径,用于日志记录
  2795. currentWorkflowFolderPath = folderPath;
  2796. // 添加分割线,分开每次执行的日志
  2797. await logMessage('========================', folderPath);
  2798. }
  2799. let completedSteps = 0;
  2800. const stepPrefix = depth > 0 ? ' '.repeat(depth) : '';
  2801. for (let i = 0; i < actions.length; i++) {
  2802. // 检查是否应该停止
  2803. if (shouldStop && shouldStop()) {
  2804. // 执行被停止
  2805. return { success: false, error: '执行被停止', completedSteps };
  2806. }
  2807. const action = actions[i];
  2808. // 处理特殊操作类型
  2809. if (action.type === 'schedule') {
  2810. // schedule 操作:根据 condition 中的 interval 和 repeat 执行动作
  2811. const condition = action.condition || {};
  2812. const intervalStr = condition.interval || '0s';
  2813. const repeat = condition.repeat !== undefined ? condition.repeat : 1;
  2814. const actionsToExecute = action.interval || [];
  2815. // 解析间隔时间
  2816. const intervalMs = parseDelayString(intervalStr) || 0;
  2817. // 确定循环次数(-1 表示无限循环)
  2818. const maxIterations = repeat === -1 ? Infinity : (typeof repeat === 'number' ? repeat : 1);
  2819. let iteration = 0;
  2820. while (iteration < maxIterations) {
  2821. if (shouldStop && shouldStop()) {
  2822. return { success: false, error: '执行被停止', completedSteps };
  2823. }
  2824. iteration++;
  2825. // 如果不是第一次迭代,等待间隔时间
  2826. if (iteration > 1 && intervalMs > 0) {
  2827. // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
  2828. let remainingTime = intervalMs;
  2829. const countdownInterval = 100;
  2830. while (remainingTime > 0) {
  2831. if (shouldStop && shouldStop()) {
  2832. return { success: false, error: '执行被停止', completedSteps };
  2833. }
  2834. const waitTime = Math.min(countdownInterval, remainingTime);
  2835. await new Promise(resolve => setTimeout(resolve, waitTime));
  2836. remainingTime -= waitTime;
  2837. }
  2838. }
  2839. // 执行动作序列
  2840. if (actionsToExecute.length > 0) {
  2841. const result = await executeActionSequence(
  2842. actionsToExecute,
  2843. device,
  2844. folderPath,
  2845. resolution,
  2846. stepInterval,
  2847. onStepComplete,
  2848. shouldStop,
  2849. depth + 1
  2850. );
  2851. if (!result.success) {
  2852. return result;
  2853. }
  2854. completedSteps += result.completedSteps || 0;
  2855. }
  2856. }
  2857. continue;
  2858. }
  2859. if (action.type === 'if') {
  2860. const conditionResult = evaluateCondition(action.condition);
  2861. // 支持 ture(拼写错误)和 false 作为 then 和 else 的别名
  2862. const actionsToExecute = conditionResult ? (action.then || action.ture || []) : (action.else || action.false || []);
  2863. if (actionsToExecute.length > 0) {
  2864. const result = await executeActionSequence(
  2865. actionsToExecute,
  2866. device,
  2867. folderPath,
  2868. resolution,
  2869. stepInterval,
  2870. onStepComplete,
  2871. shouldStop,
  2872. depth + 1
  2873. );
  2874. if (!result.success) {
  2875. return result;
  2876. }
  2877. completedSteps += result.completedSteps || 0;
  2878. }
  2879. continue;
  2880. }
  2881. if (action.type === 'for') {
  2882. const items = Array.isArray(action.items) ? action.items : [];
  2883. for (const item of items) {
  2884. if (shouldStop && shouldStop()) {
  2885. return { success: false, error: '执行被停止', completedSteps };
  2886. }
  2887. // 设置循环变量
  2888. if (action.variable) {
  2889. variableContext[action.variable] = item;
  2890. }
  2891. if (action.body && action.body.length > 0) {
  2892. const result = await executeActionSequence(
  2893. action.body,
  2894. device,
  2895. folderPath,
  2896. resolution,
  2897. stepInterval,
  2898. onStepComplete,
  2899. shouldStop,
  2900. depth + 1
  2901. );
  2902. if (!result.success) {
  2903. return result;
  2904. }
  2905. completedSteps += result.completedSteps || 0;
  2906. }
  2907. }
  2908. continue;
  2909. }
  2910. if (action.type === 'while') {
  2911. while (evaluateCondition(action.condition)) {
  2912. if (shouldStop && shouldStop()) {
  2913. return { success: false, error: '执行被停止', completedSteps };
  2914. }
  2915. if (action.body && action.body.length > 0) {
  2916. const result = await executeActionSequence(
  2917. action.body,
  2918. device,
  2919. folderPath,
  2920. resolution,
  2921. stepInterval,
  2922. onStepComplete,
  2923. shouldStop,
  2924. depth + 1
  2925. );
  2926. if (!result.success) {
  2927. return result;
  2928. }
  2929. completedSteps += result.completedSteps || 0;
  2930. }
  2931. }
  2932. continue;
  2933. }
  2934. // 普通操作
  2935. const times = action.times || 1;
  2936. // 发送步骤开始执行事件
  2937. if (onStepComplete) {
  2938. const stepName = getActionName(action);
  2939. onStepComplete(i + 1, actions.length, stepName, 0, times, 0);
  2940. }
  2941. // 计算等待时间(根据 data 和 delay)
  2942. const waitTime = calculateWaitTime(action.data, action.delay);
  2943. if (waitTime > 0) {
  2944. const waitSeconds = Math.round(waitTime / 1000);
  2945. // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
  2946. // 在等待期间也更新倒计时
  2947. let remainingTime = waitTime;
  2948. const countdownInterval = 100;
  2949. const stepName = getActionName(action);
  2950. while (remainingTime > 0) {
  2951. if (shouldStop && shouldStop()) {
  2952. return { success: false, error: '执行被停止', completedSteps };
  2953. }
  2954. if (onStepComplete) {
  2955. onStepComplete(i + 1, actions.length, stepName, remainingTime, times, 0);
  2956. }
  2957. const waitTimeChunk = Math.min(countdownInterval, remainingTime);
  2958. await new Promise(resolve => setTimeout(resolve, waitTimeChunk));
  2959. remainingTime -= waitTimeChunk;
  2960. }
  2961. }
  2962. // 根据 times 重复执行操作
  2963. for (let t = 0; t < times; t++) {
  2964. // 检查是否应该停止
  2965. if (shouldStop && shouldStop()) {
  2966. // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
  2967. return { success: false, error: '执行被停止', completedSteps };
  2968. }
  2969. // 发送步骤执行中事件(包含当前执行次数)
  2970. if (onStepComplete) {
  2971. const stepName = getActionName(action);
  2972. onStepComplete(i + 1, actions.length, stepName, 0, times, t + 1);
  2973. }
  2974. // 使用全局步骤计数器
  2975. globalStepCounter++;
  2976. const currentStepNumber = globalStepCounter;
  2977. // 记录步骤开始时间
  2978. const stepStartTime = Date.now();
  2979. const startTimeStr = new Date(stepStartTime).toLocaleString('zh-CN', {
  2980. year: 'numeric',
  2981. month: '2-digit',
  2982. day: '2-digit',
  2983. hour: '2-digit',
  2984. minute: '2-digit',
  2985. second: '2-digit',
  2986. hour12: false
  2987. });
  2988. // 获取操作类型名称
  2989. const typeName = getActionName(action);
  2990. // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
  2991. // 执行操作
  2992. const result = await executeAction(action, device, folderPath, resolution);
  2993. // 记录步骤结束时间
  2994. const stepEndTime = Date.now();
  2995. const endTimeStr = new Date(stepEndTime).toLocaleString('zh-CN', {
  2996. year: 'numeric',
  2997. month: '2-digit',
  2998. day: '2-digit',
  2999. hour: '2-digit',
  3000. minute: '2-digit',
  3001. second: '2-digit',
  3002. hour12: false
  3003. });
  3004. const stepDuration = (stepEndTime - stepStartTime) / 1000; // 转换为秒
  3005. // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
  3006. if (!result.success) {
  3007. // 记录错误到 log.txt
  3008. const now = new Date();
  3009. const year = now.getFullYear();
  3010. const month = String(now.getMonth() + 1).padStart(2, '0');
  3011. const day = String(now.getDate()).padStart(2, '0');
  3012. const hour = String(now.getHours()).padStart(2, '0');
  3013. const minute = String(now.getMinutes()).padStart(2, '0');
  3014. const second = String(now.getSeconds()).padStart(2, '0');
  3015. const timeStr = `${year}/${month}/${day} ${hour}:${minute}:${second}`;
  3016. const errorMsg = `[错误] ${getActionName(action)} 执行失败: ${result.error} [系统时间: ${timeStr}]`;
  3017. await logMessage(errorMsg, folderPath).catch(() => {
  3018. // 静默失败,不影响主流程
  3019. });
  3020. return { success: false, error: result.error, completedSteps: i };
  3021. }
  3022. // 如果不是最后一次重复,等待一小段时间
  3023. if (t < times - 1) {
  3024. await new Promise(resolve => setTimeout(resolve, 500));
  3025. }
  3026. }
  3027. completedSteps++;
  3028. // 调用完成回调
  3029. if (onStepComplete) {
  3030. const stepName = getActionName(action);
  3031. onStepComplete(i + 1, actions.length, stepName, 0, times, times);
  3032. }
  3033. // 如果不是最后一步,等待间隔时间
  3034. if (i < actions.length - 1) {
  3035. let remainingTime = stepInterval;
  3036. const countdownInterval = 100;
  3037. const nextStepName = getActionName(actions[i + 1]);
  3038. const nextTimes = actions[i + 1].times || 1;
  3039. while (remainingTime > 0) {
  3040. if (shouldStop && shouldStop()) {
  3041. return { success: false, error: '执行被停止', completedSteps };
  3042. }
  3043. if (onStepComplete) {
  3044. onStepComplete(i + 1, actions.length, nextStepName, remainingTime, nextTimes, 0);
  3045. }
  3046. const waitTime = Math.min(countdownInterval, remainingTime);
  3047. await new Promise(resolve => setTimeout(resolve, waitTime));
  3048. remainingTime -= waitTime;
  3049. }
  3050. }
  3051. }
  3052. if (depth === 0) {
  3053. // 注意:系统日志不再写入 log.txt,只保留 echo 类型的日志
  3054. }
  3055. return { success: true, completedSteps };
  3056. }
  3057. module.exports = { parseWorkflow, parseActions, calculateSwipeCoordinates, executeAction, executeActionSequence }