2026-11-10 01:24:28 +08:00
|
|
|
package cursor
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"runtime"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"cursor/internal/appdata"
|
|
|
|
|
"cursor/internal/logger"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// injectedCursorSettingsKeys 表示当前模块中的 injectedCursorSettingsKeys 状态值。
|
|
|
|
|
var injectedCursorSettingsKeys = []string{
|
|
|
|
|
"http.proxy",
|
|
|
|
|
"http.proxyKerberosServicePrincipal",
|
|
|
|
|
"http.proxySupport",
|
|
|
|
|
"cursor.general.disableHttp2",
|
|
|
|
|
"http.experimental.systemCertificatesV2",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// EnsureCACertFile 用于处理与 EnsureCACertFile 相关的逻辑。
|
|
|
|
|
func EnsureCACertFile(certPEM []byte, currentPath string) (string, error) {
|
|
|
|
|
certPath := appdata.CACertFilePath()
|
|
|
|
|
|
|
|
|
|
if err := os.MkdirAll(filepath.Dir(certPath), 0o755); err != nil {
|
|
|
|
|
return "", fmt.Errorf("创建证书配置目录失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if existing, err := os.ReadFile(certPath); err == nil && bytes.Equal(existing, certPEM) {
|
|
|
|
|
logger.Infof("ensureCACertFile: unchanged path=%s", certPath)
|
|
|
|
|
return certPath, nil
|
|
|
|
|
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
|
|
|
return "", fmt.Errorf("读取内置 CA 证书失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := os.WriteFile(certPath, certPEM, 0o644); err != nil {
|
|
|
|
|
return "", fmt.Errorf("写入内置 CA 证书失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
sum := sha256.Sum256(certPEM)
|
|
|
|
|
logger.Infof(
|
|
|
|
|
"ensureCACertFile: wrote path=%s sha256=%s size=%d",
|
|
|
|
|
certPath,
|
|
|
|
|
strings.ToUpper(hex.EncodeToString(sum[:])),
|
|
|
|
|
len(certPEM),
|
|
|
|
|
)
|
|
|
|
|
return certPath, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func samePath(left string, right string) bool {
|
|
|
|
|
if strings.TrimSpace(left) == "" || strings.TrimSpace(right) == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-11-10 09:59:34 +08:00
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
return strings.EqualFold(filepath.Clean(left), filepath.Clean(right))
|
|
|
|
|
}
|
2026-11-10 01:24:28 +08:00
|
|
|
return filepath.Clean(left) == filepath.Clean(right)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetSystemNodeExtraCACerts 用于处理与 SetSystemNodeExtraCACerts 相关的逻辑。
|
|
|
|
|
func SetSystemNodeExtraCACerts(caCertPath string) error {
|
|
|
|
|
caCertPath = strings.TrimSpace(caCertPath)
|
|
|
|
|
if caCertPath == "" {
|
|
|
|
|
return errors.New("CA 证书路径为空")
|
|
|
|
|
}
|
|
|
|
|
if err := os.Setenv("NODE_EXTRA_CA_CERTS", caCertPath); err != nil {
|
|
|
|
|
return fmt.Errorf("设置进程环境变量失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch runtime.GOOS {
|
|
|
|
|
case "darwin":
|
|
|
|
|
out, err := exec.Command("launchctl", "setenv", "NODE_EXTRA_CA_CERTS", caCertPath).CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("写入 macOS 用户环境变量失败: %v: %s", err, strings.TrimSpace(string(out)))
|
|
|
|
|
}
|
|
|
|
|
case "linux":
|
|
|
|
|
// Linux 发行版环境变量持久化方式差异较大,这里先确保当前进程生效。
|
|
|
|
|
logger.Infof("setSystemNodeExtraCACerts: linux detected, applied to current process only")
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("不支持的系统: %s", runtime.GOOS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Infof("setSystemNodeExtraCACerts: NODE_EXTRA_CA_CERTS=%s", caCertPath)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ClearSystemNodeExtraCACerts 用于处理与 ClearSystemNodeExtraCACerts 相关的逻辑。
|
|
|
|
|
func ClearSystemNodeExtraCACerts() error {
|
|
|
|
|
if err := os.Unsetenv("NODE_EXTRA_CA_CERTS"); err != nil {
|
|
|
|
|
return fmt.Errorf("清理进程环境变量失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch runtime.GOOS {
|
|
|
|
|
case "darwin":
|
|
|
|
|
out, err := exec.Command("launchctl", "unsetenv", "NODE_EXTRA_CA_CERTS").CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("清理 macOS 用户环境变量失败: %v: %s", err, strings.TrimSpace(string(out)))
|
|
|
|
|
}
|
|
|
|
|
case "linux":
|
|
|
|
|
logger.Infof("clearSystemNodeExtraCACerts: linux detected, cleared in current process only")
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("不支持的系统: %s", runtime.GOOS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.Infof("clearSystemNodeExtraCACerts: NODE_EXTRA_CA_CERTS cleared")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resolveCursorSettingsPath 用于处理与 resolveCursorSettingsPath 相关的逻辑。
|
|
|
|
|
func resolveCursorSettingsPath() (string, error) {
|
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("获取用户目录失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch runtime.GOOS {
|
|
|
|
|
case "darwin":
|
|
|
|
|
return filepath.Join(homeDir, "Library", "Application Support", "Cursor", "User", "settings.json"), nil
|
|
|
|
|
case "windows":
|
|
|
|
|
appData := os.Getenv("APPDATA")
|
|
|
|
|
if strings.TrimSpace(appData) == "" {
|
|
|
|
|
appData = filepath.Join(homeDir, "AppData", "Roaming")
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(appData, "Cursor", "User", "settings.json"), nil
|
|
|
|
|
case "linux":
|
|
|
|
|
configDir := os.Getenv("XDG_CONFIG_HOME")
|
|
|
|
|
if strings.TrimSpace(configDir) == "" {
|
|
|
|
|
configDir = filepath.Join(homeDir, ".config")
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(configDir, "Cursor", "User", "settings.json"), nil
|
|
|
|
|
default:
|
|
|
|
|
return "", fmt.Errorf("不支持的系统: %s", runtime.GOOS)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// decodeCursorSettingsJSONC 用于处理与 decodeCursorSettingsJSONC 相关的逻辑。
|
|
|
|
|
func decodeCursorSettingsJSONC(data []byte) (map[string]any, error) {
|
|
|
|
|
result := make(map[string]any)
|
|
|
|
|
normalized, err := normalizeJSONC(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
normalized = bytes.TrimSpace(normalized)
|
|
|
|
|
if len(normalized) == 0 {
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(normalized, &result); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// normalizeJSONC 用于处理与 normalizeJSONC 相关的逻辑。
|
|
|
|
|
func normalizeJSONC(data []byte) ([]byte, error) {
|
|
|
|
|
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
|
|
|
|
|
data = data[3:]
|
|
|
|
|
}
|
|
|
|
|
withoutComments, err := stripJSONCComments(data)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return stripJSONCTrailingCommas(withoutComments), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// stripJSONCComments 用于处理与 stripJSONCComments 相关的逻辑。
|
|
|
|
|
func stripJSONCComments(data []byte) ([]byte, error) {
|
|
|
|
|
out := make([]byte, 0, len(data))
|
|
|
|
|
inString := false
|
|
|
|
|
inLineComment := false
|
|
|
|
|
inBlockComment := false
|
|
|
|
|
escaped := false
|
|
|
|
|
|
|
|
|
|
for i := 0; i < len(data); i++ {
|
|
|
|
|
ch := data[i]
|
|
|
|
|
|
|
|
|
|
if inLineComment {
|
|
|
|
|
if ch == '\n' {
|
|
|
|
|
inLineComment = false
|
|
|
|
|
out = append(out, ch)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if inBlockComment {
|
|
|
|
|
if ch == '*' && i+1 < len(data) && data[i+1] == '/' {
|
|
|
|
|
inBlockComment = false
|
|
|
|
|
i++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if ch == '\n' {
|
|
|
|
|
out = append(out, ch)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if inString {
|
|
|
|
|
out = append(out, ch)
|
|
|
|
|
if escaped {
|
|
|
|
|
escaped = false
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if ch == '\\' {
|
|
|
|
|
escaped = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if ch == '"' {
|
|
|
|
|
inString = false
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ch == '"' {
|
|
|
|
|
inString = true
|
|
|
|
|
out = append(out, ch)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if ch == '/' && i+1 < len(data) {
|
|
|
|
|
next := data[i+1]
|
|
|
|
|
if next == '/' {
|
|
|
|
|
inLineComment = true
|
|
|
|
|
i++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if next == '*' {
|
|
|
|
|
inBlockComment = true
|
|
|
|
|
i++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out = append(out, ch)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if inBlockComment {
|
|
|
|
|
return nil, errors.New("JSONC 块注释未闭合")
|
|
|
|
|
}
|
|
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// stripJSONCTrailingCommas 用于处理与 stripJSONCTrailingCommas 相关的逻辑。
|
|
|
|
|
func stripJSONCTrailingCommas(data []byte) []byte {
|
|
|
|
|
out := make([]byte, 0, len(data))
|
|
|
|
|
inString := false
|
|
|
|
|
escaped := false
|
|
|
|
|
|
|
|
|
|
for i := 0; i < len(data); i++ {
|
|
|
|
|
ch := data[i]
|
|
|
|
|
if inString {
|
|
|
|
|
out = append(out, ch)
|
|
|
|
|
if escaped {
|
|
|
|
|
escaped = false
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if ch == '\\' {
|
|
|
|
|
escaped = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if ch == '"' {
|
|
|
|
|
inString = false
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ch == '"' {
|
|
|
|
|
inString = true
|
|
|
|
|
out = append(out, ch)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ch == ',' {
|
|
|
|
|
j := i + 1
|
|
|
|
|
for j < len(data) && isJSONWhitespace(data[j]) {
|
|
|
|
|
j++
|
|
|
|
|
}
|
|
|
|
|
if j < len(data) && (data[j] == '}' || data[j] == ']') {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out = append(out, ch)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isJSONWhitespace 用于处理与 isJSONWhitespace 相关的逻辑。
|
|
|
|
|
func isJSONWhitespace(ch byte) bool {
|
|
|
|
|
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ProxyURLFromListenAddr 用于处理与 ProxyURLFromListenAddr 相关的逻辑。
|
|
|
|
|
func ProxyURLFromListenAddr(listenAddr string) string {
|
|
|
|
|
addr := strings.TrimSpace(listenAddr)
|
|
|
|
|
if addr == "" {
|
|
|
|
|
return "http://127.0.0.1:8080"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// :8189 -> 127.0.0.1:8189
|
|
|
|
|
if strings.HasPrefix(addr, ":") {
|
|
|
|
|
return "http://127.0.0.1" + addr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
host, port, err := net.SplitHostPort(addr)
|
|
|
|
|
if err == nil {
|
|
|
|
|
if host == "" || host == "0.0.0.0" || host == "::" || host == "[::]" {
|
|
|
|
|
host = "127.0.0.1"
|
|
|
|
|
}
|
|
|
|
|
return "http://" + net.JoinHostPort(host, port)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "http://" + addr
|
|
|
|
|
}
|