蜂鸟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,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1630"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "VibeviewerStorageTests"
BuildableName = "VibeviewerStorageTests"
BlueprintName = "VibeviewerStorageTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2EA77F6E99C6AB702EE96DA0"
BuildableName = "Vibeviewer.app"
BlueprintName = "Vibeviewer"
ReferencedContainer = "container:../../Vibeviewer.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2EA77F6E99C6AB702EE96DA0"
BuildableName = "Vibeviewer.app"
BlueprintName = "Vibeviewer"
ReferencedContainer = "container:../../Vibeviewer.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,29 @@
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
name: "VibeviewerStorage",
platforms: [
.macOS(.v14)
],
products: [
.library(name: "VibeviewerStorage", targets: ["VibeviewerStorage"])
],
dependencies: [
.package(path: "../VibeviewerModel")
],
targets: [
.target(
name: "VibeviewerStorage",
dependencies: [
.product(name: "VibeviewerModel", package: "VibeviewerModel")
]
),
.testTarget(
name: "VibeviewerStorageTests",
dependencies: ["VibeviewerStorage"]
)
]
)

View File

@@ -0,0 +1,8 @@
import Foundation
import VibeviewerModel
public extension AppSettings {
func save(using storage: any CursorStorageService) async throws {
try await storage.saveSettings(self)
}
}

View File

@@ -0,0 +1,34 @@
import Foundation
import VibeviewerModel
// Service Protocol (exposed)
public protocol CursorStorageService: Sendable {
// Credentials
func saveCredentials(_ creds: Credentials) async throws
func loadCredentials() async -> Credentials?
func clearCredentials() async
// Dashboard Snapshot
func saveDashboardSnapshot(_ snapshot: DashboardSnapshot) async throws
func loadDashboardSnapshot() async -> DashboardSnapshot?
func clearDashboardSnapshot() async
// App Settings
func saveSettings(_ settings: AppSettings) async throws
func loadSettings() async -> AppSettings
// Billing Cycle
func saveBillingCycle(startDateMs: String, endDateMs: String) async throws
func loadBillingCycle() async -> (startDateMs: String, endDateMs: String)?
func clearBillingCycle() async
// AppSession Management
func clearAppSession() async
}
// Synchronous preload helpers for app launch use-cases
public protocol CursorStorageSyncHelpers {
static func loadCredentialsSync() -> Credentials?
static func loadDashboardSnapshotSync() -> DashboardSnapshot?
static func loadSettingsSync() -> AppSettings
}

View File

