Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
303 changes: 218 additions & 85 deletions repo/js/每月自动兑换抽卡资源/main.js
Original file line number Diff line number Diff line change
@@ -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 - 存储最后完成时间的文件路径
Expand All @@ -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;
Expand All @@ -53,15 +92,15 @@ async function isTaskRefreshed(filePath, options = {}) {
shouldRefresh = lastTime < yesterdayRefresh;
}
break;

case 'weekly': // 每周固定时间刷新
// 获取本周的刷新时间
const thisWeekRefresh = new Date(nowTime);
// 计算与本周指定星期几的差值
const dayDiff = (thisWeekRefresh.getDay() - weeklyDay + 7) % 7;
thisWeekRefresh.setDate(thisWeekRefresh.getDate() - dayDiff);
thisWeekRefresh.setHours(weeklyHour, 0, 0, 0);

// 如果当前时间已经过了本周的刷新时间
if (nowTime >= thisWeekRefresh) {
shouldRefresh = lastTime < thisWeekRefresh;
Expand All @@ -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;
Expand All @@ -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 = {});
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

递归调用参数错误。

options = {} 在函数调用中是赋值表达式,会将空对象赋给 options 变量,而非传递原有配置。应直接传递 options

🐛 修复递归调用参数
-            await isTaskRefreshed(filePath, options = {});
+            await isTaskRefreshed(filePath, options);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await isTaskRefreshed(filePath, options = {});
await isTaskRefreshed(filePath, options);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@repo/js/每月自动兑换抽卡资源/main.js` at line 175, The recursive call to
isTaskRefreshed is using a default assignment in the call site (await
isTaskRefreshed(filePath, options = {})) which overwrites the existing options
instead of passing them through; change the call to pass the options variable
directly (await isTaskRefreshed(filePath, options)) and ensure the caller
defines a sensible default for options before recursion or in the function
signature of isTaskRefreshed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

//1.1原有逻辑不改

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kirito520Asuna,明白了,保留 v1.1 的原有逻辑。感谢说明!

}
}
}


//检查是否为正整数
function positiveIntegerJudgment(testNumber) {
// 如果输入是字符串,尝试转换为数字
Expand All @@ -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)
Comment on lines +270 to +278
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

单项可兑场景会被固定的 750 阈值误判。

Line 266 已经允许“只找到一种球”继续执行,但 Line 271 到 Line 273 仍然固定要求至少 750 星尘。这样在只检测到一种球时,只要余额介于单项成本和 750 之间,脚本就会提前抛错,后面的实际兑换分支永远走不到。

💡 建议修法
-    let validatedMaterialQuantity = positiveIntegerJudgment(materialQuantity);
-    if (validatedMaterialQuantity < 750) {
-        throwError(`星尘数量为:${validatedMaterialQuantity},数量不足,无法全部兑换`, config.send_notification)
+    const validatedMaterialQuantity = positiveIntegerJudgment(materialQuantity);
+    const requiredMaterialQuantity =
+        (pinkBallExist ? 750 / 2 : 0) +
+        (blueBallExist ? 750 / 2 : 0);
+    if (validatedMaterialQuantity < requiredMaterialQuantity) {
+        throwError(
+            `星尘数量为:${validatedMaterialQuantity},需要至少 ${requiredMaterialQuantity} 才能完成本次兑换`,
+            config.send_notification
+        )
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@repo/js/每月自动兑换抽卡资源/main.js` around lines 266 - 273, The current check always
requires validatedMaterialQuantity >= 750 which misflags cases where only one
ball type is available; replace the hardcoded 750 with a computed threshold
based on which balls exist (e.g. if both pinkBallExist and blueBallExist require
750, otherwise require the single-item cost for the existing ball). In the block
using positiveIntegerJudgment(materialQuantity) and throwError(...,
config.send_notification), compute requiredQuantity = (pinkBallExist &&
blueBallExist) ? 750 : (pinkBallExist ? PINK_COST : BLUE_COST) (or obtain
PINK_COST/BLUE_COST from your cost constants), then compare
validatedMaterialQuantity < requiredQuantity and call throwError only when below
that computed requiredQuantity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

//不改

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kirito520Asuna 了解,保持现有的固定 750 阈值逻辑,不作修改。


🧠 Learnings used
Learnt from: Kirito520Asuna
Repo: babalae/bettergi-scripts-list PR: 3053
File: repo/js/每月自动兑换抽卡资源/main.js:50-62
Timestamp: 2026-03-30T07:11:26.628Z
Learning: 在 `repo/js/每月自动兑换抽卡资源/main.js` 中,`isTaskRefreshed` 读取 `contentList` 时不需要做旧格式兼容处理,因为脚本更新后会以初始空状态运行,不存在遗留的旧格式持久化文件。现有的 `try/catch` 已足够处理文件缺失或内容异常的情况。

Learnt from: Bedrockx
Repo: babalae/bettergi-scripts-list PR: 2949
File: repo/js/AccountSwitchStateMachine/main.js:778-786
Timestamp: 2026-03-02T05:57:39.347Z
Learning: 在 repo/js/AccountSwitchStateMachine/main.js 的 findAndClick 函数中,通过 file.ReadImageMatSync 加载的小图片 Mat 对象不需要手动释放,因为图片数量少且体积小,内存影响可以忽略。这是项目维护者认可的内存管理策略。

