Files
Local-Cursor/internal/app/runner.go
FuDie0915 db73cf5a39 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
2026-11-10 01:24:28 +08:00

272 lines
7.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"io/fs"
goruntime "runtime"
"strings"
"time"
serverconfig "cursor/internal/backend/server/config"
"cursor/internal/buildinfo"
"github.com/leaanthony/u"
bridge "cursor/internal/bridge"
"cursor/internal/certs"
"cursor/internal/cursor"
"cursor/internal/logger"
"cursor/internal/mitm"
"cursor/internal/netproxy"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
const (
appName = "Local-Cursor"
)
// EmbeddedResources 定义了当前模块中的 EmbeddedResources 类型。
type EmbeddedResources struct {
// Assets 表示当前声明中的 Assets。
Assets fs.FS
// AppIcon 表示当前声明中的 AppIcon。
AppIcon []byte
// TrayIcon 表示当前声明中的 TrayIcon。
TrayIcon []byte
}
// init 用于处理与 init 相关的逻辑。
func init() {
application.RegisterEvent[bridge.ProxyState]("proxy:state")
application.RegisterEvent[bridge.UserConfig]("user-config:changed")
application.RegisterEvent[bridge.ModelAdapterTestResultsPayload]("model-adapter-test:updated")
}
// Run 用于处理与 Run 相关的逻辑。
func Run(resources EmbeddedResources) error {
logger.Init()
ensureStartMenuShortcut()
netproxy.InstallDefaultTransport()
embeddedCACertPEM := certs.EmbeddedCACertPEM()
logEmbeddedCAInfo(embeddedCACertPEM)
certManager, err := certs.NewEmbeddedManager()
if err != nil {
return err
}
defaultBackendBaseURL := "http://" + serverconfig.DefaultBackendListenAddr
proxyServer, err := mitm.NewProxyServer(serverconfig.DefaultProxyListenAddr, defaultBackendBaseURL, "", "", certManager)
if err != nil {
return err
}
proxyService := bridge.NewProxyService(proxyServer, certManager, embeddedCACertPEM)
metricsService := bridge.NewMetricsService()
windowService := bridge.NewWindowService()
skillService := bridge.NewSkillService()
app := application.New(application.Options{
Name: appName,
Description: appName,
Services: []application.Service{
application.NewService(proxyService),
application.NewService(metricsService),
application.NewService(windowService),
application.NewService(skillService),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(resources.Assets),
},
Mac: application.MacOptions{
ActivationPolicy: application.ActivationPolicyAccessory,
ApplicationShouldTerminateAfterLastWindowClosed: false,
},
OnShutdown: func() {
proxyService.ShutdownForQuit()
},
SingleInstance: &application.SingleInstanceOptions{
UniqueID: "com.local-curosr.single-instance",
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
logger.Infof("检测到实例请求,已忽略")
// 不激活窗口,避免干扰用户工作
},
},
})
windowService.SetApp(app)
mainWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: appName,
Width: 760,
Height: 660,
MinWidth: 680,
MinHeight: 560,
DisableResize: false,
Frameless: goruntime.GOOS == "windows",
URL: "/",
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.True,
TextInteractionEnabled: u.True,
AllowsBackForwardNavigationGestures: u.False,
},
},
Windows: application.WindowsWindow{
HiddenOnTaskbar: false,
},
})
window := mainWindow
window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
window.Hide()
e.Cancel()
})
showMainWindow := func() {
window.Show().Focus()
}
toggleMainWindow := func() {
if window.IsVisible() {
window.Hide()
return
}
showMainWindow()
}
systray := app.SystemTray.New()
menu := app.Menu.New()
statusItem := menu.Add("状态:未启动").SetEnabled(false)
menu.AddSeparator()
startItem := menu.Add("启动服务")
stopItem := menu.Add("停止服务")
menu.AddSeparator()
menu.Add("显示窗口").OnClick(func(ctx *application.Context) {
showMainWindow()
})
menu.Add("隐藏窗口").OnClick(func(ctx *application.Context) {
window.Hide()
})
menu.AddSeparator()
menu.Add("退出").OnClick(func(ctx *application.Context) {
// 与 Cursor 进程联动:Cursor 未退出时先询问,确认后同步关闭 Cursor。
if cursor.IsCursorRunning() {
confirmed := showQuitConfirmDialog(app)
if !confirmed {
return
}
if err := cursor.KillCursorProcesses(); err != nil {
logger.Errorf("quit: kill cursor processes failed err=%v", err)
}
}
proxyService.ShutdownForQuit()
app.Quit()
})
refreshTray := func() {
state := proxyService.GetState()
if state.Running {
statusItem.SetLabel("状态:运行中")
startItem.SetEnabled(false)
stopItem.SetEnabled(true)
} else {
statusItem.SetLabel("状态:未启动")
startItem.SetEnabled(true)
stopItem.SetEnabled(false)
}
}
app.Event.On("proxy:state", func(event *application.CustomEvent) {
refreshTray()
})
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
logger.Infof("应用版本:v%s", buildinfo.CurrentVersion())
go func() {
logger.Infof("application started, begin auto start service in background")
if _, err := proxyService.StartProxy(); err != nil {
logger.Errorf("自动启动服务失败: %v", err)
} else {
state := proxyService.GetState()
logger.Infof("代理已自动启动: %s", state.ProxyListenAddr)
}
}()
})
startItem.OnClick(func(ctx *application.Context) {
if _, err := proxyService.StartProxy(); err != nil {
logger.Errorf("启动服务失败: %v", err)
}
refreshTray()
})
stopItem.OnClick(func(ctx *application.Context) {
if _, err := proxyService.StopProxy(); err != nil {
logger.Errorf("停止服务失败: %v", err)
}
refreshTray()
})
if len(resources.AppIcon) > 0 {
switch goruntime.GOOS {
case "darwin":
systray.SetTemplateIcon(resources.TrayIcon)
case "windows":
systray.SetIcon(resources.AppIcon)
default:
systray.SetIcon(resources.TrayIcon)
}
}
systray.SetTooltip(appName)
systray.OnClick(toggleMainWindow).SetMenu(menu)
refreshTray()
return app.Run()
}
// logEmbeddedCAInfo 用于处理与 logEmbeddedCAInfo 相关的逻辑。
func logEmbeddedCAInfo(certPEM []byte) {
if len(certPEM) == 0 {
logger.Errorf("embedded CA is empty")
return
}
cert, err := parseEmbeddedCert(certPEM)
if err != nil {
logger.Errorf("parse embedded CA failed: %v", err)
return
}
sum := sha256.Sum256(cert.Raw)
logger.Infof(
"embedded CA loaded: sha256=%s subject=%s valid=%s~%s",
strings.ToUpper(hex.EncodeToString(sum[:])),
cert.Subject.String(),
cert.NotBefore.Format(time.RFC3339),
cert.NotAfter.Format(time.RFC3339),
)
}
// parseEmbeddedCert 用于处理与 parseEmbeddedCert 相关的逻辑。
func parseEmbeddedCert(data []byte) (*x509.Certificate, error) {
if block, _ := pem.Decode(data); block != nil {
return x509.ParseCertificate(block.Bytes)
}
return x509.ParseCertificate(data)
}