蜂鸟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:
ccdojox-crypto
2025-12-18 11:21:52 +08:00
parent f310ca7b97
commit 73a71f198f
202 changed files with 19142 additions and 252 deletions

View 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
}

View 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"]),
]
)

View File

@@ -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 }
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1,8 @@
@testable import VibeviewerSettingsUI
import XCTest
final class VibeviewerSettingsUITests: XCTestCase {
func testExample() {
XCTAssertTrue(true)
}
}