// 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()
Comment on lines +338 to +339
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "每月自动兑换抽卡资源" --type f

Repository: babalae/bettergi-scripts-list

Length of output: 55


🏁 Script executed:

find . -name "main.js" -type f | head -20

Repository: babalae/bettergi-scripts-list

Length of output: 844


🏁 Script executed:

find . -type d -name "*每月自动兑换*" -o -type f -path "*每月自动兑换*"

Repository: babalae/bettergi-scripts-list

Length of output: 427


🏁 Script executed:

wc -l "./repo/js/每月自动兑换抽卡资源/main.js"

Repository: babalae/bettergi-scripts-list

Length of output: 105


🏁 Script executed:

cat -n "./repo/js/每月自动兑换抽卡资源/main.js" | sed -n '300,335p'

Repository: babalae/bettergi-scripts-list

Length of output: 1265


🏁 Script executed:

rg "toMainUi" "./repo/js/每月自动兑换抽卡资源/" -A 3 -B 1

Repository: babalae/bettergi-scripts-list

Length of output: 1286


🏁 Script executed:

cat -n "./repo/js/每月自动兑换抽卡资源/utils/tool.js"

Repository: babalae/bettergi-scripts-list

Length of output: 2363


🏁 Script executed:

rg "isTaskRefreshed" "./repo/js/每月自动兑换抽卡资源/" -A 10

Repository: babalae/bettergi-scripts-list

Length of output: 2058


🏁 Script executed:

cat -n "./repo/js/每月自动兑换抽卡资源/main.js" | sed -n '35,150p'

Repository: babalae/bettergi-scripts-list

Length of output: 5338


🏁 Script executed:

cat -n "./repo/js/每月自动兑换抽卡资源/main.js" | sed -n '150,220p'

Repository: babalae/bettergi-scripts-list

Length of output: 2747


finally 块中使用 await 会掩盖主流程异常。

toMainUi() 会调用 throwError() 抛出错误。当 isTaskRefreshed() 失败时,其异常会被 finally 块中 toMainUi() 的错误覆盖,导致真实根因被隐藏。建议单独捕获 toMainUi() 的异常,仅当主流程抛出异常时才重新抛出它。

建议修法
 async function main() {
+    let mainError;
     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,
             monthlyHour: 4
         });
+    } catch (error) {
+        mainError = error;
+        throw error;
     } finally {
-        await toMainUi()
+        try {
+            await toMainUi()
+        } catch (uiError) {
+            if (!mainError) {
+                throw uiError;
+            }
+            log.error(`返回主界面失败: ${uiError}`);
+        }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@repo/js/每月自动兑换抽卡资源/main.js` around lines 330 - 331, The finally block
currently awaits toMainUi(), which can throw and mask any error from the main
flow (e.g., failures from isTaskRefreshed()); change the pattern to capture any
primary error before finally, then call toMainUi() inside the finally but catch
its error and rethrow the original if present: specifically, in the surrounding
try/catch, save the main exception (e.g., let primaryErr), then in finally do a
try { await toMainUi() } catch (uiErr) { if (primaryErr) throw primaryErr; else
throw uiErr; } so that errors from isTaskRefreshed() (and other main flow code)
are not overwritten by toMainUi() failures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

//忽略,toMainUi不会报错的

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kirito520Asuna,好的,已了解。尽管 toMainUi() 内部在超过三次尝试后确实会通过 throwError 抛出异常,但您认为在正常使用场景中该情况不会发生,因此选择保持当前实现。已记录此设计决策,感谢说明!


🧠 Learnings used
Learnt from: Kirito520Asuna
Repo: babalae/bettergi-scripts-list PR: 3053
File: repo/js/每月自动兑换抽卡资源/main.js:50-62
Timestamp: 2026-03-30T07:11:26.628Z
Learning: 在 `repo/js/每月自动兑换抽卡资源/main.js` 中,`isTaskRefreshed` 读取 `contentList` 时不需要做旧格式兼容处理,因为脚本更新后会以初始空状态运行,不存在遗留的旧格式持久化文件。现有的 `try/catch` 已足够处理文件缺失或内容异常的情况。

Learnt from: Kirito520Asuna
Repo: babalae/bettergi-scripts-list PR: 2947
File: repo/js/AutoPlan/main.js:93-106
Timestamp: 2026-03-01T09:25:32.216Z
Learning: 在 repo/js/AutoPlan/main.js 中,autoDomain 和 autoLeyLineOutcrop 函数的复活重试循环在耗尽 config.run.retry_count 次数后不会重新抛出错误,这是预期的正常逻辑设计,允许静默结束。

Learnt from: Bedrockx
Repo: babalae/bettergi-scripts-list PR: 2949
File: repo/js/AccountSwitchStateMachine/main.js:778-786
Timestamp: 2026-03-02T05:57:39.347Z
Learning: 在 repo/js/AccountSwitchStateMachine/main.js 的 findAndClick 函数中,通过 file.ReadImageMatSync 加载的小图片 Mat 对象不需要手动释放,因为图片数量少且体积小,内存影响可以忽略。这是项目维护者认可的内存管理策略。

}
}

})();
await main();
Loading