新增“超量阈值”和“识别名单”输入框;新增多层弹窗逻辑 (#2174)
* Add files via upload * Add files via upload * Add files via upload * 添加日志 * Add files via upload * Add files via upload * Add files via upload --------- Co-authored-by: zaodonganqi <3193156071@qq.com>
@@ -38,14 +38,15 @@
|
||||
### 核心优势
|
||||
1. **自动CD判断**:无需手动关注材料刷新状态,脚本自动识别CD是否就绪;
|
||||
2. **灵活路径管理**:支持自定义添加路径,自动排除低效/无效路径;
|
||||
3. **独立名单识别**:不与路边NPC、神像交互;可自定义识别名单(操作见「四、问题解答」);
|
||||
3. **独立名单识别**:不与路边NPC、神像交互;可自定义识别名单(操作见「四、问题解答Q4」);
|
||||
4. **实时弹窗保护**:内置弹窗模块(覆盖路边信件、过期物品、月卡、调查等场景),运行时全程保护路径不被弹窗干扰。
|
||||
5. **自动黑名单**:内置拾取模块,联动材料统计,可识别爆满的路径材料,自动屏蔽。
|
||||
|
||||
|
||||
## 二、用前须知
|
||||
1. 需具备基础电脑操作能力(如文件夹复制、路径查找);
|
||||
2. 脚本不自带路径文件,需手动对目标文件夹进行操作(步骤见「三、使用方法」)。
|
||||
2. 非1080p显示器,使用前需要根据显示器调整背包物品界面的 拖动距离 ,推荐“一次划页稍小于4行材料的距离”;
|
||||
3. 脚本不自带路径文件,需手动对目标文件夹进行操作(步骤见「三、使用方法」)。
|
||||
|
||||
|
||||
## 三、使用方法
|
||||
@@ -110,14 +111,16 @@
|
||||
| 配置项 | 功能说明 | 操作建议 |
|
||||
|----------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------|
|
||||
| 1. 目标数量 | 仅当背包材料数量**低于此值**时,该材料的路径才会被纳入执行序列 | 这是个统一值,管理路径下全部材料的目标数量 |
|
||||
| 2. 优先级材料 | 无视“目标数量”限制,直接纳入执行序列顶层(最高优先级) | 填写当前急需材料(例:“虹滴晶”“巡陆艇”) |
|
||||
| 2. 优先级材料 | 无视“目标数量”限制,直接纳入执行序列顶层(最高优先级) | 填写当前急需材料(例:虹滴晶,巡陆艇) |
|
||||
| 3. 时间成本 | 当一个路径有3-5次运行记录后,自动计算“单材料获取时间”;超过30秒则跳过该路径 | 保持默认30秒即可,无需频繁修改(可过滤低效路径) |
|
||||
| 4. 发送通知 | ① 每类材料跑完通知一次;② 全部材料跑完汇总通知一次(需开启BGI通知) | 建议开启,方便实时了解进度(接收端如企业微信需自行配置) |
|
||||
| 5. 取消扫描 | 取消“每个路径执行后”的背包扫描,仅保留“全部执行前/后”2次扫描 | 有效路径记录达3条以上时可以开启,可节约运行时间 |
|
||||
| 6. 仅 pathing 材料 | 仅扫描 `pathing` 文件夹内的材料,跳过其他分类,大幅缩短扫描时间 | 路径配置完成后开启,提升脚本运行效率 |
|
||||
| 7. 弹窗名 | 不填则默认循环执行 `assets\imageClick` 文件夹下所有弹窗;填写则仅执行指定弹窗 | 推荐默认,需单独适配某类弹窗时填写(例:仅处理月卡弹窗则填:月卡) |
|
||||
| 8. 采用的 CD 分类 | 仅执行 `materialsCD` 文件夹内配置的材料路径(支持新增CD分类txt) | 新增材料时,需在该文件夹同步配置CD规则(操作见「四、问题解答4」) |
|
||||
| 9. 拖动距离 | 解决非1080p分辨率下“划页过头”问题,需调整到“一次划页≤4行” | 拖动点建议选“第五行材料附近”;大于1080p屏可适当减小数值 |
|
||||
| 7. 弹窗名 | 不填则默认循环执行 `assets\imageClick` 文件夹下所有弹窗;填写则仅执行指定弹窗 | 推荐默认,需单独适配某类弹窗时填写(例:月卡,复苏) |
|
||||
| 8. 采用的 CD 分类 | 不填则默认执行 `materialsCD` 文件夹内配置的CD分类;填写则仅执行指定CD分类 | 新增材料时,需在该文件夹同步配置CD规则(操作见「四、问题解答Q2」) |
|
||||
| 9. 采用的识别名单 | 不填则默认执行 `targetText` 文件夹内配置的识别名单;填写则仅执行指定识别名单 | 新增名单时,需符合配置规则(操作见「四、问题解答Q4」) |
|
||||
| 10. 超量阈值 | 首次扫描后,超量的路径材料,将从识别名单中剔除,默认5000 | 不推荐9999,怪物材料有几千就够了,采用默认数值,可自动避免爆背包 |
|
||||
| 11. 拖动距离 | 解决非1080p分辨率下“划页过头”问题,需调整到“一次划页≤4行” | 拖动点建议选“第五行材料附近”;大于1080p屏可适当减小数值 |
|
||||
|
||||
|
||||
## 四、注意事项
|
||||
@@ -134,23 +137,9 @@
|
||||
### Q1:如何排除不想要的路径?
|
||||
A:1. 打开 `pathing` 文件夹(脚本路径:`BetterGI\User\JsScript\背包材料统计\pathing`);
|
||||
2. 直接删除/移走目标材料/怪物的路径文件夹;
|
||||
3. **注意**:不要将路径文件放入 `targetText` 或 `materialsCD` 文件夹(① 这两个文件夹默认全部读取,增加负担;② 也不安全,会被更新覆盖)。
|
||||
**其他方法**:看「四、问题解答Q2」,按格式要求填入对应的材料名(或者从其他CD文件中复制过来),在「JS 自定义设置」【采用的 CD 分类】中输入新建的文件名,即可实现只加载该CD文件里材料的路径。
|
||||
|
||||
### Q2:如何自定义识别名单?
|
||||
A:1. 打开 `targetText` 文件夹(脚本路径:`BetterGI\User\JsScript\背包材料统计\targetText`);
|
||||
2. 新建/编辑txt文件,按格式填写:`自定义名称:目标1,目标2`(英文冒号+英文逗号,例:“新材料:霜盏花,便携轴承,”);
|
||||
3. 若需排除怪物掉落材料:找到“掉落.txt”,删除对应材料名即可;
|
||||
4. 操作参考截图:
|
||||
<div class="img-row-container">
|
||||
<img src="assets/Pic/Pic12.png" alt="自定义识别名单操作截图1" class="img-row-item">
|
||||
<img src="assets/Pic/Pic10.png" alt="自定义识别名单操作截图2" class="img-row-item">
|
||||
</div>
|
||||
|
||||
### Q3:如何识别不规范命名的路径文件夹(如“纳塔食材一条龙”“果园.json”)?
|
||||
A:将不规范的文件夹/文件,放入**适配的材料文件夹**中即可(路径CD由“所在材料文件夹”决定)。
|
||||
例:“果园.json”放入“苹果”文件夹,将按“苹果”的CD规则执行。
|
||||
|
||||
### Q4:如何添加新材料?
|
||||
### Q2:如何添加新材料?
|
||||
A:1. 打开 `materialsCD` 文件夹(脚本路径:`BetterGI\User\JsScript\背包材料统计\materialsCD`);
|
||||
2. 新建/编辑txt文件,按格式填写:`CD规则:材料1,材料2`(中文冒号+中文逗号,CD规则参考自带文件,例:“1次0点:月落银,宿影花,”),`材料1`或`材料2`将会作为标准名;
|
||||
3. **关键要求**:路径文件夹名、材料图片名必须与“材料1或2”完全一致(多层文件夹默认读取最外层标准名文件夹);
|
||||
@@ -160,6 +149,20 @@ A:1. 打开 `materialsCD` 文件夹(脚本路径:`BetterGI\User\JsScript\
|
||||
<img src="assets/Pic/Pic08.png" alt="添加新材料操作截图2" class="img-row-item">
|
||||
</div>
|
||||
|
||||
### Q3:如何识别不规范命名的路径文件夹(如“纳塔食材一条龙”“果园.json”)?
|
||||
A:将不规范的文件夹/文件,放入**适配的材料文件夹**中即可(路径CD由“所在材料文件夹”决定)。
|
||||
例:“果园.json”放入“苹果”文件夹,将按“苹果”的CD规则执行。
|
||||
|
||||
### Q4:如何自定义识别名单?
|
||||
A:1. 打开 `targetText` 文件夹(脚本路径:`BetterGI\User\JsScript\背包材料统计\targetText`);
|
||||
2. 新建/编辑txt文件,按格式填写:`自定义名称:目标1,目标2`(英文冒号+英文逗号,例:“新材料:霜盏花,便携轴承,”);
|
||||
3. 若需排除怪物掉落材料:找到“掉落.txt”,删除对应材料名即可;
|
||||
4. 操作参考截图:
|
||||
<div class="img-row-container">
|
||||
<img src="assets/Pic/Pic12.png" alt="自定义识别名单操作截图1" class="img-row-item">
|
||||
<img src="assets/Pic/Pic10.png" alt="自定义识别名单操作截图2" class="img-row-item">
|
||||
</div>
|
||||
|
||||
### Q5:如何取消路径执行后扫描背包?
|
||||
A:在「JS自定义设置」中勾选“取消扫描”(依旧会保留“全部材料执行始/末”的2次扫描)。
|
||||
|
||||
@@ -209,4 +212,5 @@ A:记录文件夹位于 `BetterGI\User\JsScript\背包材料统计\` 下,各
|
||||
| v2.41 | 修复“勾选分类的本地记录”bug;新增“仅背包统计”选项;补充记录损坏处理说明 |
|
||||
| v2.42 | 新增“无路径间扫描”“noRecord模式”(适合成熟路径);新增怪物材料CD文件 |
|
||||
| v2.50 | 新增独立名单拾取、弹窗模块;支持怪物名识别 |
|
||||
| v2.51 | 自定义设置新增“拖动距离/拖动点”;新增月卡弹窗识别;路径材料达9999自动上黑名单;修复怪物0收获记录 |
|
||||
| v2.51 | 自定义设置新增“拖动距离/拖动点”;新增月卡弹窗识别;路径材料达9999自动上黑名单;修复怪物0收获记录 |
|
||||
| v2.52 | 自定义设置新增“超量阈值”和“识别名单”输入框;新增多层弹窗逻辑 |
|
||||
10
repo/js/背包材料统计/assets/imageClick/其他/F/config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"isSpecial": true,
|
||||
"operationType": "key_press",
|
||||
"keyCode": "VK_F",
|
||||
"xOffset": 0,
|
||||
"yOffset": 0,
|
||||
"detectRegion": { "x": 1207, "y": 783, "width": 42, "height": 42 },
|
||||
"nextLevelOnSuccess": "",
|
||||
"nextLevelOnFailure": ""
|
||||
}
|
||||
BIN
repo/js/背包材料统计/assets/imageClick/其他/F/icon/F_2.png
Normal file
|
After Width: | Height: | Size: 461 B |
10
repo/js/背包材料统计/assets/imageClick/其他/滚轮/config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"isSpecial": true,
|
||||
"operationType": "key_mouse_script",
|
||||
"scriptPath": "assets/滚轮下翻.json",
|
||||
"xOffset": 0,
|
||||
"yOffset": 0,
|
||||
"detectRegion": { "x": 1206, "y": 175, "width": 44, "height": 735 },
|
||||
"nextLevelOnSuccess": "assets\\imageClick\\其他\\F",
|
||||
"nextLevelOnFailure": ""
|
||||
}
|
||||
BIN
repo/js/背包材料统计/assets/imageClick/其他/滚轮/icon/F_2.png
Normal file
|
After Width: | Height: | Size: 461 B |
BIN
repo/js/背包材料统计/assets/imageClick/其他/调查/Picture/image6.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
repo/js/背包材料统计/assets/imageClick/其他/调查/icon/123.png
Normal file
|
After Width: | Height: | Size: 698 B |
BIN
repo/js/背包材料统计/assets/imageClick/复苏/Picture/全员复苏.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
repo/js/背包材料统计/assets/imageClick/复苏/icon/确认.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
15
repo/js/背包材料统计/assets/imageClick/对话/config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"isSpecial": true,
|
||||
"operationType": "key_press",
|
||||
"keyCode": "VK_SPACE",
|
||||
"xOffset": 950,
|
||||
"yOffset": 1050,
|
||||
"detectRegion": {
|
||||
"x": 264,
|
||||
"y": 36,
|
||||
"width": 66,
|
||||
"height": 24
|
||||
},
|
||||
"nextLevelOnSuccess": "assets\\imageClick\\其他\\滚轮",
|
||||
"nextLevelOnFailure": ""
|
||||
}
|
||||
BIN
repo/js/背包材料统计/assets/imageClick/对话/icon/NoUI.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
6
repo/js/背包材料统计/assets/imageClick/月卡/config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"xOffset": 0,
|
||||
"yOffset": 455,
|
||||
"loopCount": 3,
|
||||
"loopDelay": 1000
|
||||
}
|
||||
BIN
repo/js/背包材料统计/assets/imageClick/登录/Picture/load.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
1
repo/js/背包材料统计/assets/imageClick/登录/config.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "xOffset": -845, "yOffset": 325 }
|
||||
BIN
repo/js/背包材料统计/assets/imageClick/登录/icon/setting.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -31,18 +31,31 @@ function readtargetTextCategories(targetTextDir) {
|
||||
const targetTextFilePaths = readAllFilePaths(targetTextDir, 0, 1);
|
||||
const materialCategories = {};
|
||||
|
||||
// 解析筛选名单
|
||||
const pickTextNames = (settings.PickCategories || "")
|
||||
.split(/[,,、 \s]+/).map(n => n.trim()).filter(n => n);
|
||||
|
||||
// 【新增:兜底日志】确认pickTextNames是否为空,方便排查
|
||||
log.info(`筛选名单状态:${pickTextNames.length === 0 ? '未指定(空),将加载所有文件' : '指定了:' + pickTextNames.join(',')}`);
|
||||
|
||||
for (const filePath of targetTextFilePaths) {
|
||||
if (state.cancelRequested) break;
|
||||
const content = file.readTextSync(filePath);
|
||||
if (!content) {
|
||||
log.error(`加载文件失败:${filePath}`);
|
||||
continue; // 跳过当前文件
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceCategory = basename(filePath).replace('.txt', ''); // 去掉文件扩展名
|
||||
// 【核心筛选:空名单直接跳过判断,加载所有】
|
||||
if (pickTextNames.length === 0) {
|
||||
// 空名单时,直接保留当前文件,不跳过
|
||||
} else if (!pickTextNames.includes(sourceCategory)) {
|
||||
// 非空名单且不在列表里,才跳过
|
||||
continue;
|
||||
}
|
||||
materialCategories[sourceCategory] = parseCategoryContent(content);
|
||||
}
|
||||
// log.info(`完成材料分类信息读取,分类信息:${JSON.stringify(materialCategories, null, 2)}`);
|
||||
return materialCategories;
|
||||
}
|
||||
// 定义替换映射表
|
||||
@@ -171,7 +184,7 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
log.info("检测中...");
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
await sleep(50); // 关键50时可避免F多目标滚动中拾取错,背包js这边有弹窗模块,就没必要增加延迟降低效率了
|
||||
await sleep(50);
|
||||
cachedFrame?.dispose();
|
||||
cachedFrame = captureGameRegion();
|
||||
|
||||
@@ -192,7 +205,6 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
for (let targetText of targetTexts) {
|
||||
let targetResult = ocrResults.find(res => res.text.includes(targetText));
|
||||
if (targetResult) {
|
||||
// log.info(`找到目标文本: ${targetText}`);
|
||||
|
||||
// 生成唯一标识并更新识别计数(文本+Y坐标)
|
||||
const materialId = `${targetText}-${targetResult.y}`;
|
||||
@@ -203,7 +215,6 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text
|
||||
// log.info(`目标文本 '${targetText}' 和 F 图标水平对齐`);
|
||||
if (recognitionCount.get(materialId) >= 1) {
|
||||
keyPress("F"); // 执行交互操作
|
||||
// log.info(`F键执行成功,识别计数: ${recognitionCount.get(materialId)}`);
|
||||
log.info(`交互或拾取: ${targetText}`);
|
||||
|
||||
// F键后清除计数,确保单次交互
|
||||
|
||||
@@ -4,6 +4,7 @@ const holdX = Math.min(1920, Math.max(0, Math.floor(Number(settings.HoldX) || 10
|
||||
const holdY = Math.min(1080, Math.max(0, Math.floor(Number(settings.HoldY) || 750)));
|
||||
const totalPageDistance = Math.max(10, Math.floor(Number(settings.PageScrollDistance) || 711));
|
||||
const targetCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.TargetCount) || 5000))); // 设定的目标数量
|
||||
const exceedCount = Math.min(9999, Math.max(0, Math.floor(Number(settings.ExceedCount) || 5000))); // 设定的超量目标数量
|
||||
const imageDelay = Math.min(1000, Math.max(0, Math.floor(Number(settings.ImageDelay) || 0))); // 识图基准时长 await sleep(imageDelay);
|
||||
|
||||
// 配置参数
|
||||
@@ -383,7 +384,7 @@ async function scanMaterials(materialsCategory, materialCategoryMap) {
|
||||
width: 66 + 2 * tolerance,
|
||||
height: 22 + 2 * tolerance
|
||||
};
|
||||
const ocrResult = await recognizeText(ocrRegion, 1000, 10, 5, 2, ra);
|
||||
const ocrResult = await recognizeText(ocrRegion, 200, 10, 5, 2, ra);
|
||||
materialInfo.push({ name, count: ocrResult || "?" });
|
||||
|
||||
if (!hasFoundFirstMaterial) {
|
||||
@@ -516,7 +517,7 @@ var excessMaterialNames = []; // 超量材料名单
|
||||
|
||||
function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) {
|
||||
// 新增:超量阈值(普通材料9999,矿石处理后也是9999)
|
||||
const EXCESS_THRESHOLD = 9999;
|
||||
const EXCESS_THRESHOLD = exceedCount;
|
||||
// 新增:临时存储本次超量材料
|
||||
const tempExcess = [];
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
// ======================== 全局工具函数(只定义1次,所有函数共用)========================
|
||||
// 1. 路径标准化函数(统一处理,消除重复)
|
||||
function normalizePath(path) {
|
||||
if (!path || typeof path !== 'string') return '';
|
||||
let standardPath = path.replace(/\\/g, '/').replace(/\/+/g, '/');
|
||||
return standardPath.endsWith('/') ? standardPath.slice(0, -1) : standardPath;
|
||||
}
|
||||
|
||||
|
||||
// ==============================================
|
||||
// 5. 角色识别与策略执行相关函数(保留原始功能)
|
||||
// ==============================================
|
||||
// 工具函数
|
||||
// 2. 提取路径最后一级名称
|
||||
function basename(filePath) {
|
||||
if (!filePath || typeof filePath !== 'string') return '';
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
const normalizedPath = normalizePath(filePath);
|
||||
const lastSlashIndex = normalizedPath.lastIndexOf('/');
|
||||
return lastSlashIndex !== -1 ? normalizedPath.substring(lastSlashIndex + 1) : normalizedPath;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
// 如果路径存在且返回的是数组,则认为是目录Folder
|
||||
function pathExists(path) {
|
||||
|
||||
@@ -1,195 +1,457 @@
|
||||
// 新增:独立的预加载函数,负责所有资源预处理
|
||||
// ======================== 1. 预加载函数(精简日志版)========================
|
||||
async function preloadImageResources(specificNames) {
|
||||
log.info("开始预加载所有图片资源");
|
||||
// log.info("开始预加载所有图片资源");
|
||||
|
||||
function hasIconFolder(dirPath) {
|
||||
try {
|
||||
const entries = readAllFilePaths(dirPath, 0, 0, [], true);
|
||||
return entries.some(entry => normalizePath(entry).endsWith('/icon'));
|
||||
} catch (e) {
|
||||
log.error(`检查目录【${dirPath}】是否有icon文件夹失败:${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 统一参数格式(与原逻辑一致)
|
||||
let preSpecificNames = specificNames;
|
||||
if (typeof specificNames === 'string') {
|
||||
preSpecificNames = [specificNames];
|
||||
}
|
||||
if (typeof specificNames === 'string') preSpecificNames = [specificNames];
|
||||
const isAll = !preSpecificNames || preSpecificNames.length === 0;
|
||||
if (isAll) {
|
||||
log.info("未指定具体弹窗名称,将执行所有弹窗目录处理");
|
||||
} else {
|
||||
log.info(`指定处理弹窗名称:${preSpecificNames.join(', ')}`);
|
||||
}
|
||||
// if (isAll) log.info("未指定具体弹窗名称,将执行所有含icon文件夹的弹窗目录处理");
|
||||
// else log.info(`指定处理弹窗名称:${preSpecificNames.join(', ')}(仅含icon文件夹的目录)`);
|
||||
|
||||
// 定义根目录(与原代码一致)
|
||||
const rootDir = "assets/imageClick";
|
||||
const rootDirNormalized = normalizePath(rootDir);
|
||||
const subDirs = readAllFilePaths(rootDir, 0, 2, [], true);
|
||||
|
||||
// 获取所有子目录(与原代码一致)
|
||||
const subDirs = readAllFilePaths(rootDir, 0, 0, [], true);
|
||||
|
||||
// 筛选目标目录(与原代码一致)
|
||||
const targetDirs = isAll
|
||||
? subDirs
|
||||
: subDirs.filter(subDir => {
|
||||
const dirName = basename(subDir);
|
||||
return preSpecificNames.includes(dirName);
|
||||
});
|
||||
const targetDirs = subDirs.filter(subDir => {
|
||||
const dirName = basename(subDir);
|
||||
const hasIcon = hasIconFolder(subDir);
|
||||
const matchName = isAll ? true : preSpecificNames.includes(dirName);
|
||||
return hasIcon && matchName;
|
||||
});
|
||||
|
||||
if (targetDirs.length === 0) {
|
||||
log.info(`未找到与指定名称匹配的目录,名称列表:${preSpecificNames?.join(', ') || '所有'}`);
|
||||
// log.info("未找到符合条件的弹窗目录");
|
||||
return [];
|
||||
}
|
||||
|
||||
// 预加载所有目录的资源(原imageClick内的资源加载逻辑)
|
||||
const preloadedResources = [];
|
||||
for (const subDir of targetDirs) {
|
||||
const dirName = basename(subDir);
|
||||
// log.info(`开始预处理弹窗类型:${dirName}`);
|
||||
const fullPath = normalizePath(subDir);
|
||||
const pathSegments = fullPath.slice(rootDirNormalized.length + 1).split('/');
|
||||
const level = pathSegments.length;
|
||||
const isFirstLevel = level === 1;
|
||||
|
||||
// 查找icon和Picture文件夹(与原代码一致)
|
||||
const entries = readAllFilePaths(subDir, 0, 1, [], true);
|
||||
const iconDir = entries.find(entry => entry.endsWith('\icon'));
|
||||
const pictureDir = entries.find(entry => entry.endsWith('\Picture'));
|
||||
let popupConfig = {
|
||||
isSpecial: false,
|
||||
operationType: "click",
|
||||
ocrConfig: null,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
detectRegion: null,
|
||||
nextLevelOnSuccess: "",
|
||||
nextLevelOnFailure: "",
|
||||
loopCount: 1,
|
||||
loopDelay: 0
|
||||
};
|
||||
const configPath = normalizePath(`${subDir}/config.json`);
|
||||
let isSpecialModule = false;
|
||||
let specialDetectRegion = null;
|
||||
|
||||
if (!iconDir) {
|
||||
log.warn(`未找到 icon 文件夹,跳过分类文件夹:${subDir}`);
|
||||
continue;
|
||||
}
|
||||
if (!pictureDir) {
|
||||
log.warn(`未找到 Picture 文件夹,跳过分类文件夹:${subDir}`);
|
||||
continue;
|
||||
if (fileExists(configPath)) {
|
||||
try {
|
||||
const configContent = file.readTextSync(configPath);
|
||||
popupConfig = { ...popupConfig, ...JSON.parse(configContent) };
|
||||
isSpecialModule = popupConfig.isSpecial === true
|
||||
&& typeof popupConfig.detectRegion === 'object'
|
||||
&& popupConfig.detectRegion !== null
|
||||
&& popupConfig.detectRegion.x != null
|
||||
&& popupConfig.detectRegion.y != null
|
||||
&& popupConfig.detectRegion.width != null
|
||||
&& popupConfig.detectRegion.height != null
|
||||
&& popupConfig.detectRegion.width > 0
|
||||
&& popupConfig.detectRegion.height > 0;
|
||||
specialDetectRegion = isSpecialModule ? popupConfig.detectRegion : null;
|
||||
// log.info(`【${dirName}】加载配置成功:${isFirstLevel ? '第一级' : '第二级'} | 模块类型:${isSpecialModule ? '特殊模块' : '普通模块'}`);
|
||||
} catch (e) {
|
||||
log.error(`【${dirName}】解析配置失败,使用默认配置:${e.message}`);
|
||||
isSpecialModule = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 读取图片文件(与原代码一致)
|
||||
const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
const pictureFilePaths = readAllFilePaths(pictureDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
|
||||
// 预创建icon识别对象(与原代码一致)
|
||||
const iconRecognitionObjects = [];
|
||||
for (const filePath of iconFilePaths) {
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`加载图标失败:${filePath}`);
|
||||
if (isSpecialModule) {
|
||||
const entries = readAllFilePaths(subDir, 0, 1, [], true);
|
||||
const iconDir = entries.find(entry => normalizePath(entry).endsWith('/icon'));
|
||||
const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
|
||||
if (iconFilePaths.length === 0) {
|
||||
log.warn(`【${dirName}】特殊模块无有效icon文件,跳过`);
|
||||
continue;
|
||||
}
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080);
|
||||
iconRecognitionObjects.push({ name: basename(filePath), ro: recognitionObject, iconDir });
|
||||
}
|
||||
|
||||
// 预创建图库区域(与原代码一致)
|
||||
const pictureRegions = [];
|
||||
for (const filePath of pictureFilePaths) {
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`加载图库图片失败:${filePath}`);
|
||||
const iconRecognitionObjects = [];
|
||||
for (const filePath of iconFilePaths) {
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`【${dirName}】特殊模块加载图标失败:${filePath}`);
|
||||
continue;
|
||||
}
|
||||
iconRecognitionObjects.push({
|
||||
name: basename(filePath),
|
||||
ro: RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080),
|
||||
iconDir,
|
||||
mat: mat
|
||||
});
|
||||
}
|
||||
|
||||
const targetIcon = iconRecognitionObjects[0];
|
||||
const manualRegion = new ImageRegion(targetIcon.mat, specialDetectRegion.x, specialDetectRegion.y);
|
||||
manualRegion.width = specialDetectRegion.width;
|
||||
manualRegion.height = specialDetectRegion.height;
|
||||
|
||||
const foundRegions = [{
|
||||
pictureName: "特殊模块",
|
||||
iconName: targetIcon.name,
|
||||
region: manualRegion,
|
||||
iconDir: iconDir
|
||||
}];
|
||||
// log.info(`【${dirName}】特殊模块生成识别区域:x=${manualRegion.x}, y=${manualRegion.y}, 宽=${manualRegion.width}, 高=${manualRegion.height}`);
|
||||
|
||||
preloadedResources.push({
|
||||
dirName,
|
||||
fullPath,
|
||||
foundRegions,
|
||||
popupConfig,
|
||||
isFirstLevel: isFirstLevel,
|
||||
level: level
|
||||
});
|
||||
|
||||
} else {
|
||||
const entries = readAllFilePaths(subDir, 0, 1, [], true);
|
||||
const iconDir = entries.find(entry => normalizePath(entry).endsWith('/icon'));
|
||||
const pictureDir = entries.find(entry => normalizePath(entry).endsWith('/Picture'));
|
||||
|
||||
if (!pictureDir) {
|
||||
log.warn(`【${dirName}】普通模块无Picture文件夹,跳过`);
|
||||
continue;
|
||||
}
|
||||
pictureRegions.push({ name: basename(filePath), region: new ImageRegion(mat, 0, 0) });
|
||||
}
|
||||
|
||||
// 预计算匹配区域(与原代码一致)
|
||||
const foundRegions = [];
|
||||
for (const picture of pictureRegions) {
|
||||
for (const icon of iconRecognitionObjects) {
|
||||
const foundRegion = picture.region.find(icon.ro);
|
||||
if (foundRegion.isExist()) {
|
||||
foundRegions.push({
|
||||
pictureName: picture.name,
|
||||
iconName: icon.name,
|
||||
region: foundRegion,
|
||||
iconDir: icon.iconDir
|
||||
});
|
||||
const iconFilePaths = readAllFilePaths(iconDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
const pictureFilePaths = readAllFilePaths(pictureDir, 0, 0, ['.png', '.jpg', '.jpeg']);
|
||||
|
||||
// 仅在资源为空时警告
|
||||
if (iconFilePaths.length === 0) {
|
||||
log.warn(`【${dirName}】普通模块无有效icon文件,跳过`);
|
||||
continue;
|
||||
}
|
||||
if (pictureFilePaths.length === 0) {
|
||||
log.warn(`【${dirName}】普通模块无有效Picture文件,跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const iconRecognitionObjects = [];
|
||||
for (const filePath of iconFilePaths) {
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`【${dirName}】加载图标失败:${filePath}`);
|
||||
continue;
|
||||
}
|
||||
iconRecognitionObjects.push({
|
||||
name: basename(filePath),
|
||||
ro: RecognitionObject.TemplateMatch(mat, 0, 0, 1920, 1080),
|
||||
iconDir
|
||||
});
|
||||
}
|
||||
|
||||
const pictureRegions = [];
|
||||
for (const filePath of pictureFilePaths) {
|
||||
const mat = file.readImageMatSync(filePath);
|
||||
if (mat.empty()) {
|
||||
log.error(`【${dirName}】加载图库图片失败:${filePath}`);
|
||||
continue;
|
||||
}
|
||||
pictureRegions.push({
|
||||
name: basename(filePath),
|
||||
region: new ImageRegion(mat, 0, 0)
|
||||
});
|
||||
}
|
||||
|
||||
const foundRegions = [];
|
||||
for (const picture of pictureRegions) {
|
||||
for (const icon of iconRecognitionObjects) {
|
||||
const foundRegion = picture.region.find(icon.ro);
|
||||
if (foundRegion.isExist()) {
|
||||
foundRegions.push({
|
||||
pictureName: picture.name,
|
||||
iconName: icon.name,
|
||||
region: foundRegion,
|
||||
iconDir: icon.iconDir
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundRegions.length === 0) {
|
||||
log.warn(`【${dirName}】普通模块无匹配图标,跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 保存预处理结果
|
||||
preloadedResources.push({
|
||||
dirName,
|
||||
foundRegions
|
||||
});
|
||||
preloadedResources.push({
|
||||
dirName,
|
||||
fullPath,
|
||||
foundRegions,
|
||||
popupConfig,
|
||||
isFirstLevel: isFirstLevel,
|
||||
level: level
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log.info(`预加载完成,共处理 ${preloadedResources.length} 个目录`);
|
||||
// log.info(`预加载完成,共${preloadedResources.length}个有效弹窗目录(第一级:${preloadedResources.filter(r => r.isFirstLevel).length}个,第二级:${preloadedResources.filter(r => !r.isFirstLevel).length}个)`);
|
||||
return preloadedResources;
|
||||
}
|
||||
|
||||
// 新增:imageClick后台任务函数
|
||||
// ======================== 2. 后台任务函数(精简日志版)========================
|
||||
async function imageClickBackgroundTask() {
|
||||
log.info("imageClick后台任务已启动");
|
||||
const imageClickDelay = Math.min(99, Math.max(1, Math.floor(Number(settings.PopupClickDelay) || 5)))*1000;
|
||||
// 可以根据需要配置要处理的弹窗名称
|
||||
|
||||
// 配置参数
|
||||
const taskDelay = Math.min(999, Math.max(1, Math.floor(Number(settings.PopupClickDelay) || 15)))*1000;
|
||||
const specificNamesStr = settings.PopupNames || "";
|
||||
const specificNames = specificNamesStr
|
||||
.split(/[,,、 \s]+/)
|
||||
.map(name => name.trim())
|
||||
.filter(name => name !== "");
|
||||
|
||||
// 调用独立预加载函数(循环前仅执行一次)
|
||||
// 预加载资源
|
||||
const preloadedResources = await preloadImageResources(specificNames);
|
||||
if (preloadedResources.length === 0) {
|
||||
log.info("无可用预加载资源,任务结束");
|
||||
log.info("无可用弹窗资源,任务结束");
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// 循环执行,仅使用预加载资源
|
||||
// 筛选一级弹窗
|
||||
const firstLevelDirs = preloadedResources.filter(res => res.isFirstLevel);
|
||||
if (firstLevelDirs.length === 0) {
|
||||
log.warn("无第一级弹窗目录,任务终止");
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// 打印资源检测结果
|
||||
log.info("\n==================== 现有弹窗加载结果 ====================");
|
||||
log.info("1. 一级弹窗(共" + firstLevelDirs.length + "个):");
|
||||
firstLevelDirs.forEach((res, idx) => log.info(` ${idx+1}. 【${res.dirName}】`));
|
||||
const secondLevelResources = preloadedResources.filter(res => !res.isFirstLevel);
|
||||
log.info("\n2. 二级弹窗(共" + secondLevelResources.length + "个):");
|
||||
secondLevelResources.forEach((res, idx) => log.info(` ${idx+1}. 【${res.dirName}】`));
|
||||
log.info("=============================================================\n");
|
||||
|
||||
// 核心逻辑:外循环遍历所有一级弹窗
|
||||
while (!state.completed && !state.cancelRequested) {
|
||||
try {
|
||||
// 调用imageClick时传入预加载资源
|
||||
await imageClick(preloadedResources, null, specificNames, true);
|
||||
} catch (error) {
|
||||
log.info(`弹窗识别失败(继续重试):${error.message}`);
|
||||
// log.info(`\n===== 外循环开始:遍历所有一级弹窗(共${firstLevelDirs.length}个) =====`);
|
||||
|
||||
// 遍历所有一级弹窗
|
||||
for (const currentFirstLevel of firstLevelDirs) {
|
||||
// 检查当前一级弹窗是否被触发
|
||||
const levelResult = await imageClick([currentFirstLevel], null, [currentFirstLevel.dirName], true);
|
||||
|
||||
if (levelResult.success) {
|
||||
log.info(`【${currentFirstLevel.dirName}】触发成功,进入内部流程...`);
|
||||
const levelStack = [currentFirstLevel];
|
||||
|
||||
// 内循环处理内部流程
|
||||
while (levelStack.length > 0 && !state.completed && !state.cancelRequested) {
|
||||
const currentResource = levelStack[levelStack.length - 1];
|
||||
const innerResult = await imageClick([currentResource], null, [currentResource.dirName], true);
|
||||
|
||||
if (innerResult.success) {
|
||||
const nextPath = normalizePath(currentResource.popupConfig.nextLevelOnSuccess);
|
||||
if (nextPath && nextPath.trim()) {
|
||||
const nextResource = preloadedResources.find(res => res.fullPath === nextPath);
|
||||
if (nextResource) {
|
||||
levelStack.push(nextResource);
|
||||
} else {
|
||||
// log.warn(`内循环:下一级(${nextPath})不存在,回退`);
|
||||
levelStack.pop();
|
||||
}
|
||||
} else {
|
||||
levelStack.pop();
|
||||
}
|
||||
} else {
|
||||
const nextPath = normalizePath(currentResource.popupConfig.nextLevelOnFailure);
|
||||
if (nextPath && nextPath.trim()) {
|
||||
const nextResource = preloadedResources.find(res => res.fullPath === nextPath);
|
||||
if (nextResource) {
|
||||
levelStack.push(nextResource);
|
||||
} else {
|
||||
// log.warn(`内循环:下一级(${nextPath})不存在,回退`);
|
||||
levelStack.pop();
|
||||
}
|
||||
} else {
|
||||
levelStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
log.info(`【${currentFirstLevel.dirName}】内部流程处理完毕`);
|
||||
}
|
||||
}
|
||||
// 短暂等待后再次执行
|
||||
await sleep(imageClickDelay);
|
||||
|
||||
// log.info(`===== 外循环结束:等待${taskDelay/1000}秒后开始下一次循环 =====`);
|
||||
await sleep(taskDelay);
|
||||
}
|
||||
|
||||
log.info("imageClick后台任务已结束");
|
||||
log.info("imageClick后台任务结束");
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// 优化:使用预加载资源,保留所有原执行逻辑
|
||||
// ======================== 3. 识别与操作函数(精简日志版)========================
|
||||
async function imageClick(preloadedResources, ra = null, specificNames = null, useNewScreenshot = false) {
|
||||
// 保留原参数格式处理(兼容历史调用)
|
||||
if (typeof specificNames === 'string') {
|
||||
specificNames = [specificNames];
|
||||
}
|
||||
const isAll = !specificNames || specificNames.length === 0;
|
||||
let isAnySuccess = false;
|
||||
|
||||
// 遍历预处理好的资源(原targetDirs循环逻辑)
|
||||
for (const resource of preloadedResources) {
|
||||
const { dirName, foundRegions } = resource;
|
||||
const { dirName, foundRegions, popupConfig } = resource;
|
||||
const { xOffset, yOffset } = popupConfig;
|
||||
let hasAnyIconDetected = false; // 标记是否有图标被识别
|
||||
|
||||
for (const foundRegion of foundRegions) {
|
||||
// 保留原识别对象创建逻辑(使用预处理的路径)
|
||||
const tolerance = 1;
|
||||
const iconMat = file.readImageMatSync(`${foundRegion.iconDir}/${foundRegion.iconName}`);
|
||||
const iconMat = file.readImageMatSync(`${normalizePath(foundRegion.iconDir)}/${foundRegion.iconName}`);
|
||||
|
||||
const { detectRegion } = popupConfig;
|
||||
const defaultX = foundRegion.region.x - tolerance;
|
||||
const defaultY = foundRegion.region.y - tolerance;
|
||||
const defaultWidth = foundRegion.region.width + 2 * tolerance;
|
||||
const defaultHeight = foundRegion.region.height + 2 * tolerance;
|
||||
const recognitionObject = RecognitionObject.TemplateMatch(
|
||||
iconMat,
|
||||
foundRegion.region.x - tolerance,
|
||||
foundRegion.region.y - tolerance,
|
||||
foundRegion.region.width + 2 * tolerance,
|
||||
foundRegion.region.height + 2 * tolerance
|
||||
detectRegion?.x ?? defaultX,
|
||||
detectRegion?.y ?? defaultY,
|
||||
detectRegion?.width ?? defaultWidth,
|
||||
detectRegion?.height ?? defaultHeight
|
||||
);
|
||||
recognitionObject.threshold = 0.85;
|
||||
|
||||
// 保留原识别逻辑
|
||||
const result = await recognizeImage(
|
||||
recognitionObject,
|
||||
ra,
|
||||
1000, // timeout
|
||||
500, // interval
|
||||
1000,
|
||||
500,
|
||||
useNewScreenshot,
|
||||
dirName // iconType
|
||||
dirName
|
||||
);
|
||||
|
||||
// 保留原点击逻辑
|
||||
if (result.isDetected && result.x !== 0 && result.y !== 0) {
|
||||
const x = Math.round(result.x + result.width / 2);
|
||||
const y = Math.round(result.y + result.height / 2);
|
||||
log.info(`即将点击【${dirName}】类型下的图标:${foundRegion.iconName},位置: (${x}, ${y})`);
|
||||
await click(x, y);
|
||||
log.info(`点击【${dirName}】类型下的${foundRegion.iconName}成功`);
|
||||
await sleep(10);
|
||||
return { success: true };
|
||||
} else {
|
||||
// log.info(`未发现弹窗【${dirName}】的图标:${foundRegion.iconName}`);
|
||||
hasAnyIconDetected = true;
|
||||
isAnySuccess = true;
|
||||
const centerX = Math.round(result.x + result.width / 2);
|
||||
const centerY = Math.round(result.y + result.height / 2);
|
||||
const actualX = centerX + xOffset;
|
||||
const actualY = centerY + yOffset;
|
||||
log.info(`识别到【${dirName}】弹窗,偏移后位置(${actualX}, ${actualY})`);
|
||||
|
||||
if (!popupConfig.isSpecial) {
|
||||
// log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})`);
|
||||
// 新增:普通点击加循环(默认1次,0间隔,与原逻辑一致)
|
||||
const clickCount = popupConfig.loopCount;
|
||||
const clickDelay = popupConfig.loopDelay;
|
||||
for (let i = 0; i < clickCount; i++) {
|
||||
await click(actualX, actualY); // 保留原始点击逻辑
|
||||
if (i < clickCount - 1) await sleep(clickDelay); // 非最后一次加间隔
|
||||
}
|
||||
} else {
|
||||
switch (popupConfig.operationType) {
|
||||
case "key_press": {
|
||||
const targetKey = popupConfig.keyCode || "VK_SPACE";
|
||||
// 新增:key_press用循环(默认3次,1000ms间隔,与原硬编码逻辑一致)
|
||||
const pressCount = popupConfig.loopCount || 3;
|
||||
const pressDelay = popupConfig.loopDelay || 1000;
|
||||
for (let i = 0; i < pressCount; i++) {
|
||||
keyPress(targetKey); // 保留原始按键逻辑
|
||||
if (i < pressCount - 1) await sleep(pressDelay); // 非最后一次加间隔
|
||||
}
|
||||
log.info(`【${dirName}】弹窗触发按键【${targetKey}】,共${pressCount}次,间隔${pressDelay}ms`);
|
||||
break;
|
||||
}
|
||||
case "ocr_click": {
|
||||
isAnySuccess = false;
|
||||
const { targetTexts, xRange, yRange, timeout = 2000 } = popupConfig.ocrConfig || {};
|
||||
if (!targetTexts || !xRange || !yRange) {
|
||||
log.error(`【${dirName}】弹窗OCR配置不全,跳过`);
|
||||
break;
|
||||
}
|
||||
const ocrResults = await performOcr(targetTexts, xRange, yRange, timeout, ra);
|
||||
if (ocrResults.length > 0) {
|
||||
const ocrActualX = Math.round(ocrResults[0].x + ocrResults[0].width/2) + xOffset;
|
||||
const ocrActualY = Math.round(ocrResults[0].y + ocrResults[0].height/2) + yOffset;
|
||||
// 新增:OCR点击加循环(默认1次,0间隔,与原逻辑一致)
|
||||
const ocrCount = popupConfig.loopCount;
|
||||
const ocrDelay = popupConfig.loopDelay;
|
||||
for (let i = 0; i < ocrCount; i++) {
|
||||
await click(ocrActualX, ocrActualY); // 保留原始OCR点击逻辑
|
||||
if (i < ocrCount - 1) await sleep(ocrDelay); // 非最后一次加间隔
|
||||
}
|
||||
log.info(`【${dirName}】弹窗OCR点击“${ocrResults[0].text}”:(${ocrActualX}, ${ocrActualY}),共${ocrCount}次,间隔${ocrDelay}ms`);
|
||||
isAnySuccess = true;
|
||||
} else {
|
||||
log.warn(`【${dirName}】弹窗OCR未识别到文本`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "key_mouse_script": {
|
||||
try {
|
||||
const scriptPath = normalizePath(popupConfig.scriptPath || "");
|
||||
if (!scriptPath) {
|
||||
log.error(`【${dirName}】弹窗未配置键鼠脚本路径,跳过执行`);
|
||||
isAnySuccess = false;
|
||||
break;
|
||||
}
|
||||
if (!fileExists(scriptPath)) {
|
||||
log.error(`【${dirName}】弹窗键鼠脚本不存在:${scriptPath}`);
|
||||
isAnySuccess = false;
|
||||
break;
|
||||
}
|
||||
// 新增:键鼠脚本加循环(默认1次,0间隔,与原逻辑一致)
|
||||
const scriptCount = popupConfig.loopCount;
|
||||
const scriptDelay = popupConfig.loopDelay;
|
||||
for (let i = 0; i < scriptCount; i++) {
|
||||
await keyMouseScript.runFile(scriptPath); // 保留原始脚本执行逻辑
|
||||
if (i < scriptCount - 1) await sleep(scriptDelay); // 非最后一次加间隔
|
||||
}
|
||||
log.info(`【${dirName}】弹窗键鼠脚本执行完成,共${scriptCount}次,间隔${scriptDelay}ms`);
|
||||
isAnySuccess = true;
|
||||
} catch (error) {
|
||||
log.error(`【${dirName}】弹窗键鼠脚本执行失败:${error.message}`);
|
||||
isAnySuccess = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
log.error(`【${dirName}】弹窗未知操作类型:${popupConfig.operationType},默认执行点击`);
|
||||
// 新增:默认操作加循环(默认1次,0间隔,与原逻辑一致)
|
||||
const defaultCount = popupConfig.loopCount;
|
||||
const defaultDelay = popupConfig.loopDelay;
|
||||
for (let i = 0; i < defaultCount; i++) {
|
||||
await click(actualX, actualY); // 保留原始默认点击逻辑
|
||||
if (i < defaultCount - 1) await sleep(defaultDelay); // 非最后一次加间隔
|
||||
}
|
||||
isAnySuccess = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 仅在该弹窗无任何图标识别时输出警告
|
||||
if (!hasAnyIconDetected && !isAnySuccess) {
|
||||
// log.warn(`【${dirName}】弹窗未发现任何有效图标`);
|
||||
}
|
||||
await sleep(10);
|
||||
}
|
||||
|
||||
// 所有目标处理完毕仍未成功(保留原返回逻辑)
|
||||
return { success: false };
|
||||
return { success: isAnySuccess, message: isAnySuccess ? "弹窗识别操作成功" : "未识别到弹窗" };
|
||||
}
|
||||
|
||||
@@ -111,7 +111,6 @@ function parseMonsterMaterials() {
|
||||
}
|
||||
materialToMonsters[mat].add(monsterName);
|
||||
});
|
||||
log.debug(`${CONSTANTS.LOG_MODULES.MONSTER}解析怪物材料:${monsterName} -> [${materials.join(', ')}]`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -155,7 +154,11 @@ function getSelectedMaterialCategories() {
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const finalSettings = { ...initialSettings, ...settings };
|
||||
const finalSettings = Object.keys(initialSettings).reduce((acc, key) => {
|
||||
// 若settings中有该键则使用其值,否则用默认的false(确保只处理material_mapping中的键)
|
||||
acc[key] = settings.hasOwnProperty(key) ? settings[key] : initialSettings[key];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.keys(finalSettings)
|
||||
.filter(key => key !== "unselected")
|
||||
@@ -294,6 +297,7 @@ function readMaterialCD() {
|
||||
const materialCDCategories = {};
|
||||
|
||||
for (const filePath of materialFilePaths) {
|
||||
if (state.cancelRequested) break;
|
||||
const content = file.readTextSync(filePath);
|
||||
if (!content) {
|
||||
log.error(`${CONSTANTS.LOG_MODULES.CD}加载文件失败:${filePath}`);
|
||||
@@ -1156,7 +1160,6 @@ function classifyNormalPathFiles(pathingDir, targetResourceNames, lowCountMateri
|
||||
if (pathEntries.length > 0) {
|
||||
log.info(`${CONSTANTS.LOG_MODULES.PATH}\n===== 匹配到的材料路径列表 =====`);
|
||||
pathEntries.forEach((entry, index) => {
|
||||
log.info(`${index + 1}. 材料:${entry.resourceName},路径:${entry.path}`);
|
||||
});
|
||||
log.info(`=================================\n`);
|
||||
} else {
|
||||
@@ -1355,6 +1358,11 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
|
||||
=========================================\n\n`;
|
||||
writeContentToFile(summaryPath, content);
|
||||
log.info(`${CONSTANTS.LOG_MODULES.RECORD}最终汇总已记录至 ${summaryPath}`);
|
||||
// ==============================================
|
||||
// 新增:汇总后强制触发结束指令,确保程序终止
|
||||
// ==============================================
|
||||
state.completed = true; // 标记任务完全完成
|
||||
state.cancelRequested = true; // 终止所有后台任务(如图像点击、OCR)
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
@@ -1387,11 +1395,15 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个
|
||||
}
|
||||
// 关键补充:等待超量名单生成(由filterLowCountMaterials更新)
|
||||
let waitTimes = 0;
|
||||
while (excessMaterialNames.length === 0 && waitTimes < 300) {
|
||||
while (excessMaterialNames.length === 0 && !state.cancelRequested && waitTimes < 100) {
|
||||
await sleep(1000); // 每1秒查一次
|
||||
waitTimes++;
|
||||
}
|
||||
|
||||
// 若收到终止信号,直接退出OCR任务(不再执行后续逻辑)
|
||||
if (state.cancelRequested) {
|
||||
log.info(`${CONSTANTS.LOG_MODULES.MAIN}OCR任务收到终止信号,已退出`);
|
||||
return;
|
||||
}
|
||||
// 现在过滤才有效(确保excessMaterialNames已生成)
|
||||
allTargetTexts = allTargetTexts.filter(name => !excessMaterialNames.includes(name));
|
||||
log.info(`OCR最终目标文本(已过滤超量):${allTargetTexts.join('、')}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "背包统计采集系统",
|
||||
"version": "2.52",
|
||||
"version": "2.53",
|
||||
"bgi_version": "0.44.8",
|
||||
"description": "可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:\n+ 1. 自动判断材料CD,不需要管材料CD有没有好;\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别,不会交互路边的npc或是神像;可自定义识别名单,具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料,自动屏蔽;更多详细内容查看readme.md",
|
||||
"saved_files": [
|
||||
|
||||
@@ -102,12 +102,27 @@
|
||||
{
|
||||
"name": "PopupClickDelay",
|
||||
"type": "input-text",
|
||||
"label": "如 过期物品,信件,自定义文件夹名。注意分隔符和文件夹格式\n----------------------------------\n弹窗循环间隔(默认:5 秒)"
|
||||
"label": "如 过期物品,信件,自定义文件夹名。注意文件夹结构\n----------------------------------\n弹窗循环间隔(默认:15 秒)"
|
||||
},
|
||||
{
|
||||
"name": "CDCategories",
|
||||
"type": "input-text",
|
||||
"label": "----------------------------------\n\n采用的CD分类(默认:全部)"
|
||||
"label": "====================\n\n采用的CD分类(默认:全部) 举例:采集,怪物,木材"
|
||||
},
|
||||
{
|
||||
"name": "PickCategories",
|
||||
"type": "input-text",
|
||||
"label": "根据CD分类来加载路径文件,具体看materialsCD目录\n====================\n\n采用的识别名单(默认:全部) 举例:交互,采集,宝箱"
|
||||
},
|
||||
{
|
||||
"name": "ExceedCount",
|
||||
"type": "input-text",
|
||||
"label": "根据拾取分类来加载OCR名单,具体看targetText目录\n----------------------------------\n\n超量阈值(默认:5000)超量的路径材料将不拾取"
|
||||
},
|
||||
{
|
||||
"name": "PageScrollDistance",
|
||||
"type": "input-text",
|
||||
"label": "====================\n拖动距离:(默认711像素点)"
|
||||
},
|
||||
{
|
||||
"name": "HoldX",
|
||||
@@ -118,10 +133,5 @@
|
||||
"name": "HoldY",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n翻页拖动点Y坐标:0~1080(默认750)"
|
||||
},
|
||||
{
|
||||
"name": "PageScrollDistance",
|
||||
"type": "input-text",
|
||||
"label": "------------------------\n拖动距离:(默认711像素点)"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||