蜂鸟Pro v2.0.1 - 基础框架版本 (待完善)
## 当前状态 - 插件界面已完成重命名 (cursorpro → hummingbird) - 双账号池 UI 已实现 (Auto/Pro 卡片) - 后端已切换到 MySQL 数据库 - 添加了 Cursor 官方用量 API 文档 ## 已知问题 (待修复) 1. 激活时检查账号导致无账号时激活失败 2. 未启用无感换号时不应获取账号 3. 账号用量模块不显示 (seamless 未启用时应隐藏) 4. 积分显示为 0 (后端未正确返回) 5. Auto/Pro 双密钥逻辑混乱,状态不同步 6. 账号添加后无自动分析功能 ## 下一版本计划 - 重构数据模型,优化账号状态管理 - 实现 Cursor API 自动分析账号 - 修复激活流程,不依赖账号 - 启用无感时才分配账号 - 完善账号用量实时显示 ## 文件说明 - docs/系统设计文档.md - 完整架构设计 - cursor 官方用量接口.md - Cursor API 文档 - 参考计费/ - Vibeviewer 开源项目参考 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
import VibeviewerAPI
|
||||
|
||||
private struct CursorServiceKey: EnvironmentKey {
|
||||
static let defaultValue: CursorService = DefaultCursorService()
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var cursorService: CursorService {
|
||||
get { self[CursorServiceKey.self] }
|
||||
set { self[CursorServiceKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
import VibeviewerStorage
|
||||
|
||||
private struct CursorStorageKey: EnvironmentKey {
|
||||
static let defaultValue: any CursorStorageService = DefaultCursorStorageService()
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var cursorStorage: any CursorStorageService {
|
||||
get { self[CursorStorageKey.self] }
|
||||
set { self[CursorStorageKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import SwiftUI
|
||||
|
||||
private struct DashboardRefreshServiceKey: EnvironmentKey {
|
||||
static let defaultValue: any DashboardRefreshService = NoopDashboardRefreshService()
|
||||
}
|
||||
|
||||
private struct ScreenPowerStateServiceKey: EnvironmentKey {
|
||||
static let defaultValue: any ScreenPowerStateService = NoopScreenPowerStateService()
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var dashboardRefreshService: any DashboardRefreshService {
|
||||
get { self[DashboardRefreshServiceKey.self] }
|
||||
set { self[DashboardRefreshServiceKey.self] = newValue }
|
||||
}
|
||||
|
||||
var screenPowerStateService: any ScreenPowerStateService {
|
||||
get { self[ScreenPowerStateServiceKey.self] }
|
||||
set { self[ScreenPowerStateServiceKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
import VibeviewerCore
|
||||
|
||||
private struct LaunchAtLoginServiceKey: EnvironmentKey {
|
||||
static let defaultValue: any LaunchAtLoginService = DefaultLaunchAtLoginService()
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var launchAtLoginService: any LaunchAtLoginService {
|
||||
get { self[LaunchAtLoginServiceKey.self] }
|
||||
set { self[LaunchAtLoginServiceKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import SwiftUI
|
||||
|
||||
private struct LoginServiceKey: EnvironmentKey {
|
||||
static let defaultValue: any LoginService = NoopLoginService()
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var loginService: any LoginService {
|
||||
get { self[LoginServiceKey.self] }
|
||||
set { self[LoginServiceKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
|
||||
private struct UpdateServiceKey: EnvironmentKey {
|
||||
static let defaultValue: any UpdateService = NoopUpdateService()
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var updateService: any UpdateService {
|
||||
get { self[UpdateServiceKey.self] }
|
||||
set { self[UpdateServiceKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import VibeviewerAPI
|
||||
import VibeviewerModel
|
||||
import VibeviewerStorage
|
||||
import VibeviewerCore
|
||||
|
||||
/// 后台刷新服务协议
|
||||
public protocol DashboardRefreshService: Sendable {
|
||||
@MainActor var isRefreshing: Bool { get }
|
||||
@MainActor var isPaused: Bool { get }
|
||||
@MainActor func start() async
|
||||
@MainActor func stop()
|
||||
@MainActor func pause()
|
||||
@MainActor func resume() async
|
||||
@MainActor func refreshNow() async
|
||||
}
|
||||
|
||||
/// 无操作默认实现,便于提供 Environment 默认值
|
||||
public struct NoopDashboardRefreshService: DashboardRefreshService {
|
||||
public init() {}
|
||||
public var isRefreshing: Bool { false }
|
||||
public var isPaused: Bool { false }
|
||||
@MainActor public func start() async {}
|
||||
@MainActor public func stop() {}
|
||||
@MainActor public func pause() {}
|
||||
@MainActor public func resume() async {}
|
||||
@MainActor public func refreshNow() async {}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
public final class DefaultDashboardRefreshService: DashboardRefreshService {
|
||||
private let api: CursorService
|
||||
private let storage: any CursorStorageService
|
||||
private let settings: AppSettings
|
||||
private let session: AppSession
|
||||
private var loopTask: Task<Void, Never>?
|
||||
public private(set) var isRefreshing: Bool = false
|
||||
public private(set) var isPaused: Bool = false
|
||||
|
||||
public init(
|
||||
api: CursorService,
|
||||
storage: any CursorStorageService,
|
||||
settings: AppSettings,
|
||||
session: AppSession
|
||||
) {
|
||||
self.api = api
|
||||
self.storage = storage
|
||||
self.settings = settings
|
||||
self.session = session
|
||||
}
|
||||
|
||||
public func start() async {
|
||||
await self.bootstrapIfNeeded()
|
||||
await self.refreshNow()
|
||||
|
||||
self.loopTask?.cancel()
|
||||
self.loopTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
while !Task.isCancelled {
|
||||
// 如果暂停,则等待一段时间后再检查
|
||||
if self.isPaused {
|
||||
try? await Task.sleep(for: .seconds(30)) // 暂停时每30秒检查一次状态
|
||||
continue
|
||||
}
|
||||
await self.refreshNow()
|
||||
// 固定 5 分钟刷新一次
|
||||
try? await Task.sleep(for: .seconds(5 * 60))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
self.loopTask?.cancel()
|
||||
self.loopTask = nil
|
||||
}
|
||||
|
||||
public func pause() {
|
||||
self.isPaused = true
|
||||
}
|
||||
|
||||
public func resume() async {
|
||||
self.isPaused = false
|
||||
// 立即刷新一次
|
||||
await self.refreshNow()
|
||||
}
|
||||
|
||||
public func refreshNow() async {
|
||||
if self.isRefreshing || self.isPaused { return }
|
||||
self.isRefreshing = true
|
||||
defer { self.isRefreshing = false }
|
||||
await self.bootstrapIfNeeded()
|
||||
guard let creds = self.session.credentials else { return }
|
||||
|
||||
do {
|
||||
// 计算时间范围
|
||||
let (analyticsStartMs, analyticsEndMs) = self.analyticsDateRangeMs()
|
||||
|
||||
// 使用 async let 并发发起所有独立的 API 请求
|
||||
async let usageSummary = try await self.api.fetchUsageSummary(
|
||||
cookieHeader: creds.cookieHeader
|
||||
)
|
||||
async let history = try await self.api.fetchFilteredUsageEvents(
|
||||
startDateMs: analyticsStartMs,
|
||||
endDateMs: analyticsEndMs,
|
||||
userId: creds.userId,
|
||||
page: 1,
|
||||
cookieHeader: creds.cookieHeader
|
||||
)
|
||||
async let billingCycleMs = try? await self.api.fetchCurrentBillingCycleMs(
|
||||
cookieHeader: creds.cookieHeader
|
||||
)
|
||||
|
||||
// 等待 usageSummary,用于判断账号类型
|
||||
let usageSummaryValue = try await usageSummary
|
||||
|
||||
// Pro 用户使用 filtered usage events 获取图表数据(700 条)
|
||||
// Team/Enterprise 用户使用 models analytics API
|
||||
let modelsUsageChart = try? await self.fetchModelsUsageChartForUser(
|
||||
usageSummary: usageSummaryValue,
|
||||
creds: creds,
|
||||
analyticsStartMs: analyticsStartMs,
|
||||
analyticsEndMs: analyticsEndMs
|
||||
)
|
||||
|
||||
// 获取计费周期(毫秒时间戳格式)
|
||||
let billingCycleValue = await billingCycleMs
|
||||
|
||||
// totalRequestsAllModels 将基于使用事件计算,而非API返回的请求数据
|
||||
let totalAll = 0 // 暂时设为0,后续通过使用事件更新
|
||||
|
||||
let current = self.session.snapshot
|
||||
|
||||
// Team Plan free usage(依赖 usageSummary 判定)
|
||||
func computeFreeCents() async -> Int {
|
||||
if usageSummaryValue.membershipType == .enterprise && creds.isEnterpriseUser == false {
|
||||
return (try? await self.api.fetchTeamFreeUsageCents(
|
||||
teamId: creds.teamId,
|
||||
userId: creds.userId,
|
||||
cookieHeader: creds.cookieHeader
|
||||
)) ?? 0
|
||||
}
|
||||
return 0
|
||||
}
|
||||
let freeCents = await computeFreeCents()
|
||||
|
||||
// 获取聚合使用事件(仅限 Pro 系列账号,非 Team)
|
||||
func fetchModelsUsageSummaryIfNeeded(billingCycleStartMs: String) async -> VibeviewerModel.ModelsUsageSummary? {
|
||||
// 仅 Pro 系列账号才获取(Pro / Pro+ / Ultra,非 Team / Enterprise)
|
||||
let isProAccount = usageSummaryValue.membershipType.isProSeries
|
||||
guard isProAccount else { return nil }
|
||||
|
||||
// 使用账单周期的开始时间(毫秒时间戳)
|
||||
let startDateMs = Int64(billingCycleStartMs) ?? 0
|
||||
|
||||
let aggregated = try? await self.api.fetchAggregatedUsageEvents(
|
||||
teamId: -1,
|
||||
startDate: startDateMs,
|
||||
cookieHeader: creds.cookieHeader
|
||||
)
|
||||
|
||||
return aggregated.map { VibeviewerModel.ModelsUsageSummary(from: $0) }
|
||||
}
|
||||
var modelsUsageSummary: VibeviewerModel.ModelsUsageSummary? = nil
|
||||
if let billingCycleStartMs = billingCycleValue?.startDateMs {
|
||||
modelsUsageSummary = await fetchModelsUsageSummaryIfNeeded(billingCycleStartMs: billingCycleStartMs)
|
||||
}
|
||||
|
||||
// 先更新一次概览(使用旧历史事件),提升 UI 及时性
|
||||
let overview = DashboardSnapshot(
|
||||
email: creds.email,
|
||||
totalRequestsAllModels: totalAll,
|
||||
spendingCents: usageSummaryValue.individualUsage.plan.used,
|
||||
hardLimitDollars: usageSummaryValue.individualUsage.plan.limit / 100,
|
||||
usageEvents: current?.usageEvents ?? [],
|
||||
requestToday: current?.requestToday ?? 0,
|
||||
requestYestoday: current?.requestYestoday ?? 0,
|
||||
usageSummary: usageSummaryValue,
|
||||
freeUsageCents: freeCents,
|
||||
modelsUsageChart: current?.modelsUsageChart,
|
||||
modelsUsageSummary: modelsUsageSummary,
|
||||
billingCycleStartMs: billingCycleValue?.startDateMs,
|
||||
billingCycleEndMs: billingCycleValue?.endDateMs
|
||||
)
|
||||
self.session.snapshot = overview
|
||||
try? await self.storage.saveDashboardSnapshot(overview)
|
||||
|
||||
// 等待并合并历史事件数据
|
||||
let historyValue = try await history
|
||||
let (reqToday, reqYesterday) = self.splitTodayAndYesterdayCounts(from: historyValue.events)
|
||||
let merged = DashboardSnapshot(
|
||||
email: overview.email,
|
||||
totalRequestsAllModels: overview.totalRequestsAllModels,
|
||||
spendingCents: overview.spendingCents,
|
||||
hardLimitDollars: overview.hardLimitDollars,
|
||||
usageEvents: historyValue.events,
|
||||
requestToday: reqToday,
|
||||
requestYestoday: reqYesterday,
|
||||
usageSummary: usageSummaryValue,
|
||||
freeUsageCents: overview.freeUsageCents,
|
||||
modelsUsageChart: modelsUsageChart,
|
||||
modelsUsageSummary: modelsUsageSummary,
|
||||
billingCycleStartMs: billingCycleValue?.startDateMs,
|
||||
billingCycleEndMs: billingCycleValue?.endDateMs
|
||||
)
|
||||
self.session.snapshot = merged
|
||||
try? await self.storage.saveDashboardSnapshot(merged)
|
||||
} catch {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
private func bootstrapIfNeeded() async {
|
||||
if self.session.snapshot == nil, let cached = await self.storage.loadDashboardSnapshot() {
|
||||
self.session.snapshot = cached
|
||||
}
|
||||
if self.session.credentials == nil {
|
||||
self.session.credentials = await self.storage.loadCredentials()
|
||||
}
|
||||
}
|
||||
|
||||
private func yesterdayToNowRangeMs() -> (String, String) {
|
||||
let (start, end) = VibeviewerCore.DateUtils.yesterdayToNowRange()
|
||||
return (VibeviewerCore.DateUtils.millisecondsString(from: start), VibeviewerCore.DateUtils.millisecondsString(from: end))
|
||||
}
|
||||
|
||||
private func analyticsDateRangeMs() -> (String, String) {
|
||||
let days = self.settings.analyticsDataDays
|
||||
let (start, end) = VibeviewerCore.DateUtils.daysAgoToNowRange(days: days)
|
||||
return (VibeviewerCore.DateUtils.millisecondsString(from: start), VibeviewerCore.DateUtils.millisecondsString(from: end))
|
||||
}
|
||||
|
||||
private func splitTodayAndYesterdayCounts(from events: [UsageEvent]) -> (Int, Int) {
|
||||
let calendar = Calendar.current
|
||||
var today = 0
|
||||
var yesterday = 0
|
||||
for e in events {
|
||||
guard let date = VibeviewerCore.DateUtils.date(fromMillisecondsString: e.occurredAtMs) else { continue }
|
||||
if calendar.isDateInToday(date) {
|
||||
today += e.requestCostCount
|
||||
} else if calendar.isDateInYesterday(date) {
|
||||
yesterday += e.requestCostCount
|
||||
}
|
||||
}
|
||||
return (today, yesterday)
|
||||
}
|
||||
|
||||
/// 计算模型分析的时间范围:使用设置中的分析数据范围天数
|
||||
private func modelsAnalyticsDateRange() -> (start: String, end: String) {
|
||||
let days = self.settings.analyticsDataDays
|
||||
return VibeviewerCore.DateUtils.daysAgoToTodayRange(days: days)
|
||||
}
|
||||
|
||||
/// 根据账号类型获取模型使用量图表数据
|
||||
/// - 非 Team 账号(Pro / Pro+ / Ultra / Free 等):使用 filtered usage events(700 条)
|
||||
/// - Team Plan 账号:使用 models analytics API(/api/v2/analytics/team/models)
|
||||
private func fetchModelsUsageChartForUser(
|
||||
usageSummary: VibeviewerModel.UsageSummary,
|
||||
creds: Credentials,
|
||||
analyticsStartMs: String,
|
||||
analyticsEndMs: String
|
||||
) async throws -> VibeviewerModel.ModelsUsageChartData {
|
||||
// 仅 Team Plan 账号调用 team analytics 接口:
|
||||
// - 后端使用 membershipType = .enterprise + isEnterpriseUser = false 表示 Team Plan
|
||||
let isTeamPlanAccount = (usageSummary.membershipType == .enterprise && creds.isEnterpriseUser == false)
|
||||
|
||||
// 非 Team 账号一律使用 filtered usage events,避免误调 /api/v2/analytics/team/ 系列接口
|
||||
guard isTeamPlanAccount else {
|
||||
return try await self.api.fetchModelsUsageChartFromEvents(
|
||||
startDateMs: analyticsStartMs,
|
||||
endDateMs: analyticsEndMs,
|
||||
userId: creds.userId,
|
||||
cookieHeader: creds.cookieHeader
|
||||
)
|
||||
}
|
||||
|
||||
// Team Plan 用户使用 models analytics API
|
||||
let dateRange = self.modelsAnalyticsDateRange()
|
||||
return try await self.api.fetchModelsAnalytics(
|
||||
startDate: dateRange.start,
|
||||
endDate: dateRange.end,
|
||||
c: creds.workosId,
|
||||
cookieHeader: creds.cookieHeader
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import Foundation
|
||||
import VibeviewerAPI
|
||||
import VibeviewerModel
|
||||
import VibeviewerStorage
|
||||
|
||||
public enum LoginServiceError: Error, Equatable {
|
||||
case fetchAccountFailed
|
||||
case saveCredentialsFailed
|
||||
case initialRefreshFailed
|
||||
}
|
||||
|
||||
public protocol LoginService: Sendable {
|
||||
/// 执行完整的登录流程:根据 Cookie 获取账号信息、保存凭据并触发 Dashboard 刷新
|
||||
@MainActor
|
||||
func login(with cookieHeader: String) async throws
|
||||
}
|
||||
|
||||
/// 无操作实现,作为 Environment 的默认值
|
||||
public struct NoopLoginService: LoginService {
|
||||
public init() {}
|
||||
@MainActor
|
||||
public func login(with cookieHeader: String) async throws {}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public final class DefaultLoginService: LoginService {
|
||||
private let api: CursorService
|
||||
private let storage: any CursorStorageService
|
||||
private let refresher: any DashboardRefreshService
|
||||
private let session: AppSession
|
||||
|
||||
public init(
|
||||
api: CursorService,
|
||||
storage: any CursorStorageService,
|
||||
refresher: any DashboardRefreshService,
|
||||
session: AppSession
|
||||
) {
|
||||
self.api = api
|
||||
self.storage = storage
|
||||
self.refresher = refresher
|
||||
self.session = session
|
||||
}
|
||||
|
||||
public func login(with cookieHeader: String) async throws {
|
||||
// 记录登录前状态,用于首次登录失败时回滚
|
||||
let previousCredentials = self.session.credentials
|
||||
let previousSnapshot = self.session.snapshot
|
||||
|
||||
// 1. 使用 Cookie 获取账号信息
|
||||
let me: Credentials
|
||||
do {
|
||||
me = try await self.api.fetchMe(cookieHeader: cookieHeader)
|
||||
} catch {
|
||||
throw LoginServiceError.fetchAccountFailed
|
||||
}
|
||||
|
||||
// 2. 保存凭据并更新会话
|
||||
do {
|
||||
try await self.storage.saveCredentials(me)
|
||||
self.session.credentials = me
|
||||
} catch {
|
||||
throw LoginServiceError.saveCredentialsFailed
|
||||
}
|
||||
|
||||
// 3. 启动后台刷新服务,让其负责拉取和写入 Dashboard 数据
|
||||
await self.refresher.start()
|
||||
|
||||
// 4. 如果是首次登录且依然没有 snapshot,视为登录失败并回滚
|
||||
if previousCredentials == nil, previousSnapshot == nil, self.session.snapshot == nil {
|
||||
await self.storage.clearCredentials()
|
||||
await self.storage.clearDashboardSnapshot()
|
||||
self.session.credentials = nil
|
||||
self.session.snapshot = nil
|
||||
throw LoginServiceError.initialRefreshFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
/// 集成刷新服务和屏幕电源状态的协调器
|
||||
@MainActor
|
||||
@Observable
|
||||
public final class PowerAwareDashboardRefreshService: DashboardRefreshService {
|
||||
private let refreshService: DefaultDashboardRefreshService
|
||||
private let screenPowerService: DefaultScreenPowerStateService
|
||||
|
||||
public var isRefreshing: Bool { refreshService.isRefreshing }
|
||||
public var isPaused: Bool { refreshService.isPaused }
|
||||
|
||||
public init(
|
||||
refreshService: DefaultDashboardRefreshService,
|
||||
screenPowerService: DefaultScreenPowerStateService
|
||||
) {
|
||||
self.refreshService = refreshService
|
||||
self.screenPowerService = screenPowerService
|
||||
|
||||
// 设置屏幕睡眠和唤醒回调
|
||||
screenPowerService.setOnScreenSleep { [weak self] in
|
||||
Task { @MainActor in
|
||||
self?.refreshService.pause()
|
||||
}
|
||||
}
|
||||
|
||||
screenPowerService.setOnScreenWake { [weak self] in
|
||||
Task { @MainActor in
|
||||
await self?.refreshService.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func start() async {
|
||||
// 启动屏幕电源状态监控
|
||||
screenPowerService.startMonitoring()
|
||||
|
||||
// 启动刷新服务
|
||||
await refreshService.start()
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
refreshService.stop()
|
||||
screenPowerService.stopMonitoring()
|
||||
}
|
||||
|
||||
public func pause() {
|
||||
refreshService.pause()
|
||||
}
|
||||
|
||||
public func resume() async {
|
||||
await refreshService.resume()
|
||||
}
|
||||
|
||||
public func refreshNow() async {
|
||||
await refreshService.refreshNow()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
/// 屏幕电源状态服务协议
|
||||
public protocol ScreenPowerStateService: Sendable {
|
||||
@MainActor var isScreenAwake: Bool { get }
|
||||
@MainActor func startMonitoring()
|
||||
@MainActor func stopMonitoring()
|
||||
@MainActor func setOnScreenSleep(_ handler: @escaping @Sendable () -> Void)
|
||||
@MainActor func setOnScreenWake(_ handler: @escaping @Sendable () -> Void)
|
||||
}
|
||||
|
||||
/// 默认屏幕电源状态服务实现
|
||||
@MainActor
|
||||
public final class DefaultScreenPowerStateService: ScreenPowerStateService, ObservableObject {
|
||||
public private(set) var isScreenAwake: Bool = true
|
||||
|
||||
private var onScreenSleep: (@Sendable () -> Void)?
|
||||
private var onScreenWake: (@Sendable () -> Void)?
|
||||
|
||||
public init() {}
|
||||
|
||||
public func setOnScreenSleep(_ handler: @escaping @Sendable () -> Void) {
|
||||
self.onScreenSleep = handler
|
||||
}
|
||||
|
||||
public func setOnScreenWake(_ handler: @escaping @Sendable () -> Void) {
|
||||
self.onScreenWake = handler
|
||||
}
|
||||
|
||||
public func startMonitoring() {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSWorkspace.willSleepNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.handleScreenSleep()
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSWorkspace.didWakeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.handleScreenWake()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func stopMonitoring() {
|
||||
NotificationCenter.default.removeObserver(self, name: NSWorkspace.willSleepNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: NSWorkspace.didWakeNotification, object: nil)
|
||||
}
|
||||
|
||||
private func handleScreenSleep() {
|
||||
isScreenAwake = false
|
||||
onScreenSleep?()
|
||||
}
|
||||
|
||||
private func handleScreenWake() {
|
||||
isScreenAwake = true
|
||||
onScreenWake?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 无操作默认实现,便于提供 Environment 默认值
|
||||
public struct NoopScreenPowerStateService: ScreenPowerStateService {
|
||||
public init() {}
|
||||
public var isScreenAwake: Bool { true }
|
||||
public func startMonitoring() {}
|
||||
public func stopMonitoring() {}
|
||||
public func setOnScreenSleep(_ handler: @escaping @Sendable () -> Void) {}
|
||||
public func setOnScreenWake(_ handler: @escaping @Sendable () -> Void) {}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
|
||||
/// 应用更新服务协议
|
||||
public protocol UpdateService: Sendable {
|
||||
/// 检查更新(手动触发)
|
||||
@MainActor func checkForUpdates()
|
||||
|
||||
/// 自动检查更新(在应用启动时调用)
|
||||
@MainActor func checkForUpdatesInBackground()
|
||||
|
||||
/// 是否正在检查更新
|
||||
@MainActor var isCheckingForUpdates: Bool { get }
|
||||
|
||||
/// 是否有可用更新
|
||||
@MainActor var updateAvailable: Bool { get }
|
||||
|
||||
/// 当前版本信息
|
||||
var currentVersion: String { get }
|
||||
|
||||
/// 最新可用版本号(如果有更新)
|
||||
@MainActor var latestVersion: String? { get }
|
||||
|
||||
/// 上次检查更新的时间
|
||||
@MainActor var lastUpdateCheckDate: Date? { get }
|
||||
|
||||
/// 更新状态描述
|
||||
@MainActor var updateStatusDescription: String { get }
|
||||
}
|
||||
|
||||
/// 无操作默认实现,便于提供 Environment 默认值
|
||||
public struct NoopUpdateService: UpdateService {
|
||||
public init() {}
|
||||
|
||||
@MainActor public func checkForUpdates() {}
|
||||
@MainActor public func checkForUpdatesInBackground() {}
|
||||
@MainActor public var isCheckingForUpdates: Bool { false }
|
||||
@MainActor public var updateAvailable: Bool { false }
|
||||
public var currentVersion: String {
|
||||
// 使用 Bundle.main 读取版本号
|
||||
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String, !version.isEmpty {
|
||||
return version
|
||||
}
|
||||
// Fallback: 尝试从 CFBundleVersion 读取
|
||||
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String, !version.isEmpty {
|
||||
return version
|
||||
}
|
||||
// 默认版本号
|
||||
return "1.1.9"
|
||||
}
|
||||
@MainActor public var latestVersion: String? { nil }
|
||||
@MainActor public var lastUpdateCheckDate: Date? { nil }
|
||||
@MainActor public var updateStatusDescription: String { "更新服务不可用" }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user