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..86ba8e720 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? = nil) 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, let Bundle = appBundle { + print("成功!在【问题报告程序】中发现了 'Arknights'") + try await NSWorkspace.shared.openApplication(at: Bundle, configuration: .init()) + } + } else { + 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 3517d6ce3..a35373899 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) 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" : {