蜂鸟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:
51
参考计费/Packages/VibeviewerSettingsUI/Package.resolved
Normal file
51
参考计费/Packages/VibeviewerSettingsUI/Package.resolved
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"originHash" : "87b7891a178f9f79751f334c663dfe85e6310bca1bb0aa6f3cce8da4fe4fb426",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "alamofire",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Alamofire/Alamofire.git",
|
||||
"state" : {
|
||||
"revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5",
|
||||
"version" : "5.10.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "moya",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/Moya/Moya.git",
|
||||
"state" : {
|
||||
"revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26",
|
||||
"version" : "15.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "reactiveswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git",
|
||||
"state" : {
|
||||
"revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c",
|
||||
"version" : "6.7.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "rxswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/ReactiveX/RxSwift.git",
|
||||
"state" : {
|
||||
"revision" : "5dd1907d64f0d36f158f61a466bab75067224893",
|
||||
"version" : "6.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sparkle",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||
"state" : {
|
||||
"revision" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb",
|
||||
"version" : "2.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
28
参考计费/Packages/VibeviewerSettingsUI/Package.swift
Normal file
28
参考计费/Packages/VibeviewerSettingsUI/Package.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
// swift-tools-version:5.10
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "VibeviewerSettingsUI",
|
||||
platforms: [
|
||||
.macOS(.v14)
|
||||
],
|
||||
products: [
|
||||
.library(name: "VibeviewerSettingsUI", targets: ["VibeviewerSettingsUI"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../VibeviewerModel"),
|
||||
.package(path: "../VibeviewerAppEnvironment"),
|
||||
.package(path: "../VibeviewerShareUI"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "VibeviewerSettingsUI",
|
||||
dependencies: [
|
||||
"VibeviewerModel",
|
||||
"VibeviewerAppEnvironment",
|
||||
"VibeviewerShareUI",
|
||||
]
|
||||
),
|
||||
.testTarget(name: "VibeviewerSettingsUITests", dependencies: ["VibeviewerSettingsUI"]),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
import SwiftUI
|
||||
|
||||
private struct SettingsWindowManagerKey: EnvironmentKey {
|
||||
@MainActor
|
||||
static var defaultValue: SettingsWindowManager {
|
||||
SettingsWindowManager.shared
|
||||
}
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var settingsWindowManager: SettingsWindowManager {
|
||||
get { self[SettingsWindowManagerKey.self] }
|
||||
set { self[SettingsWindowManagerKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import VibeviewerAppEnvironment
|
||||
import VibeviewerCore
|
||||
import VibeviewerModel
|
||||
import VibeviewerStorage
|
||||
|
||||
@MainActor
|
||||
public final class SettingsWindowManager {
|
||||
public static let shared = SettingsWindowManager()
|
||||
private var controller: NSWindowController?
|
||||
public var appSettings: AppSettings = DefaultCursorStorageService.loadSettingsSync()
|
||||
public var appSession: AppSession = AppSession(
|
||||
credentials: DefaultCursorStorageService.loadCredentialsSync(),
|
||||
snapshot: DefaultCursorStorageService.loadDashboardSnapshotSync()
|
||||
)
|
||||
public var dashboardRefreshService: any DashboardRefreshService = NoopDashboardRefreshService()
|
||||
public var updateService: any UpdateService = NoopUpdateService()
|
||||
|
||||
public func show() {
|
||||
// Close MenuBarExtra popover window if it's open
|
||||
closeMenuBarExtraWindow()
|
||||
|
||||
if let controller {
|
||||
controller.close()
|
||||
self.controller = nil
|
||||
}
|
||||
let vc = NSHostingController(rootView: SettingsView()
|
||||
.environment(self.appSettings)
|
||||
.environment(self.appSession)
|
||||
.environment(\.dashboardRefreshService, self.dashboardRefreshService)
|
||||
.environment(\.updateService, self.updateService)
|
||||
.environment(\.cursorStorage, DefaultCursorStorageService())
|
||||
.environment(\.launchAtLoginService, DefaultLaunchAtLoginService()))
|
||||
let window = NSWindow(contentViewController: vc)
|
||||
window.title = "Settings"
|
||||
window.setContentSize(NSSize(width: 560, height: 500))
|
||||
window.styleMask = [.titled, .closable]
|
||||
window.isReleasedWhenClosed = false
|
||||
window.titlebarAppearsTransparent = false
|
||||
window.toolbarStyle = .unified
|
||||
let ctrl = NSWindowController(window: window)
|
||||
self.controller = ctrl
|
||||
ctrl.window?.center()
|
||||
ctrl.showWindow(nil)
|
||||
ctrl.window?.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
private func closeMenuBarExtraWindow() {
|
||||
// Close MenuBarExtra popover windows
|
||||
// MenuBarExtra windows are typically non-activating NSPanel instances
|
||||
for window in NSApp.windows {
|
||||
if let panel = window as? NSPanel,
|
||||
panel.styleMask.contains(.nonactivatingPanel),
|
||||
window != self.controller?.window {
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func close() {
|
||||
self.controller?.close()
|
||||
self.controller = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import Observation
|
||||
import SwiftUI
|
||||
import VibeviewerAppEnvironment
|
||||
import VibeviewerModel
|
||||
import VibeviewerShareUI
|
||||
|
||||
public struct SettingsView: View {
|
||||
@Environment(AppSettings.self) private var appSettings
|
||||
@Environment(\.cursorStorage) private var storage
|
||||
@Environment(\.launchAtLoginService) private var launchAtLoginService
|
||||
@Environment(\.dashboardRefreshService) private var refresher
|
||||
@Environment(\.updateService) private var updateService
|
||||
@Environment(AppSession.self) private var session
|
||||
|
||||
@State private var refreshFrequency: Int = 5
|
||||
@State private var usageHistoryLimit: Int = 5
|
||||
@State private var pauseOnScreenSleep: Bool = false
|
||||
@State private var launchAtLogin: Bool = false
|
||||
@State private var appearanceSelection: VibeviewerModel.AppAppearance = .system
|
||||
@State private var showingClearSessionAlert: Bool = false
|
||||
@State private var showingLogoutAlert: Bool = false
|
||||
@State private var analyticsDataDays: Int = 7
|
||||
|
||||
// 预定义选项
|
||||
private let refreshFrequencyOptions: [Int] = [1, 2, 3, 5, 10, 15, 30]
|
||||
private let usageHistoryLimitOptions: [Int] = [5, 10, 20, 50, 100]
|
||||
private let analyticsDataDaysOptions: [Int] = [3, 7, 14, 30, 60, 90]
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Picker("Appearance", selection: $appearanceSelection) {
|
||||
Text("System").tag(VibeviewerModel.AppAppearance.system)
|
||||
Text("Light").tag(VibeviewerModel.AppAppearance.light)
|
||||
Text("Dark").tag(VibeviewerModel.AppAppearance.dark)
|
||||
}
|
||||
.onChange(of: appearanceSelection) { oldValue, newValue in
|
||||
appSettings.appearance = newValue
|
||||
Task { @MainActor in
|
||||
try? await appSettings.save(using: storage)
|
||||
}
|
||||
}
|
||||
|
||||
// 版本信息
|
||||
HStack {
|
||||
Text("Current Version")
|
||||
Spacer()
|
||||
Text(updateService.currentVersion)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("General")
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Refresh Frequency", selection: $refreshFrequency) {
|
||||
ForEach(refreshFrequencyOptions, id: \.self) { value in
|
||||
Text("\(value) minutes").tag(value)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.onChange(of: refreshFrequency) { oldValue, newValue in
|
||||
appSettings.overview.refreshInterval = newValue
|
||||
Task { @MainActor in
|
||||
try? await appSettings.save(using: storage)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Usage History Limit", selection: $usageHistoryLimit) {
|
||||
ForEach(usageHistoryLimitOptions, id: \.self) { value in
|
||||
Text("\(value) items").tag(value)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.onChange(of: usageHistoryLimit) { oldValue, newValue in
|
||||
appSettings.usageHistory.limit = newValue
|
||||
Task { @MainActor in
|
||||
try? await appSettings.save(using: storage)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Analytics Data Range", selection: $analyticsDataDays) {
|
||||
ForEach(analyticsDataDaysOptions, id: \.self) { value in
|
||||
Text("\(value) days").tag(value)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.onChange(of: analyticsDataDays) { oldValue, newValue in
|
||||
appSettings.analyticsDataDays = newValue
|
||||
Task { @MainActor in
|
||||
try? await appSettings.save(using: storage)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Data")
|
||||
} footer: {
|
||||
Text("Refresh Frequency: Controls the automatic refresh interval for dashboard data.\nUsage History Limit: Limits the number of usage history items displayed.\nAnalytics Data Range: Controls the number of days of data shown in analytics charts.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Pause refresh when screen sleeps", isOn: $pauseOnScreenSleep)
|
||||
.onChange(of: pauseOnScreenSleep) { oldValue, newValue in
|
||||
appSettings.pauseOnScreenSleep = newValue
|
||||
Task { @MainActor in
|
||||
try? await appSettings.save(using: storage)
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Launch at login", isOn: $launchAtLogin)
|
||||
.onChange(of: launchAtLogin) { oldValue, newValue in
|
||||
_ = launchAtLoginService.setEnabled(newValue)
|
||||
appSettings.launchAtLogin = newValue
|
||||
Task { @MainActor in
|
||||
try? await appSettings.save(using: storage)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Behavior")
|
||||
}
|
||||
|
||||
if session.credentials != nil {
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
showingLogoutAlert = true
|
||||
} label: {
|
||||
Text("Log Out")
|
||||
}
|
||||
} header: {
|
||||
Text("Account")
|
||||
} footer: {
|
||||
Text("Clear login credentials and stop data refresh. You will need to log in again to continue using the app.")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
showingClearSessionAlert = true
|
||||
} label: {
|
||||
Text("Clear App Cache")
|
||||
}
|
||||
} header: {
|
||||
Text("Advanced")
|
||||
} footer: {
|
||||
Text("Clear all stored credentials and dashboard data. You will need to log in again.")
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.frame(width: 560, height: 500)
|
||||
.onAppear {
|
||||
loadSettings()
|
||||
}
|
||||
.alert("Log Out", isPresented: $showingLogoutAlert) {
|
||||
Button("Cancel", role: .cancel) { }
|
||||
Button("Log Out", role: .destructive) {
|
||||
Task { @MainActor in
|
||||
await logout()
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text("This will clear your login credentials and stop data refresh. You will need to log in again to continue using the app.")
|
||||
}
|
||||
.alert("Clear App Cache", isPresented: $showingClearSessionAlert) {
|
||||
Button("Cancel", role: .cancel) { }
|
||||
Button("Clear", role: .destructive) {
|
||||
Task { @MainActor in
|
||||
await clearAppSession()
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text("This will clear all stored credentials and dashboard data. You will need to log in again.")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSettings() {
|
||||
// 加载设置值
|
||||
let currentRefreshFrequency = appSettings.overview.refreshInterval
|
||||
let currentUsageHistoryLimit = appSettings.usageHistory.limit
|
||||
let currentAnalyticsDataDays = appSettings.analyticsDataDays
|
||||
|
||||
// 如果当前值不在选项中,使用最接近的值并更新设置
|
||||
if refreshFrequencyOptions.contains(currentRefreshFrequency) {
|
||||
refreshFrequency = currentRefreshFrequency
|
||||
} else {
|
||||
let closest = refreshFrequencyOptions.min(by: { abs($0 - currentRefreshFrequency) < abs($1 - currentRefreshFrequency) }) ?? 5
|
||||
refreshFrequency = closest
|
||||
appSettings.overview.refreshInterval = closest
|
||||
}
|
||||
|
||||
if usageHistoryLimitOptions.contains(currentUsageHistoryLimit) {
|
||||
usageHistoryLimit = currentUsageHistoryLimit
|
||||
} else {
|
||||
let closest = usageHistoryLimitOptions.min(by: { abs($0 - currentUsageHistoryLimit) < abs($1 - currentUsageHistoryLimit) }) ?? 5
|
||||
usageHistoryLimit = closest
|
||||
appSettings.usageHistory.limit = closest
|
||||
}
|
||||
|
||||
if analyticsDataDaysOptions.contains(currentAnalyticsDataDays) {
|
||||
analyticsDataDays = currentAnalyticsDataDays
|
||||
} else {
|
||||
let closest = analyticsDataDaysOptions.min(by: { abs($0 - currentAnalyticsDataDays) < abs($1 - currentAnalyticsDataDays) }) ?? 7
|
||||
analyticsDataDays = closest
|
||||
appSettings.analyticsDataDays = closest
|
||||
}
|
||||
|
||||
pauseOnScreenSleep = appSettings.pauseOnScreenSleep
|
||||
launchAtLogin = launchAtLoginService.isEnabled
|
||||
appearanceSelection = appSettings.appearance
|
||||
|
||||
// 如果值被调整了,保存设置
|
||||
if !refreshFrequencyOptions.contains(currentRefreshFrequency) ||
|
||||
!usageHistoryLimitOptions.contains(currentUsageHistoryLimit) ||
|
||||
!analyticsDataDaysOptions.contains(currentAnalyticsDataDays) {
|
||||
Task { @MainActor in
|
||||
try? await appSettings.save(using: storage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func logout() async {
|
||||
// 停止刷新服务
|
||||
refresher.stop()
|
||||
|
||||
// 清空存储的凭据与当前仪表盘快照,使应用回到需要登录的状态
|
||||
await storage.clearCredentials()
|
||||
await storage.clearDashboardSnapshot()
|
||||
|
||||
// 重置内存中的会话数据
|
||||
session.credentials = nil
|
||||
session.snapshot = nil
|
||||
|
||||
// 关闭设置窗口
|
||||
NSApplication.shared.keyWindow?.close()
|
||||
}
|
||||
|
||||
private func clearAppSession() async {
|
||||
// 停止刷新服务
|
||||
refresher.stop()
|
||||
|
||||
// 清空存储的 AppSession 数据
|
||||
await storage.clearAppSession()
|
||||
|
||||
// 重置内存中的 AppSession
|
||||
session.credentials = nil
|
||||
session.snapshot = nil
|
||||
|
||||
// 关闭设置窗口
|
||||
NSApplication.shared.keyWindow?.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@testable import VibeviewerSettingsUI
|
||||
import XCTest
|
||||
|
||||
final class VibeviewerSettingsUITests: XCTestCase {
|
||||
func testExample() {
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user