蜂鸟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,29 @@
// swift-tools-version:5.10
import PackageDescription
let package = Package(
name: "VibeviewerShareUI",
platforms: [
.macOS(.v14)
],
products: [
.library(name: "VibeviewerShareUI", targets: ["VibeviewerShareUI"])
],
dependencies: [
.package(path: "../VibeviewerModel")
],
targets: [
.target(
name: "VibeviewerShareUI",
dependencies: ["VibeviewerModel"],
resources: [
// Sources/VibeviewerShareUI/Fonts/
// Satoshi-Regular.otfSatoshi-Medium.otfSatoshi-Bold.otfSatoshi-Italic.otf
.process("Fonts"),
.process("Images"),
.process("Shaders")
]
),
.testTarget(name: "VibeviewerShareUITests", dependencies: ["VibeviewerShareUI"]),
]
)

View File

@@ -0,0 +1,34 @@
import SwiftUI
public struct VibeButtonStyle: ButtonStyle {
var tintColor: Color
@GestureState private var isPressing = false
private let pressScale: CGFloat = 0.94
@State private var isHovering: Bool = false
public init(_ tint: Color) {
self.tintColor = tint
}
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(tintColor)
.font(.app(.satoshiMedium, size: 12))
.padding(.horizontal, 12)
.padding(.vertical, 8)
.overlayBorder(color: tintColor.opacity(isHovering ? 1 : 0.4), lineWidth: 1, cornerRadius: 100)
.scaleEffect(configuration.isPressed || isPressing ? pressScale : 1.0)
.animation(.snappy(duration: 0.2), value: configuration.isPressed || isPressing)
.scaleEffect(isHovering ? 1.05 : 1.0)
.onHover { isHovering = $0 }
.animation(.easeInOut(duration: 0.2), value: isHovering)
}
}
extension ButtonStyle where Self == VibeButtonStyle {
public static func vibe(_ tint: Color) -> Self {
VibeButtonStyle(tint)
}
}

View File

@@ -0,0 +1,26 @@
import SwiftUI
public enum AppFont: String, CaseIterable {
case satoshiRegular = "Satoshi-Regular"
case satoshiMedium = "Satoshi-Medium"
case satoshiBold = "Satoshi-Bold"
case satoshiItalic = "Satoshi-Italic"
}
public extension Font {
/// Create a Font from AppFont with given size and optional relative weight.
static func app(_ font: AppFont, size: CGFloat, weight: Weight? = nil) -> Font {
FontsRegistrar.registerAllFonts()
let f = Font.custom(font.rawValue, size: size)
if let weight {
return f.weight(weight)
}
return f
}
/// Convenience semantic fonts
static func appTitle(_ size: CGFloat = 20) -> Font { .app(.satoshiBold, size: size) }
static func appBody(_ size: CGFloat = 15) -> Font { .app(.satoshiRegular, size: size) }
static func appEmphasis(_ size: CGFloat = 15) -> Font { .app(.satoshiMedium, size: size) }
static func appCaption(_ size: CGFloat = 12) -> Font { .app(.satoshiRegular, size: size) }
}

View File

@@ -0,0 +1,34 @@
import CoreText
import Foundation
public enum FontsRegistrar {
/// Registers all font files shipped in the module bundle under `Fonts/`.
/// Safe to call multiple times; duplicates are ignored by CoreText.
public static func registerAllFonts() {
let bundle = Bundle.module
let subdir = "Fonts"
let otfURLs = bundle.urls(forResourcesWithExtension: "otf", subdirectory: subdir) ?? []
let ttfURLs = bundle.urls(forResourcesWithExtension: "ttf", subdirectory: subdir) ?? []
let urls = otfURLs + ttfURLs
for url in urls {
var error: Unmanaged<CFError>?
// Use process scope so registration lives for the app lifecycle
let ok = CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error)
if !ok {
// Ignore already-registered errors; other errors can be logged in debug
#if DEBUG
if let err = error?.takeRetainedValue() {
let cfError = err as Error
// CFError domain kCTFontManagerErrorDomain code 305 means already registered
// We'll just print in debug builds
print(
"[FontsRegistrar] Font registration error for \(url.lastPathComponent): \(cfError)"
)
}
#endif
}
}
}
}

