Files
cursor-byok/internal/cursor/state_db.go
T
杨超 e9dc2e098d fix(network): 修复长耗时请求被误报「Network disconnected」中断的问题
客户端扩展在慢请求(如提交信息生成)开始约 10 秒后会探测
NetworkService/IsConnected,非 200 应答即判定断网并中止进行中的请求。
此前该探测落入通配路由返回 404,导致模型仍在正常返回时界面先报断连。

- host.go: 注册 IsConnected 精确路由,恒定以 200 + 空 IsConnectedResponse 应答
- client.go: newProtoMessage 补齐 aiserver.v1.IsConnectedResponse 类型,
  避免 mock 编码失败导致探测返回 502
- state_db.go: 新增强开 statsig gate 机制(cursorStateEnabledStatsigGates),
  强制启用 disable_network_change_monitor_local 从源头关闭该探测;
  disableCursorStatsigGates 泛化为 syncCursorStatsigGateOverrides
2026-08-14 12:19:47 +08:00

238 lines
6.9 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",
}
// cursorStateEnabledStatsigGates forces gates to true. Disabling the network
// change monitor stops the always-local extension from probing
// NetworkService/IsConnected and aborting slow in-flight requests (such as
// commit message generation) with "Network disconnected".
var cursorStateEnabledStatsigGates = []string{
"disable_network_change_monitor_local",
}
// 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 enabled_statsig_gates=%s",
stateDBPath,
values["cursorAuth/cachedEmail"],
values["cursorAuth/stripeMembershipType"],
values["cursorAuth/stripeSubscriptionStatus"],
strings.Join(cursorStateDisabledStatsigGates, ","),
strings.Join(cursorStateEnabledStatsigGates, ","),
)
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 := syncCursorStatsigGateOverrides(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
committed = true
return nil
}
func syncCursorStatsigGateOverrides(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 {
setCursorStatsigGate(featureGates, gate, false, "local_disabled")
if strings.EqualFold(hashUsed, "djb2") {
setCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate), false, "local_disabled")
}
}
for _, gate := range cursorStateEnabledStatsigGates {
setCursorStatsigGate(featureGates, gate, true, "local_enabled")
if strings.EqualFold(hashUsed, "djb2") {
setCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate), true, "local_enabled")
}
}
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 setCursorStatsigGate(featureGates map[string]any, key string, value bool, rule string) {
gate, _ := featureGates[key].(map[string]any)
if gate == nil {
gate = map[string]any{
"name": key,
"rule_id": rule,
"ruleID": rule,
"group_name": rule,
"groupName": rule,
"id_type": "userID",
"idType": "userID",
}
featureGates[key] = gate
}
gate["value"] = value
}
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)
}
}