吃药统计更新 (#2838)

* 增加延迟

* 更新

* 解决正则表达式注入风险
This commit is contained in:
skyflag2022
2026-02-04 07:37:01 +08:00
committed by GitHub
parent c4f18a1148
commit adf1e0fb7b
4 changed files with 571 additions and 216 deletions

View File

@@ -1,12 +1,12 @@
### 药品消耗统计脚本使用指南(更新版)
### 药品消耗统计脚本使用说明
## 📋 一、脚本概述
本脚本是专为《原神》设计的自动化药品消耗统计工具,核心能力升级如下:
- **多模式识别**:支持「营养袋模式」「筛选模式」双模式适配不同场景的药品识别
- **精准OCR读取**:自动识别背包中指定回血/复活药品数量,适配不同界面布局
- **多模式识别**:支持「营养袋模式」「筛选模式」「综合模式」三模式适配不同场景的药品识别
- **精准OCR读取**:自动识别背包中指定回血/复活/攻击/防御/其他药品数量,适配不同界面布局
- **灵活时间配置**支持自定义每日统计刷新时间默认4:00可设0-24小时及一位小数
- **智能数据管理**自动保留30天历史记录按自定义时间周期统计药品消耗/新增
- **多账户隔离**不同账户数据独立存储支持合规账户名校验1-20位中英文/数字)
- **异常友好处理**:识别失败自动兜底、参数错误智能修正,拒绝进入世界的申请
- **模块化设计**:代码结构优化,新增攻击药、防御药、其他药支持
## 🛠️ 二、环境与工具要求
### 必备工具茶包版BGI
@@ -25,10 +25,13 @@
## ⚙️ 四、全参数配置说明(新增/细化)
| 参数项 | 类型 | 填写要求 | 默认值 | 核心说明 |
|--------|------|----------|--------|----------|
| **runMode** | 下拉选择 | 选一:「营养袋模式」/「筛选模式」 | 营养袋模式 | 营养袋模式:读取便携营养袋内药品;筛选模式:通过背包筛选搜索药品 |
| **runMode** | 下拉选择 | 选一:「营养袋模式」/「筛选模式」/「综合模式」 | 营养袋模式 | 营养袋模式:读取便携营养袋内药品;筛选模式:通过背包筛选搜索药品;综合模式:营养袋模式+筛选模式结合 |
| **initSelect** | 复选框 | 仅初始化/重置时临时勾选 | false | 勾选后运行脚本会删除当日同名记录,重新初始化数据(使用后务必取消) |
| **recoveryFoodName** | 文本输入 | 与游戏内回血药名称完全一致 | 空 | 例:「美味的甜甜花酿鸡」,名称错误会导致识别失败 |
| **resurrectionFoodName** | 文本输入 | 与游戏内复活药名称完全一致 | 空 | 例:「美味的提瓦特煎蛋」,支持全量游戏内可食用复活类物品 |
| **attackFoodName** | 文本输入 | 与游戏内攻击药名称完全一致 | 空 | 例:「美味的堆高高」,支持全量游戏内攻击增益类物品 |
| **defenseFoodName** | 文本输入 | 与游戏内防御药名称完全一致 | 空 | 例:「美味的贝壳彩糖」,支持全量游戏内防御增益类物品 |
| **otherFoodName** | 文本输入 | 与游戏内其他药名称完全一致 | 空 | 例:「美味的风神杂烩菜」,支持全量游戏内其他增益类物品 |
| **userName** | 文本输入 | 1-20字符仅支持中英文、数字 | 默认账户 | 多账户区分核心,违规名称自动替换为默认账户 |
| **refreshTime** | 文本输入 | 0-24之间支持一位小数 | 4.0 | 自定义每日统计周期分界点如4.5=4:30错误值自动修正为4.0 |
| **loadDelay** | 文本输入 | 非负整数(单位:毫秒) | 800 | 界面打开/切换的等待延迟,低配置设备可适当增大 |
@@ -54,11 +57,18 @@
#### 筛选模式
- 脚本自动打开背包→进入食物分类→筛选搜索指定药品→读取数量
- 无需装备营养袋,适配未配置营养袋的场景,识别流程稍长
- 只处理回血药和复活药
#### 综合模式
- 脚本自动执行:营养袋模式识别回血/复活药 + 筛选模式识别攻击/防御/其他药
- 结合两种模式优势,支持全类型药品统计
- 回血药和复活药通过营养袋模式获取,攻击药、防御药、其他药通过筛选模式获取
#### 通用规则
- 每日首次运行自定义refreshTime后记录为当日初始值
- 后续运行:对比初始值计算消耗/新增(正数=消耗,负数=新增)
- 每次运行自动保存记录仅保留30天内数据过期自动清理
- 只记录数量大于0的药品数据
### 场景三:更换药品/重置统计数据
1. 在设置中勾选`initSelect`选项
@@ -77,8 +87,16 @@
```
时间:202X/XX/XX XX:XX:XX-【药品名称】-【数量】
时间:202X/XX/XX XX:XX:XX-【药品名称】-【数量】
...
```
每条记录包含时间戳+药品名+数量,双药品记录成对生成,保证数据完整性。
每条记录包含时间戳+药品名+数量,支持5种药品类型
- 回血药
- 复活药
- 攻击药
- 防御药
- 其他药
**注意**只生成数量大于0的药品记录保证数据有效性。
### 3. OCR识别异常处理
| 异常场景 | 处理逻辑 | 通知/日志 |
@@ -87,6 +105,7 @@
| 药品名称为空 | 识别结果无效 | log.warn「XX药名字没填」 |
| refreshTime非法 | 自动修正为4.0 | log.warn「刷新时间设置错误使用默认值4.0」 |
| 账户名违规 | 替换为默认账户 | log.error「账户名XX违规使用默认账户」 |
| 数量为0 | 不记录数据 | 只记录数量大于0的药品数据通知中只显示有效数据 |
## ⚠️ 七、重要注意事项(强化)
### 1. 运行时机建议
@@ -94,13 +113,20 @@
- 避免在两次脚本运行之间大批量制作/使用药品,防止统计偏差
- 低配置设备适当增大loadDelay/stepDelay提升识别成功率
### 2. 模式适配要点
### 2. 模式适配要点
| 模式 | 优势 | 注意事项 |
|------|------|----------|
| 营养袋模式 | 识别快、步骤少 | 必须装备便携营养袋,仅识别营养袋内药品 |
| 筛选模式 | 无需装备营养袋 | 药品名称必须精准,依赖背包筛选功能正常 |
| 筛选模式 | 无需装备营养袋 | 药品名称必须精准,依赖背包筛选功能正常,只处理回血药和复活药 |
| 综合模式 | 支持全类型药品统计 | 结合营养袋模式和筛选模式,需要装备便携营养袋 |
### 3. 常见误区修正
### 3. 建议搭配JS通知使用
- **推荐使用JS通知**:本脚本会发送各类通知,包括初始化完成、使用情况、异常提醒等
- **通知好处**:及时获取统计结果,不错过重要信息,方便日常使用
- **确保通知功能正常**请确保BGI的JS通知功能已开启以便接收脚本发送的各类通知
- **通知内容**:包含账户信息、药品使用情况、库存信息等,格式清晰易读
### 4. 常见误区修正
❌ 错误:`initSelect`长期勾选 → 每次运行都重置数据,无法统计消耗
✅ 正确:仅重置/换药品时勾选,运行后立即取消
@@ -113,8 +139,9 @@
## 🔧 八、故障排查指南(新增模式/参数排查)
| 问题现象 | 优先检查项 | 解决方案 |
|----------|------------|----------|
| 药品数量识别为0 | 1. 运行模式是否匹配场景<br>2. 药品名称是否完全一致<br>| 1. 营养袋模式需装备营养袋;筛选模式检查背包食物分类<br>2. 复制游戏内药品全名(含「美味的/冷的」等前缀)<br>|
| 药品数量识别为0 | 1. 运行模式是否匹配场景<br>2. 药品名称是否完全一致<br>3. 综合模式是否装备了营养袋 | 1. 营养袋模式需装备营养袋;筛选模式检查背包食物分类<br>2. 复制游戏内药品全名(含「美味的/冷的」等前缀)<br>3. 综合模式需要装备便携营养袋 |
| 统计周期错误 | 1. refreshTime是否合法<br>2. 系统时间是否准确 | 1. 确认值在0-24之间如4.0/12.5),错误值会自动修正<br>2. 同步系统时间到网络标准时间 |
| 脚本运行卡顿/超时 | 1. loadDelay/stepDelay是否过小<br>2. 设备性能是否不足 | 1. 逐步增大延迟值如loadDelay改为1000<br>2. 关闭后台无关程序,保证游戏前台运行 |
| 脚本运行卡顿/超时 | 1. loadDelay/stepDelay是否过小<br>2. 设备性能是否不足<br>3. 综合模式下是否需要更长延迟 | 1. 逐步增大延迟值如loadDelay改为1000<br>2. 关闭后台无关程序,保证游戏前台运行<br>3. 综合模式包含更多步骤,可适当增大延迟 |
| 部分药品未记录 | 1. 药品名称是否填写<br>2. 药品数量是否大于0<br>3. 对应模式是否支持该类型药品 | 1. 检查对应药品名称是否正确填写<br>2. 只记录数量大于0的药品数据<br>3. 筛选模式只处理回血药和复活药,攻击/防御/其他药需使用综合模式 |
| 多账户数据混淆 | 1. userName是否唯一<br>2. 记录文件是否存在重名 | 1. 为每个账户设置唯一名称如「旅行者001/旅行者002」<br>2. 检查assets目录删除重名的错误记录文件 |
| 模式切换后识别失败 | 1. 对应模式的模板图片是否存在<br>2. 点击坐标是否匹配分辨率 | 1. 确认assets目录有「营养袋.png」「筛选1.png」「筛选2.png」等<br>2. 重新确认游戏分辨率为1920×1080 |
| 模式切换后识别失败 | 1. 对应模式的模板图片是否存在<br>2. 点击坐标是否匹配分辨率<br>3. 综合模式是否包含所有必要的模板图片 | 1. 确认assets目录有「营养袋.png」「筛选1.png」「筛选2.png」等<br>2. 重新确认游戏分辨率为1920×1080<br>3. 综合模式需要营养袋和筛选相关的所有模板图片 |

