diff --git a/repo/js/背包材料统计/lib/autoPick.js b/repo/js/背包材料统计/lib/autoPick.js index 73804d597..f7aa33f71 100644 --- a/repo/js/背包材料统计/lib/autoPick.js +++ b/repo/js/背包材料统计/lib/autoPick.js @@ -58,93 +58,6 @@ function readtargetTextCategories(targetTextDir) { } return materialCategories; } -// 定义替换映射表 -const replacementMap = { - "监": "盐", - "炽": "烬", - "盞": "盏", - "攜": "携", - "於": "于", - "卵": "卯" -}; - -/** - * 执行OCR识别并匹配目标文本 - * @param {string[]} targetTexts - 待匹配的目标文本列表 - * @param {Object} xRange - X轴范围 { min: number, max: number } - * @param {Object} yRange - Y轴范围 { min: number, max: number } - * @param {number} timeout - 超时时间(毫秒),默认200ms - * @param {Object} ra - 图像捕获对象(外部传入,需确保已初始化) - * @returns {Promise} 识别结果数组,包含匹配目标的文本及坐标信息 - */ -async function performOcr(targetTexts, xRange, yRange, timeout = 10, ra = null) { - // 正则特殊字符转义工具函数(避免替换时的正则语法错误) - const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - - const startTime = Date.now(); // 记录开始时间,用于超时判断 - - // 2. 计算识别区域宽高(xRange.max - xRange.min 为宽度,y同理) - const regionWidth = xRange.max - xRange.min; - const regionHeight = yRange.max - yRange.min; - if (regionWidth <= 0 || regionHeight <= 0) { - throw new Error(`无效的识别区域:宽=${regionWidth}, 高=${regionHeight}`); - } - - // 在超时时间内循环重试识别(处理临时识别失败) - while (Date.now() - startTime < timeout) { - try { - // 1. 检查图像捕获对象是否有效 - if (!ra) { - throw new Error("图像捕获对象(ra)未初始化"); - } - - // 3. 执行OCR识别(在指定区域内查找多结果) - const resList = ra.findMulti( - RecognitionObject.ocr(xRange.min, yRange.min, regionWidth, regionHeight) - ); - - // 4. 处理识别结果(文本修正 + 目标匹配) - const results = []; - for (let i = 0; i < resList.count; i++) { - const res = resList[i]; - let correctedText = res.text; // 原始识别文本 - log.info(`原始识别文本: ${res.text}`); - - // 4.1 修正识别错误(替换错误字符) - for (const [wrongChar, correctChar] of Object.entries(replacementMap)) { - const escapedWrong = escapeRegExp(wrongChar); // 转义特殊字符 - correctedText = correctedText.replace(new RegExp(escapedWrong, 'g'), correctChar); - } - - // 4.2 检查是否包含任意目标文本(避免重复添加同个结果) - const isTargetMatched = targetTexts.some(target => correctedText.includes(target)); - if (isTargetMatched) { - results.push({ - text: correctedText, - x: res.x, - y: res.y, - width: res.width, - height: res.height - }); - } - } - - // 5. 识别成功,返回结果(无论是否匹配到目标,均返回当前识别到的有效结果) - return results; - - } catch (error) { - // 识别异常时记录日志,继续重试(直到超时) - log.error(`OCR识别异常(将重试): ${error.message}`); - // 短暂等待后重试,避免高频失败占用资源 - await sleep(1); - } - await sleep(5); // 每次间隔 5 毫秒 - } - - // 超时未完成识别,返回空数组 - log.warn(`OCR识别超时(超过${timeout}ms)`); - return []; -} // const OCRdelay = Math.min(100, Math.max(0, Math.floor(Number(settings.OcrDelay) || 2))); // F识别基准时长 @@ -188,18 +101,25 @@ const ScrollRo = RecognitionObject.TemplateMatch( */ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, texttolerance, cachedFrame = null) { let lastLogTime = Date.now(); - // 记录每个材料的识别次数(文本+坐标 → 计数) - const recognitionCount = new Map(); + const recognitionCount = new Map(); // 避免误触:文本+Y坐标 → 计数 + const ocrScreenshots = []; // 收集最新版performOcr返回的截图,统一释放 - while (!state.completed && !state.cancelRequested) { - const currentTime = Date.now(); - if (currentTime - lastLogTime >= 10000) { - log.info("检测中..."); - lastLogTime = currentTime; - } - await sleep(50); - cachedFrame?.dispose(); - cachedFrame = captureGameRegion(); + try { + while (!state.completed && !state.cancelRequested) { + const currentTime = Date.now(); + // 每10秒输出检测日志(保留原逻辑) + if (currentTime - lastLogTime >= 10000) { + log.info("独立OCR识别中..."); + lastLogTime = currentTime; + } + await sleep(50); + + // 1. 释放上一帧缓存,捕获新帧(保留原逻辑) + if (cachedFrame) { + if (cachedFrame.Dispose) cachedFrame.Dispose(); + else if (cachedFrame.dispose) cachedFrame.dispose(); + } + cachedFrame = captureGameRegion(); // 尝试找到 F 图标 let fRes = await findFIcon(fDialogueRo, 10, cachedFrame); @@ -211,21 +131,33 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text continue; // 继续下一轮检测 } - // 获取 F 图标的中心点 Y 坐标 - let centerYF = fRes.y + fRes.height / 2; - let ocrResults = await performOcr(targetTexts, textxRange, { min: fRes.y - 3, max: fRes.y + 37 }, 10, cachedFrame); + // 3. 核心改造:调用最新版performOcr + // 适配点1:参数顺序调整为「targetTexts, xRange, yRange, ra, timeout, interval」 + // 适配点2:接收返回的「results+screenshot」,并收集screenshot + const yRange = { min: fRes.y - 3, max: fRes.y + 37 }; // 原Y轴范围不变 + const { results: ocrResults, screenshot: ocrScreenshot } = await performOcr( + targetTexts, // 目标文本列表(原逻辑) + textxRange, // 文本X轴范围(原逻辑) + yRange, // 文本Y轴范围(原逻辑) + cachedFrame, // 初始截图(最新版:第4个参数为ra) + 10, // 超时时间(保留原10ms) + 5 // 重试间隔(保留原5ms) + ); + ocrScreenshots.push(ocrScreenshot); // 收集截图,避免内存泄漏 - // 检查所有目标文本是否在当前页面中 - let foundTarget = false; - for (let targetText of targetTexts) { - let targetResult = ocrResults.find(res => res.text.includes(targetText)); - if (targetResult) { - + // 4. 文本匹配与交互(保留原逻辑,无修改) + let foundTarget = false; + for (const targetText of targetTexts) { + const targetResult = ocrResults.find(res => res.text.includes(targetText)); + if (!targetResult) continue; + + // 计数防误触(原逻辑) const materialId = `${targetText}-${targetResult.y}`; recognitionCount.set(materialId, (recognitionCount.get(materialId) || 0) + 1); - - let centerYTargetText = targetResult.y + targetResult.height / 2; - if (Math.abs(centerYTargetText - centerYF) <= texttolerance) { + + // Y轴对齐判断(原逻辑) + const centerYTargetText = targetResult.y + targetResult.height / 2; + if (Math.abs(centerYTargetText - (fRes.y + fRes.height / 2)) <= texttolerance) { if (recognitionCount.get(materialId) >= 1) { keyPress("F"); log.info(`交互或拾取: ${targetText}`); @@ -236,20 +168,33 @@ async function alignAndInteractTarget(targetTexts, fDialogueRo, textxRange, text break; } } - } - if (!foundTarget) { - await keyMouseScript.runFile(`assets/滚轮下翻.json`); + // 5. 未找到目标则翻滚(保留原逻辑) + if (!foundTarget) { + await keyMouseScript.runFile(`assets/滚轮下翻.json`); + } } + } catch (error) { + log.error(`对齐交互异常: ${error.message}`); + } finally { + // 6. 统一释放所有资源(新增:解决内存泄漏) + // 释放缓存帧 + if (cachedFrame) { + if (cachedFrame.Dispose) cachedFrame.Dispose(); + else if (cachedFrame.dispose) cachedFrame.dispose(); + } + // 释放OCR截图 + for (const screenshot of ocrScreenshots) { + if (screenshot) { + if (screenshot.Dispose) screenshot.Dispose(); + else if (screenshot.dispose) screenshot.dispose(); + } + } + // 任务状态日志(保留原逻辑) if (state.cancelRequested) { - break; + log.info("检测任务已取消"); + } else if (!state.completed) { + log.error("未能找到正确的目标文本或未成功交互,跳过该目标文本"); } - cachedFrame?.dispose(); - } - - if (state.cancelRequested) { - log.info("检测任务已取消"); - } else if (!state.completed) { - log.error("未能找到正确的目标文本或未成功交互,跳过该目标文本"); } } diff --git a/repo/js/背包材料统计/lib/backStats.js b/repo/js/背包材料统计/lib/backStats.js index d60db7a51..87e20d1ee 100644 --- a/repo/js/背包材料统计/lib/backStats.js +++ b/repo/js/背包材料统计/lib/backStats.js @@ -44,7 +44,19 @@ const materialPriority = { "武器突破素材": 6, }; - +// 2. 数字替换映射表(处理OCR识别误差) +var numberReplaceMap = { + "O": "0", "o": "0", "Q": "0", "0": "0", + "I": "1", "l": "1", "i": "1", "1": "1", "一": "1", + "Z": "2", "z": "2", "2": "2", "二": "2", + "E": "3", "e": "3", "3": "3", "三": "3", + "A": "4", "a": "4", "4": "4", + "S": "5", "s": "5", "5": "5", + "G": "6", "b": "6", "6": "6", + "T": "7", "t": "7", "7": "7", + "B": "8", "θ": "8", "8": "8", + "g": "9", "q": "9", "9": "9", +}; // 提前计算所有动态坐标 // 物品区左顶处物品左上角坐标(117,121) @@ -63,19 +75,6 @@ async function recognizeText(ocrRegion, timeout = 100, retryInterval = 20, maxAt // const results = []; const frequencyMap = {}; // 用于记录每个结果的出现次数 - const numberReplaceMap = { - "O": "0", "o": "0", "Q": "0", "0": "0", - "I": "1", "l": "1", "i": "1", "1": "1", "一": "1", - "Z": "2", "z": "2", "2": "2", "二": "2", - "E": "3", "e": "3", "3": "3", "三": "3", - "A": "4", "a": "4", "4": "4", - "S": "5", "s": "5", "5": "5", - "G": "6", "b": "6", "6": "6", - "T": "7", "t": "7", "7": "7", - "B": "8", "θ": "8", "8": "8", - "g": "9", "q": "9", "9": "9", - }; - while (Date.now() - startTime < timeout && retryCount < maxAttempts) { let ocrObject = RecognitionObject.Ocr(ocrRegion.x, ocrRegion.y, ocrRegion.width, ocrRegion.height); ocrObject.threshold = 0.85; // 适当降低阈值以提高速度 @@ -296,26 +295,26 @@ async function scanMaterials(materialsCategory, materialCategoryMap) { // 俏皮话逻辑 const scanPhrases = [ - "扫描中... 太好啦,有这么多素材!", - "扫描中... 不错的珍宝!", - "扫描中... 侦查骑士,发现目标!", - "扫描中... 嗯哼,意外之喜!", - "扫描中... 嗯?", - "扫描中... 很好,没有放过任何角落!", - "扫描中... 会有烟花材料嘛?", - "扫描中... 嗯,这是什么?", - "扫描中... 这些宝藏积灰了,先清洗一下", - "扫描中... 哇!都是好东西!", - "扫描中... 不虚此行!", - "扫描中... 瑰丽的珍宝,令人欣喜。", - "扫描中... 是对长高有帮助的东西吗?", - "扫描中... 嗯!品相卓越!", - "扫描中... 虽无法比拟黄金,但终有价值。", - "扫描中... 收获不少,可以拿去换几瓶好酒啦。", - "扫描中... 房租和伙食费,都有着落啦!", - "扫描中... 还不赖。", - "扫描中... 荒芜的世界,竟藏有这等瑰宝。", - "扫描中... 运气还不错。", + "... 太好啦,有这么多素材!", + "... 不错的珍宝!", + "... 侦查骑士,发现目标!", + "... 嗯哼,意外之喜!", + "... 嗯?", + "... 很好,没有放过任何角落!", + "... 会有烟花材料嘛?", + "... 嗯,这是什么?", + "... 这些宝藏积灰了,先清洗一下", + "... 哇!都是好东西!", + "... 不虚此行!", + "... 瑰丽的珍宝,令人欣喜。", + "... 是对长高有帮助的东西吗?", + "... 嗯!品相卓越!", + "... 虽无法比拟黄金,但终有价值。", + "... 收获不少,可以拿去换几瓶好酒啦。", + "... 房租和伙食费,都有着落啦!", + "... 还不赖。", + "... 荒芜的世界,竟藏有这等瑰宝。", + "... 运气还不错。", ]; let tempPhrases = [...scanPhrases]; @@ -462,45 +461,15 @@ ${Array.from(unmatchedMaterialNames).join(",")} const overwriteFilePath = `overwrite_record/${materialsCategory}.txt`; // 所有的历史记录分类储存 const latestFilePath = "latest_record.txt"; // 所有的历史记录集集合 if (pathingMode.onlyCategory) { - writeLog(categoryFilePath, logContent); + writeFile(categoryFilePath, logContent); } - writeLog(overwriteFilePath, logContent); - writeLog(latestFilePath, logContent); // 覆盖模式? + writeFile(overwriteFilePath, logContent); + writeFile(latestFilePath, logContent); // 覆盖模式? // 返回结果 return materialInfo; } -function writeLog(filePath, logContent) { - try { - // 1. 读取现有内容(原样读取,不做任何分割处理) - let existingContent = ""; - try { - existingContent = file.readTextSync(filePath); - } catch (e) { - // 文件不存在则保持空 - } - - // 2. 拼接新记录(新记录加在最前面,用两个换行分隔,保留原始格式) - const finalContent = logContent + "\n\n" + existingContent; - - // 3. 按行分割,保留最近365条完整记录(按原始换行分割,不过滤) - const lines = finalContent.split("\n"); - const keepLines = lines.length > 365 * 5 ? lines.slice(0, 365 * 5) : lines; // 假设每条记录最多5行 - const result = file.writeTextSync(filePath, keepLines.join("\n"), false); - - if (result) { - log.info(`写入成功: ${filePath}`); - } else { - log.error(`写入失败: ${filePath}`); - } - } catch (error) { - // 只在文件完全不存在时创建,避免覆盖 - file.writeTextSync(filePath, logContent, false); - log.info(`创建新文件: ${filePath}`); - } -} - // 定义所有图标的图像识别对象,每个图片都有自己的识别区域 const BagpackRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Bagpack.png"), 58, 31, 38, 38); const MaterialsRo = RecognitionObject.TemplateMatch(file.ReadImageMatSync("assets/Materials.png"), 941, 29, 38, 38); @@ -565,7 +534,11 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) { log.info(`类型 ${group.type} | 包含分类: ${group.categories.join(', ')}`); }); - while (stage <= maxStage) { + let loopCount = 0; + const maxLoopCount = 200; // 合理阈值,正常流程约50-100次循环 + + while (stage <= maxStage && loopCount <= maxLoopCount) { // ===== 补充优化:加入循环次数限制 ===== + loopCount++; switch (stage) { case 0: // 返回主界面 log.info("返回主界面"); @@ -588,6 +561,8 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) { stage = 2; // 进入下一阶段 } else { log.warn("未识别到背包图标,重新尝试"); + // ===== 补充优化:连续回退时释放资源 ===== + cachedFrame?.dispose(); stage = 0; // 回退 } break; @@ -643,6 +618,8 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) { break; default: log.error("未知的材料分类"); + // ===== 补充优化:异常时释放资源并退出 ===== + cachedFrame?.dispose(); stage = 0; // 回退到阶段0 return; } @@ -653,7 +630,9 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) { stage = 4; // 进入下一阶段 } else { log.warn("未识别到材料分类图标,重新尝试"); - log.warn(`识别结果:${JSON.stringify(CategoryResult)}`); + // log.warn(`识别结果:${JSON.stringify(CategoryResult)}`); + // ===== 补充优化:连续回退时释放资源 ===== + cachedFrame?.dispose(); stage = 2; // 回退到阶段2 } break; @@ -683,6 +662,14 @@ async function MaterialPath(materialCategoryMap, cachedFrame = null) { } } + // ===== 补充优化:循环超限处理,防止卡死 ===== + if (loopCount > maxLoopCount) { + log.error(`主循环次数超限(${maxLoopCount}次),强制退出`); + cachedFrame?.dispose(); + await genshin.returnMainUi(); + return []; + } + await genshin.returnMainUi(); // 返回主界面 log.info("扫描流程结束"); diff --git a/repo/js/背包材料统计/lib/exp.js b/repo/js/背包材料统计/lib/exp.js index 255cbca5c..5816c5ed5 100644 --- a/repo/js/背包材料统计/lib/exp.js +++ b/repo/js/背包材料统计/lib/exp.js @@ -1,35 +1,20 @@ - // ===================== 狗粮模式专属函数 ===================== -// 1. 狗粮分解配置与OCR区域 +// 1. 狗粮分解配置与OCR区域(保留原配置,无修改) const AUTO_SALVAGE_CONFIG = { autoSalvage3: settings.autoSalvage3 || "是", autoSalvage4: settings.autoSalvage4 || "是" }; -const OCR_REGIONS = { +const EXP_OCRREGIONS = { expStorage: { x: 1472, y: 883, width: 170, height: 34 }, expCount: { x: 1472, y: 895, width: 170, height: 34 } }; -// 2. 数字替换映射表(处理OCR识别误差) -const numberReplaceMap = { - "O": "0", "o": "0", "Q": "0", "0": "0", - "I": "1", "l": "1", "i": "1", "1": "1", "一": "1", - "Z": "2", "z": "2", "2": "2", "二": "2", - "E": "3", "e": "3", "3": "3", "三": "3", - "A": "4", "a": "4", "4": "4", - "S": "5", "s": "5", "5": "5", - "G": "6", "b": "6", "6": "6", - "T": "7", "t": "7", "7": "7", - "B": "8", "θ": "8", "8": "8", - "g": "9", "q": "9", "9": "9", -}; - -// 3. OCR文本处理 -function processExpText(text) { +// 处理数字文本:保留原逻辑(复用全局数字替换能力) +function processNumberText(text) { let correctedText = text || ""; let removedSymbols = []; - // 替换错误字符 + // 替换错误字符(依赖全局 numberReplaceMap) for (const [wrong, correct] of Object.entries(numberReplaceMap)) { correctedText = correctedText.replace(new RegExp(wrong, 'g'), correct); } @@ -50,64 +35,88 @@ function processExpText(text) { }; } -// 4. OCR识别EXP -async function recognizeExpRegion(regionName, ra = null, timeout = 2000) { - const ocrRegion = OCR_REGIONS[regionName]; +// 4. OCR识别EXP(核心改造:用 performOcr 替代重复OCR逻辑) +async function recognizeExpRegion(regionName, initialRa = null, timeout = 2000) { + // 1. 基础校验(保留原逻辑) + const ocrRegion = EXP_OCRREGIONS[regionName]; if (!ocrRegion) { log.error(`[狗粮OCR] 无效区域:${regionName}`); - return { success: false, expCount: 0 }; + return { success: false, expCount: 0, screenshot: null }; // 新增返回截图(便于调试) } log.info(`[狗粮OCR] 识别${regionName}(x=${ocrRegion.x}, y=${ocrRegion.y})`); - const startTime = Date.now(); - let retryCount = 0; + let ocrScreenshot = null; // 存储performOcr返回的有效截图 - while (Date.now() - startTime < timeout) { - try { - const ocrResult = ra.find(RecognitionObject.ocr( - ocrRegion.x, - ocrRegion.y, - ocrRegion.width, - ocrRegion.height - )); - log.info(`[狗粮OCR] 原始文本:${ocrResult.text}`); + try { + // 2. 转换OCR区域格式:ocrRegion(x,y,width,height) → xRange/yRange(min/max) + const xRange = { + min: ocrRegion.x, + max: ocrRegion.x + ocrRegion.width + }; + const yRange = { + min: ocrRegion.y, + max: ocrRegion.y + ocrRegion.height + }; - if (ocrResult?.text) { - const { processedText, removedSymbols } = processExpText(ocrResult.text); - if (removedSymbols.length > 0) { - log.info(`[狗粮OCR] 去除无效字符:${removedSymbols.join(', ')}`); - } - const expCount = processedText ? parseInt(processedText, 10) : 0; - log.info(`[狗粮OCR] ${regionName}结果:${expCount}`); - return { success: true, expCount }; + // 3. 调用新版 performOcr(自动重截图、资源管理、异常处理) + // 目标文本传空数组:识别数字无需匹配特定文本,仅需提取内容 + const { results, screenshot } = await performOcr( + [""], // targetTexts:空数组(数字识别无特定目标) + xRange, // 转换后的X轴范围 + yRange, // 转换后的Y轴范围 + initialRa, // 初始截图(外部传入) + timeout, // 超时时间(复用原参数) + 50 // 重试间隔(默认50ms,比原500ms更灵敏) + ); + ocrScreenshot = screenshot; // 暂存截图,后续返回或释放 + + // 4. 处理OCR结果(保留原数字处理+日志逻辑) + if (results.length > 0) { + const { originalText, text: correctedText } = results[0]; // 从performOcr拿原始/修正文本 + log.info(`[狗粮OCR] 原始文本:${originalText}`); // 保持原日志格式 + + // 用原processNumberText提纯数字 + const { processedText, removedSymbols } = processNumberText(correctedText); + if (removedSymbols.length > 0) { + log.info(`[狗粮OCR] 去除无效字符:${removedSymbols.join(', ')}`); // 保留原日志 } - } catch (error) { - retryCount++; - log.warn(`[狗粮OCR] ${regionName}第${retryCount}次识别失败:${error.message}`); + + const expCount = processedText ? parseInt(processedText, 10) : 0; + log.info(`[狗粮OCR] ${regionName}结果:${expCount}`); // 保留原日志 + return { success: true, expCount, screenshot: ocrScreenshot }; // 返回截图(调试用) + } + + } catch (error) { + // 捕获performOcr未处理的异常(如参数错误) + log.error(`[狗粮OCR] ${regionName}识别异常:${error.message}`); + // 异常时释放截图资源 + if (ocrScreenshot) { + if (ocrScreenshot.Dispose) ocrScreenshot.Dispose(); + else if (ocrScreenshot.dispose) ocrScreenshot.dispose(); } - await sleep(500); } + // 5. 识别失败/超时(保留原逻辑) log.error(`[狗粮OCR] ${regionName}超时未识别,默认0`); - return { success: false, expCount: 0 }; + return { success: false, expCount: 0, screenshot: ocrScreenshot }; // 超时也返回截图(排查用) } -// 5. 狗粮分解流程 +// 5. 狗粮分解流程(调整:适配recognizeExpRegion的新返回值,优化资源释放) async function executeSalvageWithOCR() { log.info("[狗粮分解] 开始执行分解流程"); let storageExp = 0; let countExp = 0; - let cachedFrame = null; + let ocrScreenshots = []; // 存储识别过程中产生的截图(统一释放) try { - keyPress("B"); await sleep(1000); + keyPress("B"); + await sleep(1000); + const coords = [ [670, 40], // 打开背包 [660, 1010], // 打开分解 [300, 1020], // 打开分解选项页面 - // [200, 150, 500], // 勾选1星狗粮 - // [200, 220, 500], // 勾选2星狗粮 [200, 300, 500, AUTO_SALVAGE_CONFIG.autoSalvage3 !== '否'], // 3星(按配置) [200, 380, 500, AUTO_SALVAGE_CONFIG.autoSalvage4 !== '否'], // 4星(按配置) [340, 1000], // 确认选择 @@ -125,35 +134,64 @@ async function executeSalvageWithOCR() { await sleep(delay); log.debug(`[狗粮分解] 点击(${x},${y}),延迟${delay}ms`); - // 分解前识别储存EXP + // 分解前识别储存EXP(适配新的recognizeExpRegion返回值) if (x === 660 && y === 1010) { - cachedFrame?.dispose(); + // 释放旧缓存帧 + if (cachedFrame) { + if (cachedFrame.Dispose) cachedFrame.Dispose(); + else if (cachedFrame.dispose) cachedFrame.dispose(); + } + // 捕获新帧 cachedFrame = captureGameRegion(); - const { expCount } = await recognizeExpRegion("expStorage", cachedFrame, 1000); + // 调用改造后的recognizeExpRegion(接收expCount和screenshot) + const { expCount, screenshot } = await recognizeExpRegion("expStorage", cachedFrame, 1000); storageExp = expCount; + ocrScreenshots.push(screenshot); // 收集截图(后续统一释放) } - // 分解后识别新增EXP + // 分解后识别新增EXP(同上,适配新返回值) if (x === 340 && y === 1000) { - cachedFrame?.dispose(); + if (cachedFrame) { + if (cachedFrame.Dispose) cachedFrame.Dispose(); + else if (cachedFrame.dispose) cachedFrame.dispose(); + } cachedFrame = captureGameRegion(); - const { expCount } = await recognizeExpRegion("expCount", cachedFrame, 1000); + const { expCount, screenshot } = await recognizeExpRegion("expCount", cachedFrame, 1000); countExp = expCount; + ocrScreenshots.push(screenshot); // 收集截图 } } } - const totalExp = countExp - storageExp; // 分解新增EXP = 分解后 - 分解前 + // 计算并返回结果(保留原逻辑) + const totalExp = countExp - storageExp; log.info(`[狗粮分解] 完成,新增EXP:${totalExp}(分解前:${storageExp},分解后:${countExp})`); - return { success: true, totalExp: Math.max(totalExp, 0) }; // 避免负数 + return { success: true, totalExp: Math.max(totalExp, 0) }; + } catch (error) { log.error(`[狗粮分解] 失败:${error.message}`); return { success: false, totalExp: 0 }; + + } finally { + // 最终统一释放所有资源(避免内存泄漏) + // 1. 释放缓存帧 + if (cachedFrame) { + if (cachedFrame.Dispose) cachedFrame.Dispose(); + else if (cachedFrame.dispose) cachedFrame.dispose(); + } + // 2. 释放OCR过程中产生的截图 + for (const screenshot of ocrScreenshots) { + if (screenshot) { + if (screenshot.Dispose) screenshot.Dispose(); + else if (screenshot.dispose) screenshot.dispose(); + } + } + log.debug("[狗粮分解] 所有资源已释放"); } } -// 6. 判断是否为狗粮资源(关键词匹配) +// 6. 判断是否为狗粮资源(保留原逻辑,无修改) function isFoodResource(resourceName) { const foodKeywords = ["12h狗粮", "24h狗粮"]; return resourceName && foodKeywords.some(keyword => resourceName.includes(keyword)); -} +} \ No newline at end of file diff --git a/repo/js/背包材料统计/lib/file.js b/repo/js/背包材料统计/lib/file.js index a5d3d5666..b960e84a1 100644 --- a/repo/js/背包材料统计/lib/file.js +++ b/repo/js/背包材料统计/lib/file.js @@ -78,43 +78,8 @@ function readAllFilePaths(dir, depth = 0, maxDepth = 3, includeExtensions = ['.p return []; } } -/* -// 新记录在最下面 -async function writeFile(filePath, content, isAppend = false, maxRecords = 365) { - try { - if (isAppend) { - // 读取现有内容(如果文件不存在则为空) - const existingContent = file.readTextSync(filePath) || ""; - // 分割成记录数组(过滤空字符串) - const records = existingContent.split("\n\n").filter(Boolean); - - // 关键修复:将新内容添加到末尾,然后只保留最后maxRecords条 - const allRecords = [...records, content]; // 新内容放在最后 - const latestRecords = allRecords.slice(-maxRecords); // 整体截取最新的maxRecords条 - - // 拼接成最终内容 - const finalContent = latestRecords.join("\n\n"); - const result = file.WriteTextSync(filePath, finalContent, false); - - // 日志输出(可根据需要启用) - // log.info(result ? `[追加] 成功写入: ${filePath}` : `[追加] 写入失败: ${filePath}`); - return result; - } else { - // 非追加模式:直接覆盖写入 - const result = file.WriteTextSync(filePath, content, false); - // log.info(result ? `[覆盖] 成功写入: ${filePath}` : `[覆盖] 写入失败: ${filePath}`); - return result; - } - } catch (error) { - // 发生错误时尝试创建文件并写入 - const result = file.WriteTextSync(filePath, content, false); - log.info(result ? `[新建] 成功创建: ${filePath}` : `[新建] 创建失败: ${filePath}`); - return result; - } -} -*/ -// 新记录在最上面20250531 -async function writeFile(filePath, content, isAppend = false, maxRecords = 365) { +// 新记录在最上面20250531 isAppend默认就是true追加 +function writeFile(filePath, content, isAppend = true, maxRecords = 36500) { try { if (isAppend) { // 读取现有内容,处理文件不存在的情况 diff --git a/repo/js/背包材料统计/lib/imageClick.js b/repo/js/背包材料统计/lib/imageClick.js index 1eeecf484..430de9df5 100644 --- a/repo/js/背包材料统计/lib/imageClick.js +++ b/repo/js/背包材料统计/lib/imageClick.js @@ -104,17 +104,19 @@ async function preloadImageResources(specificNames) { }); } - 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 - }]; + // 关键修改:遍历所有图标,为每个图标生成识别信息 + const foundRegions = []; // 存储所有图标的识别配置 + for (const targetIcon of iconRecognitionObjects) { // 遍历每个图标 + const manualRegion = new ImageRegion(targetIcon.mat, specialDetectRegion.x, specialDetectRegion.y); + manualRegion.width = specialDetectRegion.width; + manualRegion.height = specialDetectRegion.height; + foundRegions.push({ + pictureName: "特殊模块", + iconName: targetIcon.name, // 当前图标的名称 + region: manualRegion, // 复用同一个detectRegion区域 + iconDir: iconDir + }); + } // log.info(`【${dirName}】特殊模块生成识别区域:x=${manualRegion.x}, y=${manualRegion.y}, 宽=${manualRegion.width}, 高=${manualRegion.height}`); preloadedResources.push({ @@ -251,11 +253,12 @@ async function imageClickBackgroundTask() { // 遍历所有一级弹窗 for (const currentFirstLevel of firstLevelDirs) { + log.info(`【${currentFirstLevel.dirName}】准备识别...`); // 检查当前一级弹窗是否被触发 const levelResult = await imageClick([currentFirstLevel], null, [currentFirstLevel.dirName], true); if (levelResult.success) { - log.info(`【${currentFirstLevel.dirName}】触发成功,进入内部流程...`); + // log.info(`【${currentFirstLevel.dirName}】触发成功,进入内部流程...`); const levelStack = [currentFirstLevel]; // 内循环处理内部流程 @@ -335,6 +338,7 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u detectRegion?.width ?? defaultWidth, detectRegion?.height ?? defaultHeight ); + // log.info(JSON.stringify(detectRegion, null, 2)); recognitionObject.threshold = 0.85; const result = await recognizeImage( @@ -356,12 +360,12 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u 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); // 保留原始点击逻辑 + // log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})${i+1}次`); if (i < clickCount - 1) await sleep(clickDelay); // 非最后一次加间隔 } } else { @@ -370,9 +374,10 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u const targetKey = popupConfig.keyCode || "VK_SPACE"; // 新增:key_press用循环(默认3次,1000ms间隔,与原硬编码逻辑一致) const pressCount = popupConfig.loopCount || 3; - const pressDelay = popupConfig.loopDelay || 1000; + const pressDelay = popupConfig.loopDelay || 500; for (let i = 0; i < pressCount; i++) { keyPress(targetKey); // 保留原始按键逻辑 + log.info(`【${dirName}】弹窗触发按键【${targetKey}】${i+1}次`); if (i < pressCount - 1) await sleep(pressDelay); // 非最后一次加间隔 } log.info(`【${dirName}】弹窗触发按键【${targetKey}】,共${pressCount}次,间隔${pressDelay}ms`); @@ -438,6 +443,7 @@ async function imageClick(preloadedResources, ra = null, specificNames = null, u const defaultDelay = popupConfig.loopDelay; for (let i = 0; i < defaultCount; i++) { await click(actualX, actualY); // 保留原始默认点击逻辑 + log.info(`点击【${dirName}】弹窗:(${actualX}, ${actualY})${i+1}次`); if (i < defaultCount - 1) await sleep(defaultDelay); // 非最后一次加间隔 } isAnySuccess = true; diff --git a/repo/js/背包材料统计/lib/ocr.js b/repo/js/背包材料统计/lib/ocr.js new file mode 100644 index 000000000..44aef5aae --- /dev/null +++ b/repo/js/背包材料统计/lib/ocr.js @@ -0,0 +1,122 @@ +// 定义替换映射表 +const replacementMap = { + "监": "盐", + "炽": "烬", + "盞": "盏", + "攜": "携", + "於": "于", + "卵": "卯" +}; + + +/** + * 执行OCR识别并匹配目标文本(失败自动重截图,返回结果+有效截图) + * @param {string[]} targetTexts - 待匹配的目标文本列表 + * @param {Object} xRange - X轴范围 { min: number, max: number } + * @param {Object} yRange - Y轴范围 { min: number, max: number } + * @param {Object} ra - 初始图像捕获对象(外部传入,需确保已初始化) + * @param {number} timeout - 超时时间(毫秒),默认200ms + * @param {number} interval - 重试间隔(毫秒),默认50ms + * @returns {Promise<{ + * results: Object[], // 识别结果数组(含文本、坐标) + * screenshot: Object // 有效截图(成功时用的截图/超时前最后一次截图) + * }>} + */ +async function performOcr(targetTexts, xRange, yRange, ra = null, timeout = 200, interval = 50) { + // 正则特殊字符转义工具函数(避免替换时的正则语法错误) + const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + const startTime = Date.now(); // 记录开始时间,用于超时判断 + let currentScreenshot = ra; // 跟踪当前有效截图(初始为外部传入的原图) + + // 1. 初始参数校验(提前拦截无效输入) + if (!currentScreenshot) { + throw new Error("初始图像捕获对象(ra)未初始化,请传入有效截图"); + } + const regionWidth = xRange.max - xRange.min; + const regionHeight = yRange.max - yRange.min; + if (regionWidth <= 0 || regionHeight <= 0) { + throw new Error(`无效的识别区域:宽=${regionWidth}, 高=${regionHeight}`); + } + + // 在超时时间内循环重试识别(处理临时失败,自动重截图) + while (Date.now() - startTime < timeout) { + // 额外增加空值检查,防止currentScreenshot变为null + if (!currentScreenshot) { + log.error("currentScreenshot为null,尝试重新捕获"); + currentScreenshot = captureGameRegion(); + await sleep(interval); + continue; + } + + try { + // 2. 执行OCR识别(基于当前有效截图的指定区域) + const resList = currentScreenshot.findMulti( + RecognitionObject.ocr(xRange.min, yRange.min, regionWidth, regionHeight) + ); + + // 3. 处理识别结果(文本修正 + 目标匹配) + const results = []; + for (let i = 0; i < resList.count; i++) { + const res = resList[i]; + let correctedText = res.text; // 修正后的文本 + const originalText = res.text; // 保留原始识别文本(便于调试) + log.debug(`OCR原始文本: ${res.text}`); + + // 3.1 修正OCR常见错误(基于替换映射表) + for (const [wrongChar, correctChar] of Object.entries(replacementMap)) { + const escapedWrong = escapeRegExp(wrongChar); + correctedText = correctedText.replace(new RegExp(escapedWrong, 'g'), correctChar); + } + + // 3.2 匹配目标文本(避免重复添加同一结果) + const isTargetMatched = targetTexts.some(target => correctedText.includes(target)); + if (isTargetMatched) { + results.push({ + text: correctedText, // 最终修正后的文本 + originalText: originalText, // 原始识别文本(调试用) + x: res.x, y: res.y, // 文本在截图中的X/Y坐标 + width: res.width, height: res.height // 文本区域尺寸 + }); + } + } + + // 4. 识别成功:返回「结果数组 + 本次成功用的截图」 + // log.info(`OCR识别完成,匹配到${results.length}个目标文本`); + return { + results: results, + screenshot: currentScreenshot // 成功截图(与结果对应的有效画面) + }; + + } catch (error) { + // 5. 识别失败:释放旧截图→重新捕获→更新当前截图 + if (currentScreenshot) { + // 检查是否存在释放方法,支持不同可能的命名 + if (typeof currentScreenshot.Dispose === 'function') { + currentScreenshot.Dispose(); + } else if (typeof currentScreenshot.dispose === 'function') { + currentScreenshot.dispose(); + } + log.debug("已释放旧截图资源,准备重新捕获"); + } + + // 重新捕获后增加null校验 + currentScreenshot = captureGameRegion(); + if (!currentScreenshot) { + log.error("重新捕获截图失败,返回了null值"); + } + + log.error(`OCR识别异常(已重新截图,将重试): ${error.message}`); + await sleep(5); // 短暂等待,避免高频截图占用CPU/内存 + } + + await sleep(interval); // 每次重试间隔(默认50ms) + } + + // 6. 超时未成功:返回「空结果 + 超时前最后一次截图」 + log.warn(`OCR识别超时(超过${timeout}ms)`); + return { + results: [], + screenshot: currentScreenshot // 超时前最后一次有效截图(可用于排查原因) + }; +} diff --git a/repo/js/背包材料统计/main.js b/repo/js/背包材料统计/main.js index 713cebb84..53721c5a6 100644 --- a/repo/js/背包材料统计/main.js +++ b/repo/js/背包材料统计/main.js @@ -34,6 +34,7 @@ const CONSTANTS = { // 引入外部脚本(源码不变) // ============================================== eval(file.readTextSync("lib/file.js")); +eval(file.readTextSync("lib/ocr.js")); eval(file.readTextSync("lib/autoPick.js")); eval(file.readTextSync("lib/exp.js")); eval(file.readTextSync("lib/backStats.js")); @@ -851,35 +852,63 @@ function filterLowCountMaterials(pathingMaterialCounts, materialCategoryMap) { // 提取所有需要扫描的材料(含怪物材料) const allMaterials = Object.values(materialCategoryMap).flat(); + log.info(`【材料基准】本次需扫描的全量材料:${allMaterials.join("、")}`); - const filteredMaterials = pathingMaterialCounts - .filter(item => - allMaterials.includes(item.name) && - (item.count < targetCount || item.count === "?") - ) + // ========== 第一步:平行判断超量材料(原始数据,不经过低数量过滤) ========== + pathingMaterialCounts.forEach(item => { + // 只处理allMaterials内的材料(同源) + if (!allMaterials.includes(item.name)) return; + // 未知数量(?)不判断超量 + if (item.count === "?") return; + + // 矿石数量特殊处理(和低数量筛选的处理逻辑一致) + let processedCount = Number(item.count); + if (specialMaterials.includes(item.name)) { + processedCount = Math.floor(processedCount / 10); + } + + // 超量判断(平行逻辑:只要≥阈值就标记,和低数量无关) + if (processedCount >= EXCESS_THRESHOLD) { + tempExcess.push(item.name); + log.debug(`【超量标记】${item.name} 原始数量:${item.count} → 处理后:${processedCount} ≥ 阈值${EXCESS_THRESHOLD},标记为超量`); + } + }); + + // ========== 第二步:平行筛选低数量材料(原有逻辑保留) ========== + const filteredLowCountMaterials = pathingMaterialCounts + .filter(item => { + // 只处理allMaterials内的材料(同源) + if (!allMaterials.includes(item.name)) return false; + // 低数量判断:<目标值 或 数量未知(?) + return item.count < targetCount || item.count === "?"; + }) .map(item => { // 矿石数量÷10 let processedCount = item.count; if (specialMaterials.includes(item.name) && item.count !== "?") { processedCount = Math.floor(Number(item.count) / 10); } - - // 判断是否超量(用处理后数量对比阈值) - if (item.count !== "?" && processedCount >= EXCESS_THRESHOLD) { - tempExcess.push(item.name); // 记录超量材料名 - } - return { ...item, count: processedCount }; }); tempExcess.push("OCR启动"); // 添加特殊标记,用于终止OCR等待 - // 更新全局超量名单(去重) + + // ========== 第三步:更新全局超量名单(去重) ========== excessMaterialNames = [...new Set(tempExcess)]; log.info(`【超量材料更新】共${excessMaterialNames.length}种:${excessMaterialNames.join("、")}`); + log.info(`【低数量材料】筛选后共${filteredLowCountMaterials.length}种:${filteredLowCountMaterials.map(m => m.name).join("、")}`); - return filteredMaterials; + // 返回低数量材料(超量名单已独立生成) + return filteredLowCountMaterials; +} +// 极简封装:用路径和当前目标发通知,然后执行路径 +async function runPathAndNotify(pathingFilePath, currentMaterialName) { + const pathName = basename(pathingFilePath); // 取路径名 + if (notify) { // 只在需要通知时执行 + notification.Send(`当前执行路径:${pathName}\n目标:${currentMaterialName || '未知'}`); + } + return await pathingScript.runFile(pathingFilePath); // 执行路径 } - // ============================================== // 路径处理(拆分巨型函数) // ============================================== @@ -911,7 +940,7 @@ async function processFoodPathEntry(entry, accumulators, recordDir, noRecordDir) // 执行路径 const startTime = new Date().toLocaleString(); const initialPosition = genshin.getPositionFromMap(); - await pathingScript.runFile(pathingFilePath); + await runPathAndNotify(pathingFilePath, currentMaterialName); const finalPosition = genshin.getPositionFromMap(); const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); const endTime = new Date().toLocaleString(); @@ -1024,7 +1053,7 @@ async function processMonsterPathEntry(entry, context) { const startTime = new Date().toLocaleString(); const initialPosition = genshin.getPositionFromMap(); - await pathingScript.runFile(pathingFilePath); + await runPathAndNotify(pathingFilePath, currentMaterialName); const finalPosition = genshin.getPositionFromMap(); const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); const endTime = new Date().toLocaleString(); @@ -1049,7 +1078,7 @@ async function processMonsterPathEntry(entry, context) { const startTime = new Date().toLocaleString(); const initialPosition = genshin.getPositionFromMap(); - await pathingScript.runFile(pathingFilePath); + await runPathAndNotify(pathingFilePath, currentMaterialName); const finalPosition = genshin.getPositionFromMap(); const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); const endTime = new Date().toLocaleString(); @@ -1171,7 +1200,7 @@ async function processNormalPathEntry(entry, context) { const startTime = new Date().toLocaleString(); const initialPosition = genshin.getPositionFromMap(); - await pathingScript.runFile(pathingFilePath); + await runPathAndNotify(pathingFilePath, currentMaterialName); const finalPosition = genshin.getPositionFromMap(); const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); const endTime = new Date().toLocaleString(); @@ -1195,7 +1224,7 @@ async function processNormalPathEntry(entry, context) { const startTime = new Date().toLocaleString(); const initialPosition = genshin.getPositionFromMap(); - await pathingScript.runFile(pathingFilePath); + await runPathAndNotify(pathingFilePath, currentMaterialName); const finalPosition = genshin.getPositionFromMap(); const finalCumulativeDistance = calculateDistance(initialPosition, finalPosition); const endTime = new Date().toLocaleString(); @@ -1499,8 +1528,9 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames // 1. 怪物材料筛选(复用全量扫描结果) log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 基于全量扫描结果筛选有效材料`); - const filteredMonsterMaterials = filterLowCountMaterials(allMaterialCounts.flat(), materialCategoryMap); // 复用结果 - const validMonsterMaterialNames = filteredMonsterMaterials.map(m => m.name); + const filteredMaterials = filterLowCountMaterials(allMaterialCounts.flat(), materialCategoryMap); // 仅调用一次! + // 怪物材料复用结果 + const validMonsterMaterialNames = filteredMaterials.map(m => m.name); log.info(`${CONSTANTS.LOG_MODULES.MONSTER}[怪物材料] 筛选后有效材料:${validMonsterMaterialNames.join('、')}`); // 2. 普通材料筛选(同样复用全量扫描结果,无需再次扫描) @@ -1509,7 +1539,7 @@ async function generateAllPaths(pathingDir, targetResourceNames, cdMaterialNames return { allPaths: [], pathingMaterialCounts }; } log.info(`${CONSTANTS.LOG_MODULES.PATH}[普通材料] 基于全量扫描结果筛选低数量材料`); - const lowCountMaterialsFiltered = filterLowCountMaterials(allMaterialCounts.flat(), materialCategoryMap); // 复用结果 + const lowCountMaterialsFiltered = filteredMaterials; // 复用第一次的结果! const flattenedLowCountMaterials = lowCountMaterialsFiltered.flat().sort((a, b) => a.count - b.count); const lowCountMaterialNames = flattenedLowCountMaterials.map(material => material.name); @@ -1656,20 +1686,23 @@ ${Object.entries(totalDifferences).map(([name, diff]) => ` ${name}: +${diff}个 const targetTexts = targetTextCategories[categoryName]; allTargetTexts = allTargetTexts.concat(Object.values(targetTexts).flat()); } - // 关键补充:等待超量名单生成(由filterLowCountMaterials更新) - let waitTimes = 0; - 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('、')}`); + + // 关键补充:等待超量名单生成(由filterLowCountMaterials更新) + let waitTimes = 0; + 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(`超量名单:${excessMaterialNames.join('、')}`); + log.info(`OCR最终目标文本(已过滤超量):${allTargetTexts.join('、')}`); + await alignAndInteractTarget(allTargetTexts, fDialogueRo, textxRange, texttolerance); })(); diff --git a/repo/js/背包材料统计/manifest.json b/repo/js/背包材料统计/manifest.json index fa7ed6070..e6dd9e3d9 100644 --- a/repo/js/背包材料统计/manifest.json +++ b/repo/js/背包材料统计/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 1, "name": "背包统计采集系统", - "version": "2.54", + "version": "2.55", "bgi_version": "0.44.8", "description": "可统计背包养成道具、部分食物、素材的数量;根据设定数量、根据材料刷新CD执行挖矿、采集、刷怪等的路径。优势:\n+ 1. 自动判断材料CD,不需要管材料CD有没有好;\n+ 2. 可以随意添加路径,能自动排除低效、无效路径;\n+ 3. 有独立名单识别,不会交互路边的npc或是神像;可自定义识别名单,具体方法看【问题解答】增减识别名单\n+ 4. 有实时的弹窗模块,提供了常见的几种:路边信件、过期物品、月卡、调查;\n+ 5. 可识别爆满的路径材料,自动屏蔽;更多详细内容查看readme.md", "saved_files": [