346 lines
10 KiB
Go
346 lines
10 KiB
Go
|
|
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
|
|||
|
|
}
|