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
48 changes: 48 additions & 0 deletions MeoAsstMac.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
objects = {

/* Begin PBXBuildFile section */
4505F96D2E7E9852006E7D28 /* DingTalkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505F9672E7E9852006E7D28 /* DingTalkService.swift */; };
4505F96E2E7E9852006E7D28 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505F9692E7E9852006E7D28 /* NotificationService.swift */; };
4505F96F2E7E9852006E7D28 /* DingTalkBotClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505F9662E7E9852006E7D28 /* DingTalkBotClient.swift */; };
4505F9702E7E9852006E7D28 /* CustomWebhookService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505F9652E7E9852006E7D28 /* CustomWebhookService.swift */; };
4505F9712E7E9852006E7D28 /* BarkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505F9642E7E9852006E7D28 /* BarkService.swift */; };
4505F9722E7E9852006E7D28 /* QmsgService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505F96A2E7E9852006E7D28 /* QmsgService.swift */; };
4505F9732E7E9852006E7D28 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505F9682E7E9852006E7D28 /* NotificationManager.swift */; };
4505F9752E7E9884006E7D28 /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505F9742E7E9884006E7D28 /* NotificationSettingsView.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 +139,14 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
4505F9642E7E9852006E7D28 /* BarkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkService.swift; sourceTree = "<group>"; };
4505F9652E7E9852006E7D28 /* CustomWebhookService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomWebhookService.swift; sourceTree = "<group>"; };
4505F9662E7E9852006E7D28 /* DingTalkBotClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DingTalkBotClient.swift; sourceTree = "<group>"; };
4505F9672E7E9852006E7D28 /* DingTalkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DingTalkService.swift; sourceTree = "<group>"; };
4505F9682E7E9852006E7D28 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
4505F9692E7E9852006E7D28 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
4505F96A2E7E9852006E7D28 /* QmsgService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QmsgService.swift; sourceTree = "<group>"; };
4505F9742E7E9884006E7D28 /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.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 @@ -244,6 +260,28 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
4505F96B2E7E9852006E7D28 /* Notification */ = {
isa = PBXGroup;
children = (
4505F9642E7E9852006E7D28 /* BarkService.swift */,
4505F9652E7E9852006E7D28 /* CustomWebhookService.swift */,
4505F9662E7E9852006E7D28 /* DingTalkBotClient.swift */,
4505F9672E7E9852006E7D28 /* DingTalkService.swift */,
4505F9682E7E9852006E7D28 /* NotificationManager.swift */,
4505F9692E7E9852006E7D28 /* NotificationService.swift */,
4505F96A2E7E9852006E7D28 /* QmsgService.swift */,
);
path = Notification;
sourceTree = "<group>";
};
4505F96C2E7E9852006E7D28 /* Services */ = {
isa = PBXGroup;
children = (
4505F96B2E7E9852006E7D28 /* Notification */,
);
path = Services;
sourceTree = "<group>";
};
83245A0128F467C300ED6D4C /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -382,6 +420,7 @@
92CDF6202A065E20005669EC /* Utils */,
83FF5A98297855E5000AE831 /* Settings */,
8337870728F1DA7B00D39D6F /* Resources */,
4505F96C2E7E9852006E7D28 /* Services */,
833786D228F188D900D39D6F /* Assets.xcassets */,
830C3D1F29BC86EE00B13A9F /* Info.plist */,
833786D728F188D900D39D6F /* MeoAsstMac.entitlements */,
Expand Down Expand Up @@ -458,6 +497,7 @@
838FF82129FBB9E3000566A0 /* GameSettingsView.swift */,
83DD50B629BE385A000E84E7 /* UpdaterSettingsView.swift */,
92EB82862A0CD315006DD9D2 /* SystemSettingsView.swift */,
4505F9742E7E9884006E7D28 /* NotificationSettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
Expand Down Expand Up @@ -578,6 +618,13 @@
834DEC122C4D94BD0073B5D4 /* ClosedownConfiguration.swift in Sources */,
83477CB429FEA7A500A5FF99 /* TaskButtons.swift in Sources */,
8355E7B829E9E2B2005D5A1C /* Sidebar.swift in Sources */,
4505F96D2E7E9852006E7D28 /* DingTalkService.swift in Sources */,
4505F96E2E7E9852006E7D28 /* NotificationService.swift in Sources */,
4505F96F2E7E9852006E7D28 /* DingTalkBotClient.swift in Sources */,
4505F9702E7E9852006E7D28 /* CustomWebhookService.swift in Sources */,
4505F9712E7E9852006E7D28 /* BarkService.swift in Sources */,
4505F9722E7E9852006E7D28 /* QmsgService.swift in Sources */,
4505F9732E7E9852006E7D28 /* NotificationManager.swift in Sources */,
83661E2C29F77CFF007793CA /* VideoRecogView.swift in Sources */,
8337870228F1D95F00D39D6F /* Maa.swift in Sources */,
832985B929EF9D50009F7ED5 /* MAAContent.swift in Sources */,
Expand All @@ -602,6 +649,7 @@
83264A3E29EC22DE0074E33A /* MAAViewModel.swift in Sources */,
92F78D4D2A0A7B2E00999BD0 /* TaskTimerView.swift in Sources */,
8383EA2F28F2C5BD007DAB9D /* FightSettingsView.swift in Sources */,
4505F9752E7E9884006E7D28 /* NotificationSettingsView.swift in Sources */,
83531D5F29F00F0A008A5DF8 /* CopilotView.swift in Sources */,
83477CB229FEA12300A5FF99 /* TaskCommands.swift in Sources */,
8329858C29EDA952009F7ED5 /* OpenLogFileView.swift in Sources */,
Expand Down
22 changes: 22 additions & 0 deletions MeoAsstMac/MeoAsstMacApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,30 @@ struct MeoAsstMacApp: App {
private let updaterController: SPUStandardUpdaterController
private let updaterDelegate = MaaUpdaterDelegate()

// 通知管理器
private var notificationManager: NotificationManager

init() {
// 创建 MAAViewModel 的唯一实例
let viewModel = MAAViewModel()

// 使用创建的实例初始化 @StateObject 属性。
// 注意在初始化器中为属性包装器赋值时使用的下划线语法。
_appViewModel = StateObject(wrappedValue: viewModel)

// 现在,使用同一个实例来初始化通知管理器
let manager = NotificationManager(viewModel: viewModel)
self.notificationManager = manager
#if DEBUG
let isRelease = false
#else
let isRelease = true
#endif
updaterController = .init(startingUpdater: isRelease, updaterDelegate: updaterDelegate, userDriverDelegate: nil)

// 立即开始监听日志
self.notificationManager.startObserving()

}

var body: some Scene {
Expand Down Expand Up @@ -65,6 +82,11 @@ struct MeoAsstMacApp: App {
.tabItem {
Label("系统设置", systemImage: "wrench.adjustable")
}

NotificationSettingsView()
.tabItem {
Label("通知设置", systemImage: "ellipsis.message")
}
}
.environmentObject(appViewModel)
.frame(maxWidth: 360, minHeight: 240)
Expand Down
113 changes: 113 additions & 0 deletions MeoAsstMac/Model/MAAViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ import Combine
import IOKit.pwr_mgt
import SwiftUI

/// 用于存储通知触发器配置的结构体
struct NotificationTriggers: Codable {
var onTaskCompletion: Bool = false
var sendAllLogs: Bool = true
var onTaskError: Bool = false
var onTaskTimeout: Bool = false
var sendAllLogsAfterFinish: Bool = true
}

/// 自定义 Webhook 配置结构体
struct CustomWebhookSettings: Codable {
var isEnabled: Bool = false
var url: String = ""
var bodyTemplate: String = """
{
"content": "{title}\n{content}"
}
"""
}

/// 用于存储 Qmsg 配置的结构体
struct QmsgSettings: Codable {
var isEnabled: Bool = false
var server: String = "https://qmsg.zendee.cn/"
var key: String = ""
var userQQ: String = "" // 推送目标的 QQ
var botQQ: String = "" // 机器人的 QQ
}

@MainActor class MAAViewModel: ObservableObject {
// MARK: - Core Status

Expand Down Expand Up @@ -151,6 +180,90 @@ import SwiftUI
}
}

// MARK: - General Notification Settings

@AppStorage("notificationSendingInterval") var notificationSendingInterval: Double = 1.0

@AppStorage("notificationTriggersData") private var notificationTriggersData: Data?

var notificationTriggers: NotificationTriggers {
get {
if let data = notificationTriggersData,
let decodedSettings = try? JSONDecoder().decode(NotificationTriggers.self, from: data)
{
return decodedSettings
}
return NotificationTriggers()
}
set {
if let encodedData = try? JSONEncoder().encode(newValue) {
notificationTriggersData = encodedData
}
}
}

// MARK: - DingTalk Settings

@AppStorage("SendLogsInGUI") var showSendLogsInGUI: Bool = true

@AppStorage("DingTalkBotURL") var DKwebhookURL = ""

@AppStorage("DingTalkBotSecret") var DKsecret = ""

@AppStorage("DingTalkBot") var DingTalkBot = false

// MARK: - Bark Settings

@AppStorage("Bark") var BarkBot: Bool = false

@AppStorage("BarkKey") var BarkKey: String = ""

@AppStorage("BarkServer") var BarkServer: String = "https://api.day.app/"

// MARK: - Qmsg Settings
@AppStorage("qmsgSettingsData") private var qmsgSettingsData: Data?

var qmsg: QmsgSettings {
get {
if let data = qmsgSettingsData,
let decodedSettings = try? JSONDecoder().decode(QmsgSettings.self, from: data)
{
return decodedSettings
}
return QmsgSettings()
}
set {
if let encodedData = try? JSONEncoder().encode(newValue) {
qmsgSettingsData = encodedData
}
}
}

// MARK: - Custom Webhook Settings

@AppStorage("customWebhookSettingsData") private var customWebhookSettingsData: Data?
/// 这是我们提供给 App 其他部分(比如 UI)使用的接口。
/// 它是一个计算属性,自动处理编码和解码的复杂工作。
var customWebhook: CustomWebhookSettings {
get {
// “get” - 当 UI 需要读取设置时:
// 尝试从 Data 中解码。如果失败或没有数据,就返回一个全新的默认设置。
if let data = customWebhookSettingsData,
let decodedSettings = try? JSONDecoder().decode(CustomWebhookSettings.self, from: data)
{
return decodedSettings
}
return CustomWebhookSettings()
}
set {
// “set” - 当 UI 修改了设置时:
// 将新的设置编码成 Data,然后保存到 AppStorage 中。
if let encodedData = try? JSONEncoder().encode(newValue) {
customWebhookSettingsData = encodedData
}
}
}

// MARK: - Initializer

init() {
Expand Down
123 changes: 123 additions & 0 deletions MeoAsstMac/Services/Notification/BarkService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// BarkService.swift
// MAA
//
// Created by RainYang on 2025/8/30.
//

import Foundation

/// Bark 通知服务,实现了 NotificationService 协议。
///
/// 这个类包含了发送 Bark 通知的具体逻辑,并内置了独立的失败重试和自动禁用机制。
@MainActor
class BarkService: NotificationService {

/// 用于记录连续发送失败的次数。
private var consecutiveFailureCount = 0
/// 连续失败多少次后自动禁用功能。
private let failureThreshold = 3
/// 所有发送失败日志的缓冲区。
private var faillogBuffer: [MAALog] = []

/// 发送 Bark 通知。
/// - Returns: 如果发送失败且需要重试,则返回原始的日志数组;否则返回 `nil`。
func send(logs: [MAALog], using config: NotificationConfig, viewModel: MAAViewModel) async -> [MAALog]? {
// 1. 构建 Bark 请求 URLRequest (使用 JSON POST)
guard let request = buildRequest(for: logs, config: config, viewModel: viewModel) else {
if viewModel.showSendLogsInGUI {
viewModel.logError("构建 Bark POST 请求失败。")
}
viewModel.BarkBot = false
// 这是配置错误或 JSON 编码错误,不进行重试。
return nil
}

do {
// 2. 发送网络请求
if viewModel.showSendLogsInGUI {
viewModel.logInfo("准备发送 Bark 日志摘要 (\(logs.count)条)...")
}
// 改为使用 URLSession.shared.upload(for:from:) 来发送 POST 请求体
let (_, response) = try await URLSession.shared.data(for: request)

// 3. 检查服务器响应
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
throw URLError(.badServerResponse, userInfo: ["statusCode": statusCode])
}
if viewModel.showSendLogsInGUI {
viewModel.logInfo("Bark 通知发送成功。")
}
// 发送成功,重置失败计数器并返回 nil (表示无需重试)。
consecutiveFailureCount = 0
return nil

} catch {
// 4. 捕获错误,执行失败重试或自动禁用逻辑
consecutiveFailureCount += 1
if viewModel.showSendLogsInGUI {
viewModel.logError("发送 Bark 通知失败: \(error.localizedDescription)")
}

if consecutiveFailureCount >= failureThreshold {
// 如果连续失败次数达到阈值,则禁用功能。
viewModel.BarkBot = false
if viewModel.showSendLogsInGUI {
viewModel.logError("Bark 通知连续失败 \(consecutiveFailureCount) 次,已自动关闭此功能。请检查网络和 Bark Key/服务器配置。")
}
// 返回 nil,停止重试。
return nil
} else {
// 如果失败次数未达到阈值,则请求重试。
if viewModel.showSendLogsInGUI {
viewModel.logWarn("Bark 通知发送失败(第 \(consecutiveFailureCount) 次尝试),将在10s后重试。")
}
// 返回原始日志数组,通知上层管理器将它们加回缓冲区。
return logs
}
}
}

/// 根据配置和日志内容构建最终的 Bark URLRequest 和 JSON Body。
/// 移除原 buildURL 方法,改为此 POST 请求构建方法。
private func buildRequest(for logs: [MAALog], config: NotificationConfig, viewModel: MAAViewModel) -> URLRequest? {
// 1. 准备通知内容
let title = (viewModel.notificationTriggers.sendAllLogsAfterFinish) ? "MAA 任务已全部完成" : "MAA 日志摘要 (\(logs.count)条)"
let bodyItems = logs.map { log in
let timeString = log.date.formatted(date: .omitted, time: .standard)
return "[\(timeString)] \(log.content)"
}
let body = bodyItems.joined(separator: "\n")

// 2. 准备 Bark 服务器 URL
// 注意:POST 请求的 URL 路径与 GET 不同,只需要到 /push 即可。
// 为了兼容旧的配置,我们从 config.barkServer 和 config.barkKey 构造基础 URL。
let barkBaseURL = config.barkServer.hasSuffix("/") ? config.barkServer : "\(config.barkServer)/"
guard let url = URL(string: "\(barkBaseURL)push") else {
return nil
}

// 3. 构建 JSON Payload
let jsonPayload: [String: Any] = [
"title": title,
"body": body,
"device_key": config.barkKey, // 使用 device_key 字段传递 Bark Key
"group": "MAA",
"icon": "https://maa.plus/docs/images/maa-logo_512x512.png"
]

// 4. JSON 编码
guard let httpBody = try? JSONSerialization.data(withJSONObject: jsonPayload, options: []) else {
return nil
}

// 5. 构建 URLRequest
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = httpBody

return request
}
}
Loading