蜂鸟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,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>
|
||||
29
参考计费/Packages/VibeviewerStorage/Package.swift
Normal file
29
参考计费/Packages/VibeviewerStorage/Package.swift
Normal 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"]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import VibeviewerModel
|
||||
|
||||
public extension AppSettings {
|
||||
func save(using storage: any CursorStorageService) async throws {
|
||||
try await storage.saveSettings(self)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user