From 624cd61f5a10274f65616486e2cbf10d56764507 Mon Sep 17 00:00:00 2001 From: ShadowLemoon <119576779+ShadowLemoon@users.noreply.github.com> Date: Fri, 1 May 2026 19:23:52 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E7=9A=84OCR=E6=96=B9=E6=B3=95=20(#3176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 修复切换账号的OCR方法 * 更新版本号 --- repo/js/SwitchAccountMultipleMode/main.js | 125 ++++++++++++++++-- .../SwitchAccountMultipleMode/manifest.json | 5 +- 2 files changed, 117 insertions(+), 13 deletions(-) diff --git a/repo/js/SwitchAccountMultipleMode/main.js b/repo/js/SwitchAccountMultipleMode/main.js index dfa7603fe..cad6a577e 100644 --- a/repo/js/SwitchAccountMultipleMode/main.js +++ b/repo/js/SwitchAccountMultipleMode/main.js @@ -151,6 +151,107 @@ async function recognizeTextAndClick(targetText, ocrRegion, timeout = 8000) { } return { success: false }; } + +function normalizeOcrText(text) { + return String(text || "").replace(/\s+/g, ""); +} + +function levenshteinDistance(a, b) { + const rows = a.length + 1; + const cols = b.length + 1; + const dp = Array.from({ length: rows }, () => Array(cols).fill(0)); + + for (let i = 0; i < rows; i++) dp[i][0] = i; + for (let j = 0; j < cols; j++) dp[0][j] = j; + + for (let i = 1; i < rows; i++) { + for (let j = 1; j < cols; j++) { + const cost = a[i - 1] === b[j - 1] ? 0 : 1; + dp[i][j] = Math.min( + dp[i - 1][j] + 1, + dp[i][j - 1] + 1, + dp[i - 1][j - 1] + cost + ); + } + } + + return dp[a.length][b.length]; +} + +function ocrSimilarity(actual, target) { + actual = normalizeOcrText(actual); + target = normalizeOcrText(target); + + if (!actual || !target) return 0; + if (actual.includes(target) || target.includes(actual)) return 1; + + let best = 1 - levenshteinDistance(actual, target) / Math.max(actual.length, target.length); + if (actual.length > target.length) { + for (let i = 0; i <= actual.length - target.length; i++) { + const part = actual.substring(i, i + target.length); + best = Math.max(best, 1 - levenshteinDistance(part, target) / target.length); + } + } + + return best; +} + +function rectToOcrObject(rect) { + if (!rect) return RecognitionObject.ocrThis; + + const x = rect.X != null ? rect.X : (rect.x != null ? rect.x : 0); + const y = rect.Y != null ? rect.Y : (rect.y != null ? rect.y : 0); + const width = rect.Width != null ? rect.Width : (rect.width != null ? rect.width : 0); + const height = rect.Height != null ? rect.Height : (rect.height != null ? rect.height : 0); + + if (x === 0 && y === 0 && width === 0 && height === 0) { + return RecognitionObject.ocrThis; + } + + return RecognitionObject.Ocr(x, y, width, height); +} + +function ocrMatch(target, rect = null, threshold = null) { + const actualThreshold = threshold != null ? threshold : 0.75; + const targetText = normalizeOcrText(target); + const captureRegion = captureGameRegion(); + try { + const resultList = captureRegion.findMulti(rectToOcrObject(rect)); + const texts = []; + + for (let result of resultList) { + const text = normalizeOcrText(result.text); + if (!text) continue; + if (ocrSimilarity(text, targetText) >= actualThreshold) { + return true; + } + texts.push(text); + } + + return ocrSimilarity(texts.join(""), targetText) >= actualThreshold; + } finally { + captureRegion.dispose(); + } +} + +async function waitForOcrMatch(target, rect = null, threshold = null, timeout = null) { + const actualTimeout = timeout != null ? timeout : 10000; + const interval = 1000; + const start = Date.now(); + let retryCount = 0; + + while (Date.now() - start < actualTimeout) { + if (ocrMatch(target, rect, threshold)) { + return true; + } + + retryCount++; + await sleep(interval); + } + + log.warn(`【OCR】等待文字匹配超时: ${target} | 重试次数: ${retryCount} | 耗时: ${Date.now() - start}ms`); + return false; +} // 切换账号OCR模式 // ====================================================== @@ -510,12 +611,12 @@ async function recognizeTextAndClick(targetText, ocrRegion, timeout = 8000) { await stateReturnToGenshinGate(); await sleep(1000); } - await page.WaitForOcrMatch("开始游戏"); + await waitForOcrMatch("开始游戏"); await matchImgAndClick(login_out_account, "登录页的右下角退出按钮"); - await page.WaitForOcrMatch("切换账号"); + await waitForOcrMatch("切换账号"); await matchImgAndClick(confirm_switch_account, "确认切换账号"); // 检测是否弹出保存登陆记录弹框 - if (await page.WaitForOcrMatch("退出并", new OpenCvSharp.OpenCvSharp.Rect(0, 0, 1920, 1080), 0.8, 1000)) { + if (await waitForOcrMatch("退出并", new OpenCvSharp.OpenCvSharp.Rect(0, 0, 1920, 1080), 0.8, 1000)) { await matchImgAndClick(save_login_info, "保留登陆记录"); await sleep(500); await matchImgAndClick(confirm_switch_account, "确认切换账号"); @@ -525,7 +626,7 @@ async function recognizeTextAndClick(targetText, ocrRegion, timeout = 8000) { await sleep(500); // 换服务器操作 if (settings.Servers && (settings.Servers !== "不切换服务器" || settings.Servers == "")) { - await page.WaitForOcrMatch("开始游戏"); + await waitForOcrMatch("开始游戏"); log.info("正在更换服务器") await matchImgAndClick(switch_server, "更换服务器"); let serversMatched = true; @@ -546,11 +647,11 @@ async function recognizeTextAndClick(targetText, ocrRegion, timeout = 8000) { } } await keyPress("VK_ESCAPE"); - await page.WaitForOcrMatch("开始游戏"); + await waitForOcrMatch("开始游戏"); await click(960, 640); await page.Wait(5000); log.info('等待提瓦特大门加载'); - await page.WaitForOcrMatch("点击进入"); + await waitForOcrMatch("点击进入"); await click(960, 640); // 可能登录账号的时候出现月卡提醒,则先点击一次月卡。 await genshin.blessingOfTheWelkinMoon(); @@ -748,12 +849,12 @@ async function recognizeTextAndClick(targetText, ocrRegion, timeout = 8000) { try { await matchImgAndClick(pm_out, "左下角退出门"); await matchImgAndClick(out_to_login, "退出至登陆页面"); - await page.WaitForOcrMatch("开始游戏"); + await waitForOcrMatch("开始游戏"); await matchImgAndClick(login_out_account, "登录页的右下角退出按钮"); - await page.WaitForOcrMatch("切换账号"); + await waitForOcrMatch("切换账号"); await matchImgAndClick(confirm_switch_account, "确认切换账号"); // 检测是否弹出保存登陆记录弹框 - if (await page.WaitForOcrMatch("退出并", new OpenCvSharp.OpenCvSharp.Rect(0, 0, 1920, 1080), 0.8, 1000)) { + if (await waitForOcrMatch("退出并", new OpenCvSharp.OpenCvSharp.Rect(0, 0, 1920, 1080), 0.8, 1000)) { await matchImgAndClick(save_login_info, "保留登陆记录"); await sleep(500); await matchImgAndClick(confirm_switch_account, "确认切换账号"); @@ -783,7 +884,7 @@ async function recognizeTextAndClick(targetText, ocrRegion, timeout = 8000) { } // 换服务器操作 if (settings.Servers && (settings.Servers !== "不切换服务器" || settings.Servers == "")) { - await page.WaitForOcrMatch("开始游戏"); + await waitForOcrMatch("开始游戏"); log.info("正在更换服务器") await matchImgAndClick(switch_server, "更换服务器"); let serversMatched = true; @@ -804,11 +905,11 @@ async function recognizeTextAndClick(targetText, ocrRegion, timeout = 8000) { } } await keyPress("VK_ESCAPE"); - await page.WaitForOcrMatch("开始游戏"); + await waitForOcrMatch("开始游戏"); await click(960, 640); await page.Wait(5000); log.info('等待提瓦特大门加载'); - await page.WaitForOcrMatch("点击进入"); + await waitForOcrMatch("点击进入"); await click(960, 640); // 可能登录账号的时候出现月卡提醒,则先点击一次月卡。 await genshin.blessingOfTheWelkinMoon(); diff --git a/repo/js/SwitchAccountMultipleMode/manifest.json b/repo/js/SwitchAccountMultipleMode/manifest.json index b2dfef834..277d15415 100644 --- a/repo/js/SwitchAccountMultipleMode/manifest.json +++ b/repo/js/SwitchAccountMultipleMode/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 1, "name": "切换账号多模式", - "version": "2.0", + "version": "2.1", "bgi_version": "0.57.0", "description": "多种模式的切换账号,有下拉列表、填写账号密码OCR操作或键鼠操作,目前支持B服/国际服但不完整\n免责申明:所有的账号密码均保存在本地,请使用者妥善保管账号密码,请勿外泄账号密码。若因使用此脚本导致的账号泄露、封禁问题与脚本作者无关。", "tags": [ @@ -33,6 +33,9 @@ { "name": "空", "links": "https://pd.qq.com/g/620449244035027412/post/B_022e6a69a93f0a001441152193488791090X60?showGuild=1&errCode=0&asideType=Full" + }, + { + "name": "檬" } ], "settings_ui": "settings.json",