mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 11:37:20 +08:00
- Deleted the CursorAccountCard.vue component, which handled user account login and status. - Removed associated localization entries from catalog.json and various language files. - Updated Home.vue to eliminate references to the removed component. - Refactored clientApi.js to remove unused account-related API functions.
278 lines
7.6 KiB
Go
278 lines
7.6 KiB
Go
package cursor
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
|
|
"cursor/internal/logger"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
const (
|
|
cursorStateMembershipType = "ultra"
|
|
cursorStateSubscriptionStatus = "active"
|
|
cursorStateDefaultSignUpType = "Google"
|
|
cursorStateSQLiteBusyTimeoutMS = 2000
|
|
cursorStateDBRelativePath = "Cursor/User/globalStorage/state.vscdb"
|
|
cursorStateDarwinRelativePath = "Library/Application Support/Cursor/User/globalStorage/state.vscdb"
|
|
cursorStateLinuxRelativePath = ".config/Cursor/User/globalStorage/state.vscdb"
|
|
cursorStateStatsigBootstrapKey = "workbench.experiments.statsigBootstrap"
|
|
)
|
|
|
|
var cursorStateDisabledStatsigGates = []string{
|
|
"decompose_always_local_ext_host",
|
|
"cursor_extensions_isolation_v2",
|
|
"disable_terminal_output_ui_streaming",
|
|
}
|
|
|
|
// InjectCursorUserInfo synchronizes the Cursor user-level auth cache used by the
|
|
// Settings page. It does not modify the installed Cursor app bundle.
|
|
func InjectCursorUserInfo(email, token string) error {
|
|
stateDBPath, err := resolveCursorStateDBPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(stateDBPath), 0o755); err != nil {
|
|
return fmt.Errorf("创建 Cursor 状态目录失败: %w", err)
|
|
}
|
|
|
|
values := buildCursorAuthStateValues(email, token)
|
|
if err := syncCursorAuthStateDB(stateDBPath, values); err != nil {
|
|
return fmt.Errorf("同步 Cursor 状态库失败 path=%s: %w", stateDBPath, err)
|
|
}
|
|
|
|
logger.Infof(
|
|
"injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s disabled_statsig_gates=%s",
|
|
stateDBPath,
|
|
values["cursorAuth/cachedEmail"],
|
|
values["cursorAuth/stripeMembershipType"],
|
|
values["cursorAuth/stripeSubscriptionStatus"],
|
|
strings.Join(cursorStateDisabledStatsigGates, ","),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// DisableCursorStatsigGates preserves the local-mode feature gates without
|
|
// injecting or replacing Cursor account state.
|
|
func DisableCursorStatsigGates() error {
|
|
stateDBPath, err := resolveCursorStateDBPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(stateDBPath), 0o755); err != nil {
|
|
return fmt.Errorf("创建 Cursor 状态目录失败: %w", err)
|
|
}
|
|
if err := disableCursorStatsigGatesInDB(stateDBPath); err != nil {
|
|
return fmt.Errorf("同步 Cursor Statsig gates 失败 path=%s: %w", stateDBPath, err)
|
|
}
|
|
logger.Infof("disableCursorStatsigGates synced path=%s gates=%s", stateDBPath, strings.Join(cursorStateDisabledStatsigGates, ","))
|
|
return nil
|
|
}
|
|
|
|
func buildCursorAuthStateValues(email, token string) map[string]string {
|
|
email = strings.TrimSpace(email)
|
|
token = strings.TrimSpace(token)
|
|
|
|
return map[string]string{
|
|
"cursorAuth/accessToken": token,
|
|
"cursorAuth/cachedEmail": email,
|
|
"cursorAuth/cachedSignUpType": cursorStateDefaultSignUpType,
|
|
"cursorAuth/refreshToken": token,
|
|
"cursorAuth/stripeMembershipType": cursorStateMembershipType,
|
|
"cursorAuth/stripeSubscriptionStatus": cursorStateSubscriptionStatus,
|
|
}
|
|
}
|
|
|
|
func syncCursorAuthStateDB(path string, values map[string]string) error {
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
db.SetMaxOpenConns(1)
|
|
db.SetMaxIdleConns(1)
|
|
|
|
ctx := context.Background()
|
|
if _, err := db.ExecContext(ctx, fmt.Sprintf("PRAGMA busy_timeout = %d", cursorStateSQLiteBusyTimeoutMS)); err != nil {
|
|
return err
|
|
}
|
|
if _, err := db.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)"); err != nil {
|
|
return err
|
|
}
|
|
|
|
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
committed := false
|
|
defer func() {
|
|
if !committed {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
stmt, err := tx.PrepareContext(ctx, "INSERT OR REPLACE INTO ItemTable(key, value) VALUES(?, ?)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, key := range keys {
|
|
if _, err := stmt.ExecContext(ctx, key, values[key]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := disableCursorStatsigGates(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
committed = true
|
|
return nil
|
|
}
|
|
|
|
func disableCursorStatsigGatesInDB(path string) error {
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
db.SetMaxOpenConns(1)
|
|
db.SetMaxIdleConns(1)
|
|
|
|
ctx := context.Background()
|
|
if _, err := db.ExecContext(ctx, fmt.Sprintf("PRAGMA busy_timeout = %d", cursorStateSQLiteBusyTimeoutMS)); err != nil {
|
|
return err
|
|
}
|
|
if _, err := db.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)"); err != nil {
|
|
return err
|
|
}
|
|
|
|
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
committed := false
|
|
defer func() {
|
|
if !committed {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
if err := disableCursorStatsigGates(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
committed = true
|
|
return nil
|
|
}
|
|
|
|
func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error {
|
|
var raw []byte
|
|
err := tx.QueryRowContext(ctx, "SELECT value FROM ItemTable WHERE key = ?", cursorStateStatsigBootstrapKey).Scan(&raw)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
return fmt.Errorf("解析 Cursor Statsig bootstrap 失败: %w", err)
|
|
}
|
|
|
|
featureGates, _ := payload["feature_gates"].(map[string]any)
|
|
if featureGates == nil {
|
|
featureGates = map[string]any{}
|
|
payload["feature_gates"] = featureGates
|
|
}
|
|
|
|
hashUsed, _ := payload["hash_used"].(string)
|
|
for _, gate := range cursorStateDisabledStatsigGates {
|
|
disableCursorStatsigGate(featureGates, gate)
|
|
if strings.EqualFold(hashUsed, "djb2") {
|
|
disableCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate))
|
|
}
|
|
}
|
|
|
|
updated, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("编码 Cursor Statsig bootstrap 失败: %w", err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, "UPDATE ItemTable SET value = ? WHERE key = ?", updated, cursorStateStatsigBootstrapKey); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func disableCursorStatsigGate(featureGates map[string]any, key string) {
|
|
gate, _ := featureGates[key].(map[string]any)
|
|
if gate == nil {
|
|
gate = map[string]any{
|
|
"name": key,
|
|
"rule_id": "local_disabled",
|
|
"ruleID": "local_disabled",
|
|
"group_name": "local_disabled",
|
|
"groupName": "local_disabled",
|
|
"id_type": "userID",
|
|
"idType": "userID",
|
|
}
|
|
featureGates[key] = gate
|
|
}
|
|
gate["value"] = false
|
|
}
|
|
|
|
func cursorStateDJB2Hash(value string) string {
|
|
var hash uint32
|
|
for _, b := range []byte(value) {
|
|
hash = hash*31 + uint32(b)
|
|
}
|
|
return fmt.Sprintf("%d", hash)
|
|
}
|
|
|
|
func resolveCursorStateDBPath() (string, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("获取用户目录失败: %w", err)
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return filepath.Join(homeDir, filepath.FromSlash(cursorStateDarwinRelativePath)), nil
|
|
case "windows":
|
|
appData := strings.TrimSpace(os.Getenv("APPDATA"))
|
|
if appData == "" {
|
|
appData = filepath.Join(homeDir, "AppData", "Roaming")
|
|
}
|
|
return filepath.Join(appData, "Cursor", "User", "globalStorage", "state.vscdb"), nil
|
|
case "linux":
|
|
configDir := strings.TrimSpace(os.Getenv("XDG_CONFIG_HOME"))
|
|
if configDir == "" {
|
|
return filepath.Join(homeDir, filepath.FromSlash(cursorStateLinuxRelativePath)), nil
|
|
}
|
|
return filepath.Join(configDir, filepath.FromSlash(cursorStateDBRelativePath)), nil
|
|
default:
|
|
return "", fmt.Errorf("不支持的系统: %s", runtime.GOOS)
|
|
}
|
|
}
|