View File

@@ -0,0 +1,53 @@
import Foundation
import SwiftUI
import VibeviewerModel
public extension AIModelBrands {
/// SPM macOS URL PDF/PNG
private func moduleImage(_ name: String) -> Image {
#if canImport(AppKit)
if let url = Bundle.module.url(forResource: name, withExtension: "pdf"),
let nsImage = NSImage(contentsOf: url) {
return Image(nsImage: nsImage)
}
if let url = Bundle.module.url(forResource: name, withExtension: "png"),
let nsImage = NSImage(contentsOf: url) {
return Image(nsImage: nsImage)
}
// 退
return Image(systemName: "app")
#else
if let url = Bundle.module.url(forResource: name, withExtension: "pdf"),
let data = try? Data(contentsOf: url),
let uiImage = UIImage(data: data) {
return Image(uiImage: uiImage)
}
if let url = Bundle.module.url(forResource: name, withExtension: "png"),
let data = try? Data(contentsOf: url),
let uiImage = UIImage(data: data) {
return Image(uiImage: uiImage)
}
// 退
return Image(systemName: "app")
#endif
}
var logo: Image {
switch self {
case .gpt:
return moduleImage("gpt")
case .claude:
return moduleImage("claude")
case .deepseek:
return moduleImage("deepseek")
case .gemini:
return moduleImage("gemini")
case .grok:
return moduleImage("grok").renderingMode(.template)
case .kimi:
return moduleImage("kimi")
case .default:
return moduleImage("cursor")
}
}
}

View File

@@ -0,0 +1,21 @@
//
// noise.metal
// Goby
//
// Created by Groot on 2025/8/14.
//
#include <metal_stdlib>
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;
[[ stitchable ]]
half4 parameterizedNoise(float2 position, half4 color, float intensity, float frequency, float opacity) {
float value = fract(cos(dot(position * frequency, float2(12.9898, 78.233))) * 43758.5453);
float r = color.r * mix(1.0, value, intensity);
float g = color.g * mix(1.0, value, intensity);
float b = color.b * mix(1.0, value, intensity);
return half4(r, g, b, opacity);
}

View File

@@ -0,0 +1,47 @@
#include <metal_stdlib>
#include <SwiftUI/SwiftUI.h>
using namespace metal;
[[ stitchable ]]
half4 Ripple(
float2 position,
SwiftUI::Layer layer,
float2 origin,
float time,
float amplitude,
float frequency,
float decay,
float speed
) {
// The distance of the current pixel position from `origin`.
float distance = length(position - origin);
// The amount of time it takes for the ripple to arrive at the current pixel position.
float delay = distance / speed;
// Adjust for delay, clamp to 0.
time -= delay;
time = max(0.0, time);
// The ripple is a sine wave that Metal scales by an exponential decay
// function.
float rippleAmount = amplitude * sin(frequency * time) * exp(-decay * time);
// A vector of length `amplitude` that points away from position.
float2 n = normalize(position - origin);
// Scale `n` by the ripple amount at the current pixel position and add it
// to the current pixel position.
//
// This new position moves toward or away from `origin` based on the
// sign and magnitude of `rippleAmount`.
float2 newPosition = position + rippleAmount * n;
// Sample the layer at the new position.
half4 color = layer.sample(newPosition);
// Lighten or darken the color based on the ripple amount and its alpha
// component.
color.rgb += 0.3 * (rippleAmount / amplitude) * color.a;
return color;
}

View File

@@ -0,0 +1,16 @@
import VibeviewerModel
import SwiftUI
public extension View {
@ViewBuilder
func applyPreferredColorScheme(_ appearance: VibeviewerModel.AppAppearance) -> some View {
switch appearance {
case .system:
self
case .light:
self.environment(\.colorScheme, .light)
case .dark:
self.environment(\.colorScheme, .dark)
}
}
}

View File

@@ -0,0 +1,28 @@
import SwiftUI
public extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}

View File

