Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions MeoAsstMac.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -131,6 +132,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
4524D2052EEC60A7009DC898 /* ProblemReporterScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReporterScanner.swift; sourceTree = "<group>"; };
830C3D1D29BC7DE200B13A9F /* CheckForUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForUpdatesView.swift; sourceTree = "<group>"; };
830C3D1F29BC86EE00B13A9F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8315B0BC290E48F300AE7FDC /* StartupSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupSettingsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -465,6 +467,7 @@
92CDF6202A065E20005669EC /* Utils */ = {
isa = PBXGroup;
children = (
4524D2052EEC60A7009DC898 /* ProblemReporterScanner.swift */,
92CDF6212A065E55005669EC /* Codeable+JSON.swift */,
92F78D4E2A0B361C00999BD0 /* TaskTimerManager.swift */,
8347B5AE2CDF925400AEB627 /* URLSession+downloadProgress.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down
19 changes: 17 additions & 2 deletions MeoAsstMac/Core/MaaToolClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions MeoAsstMac/Model/MAAViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ import SwiftUI
}
}
}

@AppStorage("MAAReporterscan") var allowscanreporter = false

// MARK: - Game Settings

Expand Down Expand Up @@ -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
Expand All @@ -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()
}
}
13 changes: 13 additions & 0 deletions MeoAsstMac/Settings/ConnectionSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
92 changes: 92 additions & 0 deletions MeoAsstMac/Utils/ProblemReporterScanner.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
6 changes: 6 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1868,6 +1868,9 @@
}
}
}
},
"仅在弹出问题报告时生效,需要录屏权限。" : {

},
"从剪贴板读取" : {
"localizations" : {
Expand Down Expand Up @@ -2128,6 +2131,9 @@
}
}
}
},
"允许在游戏意外退出时尝试重启" : {

},
"允许阻止系统睡眠" : {
"localizations" : {
Expand Down