From c239b5e5f92222e717b729c64549e12dae91838c Mon Sep 17 00:00:00 2001 From: Rain Yang Date: Fri, 12 Dec 2025 23:34:14 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E6=84=8F=E5=A4=96=E9=80=80=E5=87=BA=E5=B0=9D=E8=AF=95?= =?UTF-8?q?=E9=87=8D=E5=90=AF=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Summary** This Pull Request introduces a new feature to detect **`Arknights`** game crashes on macOS using Optical Character Recognition (OCR) against the system's Problem Reporter window. The core addition is the **`ProblemReporterScanner`**, which periodically checks the screen for the crash report window and uses OCR to confirm if the reported process is related to the game. Alongside the detection logic, a new user preference is implemented: **`autoRestartGameOnCrash`**. When this setting is enabled and a crash is detected, the `MaaToolClient` logic is triggered to automatically restart the game, significantly improving the user experience during unattended operation. This change provides a robust, platform-specific solution for high-fidelity crash detection, allowing the application to autonomously recover and continue operation, thus enhancing the overall stability and reliability of the automation tool. ### **Key Changes** 1. **New Feature:** Introduced **`ProblemReporterScanner`** for macOS-specific crash detection using OCR on the Problem Reporter window. 2. **User Preference:** Added new setting `autoRestartGameOnCrash` to store the user's preference for automatic game restart upon crash detection. 3. **UI Integration:** Added a corresponding option to the user settings interface. 4. **Logic Integration:** * Updated **`MaaToolClient`** to incorporate the new crash detection logic and initiate the game restart process when the setting is enabled. * Updated **`MAAViewModel`** to handle the new setting storage, retrieval, and UI state synchronization. ----- ----- ## **摘要** 本次拉取请求(PR)引入了一项新功能,通过对系统**“问题报告”**窗口执行光学字符识别(OCR),来检测 macOS 上的 **《明日方舟》** 游戏崩溃。 核心新增部分是 **`ProblemReporterScanner`**,它会周期性地检查屏幕上的崩溃报告窗口,并利用 OCR 确认报告的进程是否与游戏相关。 与检测逻辑同时,我们实现了一个新的用户偏好设置:**`autoRestartGameOnCrash`**。当此设置启用且检测到崩溃时,将触发 `MaaToolClient` 逻辑来自动重启游戏,从而显著改善无人值守操作期间的用户体验。 本次更改提供了一个健壮的、针对特定平台的解决方案,用于高精度的崩溃检测。它允许应用程序在检测到崩溃后自动恢复并继续运行,从而增强了自动化工具的整体稳定性和可靠性。 ### **主要更改** 1. **新功能:** 引入 **`ProblemReporterScanner`**,使用 OCR 技术对“问题报告”窗口进行扫描,实现 macOS 特有的崩溃检测。 2. **用户偏好设置:** 新增 `autoRestartGameOnCrash` 设置项,用于存储用户关于检测到崩溃时是否自动重启游戏的偏好。 3. **界面集成:** 在用户设置界面中添加了相应选项。 4. **逻辑集成:** * 更新 **`MaaToolClient`**,纳入新的崩溃检测逻辑,并在设置启用时启动游戏重启流程。 * 更新 **`MAAViewModel`**,处理新设置的存储、检索和 UI 状态同步。 --- MeoAsstMac.xcodeproj/project.pbxproj | 4 + MeoAsstMac/Core/MaaToolClient.swift | 19 +++- MeoAsstMac/Model/MAAViewModel.swift | 6 +- .../Settings/ConnectionSettingsView.swift | 13 +++ MeoAsstMac/Utils/ProblemReporterScanner.swift | 92 +++++++++++++++++++ Resources/Localizable.xcstrings | 6 ++ 6 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 MeoAsstMac/Utils/ProblemReporterScanner.swift diff --git a/MeoAsstMac.xcodeproj/project.pbxproj b/MeoAsstMac.xcodeproj/project.pbxproj index d498427d9..7ec0122f8 100644 --- a/MeoAsstMac.xcodeproj/project.pbxproj +++ b/MeoAsstMac.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 4524D2062EEC60A7009DC898 /* ProblemReporterScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4524D2052EEC60A7009DC898 /* ProblemReporterScanner.swift */; }; 830C3D1C29BC7D8600B13A9F /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 830C3D1B29BC7D8600B13A9F /* Sparkle */; }; 830C3D1E29BC7DE200B13A9F /* CheckForUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830C3D1D29BC7DE200B13A9F /* CheckForUpdatesView.swift */; }; 8315B0BD290E48F300AE7FDC /* StartupSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8315B0BC290E48F300AE7FDC /* StartupSettingsView.swift */; }; @@ -131,6 +132,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 4524D2052EEC60A7009DC898 /* ProblemReporterScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReporterScanner.swift; sourceTree = ""; }; 830C3D1D29BC7DE200B13A9F /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = ""; }; 830C3D1F29BC86EE00B13A9F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8315B0BC290E48F300AE7FDC /* StartupSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupSettingsView.swift; sourceTree = ""; }; @@ -465,6 +467,7 @@ 92CDF6202A065E20005669EC /* Utils */ = { isa = PBXGroup; children = ( + 4524D2052EEC60A7009DC898 /* ProblemReporterScanner.swift */, 92CDF6212A065E55005669EC /* Codeable+JSON.swift */, 92F78D4E2A0B361C00999BD0 /* TaskTimerManager.swift */, 8347B5AE2CDF925400AEB627 /* URLSession+downloadProgress.swift */, @@ -628,6 +631,7 @@ 8347B5AD2CDF8B2800AEB627 /* FileManager+unzipProgress.swift in Sources */, 83264A2929EBEBB00074E33A /* MAATaskConfiguration.swift in Sources */, 83264A3329EBEE680074E33A /* MallConfiguration.swift in Sources */, + 4524D2062EEC60A7009DC898 /* ProblemReporterScanner.swift in Sources */, 83D061F62CF81519008F0140 /* FileLogger.swift in Sources */, 8341195729EC58CF00044F45 /* MAACopilot.swift in Sources */, 8347B5A82CDF85A000AEB627 /* ResourceUpdateView.swift in Sources */, diff --git a/MeoAsstMac/Core/MaaToolClient.swift b/MeoAsstMac/Core/MaaToolClient.swift index 189a941e7..4a6f1de49 100644 --- a/MeoAsstMac/Core/MaaToolClient.swift +++ b/MeoAsstMac/Core/MaaToolClient.swift @@ -7,11 +7,12 @@ import Foundation import Network +import SwiftUI actor MaaToolClient { private let connection: NWConnection - init?(address: String) async { + init?(address: String, allowscanreporter: Bool, appBundle: URL) async { let parts = address.split(separator: ":") guard parts.count >= 2, let portNumber = UInt16(parts[1]), @@ -49,7 +50,21 @@ actor MaaToolClient { break case .waiting: try? await Task.sleep(for: .seconds(0.5)) - connection.restart() + // 开启一个新的异步任务 + Task { + if allowscanreporter { + let isFound = await ProblemReporterScanner.checkArknights() + + if isFound { + print("成功!在【问题报告程序】中发现了 'Arknights'") + try await NSWorkspace.shared.openApplication(at: appBundle, configuration: .init()) + } + } else { + await MainActor.run { + self.connection.restart() + } + } + } case .failed: retryCount += 1 if retryCount > maxRetries { diff --git a/MeoAsstMac/Model/MAAViewModel.swift b/MeoAsstMac/Model/MAAViewModel.swift index 3517d6ce3..104e71315 100644 --- a/MeoAsstMac/Model/MAAViewModel.swift +++ b/MeoAsstMac/Model/MAAViewModel.swift @@ -126,6 +126,8 @@ import SwiftUI } } } + + @AppStorage("MAAReporterscan") var allowscanreporter = false // MARK: - Game Settings @@ -682,7 +684,7 @@ extension MAAViewModel { do { try await NSWorkspace.shared.openApplication(at: appBundle, configuration: .init()) - let client = await MaaToolClient(address: connectionAddress) + let client = await MaaToolClient(address: connectionAddress, allowscanreporter: allowscanreporter, appBundle: appBundle) return client != nil } catch { let nsError = error as NSError @@ -694,7 +696,7 @@ extension MAAViewModel { } func stopGame() async throws { - guard let client = await MaaToolClient(address: connectionAddress) else { return } + guard let client = await MaaToolClient(address: connectionAddress, allowscanreporter: allowscanreporter, appBundle: URL(fileURLWithPath: "/Users")) else { return } try await client.terminate() } } diff --git a/MeoAsstMac/Settings/ConnectionSettingsView.swift b/MeoAsstMac/Settings/ConnectionSettingsView.swift index ee7803f4e..e053d988b 100644 --- a/MeoAsstMac/Settings/ConnectionSettingsView.swift +++ b/MeoAsstMac/Settings/ConnectionSettingsView.swift @@ -37,6 +37,19 @@ struct ConnectionSettingsView: View { .font(.caption).foregroundStyle(.secondary) } } + + Toggle(isOn: $viewModel.allowscanreporter) { + VStack(alignment: .leading, spacing: 2) { + Text("允许在游戏意外退出时尝试重启") + Text("仅在弹出问题报告时生效,需要录屏权限。") + .font(.caption).foregroundStyle(.secondary) + } + } + .onChange(of: viewModel.allowscanreporter, perform: { isallowed in + if isallowed { + _ = CGRequestScreenCaptureAccess() + } + }) Toggle(isOn: $viewModel.useAdbLite) { VStack(alignment: .leading, spacing: 2) { diff --git a/MeoAsstMac/Utils/ProblemReporterScanner.swift b/MeoAsstMac/Utils/ProblemReporterScanner.swift new file mode 100644 index 000000000..e131dcde0 --- /dev/null +++ b/MeoAsstMac/Utils/ProblemReporterScanner.swift @@ -0,0 +1,92 @@ +// +// ProblemReporterScanner.swift +// MAA +// +// Created by RainYang on 2025/12/12. +// + +import Cocoa +import CoreGraphics +import Vision + +class ProblemReporterScanner { + + // MARK: - 1. 对外接口:检查并识别 + /// 查找“问题报告程序”并检查是否包含 "Arknights" + static func checkArknights() async -> Bool { + // 使用 Task.detached 强制在后台线程执行耗时操作,避免卡主线程 + return await Task.detached(priority: .userInitiated) { + // 1. 截图 + guard let capturedImage = captureWindow(ownerName: "问题报告程序") else { + print("⚠️ 未找到名为 '问题报告程序' 的窗口") + return false + } + + // 2. 识别 + return containsText(targetText: "Arknights", in: capturedImage) + }.value + } + + // MARK: - 2. 核心逻辑:截图指定 App 窗口 + private static func captureWindow(ownerName: String) -> CGImage? { + // 获取所有屏幕上的窗口信息 + guard let windowInfoList = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[String: Any]] + else { + return nil + } + + // 遍历查找名为 ownerName 的窗口 + for info in windowInfoList { + // 获取窗口所属的应用名称 (kCGWindowOwnerName) + if let name = info[kCGWindowOwnerName as String] as? String, name == ownerName { + + // 获取窗口 ID + guard let windowID = info[kCGWindowNumber as String] as? CGWindowID else { continue } + + // 排除很小的窗口(有时候应用会有不可见的 1x1 窗口) + if let boundsDict = info[kCGWindowBounds as String] as? [String: CGFloat], + let width = boundsDict["Width"], width < 50 + { + continue + } + + // 创建截图:只截取该 WindowID 的窗口 + let imageOption: CGWindowListOption = [.optionIncludingWindow] + let image = CGWindowListCreateImage(.null, imageOption, windowID, .boundsIgnoreFraming) + + return image + } + } + return nil + } + + // MARK: - 3. 核心逻辑:OCR 文字识别 + private static func containsText(targetText: String, in cgImage: CGImage) -> Bool { + let request = VNRecognizeTextRequest() + request.recognitionLevel = .accurate // 追求高准确率 + request.usesLanguageCorrection = true + + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + + do { + try handler.perform([request]) + guard let observations = request.results else { return false } + + for observation in observations { + // 获取置信度最高的候选词 + if let candidate = observation.topCandidates(1).first { + // 不区分大小写匹配 + if candidate.string.localizedCaseInsensitiveContains(targetText) { + // 调试打印:看到了什么文字 + print("🔎 识别到匹配文本: \(candidate.string)") + return true + } + } + } + } catch { + print("OCR Error: \(error)") + } + + return false + } +} diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 25c43a3a1..0962b323c 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1868,6 +1868,9 @@ } } } + }, + "仅在弹出问题报告时生效,需要录屏权限。" : { + }, "从剪贴板读取" : { "localizations" : { @@ -2128,6 +2131,9 @@ } } } + }, + "允许在游戏意外退出时尝试重启" : { + }, "允许阻止系统睡眠" : { "localizations" : { From 196245a0366c40769953a564fe8fc3ae08e8ce19 Mon Sep 17 00:00:00 2001 From: Rain Yang Date: Sat, 13 Dec 2025 12:00:12 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BA=86=E6=8F=90?= =?UTF-8?q?=E5=87=BA=E7=9A=84=E8=8B=A5=E5=B9=B2=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MeoAsstMac/Core/MaaToolClient.swift | 14 +++++++------- MeoAsstMac/Model/MAAViewModel.swift | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/MeoAsstMac/Core/MaaToolClient.swift b/MeoAsstMac/Core/MaaToolClient.swift index 4a6f1de49..86ba8e720 100644 --- a/MeoAsstMac/Core/MaaToolClient.swift +++ b/MeoAsstMac/Core/MaaToolClient.swift @@ -12,7 +12,7 @@ import SwiftUI actor MaaToolClient { private let connection: NWConnection - init?(address: String, allowscanreporter: Bool, appBundle: URL) async { + init?(address: String, allowscanreporter: Bool, appBundle: URL? = nil) async { let parts = address.split(separator: ":") guard parts.count >= 2, let portNumber = UInt16(parts[1]), @@ -54,17 +54,17 @@ actor MaaToolClient { Task { if allowscanreporter { let isFound = await ProblemReporterScanner.checkArknights() - - if isFound { + + if isFound, let Bundle = appBundle { print("成功!在【问题报告程序】中发现了 'Arknights'") - try await NSWorkspace.shared.openApplication(at: appBundle, configuration: .init()) + try await NSWorkspace.shared.openApplication(at: Bundle, configuration: .init()) } } else { - await MainActor.run { - self.connection.restart() - } + self.connection.restart() } } + + self.connection.restart() case .failed: retryCount += 1 if retryCount > maxRetries { diff --git a/MeoAsstMac/Model/MAAViewModel.swift b/MeoAsstMac/Model/MAAViewModel.swift index 104e71315..a35373899 100644 --- a/MeoAsstMac/Model/MAAViewModel.swift +++ b/MeoAsstMac/Model/MAAViewModel.swift @@ -696,7 +696,7 @@ extension MAAViewModel { } func stopGame() async throws { - guard let client = await MaaToolClient(address: connectionAddress, allowscanreporter: allowscanreporter, appBundle: URL(fileURLWithPath: "/Users")) else { return } + guard let client = await MaaToolClient(address: connectionAddress, allowscanreporter: allowscanreporter) else { return } try await client.terminate() } }