ef-compiler.js 124 KB

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