ef-compiler.js 119 KB

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