From 6c7004d68036a3df647d7f2e21b826561da68d48 Mon Sep 17 00:00:00 2001 From: yan Date: Sun, 29 Mar 2026 21:31:16 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(auto-exchange):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=8A=BD=E5=8D=A1=E8=B5=84=E6=BA=90=E5=85=91=E6=8D=A2?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E7=9A=84=E5=8F=98=E9=87=8F=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除不必要的注释内容,保持代码简洁性 - 确保 contentList 变量正确初始化为空数组 - 提升代码可读性和维护性 refactor(auto-exchange): 优化抽卡资源自动兑换的时间记录逻辑 - 将存储结构从 Map 改为数组对象列表,每个对象包含 uid 和时间戳 - 使用 find 方法替代 get 方法查找用户对应的完成时间记录 - 实现了更准确的时间戳记录和更新机制 - 修改文件读写操作以适配新的数据结构 - 修复了时间记录的数据持久化逻辑 - 更新了调试日志的内容显示方式 feat(auto-exchange): 添加兑换抽卡资源任务的通知配置选项 - 实现通知发送条件控制,仅当 config.send_notification 为 true 时发送 - 将固定的通知消息改为变量存储并记录到日志 - 调整文件创建成功的日志级别从 info 降至 debug - 优化代码结构以支持更灵活的通知策略 feat(auto-exchange): 添加通知配置和错误处理优化 - 引入 throwError 工具函数替代原有错误抛出方式 - 新增 send_notification 配置选项控制通知发送 - 优化星尘数量不足时的错误提示信息 - 实现条件通知发送机制,避免重复通知 - 更新设置界面添加通知配置复选框 - 重构错误处理逻辑提高代码健壮性 feat(exchange): 实现每月自动兑换抽卡资源功能 - 添加了OCR识别UID功能用于用户标识 - 实现了重试机制和错误处理逻辑 - 集成了定时刷新任务系统,默认每月1号凌晨4点执行 - 添加了商城抽卡资源自动兑换流程 - 集成了返回主界面的安全导航功能 - 添加了配置文件存储兑换记录和状态管理 - 实现了图像识别定位和点击操作自动化 - 添加了设置界面支持最大重试次数配置 feat(exchange): 实现基于UID的任务状态管理与重试机制 - 添加OCR识别UID功能,支持多用户任务状态区分 - 修改任务刷新逻辑,将单一时间戳改为UID映射的时间存储 - 实现重试机制,支持最大重试次数限制 - 优化资源兑换流程,添加空球检测和安全释放机制 - 改进错误处理,添加详细的日志记录和异常处理 - 升级版本号至1.2并添加新的开发者信息 --- .../assets/paimon_menu.png" | Bin 0 -> 2372 bytes .../main.js" | 276 +++++++++++++----- .../manifest.json" | 6 +- .../settings.json" | 18 ++ .../utils/tool.js" | 82 ++++++ .../utils/uid.js" | 64 ++++ 6 files changed, 368 insertions(+), 78 deletions(-) create mode 100644 "repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/assets/paimon_menu.png" create mode 100644 "repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/settings.json" create mode 100644 "repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/utils/tool.js" create mode 100644 "repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/utils/uid.js" diff --git "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/assets/paimon_menu.png" "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/assets/paimon_menu.png" new file mode 100644 index 0000000000000000000000000000000000000000..c424325b17a4956f7802352e7609bfcff2721afa GIT binary patch literal 2372 zcmV-K3A^@*P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2-!(QK~z{rt(SRF zR96@7{OrIrs3OB6ST- zP@DTTKA&cd-_x`4ifbQ;v^!UEWYd=r=ft5o>jaWdZ$#*F1Dp=`Lw#i>UdiRG$$NWQ zlS!qJbxWbLunJ)v#?D#+4Ko*L&U1#Kww~+hpIblw8}X6L@I3V>qTOd=&jwpmmE@tP zN6PlU71k%NS0-b{sVwYPN+w3?*E%@$g9jWc1ShNWOUqVas)}Z=D&g zor^(ddmGo&$7^Bli&I(H`jA5yHPeBFc|eOy)65lLn65z7v3Rt#cd}#T%%3oS_UaWY zP8XsoYg`}%_!_r5rlgw?!gfco5(B+3n1mL!af6==BzpXu8{&uT3qW6mS~El zkk8E=nsSFID@2ckOt;?zeTP8kFWU_L<(r{v?++`_P}J1FJ zrGzD=Wn-M71IC)Hz~luUn6cEC1O>prF&HzQw!v`aH!xiFElga#ftK|K6j!LeQ~fcd zu+pkpT)duyod@HvcufdKo4P~8+8;*FJ6JPb9Y(ejI`+W`*mDLlI_jSS1!4JxCG0UD zjGX)uI0x;=R2zR7yM)5TEgZ8vA~DL$8>v}OxltdFg0S1arJ|~;ihCNEj?QkRP&zVj z41%F+IE+Xf^I2P(TeuM)kAkquN&(Ks|A_jR&0Ndi6qZ$E%2INE=P)evj^tWD4GO}l zs|660<>Kz0L`bDQTlvJ_ zd*fhAC$o!g1)mB9VKP}S5-webP*{r6(tI2{eg@gOg$bAczCL=vv$<53XC zkeQu}q9VFy$(tdpeg^M=ojCvFHMF#d*ud8xObT(KlbXT*a1qj;9&~keb5F|ifiUJp zNhv9icK(5G$#b-d>yYrnMFa##prLW#L-Bw@#s88sxTm*NR@Wfn@W}xeH4txvNhA_- zcq#jW+`Iw^Y1d>OFVNK}fV`&#XU@hWEkpUF{2C@MnmesS7C)bBQI0pl_)qhZ`REaH zbDpBOT!@R8ZXho^3q6ur^t9F?BQ1rEbE?8D^!SPMuy^t0T9ku#bS|6c^{Bu}MgWpb_a$GGT7-OE0cORWTp;=~E8}W|#CW zR${z~Eqp`wL7jee<~rcixt~x{E}+8X!^F>HL`^VR!V1gg=di+jLJq*rB>)Q@{g7Lj zN99xl$_gIiNnRc|O*L9+cNVVl#e~`R*dBe9y@5R87vNS>8Xjb1<5}5<`I$FqjvfA{ zJ{#fD2dJ13WAB0QFoW(@F(=Q`N}ACvu7 z94CBkV1wTuWpOIxN6it998EI_F8V*b>3*K`Kbcg3(3`{ZXAAT4bR7q)3 zi8tS63pzXdYS4k;sTtc~=+|VZ=M}SYR<;_3-Qba3-?kI z@t^~J`z(Eg++aT0lFm2#AOF&x$eXiTPDp7 z;rzyS?wDrzH8d>N!Q5>Jw(UO$fv{#+$=c8<2qR6M$*06z3(!p0ZLKAKm_<8f>KcaW zD?;EHxOd1!^DJDqu(_;R)XLV2!$I8I4&R6q7;oW)^}CL8txPKF!PbNS!W0rV$88s8 zyGKB6kw0#xq;n$%hBwJN2#F7dP7#9<6del#`#`8ISPy>D+c2;{HUVl@8(`$M2PW>3 z7-Q~*9Wn7r`7O`VI~H~U2hk!@J|r+OaV_~#E-csV!lVV>RK9J;^`s2e1q2j4nK*SX zGZ)dZKfxvV0K9gdz}34C(5Yl$!kg!F3Wgp>VC)%%!qQ5vd2ljv^5GwL7@MQMN9={` zxc~SmS}1mjByBi&DgoNon-Ca%l1BE@0_1GH$rL%t!bs item.uid === current.uid) + //todo:uid取lastTime + const lastTime = lastTimeStr ? new Date(lastTimeStr) : new Date(0); const nowTime = new Date(); - + current.time=nowTime.getTime(); + if (contentList.some(item => item.uid === current.uid)) { + contentList.forEach(item => { + if (item.uid === current.uid) { + item.time = current.time; + } + }) + }else { + contentList.push(current); + } + let shouldRefresh = false; - + switch (refreshType) { case 'hourly': // 每小时刷新 shouldRefresh = (nowTime - lastTime) >= 3600 * 1000; break; - + case 'daily': // 每天固定时间刷新 // 检查是否已经过了当天的刷新时间 const todayRefresh = new Date(nowTime); todayRefresh.setHours(dailyHour, 0, 0, 0); - + // 如果当前时间已经过了今天的刷新时间,检查上次完成时间是否在今天刷新之前 if (nowTime >= todayRefresh) { shouldRefresh = lastTime < todayRefresh; @@ -53,7 +94,7 @@ async function isTaskRefreshed(filePath, options = {}) { shouldRefresh = lastTime < yesterdayRefresh; } break; - + case 'weekly': // 每周固定时间刷新 // 获取本周的刷新时间 const thisWeekRefresh = new Date(nowTime); @@ -61,7 +102,7 @@ async function isTaskRefreshed(filePath, options = {}) { const dayDiff = (thisWeekRefresh.getDay() - weeklyDay + 7) % 7; thisWeekRefresh.setDate(thisWeekRefresh.getDate() - dayDiff); thisWeekRefresh.setHours(weeklyHour, 0, 0, 0); - + // 如果当前时间已经过了本周的刷新时间 if (nowTime >= thisWeekRefresh) { shouldRefresh = lastTime < thisWeekRefresh; @@ -72,14 +113,14 @@ async function isTaskRefreshed(filePath, options = {}) { shouldRefresh = lastTime < lastWeekRefresh; } break; - + case 'monthly': // 每月固定时间刷新 // 获取本月的刷新时间 const thisMonthRefresh = new Date(nowTime); // 设置为本月指定日期的凌晨 thisMonthRefresh.setDate(monthlyDay); thisMonthRefresh.setHours(monthlyHour, 0, 0, 0); - + // 如果当前时间已经过了本月的刷新时间 if (nowTime >= thisMonthRefresh) { shouldRefresh = lastTime < thisMonthRefresh; @@ -94,39 +135,48 @@ async function isTaskRefreshed(filePath, options = {}) { case 'custom': // 自定义小时数刷新 shouldRefresh = (nowTime - lastTime) >= customHours * 3600 * 1000; break; - + default: throw new Error(`未知的刷新类型: ${refreshType}`); } - - // 如果文件内容无效或不存在,视为需要刷新 - if (!content || isNaN(lastTime.getTime())) { - await file.writeText(filePath, nowTime.toISOString()); - shouldRefresh = true; - } - + + // // 如果文件内容无效或不存在,视为需要刷新 + // if (!contentList || isNaN(lastTime.getTime())) { + // //todo:写入也要改 contentList.put(uid, nowTime.toISOString()) + // // await file.writeText(filePath, JSON.stringify(contentList)); + // shouldRefresh = true; + // } + if (shouldRefresh) { - notification.send(`任务已刷新,执行每月兑换抽卡资源`); + const message = `任务已刷新,执行每月兑换抽卡资源`; + log.info( message) + if (config.send_notification){ + notification.send(message); + } await exchangeGoods(); // 更新最后完成时间 - await file.writeText(filePath, nowTime.toISOString()); + await file.writeText(filePath, JSON.stringify(contentList)); return true; } else { - notification.send(`任务未刷新,跳过每月兑换抽卡资源`); + const message = `任务未刷新,跳过每月兑换抽卡资源`; + log.info( message) + if (config.send_notification){ + notification.send(message); + } return false; } - + log.debug("contentList:", JSON.stringify(contentList)) } catch (error) { + log.error(`刷新任务失败: ${error}`); // 如果文件不存在,创建新文件并返回true(视为需要刷新) - const createResult = await file.writeText(filePath, ''); + const createResult = await file.writeText(filePath, JSON.stringify(contentList)); if (createResult) { - log.info("创建新文件成功"); - await isTaskRefreshed(filePath, options = {}); + log.debug("创建新文件成功"); + await isTaskRefreshed(filePath, options = {}); } } } - //检查是否为正整数 function positiveIntegerJudgment(testNumber) { // 如果输入是字符串,尝试转换为数字 @@ -135,76 +185,148 @@ function positiveIntegerJudgment(testNumber) { const cleaned = testNumber.replace(/[^\d]/g, ''); testNumber = parseInt(cleaned, 10); } - + // 检查是否为有效的数字 if (typeof testNumber !== 'number' || isNaN(testNumber)) { throw new Error(`无效的值: ${testNumber} (必须为数字)`); } - + // 检查是否为整数 if (!Number.isInteger(testNumber)) { throw new Error(`必须为整数: ${testNumber}`); } - + return testNumber; } - async function exchangeGoods() { - - await genshin.returnMainUi();await sleep(1000); - keyPress("ESCAPE"); await sleep(2000);//呼叫派蒙 - click(198,416);await sleep(2000);//点击商城 - click(127,434);await sleep(1000);//尘辉兑换 - click(998,125);await sleep(1000);//星辰兑换 + await toMainUi(); + await sleep(1000); + keyPress("ESCAPE"); + await sleep(2000);//呼叫派蒙 + click(198, 416); + await sleep(2000);//点击商城 + click(127, 434); + await sleep(1000);//尘辉兑换 + click(998, 125); + await sleep(1000);//星辰兑换 + let materialQuantity = ""; //检查星辰的数量 const region = RecognitionObject.ocr(1400, 31, 150, 50); // 星辰数量区域 let capture = captureGameRegion(); - let res = capture.find(region); - capture.dispose(); - let materialQuantity = res.text; - let validatedMaterialQuantity = positiveIntegerJudgment(materialQuantity); - if(validatedMaterialQuantity < 750){ - notification.send(`星尘数量为:${validatedMaterialQuantity},无法全部兑换`); - throw new Error(`星尘数量为:${validatedMaterialQuantity},不能完全兑换`); + try { + let res = capture.find(region); + materialQuantity = res.text; + } finally { + if (capture) { + capture.dispose(); + } } - log.info(`星尘数量为:${validatedMaterialQuantity},数量充足,可以全部兑换`); + const pinkBallRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/pinkBall.png")); let ro1 = captureGameRegion(); - let pinkBall = ro1.find(pinkBallRo); - ro1.dispose(); - if (pinkBall.isExist()) { - pinkBall.click();await sleep(1000); - click(1290,604);await sleep(500);//增加 - click(1290,604);await sleep(500);//增加 - click(1290,604);await sleep(500);//增加 - click(1290,604);await sleep(500);//增加 - click(1164,782);await sleep(500);//确认兑换 - click(960,754);await sleep(1000);//点击空白处继续 + let pinkBallExist = false + let pinkBall + try { + let pinkBallFind = ro1.find(pinkBallRo); + pinkBallExist = pinkBallFind.isExist(); + pinkBall = { + x: pinkBallFind.x, + y: pinkBallFind.y + } + } finally { + if (ro1) { + ro1.dispose(); + } } + const blueBallRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/blueBall.png")); let ro2 = captureGameRegion(); - let blueBall = ro2.find(blueBallRo); - ro2.dispose(); - if (blueBall.isExist()) { - blueBall.click();await sleep(1000); - click(1290,604);await sleep(500);//增加 - click(1290,604);await sleep(500);//增加 - click(1290,604);await sleep(500);//增加 - click(1290,604);await sleep(500);//增加 - click(1164,782);await sleep(500);//确认兑换 - click(960,754);await sleep(1000);//点击空白处继续 - } - notification.send(`商城抽卡资源兑换完成`); -} + let blueBallExist = false + let blueBall + try { + let blueBallFind = ro2.find(blueBallRo); + blueBallExist = blueBallFind.isExist(); + blueBall = { + x: blueBallFind.x, + y: blueBallFind.y + } + } finally { + if (ro2) { + ro2.dispose(); + } + } + + if (!pinkBallExist && !blueBallExist) { + log.info(`没有粉球和蓝球,跳过兑换`); + return + } + let validatedMaterialQuantity = positiveIntegerJudgment(materialQuantity); + if (validatedMaterialQuantity < 750) { + throwError(`星尘数量为:${validatedMaterialQuantity},数量不足,无法全部兑换`,config.send_notification) + // notification.send(`星尘数量为:${validatedMaterialQuantity},无法全部兑换`); + // throw new Error(`星尘数量为:${validatedMaterialQuantity},不能完全兑换`); + } + log.info(`星尘数量为:${validatedMaterialQuantity},数量充足,可以全部兑换`); - -await isTaskRefreshed("assets/monthly.txt", { - refreshType: 'monthly', - monthlyDay: 1, // 每月第1天(默认值,可省略) - monthlyHour: 4 // 凌晨4点(默认值,可省略) -}); + if (pinkBallExist && pinkBall) { + // pinkBall.click(); + click(pinkBall.x, pinkBall.y) + await sleep(1000); + click(1290, 604); + await sleep(500);//增加 + click(1290, 604); + await sleep(500);//增加 + click(1290, 604); + await sleep(500);//增加 + click(1290, 604); + await sleep(500);//增加 + click(1164, 782); + await sleep(500);//确认兑换 + click(960, 754); + await sleep(1000);//点击空白处继续 + } + + if (blueBallExist && blueBall) { + // blueBall.click(); + click(blueBall.x, blueBall.y) + await sleep(1000); + click(1290, 604); + await sleep(500);//增加 + click(1290, 604); + await sleep(500);//增加 + click(1290, 604); + await sleep(500);//增加 + click(1290, 604); + await sleep(500);//增加 + click(1164, 782); + await sleep(500);//确认兑换 + click(960, 754); + await sleep(1000);//点击空白处继续 + } + const message = `商城抽卡资源兑换完成`; + log.info(message) + if (config.send_notification){ + notification.send(message); + } +} +async function main() { + try { + config.tryRe.max = parseInt(settings.try_count_max + "") || config.tryRe.max + config.send_notification= settings.send_notification + } catch (e) { + } + try { + await isTaskRefreshed("assets/monthly.txt", { + refreshType: 'monthly', + monthlyDay: 1, // 每月第1天(默认值,可省略) + monthlyHour: 4 // 凌晨4点(默认值,可省略) + }); + }finally { + await toMainUi() + } +} -})(); +await main(); diff --git "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/manifest.json" "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/manifest.json" index b8e6e1a01d..e87a34f418 100644 --- "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/manifest.json" +++ "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/manifest.json" @@ -1,12 +1,16 @@ { "manifest_version": 1, "name": "每月自动兑换抽卡资源", - "version": "1.1", + "version": "1.2", "description": "每个月自动兑换蓝球和粉球,兑换资源不够会提醒(需要打开 js 通知),本月兑换过会自动跳过,想要重置 CD可以把monthly.txt中的时间删掉", "authors": [ { "name": "柒叶子", "links": "https://github.com/5117600049" + }, + { + "name": "云端客", + "links": "https://github.com/Kirito520Asuna" } ], "settings_ui": "settings.json", diff --git "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/settings.json" "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/settings.json" new file mode 100644 index 0000000000..5f517cdbbb --- /dev/null +++ "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/settings.json" @@ -0,0 +1,18 @@ +[ + { + "name": "send_notification", + "type": "checkbox", + "label": "发送通知" + }, + { + "name": "try_count_max", + "type": "select", + "label": "最大重试次数", + "options": [ + "1","2","3", + "4","5","6", + "7","8","9" + ] , + "default": "3" + } +] \ No newline at end of file diff --git "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/utils/tool.js" "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/utils/tool.js" new file mode 100644 index 0000000000..603e8485fc --- /dev/null +++ "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/utils/tool.js" @@ -0,0 +1,82 @@ +const commonPath = 'assets/' +const commonMap = new Map([ + ['main_ui', { + path: `${commonPath}`, + name: 'paimon_menu', + type: '.png', + }], +]) +const genshinJson = { + width: 1920,//genshin.width, + height: 1080,//genshin.height, +} + +/** + * 根据键值获取JSON路径 + * @param {string} key - 要查找的键值 + * @returns {any} 返回与键值对应的JSON路径值 + */ +function getJsonPath(key) { + return commonMap.get(key); // 通过commonMap的get方法获取指定键对应的值 +} + +// 判断是否在主界面的函数 +const isInMainUI = () => { + // let name = '主界面' + let main_ui = getJsonPath('main_ui'); + // 定义识别对象 + let paimonMenuRo = RecognitionObject.TemplateMatch( + file.ReadImageMatSync(`${main_ui.path}${main_ui.name}${main_ui.type}`), + 0, + 0, + genshinJson.width / 3.0, + genshinJson.width / 5.0 + ); + let captureRegion = captureGameRegion(); + try { + let res = captureRegion.find(paimonMenuRo); + return !res.isEmpty(); + } finally { + captureRegion.dispose() + } + +}; + +async function toMainUi() { + let ms = 300 + let index = 1 + await sleep(ms); + while (!isInMainUI()) { + await sleep(ms); + await genshin.returnMainUi(); // 如果未启用,则返回游戏主界面 + await sleep(ms); + if (index > 3) { + throwError(`多次尝试返回主界面失败`); + } + index += 1 + } + +} + + +/** + * 抛出错误函数 + * 该函数用于显示错误通知并抛出错误对象 + * @param {string} msg - 错误信息,将用于通知和错误对象 + */ +function throwError(msg, isNotification = false) { + // 使用notification组件显示错误通知 + // notification.error(`${msg}`); + if (isNotification) { + notification.error(`${msg}`); + } + // 抛出一个包含错误信息的Error对象 + throw new Error(`${msg}`); +} + +export { + getJsonPath, + isInMainUI, + toMainUi, + throwError, +} \ No newline at end of file diff --git "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/utils/uid.js" "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/utils/uid.js" new file mode 100644 index 0000000000..f5c18cc90c --- /dev/null +++ "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/utils/uid.js" @@ -0,0 +1,64 @@ + +async function saveOnlyNumber(str) { + str = str ? str : ''; + // 使用正则表达式匹配字符串中的所有数字 + // \d匹配一个或多个数字 + // .join('') 将匹配到的数字数组连接成一个字符串 + // parseInt 将连接后的字符串转换为整数 + // return parseInt(str.match(/\d+/g).join('')); + const matches = str.match(/\d+/g); + if (!matches) { + return 0; // 或抛出错误 + } + return parseInt(matches.join(''), 10); +} +/** + * 对指定区域进行OCR文字识别 + * @param {number} x - 区域左上角x坐标,默认为0 + * @param {number} y - 区域左上角y坐标,默认为0 + * @param {number} w - 区域宽度,默认为1920 + * @param {number} h - 区域高度,默认为1080 + * @returns {Promise} 返回识别到的文本内容,如果识别失败则返回null + */ +async function ocrRegion(x = 0, + y = 0, + w = 1920, + h = 1080) { + // 创建OCR识别对象,使用指定的坐标和尺寸 + let recognitionObjectOcr = RecognitionObject.Ocr(x, y, w, h); + // 捕获游戏区域图像 + let region3 = captureGameRegion() + try { + // 在捕获的区域中查找OCR识别对象 + let res = region3.find(recognitionObjectOcr); + // 返回识别到的文本内容,如果不存在则返回undefined + return res?.text + } catch (e) { + // 捕获并记录错误信息 + log.error("识别异常:{1}", e.message) + return null + } finally { + // 确保释放区域资源 + region3.dispose() + } +} +/** + * OCR识别UID的异步函数 + * 该函数用于通过OCR技术识别屏幕上特定位置的UID文本 + * @returns {Promise} - 异步函数,没有明确的返回值 + */ +async function ocrUid() { + // 定义OCR识别的坐标和尺寸参数 + let uid_json = { + x: 1683, // OCR识别区域的左上角x坐标 + y: 1051, // OCR识别区域的左上角y坐标 + width: 234, // OCR识别区域的宽度 + height: 28, // OCR识别区域的高度 + } + let text = await ocrRegion(uid_json.x, uid_json.y, uid_json.width, uid_json.height); + return await saveOnlyNumber(text); +} + +export { + ocrUid, +} From a8505de72d9b6c5bb3bb0eef1a7dc37cb19b3f22 Mon Sep 17 00:00:00 2001 From: yan Date: Mon, 30 Mar 2026 14:51:13 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor(auto-exchange):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=8A=BD=E5=8D=A1=E8=B5=84=E6=BA=90=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=85=91=E6=8D=A2=E7=9A=84=E6=97=B6=E9=97=B4=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除重复的时间记录代码,将逻辑集中到兑换成功后执行 - 调整代码结构,先获取当前时间再进行兑换操作 - 简化最后一次执行时间的查找逻辑 - 删除无用的注释代码 - 统一时间戳更新流程,确保数据一致性 fix(auto-exchange): 解决UID识别验证问题 - 添加UID类型检查确保为正整数 - 验证OCR识别结果的有效性 - 在UID识别失败时抛出具体错误信息 - 防止无效UID导致后续兑换流程异常 ``` refactor(exchange): 优化每月兑换抽卡资源任务逻辑 - 将代码块包裹在 try-finally 结构中确保调试日志始终输出 - 移除多余的注释代码以提高代码整洁性 - 保持原有的业务逻辑不变,仅调整代码结构 - 确保 contentList 调试信息在所有情况下都能正确记录 ``` fix(auto-exchange): 修复抽卡资源自动兑换的时间处理逻辑 - 修正了查找用户最后兑换时间的变量名错误 - 更新了时间数据的获取方式,从字符串改为数字格式 - 修复了时间对象创建的逻辑以正确处理时间戳 - 确保当前时间戳能正确更新到用户记录中 --- .../main.js" | 90 ++++++++++--------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/main.js" "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/main.js" index 75d1265de1..0700e8e017 100644 --- "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/main.js" +++ "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/main.js" @@ -1,5 +1,5 @@ import {ocrUid} from "./utils/uid.js"; -import {toMainUi,throwError} from "./utils/tool.js"; +import {toMainUi, throwError} from "./utils/tool.js"; const config = { tryRe: { @@ -9,7 +9,7 @@ const config = { user: { uid: undefined }, - send_notification:false + send_notification: false } /** @@ -43,33 +43,27 @@ async function isTaskRefreshed(filePath, options = {}) { throw new Error("已重试" + (try_count - 1) + "次数,超出最大重试" + try_count_max + "次数"); } if (!config.user.uid) { - config.user.uid = await ocrUid(); + const resolvedUid = await ocrUid(); + if (!Number.isInteger(resolvedUid) || resolvedUid <= 0) { + throw new Error(`UID 识别失败: ${resolvedUid}`); + } + config.user.uid = resolvedUid; } const uid = config.user.uid; const current = {uid: uid, time: undefined} // 读取文件内容 let contentList = []; try { - contentList = JSON.parse(file.readTextSync(filePath)) - }catch (e) { - log.debug("warn:"+e.message) + contentList = JSON.parse(file.readTextSync(filePath)) + } catch (e) { + log.debug("warn:" + e.message) } - + const last = contentList.find(item => item.uid === uid) + const lastTimeNumber = last?.time + const lastTime = lastTimeNumber ? new Date(lastTimeNumber) : new Date(0); try { - const lastTimeStr = contentList.find(item => item.uid === current.uid) - //todo:uid取lastTime - const lastTime = lastTimeStr ? new Date(lastTimeStr) : new Date(0); const nowTime = new Date(); - current.time=nowTime.getTime(); - if (contentList.some(item => item.uid === current.uid)) { - contentList.forEach(item => { - if (item.uid === current.uid) { - item.time = current.time; - } - }) - }else { - contentList.push(current); - } + current.time = nowTime.getTime(); let shouldRefresh = false; @@ -146,29 +140,41 @@ async function isTaskRefreshed(filePath, options = {}) { // // await file.writeText(filePath, JSON.stringify(contentList)); // shouldRefresh = true; // } + try { + if (shouldRefresh) { + const message = `任务已刷新,执行每月兑换抽卡资源`; + log.info(message) + if (config.send_notification) { + notification.send(message); + } + await exchangeGoods(); - if (shouldRefresh) { - const message = `任务已刷新,执行每月兑换抽卡资源`; - log.info( message) - if (config.send_notification){ - notification.send(message); - } - await exchangeGoods(); - // 更新最后完成时间 - await file.writeText(filePath, JSON.stringify(contentList)); - return true; - } else { - const message = `任务未刷新,跳过每月兑换抽卡资源`; - log.info( message) - if (config.send_notification){ - notification.send(message); + if (contentList.some(item => item.uid === current.uid)) { + contentList.forEach(item => { + if (item.uid === current.uid) { + item.time = current.time; + } + }) + } else { + contentList.push(current); + } + + // 更新最后完成时间 + await file.writeText(filePath, JSON.stringify(contentList)); + return true; + } else { + const message = `任务未刷新,跳过每月兑换抽卡资源`; + log.info(message) + if (config.send_notification) { + notification.send(message); + } + return false; } - return false; + } finally { + log.debug("contentList:", JSON.stringify(contentList)) } - log.debug("contentList:", JSON.stringify(contentList)) } catch (error) { log.error(`刷新任务失败: ${error}`); - // 如果文件不存在,创建新文件并返回true(视为需要刷新) const createResult = await file.writeText(filePath, JSON.stringify(contentList)); if (createResult) { log.debug("创建新文件成功"); @@ -264,7 +270,7 @@ async function exchangeGoods() { let validatedMaterialQuantity = positiveIntegerJudgment(materialQuantity); if (validatedMaterialQuantity < 750) { - throwError(`星尘数量为:${validatedMaterialQuantity},数量不足,无法全部兑换`,config.send_notification) + throwError(`星尘数量为:${validatedMaterialQuantity},数量不足,无法全部兑换`, config.send_notification) // notification.send(`星尘数量为:${validatedMaterialQuantity},无法全部兑换`); // throw new Error(`星尘数量为:${validatedMaterialQuantity},不能完全兑换`); } @@ -307,7 +313,7 @@ async function exchangeGoods() { } const message = `商城抽卡资源兑换完成`; log.info(message) - if (config.send_notification){ + if (config.send_notification) { notification.send(message); } } @@ -315,7 +321,7 @@ async function exchangeGoods() { async function main() { try { config.tryRe.max = parseInt(settings.try_count_max + "") || config.tryRe.max - config.send_notification= settings.send_notification + config.send_notification = settings.send_notification } catch (e) { } try { @@ -324,7 +330,7 @@ async function main() { monthlyDay: 1, // 每月第1天(默认值,可省略) monthlyHour: 4 // 凌晨4点(默认值,可省略) }); - }finally { + } finally { await toMainUi() } } From 970478a5b8705fb984f79a45061c92cc23dcb7fa Mon Sep 17 00:00:00 2001 From: yan Date: Wed, 1 Apr 2026 21:43:19 +0800 Subject: [PATCH 3/3] =?UTF-8?q?```=20feat(exchange):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=85=91=E6=8D=A2=E4=B8=8D=E8=B6=B3=E6=A3=80=E6=B5=8B=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在用户配置中增加insufficient_exchange状态标识 - 实现兑换不足时的错误抛出机制 - 添加星尘数量验证逻辑 - 当星尘数量少于750时设置兑换不足标志 - 集成通知发送功能用于兑换异常提醒 ``` --- .../main.js" | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/main.js" "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/main.js" index 0700e8e017..ebd7917861 100644 --- "a/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/main.js" +++ "b/repo/js/\346\257\217\346\234\210\350\207\252\345\212\250\345\205\221\346\215\242\346\212\275\345\215\241\350\265\204\346\272\220/main.js" @@ -7,7 +7,8 @@ const config = { count: 0 }, user: { - uid: undefined + uid: undefined, + insufficient_exchange: false,//兑换不足 }, send_notification: false } @@ -35,6 +36,9 @@ async function isTaskRefreshed(filePath, options = {}) { monthlyDay = 1, // 每月刷新默认第1天 monthlyHour = 4 // 每月刷新默认凌晨4点 } = options; + if (config.user.insufficient_exchange){ + throwError("兑换不足,请手动兑换", config.send_notification) + } config.tryRe.count++ const retry = config.tryRe; const try_count_max = retry.max @@ -270,6 +274,7 @@ async function exchangeGoods() { let validatedMaterialQuantity = positiveIntegerJudgment(materialQuantity); if (validatedMaterialQuantity < 750) { + config.user.insufficient_exchange=true throwError(`星尘数量为:${validatedMaterialQuantity},数量不足,无法全部兑换`, config.send_notification) // notification.send(`星尘数量为:${validatedMaterialQuantity},无法全部兑换`); // throw new Error(`星尘数量为:${validatedMaterialQuantity},不能完全兑换`);