View File

@@ -1,7 +1,10 @@
let userName = settings.userName || "默认账户";
const mode = settings.runMode || "营养袋模式"
let recoveryFoodName = settings.recoveryFoodName || "回血药名字没填";
let resurrectionFoodName = settings.resurrectionFoodName || "复活药名字没填";
let recoveryFoodName = settings.recoveryFoodName || "";
let resurrectionFoodName = settings.resurrectionFoodName || "";
let attackFoodName = settings.attackFoodName || "";
let defenseFoodName = settings.defenseFoodName || "";
let otherFoodName = settings.otherFoodName || "";
const ocrRegion = {
x: 1422,
y: 586,
@@ -31,11 +34,16 @@ if (isNaN(refreshTime) || refreshTime < 0 || refreshTime >= 24) {
const refreshHour = Math.floor(refreshTime);
const refreshMinute = Math.floor((refreshTime - refreshHour) * 60);
log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0')}`);
// 正则特殊字符转义函数
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
(async function () {
// 检验账户名
async function getUserName() {
userName = userName.trim();
// 数字中英文,长度20字符以内
// 账户名规则:数字中英文,长度1-20字符
if (!userName || !/^[\u4e00-\u9fa5A-Za-z0-9]{1,20}$/.test(userName)) {
log.error(`账户名${userName}违规暂时使用默认账户名请查看readme后修改`)
userName = "默认账户";
@@ -81,16 +89,22 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
* 获取本地记录中当天4点至次日4点间的最早记录
* @param {string} filePath - 记录文件路径
* @returns {Promise<object>} 包含药品数据的对象
* 格式: { recovery: { count }, resurrection: { count }, initialized: { recovery, resurrection } }
* 格式: { recovery: { count }, resurrection: { count }, attack: { count }, defense: { count }, other: { count }, initialized: { recovery, resurrection, attack, defense, other } }
*/
async function getLocalData(filePath) {
// 初始化返回结果
const result = {
recovery: null,
resurrection: null,
attack: null,
defense: null,
other: null,
initialized: {
recovery: false,
resurrection: false
resurrection: false,
attack: false,
defense: false,
other: false
}
};
@@ -124,8 +138,11 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
// 时间格式正则:匹配 "时间:YYYY/MM/DD HH:mm:ss"
const timeRegex = /时间:(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2})/;
// 药品匹配正则
const recoveryRegex = new RegExp(`${recoveryFoodName}-(\\d+)`);
const resurrectionRegex = new RegExp(`${resurrectionFoodName}-(\\d+)`);
const recoveryRegex = new RegExp(`${escapeRegExp(recoveryFoodName)}-(\\d+)`);
const resurrectionRegex = new RegExp(`${escapeRegExp(resurrectionFoodName)}-(\\d+)`);
const attackRegex = new RegExp(`${escapeRegExp(attackFoodName)}-(\\d+)`);
const defenseRegex = new RegExp(`${escapeRegExp(defenseFoodName)}-(\\d+)`);
const otherRegex = new RegExp(`${escapeRegExp(otherFoodName)}-(\\d+)`);
// 正向遍历找到第一个小于startTime的行索引边界
let firstOutOfRangeIndex = -1; // 初始化为-1表示所有行都在时间范围内
@@ -155,6 +172,9 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
// 反向遍历的终止索引0顶部
const reverseEndIndex = 0;
// 根据当前模式确定需要处理的药品类型
const needAttackDefenseOther = mode === "综合模式";
// 反向遍历:找时间范围内最早的药品记录
// 遍历范围:[reverseStartIndex, reverseEndIndex](从时间范围的最旧→最新)
for (let i = reverseStartIndex; i >= reverseEndIndex; i--) {
@@ -189,8 +209,39 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
}
}
// 两个药品都找到,提前终止遍历(已拿到最早记录)
if (result.initialized.recovery && result.initialized.resurrection) {
// 匹配攻击药:未初始化时才赋值,仅在综合模式下处理
if (needAttackDefenseOther && !result.initialized.attack) {
const attackMatch = line.match(attackRegex);
if (attackMatch) {
result.attack = { count: parseInt(attackMatch[1]) };
result.initialized.attack = true;
}
}
// 匹配防御药:未初始化时才赋值,仅在综合模式下处理
if (needAttackDefenseOther && !result.initialized.defense) {
const defenseMatch = line.match(defenseRegex);
if (defenseMatch) {
result.defense = { count: parseInt(defenseMatch[1]) };
result.initialized.defense = true;
}
}
// 匹配其他药:未初始化时才赋值,仅在综合模式下处理
if (needAttackDefenseOther && !result.initialized.other) {
const otherMatch = line.match(otherRegex);
if (otherMatch) {
result.other = { count: parseInt(otherMatch[1]) };
result.initialized.other = true;
}
}
// 所有需要的药品都找到,提前终止遍历(已拿到最早记录)
let allFound = result.initialized.recovery && result.initialized.resurrection;
if (needAttackDefenseOther) {
allFound = allFound && result.initialized.attack && result.initialized.defense && result.initialized.other;
}
if (allFound) {
break;
}
}
@@ -201,7 +252,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
}
}
async function updateRecord(filePath, currentRecovery, currentResurrection, deleteSameDayRecords = false) {
async function updateRecord(filePath, currentRecovery, currentResurrection, currentAttack, currentDefense, currentOther, deleteSameDayRecords = false) {
// 生成当前时间字符串
const now = new Date();
const timeStr = `${now.getFullYear()}/${
@@ -216,9 +267,37 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
String(now.getSeconds()).padStart(2, '0')
}`;
// 生成两条新记录
const recoveryLine = `时间:${timeStr}-${recoveryFoodName}-${currentRecovery}`;
const resurrectionLine = `时间:${timeStr}-${resurrectionFoodName}-${currentResurrection}`;
// 根据当前模式确定需要处理的药品类型
const needAttackDefenseOther = mode === "综合模式";
// 基础药品:回血药和复活药
const baseDrugs = [
{ name: recoveryFoodName, count: currentRecovery },
{ name: resurrectionFoodName, count: currentResurrection }
];
// 根据模式确定要处理的药品列表
let drugs = [...baseDrugs];
// 只在综合模式下添加攻击药、防御药和其他药
if (needAttackDefenseOther) {
drugs = drugs.concat([
{ name: attackFoodName, count: currentAttack },
{ name: defenseFoodName, count: currentDefense },
{ name: otherFoodName, count: currentOther }
]);
}
// 生成记录只包含name不为空且数量>0的数据
const records = drugs
.filter(drug => drug.name.trim() && drug.count > 0)
.map(drug => `时间:${timeStr}-${drug.name}-${drug.count}`);
// 如果没有需要记录的数据,直接返回
if (records.length === 0) {
log.info("没有需要记录的数据");
return true;
}
try {
let content = await file.readText(filePath);
@@ -226,7 +305,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
if (lines.length === 0) {
// 文件为空,直接写入新记录
await file.writeText(filePath, `${recoveryLine}\n${resurrectionLine}`);
await file.writeText(filePath, records.join('\n'));
return true;
}
@@ -251,9 +330,24 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
endTime.setDate(endTime.getDate() + 1);
}
// 创建药品匹配正则
const recoveryRegex = new RegExp(`${recoveryFoodName}-\\d+$`);
const resurrectionRegex = new RegExp(`${resurrectionFoodName}-\\d+$`);
// 根据当前模式确定需要处理的药品类型
const needAttackDefenseOther = mode === "综合模式";
// 基础药品:回血药和复活药
const baseDrugs = [recoveryFoodName, resurrectionFoodName];
// 根据模式确定要处理的药品列表
let drugs = [...baseDrugs];
// 只在综合模式下添加攻击药、防御药和其他药
if (needAttackDefenseOther) {
drugs = drugs.concat([attackFoodName, defenseFoodName, otherFoodName]);
}
// 创建药品匹配正则,只处理需要记录的药品
const regexList = drugs
.filter(name => name.trim())
.map(name => new RegExp(`${escapeRegExp(name)}-\\d+$`));
// 过滤掉当天时间范围内的同名记录
lines = lines.filter(line => {
@@ -263,9 +357,11 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
const recordTime = new Date(timeMatch[1]);
// 检查是否在当天时间范围内
if (recordTime >= startTime && recordTime < endTime) {
// 检查是否为回血药或复活药记录
if (recoveryRegex.test(line) || resurrectionRegex.test(line)) {
return false; // 删除该记录
// 检查是否为需要记录的药品记录
for (const regex of regexList) {
if (regex.test(line)) {
return false; // 删除该记录
}
}
}
return true; // 保留该记录
@@ -273,8 +369,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
}
// 添加新记录到最前面
lines.unshift(resurrectionLine);
lines.unshift(recoveryLine);
lines.unshift(...records);
// 只保留30天内的记录
const thirtyDaysAgo = new Date();
@@ -292,7 +387,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
return true;
} catch (error) {
// 文件不存在时创建新文件
await file.writeText(filePath, `${recoveryLine}\n${resurrectionLine}`);
await file.writeText(filePath, records.join('\n'));
return true;
}
}
@@ -380,7 +475,7 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
}
}
return { name: null, count: null };
}
}
async function findAndClick(target, doClick = true, maxAttempts = 60) {
for (let i = 0; i < maxAttempts; i++) {
@@ -402,9 +497,119 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
return await findAndClick(pngRo, doClick, maxAttempts);
}
// 生成药品描述的函数
async function generateDrugDescription(drugName, diffValue,changes) {
if (!drugName.trim()) return;
let desc = "";
if (diffValue > 0) {
desc = `- ${drugName}:消耗 ${diffValue}`;
} else if (diffValue < 0) {
desc = `- ${drugName}:新增 ${-diffValue}`;
} else {
desc = `- ${drugName}:无变化`;
}
changes.push(desc);
}
async function main() {
let recoveryNumber = 0;
let resurrectionNumber = 0;
let attackNumber = 0;
let defenseNumber = 0;
let otherNumber = 0;
// 进入界面的通用函数
async function enterInterface(interfaceType, maxRetries = 5) {
let retryCount = 0;
let successClick = false;
while (retryCount < maxRetries && !successClick) {
retryCount++;
await close_join_world_popup_window();
if (interfaceType === 'nutrition_bag') {
// 营养袋模式:进入小道具界面
click(1051, 51); // 选择小道具
await sleep(loadDelay);
if (await clickPNG('拒绝', 3)) {
log.info("检测到进入世界申请,已拒绝,重新尝试点击分类标签");
await sleep(stepDelay);
continue;
}
if (await clickPNG('营养袋', 1, false)) {
successClick = true;
log.info("成功进入小道具界面");
break;
}
} else if (interfaceType === 'filter') {
// 筛选模式:进入食物界面
click(863, 51); // 选择食物
await sleep(loadDelay);
if (await clickPNG('拒绝', 3)) {
log.info("检测到进入世界申请,已拒绝,重新尝试点击分类标签");
await sleep(stepDelay);
continue;
}
if (await clickPNG('筛选1', 1, false) || await clickPNG('筛选2', 1, false)) {
successClick = true;
log.info("成功进入食物界面");
break;
}
}
log.warn(`尝试点击分类标签失败,第${retryCount}次重试`);
await sleep(stepDelay);
}
return successClick;
}
// 搜索和识别药品的通用函数
async function searchAndRecognizeDrug(drugName, drugType) {
if (!drugName.trim()) return 0;
await clickPNG('筛选1', 1);
await clickPNG('筛选2', 1);
await clickPNG('重置');
await sleep(stepDelay);
await clickPNG('搜索');
await sleep(loadDelay);
log.info(`搜索${drugName}`);
inputText(drugName);
await clickPNG('确认筛选');
await sleep(loadDelay);
const count = await recognizeNumberByOCR(ocrRegion2, /\d+/) || 0;
if (count === 0) {
notification.send(`【营养袋吃药统计】\n未识别到${drugType}数量\n药品名:${drugName}\n设置数量为0`);
}
return count;
}
// 营养袋药品识别的通用函数
async function recognizeNutritionBagDrug(ocrRegionId, pattern, drugType) {
let result = await recognizeFoodItemByOCR(ocrRegionId, pattern);
if (result.name && result.count !== null) {
log.info(`识别到: ${result.name}, 份数: ${result.count}`);
} else {
log.warn(`未识别到有效的${drugType}信息`);
}
const count = result.count || 0;
const name = result.name || `未识别到${drugType}名称`;
if (count === 0) {
notification.send(`【营养袋吃药统计】\n未识别到${drugType}数量\n药品名:${name}\n设置数量为0`);
}
return { count, name };
}
// 设置分辨率和缩放
setGameMetrics(1920, 1080, 1);
await genshin.returnMainUi();
@@ -414,221 +619,325 @@ log.info(`刷新时间为: ${refreshHour}:${String(refreshMinute).padStart(2, '0
await close_expired_stuff_popup_window();
await close_join_world_popup_window();
await sleep(loadDelay);
// 打开界面
let maxRetries = 5; // 最大重试次数
let retryCount = 0;
let successClick = false;
// 根据模式选择点击的位置
let clickX, clickY;
if (mode === "营养袋模式") {
clickX = 1051; // 选择小道具
clickY = 51;
if (mode === "综合模式") {
// 综合模式:回血药和复活药通过营养袋模式获取,攻击药和防御药通过筛选模式获取
// 1. 先处理营养袋模式(识别回血药和复活药)
const successClick = await enterInterface('nutrition_bag');
if (successClick) {
await clickPNG('营养袋', 1);
await sleep(loadDelay);
const pattern = /(.+?)\s*[\(](\d+)[份\s]*[\)]/;
// 使用模块化函数识别各种药品
const recoveryResult = await recognizeNutritionBagDrug(ocrRegion, pattern, '回血药');
recoveryNumber = recoveryResult.count;
recoveryFoodName = recoveryResult.name;
const resurrectionResult = await recognizeNutritionBagDrug(ocrRegion1, pattern, '复活药');
resurrectionNumber = resurrectionResult.count;
resurrectionFoodName = resurrectionResult.name;
}
// 2. 然后处理筛选模式(识别攻击药和防御药,只有填了名字才筛选)
// 检查是否需要进行筛选(攻击药、防御药或其他药名字已填)
const needFilter = !!attackFoodName.trim() || !!defenseFoodName.trim() || !!otherFoodName.trim();
if (needFilter) {
const successClick = await enterInterface('filter');
if (successClick) {
// 使用模块化函数识别各种药品
attackNumber = await searchAndRecognizeDrug(attackFoodName, '攻击药');
defenseNumber = await searchAndRecognizeDrug(defenseFoodName, '防御药');
otherNumber = await searchAndRecognizeDrug(otherFoodName, '其他药');
// 重置筛选
await clickPNG('筛选1', 1);
await clickPNG('筛选2', 1);
await clickPNG('重置');
await sleep(stepDelay);
await clickPNG('确认筛选');
}
}
} else if (mode === "营养袋模式") {
// 使用通用进入界面函数
const successClick = await enterInterface('nutrition_bag');
if (successClick) {
await clickPNG('营养袋', 1);
await sleep(loadDelay);
const pattern = /(.+?)\s*[\(](\d+)[份\s]*[\)]/;
// 使用模块化函数识别各种药品
const recoveryResult = await recognizeNutritionBagDrug(ocrRegion, pattern, '回血药');
recoveryNumber = recoveryResult.count;
recoveryFoodName = recoveryResult.name;
const resurrectionResult = await recognizeNutritionBagDrug(ocrRegion1, pattern, '复活药');
resurrectionNumber = resurrectionResult.count;
resurrectionFoodName = resurrectionResult.name;
}
} else if (mode === "筛选模式") {
clickX = 863; // 选择食物
clickY = 51;
}
while (retryCount < maxRetries && !successClick) {
retryCount++;
await close_join_world_popup_window();
click(clickX, clickY);
await sleep(loadDelay);
// 检查是否进入了申请界面(通过查找"拒绝"按钮)
if (await clickPNG('拒绝', 3)) { // 找到拒绝按钮,说明在申请界面
log.info("检测到进入世界申请,已拒绝,重新尝试点击分类标签");
// 筛选模式:只处理回血药和复活药
// 使用通用进入界面函数
const successClick = await enterInterface('filter');
if (successClick) {
// 使用模块化函数识别各种药品
recoveryNumber = await searchAndRecognizeDrug(recoveryFoodName, '回血药');
resurrectionNumber = await searchAndRecognizeDrug(resurrectionFoodName, '复活药');
// 重置筛选
await clickPNG('筛选1', 1);
await clickPNG('筛选2', 1);
await clickPNG('重置');
await sleep(stepDelay);
continue; // 继续下一次循环
await clickPNG('确认筛选');
}
if (mode === "营养袋模式") {
if (await clickPNG('营养袋', 1, false)) { // 只检查不点击
successClick = true;
log.info("成功进入小道具界面");
break;
}
} else if (mode === "筛选模式") {
if (await clickPNG('筛选1', 1, false)||await clickPNG('筛选2', 1, false)) { // 只检查不点击
successClick = true;
log.info("成功进入食物界面");
break;
}
}
log.warn(`尝试点击分类标签失败,第${retryCount}次重试`);
await sleep(stepDelay);
}
if (!successClick) {
log.error("多次尝试点击分类标签失败,脚本终止");
return { recoveryNumber, resurrectionNumber};
}
if (mode === "营养袋模式") {
// 营养袋模式
await clickPNG('营养袋', 1);
await sleep(loadDelay);
const pattern = /(.+?)\s*[\(](\d+)[份\s]*[\)]/;
// 识别回血药
let result = await recognizeFoodItemByOCR(ocrRegion, pattern);
if (result.name && result.count !== null) {
log.info(`识别到: ${result.name}, 份数: ${result.count}`);
} else {
log.warn("未识别到有效的回血药信息");
}
recoveryNumber = result.count; // 识别回血药数量
recoveryFoodName = result.name || '未识别到回血药名称'; // 如果识别失败使用settings中的名字
// 处理回血药识别结果
if (recoveryNumber === null) {
recoveryNumber = 0;
notification.send(`未识别到回血药数量设置数量为0药品名${recoveryFoodName}`);
}
// 识别复活药
result = await recognizeFoodItemByOCR(ocrRegion1, pattern);
if (result.name && result.count !== null) {
log.info(`识别到: ${result.name}, 份数: ${result.count}`);
} else {
log.warn("未识别到有效的复活药信息");
}
resurrectionNumber = result.count; // 识别复活药数量
resurrectionFoodName = result.name || '未识别到复活药名称'; // 如果识别失败使用settings中的名字
// 处理复活药识别结果
if (resurrectionNumber === null) {
resurrectionNumber = 0;
notification.send(`未识别到复活药数量设置数量为0药品名${resurrectionFoodName}`);
}
} else if (mode === "筛选模式") {
// 食物筛选模式
// 先识别回血药
await clickPNG('筛选1', 1);
await clickPNG('筛选2', 1);
await clickPNG('重置');
await sleep(stepDelay);
await clickPNG('搜索');
await sleep(loadDelay);
log.info(`搜索${recoveryFoodName}`);
inputText(recoveryFoodName);
await clickPNG('确认筛选');
await sleep(loadDelay);
recoveryNumber = await recognizeNumberByOCR(ocrRegion2, /\d+/); // 识别回血药数量
// 处理回血药识别结果
if (recoveryNumber === null) {
recoveryNumber = 0;
notification.send(`未识别到回血药数量设置数量为0药品名${recoveryFoodName}`);
await sleep(5000);
click(863, 51); // 选择食物
await sleep(1000);
}
// 重置筛选,识别复活药
await clickPNG('筛选1', 1);
await clickPNG('筛选2', 1);
await clickPNG('重置');
await sleep(stepDelay);
await clickPNG('搜索');
await sleep(loadDelay);
log.info(`搜索${resurrectionFoodName}`);
inputText(resurrectionFoodName);
await clickPNG('确认筛选');
await sleep(loadDelay);
resurrectionNumber = await recognizeNumberByOCR(ocrRegion2, /\d+/); // 识别复活药数量
// 处理复活药识别结果
if (resurrectionNumber === null) {
resurrectionNumber = 0;
notification.send(`未识别到复活药数量设置数量为0药品名${resurrectionFoodName}`);
await sleep(5000);
click(863, 51); // 选择食物
await sleep(1000);
}
// 重置筛选
await clickPNG('筛选1', 1);
await clickPNG('筛选2', 1);
await clickPNG('重置');
await sleep(stepDelay);
await clickPNG('确认筛选');
}
await genshin.returnMainUi();
return { recoveryNumber, resurrectionNumber };
return { recoveryNumber, resurrectionNumber, attackNumber, defenseNumber, otherNumber };
}
// 主执行流程
userName = await getUserName();
const recordPath = `assets/${userName}.txt`;
// 获取当前药物数量
const { recoveryNumber, resurrectionNumber } = await main();
const { recoveryNumber, resurrectionNumber, attackNumber, defenseNumber, otherNumber } = await main();
// 获取本地保存的数据
const localData = await getLocalData(recordPath);
// 确定初始化数据
let initRecovery, initResurrection;
let initRecovery, initResurrection, initAttack, initDefense, initOther;
let useLocalDataAsInit = false;
if (localData.initialized.recovery && localData.initialized.resurrection) {
// 情况1两者都有
initRecovery = localData.recovery.count;
initResurrection = localData.resurrection.count;
// 检查本地数据初始化情况只处理name不为空的数据
const hasLocalRecovery = recoveryFoodName.trim() && localData.initialized.recovery;
const hasLocalResurrection = resurrectionFoodName.trim() && localData.initialized.resurrection;
const hasLocalAttack = attackFoodName.trim() && localData.initialized.attack;
const hasLocalDefense = defenseFoodName.trim() && localData.initialized.defense;
const hasLocalOther = otherFoodName.trim() && localData.initialized.other;
// 根据当前模式确定需要处理的药品类型
const needAttackDefenseOther = mode === "综合模式";
// 计算有效药品数量name不为空的药品只考虑当前模式下需要处理的药品
const baseFoods = [recoveryFoodName, resurrectionFoodName];
const allFoods = needAttackDefenseOther
? [...baseFoods, attackFoodName, defenseFoodName, otherFoodName]
: baseFoods;
const validFoodCount = allFoods.filter(name => name.trim()).length;
// 计算已读取到本地数据的有效药品数量,只考虑当前模式下需要处理的药品
const baseLoaded = [hasLocalRecovery, hasLocalResurrection];
const allLoaded = needAttackDefenseOther
? [...baseLoaded, hasLocalAttack, hasLocalDefense, hasLocalOther]
: baseLoaded;
const loadedFoodCount = allLoaded.filter(Boolean).length;
if (validFoodCount > 0 && validFoodCount === loadedFoodCount) {
// 情况1所有有效药品name不为空都有本地数据
initRecovery = hasLocalRecovery ? localData.recovery.count : recoveryNumber;
initResurrection = hasLocalResurrection ? localData.resurrection.count : resurrectionNumber;
initAttack = hasLocalAttack ? localData.attack.count : attackNumber;
initDefense = hasLocalDefense ? localData.defense.count : defenseNumber;
initOther = hasLocalOther ? localData.other.count : otherNumber;
useLocalDataAsInit = true;
log.info(`已读取到本地数据`)
} else if (localData.initialized.recovery || localData.initialized.resurrection) {
// 情况2一有一无用有的那个缺的用当前数据
initRecovery = localData.initialized.recovery ? localData.recovery.count : recoveryNumber;
initResurrection = localData.initialized.resurrection ? localData.resurrection.count : resurrectionNumber;
log.info(`未读取到全部的本地数据,缺失部分使用当前数据作为初始数据`)
} else {
// 情况3两者都无使用当前数据
initRecovery = recoveryNumber;
initResurrection = resurrectionNumber;
log.info(`未读取到本地数据,使用当前数据作为初始数据`)
// 情况2部分有部分无用有的那个缺的用当前数据
// 情况3全部本地数据都没有所有药品都使用当前数据作为初始数据
initRecovery = hasLocalRecovery ? localData.recovery.count : recoveryNumber;
initResurrection = hasLocalResurrection ? localData.resurrection.count : resurrectionNumber;
initAttack = hasLocalAttack ? localData.attack.count : attackNumber;
initDefense = hasLocalDefense ? localData.defense.count : defenseNumber;
initOther = hasLocalOther ? localData.other.count : otherNumber;
if (loadedFoodCount === 0) {
log.info(`未读取到本地数据,所有药品使用当前数据作为初始数据`)
} else {
log.info(`未读取到全部的本地数据,缺失部分使用当前数据作为初始数据`)
}
}
// 判断是否需要写入两个数据都不为0时才写入
const shouldWriteRecord = recoveryNumber > 0 && resurrectionNumber > 0;
// 判断是否需要写入(只写入填了名字的药品)
const shouldWriteRecovery = recoveryFoodName.trim() && recoveryNumber > 0;
const shouldWriteResurrection = resurrectionFoodName.trim() && resurrectionNumber > 0;
const shouldWriteAttack = attackFoodName.trim() && attackNumber > 0;
const shouldWriteDefense = defenseFoodName.trim() && defenseNumber > 0;
const shouldWriteOther = otherFoodName.trim() && otherNumber > 0;
const shouldWriteRecord = shouldWriteRecovery || shouldWriteResurrection || shouldWriteAttack || shouldWriteDefense || shouldWriteOther;
// initSelect处理逻辑
if (settings.initSelect && shouldWriteRecord) {
// 强制初始化:初始化数量和最后一次运行数量都设为当前值
await updateRecord(recordPath, recoveryNumber, resurrectionNumber,deleteSameDayRecords=true);
notification.send(`${userName}: 强制初始化完成!${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`);
await updateRecord(recordPath, recoveryNumber, resurrectionNumber, attackNumber, defenseNumber, otherNumber, deleteSameDayRecords=true);
// 构建通知消息
let initMsg = `【营养袋吃药统计】\n\n`;
initMsg += `📋 强制初始化完成!\n`;
initMsg += `👤 账户:${userName}\n\n`;
initMsg += `📊 初始药品数据:\n`;
let items = [];
if (shouldWriteRecovery) items.push(`- ${recoveryFoodName}${recoveryNumber}`);
if (shouldWriteResurrection) items.push(`- ${resurrectionFoodName}${resurrectionNumber}`);
if (shouldWriteAttack) items.push(`- ${attackFoodName}${attackNumber}`);
if (shouldWriteDefense) items.push(`- ${defenseFoodName}${defenseNumber}`);
if (shouldWriteOther) items.push(`- ${otherFoodName}${otherNumber}`);
initMsg += items.join('\n');
notification.send(initMsg);
// 添加简单格式的日志记录
let initItemsSummary = items.map(item => item.replace(/- /g, "")).join(", ");
log.info(`${userName}:强制初始化完成|当前库存:${initItemsSummary}`);
return
}
if (shouldWriteRecord) {
// 使用当前的数据更新记录
await updateRecord(recordPath, recoveryNumber, resurrectionNumber);
await updateRecord(recordPath, recoveryNumber, resurrectionNumber, attackNumber, defenseNumber, otherNumber);
// 本地有初始记录
if(useLocalDataAsInit){
// 计算消耗/增加数量
const diffRecovery = initRecovery - recoveryNumber;
const diffResurrection = initResurrection - resurrectionNumber;
// 根据当前模式确定需要处理的药品类型
const needAttackDefenseOther = mode === "综合模式";
const diffAttack = needAttackDefenseOther ? initAttack - attackNumber : 0;
const diffDefense = needAttackDefenseOther ? initDefense - defenseNumber : 0;
const diffOther = needAttackDefenseOther ? initOther - otherNumber : 0;
let logMsg = "";
// 处理回血药描述
let descRecovery = "";
if (diffRecovery > 0) {
descRecovery = `消耗${recoveryFoodName}${diffRecovery}`;
} else if (diffRecovery < 0) {
descRecovery = `新增${recoveryFoodName}${-diffRecovery}`;
} else {
descRecovery = `${recoveryFoodName}无变化`;
let changes = [];
await generateDrugDescription(recoveryFoodName, diffRecovery,changes);
await generateDrugDescription(resurrectionFoodName, diffResurrection,changes);
// 只在综合模式下处理攻击药、防御药和其他药
if (needAttackDefenseOther) {
await generateDrugDescription(attackFoodName, diffAttack,changes);
await generateDrugDescription(defenseFoodName, diffDefense,changes);
await generateDrugDescription(otherFoodName, diffOther,changes);
}
// 处理复活药描述
let descResurrection = "";
if (diffResurrection > 0) {
descResurrection = `消耗${resurrectionFoodName}${diffResurrection}`;
} else if (diffResurrection < 0) {
descResurrection = `新增${resurrectionFoodName}${-diffResurrection}`;
// 构建通知消息
let logMsg = `【营养袋吃药统计】\n\n`;
logMsg += `📊 今日药品使用情况\n`;
logMsg += `👤 账户:${userName}\n\n`;
if (changes.every(change => change.includes("无变化"))) {
logMsg += `✅ 今日药物数量无变化\n\n`;
} else {
descResurrection = `${resurrectionFoodName}无变化`;
}
// 根据变化组合日志消息
if (diffRecovery === 0 && diffResurrection === 0) {
// 两个值都等于0输出无变化
logMsg = `${userName}: 今日药物数量无变化`;
} else {
// 其他情况
logMsg = `${userName}: 今日${descRecovery}${descResurrection}`;
logMsg += `📝 使用记录:\n`;
logMsg += changes.join('\n');
logMsg += `\n\n`;
}
// 添加库存信息
logMsg += ` | 当前库存:${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}`;
// 发送通知
const baseDrugs = [
{ name: recoveryFoodName, count: recoveryNumber },
{ name: resurrectionFoodName, count: resurrectionNumber }
];
let inventoryDrugs = [...baseDrugs];
// 只在综合模式下添加攻击药、防御药和其他药的库存信息
if (needAttackDefenseOther) {
inventoryDrugs = inventoryDrugs.concat([
{ name: attackFoodName, count: attackNumber },
{ name: defenseFoodName, count: defenseNumber },
{ name: otherFoodName, count: otherNumber }
]);
}
let inventory = inventoryDrugs
.filter(drug => drug.name.trim() && drug.count > 0)
.map(drug => `- ${drug.name}${drug.count}`);
if (inventory.length > 0) {
logMsg += `📦 当前库存:\n`;
logMsg += inventory.join('\n');
}
notification.send(logMsg);
}else{
// 添加账户名称的通知
notification.send(`${userName}: 今日初始化完成!${recoveryFoodName}${initRecovery}个, ${resurrectionFoodName}${initResurrection}`);
// 添加简单格式的日志记录
let usageSummary = changes.every(change => change.includes("无变化")) ? "药物数量无变化" : changes.map(change => change.replace(/- /g, "")).join(", ");
let inventorySummary = inventory.length > 0 ? inventory.map(item => item.replace(/- /g, "")).join(", ") : "无";
log.info(`${userName}:今日使用情况|${usageSummary}|当前库存:${inventorySummary}`);
} else {
// 构建通知消息
let initMsg = `【营养袋吃药统计】\n\n`;
initMsg += `✅ 今日初始化完成!\n`;
initMsg += `👤 账户:${userName}\n\n`;
// 根据当前模式确定需要显示的药品类型
const needAttackDefenseOther = mode === "综合模式";
const baseDrugs = [
{ name: recoveryFoodName, count: initRecovery },
{ name: resurrectionFoodName, count: initResurrection }
];
let drugs = [...baseDrugs];
// 只在综合模式下添加攻击药、防御药和其他药
if (needAttackDefenseOther) {
drugs = drugs.concat([
{ name: attackFoodName, count: initAttack },
{ name: defenseFoodName, count: initDefense },
{ name: otherFoodName, count: initOther }
]);
}
let items = drugs
.filter(drug => drug.name.trim() && drug.count > 0)
.map(drug => `- ${drug.name}${drug.count}`);
if (items.length > 0) {
initMsg += `📊 初始药品数据:\n`;
initMsg += items.join('\n');
notification.send(initMsg);
// 添加简单格式的日志记录
let initItemsSummary = items.map(item => item.replace(/- /g, "")).join(", ");
log.info(`${userName}:初始化完成|当前库存:${initItemsSummary}`);
} else {
initMsg += `⚠️ 未识别到有效药品数据\n`;
notification.send(initMsg);
// 添加简单格式的日志记录
log.info(`${userName}:初始化完成|当前库存:无`);
}
}
} else {
// 当前数据有任意一个为0不写入记录只发送通知
notification.send(`${userName}: 当前药品数量识别不全(${recoveryFoodName}${recoveryNumber}个, ${resurrectionFoodName}${resurrectionNumber}个),不更新记录`);
// 构建通知消息
let msg = `【营养袋吃药统计】\n\n`;
msg += `⚠️ 识别异常提醒\n`;
msg += `👤 账户:${userName}\n\n`;
msg += `📋 当前药品数量识别不全\n\n`;
const drugs = [
{ name: recoveryFoodName, count: recoveryNumber },
{ name: resurrectionFoodName, count: resurrectionNumber },
{ name: attackFoodName, count: attackNumber },
{ name: defenseFoodName, count: defenseNumber },
{ name: otherFoodName, count: otherNumber }
];
let items = drugs
.filter(drug => drug.name.trim())
.map(drug => `- ${drug.name}${drug.count}`);
msg += `🔍 识别结果:\n`;
msg += items.join('\n');
msg += `\n\n`;
msg += `❌ 不更新记录\n`;
notification.send(msg);
// 添加简单格式的日志记录
log.info(`${userName}:识别异常,未更新记录|当前库存:无`);
}
})();

View File

@@ -1,9 +1,9 @@
{
"manifest_version": 1,
"name": "吃药统计",
"version": "1.7",
"version": "1.8",
"bgi_version": "0.51",
"description": "用于统计指定个食物的消耗,推荐锄地前后使用",
"description": "用于查询指定个食物的消耗,推荐锄地前后使用",
"authors": [
{
"name": "勺子",

View File

@@ -5,7 +5,8 @@
"label": "运行模式",
"options": [
"营养袋模式",
"筛选模式"
"筛选模式",
"综合模式"
],
"default": "营养袋模式"
},
@@ -27,6 +28,24 @@
"label": "复活药名称",
"default": ""
},
{
"name": "attackFoodName",
"type": "input-text",
"label": "攻击药名称",
"default": ""
},
{
"name": "defenseFoodName",
"type": "input-text",
"label": "防御药名称",
"default": ""
},
{
"name": "otherFoodName",
"type": "input-text",
"label": "其他药名称",
"default": ""
},
{
"name": "userName",
"type": "input-text",