@@ -0,0 +1,105 @@
import SwiftUI
import AppKit
public struct MenuBarExtraTransparencyHelperView: NSViewRepresentable {
public init() {}
public class WindowConfiguratorView: NSView {
public override func viewWillDraw() {
super.viewWillDraw()
self.configure(window: self.window)
}
public override func viewWillMove(toWindow newWindow: NSWindow?) {
super.viewWillMove(toWindow: newWindow)
self.configure(window: newWindow)
}
public override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
self.configure(window: self.window)
}
private func configure(window: NSWindow?) {
guard let window else { return }
// Make the underlying Menu Bar Extra panel/window transparent
window.styleMask.insert(.fullSizeContentView)
window.isOpaque = false
window.backgroundColor = .clear
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.hasShadow = true
guard let contentView = window.contentView else { return }
// Ensure content view is fully transparent
contentView.wantsLayer = true
contentView.layer?.backgroundColor = NSColor.clear.cgColor
contentView.layer?.isOpaque = false
// Clear any default backgrounds across the entire ancestor chain
self.clearBackgroundUpwards(from: contentView)
// If you want translucent blur instead of fully transparent, uncomment the block below
// addBlur(in: contentView)
}
private func clearBackgroundRecursively(in view: NSView?) {
guard let view else { return }
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.clear.cgColor
view.layer?.isOpaque = false
if let eff = view as? NSVisualEffectView, eff.identifier?.rawValue != "vv_transparent_blur" {
// /
eff.removeFromSuperview()
return
}
for sub in view.subviews { clearBackgroundRecursively(in: sub) }
}
private func clearBackgroundUpwards(from view: NSView) {
var current: NSView? = view
while let node = current {
clearBackgroundRecursively(in: node)
current = node.superview
}
}
private func addBlur(in contentView: NSView) {
let identifier = NSUserInterfaceItemIdentifier("vv_transparent_blur")
if contentView.subviews.contains(where: { $0.identifier == identifier }) { return }
let blurView = NSVisualEffectView()
blurView.identifier = identifier
blurView.blendingMode = .withinWindow
blurView.state = .active
blurView.material = .hudWindow
blurView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(blurView, positioned: .below, relativeTo: nil)
NSLayoutConstraint.activate([
blurView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
blurView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
blurView.topAnchor.constraint(equalTo: contentView.topAnchor),
blurView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
}
public func makeNSView(context: Context) -> WindowConfiguratorView {
WindowConfiguratorView()
}
public func updateNSView(_ nsView: WindowConfiguratorView, context: Context) { }
}
public extension View {
/// MenuBarExtra
/// 使
func menuBarExtraTransparentBackground() -> some View {
self.background(MenuBarExtraTransparencyHelperView())
}
}

View File

@@ -0,0 +1,104 @@
import SwiftUI
extension NSObject {
/// Swap the given named instance method of the given named class with the given
/// named instance method of this class.
/// - Parameters:
/// - method: The name of the instance method whose implementation will be exchanged.
/// - className: The name of the class whose instance method implementation will be exchanged.
/// - newMethod: The name of the instance method on this class which will replace the first given method.
static func exchange(method: String, in className: String, for newMethod: String) {
guard let classRef = objc_getClass(className) as? AnyClass,
let original = class_getInstanceMethod(classRef, Selector((method))),
let replacement = class_getInstanceMethod(self, Selector((newMethod)))
else {
fatalError("Could not exchange method \(method) on class \(className).");
}
method_exchangeImplementations(original, replacement);
}
}
// MARK: - Custom Window Corner Mask Implementation
/// Exchange Flag
///
var __SwiftUIMenuBarExtraPanel___cornerMask__didExchange = false;
/// Custom Corner Radius
///
fileprivate let kWindowCornerRadius: CGFloat = 32;
extension NSObject {
@objc func __SwiftUIMenuBarExtraPanel___cornerMask() -> NSImage? {
let width = kWindowCornerRadius * 2;
let height = kWindowCornerRadius * 2;
let image = NSImage(size: CGSizeMake(width, height));
image.lockFocus();
/// Draw a rounded-rectangle corner mask.
///
NSColor.black.setFill();
NSBezierPath(
roundedRect: CGRectMake(0, 0, width, height),
xRadius: kWindowCornerRadius,
yRadius: kWindowCornerRadius).fill();
image.unlockFocus();
image.capInsets = .init(
top: kWindowCornerRadius,
left: kWindowCornerRadius,
bottom: kWindowCornerRadius,
right: kWindowCornerRadius);
return image;
}
}
// MARK: - Context Window Accessor
public struct MenuBarExtraWindowHelperView: NSViewRepresentable {
public init() {}
public class WindowHelper: NSView {
public override func viewWillDraw() {
if __SwiftUIMenuBarExtraPanel___cornerMask__didExchange { return }
guard
let window: AnyObject = self.window,
let windowClass = window.className
else { return }
NSObject.exchange(
method: "_cornerMask",
in: windowClass,
for: "__SwiftUIMenuBarExtraPanel___cornerMask");
let _ = window.perform(Selector(("_cornerMaskChanged")));
__SwiftUIMenuBarExtraPanel___cornerMask__didExchange = true;
}
}
public func updateNSView(_ nsView: WindowHelper, context: Context) { }
public func makeNSView(context: Context) -> WindowHelper { WindowHelper() }
}
public extension View {
func menuBarExtraWindowCorner() -> some View {
self.background(MenuBarExtraWindowHelperView())
}
}

View File

@@ -0,0 +1,25 @@
import SwiftUI
import Foundation
public extension View {
func noiseEffect(seed: Float, frequency: Float, amplitude: Float) -> some View {
self.modifier(NoiseEffectModifier(seed: seed, frequency: frequency, amplitude: amplitude))
}
}
public struct NoiseEffectModifier: ViewModifier {
var seed: Float
var frequency: Float
var amplitude: Float
public init(seed: Float, frequency: Float, amplitude: Float) {
self.seed = seed
self.frequency = frequency
self.amplitude = amplitude
}
public func body(content: Content) -> some View {
content
.colorEffect(ShaderLibrary.parameterizedNoise(.float(seed), .float(frequency), .float(amplitude)))
}
}

View File

@@ -0,0 +1,100 @@
import SwiftUI
public extension View {
func rippleEffect(at origin: CGPoint, isRunning: Bool, interval: TimeInterval) -> some View {
self.modifier(RippleEffect(at: origin, isRunning: isRunning, interval: interval))
}
}
@MainActor
struct RippleEffect: ViewModifier {
var origin: CGPoint
var isRunning: Bool
var interval: TimeInterval
@State private var tick: Int = 0
init(at origin: CGPoint, isRunning: Bool, interval: TimeInterval) {
self.origin = origin
self.isRunning = isRunning
self.interval = interval
}
func body(content: Content) -> some View {
let origin = origin
let animationDuration = animationDuration
return content
.keyframeAnimator(
initialValue: 0,
trigger: tick
) { view, elapsedTime in
view.modifier(RippleModifier(
origin: origin,
elapsedTime: elapsedTime,
duration: animationDuration
))
} keyframes: { _ in
MoveKeyframe(0)
CubicKeyframe(animationDuration, duration: animationDuration)
}
.task(id: isRunning ? interval : -1.0) {
guard isRunning else { return }
while !Task.isCancelled && isRunning {
try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000))
if isRunning {
tick &+= 1
}
}
}
}
var animationDuration: TimeInterval { 3 }
}
struct RippleModifier: ViewModifier {
var origin: CGPoint
var elapsedTime: TimeInterval
var duration: TimeInterval
var amplitude: Double = 12
var frequency: Double = 15
var decay: Double = 8
var speed: Double = 1200
func body(content: Content) -> some View {
let shader = ShaderLibrary.Ripple(
.float2(origin),
.float(elapsedTime),
.float(amplitude),
.float(frequency),
.float(decay),
.float(speed)
)
let maxSampleOffset = maxSampleOffset
let elapsedTime = elapsedTime
let duration = duration
content.visualEffect { view, _ in
view.layerEffect(
shader,
maxSampleOffset: maxSampleOffset,
isEnabled: 0 < elapsedTime && elapsedTime < duration
)
}
}
var maxSampleOffset: CGSize {
CGSize(width: amplitude, height: amplitude)
}
}
struct NoiseEffect: ViewModifier {
func body(content: Content) -> some View {
content
}
}

