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 0000000000..c424325b17 Binary files /dev/null and "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" differ 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 2896941caf..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" @@ -1,5 +1,18 @@ -(async function () { - +import {ocrUid} from "./utils/uid.js"; +import {toMainUi, throwError} from "./utils/tool.js"; + +const config = { + tryRe: { + max: 3, + count: 0 + }, + user: { + uid: undefined, + insufficient_exchange: false,//兑换不足 + }, + send_notification: false +} + /** * 判断任务是否已刷新 * @param {string} filePath - 存储最后完成时间的文件路径 @@ -23,26 +36,52 @@ 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 + const try_count = retry.count + if (try_count_max < try_count) { + throw new Error("已重试" + (try_count - 1) + "次数,超出最大重试" + try_count_max + "次数"); + } + if (!config.user.uid) { + 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) + } + const last = contentList.find(item => item.uid === uid) + const lastTimeNumber = last?.time + const lastTime = lastTimeNumber ? new Date(lastTimeNumber) : new Date(0); try { - // 读取文件内容 - let content = await file.readText(filePath); - const lastTime = new Date(content); const nowTime = new Date(); - + current.time = nowTime.getTime(); + 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 +92,7 @@ async function isTaskRefreshed(filePath, options = {}) { shouldRefresh = lastTime < yesterdayRefresh; } break; - + case 'weekly': // 每周固定时间刷新 // 获取本周的刷新时间 const thisWeekRefresh = new Date(nowTime); @@ -61,7 +100,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 +111,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 +133,60 @@ 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 (shouldRefresh) { - notification.send(`任务已刷新,执行每月兑换抽卡资源`); - await exchangeGoods(); - // 更新最后完成时间 - await file.writeText(filePath, nowTime.toISOString()); - return true; - } else { - notification.send(`任务未刷新,跳过每月兑换抽卡资源`); - return false; + + // // 如果文件内容无效或不存在,视为需要刷新 + // if (!contentList || isNaN(lastTime.getTime())) { + // //todo:写入也要改 contentList.put(uid, nowTime.toISOString()) + // // 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 (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; + } + } finally { + log.debug("contentList:", JSON.stringify(contentList)) } - } catch (error) { - // 如果文件不存在,创建新文件并返回true(视为需要刷新) - const createResult = await file.writeText(filePath, ''); + log.error(`刷新任务失败: ${error}`); + 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 +195,149 @@ 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) { + config.user.insufficient_exchange=true + 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, +}