chore: import Local-Cursor v1.1.8 source snapshot
Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage. Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
This commit is contained in:
251
internal/bridge/cli_terminal.go
Normal file
251
internal/bridge/cli_terminal.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
goruntime "runtime"
|
||||
|
||||
"cursor/internal/cursor"
|
||||
"cursor/internal/logger"
|
||||
)
|
||||
|
||||
// CLITerminalResult 返回打开 CLI 终端的结果。
|
||||
type CLITerminalResult struct {
|
||||
Ok bool `json:"ok"`
|
||||
Error string `json:"error"`
|
||||
WrapperDir string `json:"wrapperDir"`
|
||||
}
|
||||
|
||||
// cliTerminalInitFilename 是 Windows 终端初始化批处理脚本的文件名。
|
||||
const cliTerminalInitFilename = "cli-terminal-init.bat"
|
||||
|
||||
// extractEnvSetupLines 从 wrapper 脚本提取环境变量设置行。
|
||||
// wrapper 是 CLI 环境配置的唯一来源,终端初始化脚本从中派生,
|
||||
// 避免两套独立的环境变量配置逻辑产生不一致。
|
||||
func extractEnvSetupLines(wrapperPath string) ([]string, error) {
|
||||
data, err := os.ReadFile(wrapperPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var lines []string
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "set \"") || // Windows: set "KEY=VALUE"
|
||||
strings.HasPrefix(trimmed, "export ") || // Unix: export KEY="VALUE"
|
||||
strings.HasPrefix(trimmed, "unset ") { // Unix: unset KEY
|
||||
lines = append(lines, trimmed)
|
||||
}
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// OpenCLITerminal 检测终端并打开一个预配置 wrapper 环境的终端窗口。
|
||||
//
|
||||
// 行为:
|
||||
// - 检测 CLI 版 cursor-agent 是否已安装
|
||||
// - 检测 wrapper 是否已生成(服务启动时自动生成)
|
||||
// - 以管理员/elevated 权限启动终端
|
||||
// - 终端启动时自动设置 wrapper 所需的环境变量
|
||||
// - 不自动执行 cursor-agent 命令(用户需自行 cd 到项目目录后调用)
|
||||
func (s *WindowService) OpenCLITerminal() (result CLITerminalResult) {
|
||||
logServiceCall("window", "open_cli_terminal")
|
||||
defer func() {
|
||||
if result.Ok {
|
||||
logServiceResult("window", "open_cli_terminal", nil)
|
||||
return
|
||||
}
|
||||
logServiceResult("window", "open_cli_terminal", fmt.Errorf("%s", result.Error))
|
||||
}()
|
||||
|
||||
cliPath := cursor.DetectCLIInstallPath()
|
||||
if cliPath == "" {
|
||||
return CLITerminalResult{
|
||||
Ok: false,
|
||||
Error: "未检测到 CLI 版 cursor-agent,请先安装",
|
||||
}
|
||||
}
|
||||
|
||||
if !cursor.CLIWrapperExists() {
|
||||
return CLITerminalResult{
|
||||
Ok: false,
|
||||
Error: "CLI wrapper 尚未生成,请先启动服务",
|
||||
}
|
||||
}
|
||||
|
||||
wrapperDir := cursor.CLIWrapperDir()
|
||||
|
||||
if err := openElevatedTerminal(wrapperDir); err != nil {
|
||||
logger.Errorf("openCLITerminal failed: %v", err)
|
||||
return CLITerminalResult{
|
||||
Ok: false,
|
||||
Error: err.Error(),
|
||||
WrapperDir: wrapperDir,
|
||||
}
|
||||
}
|
||||
|
||||
return CLITerminalResult{
|
||||
Ok: true,
|
||||
WrapperDir: wrapperDir,
|
||||
}
|
||||
}
|
||||
|
||||
// openElevatedTerminal 以管理员/elevated 权限启动终端,
|
||||
// 并通过 shell 初始化脚本预设 wrapper 环境变量。
|
||||
func openElevatedTerminal(workDir string) error {
|
||||
switch detectOS() {
|
||||
case "windows":
|
||||
return openWindowsTerminal(workDir)
|
||||
case "darwin":
|
||||
return openDarwinTerminal(workDir)
|
||||
default:
|
||||
return openLinuxTerminal(workDir)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Windows ──
|
||||
|
||||
func openWindowsTerminal(workDir string) error {
|
||||
// 生成 batch 文件设置环境变量,避免 PowerShell 内联引号转义问题
|
||||
batchPath, err := writeWindowsEnvBatch(workDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成终端初始化脚本失败: %w", err)
|
||||
}
|
||||
|
||||
// 优先使用 Windows Terminal (wt.exe),回退到 cmd.exe
|
||||
// batch 文件内部已 cd /d 到工作目录,无需 -WorkingDirectory 参数
|
||||
wtPath, wtErr := exec.LookPath("wt.exe")
|
||||
if wtErr == nil {
|
||||
args := []string{
|
||||
"-NoProfile", "-Command",
|
||||
fmt.Sprintf(
|
||||
"Start-Process -FilePath '%s' -ArgumentList @('-d','%s','cmd.exe','/k','%s') -Verb RunAs",
|
||||
wtPath, workDir, batchPath,
|
||||
),
|
||||
}
|
||||
return exec.Command("powershell", args...).Start()
|
||||
}
|
||||
|
||||
// 回退到 cmd.exe,以管理员启动
|
||||
args := []string{
|
||||
"-NoProfile", "-Command",
|
||||
fmt.Sprintf(
|
||||
"Start-Process -FilePath 'cmd.exe' -ArgumentList @('/k','%s') -Verb RunAs",
|
||||
batchPath,
|
||||
),
|
||||
}
|
||||
return exec.Command("powershell", args...).Start()
|
||||
}
|
||||
|
||||
// writeWindowsEnvBatch 生成 batch 文件设置 wrapper 环境变量。
|
||||
// 环境变量配置直接从 wrapper 脚本提取,确保与 wrapper 保持一致。
|
||||
func writeWindowsEnvBatch(workDir string) (string, error) {
|
||||
envLines, err := extractEnvSetupLines(cursor.CLIWrapperPath())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取 wrapper 环境配置失败: %w", err)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("@echo off\n")
|
||||
for _, line := range envLines {
|
||||
b.WriteString(line)
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
fmt.Fprintf(&b, "cd /d \"%s\"\n", workDir)
|
||||
b.WriteString("echo CLI environment initialized. cd to your project and run: cursor-agent\n")
|
||||
b.WriteString("title Local-Cursor CLI Terminal\n")
|
||||
|
||||
batchPath := filepath.Join(cursor.CLIWrapperDir(), cliTerminalInitFilename)
|
||||
if err := os.WriteFile(batchPath, []byte(b.String()), 0o644); err != nil {
|
||||
return "", fmt.Errorf("写入终端初始化脚本失败: %w", err)
|
||||
}
|
||||
return batchPath, nil
|
||||
}
|
||||
|
||||
// ── macOS ──
|
||||
|
||||
func openDarwinTerminal(workDir string) error {
|
||||
envLines, err := extractEnvSetupLines(cursor.CLIWrapperPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 wrapper 环境配置失败: %w", err)
|
||||
}
|
||||
|
||||
parts := []string{fmt.Sprintf("cd '%s'", workDir)}
|
||||
parts = append(parts, envLines...)
|
||||
parts = append(parts, "echo 'CLI environment initialized. cd to your project and run: cursor-agent'")
|
||||
initCmd := strings.Join(parts, " && ")
|
||||
|
||||
// 用 osascript 以管理员权限打开 Terminal 并执行初始化
|
||||
script := fmt.Sprintf(`
|
||||
do shell script "osascript -e 'tell application \"Terminal\" to do script \"%s\"' with administrator privileges" with prompt "Local-Cursor needs to open a terminal with administrator privileges"
|
||||
`, escapeAppleScript(initCmd))
|
||||
|
||||
return exec.Command("osascript", "-e", script).Start()
|
||||
}
|
||||
|
||||
// escapeAppleScript 转义 AppleScript 字符串中的特殊字符。
|
||||
func escapeAppleScript(s string) string {
|
||||
s = strings.ReplaceAll(s, `\`, `\\`)
|
||||
s = strings.ReplaceAll(s, `"`, `\"`)
|
||||
return s
|
||||
}
|
||||
|
||||
// ── Linux ──
|
||||
|
||||
func openLinuxTerminal(workDir string) error {
|
||||
terminals := []string{"gnome-terminal", "konsole", "xterm", "xfce4-terminal", "alacritty", "kitty"}
|
||||
var terminalPath string
|
||||
for _, t := range terminals {
|
||||
if p, err := exec.LookPath(t); err == nil {
|
||||
terminalPath = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if terminalPath == "" {
|
||||
return fmt.Errorf("未检测到可用的终端模拟器")
|
||||
}
|
||||
|
||||
envLines, err := extractEnvSetupLines(cursor.CLIWrapperPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 wrapper 环境配置失败: %w", err)
|
||||
}
|
||||
|
||||
parts := []string{fmt.Sprintf("cd '%s'", workDir)}
|
||||
parts = append(parts, envLines...)
|
||||
parts = append(parts, "echo 'CLI environment initialized. cd to your project and run: cursor-agent'")
|
||||
parts = append(parts, "exec bash")
|
||||
initCmd := strings.Join(parts, " && ")
|
||||
|
||||
// 用 pkexec 以管理员权限启动终端,不同终端的参数不同
|
||||
var cmd *exec.Cmd
|
||||
switch filepath.Base(terminalPath) {
|
||||
case "gnome-terminal":
|
||||
cmd = exec.Command("pkexec", terminalPath, "--", "bash", "-c", initCmd)
|
||||
case "konsole", "xfce4-terminal":
|
||||
cmd = exec.Command("pkexec", terminalPath, "-e", "bash", "-c", initCmd)
|
||||
case "xterm":
|
||||
cmd = exec.Command("pkexec", terminalPath, "-e", "bash", "-c", initCmd)
|
||||
case "alacritty":
|
||||
cmd = exec.Command("pkexec", terminalPath, "-e", "bash", "-c", initCmd)
|
||||
case "kitty":
|
||||
cmd = exec.Command("pkexec", terminalPath, "bash", "-c", initCmd)
|
||||
default:
|
||||
cmd = exec.Command("pkexec", terminalPath, "-e", "bash", "-c", initCmd)
|
||||
}
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// detectOS 返回当前操作系统名称。
|
||||
func detectOS() string {
|
||||
switch goruntime.GOOS {
|
||||
case "windows":
|
||||
return "windows"
|
||||
case "darwin":
|
||||
return "darwin"
|
||||
default:
|
||||
return "linux"
|
||||
}
|
||||
}
|
||||
2
internal/bridge/doc.go
Normal file
2
internal/bridge/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package bridge 负责向 Wails 暴露稳定的桌面桥接服务与 DTO。
|
||||
package bridge
|
||||
24
internal/bridge/logging.go
Normal file
24
internal/bridge/logging.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"cursor/internal/logger"
|
||||
)
|
||||
|
||||
var errServiceNotReady = errors.New("应用未初始化")
|
||||
|
||||
// logServiceCall 记录用户触发或后台调用的服务操作,不记录密钥和配置正文。
|
||||
func logServiceCall(service string, operation string, attrs ...any) {
|
||||
args := append([]any{"service", service, "operation", operation}, attrs...)
|
||||
logger.Info("service call", args...)
|
||||
}
|
||||
|
||||
// logServiceResult 统一记录服务操作结果,失败时保留原始错误。
|
||||
func logServiceResult(service string, operation string, err error) {
|
||||
if err != nil {
|
||||
logger.Error("service call failed", "service", service, "operation", operation, "error", err)
|
||||
return
|
||||
}
|
||||
logger.Info("service call completed", "service", service, "operation", operation)
|
||||
}
|
||||
65
internal/bridge/metrics.go
Normal file
65
internal/bridge/metrics.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"cursor/internal/appdata"
|
||||
serverconfig "cursor/internal/backend/server/config"
|
||||
"cursor/internal/historymetrics"
|
||||
)
|
||||
|
||||
// HomeMetricsSummary 定义首页展示的历史统计摘要。
|
||||
type HomeMetricsSummary struct {
|
||||
ProviderCallsTotal int `json:"providerCallsTotal"`
|
||||
TurnsTotal int `json:"turnsTotal"`
|
||||
ValidTurnsTotal int `json:"validTurnsTotal"`
|
||||
InvalidTurnsTotal int `json:"invalidTurnsTotal"`
|
||||
RequestTokensTotal int64 `json:"requestTokensTotal"`
|
||||
PromptTokensTotal int64 `json:"promptTokensTotal"`
|
||||
CacheReadTokens int64 `json:"cacheReadTokens"`
|
||||
CacheWriteTokens int64 `json:"cacheWriteTokens"`
|
||||
CacheHitRate *float64 `json:"cacheHitRate"`
|
||||
}
|
||||
|
||||
// MetricsService 定义首页统计相关的 Wails service。
|
||||
type MetricsService struct {
|
||||
configStore *serverconfig.Store
|
||||
}
|
||||
|
||||
// NewMetricsService 创建首页统计 service。
|
||||
func NewMetricsService() *MetricsService {
|
||||
return &MetricsService{
|
||||
configStore: serverconfig.NewStore(appdata.ConfigFilePath(), ""),
|
||||
}
|
||||
}
|
||||
|
||||
// GetHomeMetricsSummary 返回首页展示的全量历史统计摘要。
|
||||
// 命中率口径(是否计入缓存写入)由后端按用户配置统一计算。
|
||||
func (service *MetricsService) GetHomeMetricsSummary() (HomeMetricsSummary, error) {
|
||||
if err := appdata.EnsureAssistantHome(); err != nil {
|
||||
return HomeMetricsSummary{}, err
|
||||
}
|
||||
|
||||
includeCacheWriteInHitRate := false
|
||||
if service != nil && service.configStore != nil {
|
||||
if cfg, err := service.configStore.Load(context.Background()); err == nil {
|
||||
includeCacheWriteInHitRate = cfg.HomeMetrics.IncludeCacheWriteInHitRate
|
||||
}
|
||||
}
|
||||
|
||||
summary, err := historymetrics.LoadUsageSummary(appdata.UsageFilePath(), includeCacheWriteInHitRate)
|
||||
if err != nil {
|
||||
return HomeMetricsSummary{}, err
|
||||
}
|
||||
return HomeMetricsSummary{
|
||||
ProviderCallsTotal: summary.ProviderCallsTotal,
|
||||
TurnsTotal: summary.TurnsTotal,
|
||||
ValidTurnsTotal: summary.ValidTurnsTotal,
|
||||
InvalidTurnsTotal: summary.InvalidTurnsTotal,
|
||||
RequestTokensTotal: summary.RequestTokensTotal,
|
||||
PromptTokensTotal: summary.PromptTokensTotal,
|
||||
CacheReadTokens: summary.CacheReadTokens,
|
||||
CacheWriteTokens: summary.CacheWriteTokens,
|
||||
CacheHitRate: summary.CacheHitRate,
|
||||
}, nil
|
||||
}
|
||||
200
internal/bridge/proxy.go
Normal file
200
internal/bridge/proxy.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
serverconfig "cursor/internal/backend/server/config"
|
||||
"cursor/internal/certs"
|
||||
"cursor/internal/client"
|
||||
"cursor/internal/mitm"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Public DTOs remain in package main for Wails service compatibility.
|
||||
// ProxyState 定义了当前模块中的 ProxyState 类型。
|
||||
type ProxyState = client.ProxyState
|
||||
|
||||
// UserConfig 定义了当前模块中的 UserConfig 类型。
|
||||
type UserConfig = client.UserConfig
|
||||
|
||||
// ModelAdapterConfig 定义模型测速使用的模型配置结构。
|
||||
type ModelAdapterConfig = serverconfig.ModelAdapterConfig
|
||||
|
||||
// ModelAdapterTestResult 定义一次模型测速结果。
|
||||
type ModelAdapterTestResult = client.ModelAdapterTestResult
|
||||
|
||||
// CLIStatus 定义 CLI 版 cursor-agent 的代理状态。
|
||||
type CLIStatus = client.CLIStatus
|
||||
|
||||
// ModelAdapterTestResultsPayload 定义测速结果事件载荷。
|
||||
type ModelAdapterTestResultsPayload = client.ModelAdapterTestResultsPayload
|
||||
|
||||
// LicenseActionRequest 定义了当前模块中的 LicenseActionRequest 类型。
|
||||
type LicenseActionRequest = client.LicenseActionRequest
|
||||
|
||||
// LicenseSwitchDeviceRequest 定义了当前模块中的 LicenseSwitchDeviceRequest 类型。
|
||||
type LicenseSwitchDeviceRequest = client.LicenseSwitchDeviceRequest
|
||||
|
||||
// LicenseAPIResult 定义了当前模块中的 LicenseAPIResult 类型。
|
||||
type LicenseAPIResult = client.LicenseAPIResult
|
||||
|
||||
// UsageRecordsRequest 定义了当前模块中的 UsageRecordsRequest 类型。
|
||||
type UsageRecordsRequest = client.UsageRecordsRequest
|
||||
|
||||
// UsageRecord 定义了当前模块中的 UsageRecord 类型。
|
||||
type UsageRecord = client.UsageRecord
|
||||
|
||||
// UsageRecordsData 定义了当前模块中的 UsageRecordsData 类型。
|
||||
type UsageRecordsData = client.UsageRecordsData
|
||||
|
||||
// UsageRecordsResult 定义了当前模块中的 UsageRecordsResult 类型。
|
||||
type UsageRecordsResult = client.UsageRecordsResult
|
||||
|
||||
// RemoteModel 表示从模型服务 /models 端点拉取到的一个模型。
|
||||
type RemoteModel = client.RemoteModel
|
||||
|
||||
// RemoteModelsResult 表示一次模型列表拉取结果。
|
||||
type RemoteModelsResult = client.RemoteModelsResult
|
||||
|
||||
// ProxyService 定义了当前模块中的 ProxyService 类型。
|
||||
type ProxyService struct {
|
||||
// core 表示当前声明中的 core。
|
||||
core *client.ProxyService
|
||||
}
|
||||
|
||||
// NewProxyService 用于处理与 NewProxyService 相关的逻辑。
|
||||
func NewProxyService(proxy *mitm.ProxyServer, certManager *certs.Manager, caCertPEM []byte) *ProxyService {
|
||||
return &ProxyService{core: client.NewProxyService(proxy, certManager, caCertPEM)}
|
||||
}
|
||||
|
||||
// StartProxy 用于处理与 StartProxy 相关的逻辑。
|
||||
func (s *ProxyService) StartProxy() (state ProxyState, err error) {
|
||||
logServiceCall("proxy", "start")
|
||||
defer func() { logServiceResult("proxy", "start", err) }()
|
||||
return s.core.StartProxy()
|
||||
}
|
||||
|
||||
// StopProxy 用于处理与 StopProxy 相关的逻辑。
|
||||
func (s *ProxyService) StopProxy() (state ProxyState, err error) {
|
||||
logServiceCall("proxy", "stop")
|
||||
defer func() { logServiceResult("proxy", "stop", err) }()
|
||||
return s.core.StopProxy()
|
||||
}
|
||||
|
||||
// GetState 用于处理与 GetState 相关的逻辑。
|
||||
func (s *ProxyService) GetState() ProxyState {
|
||||
return s.core.GetState()
|
||||
}
|
||||
|
||||
// ClearLastError 用于处理与 ClearLastError 相关的逻辑。
|
||||
func (s *ProxyService) ClearLastError() ProxyState {
|
||||
logServiceCall("proxy", "clear_last_error")
|
||||
state := s.core.ClearLastError()
|
||||
logServiceResult("proxy", "clear_last_error", nil)
|
||||
return state
|
||||
}
|
||||
|
||||
// SetBaseURL 用于处理与 SetBaseURL 相关的逻辑。
|
||||
func (s *ProxyService) SetBaseURL(baseURL string) (state ProxyState, err error) {
|
||||
logServiceCall("proxy", "set_base_url")
|
||||
defer func() { logServiceResult("proxy", "set_base_url", err) }()
|
||||
return s.core.SetBaseURL(baseURL)
|
||||
}
|
||||
|
||||
// LoadUserConfig 用于处理与 LoadUserConfig 相关的逻辑。
|
||||
func (s *ProxyService) LoadUserConfig() (UserConfig, error) {
|
||||
return s.core.LoadUserConfig()
|
||||
}
|
||||
|
||||
// SaveUserConfig 用于处理与 SaveUserConfig 相关的逻辑。
|
||||
func (s *ProxyService) SaveUserConfig(cfg UserConfig) error {
|
||||
logServiceCall("config", "save_user_config")
|
||||
err := s.core.SaveUserConfig(cfg)
|
||||
logServiceResult("config", "save_user_config", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// TestModelAdapter 用于处理与 TestModelAdapter 相关的逻辑。
|
||||
func (s *ProxyService) TestModelAdapter(adapter ModelAdapterConfig) (result ModelAdapterTestResult, err error) {
|
||||
logServiceCall("models", "test_adapter")
|
||||
defer func() { logServiceResult("models", "test_adapter", err) }()
|
||||
return s.core.TestModelAdapter(adapter)
|
||||
}
|
||||
|
||||
// ListRemoteModels 通过模型服务的 /v1/models 端点拉取可用模型列表。
|
||||
func (s *ProxyService) ListRemoteModels(adapterType string, baseURL string, apiKey string) (result RemoteModelsResult, err error) {
|
||||
logServiceCall("models", "list_remote", "adapter_type", adapterType)
|
||||
defer func() { logServiceResult("models", "list_remote", err) }()
|
||||
return s.core.ListRemoteModels(adapterType, baseURL, apiKey)
|
||||
}
|
||||
|
||||
// GetModelAdapterTestResults 用于处理与 GetModelAdapterTestResults 相关的逻辑。
|
||||
func (s *ProxyService) GetModelAdapterTestResults() []ModelAdapterTestResult {
|
||||
return s.core.GetModelAdapterTestResults()
|
||||
}
|
||||
|
||||
// GetDeviceID 用于处理与 GetDeviceID 相关的逻辑。
|
||||
func (s *ProxyService) GetDeviceID() (deviceID string, err error) {
|
||||
logServiceCall("license", "get_device_id")
|
||||
defer func() { logServiceResult("license", "get_device_id", err) }()
|
||||
return s.core.GetDeviceID()
|
||||
}
|
||||
|
||||
// ActivateLicense 用于处理与 ActivateLicense 相关的逻辑。
|
||||
func (s *ProxyService) ActivateLicense(req LicenseActionRequest) (result LicenseAPIResult, err error) {
|
||||
logServiceCall("license", "activate")
|
||||
defer func() { logServiceResult("license", "activate", err) }()
|
||||
return s.core.ActivateLicense(req)
|
||||
}
|
||||
|
||||
// BindLicenseDevice 用于处理与 BindLicenseDevice 相关的逻辑。
|
||||
func (s *ProxyService) BindLicenseDevice(req LicenseActionRequest) (result LicenseAPIResult, err error) {
|
||||
logServiceCall("license", "bind_device")
|
||||
defer func() { logServiceResult("license", "bind_device", err) }()
|
||||
return s.core.BindLicenseDevice(req)
|
||||
}
|
||||
|
||||
// SwitchLicenseDevice 用于处理与 SwitchLicenseDevice 相关的逻辑。
|
||||
func (s *ProxyService) SwitchLicenseDevice(req LicenseSwitchDeviceRequest) (result LicenseAPIResult, err error) {
|
||||
logServiceCall("license", "switch_device")
|
||||
defer func() { logServiceResult("license", "switch_device", err) }()
|
||||
return s.core.SwitchLicenseDevice(req)
|
||||
}
|
||||
|
||||
// QueryUsageRecords 用于处理与 QueryUsageRecords 相关的逻辑。
|
||||
func (s *ProxyService) QueryUsageRecords(req UsageRecordsRequest) (result UsageRecordsResult, err error) {
|
||||
logServiceCall("license", "query_usage_records")
|
||||
defer func() { logServiceResult("license", "query_usage_records", err) }()
|
||||
return s.core.QueryUsageRecords(req)
|
||||
}
|
||||
|
||||
// ApplyCursorSettings 用于处理与 ApplyCursorSettings 相关的逻辑。
|
||||
func (s *ProxyService) ApplyCursorSettings() error {
|
||||
logServiceCall("cursor", "apply_settings")
|
||||
err := s.core.ApplyCursorSettings()
|
||||
logServiceResult("cursor", "apply_settings", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// ClearCursorSettings 用于处理与 ClearCursorSettings 相关的逻辑。
|
||||
func (s *ProxyService) ClearCursorSettings() error {
|
||||
logServiceCall("cursor", "clear_settings")
|
||||
err := s.core.ClearCursorSettings()
|
||||
logServiceResult("cursor", "clear_settings", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetCLIStatus 返回 CLI 版 cursor-agent 的代理状态。
|
||||
func (s *ProxyService) GetCLIStatus() CLIStatus {
|
||||
return s.core.GetCLIStatus()
|
||||
}
|
||||
|
||||
// ShutdownForQuit 用于处理与 ShutdownForQuit 相关的逻辑。
|
||||
func (s *ProxyService) ShutdownForQuit() {
|
||||
logServiceCall("proxy", "shutdown_for_quit")
|
||||
s.core.ShutdownForQuit()
|
||||
logServiceResult("proxy", "shutdown_for_quit", nil)
|
||||
}
|
||||
|
||||
// IsWindows 用于处理与 IsWindows 相关的逻辑。
|
||||
func (s *ProxyService) IsWindows() bool {
|
||||
return runtime.GOOS == "windows"
|
||||
}
|
||||
69
internal/bridge/skill.go
Normal file
69
internal/bridge/skill.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// skill.go 提供 Skill 管理相关的 Wails service。
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"cursor/internal/cursor"
|
||||
"cursor/internal/skills"
|
||||
)
|
||||
|
||||
// SkillProject 表示一个用户打开过的 Cursor 项目。
|
||||
type SkillProject = cursor.ProjectEntry
|
||||
|
||||
// SkillInfo 表示一个内置 Skill。
|
||||
type SkillInfo = skills.Skill
|
||||
|
||||
// SkillService 定义 Skill 管理相关的 Wails service。
|
||||
type SkillService struct{}
|
||||
|
||||
// NewSkillService 创建 Skill 管理 service。
|
||||
func NewSkillService() *SkillService {
|
||||
return &SkillService{}
|
||||
}
|
||||
|
||||
// ListRecentProjects 返回用户打开过的 Cursor 项目列表。
|
||||
func (s *SkillService) ListRecentProjects() ([]SkillProject, error) {
|
||||
logServiceCall("skills", "list_recent_projects")
|
||||
projects, err := cursor.ListRecentProjects()
|
||||
logServiceResult("skills", "list_recent_projects", err)
|
||||
return projects, err
|
||||
}
|
||||
|
||||
// ListSkills 返回所有内置 Skill。
|
||||
func (s *SkillService) ListSkills() ([]SkillInfo, error) {
|
||||
logServiceCall("skills", "list_builtin")
|
||||
result := skills.List()
|
||||
logServiceResult("skills", "list_builtin", nil)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteProjectRecord 从 Cursor 的项目记录中删除指定项目。
|
||||
func (s *SkillService) DeleteProjectRecord(projectPath string) error {
|
||||
logServiceCall("skills", "delete_project_record", "project", projectPath)
|
||||
err := cursor.DeleteProjectRecord(projectPath)
|
||||
logServiceResult("skills", "delete_project_record", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetProjectSkillStates 返回指定项目内各内置 skill 的安装状态。
|
||||
func (s *SkillService) GetProjectSkillStates(projectPath string) (map[string]bool, error) {
|
||||
logServiceCall("skills", "get_project_states", "project", projectPath)
|
||||
states := make(map[string]bool)
|
||||
for _, skill := range skills.List() {
|
||||
states[skill.Name] = skills.IsInstalled(projectPath, skill.Name)
|
||||
}
|
||||
logServiceResult("skills", "get_project_states", nil)
|
||||
return states, nil
|
||||
}
|
||||
|
||||
// SetProjectSkill 开启时将内置 skill 安装到项目,关闭时移除。
|
||||
func (s *SkillService) SetProjectSkill(projectPath string, skillName string, enabled bool) error {
|
||||
logServiceCall("skills", "set_project_skill", "project", projectPath, "skill", skillName, "enabled", enabled)
|
||||
var err error
|
||||
if enabled {
|
||||
err = skills.Install(projectPath, skillName)
|
||||
} else {
|
||||
err = skills.Uninstall(projectPath, skillName)
|
||||
}
|
||||
logServiceResult("skills", "set_project_skill", err)
|
||||
return err
|
||||
}
|
||||
345
internal/bridge/window.go
Normal file
345
internal/bridge/window.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"cursor/internal/buildinfo"
|
||||
"cursor/internal/client"
|
||||
"cursor/internal/cursor"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
goruntime "runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/leaanthony/u"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
// modelEditorContext 保存当前模型编辑器窗口的初始化上下文。
|
||||
type modelEditorContext struct {
|
||||
Index int `json:"index"`
|
||||
AdapterJSON string `json:"adapterJSON"`
|
||||
}
|
||||
|
||||
// WindowService 定义了当前模块中的 WindowService 类型。
|
||||
type WindowService struct {
|
||||
app *application.App
|
||||
modelConfigWindow *application.WebviewWindow
|
||||
modelEditorWindow *application.WebviewWindow
|
||||
skillWindow *application.WebviewWindow
|
||||
editorCtx *modelEditorContext
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewWindowService 用于处理与 NewWindowService 相关的逻辑。
|
||||
func NewWindowService() *WindowService {
|
||||
return &WindowService{}
|
||||
}
|
||||
|
||||
// SetApp 用于处理与 SetApp 相关的逻辑。
|
||||
func (s *WindowService) SetApp(app *application.App) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.app = app
|
||||
}
|
||||
|
||||
// GetAppVersion 返回当前应用版本号。
|
||||
func (s *WindowService) GetAppVersion() string {
|
||||
return buildinfo.CurrentVersion()
|
||||
}
|
||||
|
||||
// OpenConfigWindow 打开本地设置目录。
|
||||
func (s *WindowService) OpenConfigWindow() {
|
||||
logServiceCall("window", "open_config_directory")
|
||||
if err := os.MkdirAll(client.ResolveSettingsRootPath(), 0o755); err != nil {
|
||||
logServiceResult("window", "open_config_directory", err)
|
||||
return
|
||||
}
|
||||
logServiceResult("window", "open_config_directory", openDirectory(client.ResolveSettingsRootPath()))
|
||||
}
|
||||
|
||||
// OpenModelConfigWindow 打开模型配置独立窗口。如果窗口已存在则聚焦。
|
||||
func (s *WindowService) OpenModelConfigWindow() {
|
||||
logServiceCall("window", "open_model_config")
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.app == nil {
|
||||
logServiceResult("window", "open_model_config", errServiceNotReady)
|
||||
return
|
||||
}
|
||||
|
||||
if s.modelConfigWindow != nil {
|
||||
s.modelConfigWindow.Show()
|
||||
s.modelConfigWindow.Focus()
|
||||
logServiceResult("window", "open_model_config", nil)
|
||||
return
|
||||
}
|
||||
|
||||
win := s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: "模型配置",
|
||||
Width: 980,
|
||||
Height: 700,
|
||||
MinWidth: 820,
|
||||
MinHeight: 560,
|
||||
DisableResize: false,
|
||||
Frameless: goruntime.GOOS == "windows",
|
||||
URL: "/#/model-config",
|
||||
Hidden: false,
|
||||
HideOnEscape: false,
|
||||
MinimiseButtonState: application.ButtonEnabled,
|
||||
MaximiseButtonState: application.ButtonEnabled,
|
||||
CloseButtonState: application.ButtonEnabled,
|
||||
BackgroundColour: application.RGBA{Red: 25, Green: 25, Blue: 25, Alpha: 255},
|
||||
Mac: application.MacWindow{
|
||||
Backdrop: application.MacBackdropLiquidGlass,
|
||||
DisableShadow: false,
|
||||
TitleBar: application.MacTitleBar{
|
||||
AppearsTransparent: true,
|
||||
Hide: false,
|
||||
HideTitle: true,
|
||||
FullSizeContent: true,
|
||||
UseToolbar: false,
|
||||
HideToolbarSeparator: true,
|
||||
},
|
||||
WebviewPreferences: application.MacWebviewPreferences{
|
||||
FullscreenEnabled: u.True,
|
||||
TextInteractionEnabled: u.True,
|
||||
AllowsBackForwardNavigationGestures: u.False,
|
||||
},
|
||||
},
|
||||
Windows: application.WindowsWindow{
|
||||
HiddenOnTaskbar: false,
|
||||
},
|
||||
})
|
||||
|
||||
win.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.modelConfigWindow = nil
|
||||
})
|
||||
|
||||
s.modelConfigWindow = win
|
||||
logServiceResult("window", "open_model_config", nil)
|
||||
}
|
||||
|
||||
// OpenModelEditorWindow 打开模型编辑器独立窗口。
|
||||
// index < 0 表示新增,>= 0 表示编辑对应索引的适配器。
|
||||
// adapterJSON 为编辑器初始数据的 JSON 字符串。
|
||||
func (s *WindowService) OpenModelEditorWindow(index int, adapterJSON string) {
|
||||
logServiceCall("window", "open_model_editor", "index", index)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.app == nil {
|
||||
logServiceResult("window", "open_model_editor", errServiceNotReady)
|
||||
return
|
||||
}
|
||||
|
||||
s.editorCtx = &modelEditorContext{
|
||||
Index: index,
|
||||
AdapterJSON: adapterJSON,
|
||||
}
|
||||
|
||||
if s.modelEditorWindow != nil {
|
||||
s.modelEditorWindow.Show()
|
||||
s.modelEditorWindow.Focus()
|
||||
logServiceResult("window", "open_model_editor", nil)
|
||||
return
|
||||
}
|
||||
|
||||
title := "新增模型配置"
|
||||
if index >= 0 {
|
||||
title = "编辑模型配置"
|
||||
}
|
||||
|
||||
win := s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: title,
|
||||
Width: 840,
|
||||
Height: 680,
|
||||
MinWidth: 740,
|
||||
MinHeight: 600,
|
||||
DisableResize: false,
|
||||
Frameless: goruntime.GOOS == "windows",
|
||||
URL: fmt.Sprintf("/#/model-editor?index=%d", index),
|
||||
Hidden: false,
|
||||
HideOnEscape: false,
|
||||
MinimiseButtonState: application.ButtonEnabled,
|
||||
MaximiseButtonState: application.ButtonHidden,
|
||||
CloseButtonState: application.ButtonEnabled,
|
||||
BackgroundColour: application.RGBA{Red: 25, Green: 25, Blue: 25, Alpha: 255},
|
||||
Mac: application.MacWindow{
|
||||
Backdrop: application.MacBackdropLiquidGlass,
|
||||
DisableShadow: false,
|
||||
TitleBar: application.MacTitleBar{
|
||||
AppearsTransparent: true,
|
||||
Hide: false,
|
||||
HideTitle: true,
|
||||
FullSizeContent: true,
|
||||
UseToolbar: false,
|
||||
HideToolbarSeparator: true,
|
||||
},
|
||||
WebviewPreferences: application.MacWebviewPreferences{
|
||||
FullscreenEnabled: u.False,
|
||||
TextInteractionEnabled: u.True,
|
||||
AllowsBackForwardNavigationGestures: u.False,
|
||||
},
|
||||
},
|
||||
Windows: application.WindowsWindow{
|
||||
HiddenOnTaskbar: false,
|
||||
},
|
||||
})
|
||||
|
||||
win.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.modelEditorWindow = nil
|
||||
s.editorCtx = nil
|
||||
})
|
||||
|
||||
s.modelEditorWindow = win
|
||||
logServiceResult("window", "open_model_editor", nil)
|
||||
}
|
||||
|
||||
// GetModelEditorContext 返回当前编辑器窗口的初始化上下文。
|
||||
func (s *WindowService) GetModelEditorContext() map[string]any {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if s.editorCtx == nil {
|
||||
return map[string]any{
|
||||
"index": -1,
|
||||
"adapterJSON": "{}",
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"index": s.editorCtx.Index,
|
||||
"adapterJSON": s.editorCtx.AdapterJSON,
|
||||
}
|
||||
}
|
||||
|
||||
// CursorStatus 描述 Cursor IDE 的连接状态。
|
||||
type CursorStatus struct {
|
||||
InstallFound bool `json:"installFound"`
|
||||
InstallPath string `json:"installPath"`
|
||||
ProcessRunning bool `json:"processRunning"`
|
||||
}
|
||||
|
||||
// GetCursorStatus 返回 Cursor 安装目录探测结果与进程运行状态。
|
||||
func (s *WindowService) GetCursorStatus() CursorStatus {
|
||||
path := cursor.DetectCursorInstallPath()
|
||||
return CursorStatus{
|
||||
InstallFound: path != "",
|
||||
InstallPath: path,
|
||||
ProcessRunning: cursor.IsCursorRunning(),
|
||||
}
|
||||
}
|
||||
|
||||
// LocalizeCursor 关闭 Cursor 进程并运行社区汉化包。
|
||||
// installDir 为空时自动检测 Cursor 安装目录。
|
||||
func (s *WindowService) LocalizeCursor(installDir string) (string, error) {
|
||||
logServiceCall("cursor", "localize")
|
||||
output, err := cursor.LocalizeCursor(installDir)
|
||||
logServiceResult("cursor", "localize", err)
|
||||
return output, err
|
||||
}
|
||||
|
||||
// OpenCursor 启动 Cursor IDE(installDir 为空时自动检测)。
|
||||
func (s *WindowService) OpenCursor(installDir string) error {
|
||||
logServiceCall("cursor", "open", "install_dir", installDir)
|
||||
err := cursor.OpenCursor(installDir)
|
||||
logServiceResult("cursor", "open", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// SelectDirectory 弹出系统目录选择对话框,返回所选目录;取消时返回空字符串。
|
||||
func (s *WindowService) SelectDirectory() (string, error) {
|
||||
logServiceCall("window", "select_cursor_directory")
|
||||
if s == nil || s.app == nil {
|
||||
err := errServiceNotReady
|
||||
logServiceResult("window", "select_cursor_directory", err)
|
||||
return "", err
|
||||
}
|
||||
path, err := s.app.Dialog.OpenFile().
|
||||
CanChooseDirectories(true).
|
||||
CanChooseFiles(false).
|
||||
SetTitle("选择 Cursor 安装目录").
|
||||
PromptForSingleSelection()
|
||||
logServiceResult("window", "select_cursor_directory", err)
|
||||
return path, err
|
||||
}
|
||||
|
||||
// OpenSkillWindow 打开 Skill 管理子窗口。如果窗口已存在则聚焦。
|
||||
func (s *WindowService) OpenSkillWindow() {
|
||||
logServiceCall("window", "open_skills")
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.app == nil {
|
||||
logServiceResult("window", "open_skills", errServiceNotReady)
|
||||
return
|
||||
}
|
||||
|
||||
if s.skillWindow != nil {
|
||||
s.skillWindow.Show()
|
||||
s.skillWindow.Focus()
|
||||
logServiceResult("window", "open_skills", nil)
|
||||
return
|
||||
}
|
||||
|
||||
win := s.app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: "Skill 管理",
|
||||
Width: 720,
|
||||
Height: 640,
|
||||
MinWidth: 620,
|
||||
MinHeight: 520,
|
||||
DisableResize: false,
|
||||
Frameless: goruntime.GOOS == "windows",
|
||||
URL: "/#/skills",
|
||||
Hidden: false,
|
||||
HideOnEscape: false,
|
||||
MinimiseButtonState: application.ButtonEnabled,
|
||||
MaximiseButtonState: application.ButtonEnabled,
|
||||
CloseButtonState: application.ButtonEnabled,
|
||||
BackgroundColour: application.RGBA{Red: 25, Green: 25, Blue: 25, Alpha: 255},
|
||||
Windows: application.WindowsWindow{
|
||||
HiddenOnTaskbar: false,
|
||||
},
|
||||
})
|
||||
|
||||
win.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.skillWindow = nil
|
||||
})
|
||||
|
||||
s.skillWindow = win
|
||||
logServiceResult("window", "open_skills", nil)
|
||||
}
|
||||
|
||||
// OpenHistoryWindow 打开日志目录。
|
||||
func (s *WindowService) OpenHistoryWindow() {
|
||||
logServiceCall("window", "open_logs_directory")
|
||||
if err := os.MkdirAll(client.ResolveLogsRootPath(), 0o755); err != nil {
|
||||
logServiceResult("window", "open_logs_directory", err)
|
||||
return
|
||||
}
|
||||
logServiceResult("window", "open_logs_directory", openDirectory(client.ResolveLogsRootPath()))
|
||||
}
|
||||
|
||||
// openDirectory 使用系统默认文件管理器打开目录并返回启动错误。
|
||||
func openDirectory(path string) error {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
switch goruntime.GOOS {
|
||||
case "darwin":
|
||||
err = exec.Command("open", path).Start()
|
||||
case "windows":
|
||||
err = exec.Command("explorer", path).Start()
|
||||
default:
|
||||
err = exec.Command("xdg-open", path).Start()
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user