@@ -0,0 +1,117 @@
import Foundation
import VibeviewerModel
public enum CursorStorageKeys {
public static let credentials = "cursor.credentials.v1"
public static let settings = "app.settings.v1"
public static let dashboardSnapshot = "cursor.dashboard.snapshot.v1"
public static let billingCycle = "cursor.billing.cycle.v1"
}
public struct DefaultCursorStorageService: CursorStorageService, CursorStorageSyncHelpers {
private let defaults: UserDefaults
public init(userDefaults: UserDefaults = .standard) {
self.defaults = userDefaults
}
// MARK: - Credentials
public func saveCredentials(_ me: Credentials) async throws {
let data = try JSONEncoder().encode(me)
self.defaults.set(data, forKey: CursorStorageKeys.credentials)
}
public func loadCredentials() async -> Credentials? {
guard let data = self.defaults.data(forKey: CursorStorageKeys.credentials) else { return nil }
return try? JSONDecoder().decode(Credentials.self, from: data)
}
public func clearCredentials() async {
self.defaults.removeObject(forKey: CursorStorageKeys.credentials)
}
// MARK: - Dashboard Snapshot
public func saveDashboardSnapshot(_ snapshot: DashboardSnapshot) async throws {
let data = try JSONEncoder().encode(snapshot)
self.defaults.set(data, forKey: CursorStorageKeys.dashboardSnapshot)
}
public func loadDashboardSnapshot() async -> DashboardSnapshot? {
guard let data = self.defaults.data(forKey: CursorStorageKeys.dashboardSnapshot) else { return nil }
return try? JSONDecoder().decode(DashboardSnapshot.self, from: data)
}
public func clearDashboardSnapshot() async {
self.defaults.removeObject(forKey: CursorStorageKeys.dashboardSnapshot)
}
// MARK: - App Settings
public func saveSettings(_ settings: AppSettings) async throws {
let data = try JSONEncoder().encode(settings)
self.defaults.set(data, forKey: CursorStorageKeys.settings)
}
public func loadSettings() async -> AppSettings {
if let data = self.defaults.data(forKey: CursorStorageKeys.settings),
let decoded = try? JSONDecoder().decode(AppSettings.self, from: data)
{
return decoded
}
return AppSettings()
}
// MARK: - Billing Cycle
public func saveBillingCycle(startDateMs: String, endDateMs: String) async throws {
let data: [String: String] = [
"startDateMs": startDateMs,
"endDateMs": endDateMs
]
let jsonData = try JSONEncoder().encode(data)
self.defaults.set(jsonData, forKey: CursorStorageKeys.billingCycle)
}
public func loadBillingCycle() async -> (startDateMs: String, endDateMs: String)? {
guard let data = self.defaults.data(forKey: CursorStorageKeys.billingCycle),
let dict = try? JSONDecoder().decode([String: String].self, from: data),
let startDateMs = dict["startDateMs"],
let endDateMs = dict["endDateMs"] else {
return nil
}
return (startDateMs: startDateMs, endDateMs: endDateMs)
}
public func clearBillingCycle() async {
self.defaults.removeObject(forKey: CursorStorageKeys.billingCycle)
}
// MARK: - AppSession Management
public func clearAppSession() async {
await clearCredentials()
await clearDashboardSnapshot()
}
// MARK: - Sync Helpers
public static func loadCredentialsSync() -> Credentials? {
let defaults = UserDefaults.standard
guard let data = defaults.data(forKey: CursorStorageKeys.credentials) else { return nil }
return try? JSONDecoder().decode(Credentials.self, from: data)
}
public static func loadDashboardSnapshotSync() -> DashboardSnapshot? {
let defaults = UserDefaults.standard
guard let data = defaults.data(forKey: CursorStorageKeys.dashboardSnapshot) else { return nil }
return try? JSONDecoder().decode(DashboardSnapshot.self, from: data)
}
public static func loadSettingsSync() -> AppSettings {
let defaults = UserDefaults.standard
guard let data = defaults.data(forKey: CursorStorageKeys.settings) else { return AppSettings() }
return (try? JSONDecoder().decode(AppSettings.self, from: data)) ?? AppSettings()
}
}

View File

@@ -0,0 +1,37 @@
import Foundation
import Testing
import VibeviewerModel
@testable import VibeviewerStorage
@Suite("StorageService basic")
struct StorageServiceTests {
@Test("Credentials save/load/clear")
func credentialsCRUD() async throws {
let suite = UserDefaults(suiteName: "test.credentials.")!
suite.removePersistentDomain(forName: "test.credentials.")
let storage = DefaultCursorStorageService(userDefaults: suite)
let creds = Credentials(userId: 123_456, workosId: "w1", email: "e@x.com", teamId: 1, cookieHeader: "c", isEnterpriseUser: false)
try await storage.saveCredentials(creds)
let loaded = await storage.loadCredentials()
#expect(loaded == creds)
await storage.clearCredentials()
let cleared = await storage.loadCredentials()
#expect(cleared == nil)
}
@Test("Snapshot save/load/clear")
func snapshotCRUD() async throws {
let suite = UserDefaults(suiteName: "test.snapshot.")!
suite.removePersistentDomain(forName: "test.snapshot.")
let storage = DefaultCursorStorageService(userDefaults: suite)
let snap = DashboardSnapshot(email: "e@x.com", totalRequestsAllModels: 2, spendingCents: 3, hardLimitDollars: 4)
try await storage.saveDashboardSnapshot(snap)
let loaded = await storage.loadDashboardSnapshot()
#expect(loaded == snap)
await storage.clearDashboardSnapshot()
let cleared = await storage.loadDashboardSnapshot()
#expect(cleared == nil)
}
}