View File

@@ -0,0 +1,135 @@
import SwiftUI
import Foundation
public extension View {
func maxFrame(
_ width: Bool = true, _ height: Bool = true, alignment: SwiftUI.Alignment = .center
) -> some View {
Group {
if width, height {
frame(maxWidth: .infinity, maxHeight: .infinity, alignment: alignment)
} else if width {
frame(maxWidth: .infinity, alignment: alignment)
} else if height {
frame(maxHeight: .infinity, alignment: alignment)
} else {
self
}
}
}
func cornerRadiusWithCorners(
_ radius: CGFloat, corners: RectCorner = .allCorners
) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
func linearBorder(
color: Color, cornerRadius: CGFloat, lineWidth: CGFloat = 1, from: UnitPoint = .top,
to: UnitPoint = .center
) -> some View {
overlay(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.inset(by: lineWidth)
.stroke(
LinearGradient(
stops: [
.init(color: color.opacity(0.1), location: 0),
.init(color: color.opacity(0.02), location: 0.5),
.init(color: color.opacity(0.06), location: 1),
], startPoint: from, endPoint: to),
lineWidth: lineWidth
)
)
}
func linearBorder(
stops: [Gradient.Stop], cornerRadius: CGFloat, lineWidth: CGFloat = 1,
from: UnitPoint = .top, to: UnitPoint = .center
) -> some View {
overlay(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.inset(by: lineWidth)
.stroke(
LinearGradient(stops: stops, startPoint: from, endPoint: to),
lineWidth: lineWidth
)
)
}
func overlayBorder(
color: Color,
lineWidth: CGFloat = 1,
insets: CGFloat = 0,
cornerRadius: CGFloat = 0,
hidden: Bool = false
) -> some View {
overlay(
RoundedCorner(radius: cornerRadius, corners: .allCorners)
.fill(color)
.mask(
RoundedCorner(radius: cornerRadius, corners: .allCorners)
.stroke(style: .init(lineWidth: lineWidth))
)
.allowsHitTesting(false)
.padding(insets)
)
}
func extendTapGesture(_ value: CGFloat = 8, _ action: @escaping () -> Void) -> some View {
self
.padding(value)
.contentShape(Rectangle())
.onTapGesture {
action()
}
.padding(-value)
}
}
public struct RectCorner: OptionSet, Sendable {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
public static let topLeft: RectCorner = RectCorner(rawValue: 1 << 0)
public static let topRight: RectCorner = RectCorner(rawValue: 1 << 1)
public static let bottomLeft: RectCorner = RectCorner(rawValue: 1 << 2)
public static let bottomRight: RectCorner = RectCorner(rawValue: 1 << 3)
public static let allCorners: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight]
}
struct RoundedCorner: Shape, InsettableShape {
var radius: CGFloat
var corners: RectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let topLeft = corners.contains(.topLeft) ? radius : 0
let topRight = corners.contains(.topRight) ? radius : 0
let bottomLeft = corners.contains(.bottomLeft) ? radius : 0
let bottomRight = corners.contains(.bottomRight) ? radius : 0
if #available(iOS 17.0, macOS 14.0, *) {
return UnevenRoundedRectangle(
topLeadingRadius: topLeft,
bottomLeadingRadius: bottomLeft,
bottomTrailingRadius: bottomRight,
topTrailingRadius: topRight,
style: .continuous
).path(in: rect)
} else {
if corners == .allCorners {
return RoundedRectangle(cornerRadius: radius, style: .continuous).path(in: rect)
} else {
return Path(rect)
}
}
}
nonisolated func inset(by amount: CGFloat) -> some InsettableShape {
var shape = self
shape.radius -= amount
return shape
}
}

View File

@@ -0,0 +1,5 @@
import Testing
@Test func placeholderCompiles() {
#expect(true)
}