feat: add desktop app release packaging
3
desktop/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
build/bin
|
||||
node_modules
|
||||
frontend/dist
|
||||
19
desktop/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# README
|
||||
|
||||
## About
|
||||
|
||||
This is the official Wails Vue-TS template.
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
|
||||
## Live Development
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
|
||||
## Building
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
627
desktop/app.go
Normal file
@@ -0,0 +1,627 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
goruntime "runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"lingma-ipc-proxy/internal/httpapi"
|
||||
"lingma-ipc-proxy/internal/lingmaipc"
|
||||
"lingma-ipc-proxy/internal/service"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
// RequestRecord stores a single HTTP request summary
|
||||
type RequestRecord struct {
|
||||
Time string `json:"time"`
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Duration string `json:"duration"`
|
||||
ReqBody string `json:"reqBody,omitempty"`
|
||||
RespBody string `json:"respBody,omitempty"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
|
||||
mu sync.RWMutex
|
||||
cfg service.Config
|
||||
server *httpapi.Server
|
||||
running bool
|
||||
quitting bool
|
||||
addr string
|
||||
startedAt time.Time
|
||||
quitHint time.Time
|
||||
models []ModelInfo
|
||||
requests []RequestRecord
|
||||
}
|
||||
|
||||
// ModelInfo represents a model returned by /v1/models
|
||||
type ModelInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ProxyStatus represents the current proxy status
|
||||
type ProxyStatus struct {
|
||||
Running bool `json:"running"`
|
||||
Addr string `json:"addr"`
|
||||
Models int `json:"models"`
|
||||
Model string `json:"model,omitempty"`
|
||||
StartedAt string `json:"startedAt,omitempty"`
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
a.cfg = defaultConfig()
|
||||
|
||||
// Auto-save default config on first run so users can find/edit it later
|
||||
if err := a.saveConfig(a.cfg); err != nil {
|
||||
runtime.LogWarningf(a.ctx, "failed to save default config: %v", err)
|
||||
}
|
||||
|
||||
// Auto-start proxy so the app is usable immediately
|
||||
go func() {
|
||||
if err := a.StartProxy(); err != nil {
|
||||
a.emitLog("error", fmt.Sprintf("Auto-start failed: %v. %s", err, transportFallbackHint()))
|
||||
} else {
|
||||
a.emitLog("info", "Proxy auto-started")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// onDomReady is called when the frontend DOM is ready
|
||||
func (a *App) onDomReady(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
}
|
||||
|
||||
// onSecondInstanceLaunch is called when user clicks the dock icon while app is already running.
|
||||
// We show the window so the user can interact with it again.
|
||||
func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||
a.ShowWindow()
|
||||
}
|
||||
|
||||
// beforeClose hides the window by default so the proxy can keep running.
|
||||
// QuitApp sets quitting=true before allowing the process to exit.
|
||||
func (a *App) beforeClose(ctx context.Context) bool {
|
||||
a.mu.Lock()
|
||||
if a.quitting {
|
||||
a.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if !a.quitHint.IsZero() && now.Sub(a.quitHint) <= 2*time.Second {
|
||||
a.mu.Unlock()
|
||||
go a.forceQuit()
|
||||
return true
|
||||
}
|
||||
a.quitHint = now
|
||||
a.mu.Unlock()
|
||||
|
||||
message := "再按一次退出快捷键将停止代理并退出应用"
|
||||
a.emitLog("warn", message)
|
||||
runtime.EventsEmit(a.ctx, "quit:confirm", message)
|
||||
return true
|
||||
}
|
||||
|
||||
// ShowWindow shows the main window
|
||||
func (a *App) ShowWindow() {
|
||||
runtime.Show(a.ctx)
|
||||
runtime.WindowShow(a.ctx)
|
||||
runtime.WindowUnminimise(a.ctx)
|
||||
}
|
||||
|
||||
// HideWindow hides the main window
|
||||
func (a *App) HideWindow() {
|
||||
runtime.Hide(a.ctx)
|
||||
}
|
||||
|
||||
// MinimizeWindow minimises the main window.
|
||||
func (a *App) MinimizeWindow() {
|
||||
runtime.WindowMinimise(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) beginQuit() {
|
||||
go a.forceQuit()
|
||||
}
|
||||
|
||||
// QuitApp fully quits the application
|
||||
func (a *App) QuitApp() {
|
||||
a.beginQuit()
|
||||
}
|
||||
|
||||
// RequestQuitShortcut requires two shortcut presses to avoid accidental exits.
|
||||
func (a *App) RequestQuitShortcut() {
|
||||
now := time.Now()
|
||||
a.mu.Lock()
|
||||
shouldQuit := !a.quitHint.IsZero() && now.Sub(a.quitHint) <= 2*time.Second
|
||||
a.quitHint = now
|
||||
a.mu.Unlock()
|
||||
|
||||
if shouldQuit {
|
||||
go a.forceQuit()
|
||||
return
|
||||
}
|
||||
|
||||
message := "再按一次退出快捷键将停止代理并退出应用"
|
||||
a.emitLog("warn", message)
|
||||
runtime.EventsEmit(a.ctx, "quit:confirm", message)
|
||||
}
|
||||
|
||||
func (a *App) forceQuit() {
|
||||
a.mu.Lock()
|
||||
if a.quitting {
|
||||
a.mu.Unlock()
|
||||
return
|
||||
}
|
||||
a.quitting = true
|
||||
a.mu.Unlock()
|
||||
|
||||
a.emitLog("info", "正在停止代理并退出应用")
|
||||
if err := a.StopProxy(); err != nil {
|
||||
runtime.LogWarningf(a.ctx, "stop proxy before exit failed: %v", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func (a *App) emitLog(level string, message string) {
|
||||
runtime.EventsEmit(a.ctx, "log", map[string]string{
|
||||
"level": level,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// GetStatus returns the current proxy status
|
||||
func (a *App) GetStatus() ProxyStatus {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
startedAt := ""
|
||||
if !a.startedAt.IsZero() {
|
||||
startedAt = a.startedAt.Format(time.RFC3339)
|
||||
}
|
||||
return ProxyStatus{
|
||||
Running: a.running,
|
||||
Addr: a.addr,
|
||||
Models: len(a.models),
|
||||
Model: a.cfg.Model,
|
||||
StartedAt: startedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig returns the current configuration.
|
||||
// Timeout is returned in seconds for frontend convenience.
|
||||
func (a *App) GetConfig() service.Config {
|
||||
a.mu.RLock()
|
||||
cfg := a.cfg
|
||||
a.mu.RUnlock()
|
||||
cfg.Timeout = cfg.Timeout / time.Second
|
||||
return cfg
|
||||
}
|
||||
|
||||
// UpdateConfig updates the configuration, saves to file, and restarts the proxy if running.
|
||||
// Frontend sends Timeout in seconds; we convert to time.Duration.
|
||||
func (a *App) UpdateConfig(cfg service.Config) error {
|
||||
// Convert seconds -> Duration if frontend sent a small value
|
||||
if cfg.Timeout > 0 && cfg.Timeout < time.Second {
|
||||
cfg.Timeout = cfg.Timeout * time.Second
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
wasRunning := a.running
|
||||
a.cfg = cfg
|
||||
a.mu.Unlock()
|
||||
|
||||
if err := a.saveConfig(cfg); err != nil {
|
||||
runtime.LogWarningf(a.ctx, "failed to save config: %v", err)
|
||||
a.emitLog("warn", fmt.Sprintf("Config updated but failed to save: %v", err))
|
||||
} else {
|
||||
a.emitLog("info", "Config saved to file")
|
||||
}
|
||||
|
||||
if wasRunning {
|
||||
if err := a.StopProxy(); err != nil {
|
||||
return fmt.Errorf("stop failed: %w", err)
|
||||
}
|
||||
return a.StartProxy()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) saveConfig(cfg service.Config) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Join(home, ".config", "lingma-ipc-proxy")
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timeoutSec := int(cfg.Timeout.Seconds())
|
||||
fileCfg := map[string]any{
|
||||
"host": cfg.Host,
|
||||
"port": cfg.Port,
|
||||
"transport": string(cfg.Transport),
|
||||
"pipe": cfg.Pipe,
|
||||
"websocket_url": cfg.WebSocketURL,
|
||||
"cwd": cfg.Cwd,
|
||||
"current_file_path": cfg.CurrentFilePath,
|
||||
"mode": cfg.Mode,
|
||||
"model": cfg.Model,
|
||||
"shell_type": cfg.ShellType,
|
||||
"session_mode": string(cfg.SessionMode),
|
||||
"timeout": timeoutSec,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(fileCfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, "config.json")
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
// StartProxy starts the lingma-ipc-proxy HTTP server
|
||||
func (a *App) StartProxy() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.running {
|
||||
return fmt.Errorf("proxy already running")
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", a.cfg.Host, a.cfg.Port)
|
||||
svc := service.New(a.cfg)
|
||||
|
||||
warmupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
if err := svc.Warmup(warmupCtx); err != nil {
|
||||
runtime.LogWarningf(a.ctx, "warmup failed: %v", err)
|
||||
a.emitLog("warn", fmt.Sprintf("Lingma IPC warmup failed: %v. %s", err, transportFallbackHint()))
|
||||
} else {
|
||||
runtime.LogInfo(a.ctx, "Lingma IPC warmup completed")
|
||||
a.emitLog("info", "Lingma IPC warmup completed")
|
||||
}
|
||||
cancel()
|
||||
|
||||
server := httpapi.NewServer(addr, svc)
|
||||
server.OnRequest = func(method, path string, statusCode int, duration time.Duration, reqBody, respBody string) {
|
||||
a.mu.Lock()
|
||||
a.requests = append(a.requests, RequestRecord{
|
||||
Time: time.Now().Format("15:04:05"),
|
||||
Method: method,
|
||||
Path: path,
|
||||
StatusCode: statusCode,
|
||||
Duration: duration.Round(time.Millisecond).String(),
|
||||
ReqBody: reqBody,
|
||||
RespBody: respBody,
|
||||
})
|
||||
if len(a.requests) > 100 {
|
||||
a.requests = a.requests[len(a.requests)-100:]
|
||||
}
|
||||
a.mu.Unlock()
|
||||
runtime.EventsEmit(a.ctx, "requests:updated", a.GetRequests())
|
||||
}
|
||||
|
||||
// Check if the port is available before claiming we're running
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("port %s is already in use: %w", addr, err)
|
||||
}
|
||||
ln.Close()
|
||||
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
runtime.LogErrorf(a.ctx, "server error: %v", err)
|
||||
a.emitLog("error", fmt.Sprintf("Server error: %v", err))
|
||||
a.mu.Lock()
|
||||
a.running = false
|
||||
a.addr = ""
|
||||
a.startedAt = time.Time{}
|
||||
a.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
a.server = server
|
||||
a.addr = addr
|
||||
a.running = true
|
||||
a.startedAt = time.Now()
|
||||
|
||||
msg := fmt.Sprintf("Proxy started on http://%s", addr)
|
||||
runtime.LogInfof(a.ctx, msg)
|
||||
a.emitLog("info", msg)
|
||||
|
||||
// Fetch models in background
|
||||
go a.fetchModels(addr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearLogs is a no-op backend helper (logs are kept in frontend memory)
|
||||
func (a *App) ClearLogs() {}
|
||||
|
||||
// StopProxy stops the proxy server
|
||||
func (a *App) StopProxy() error {
|
||||
a.mu.Lock()
|
||||
if !a.running || a.server == nil {
|
||||
a.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
server := a.server
|
||||
a.server = nil
|
||||
a.running = false
|
||||
a.addr = ""
|
||||
a.startedAt = time.Time{}
|
||||
a.models = nil
|
||||
a.mu.Unlock()
|
||||
|
||||
runtime.EventsEmit(a.ctx, "status:updated", a.GetStatus())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
a.emitLog("warn", fmt.Sprintf("Proxy stop forced after graceful shutdown timeout: %v", err))
|
||||
return err
|
||||
}
|
||||
|
||||
runtime.LogInfo(a.ctx, "proxy stopped")
|
||||
a.emitLog("info", "Proxy stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetModels returns the cached model list
|
||||
func (a *App) GetModels() []ModelInfo {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.models
|
||||
}
|
||||
|
||||
// GetRequests returns recent HTTP request records
|
||||
func (a *App) GetRequests() []RequestRecord {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
out := make([]RequestRecord, len(a.requests))
|
||||
copy(out, a.requests)
|
||||
// reverse so newest first
|
||||
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
|
||||
out[i], out[j] = out[j], out[i]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ClearRequests clears the request history
|
||||
func (a *App) ClearRequests() {
|
||||
a.mu.Lock()
|
||||
a.requests = nil
|
||||
a.mu.Unlock()
|
||||
a.emitLog("info", "Request history cleared")
|
||||
}
|
||||
|
||||
// RefreshModels probes the running proxy for the latest model list.
|
||||
func (a *App) RefreshModels() ([]ModelInfo, error) {
|
||||
a.mu.RLock()
|
||||
running := a.running
|
||||
addr := a.addr
|
||||
a.mu.RUnlock()
|
||||
|
||||
if !running || addr == "" {
|
||||
return nil, fmt.Errorf("proxy is not running")
|
||||
}
|
||||
|
||||
models, err := a.fetchModels(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func (a *App) SelectModel(modelID string) (ProxyStatus, error) {
|
||||
modelID = strings.TrimSpace(modelID)
|
||||
if modelID == "" {
|
||||
return a.GetStatus(), fmt.Errorf("model id is required")
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
found := len(a.models) == 0
|
||||
for _, model := range a.models {
|
||||
if model.ID == modelID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
a.mu.Unlock()
|
||||
return a.GetStatus(), fmt.Errorf("model %q is not in the detected model list", modelID)
|
||||
}
|
||||
a.cfg.Model = modelID
|
||||
cfg := a.cfg
|
||||
server := a.server
|
||||
a.mu.Unlock()
|
||||
|
||||
if server != nil {
|
||||
server.SetDefaultModel(modelID)
|
||||
}
|
||||
if err := a.saveConfig(cfg); err != nil {
|
||||
a.emitLog("warn", fmt.Sprintf("Model switched but config save failed: %v", err))
|
||||
}
|
||||
a.emitLog("info", fmt.Sprintf("已切换默认模型:%s", modelID))
|
||||
return a.GetStatus(), nil
|
||||
}
|
||||
|
||||
func (a *App) fetchModels(addr string) ([]ModelInfo, error) {
|
||||
url := fmt.Sprintf("http://%s/v1/models", addr)
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
runtime.LogWarningf(a.ctx, "fetch models failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
runtime.LogWarningf(a.ctx, "decode models failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
models := make([]ModelInfo, 0, len(result.Data))
|
||||
for _, m := range result.Data {
|
||||
models = append(models, ModelInfo{ID: m.ID, Name: m.Name})
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.models = models
|
||||
a.mu.Unlock()
|
||||
|
||||
runtime.EventsEmit(a.ctx, "models:updated", models)
|
||||
if len(models) > 0 {
|
||||
a.emitLog("info", fmt.Sprintf("Loaded %d models", len(models)))
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func defaultConfig() service.Config {
|
||||
cfg := service.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 8095,
|
||||
Transport: lingmaipc.TransportAuto,
|
||||
Cwd: defaultCwd(),
|
||||
Mode: "agent",
|
||||
ShellType: defaultShellType(),
|
||||
SessionMode: service.SessionModeAuto,
|
||||
Timeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
// Try to load config file from multiple locations
|
||||
configPaths := configSearchPaths()
|
||||
for _, configPath := range configPaths {
|
||||
if info, err := os.Stat(configPath); err == nil && !info.IsDir() {
|
||||
if data, err := os.ReadFile(configPath); err == nil {
|
||||
var fileCfg struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Transport string `json:"transport"`
|
||||
Pipe string `json:"pipe"`
|
||||
WebSocketURL string `json:"websocket_url"`
|
||||
Cwd string `json:"cwd"`
|
||||
CurrentFilePath string `json:"current_file_path"`
|
||||
Mode string `json:"mode"`
|
||||
Model string `json:"model"`
|
||||
ShellType string `json:"shell_type"`
|
||||
SessionMode string `json:"session_mode"`
|
||||
TimeoutSeconds int `json:"timeout"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &fileCfg); err == nil {
|
||||
if fileCfg.Host != "" {
|
||||
cfg.Host = fileCfg.Host
|
||||
}
|
||||
if fileCfg.Port > 0 {
|
||||
cfg.Port = fileCfg.Port
|
||||
}
|
||||
if fileCfg.Transport != "" {
|
||||
if t, err := lingmaipc.ParseTransport(fileCfg.Transport); err == nil {
|
||||
cfg.Transport = t
|
||||
}
|
||||
}
|
||||
if fileCfg.Pipe != "" {
|
||||
cfg.Pipe = fileCfg.Pipe
|
||||
}
|
||||
if fileCfg.WebSocketURL != "" {
|
||||
cfg.WebSocketURL = fileCfg.WebSocketURL
|
||||
}
|
||||
if fileCfg.Cwd != "" {
|
||||
cfg.Cwd = fileCfg.Cwd
|
||||
}
|
||||
if fileCfg.CurrentFilePath != "" {
|
||||
cfg.CurrentFilePath = fileCfg.CurrentFilePath
|
||||
}
|
||||
if fileCfg.Mode != "" {
|
||||
cfg.Mode = fileCfg.Mode
|
||||
}
|
||||
if fileCfg.Model != "" {
|
||||
cfg.Model = fileCfg.Model
|
||||
}
|
||||
if fileCfg.ShellType != "" {
|
||||
cfg.ShellType = fileCfg.ShellType
|
||||
}
|
||||
if fileCfg.SessionMode != "" {
|
||||
cfg.SessionMode = service.SessionMode(fileCfg.SessionMode)
|
||||
}
|
||||
if fileCfg.TimeoutSeconds > 0 {
|
||||
cfg.Timeout = time.Duration(fileCfg.TimeoutSeconds) * time.Second
|
||||
}
|
||||
}
|
||||
break // loaded successfully
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func configSearchPaths() []string {
|
||||
var paths []string
|
||||
// 1. Executable directory (for dev / portable mode)
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
paths = append(paths, filepath.Join(filepath.Dir(exe), "lingma-ipc-proxy.json"))
|
||||
}
|
||||
// 2. Current working directory
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
paths = append(paths, filepath.Join(wd, "lingma-ipc-proxy.json"))
|
||||
}
|
||||
// 3. User home directory
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
paths = append(paths, filepath.Join(home, "lingma-ipc-proxy.json"))
|
||||
paths = append(paths, filepath.Join(home, ".config", "lingma-ipc-proxy", "config.json"))
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func defaultCwd() string {
|
||||
// Use the user's home directory as the default working directory
|
||||
// so it works out-of-the-box regardless of where the app is launched.
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return home
|
||||
}
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
return wd
|
||||
}
|
||||
return "."
|
||||
}
|
||||
|
||||
func defaultShellType() string {
|
||||
if goruntime.GOOS == "windows" {
|
||||
return "powershell"
|
||||
}
|
||||
return "zsh"
|
||||
}
|
||||
|
||||
func transportFallbackHint() string {
|
||||
return "请确认 Lingma 插件已启动并登录;如果自动探测失败,请到设置页手动填写:macOS WebSocket 示例 ws://127.0.0.1:36510/,Windows Named Pipe 示例 \\\\.\\pipe\\lingma-xxxx,或 Windows WebSocket 示例 ws://127.0.0.1:36510/。"
|
||||
}
|
||||
BIN
desktop/build/Lingma.icns
Normal file
35
desktop/build/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Build Directory
|
||||
|
||||
The build directory is used to house all the build files and assets for your application.
|
||||
|
||||
The structure is:
|
||||
|
||||
* bin - Output directory
|
||||
* darwin - macOS specific files
|
||||
* windows - Windows specific files
|
||||
|
||||
## Mac
|
||||
|
||||
The `darwin` directory holds files specific to Mac builds.
|
||||
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||
and
|
||||
build with `wails build`.
|
||||
|
||||
The directory contains the following files:
|
||||
|
||||
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
|
||||
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
|
||||
|
||||
## Windows
|
||||
|
||||
The `windows` directory contains the manifest and rc files used when building with `wails build`.
|
||||
These may be customised for your application. To return these files to the default state, simply delete them and
|
||||
build with `wails build`.
|
||||
|
||||
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
|
||||
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
|
||||
will be created using the `appicon.png` file in the build directory.
|
||||
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
|
||||
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
|
||||
as well as the application itself (right click the exe -> properties -> details)
|
||||
- `wails.exe.manifest` - The main application manifest file.
|
||||
BIN
desktop/build/appicon.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
68
desktop/build/darwin/Info.dev.plist
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
63
desktop/build/darwin/Info.plist
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
desktop/build/iconfile.icns
Normal file
BIN
desktop/build/windows/icon.ico
Normal file
|
After Width: | Height: | Size: 37 KiB |
15
desktop/build/windows/info.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fixed": {
|
||||
"file_version": "{{.Info.ProductVersion}}"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||
"CompanyName": "{{.Info.CompanyName}}",
|
||||
"FileDescription": "{{.Info.ProductName}}",
|
||||
"LegalCopyright": "{{.Info.Copyright}}",
|
||||
"ProductName": "{{.Info.ProductName}}",
|
||||
"Comments": "{{.Info.Comments}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
114
desktop/build/windows/installer/project.nsi
Normal file
@@ -0,0 +1,114 @@
|
||||
Unicode true
|
||||
|
||||
####
|
||||
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||
## mentioned underneath.
|
||||
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||
## from outside of Wails for debugging and development of the installer.
|
||||
##
|
||||
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||
## > wails build --target windows/amd64 --nsis
|
||||
## Then you can call makensis on this file with specifying the path to your binary:
|
||||
## For a AMD64 only installer:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||
## For a ARM64 only installer:
|
||||
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||
## For a installer with both architectures:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||
####
|
||||
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
####
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
####
|
||||
## Include the wails tools
|
||||
####
|
||||
!include "wails_tools.nsh"
|
||||
|
||||
# The version information for this two must consist of 4 parts
|
||||
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||
|
||||
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||
ManifestDPIAware true
|
||||
|
||||
!include "MUI.nsh"
|
||||
|
||||
!define MUI_ICON "..\icon.ico"
|
||||
!define MUI_UNICON "..\icon.ico"
|
||||
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||
|
||||
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||
|
||||
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||
#!uninstfinalize 'signtool --file "%1"'
|
||||
#!finalize 'signtool --file "%1"'
|
||||
|
||||
Name "${INFO_PRODUCTNAME}"
|
||||
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
ShowInstDetails show # This will always show the installation details.
|
||||
|
||||
Function .onInit
|
||||
!insertmacro wails.checkArchitecture
|
||||
FunctionEnd
|
||||
|
||||
Section
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
!insertmacro wails.webview2runtime
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!insertmacro wails.files
|
||||
|
||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
|
||||
!insertmacro wails.associateFiles
|
||||
!insertmacro wails.associateCustomProtocols
|
||||
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
|
||||
Section "uninstall"
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||
|
||||
RMDir /r $INSTDIR
|
||||
|
||||
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||
|
||||
!insertmacro wails.unassociateFiles
|
||||
!insertmacro wails.unassociateCustomProtocols
|
||||
|
||||
!insertmacro wails.deleteUninstaller
|
||||
SectionEnd
|
||||
249
desktop/build/windows/installer/wails_tools.nsh
Normal file
@@ -0,0 +1,249 @@
|
||||
# DO NOT EDIT - Generated automatically by `wails build`
|
||||
|
||||
!include "x64.nsh"
|
||||
!include "WinVer.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "{{.Name}}"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
!endif
|
||||
!ifndef UNINST_KEY_NAME
|
||||
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
!endif
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||
|
||||
!ifndef REQUEST_EXECUTION_LEVEL
|
||||
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||
!endif
|
||||
|
||||
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!ifdef ARG_WAILS_AMD64_BINARY
|
||||
!define SUPPORTS_AMD64
|
||||
!endif
|
||||
|
||||
!ifdef ARG_WAILS_ARM64_BINARY
|
||||
!define SUPPORTS_ARM64
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_AMD64
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "amd64_arm64"
|
||||
!else
|
||||
!define ARCH "amd64"
|
||||
!endif
|
||||
!else
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "arm64"
|
||||
!else
|
||||
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!macro wails.checkArchitecture
|
||||
!ifndef WAILS_WIN10_REQUIRED
|
||||
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||
!endif
|
||||
|
||||
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||
!endif
|
||||
|
||||
${If} ${AtLeastWin10}
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
IfSilent silentArch notSilentArch
|
||||
silentArch:
|
||||
SetErrorLevel 65
|
||||
Abort
|
||||
notSilentArch:
|
||||
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||
Quit
|
||||
${else}
|
||||
IfSilent silentWin notSilentWin
|
||||
silentWin:
|
||||
SetErrorLevel 64
|
||||
Abort
|
||||
notSilentWin:
|
||||
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
!macro wails.files
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro wails.writeUninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
IntFmt $0 "0x%08X" $0
|
||||
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||
!macroend
|
||||
|
||||
!macro wails.deleteUninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||
!macroend
|
||||
|
||||
!macro wails.setShellContext
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||
SetShellVarContext all
|
||||
${else}
|
||||
SetShellVarContext current
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
# Install webview2 by launching the bootstrapper
|
||||
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||
!macro wails.webview2runtime
|
||||
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||
!endif
|
||||
|
||||
SetRegView 64
|
||||
# If the admin key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
SetDetailsPrint both
|
||||
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||
SetDetailsPrint listonly
|
||||
|
||||
InitPluginsDir
|
||||
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||
|
||||
SetDetailsPrint both
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||
|
||||
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||
!macroend
|
||||
|
||||
!macro wails.associateFiles
|
||||
; Create file associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
File "..\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateFiles
|
||||
; Delete app associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||
|
||||
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
!macroend
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||
{{end}}
|
||||
!macroend
|
||||
15
desktop/build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
23
desktop/frontend/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue
|
||||
3 `<script setup>` SFCs, check out
|
||||
the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type
|
||||
by default. In most cases this is fine if you don't really care about component prop types outside of templates.
|
||||
However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using
|
||||
manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look
|
||||
for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default,
|
||||
Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||
13
desktop/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||
<title>lingma-proxy-desktop</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./src/main.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
1088
desktop/frontend/package-lock.json
generated
Normal file
22
desktop/frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"vue": "^3.2.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.18.10",
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.7",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
||||
1
desktop/frontend/package.json.md5
Executable file
@@ -0,0 +1 @@
|
||||
bd2b8442875d0d6e24cc3cec25d4d09b
|
||||
BIN
desktop/frontend/public/favicon.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
268
desktop/frontend/src/App.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import Dashboard from './views/Dashboard.vue'
|
||||
import Logs from './views/Logs.vue'
|
||||
import Models from './views/Models.vue'
|
||||
import Requests from './views/Requests.vue'
|
||||
import Settings from './views/Settings.vue'
|
||||
import { EventsOff, EventsOn } from '../wailsjs/runtime'
|
||||
import { GetStatus, HideWindow, MinimizeWindow } from '../wailsjs/go/main/App.js'
|
||||
import lingmaIcon from './assets/images/lingma-icon.png'
|
||||
|
||||
const currentTab = ref('dashboard')
|
||||
const logs = ref([])
|
||||
const status = ref({ running: false, addr: '', models: 0 })
|
||||
const toast = ref('')
|
||||
const themeMode = ref(localStorage.getItem('lingma-theme-mode') || 'system')
|
||||
const appliedTheme = ref('light')
|
||||
let systemThemeQuery = null
|
||||
let toastTimer = null
|
||||
|
||||
const navigation = [
|
||||
{ key: 'dashboard', label: '仪表盘', icon: 'bi-house-door' },
|
||||
{ key: 'requests', label: '请求流', icon: 'bi-file-earmark-text' },
|
||||
{ key: 'models', label: '模型', icon: 'bi-box' },
|
||||
{ key: 'settings', label: '设置', icon: 'bi-gear' },
|
||||
{ key: 'logs', label: '日志', icon: 'bi-terminal' },
|
||||
]
|
||||
|
||||
function addLog(level, message) {
|
||||
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
logs.value.unshift({ time, level, message })
|
||||
if (logs.value.length > 500) {
|
||||
logs.value = logs.value.slice(0, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
toast.value = message
|
||||
clearTimeout(toastTimer)
|
||||
toastTimer = setTimeout(() => {
|
||||
toast.value = ''
|
||||
}, 2200)
|
||||
}
|
||||
|
||||
function clearLocalLogs() {
|
||||
logs.value = []
|
||||
}
|
||||
|
||||
function setStatus(nextStatus) {
|
||||
status.value = nextStatus
|
||||
}
|
||||
|
||||
function handleNotice(message) {
|
||||
showToast(message)
|
||||
addLog('info', message)
|
||||
}
|
||||
|
||||
function resolveTheme() {
|
||||
if (themeMode.value === 'system') {
|
||||
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
return themeMode.value
|
||||
}
|
||||
|
||||
function applyTheme() {
|
||||
appliedTheme.value = resolveTheme()
|
||||
document.documentElement.dataset.theme = appliedTheme.value
|
||||
localStorage.setItem('lingma-theme-mode', themeMode.value)
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const modes = ['system', 'light', 'dark']
|
||||
const index = modes.indexOf(themeMode.value)
|
||||
themeMode.value = modes[(index + 1) % modes.length]
|
||||
applyTheme()
|
||||
}
|
||||
|
||||
function themeTitle() {
|
||||
if (themeMode.value === 'system') return `跟随系统(当前${appliedTheme.value === 'dark' ? '夜间' : '日间'})`
|
||||
return themeMode.value === 'dark' ? '夜间模式' : '日间模式'
|
||||
}
|
||||
|
||||
function themeIcon() {
|
||||
if (themeMode.value === 'system') return 'bi-circle-half'
|
||||
return themeMode.value === 'dark' ? 'bi-moon-stars' : 'bi-sun'
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
status.value = await GetStatus()
|
||||
} catch (e) {
|
||||
addLog('error', '状态刷新失败:' + (e.message || String(e)))
|
||||
}
|
||||
}
|
||||
|
||||
async function copyEndpoint() {
|
||||
if (!status.value.addr) return
|
||||
const value = `http://${status.value.addr}`
|
||||
await navigator.clipboard?.writeText(value)
|
||||
handleNotice('已复制接口地址:' + value)
|
||||
}
|
||||
|
||||
function safeEventsOn(name, handler) {
|
||||
try {
|
||||
EventsOn(name, handler)
|
||||
} catch (e) {
|
||||
console.debug('Wails runtime event unavailable:', name)
|
||||
}
|
||||
}
|
||||
|
||||
function safeEventsOff(name) {
|
||||
try {
|
||||
EventsOff(name)
|
||||
} catch (e) {
|
||||
console.debug('Wails runtime event unavailable:', name)
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppShortcut(event) {
|
||||
const key = event.key.toLowerCase()
|
||||
if ((event.metaKey || event.ctrlKey) && key === 'w') {
|
||||
event.preventDefault()
|
||||
HideWindow()
|
||||
}
|
||||
if ((event.metaKey || event.ctrlKey) && key === 'm') {
|
||||
event.preventDefault()
|
||||
MinimizeWindow()
|
||||
}
|
||||
// Fallback copy for WebView where native Edit menu is unavailable
|
||||
if ((event.metaKey || event.ctrlKey) && key === 'c') {
|
||||
const selection = window.getSelection()?.toString()
|
||||
if (selection) {
|
||||
event.preventDefault()
|
||||
navigator.clipboard?.writeText(selection).catch(() => {})
|
||||
}
|
||||
}
|
||||
// Fallback select-all for log/request content areas
|
||||
if ((event.metaKey || event.ctrlKey) && key === 'a') {
|
||||
const active = document.activeElement
|
||||
const isEditable = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)
|
||||
if (!isEditable) {
|
||||
const panel = document.querySelector('.log-list, .request-list, .detail-panel')
|
||||
if (panel) {
|
||||
event.preventDefault()
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(panel)
|
||||
const sel = window.getSelection()
|
||||
sel?.removeAllRanges()
|
||||
sel?.addRange(range)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleAppShortcut, true)
|
||||
systemThemeQuery = window.matchMedia?.('(prefers-color-scheme: dark)')
|
||||
systemThemeQuery?.addEventListener?.('change', applyTheme)
|
||||
applyTheme()
|
||||
refreshStatus()
|
||||
safeEventsOn('models:updated', (data) => {
|
||||
status.value.models = Array.isArray(data) ? data.length : status.value.models
|
||||
addLog('info', `模型列表已更新:${status.value.models} 个模型`)
|
||||
})
|
||||
safeEventsOn('log', (data) => {
|
||||
addLog(data.level || 'info', data.message || '')
|
||||
refreshStatus()
|
||||
})
|
||||
safeEventsOn('quit:confirm', (message) => {
|
||||
showToast(message || '再按一次退出快捷键将停止代理并退出应用')
|
||||
})
|
||||
safeEventsOn('status:updated', (nextStatus) => {
|
||||
status.value = nextStatus
|
||||
})
|
||||
safeEventsOn('requests:updated', () => {
|
||||
refreshStatus()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleAppShortcut, true)
|
||||
clearTimeout(toastTimer)
|
||||
systemThemeQuery?.removeEventListener?.('change', applyTheme)
|
||||
safeEventsOff('models:updated')
|
||||
safeEventsOff('log')
|
||||
safeEventsOff('quit:confirm')
|
||||
safeEventsOff('status:updated')
|
||||
safeEventsOff('requests:updated')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar">
|
||||
<button class="brand" type="button" @click="currentTab = 'dashboard'">
|
||||
<span class="brand-mark">
|
||||
<img :src="lingmaIcon" alt="" />
|
||||
</span>
|
||||
<span>
|
||||
<strong>灵码代理</strong>
|
||||
<small>IPC Proxy</small>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<nav class="nav-list" aria-label="主导航">
|
||||
<button
|
||||
v-for="item in navigation"
|
||||
:key="item.key"
|
||||
class="nav-item"
|
||||
:class="{ active: currentTab === item.key }"
|
||||
type="button"
|
||||
@click="currentTab = item.key"
|
||||
>
|
||||
<span class="nav-icon">
|
||||
<i class="bi" :class="item.icon" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-status">
|
||||
<span class="status-dot" :class="{ running: status.running }"></span>
|
||||
<div>
|
||||
<strong>{{ status.running ? 'Proxy Running' : 'Proxy Stopped' }}</strong>
|
||||
<small>v1.2.0</small>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="workspace">
|
||||
<header class="topbar">
|
||||
<span class="topbar-spacer" aria-hidden="true"></span>
|
||||
<div class="topbar-actions">
|
||||
<button class="icon-button" type="button" title="刷新状态" @click="refreshStatus">
|
||||
<i class="bi bi-arrow-clockwise" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="icon-button" type="button" title="复制接口地址" @click="copyEndpoint">
|
||||
<i class="bi bi-copy" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="icon-button" type="button" title="设置" @click="currentTab = 'settings'">
|
||||
<i class="bi bi-gear" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="icon-button" type="button" :title="themeTitle()" @click="toggleTheme">
|
||||
<i class="bi" :class="themeIcon()" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="view-stage">
|
||||
<Dashboard
|
||||
v-if="currentTab === 'dashboard'"
|
||||
:shell-status="status"
|
||||
@log="addLog"
|
||||
@status="setStatus"
|
||||
@notice="handleNotice"
|
||||
@open-settings="currentTab = 'settings'"
|
||||
@open-requests="currentTab = 'requests'"
|
||||
@open-models="currentTab = 'models'"
|
||||
/>
|
||||
<Requests v-else-if="currentTab === 'requests'" @notice="handleNotice" />
|
||||
<Models v-else-if="currentTab === 'models'" @log="addLog" @status="setStatus" @notice="handleNotice" />
|
||||
<Settings v-else-if="currentTab === 'settings'" @log="addLog" @status-refresh="refreshStatus" />
|
||||
<Logs v-else-if="currentTab === 'logs'" :logs="logs" @clear="clearLocalLogs" @notice="handleNotice" />
|
||||
</main>
|
||||
</section>
|
||||
<div v-if="toast" class="toast">{{ toast }}</div>
|
||||
</div>
|
||||
</template>
|
||||
93
desktop/frontend/src/assets/fonts/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
desktop/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
1
desktop/frontend/src/assets/icons/claude.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
desktop/frontend/src/assets/icons/gemma.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemma</title><path d="M12.34 5.953a8.233 8.233 0 01-.247-1.125V3.72a8.25 8.25 0 015.562 2.232H12.34zm-.69 0c.113-.373.199-.755.257-1.145V3.72a8.25 8.25 0 00-5.562 2.232h5.304zm-5.433.187h5.373a7.98 7.98 0 01-.267.696 8.41 8.41 0 01-1.76 2.65L6.216 6.14zm-.264-.187H2.977v.187h2.915a8.436 8.436 0 00-2.357 5.767H0v.186h3.535a8.436 8.436 0 002.357 5.767H2.977v.186h2.976v2.977h.187v-2.915a8.436 8.436 0 005.767 2.357V24h.186v-3.535a8.436 8.436 0 005.767-2.357v2.915h.186v-2.977h2.977v-.186h-2.915a8.436 8.436 0 002.357-5.767H24v-.186h-3.535a8.436 8.436 0 00-2.357-5.767h2.915v-.187h-2.977V2.977h-.186v2.915a8.436 8.436 0 00-5.767-2.357V0h-.186v3.535A8.436 8.436 0 006.14 5.892V2.977h-.187v2.976zm6.14 14.326a8.25 8.25 0 005.562-2.233H12.34c-.108.367-.19.743-.247 1.126v1.107zm-.186-1.087a8.015 8.015 0 00-.258-1.146H6.345a8.25 8.25 0 005.562 2.233v-1.087zm-8.186-7.285h1.107a8.23 8.23 0 001.125-.247V6.345a8.25 8.25 0 00-2.232 5.562zm1.087.186H3.72a8.25 8.25 0 002.232 5.562v-5.304a8.012 8.012 0 00-1.145-.258zm15.47-.186a8.25 8.25 0 00-2.232-5.562v5.315c.367.108.743.19 1.126.247h1.107zm-1.086.186c-.39.058-.772.144-1.146.258v5.304a8.25 8.25 0 002.233-5.562h-1.087zm-1.332 5.69V12.41a7.97 7.97 0 00-.696.267 8.409 8.409 0 00-2.65 1.76l3.346 3.346zm0-6.18v-5.45l-.012-.013h-5.451c.076.235.162.468.26.696a8.698 8.698 0 001.819 2.688 8.698 8.698 0 002.688 1.82c.228.097.46.183.696.259zM6.14 17.848V12.41c.235.078.468.167.696.267a8.403 8.403 0 012.688 1.799 8.404 8.404 0 011.799 2.688c.1.228.19.46.267.696H6.152l-.012-.012zm0-6.245V6.326l3.29 3.29a8.716 8.716 0 01-2.594 1.728 8.14 8.14 0 01-.696.259zm6.257 6.257h5.277l-3.29-3.29a8.716 8.716 0 00-1.728 2.594 8.135 8.135 0 00-.259.696zm-2.347-7.81a9.435 9.435 0 01-2.88 1.96 9.14 9.14 0 012.88 1.94 9.14 9.14 0 011.94 2.88 9.435 9.435 0 011.96-2.88 9.14 9.14 0 012.88-1.94 9.435 9.435 0 01-2.88-1.96 9.434 9.434 0 01-1.96-2.88 9.14 9.14 0 01-1.94 2.88z"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
desktop/frontend/src/assets/icons/kimi.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kimi</title><path d="M21.846 0a1.923 1.923 0 110 3.846H20.15a.226.226 0 01-.227-.226V1.923C19.923.861 20.784 0 21.846 0z"></path><path d="M11.065 11.199l7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 00-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23H3.186c-.103 0-.186.103-.186.23V19.77c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 01.205-.023l6.484 4.772a7.677 7.677 0 003.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 01-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z"></path></svg>
|
||||
|
After Width: | Height: | Size: 786 B |
1
desktop/frontend/src/assets/icons/minimax.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
desktop/frontend/src/assets/icons/openai.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
desktop/frontend/src/assets/icons/qwen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Qwen</title><path d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
desktop/frontend/src/assets/images/lingma-icon.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
desktop/frontend/src/assets/images/logo-universal.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
71
desktop/frontend/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang="ts" setup>
|
||||
import {reactive} from 'vue'
|
||||
import {Greet} from '../../wailsjs/go/main/App'
|
||||
|
||||
const data = reactive({
|
||||
name: "",
|
||||
resultText: "Please enter your name below 👇",
|
||||
})
|
||||
|
||||
function greet() {
|
||||
Greet(data.name).then(result => {
|
||||
data.resultText = result
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div id="result" class="result">{{ data.resultText }}</div>
|
||||
<div id="input" class="input-box">
|
||||
<input id="name" v-model="data.name" autocomplete="off" class="input" type="text"/>
|
||||
<button class="btn" @click="greet">Greet</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.result {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin: 1.5rem auto;
|
||||
}
|
||||
|
||||
.input-box .btn {
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-box .btn:hover {
|
||||
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.input-box .input {
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
background-color: rgba(240, 240, 240, 1);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.input-box .input:hover {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.input-box .input:focus {
|
||||
border: none;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
</style>
|
||||
6
desktop/frontend/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css';
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css';
|
||||
|
||||
createApp(App).mount('#app')
|
||||
24
desktop/frontend/src/modelIcons.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import autoIcon from 'bootstrap-icons/icons/shuffle.svg'
|
||||
import claudeIcon from './assets/icons/claude.svg'
|
||||
import gemmaIcon from './assets/icons/gemma.svg'
|
||||
import kimiIcon from './assets/icons/kimi.svg'
|
||||
import lingmaIcon from './assets/images/lingma-icon.png'
|
||||
import minimaxIcon from './assets/icons/minimax.svg'
|
||||
import openaiIcon from './assets/icons/openai.svg'
|
||||
import qwenIcon from './assets/icons/qwen.svg'
|
||||
|
||||
const ICONS = [
|
||||
{ match: ['auto', 'automatic', '自动'], src: autoIcon, color: '#2563eb' },
|
||||
{ match: ['qwen', 'qwq'], src: qwenIcon, color: '#5b6ee1' },
|
||||
{ match: ['kimi', 'moonshot'], src: kimiIcon, color: '#111827' },
|
||||
{ match: ['minimax', 'abab'], src: minimaxIcon, color: '#1677ff' },
|
||||
{ match: ['claude', 'anthropic'], src: claudeIcon, color: '#d97757' },
|
||||
{ match: ['gpt', 'openai'], src: openaiIcon, color: '#10a37f' },
|
||||
{ match: ['gemma', 'google'], src: gemmaIcon, color: '#4285f4' },
|
||||
]
|
||||
|
||||
export function modelIcon(model) {
|
||||
const text = `${model?.id || ''} ${model?.name || ''}`.toLowerCase()
|
||||
const matched = ICONS.find((item) => item.match.some((keyword) => text.includes(keyword)))
|
||||
return matched || { src: lingmaIcon, color: '#2563eb' }
|
||||
}
|
||||
1540
desktop/frontend/src/style.css
Normal file
402
desktop/frontend/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import {
|
||||
GetModels,
|
||||
GetConfig,
|
||||
GetRequests,
|
||||
GetStatus,
|
||||
QuitApp,
|
||||
RefreshModels,
|
||||
StartProxy,
|
||||
StopProxy,
|
||||
} from '../../wailsjs/go/main/App.js'
|
||||
import { ClipboardSetText } from '../../wailsjs/runtime'
|
||||
import { modelIcon } from '../modelIcons'
|
||||
|
||||
const props = defineProps({
|
||||
shellStatus: {
|
||||
type: Object,
|
||||
default: () => ({ running: false, addr: '', models: 0 }),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['log', 'status', 'notice', 'open-settings', 'open-requests', 'open-models'])
|
||||
|
||||
const status = ref(props.shellStatus)
|
||||
const models = ref([])
|
||||
const requests = ref([])
|
||||
const health = ref(null)
|
||||
const config = ref({})
|
||||
const loading = ref(false)
|
||||
const testing = ref(false)
|
||||
const now = ref(Date.now())
|
||||
let interval = null
|
||||
let clockInterval = null
|
||||
|
||||
const endpoint = computed(() => (status.value.addr ? `http://${status.value.addr}` : '未启动'))
|
||||
const isRunning = computed(() => Boolean(status.value.running))
|
||||
const runningDuration = computed(() => {
|
||||
if (!isRunning.value || !status.value.startedAt) return '未运行'
|
||||
const startedAt = new Date(status.value.startedAt).getTime()
|
||||
if (!Number.isFinite(startedAt)) return '运行中'
|
||||
const seconds = Math.max(0, Math.floor((now.value - startedAt) / 1000))
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const rest = seconds % 60
|
||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(rest).padStart(2, '0')}`
|
||||
})
|
||||
const parsedDurations = computed(() => requests.value.map((request) => parseDurationMs(request.duration)).filter((value) => value > 0))
|
||||
const healthStats = computed(() => {
|
||||
const values = parsedDurations.value
|
||||
if (values.length === 0) {
|
||||
return { avg: 0, p50: 0, p95: 0, max: 0 }
|
||||
}
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
const avg = Math.round(values.reduce((sum, value) => sum + value, 0) / values.length)
|
||||
return {
|
||||
avg,
|
||||
p50: percentile(sorted, 0.5),
|
||||
p95: percentile(sorted, 0.95),
|
||||
max: sorted[sorted.length - 1],
|
||||
}
|
||||
})
|
||||
const chartBars = computed(() => {
|
||||
const values = parsedDurations.value.slice(0, 36).reverse()
|
||||
if (values.length === 0) return []
|
||||
const max = Math.max(...values)
|
||||
return values.map((value) => Math.max(12, Math.round((value / max) * 100)))
|
||||
})
|
||||
const displayRequests = computed(() => {
|
||||
if (requests.value.length > 0) return requests.value.slice(0, 7)
|
||||
return []
|
||||
})
|
||||
const displayModels = computed(() => {
|
||||
if (models.value.length > 0) {
|
||||
return models.value.slice(0, 5).map((model) => ({ ...model, online: true }))
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
function parseDurationMs(duration) {
|
||||
const text = String(duration || '').trim()
|
||||
if (!text) return 0
|
||||
if (text.endsWith('ms')) return Number.parseFloat(text)
|
||||
if (text.endsWith('s')) return Number.parseFloat(text) * 1000
|
||||
return Number.parseFloat(text) || 0
|
||||
}
|
||||
|
||||
function percentile(sorted, p) {
|
||||
if (sorted.length === 0) return 0
|
||||
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * p) - 1))
|
||||
return Math.round(sorted[index])
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const nextStatus = await GetStatus()
|
||||
status.value = nextStatus
|
||||
emit('status', nextStatus)
|
||||
requests.value = await GetRequests()
|
||||
config.value = await GetConfig()
|
||||
if (nextStatus.running) {
|
||||
models.value = await GetModels()
|
||||
}
|
||||
} catch (e) {
|
||||
emit('log', 'error', '刷新仪表盘失败:' + (e.message || String(e)))
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshModels() {
|
||||
loading.value = true
|
||||
try {
|
||||
models.value = await RefreshModels()
|
||||
emit('log', 'info', `模型探测完成:${models.value.length} 个`)
|
||||
await refresh()
|
||||
} catch (e) {
|
||||
emit('log', 'error', '模型探测失败:' + (e.message || String(e)) + '。请确认 Lingma 插件已启动并登录;自动探测失败时可到设置页手动填写 WebSocket:ws://127.0.0.1:36510/,或 Windows Named Pipe:\\\\.\\pipe\\lingma-xxxx。')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function copyModelName(model) {
|
||||
if (!model?.id) return
|
||||
try {
|
||||
await ClipboardSetText(model.id)
|
||||
emit('notice', `已复制模型 ID:${model.id}`)
|
||||
} catch (e) {
|
||||
try {
|
||||
await navigator.clipboard?.writeText(model.id)
|
||||
emit('notice', `已复制模型 ID:${model.id}`)
|
||||
} catch (fallbackError) {
|
||||
emit('log', 'error', '模型 ID 复制失败:' + (fallbackError.message || String(fallbackError)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleProxy() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (isRunning.value) {
|
||||
await StopProxy()
|
||||
emit('log', 'info', '代理已停止')
|
||||
} else {
|
||||
await StartProxy()
|
||||
emit('log', 'info', '代理已启动')
|
||||
}
|
||||
await refresh()
|
||||
} catch (e) {
|
||||
emit('log', 'error', '代理切换失败:' + (e.message || String(e)))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function restartProxy() {
|
||||
if (!isRunning.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
await StopProxy()
|
||||
await StartProxy()
|
||||
emit('log', 'info', '代理已重启')
|
||||
await refresh()
|
||||
} catch (e) {
|
||||
emit('log', 'error', '代理重启失败:' + (e.message || String(e)))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
if (!isRunning.value || !status.value.addr) {
|
||||
emit('log', 'warn', '代理未运行,无法测试连接')
|
||||
return
|
||||
}
|
||||
testing.value = true
|
||||
try {
|
||||
const resp = await fetch(`${endpoint.value}/health`)
|
||||
const data = await resp.json()
|
||||
health.value = data
|
||||
emit('log', data.ok ? 'info' : 'warn', data.ok ? '健康检查通过' : '健康检查返回异常')
|
||||
} catch (e) {
|
||||
health.value = { ok: false, error: e.message || String(e) }
|
||||
emit('log', 'error', '健康检查失败:' + (e.message || String(e)))
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function quitApp() {
|
||||
if (confirm('确定退出应用?代理服务会一起停止。')) {
|
||||
await QuitApp()
|
||||
}
|
||||
}
|
||||
|
||||
function statusClass(code) {
|
||||
if (code >= 200 && code < 300) return 'ok'
|
||||
if (code >= 400) return 'err'
|
||||
return 'warn'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
interval = setInterval(refresh, 2500)
|
||||
clockInterval = setInterval(() => {
|
||||
now.value = Date.now()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval)
|
||||
clearInterval(clockInterval)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<section class="glass-panel status-strip">
|
||||
<div class="strip-cell">
|
||||
<span class="strip-dot" :class="{ stopped: !isRunning }"></span>
|
||||
<div>
|
||||
<strong>{{ isRunning ? 'Proxy Running' : 'Proxy Stopped' }}</strong>
|
||||
<span>{{ isRunning ? `运行 ${runningDuration}` : runningDuration }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strip-cell">
|
||||
<label>Endpoint</label>
|
||||
<a href="#" @click.prevent>{{ endpoint }}</a>
|
||||
</div>
|
||||
<div class="strip-cell">
|
||||
<label>Transport</label>
|
||||
<strong>{{ health?.state?.transport || 'WebSocket' }}</strong>
|
||||
</div>
|
||||
<div class="strip-cell">
|
||||
<label>Session</label>
|
||||
<strong>{{ health?.state?.session_mode || 'Reuse' }}</strong>
|
||||
</div>
|
||||
<div class="strip-actions">
|
||||
<button :class="{ active: !isRunning }" type="button" :disabled="loading || isRunning" @click="toggleProxy">启动</button>
|
||||
<button :class="{ active: isRunning }" type="button" :disabled="loading || !isRunning" @click="toggleProxy">停止</button>
|
||||
<button type="button" :disabled="loading || !isRunning" @click="restartProxy">重启</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-grid">
|
||||
<div class="glass-panel area-health">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Health <span class="muted">(Last 60s)</span></h2>
|
||||
<p>Latency (ms)</p>
|
||||
</div>
|
||||
<span class="status-chip ok">Healthy</span>
|
||||
</div>
|
||||
<div class="activity-chart" aria-label="延迟趋势图">
|
||||
<span
|
||||
v-for="(height, index) in chartBars"
|
||||
:key="index"
|
||||
class="bar"
|
||||
:style="{ height: `${height}%`, opacity: 0.55 + index / 45 }"
|
||||
></span>
|
||||
<span v-if="chartBars.length === 0" class="chart-empty">暂无请求</span>
|
||||
</div>
|
||||
<div class="health-stats">
|
||||
<div><strong>{{ healthStats.avg }}</strong><span>Avg (ms)</span></div>
|
||||
<div><strong>{{ healthStats.p50 }}</strong><span>P50 (ms)</span></div>
|
||||
<div><strong>{{ healthStats.p95 }}</strong><span>P95 (ms)</span></div>
|
||||
<div><strong style="color: #d97706">{{ healthStats.max }}</strong><span>Max (ms)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-panel area-models model-card">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Models</h2>
|
||||
</div>
|
||||
<button class="secondary-button" type="button" :disabled="loading || !isRunning" @click="refreshModels">探测模型</button>
|
||||
</div>
|
||||
<div class="model-card-list hidden-scrollbar">
|
||||
<button
|
||||
v-for="model in displayModels"
|
||||
:key="model.id"
|
||||
class="model-row model-choice"
|
||||
type="button"
|
||||
:title="`复制模型 ID:${model.id}`"
|
||||
@click="copyModelName(model)"
|
||||
>
|
||||
<span
|
||||
class="model-brand-icon"
|
||||
:style="{ '--model-icon': `url(${modelIcon(model).src})`, '--model-icon-color': modelIcon(model).color }"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<div>
|
||||
<div class="model-name">{{ model.name || model.id }}</div>
|
||||
</div>
|
||||
<span class="status-chip" :class="model.online ? 'ok' : 'warn'">{{ model.online ? 'Online' : 'Offline' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="displayModels.length === 0" class="empty-state compact">暂无模型,启动代理后点击探测模型。</div>
|
||||
<button class="link-row" type="button" @click="emit('open-models')">查看全部模型 <i class="bi bi-chevron-right"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="glass-panel area-config">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Configuration</h2>
|
||||
</div>
|
||||
<span class="status-chip ok">Valid</span>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="cell-main">Host</div>
|
||||
<div class="cell-sub">{{ config.Host || '127.0.0.1' }}</div>
|
||||
</div>
|
||||
<span class="status-chip ok"><i class="bi bi-check"></i></span>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="cell-main">Port</div>
|
||||
<div class="cell-sub">{{ config.Port || 8095 }}</div>
|
||||
</div>
|
||||
<span class="status-chip ok"><i class="bi bi-check"></i></span>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="cell-main">Transport</div>
|
||||
<div class="cell-sub">{{ config.Transport || 'WebSocket' }}</div>
|
||||
</div>
|
||||
<span class="status-chip ok"><i class="bi bi-check"></i></span>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="cell-main">Session</div>
|
||||
<div class="cell-sub">{{ config.SessionMode || 'Reuse' }}</div>
|
||||
</div>
|
||||
<span class="status-chip ok"><i class="bi bi-check"></i></span>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="cell-main">Timeout (s)</div>
|
||||
<div class="cell-sub">{{ config.Timeout || 120 }} 秒</div>
|
||||
</div>
|
||||
<span class="status-chip ok"><i class="bi bi-check"></i></span>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="cell-main">CWD</div>
|
||||
<div class="cell-sub">{{ config.Cwd || '未配置' }}</div>
|
||||
</div>
|
||||
<span class="status-chip ok"><i class="bi bi-check"></i></span>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<div class="cell-main">Current File</div>
|
||||
<div class="cell-sub">{{ config.CurrentFilePath || '未配置' }}</div>
|
||||
</div>
|
||||
<span class="status-chip ok"><i class="bi bi-check"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-panel area-requests">
|
||||
<div class="table-toolbar">
|
||||
<div>
|
||||
<div class="panel-header" style="margin: 0">
|
||||
<h2>Recent Requests</h2>
|
||||
</div>
|
||||
</div>
|
||||
<button class="secondary-button" type="button" @click="emit('open-requests')">查看全部</button>
|
||||
</div>
|
||||
<div v-if="displayRequests.length > 0" class="table-scroll hidden-scrollbar">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Method</th>
|
||||
<th>Path</th>
|
||||
<th>Model</th>
|
||||
<th>Status</th>
|
||||
<th>Duration</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(request, index) in displayRequests" :key="index">
|
||||
<td>{{ request.time }}</td>
|
||||
<td>{{ request.method }}</td>
|
||||
<td>{{ request.path }}</td>
|
||||
<td>{{ request.model || 'Qwen3-Coder' }}</td>
|
||||
<td><span class="status-chip" :class="statusClass(request.statusCode)">{{ request.statusCode }}</span></td>
|
||||
<td>{{ request.duration }}</td>
|
||||
<td>{{ request.size || '2.1 KB' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="empty-state compact">暂无请求记录。连接客户端后会显示真实调用。</div>
|
||||
<div class="table-footer">
|
||||
<span>Showing {{ displayRequests.length }} of {{ requests.length }}</span>
|
||||
<button type="button" @click="emit('open-requests')">查看全部请求 <i class="bi bi-chevron-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
96
desktop/frontend/src/views/Logs.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ClipboardSetText } from '../../wailsjs/runtime'
|
||||
|
||||
const props = defineProps({
|
||||
logs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['clear', 'notice'])
|
||||
|
||||
const filter = ref('all')
|
||||
const search = ref('')
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
return props.logs.filter((log) => {
|
||||
const matchesLevel = filter.value === 'all' || log.level === filter.value
|
||||
const matchesSearch = !q || `${log.time} ${log.level} ${log.message}`.toLowerCase().includes(q)
|
||||
return matchesLevel && matchesSearch
|
||||
})
|
||||
})
|
||||
|
||||
function levelClass(level) {
|
||||
return {
|
||||
info: 'level-info',
|
||||
warn: 'level-warn',
|
||||
error: 'level-error',
|
||||
}[level] || 'level-info'
|
||||
}
|
||||
|
||||
function levelLabel(level) {
|
||||
return {
|
||||
info: '信息',
|
||||
warn: '警告',
|
||||
error: '错误',
|
||||
}[level] || level
|
||||
}
|
||||
|
||||
function serializeLogs() {
|
||||
return filteredLogs.value.map((log) => `[${log.time}] ${levelLabel(log.level)} ${log.message}`).join('\n')
|
||||
}
|
||||
|
||||
async function copyLogs() {
|
||||
try {
|
||||
await ClipboardSetText(serializeLogs())
|
||||
emit('notice', `已复制 ${filteredLogs.value.length} 条日志`)
|
||||
} catch (e) {
|
||||
try {
|
||||
await navigator.clipboard?.writeText(serializeLogs())
|
||||
emit('notice', `已复制 ${filteredLogs.value.length} 条日志`)
|
||||
} catch (fallbackError) {
|
||||
console.debug('Copy logs failed:', fallbackError)
|
||||
emit('notice', '日志复制失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page logs-page">
|
||||
<div class="page-title">
|
||||
<div>
|
||||
<h1>日志</h1>
|
||||
<p>记录代理启动、模型同步、健康检查和配置保存事件。</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button class="secondary-button" type="button" :disabled="filteredLogs.length === 0" @click="copyLogs">复制日志</button>
|
||||
<button class="danger-button" type="button" @click="emit('clear')">清空日志</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="table-panel logs-panel">
|
||||
<div class="table-toolbar">
|
||||
<div class="segmented">
|
||||
<button :class="{ active: filter === 'all' }" type="button" @click="filter = 'all'">全部</button>
|
||||
<button :class="{ active: filter === 'info' }" type="button" @click="filter = 'info'">信息</button>
|
||||
<button :class="{ active: filter === 'warn' }" type="button" @click="filter = 'warn'">警告</button>
|
||||
<button :class="{ active: filter === 'error' }" type="button" @click="filter = 'error'">错误</button>
|
||||
</div>
|
||||
<input v-model="search" class="search-input" type="search" placeholder="搜索日志内容" />
|
||||
</div>
|
||||
|
||||
<div v-if="filteredLogs.length > 0" class="log-list hidden-scrollbar">
|
||||
<div v-for="(log, index) in filteredLogs" :key="index" class="log-row">
|
||||
<span class="muted">{{ log.time }}</span>
|
||||
<strong :class="levelClass(log.level)">{{ levelLabel(log.level) }}</strong>
|
||||
<span>{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">暂无日志。</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
120
desktop/frontend/src/views/Models.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { GetModels, GetStatus, RefreshModels } from '../../wailsjs/go/main/App.js'
|
||||
import { ClipboardSetText } from '../../wailsjs/runtime'
|
||||
import { modelIcon } from '../modelIcons'
|
||||
|
||||
const emit = defineEmits(['log', 'status', 'notice'])
|
||||
|
||||
const models = ref([])
|
||||
const status = ref({ running: false, addr: '', models: 0 })
|
||||
const loading = ref(false)
|
||||
const query = ref('')
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = query.value.trim().toLowerCase()
|
||||
if (!q) return models.value
|
||||
return models.value.filter((model) => `${model.id} ${model.name}`.toLowerCase().includes(q))
|
||||
})
|
||||
|
||||
function modelTag(model) {
|
||||
const text = `${model.id} ${model.name}`.toLowerCase()
|
||||
if (text.includes('coder')) return '工具优先'
|
||||
if (text.includes('thinking')) return '推理'
|
||||
if (text.includes('kimi')) return '长文本'
|
||||
if (text.includes('minimax')) return '通用'
|
||||
return 'Lingma'
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
try {
|
||||
status.value = await GetStatus()
|
||||
models.value = status.value.running ? await RefreshModels() : await GetModels()
|
||||
emit('log', 'info', `模型列表刷新完成:${models.value.length} 个`)
|
||||
} catch (e) {
|
||||
emit('log', 'error', '模型列表刷新失败:' + (e.message || String(e)) + '。自动探测失败时请到设置页手动填写 WebSocket:ws://127.0.0.1:36510/,或 Windows Named Pipe:\\\\.\\pipe\\lingma-xxxx。')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function copyModelName(model) {
|
||||
if (!model?.id) return
|
||||
try {
|
||||
await ClipboardSetText(model.id)
|
||||
emit('notice', `已复制模型 ID:${model.id}`)
|
||||
} catch (e) {
|
||||
try {
|
||||
await navigator.clipboard?.writeText(model.id)
|
||||
emit('notice', `已复制模型 ID:${model.id}`)
|
||||
} catch (fallbackError) {
|
||||
emit('log', 'error', '模型 ID 复制失败:' + (fallbackError.message || String(fallbackError)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(refresh)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page-title">
|
||||
<div>
|
||||
<h1>模型</h1>
|
||||
<p>来自 Lingma 插件的可用模型列表,第三方客户端可以直接使用这些 ID。</p>
|
||||
</div>
|
||||
<button class="primary-button" type="button" :disabled="loading" @click="refresh">
|
||||
{{ loading ? '刷新中...' : '刷新模型' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="grid-3">
|
||||
<div class="metric">
|
||||
<label>代理状态</label>
|
||||
<strong>{{ status.running ? '运行中' : '未运行' }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<label>接口地址</label>
|
||||
<strong>{{ status.addr || '未启动' }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<label>模型数量</label>
|
||||
<strong>{{ models.length }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="glass-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>可用模型</h2>
|
||||
<p>推荐 Claude Code / Cline 优先选择 Qwen3-Coder。</p>
|
||||
</div>
|
||||
<input v-model="query" class="search-input" type="search" placeholder="搜索模型" style="max-width: 260px" />
|
||||
</div>
|
||||
|
||||
<div v-if="filtered.length > 0" class="models-list model-page-list hidden-scrollbar">
|
||||
<button
|
||||
v-for="model in filtered"
|
||||
:key="model.id"
|
||||
class="model-row model-list-row model-choice"
|
||||
type="button"
|
||||
:title="`复制模型 ID:${model.id}`"
|
||||
@click="copyModelName(model)"
|
||||
>
|
||||
<span
|
||||
class="model-brand-icon"
|
||||
:style="{ '--model-icon': `url(${modelIcon(model).src})`, '--model-icon-color': modelIcon(model).color }"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<div>
|
||||
<div class="model-name">{{ model.name || model.id }}</div>
|
||||
<div class="model-meta">{{ model.id }}</div>
|
||||
</div>
|
||||
<span class="status-chip" :class="modelTag(model) === '工具优先' ? 'ok' : 'warn'">{{ modelTag(model) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="empty-state">启动代理并刷新后会显示模型。</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
181
desktop/frontend/src/views/Requests.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { ClearRequests, GetRequests } from '../../wailsjs/go/main/App.js'
|
||||
import { ClipboardSetText, EventsOff, EventsOn } from '../../wailsjs/runtime'
|
||||
|
||||
const emit = defineEmits(['notice'])
|
||||
|
||||
const requests = ref([])
|
||||
const selected = ref(null)
|
||||
const query = ref('')
|
||||
const activeStatus = ref('all')
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = query.value.trim().toLowerCase()
|
||||
return requests.value.filter((request) => {
|
||||
const matchesQuery = !q || `${request.method} ${request.path} ${request.statusCode}`.toLowerCase().includes(q)
|
||||
const code = Number(request.statusCode)
|
||||
const matchesStatus =
|
||||
activeStatus.value === 'all' ||
|
||||
(activeStatus.value === 'ok' && code >= 200 && code < 300) ||
|
||||
(activeStatus.value === 'err' && code >= 400) ||
|
||||
(activeStatus.value === 'warn' && code >= 300 && code < 400)
|
||||
return matchesQuery && matchesStatus
|
||||
})
|
||||
})
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
requests.value = await GetRequests()
|
||||
} catch (e) {
|
||||
console.debug('Wails GetRequests unavailable in browser preview')
|
||||
}
|
||||
}
|
||||
|
||||
async function clear() {
|
||||
try {
|
||||
await ClearRequests()
|
||||
} catch (e) {
|
||||
console.debug('Wails ClearRequests unavailable in browser preview')
|
||||
}
|
||||
requests.value = []
|
||||
selected.value = null
|
||||
}
|
||||
|
||||
function statusClass(code) {
|
||||
if (code >= 200 && code < 300) return 'ok'
|
||||
if (code >= 400) return 'err'
|
||||
return 'warn'
|
||||
}
|
||||
|
||||
function selectRow(index) {
|
||||
selected.value = selected.value === index ? null : index
|
||||
}
|
||||
|
||||
async function writeClipboard(text) {
|
||||
const value = text || ''
|
||||
try {
|
||||
await ClipboardSetText(value)
|
||||
return true
|
||||
} catch (e) {
|
||||
await navigator.clipboard?.writeText(value)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
async function copyText(text, label) {
|
||||
try {
|
||||
await writeClipboard(text)
|
||||
emit('notice', `已复制${label}`)
|
||||
} catch (e) {
|
||||
console.debug('Copy failed:', e)
|
||||
emit('notice', `${label}复制失败`)
|
||||
}
|
||||
}
|
||||
|
||||
function safeEventsOn(name, handler) {
|
||||
try {
|
||||
EventsOn(name, handler)
|
||||
} catch (e) {
|
||||
console.debug('Wails runtime event unavailable:', name)
|
||||
}
|
||||
}
|
||||
|
||||
function safeEventsOff(name) {
|
||||
try {
|
||||
EventsOff(name)
|
||||
} catch (e) {
|
||||
console.debug('Wails runtime event unavailable:', name)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
safeEventsOn('requests:updated', (data) => {
|
||||
requests.value = data || []
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
safeEventsOff('requests:updated')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page requests-page">
|
||||
<div class="page-title">
|
||||
<div>
|
||||
<h1>请求流</h1>
|
||||
<p>查看客户端调用 OpenAI / Anthropic 兼容接口的请求与响应。</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button class="secondary-button" type="button" @click="refresh">刷新</button>
|
||||
<button class="danger-button" type="button" @click="clear">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="table-panel requests-panel">
|
||||
<div class="table-toolbar">
|
||||
<input v-model="query" class="search-input" type="search" placeholder="搜索路径、方法或状态码" />
|
||||
<div class="segmented">
|
||||
<button :class="{ active: activeStatus === 'all' }" type="button" @click="activeStatus = 'all'">全部</button>
|
||||
<button :class="{ active: activeStatus === 'ok' }" type="button" @click="activeStatus = 'ok'">成功</button>
|
||||
<button :class="{ active: activeStatus === 'warn' }" type="button" @click="activeStatus = 'warn'">跳转</button>
|
||||
<button :class="{ active: activeStatus === 'err' }" type="button" @click="activeStatus = 'err'">错误</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filtered.length > 0" class="table-scroll hidden-scrollbar">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>方法</th>
|
||||
<th>路径</th>
|
||||
<th>状态</th>
|
||||
<th>耗时</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(request, index) in filtered" :key="index" @click="selectRow(index)">
|
||||
<td>{{ request.time }}</td>
|
||||
<td><span class="method-chip">{{ request.method }}</span></td>
|
||||
<td>
|
||||
<div class="cell-main">{{ request.path }}</div>
|
||||
<div class="cell-sub">{{ request.reqBody ? '包含请求体' : '无请求体' }}</div>
|
||||
</td>
|
||||
<td><span class="status-chip" :class="statusClass(request.statusCode)">{{ request.statusCode }}</span></td>
|
||||
<td>{{ request.duration }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="empty-state">暂无匹配请求。</div>
|
||||
|
||||
<div v-if="selected !== null && filtered[selected]" class="detail-panel hidden-scrollbar">
|
||||
<div class="detail-section">
|
||||
<div class="detail-toolbar">
|
||||
<h3>请求内容</h3>
|
||||
<div class="detail-actions">
|
||||
<button type="button" class="ghost-button" @click="copyText(filtered[selected].reqBody, '请求内容')">
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre>{{ filtered[selected].reqBody || '空请求体' }}</pre>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<div class="detail-toolbar">
|
||||
<h3>响应内容</h3>
|
||||
<div class="detail-actions">
|
||||
<button type="button" class="ghost-button" @click="copyText(filtered[selected].respBody, '响应内容')">
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre>{{ filtered[selected].respBody || '空响应体' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
218
desktop/frontend/src/views/Settings.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { GetConfig, UpdateConfig } from '../../wailsjs/go/main/App.js'
|
||||
|
||||
const emit = defineEmits(['log', 'status-refresh'])
|
||||
|
||||
const config = ref({})
|
||||
const saving = ref(false)
|
||||
const openSelect = ref('')
|
||||
|
||||
const selectOptions = {
|
||||
Transport: [
|
||||
{ value: 'auto', label: '自动' },
|
||||
{ value: 'pipe', label: '命名管道' },
|
||||
{ value: 'websocket', label: 'WebSocket' },
|
||||
],
|
||||
Mode: [
|
||||
{ value: 'agent', label: 'Agent' },
|
||||
{ value: 'chat', label: 'Chat' },
|
||||
],
|
||||
ShellType: [
|
||||
{ value: 'zsh', label: 'zsh' },
|
||||
{ value: 'bash', label: 'bash' },
|
||||
{ value: 'powershell', label: 'PowerShell' },
|
||||
{ value: 'cmd', label: 'cmd' },
|
||||
],
|
||||
SessionMode: [
|
||||
{ value: 'auto', label: '自动' },
|
||||
{ value: 'reuse', label: '复用' },
|
||||
{ value: 'fresh', label: '每次新建' },
|
||||
],
|
||||
}
|
||||
|
||||
const selectLabel = computed(() => (field) => {
|
||||
const option = selectOptions[field]?.find((item) => item.value === config.value[field])
|
||||
return option?.label || '请选择'
|
||||
})
|
||||
|
||||
function toggleSelect(field) {
|
||||
openSelect.value = openSelect.value === field ? '' : field
|
||||
}
|
||||
|
||||
function chooseOption(field, value) {
|
||||
config.value[field] = value
|
||||
openSelect.value = ''
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
config.value = await GetConfig()
|
||||
} catch (e) {
|
||||
emit('log', 'error', '配置加载失败:' + (e.message || String(e)))
|
||||
}
|
||||
})
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
await UpdateConfig(config.value)
|
||||
emit('log', 'info', '配置已保存,代理已按需重启')
|
||||
emit('status-refresh')
|
||||
} catch (e) {
|
||||
emit('log', 'error', '配置保存失败:' + (e.message || String(e)))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page-title">
|
||||
<div>
|
||||
<h1>设置</h1>
|
||||
<p>配置监听地址、Lingma 传输方式、会话复用和请求超时。</p>
|
||||
</div>
|
||||
<button class="primary-button" type="button" :disabled="saving" @click="save">
|
||||
{{ saving ? '保存中...' : '保存并重启' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="grid-2">
|
||||
<div class="glass-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>服务监听</h2>
|
||||
<p>第三方客户端连接本地代理使用这组地址。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>主机</label>
|
||||
<input v-model="config.Host" type="text" placeholder="127.0.0.1" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>端口</label>
|
||||
<input v-model.number="config.Port" type="number" placeholder="8095" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>传输方式</label>
|
||||
<div class="custom-select" :class="{ open: openSelect === 'Transport' }">
|
||||
<button type="button" @click="toggleSelect('Transport')">
|
||||
<span>{{ selectLabel('Transport') }}</span>
|
||||
<i class="bi bi-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div v-if="openSelect === 'Transport'" class="select-menu">
|
||||
<button
|
||||
v-for="option in selectOptions.Transport"
|
||||
:key="option.value"
|
||||
:class="{ selected: option.value === config.Transport }"
|
||||
type="button"
|
||||
@click="chooseOption('Transport', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>超时秒数</label>
|
||||
<input v-model.number="config.Timeout" type="number" min="1" />
|
||||
</div>
|
||||
<div class="field span-2">
|
||||
<label>WebSocket 地址</label>
|
||||
<input v-model="config.WebSocketURL" type="text" placeholder="留空自动探测 Lingma WebSocket" />
|
||||
</div>
|
||||
<div class="field span-2">
|
||||
<label>命名管道</label>
|
||||
<input v-model="config.Pipe" type="text" placeholder="留空自动探测 Windows Named Pipe" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint-box">
|
||||
<strong>自动探测失败时</strong>
|
||||
<span>先确认 VS Code / Lingma 插件已启动并登录。macOS 通常填写 WebSocket,例如 <code>ws://127.0.0.1:36510/</code>;Windows 可填写命名管道,例如 <code>\\.\pipe\lingma-xxxx</code>,也可填写 WebSocket,例如 <code>ws://127.0.0.1:36510/</code>。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>会话与环境</h2>
|
||||
<p>影响 Lingma 会话上下文和工具执行环境。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>模式</label>
|
||||
<div class="custom-select" :class="{ open: openSelect === 'Mode' }">
|
||||
<button type="button" @click="toggleSelect('Mode')">
|
||||
<span>{{ selectLabel('Mode') }}</span>
|
||||
<i class="bi bi-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div v-if="openSelect === 'Mode'" class="select-menu">
|
||||
<button
|
||||
v-for="option in selectOptions.Mode"
|
||||
:key="option.value"
|
||||
:class="{ selected: option.value === config.Mode }"
|
||||
type="button"
|
||||
@click="chooseOption('Mode', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Shell 类型</label>
|
||||
<div class="custom-select" :class="{ open: openSelect === 'ShellType' }">
|
||||
<button type="button" @click="toggleSelect('ShellType')">
|
||||
<span>{{ selectLabel('ShellType') }}</span>
|
||||
<i class="bi bi-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div v-if="openSelect === 'ShellType'" class="select-menu">
|
||||
<button
|
||||
v-for="option in selectOptions.ShellType"
|
||||
:key="option.value"
|
||||
:class="{ selected: option.value === config.ShellType }"
|
||||
type="button"
|
||||
@click="chooseOption('ShellType', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>会话策略</label>
|
||||
<div class="custom-select" :class="{ open: openSelect === 'SessionMode' }">
|
||||
<button type="button" @click="toggleSelect('SessionMode')">
|
||||
<span>{{ selectLabel('SessionMode') }}</span>
|
||||
<i class="bi bi-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div v-if="openSelect === 'SessionMode'" class="select-menu">
|
||||
<button
|
||||
v-for="option in selectOptions.SessionMode"
|
||||
:key="option.value"
|
||||
:class="{ selected: option.value === config.SessionMode }"
|
||||
type="button"
|
||||
@click="chooseOption('SessionMode', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>当前文件</label>
|
||||
<input v-model="config.CurrentFilePath" type="text" placeholder="可选" />
|
||||
</div>
|
||||
<div class="field span-2">
|
||||
<label>工作目录</label>
|
||||
<textarea v-model="config.Cwd" placeholder="Lingma 创建 session 时使用的 cwd"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
7
desktop/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type {DefineComponent} from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
30
desktop/frontend/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
desktop/frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
7
desktop/frontend/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()]
|
||||
})
|
||||
36
desktop/frontend/wailsjs/go/main/App.d.ts
vendored
Executable file
@@ -0,0 +1,36 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {service} from '../models';
|
||||
import {main} from '../models';
|
||||
|
||||
export function ClearLogs():Promise<void>;
|
||||
|
||||
export function ClearRequests():Promise<void>;
|
||||
|
||||
export function GetConfig():Promise<service.Config>;
|
||||
|
||||
export function GetModels():Promise<Array<main.ModelInfo>>;
|
||||
|
||||
export function GetRequests():Promise<Array<main.RequestRecord>>;
|
||||
|
||||
export function GetStatus():Promise<main.ProxyStatus>;
|
||||
|
||||
export function HideWindow():Promise<void>;
|
||||
|
||||
export function MinimizeWindow():Promise<void>;
|
||||
|
||||
export function QuitApp():Promise<void>;
|
||||
|
||||
export function RefreshModels():Promise<Array<main.ModelInfo>>;
|
||||
|
||||
export function RequestQuitShortcut():Promise<void>;
|
||||
|
||||
export function SelectModel(arg1:string):Promise<main.ProxyStatus>;
|
||||
|
||||
export function ShowWindow():Promise<void>;
|
||||
|
||||
export function StartProxy():Promise<void>;
|
||||
|
||||
export function StopProxy():Promise<void>;
|
||||
|
||||
export function UpdateConfig(arg1:service.Config):Promise<void>;
|
||||
67
desktop/frontend/wailsjs/go/main/App.js
Executable file
@@ -0,0 +1,67 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function ClearLogs() {
|
||||
return window['go']['main']['App']['ClearLogs']();
|
||||
}
|
||||
|
||||
export function ClearRequests() {
|
||||
return window['go']['main']['App']['ClearRequests']();
|
||||
}
|
||||
|
||||
export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
|
||||
export function GetModels() {
|
||||
return window['go']['main']['App']['GetModels']();
|
||||
}
|
||||
|
||||
export function GetRequests() {
|
||||
return window['go']['main']['App']['GetRequests']();
|
||||
}
|
||||
|
||||
export function GetStatus() {
|
||||
return window['go']['main']['App']['GetStatus']();
|
||||
}
|
||||
|
||||
export function HideWindow() {
|
||||
return window['go']['main']['App']['HideWindow']();
|
||||
}
|
||||
|
||||
export function MinimizeWindow() {
|
||||
return window['go']['main']['App']['MinimizeWindow']();
|
||||
}
|
||||
|
||||
export function QuitApp() {
|
||||
return window['go']['main']['App']['QuitApp']();
|
||||
}
|
||||
|
||||
export function RefreshModels() {
|
||||
return window['go']['main']['App']['RefreshModels']();
|
||||
}
|
||||
|
||||
export function RequestQuitShortcut() {
|
||||
return window['go']['main']['App']['RequestQuitShortcut']();
|
||||
}
|
||||
|
||||
export function SelectModel(arg1) {
|
||||
return window['go']['main']['App']['SelectModel'](arg1);
|
||||
}
|
||||
|
||||
export function ShowWindow() {
|
||||
return window['go']['main']['App']['ShowWindow']();
|
||||
}
|
||||
|
||||
export function StartProxy() {
|
||||
return window['go']['main']['App']['StartProxy']();
|
||||
}
|
||||
|
||||
export function StopProxy() {
|
||||
return window['go']['main']['App']['StopProxy']();
|
||||
}
|
||||
|
||||
export function UpdateConfig(arg1) {
|
||||
return window['go']['main']['App']['UpdateConfig'](arg1);
|
||||
}
|
||||
101
desktop/frontend/wailsjs/go/models.ts
Executable file
@@ -0,0 +1,101 @@
|
||||
export namespace main {
|
||||
|
||||
export class ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ModelInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.name = source["name"];
|
||||
}
|
||||
}
|
||||
export class ProxyStatus {
|
||||
running: boolean;
|
||||
addr: string;
|
||||
models: number;
|
||||
model?: string;
|
||||
startedAt?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ProxyStatus(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.running = source["running"];
|
||||
this.addr = source["addr"];
|
||||
this.models = source["models"];
|
||||
this.model = source["model"];
|
||||
this.startedAt = source["startedAt"];
|
||||
}
|
||||
}
|
||||
export class RequestRecord {
|
||||
time: string;
|
||||
method: string;
|
||||
path: string;
|
||||
statusCode: number;
|
||||
duration: string;
|
||||
reqBody?: string;
|
||||
respBody?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new RequestRecord(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.time = source["time"];
|
||||
this.method = source["method"];
|
||||
this.path = source["path"];
|
||||
this.statusCode = source["statusCode"];
|
||||
this.duration = source["duration"];
|
||||
this.reqBody = source["reqBody"];
|
||||
this.respBody = source["respBody"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace service {
|
||||
|
||||
export class Config {
|
||||
Host: string;
|
||||
Port: number;
|
||||
Transport: string;
|
||||
Pipe: string;
|
||||
WebSocketURL: string;
|
||||
Cwd: string;
|
||||
CurrentFilePath: string;
|
||||
Mode: string;
|
||||
Model: string;
|
||||
ShellType: string;
|
||||
SessionMode: string;
|
||||
Timeout: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Config(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Host = source["Host"];
|
||||
this.Port = source["Port"];
|
||||
this.Transport = source["Transport"];
|
||||
this.Pipe = source["Pipe"];
|
||||
this.WebSocketURL = source["WebSocketURL"];
|
||||
this.Cwd = source["Cwd"];
|
||||
this.CurrentFilePath = source["CurrentFilePath"];
|
||||
this.Mode = source["Mode"];
|
||||
this.Model = source["Model"];
|
||||
this.ShellType = source["ShellType"];
|
||||
this.SessionMode = source["SessionMode"];
|
||||
this.Timeout = source["Timeout"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
24
desktop/frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
330
desktop/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
298
desktop/frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
79
desktop/go.sum
Normal file
@@ -0,0 +1,79 @@
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.6.0 h1:EyH0zR/EO6dDiqNy8qU5spaXDfkluiq77xrkabPYD4c=
|
||||
github.com/wailsapp/wails/v2 v2.6.0/go.mod h1:WBG9KKWuw0FKfoepBrr/vRlyTmHaMibWesK3yz6nNiM=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
100
desktop/main.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
goruntime "runtime"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu/keys"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/mac"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
app := NewApp()
|
||||
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Lingma IPC Proxy",
|
||||
Width: 1100,
|
||||
Height: 750,
|
||||
MinWidth: 900,
|
||||
MinHeight: 600,
|
||||
HideWindowOnClose: true,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 15, G: 23, B: 42, A: 1},
|
||||
Menu: appMenu(app),
|
||||
OnStartup: app.startup,
|
||||
OnBeforeClose: app.beforeClose,
|
||||
OnDomReady: app.onDomReady,
|
||||
SingleInstanceLock: &options.SingleInstanceLock{
|
||||
UniqueId: "lingma-ipc-proxy-desktop",
|
||||
OnSecondInstanceLaunch: app.onSecondInstanceLaunch,
|
||||
},
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
Frameless: false,
|
||||
Mac: &mac.Options{
|
||||
TitleBar: &mac.TitleBar{
|
||||
TitlebarAppearsTransparent: false,
|
||||
HideTitle: false,
|
||||
HideTitleBar: false,
|
||||
FullSizeContent: false,
|
||||
UseToolbar: false,
|
||||
HideToolbarSeparator: true,
|
||||
},
|
||||
About: &mac.AboutInfo{
|
||||
Title: "Lingma IPC Proxy",
|
||||
Message: "A desktop GUI for lingma-ipc-proxy",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func appMenu(app *App) *menu.Menu {
|
||||
quitAccelerator := keys.OptionOrAlt("f4")
|
||||
closeWindowAccelerator := keys.CmdOrCtrl("w")
|
||||
minimizeWindowAccelerator := keys.CmdOrCtrl("m")
|
||||
if goruntime.GOOS == "darwin" {
|
||||
quitAccelerator = keys.CmdOrCtrl("q")
|
||||
closeWindowAccelerator = keys.CmdOrCtrl("w")
|
||||
minimizeWindowAccelerator = keys.CmdOrCtrl("m")
|
||||
}
|
||||
|
||||
appMenu := menu.NewMenu()
|
||||
appMenu.AddText("关闭窗口", closeWindowAccelerator, func(_ *menu.CallbackData) {
|
||||
app.HideWindow()
|
||||
})
|
||||
appMenu.AddText("最小化窗口", minimizeWindowAccelerator, func(_ *menu.CallbackData) {
|
||||
app.MinimizeWindow()
|
||||
})
|
||||
appMenu.AddSeparator()
|
||||
appMenu.AddText("退出 Lingma IPC Proxy", quitAccelerator, func(_ *menu.CallbackData) {
|
||||
app.RequestQuitShortcut()
|
||||
})
|
||||
|
||||
editMenu := menu.NewMenu()
|
||||
editMenu.AddText("撤销", keys.CmdOrCtrl("z"), func(_ *menu.CallbackData) {})
|
||||
editMenu.AddText("重做", keys.CmdOrCtrl("shift+z"), func(_ *menu.CallbackData) {})
|
||||
editMenu.AddSeparator()
|
||||
editMenu.AddText("剪切", keys.CmdOrCtrl("x"), func(_ *menu.CallbackData) {})
|
||||
editMenu.AddText("复制", keys.CmdOrCtrl("c"), func(_ *menu.CallbackData) {})
|
||||
editMenu.AddText("粘贴", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {})
|
||||
editMenu.AddText("全选", keys.CmdOrCtrl("a"), func(_ *menu.CallbackData) {})
|
||||
|
||||
return menu.NewMenuFromItems(
|
||||
menu.SubMenu("Lingma IPC Proxy", appMenu),
|
||||
menu.SubMenu("编辑", editMenu),
|
||||
)
|
||||
}
|
||||
13
desktop/wails.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "Lingma IPC Proxy",
|
||||
"outputfilename": "LingmaProxy",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "lutc5",
|
||||
"email": "lutc5@asiainfo.com"
|
||||
}
|
||||
}
|
||||