Files
picoclaw/web/backend/main.go
T
Cytown e41423483e add systray ui for all platform (#1649)
* add systray ui for all platform

* update from getlantern/systray to fyne.io/systray for fix test
2026-03-17 14:12:32 +08:00

194 lines
5.0 KiB
Go

// PicoClaw Web Console - Web-based chat and management interface
//
// Provides a web UI for chatting with PicoClaw via the Pico Channel WebSocket,
// with configuration management and gateway process control.
//
// Usage:
//
// go build -o picoclaw-web ./web/backend/
// ./picoclaw-web [config.json]
// ./picoclaw-web -public config.json
package main
import (
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"fyne.io/systray"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/web/backend/api"
"github.com/sipeed/picoclaw/web/backend/launcherconfig"
"github.com/sipeed/picoclaw/web/backend/middleware"
"github.com/sipeed/picoclaw/web/backend/utils"
)
const (
appName = "PicoClaw"
)
var (
appVersion = config.Version
server *http.Server
serverAddr string
apiHandler *api.Handler
noBrowser *bool
)
func main() {
port := flag.String("port", "18800", "Port to listen on")
public := flag.Bool("public", false, "Listen on all interfaces (0.0.0.0) instead of localhost only")
noBrowser = flag.Bool("no-browser", false, "Do not auto-open browser on startup")
lang := flag.String("lang", "", "Language: en (English) or zh (Chinese). Default: auto-detect from system locale")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "PicoClaw Launcher - A web-based configuration editor\n\n")
fmt.Fprintf(os.Stderr, "Usage: %s [options] [config.json]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Arguments:\n")
fmt.Fprintf(os.Stderr, " config.json Path to the configuration file (default: ~/.picoclaw/config.json)\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " %s Use default config path\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s ./config.json Specify a config file\n", os.Args[0])
fmt.Fprintf(
os.Stderr,
" %s -public ./config.json Allow access from other devices on the network\n",
os.Args[0],
)
}
flag.Parse()
// Set language from command line or auto-detect
if *lang != "" {
SetLanguage(*lang)
}
// Resolve config path
configPath := utils.GetDefaultConfigPath()
if flag.NArg() > 0 {
configPath = flag.Arg(0)
}
absPath, err := filepath.Abs(configPath)
if err != nil {
log.Fatalf("Failed to resolve config path: %v", err)
}
err = utils.EnsureOnboarded(absPath)
if err != nil {
log.Printf("Warning: Failed to initialize PicoClaw config automatically: %v", err)
}
var explicitPort bool
var explicitPublic bool
flag.Visit(func(f *flag.Flag) {
switch f.Name {
case "port":
explicitPort = true
case "public":
explicitPublic = true
}
})
launcherPath := launcherconfig.PathForAppConfig(absPath)
launcherCfg, err := launcherconfig.Load(launcherPath, launcherconfig.Default())
if err != nil {
log.Printf("Warning: Failed to load %s: %v", launcherPath, err)
launcherCfg = launcherconfig.Default()
}
effectivePort := *port
effectivePublic := *public
if !explicitPort {
effectivePort = strconv.Itoa(launcherCfg.Port)
}
if !explicitPublic {
effectivePublic = launcherCfg.Public
}
portNum, err := strconv.Atoi(effectivePort)
if err != nil || portNum < 1 || portNum > 65535 {
if err == nil {
err = errors.New("must be in range 1-65535")
}
log.Fatalf("Invalid port %q: %v", effectivePort, err)
}
// Determine listen address
var addr string
if effectivePublic {
addr = "0.0.0.0:" + effectivePort
} else {
addr = "127.0.0.1:" + effectivePort
}
// Initialize Server components
mux := http.NewServeMux()
// API Routes (e.g. /api/status)
apiHandler = api.NewHandler(absPath)
apiHandler.SetServerOptions(portNum, effectivePublic, explicitPublic, launcherCfg.AllowedCIDRs)
apiHandler.RegisterRoutes(mux)
// Frontend Embedded Assets
registerEmbedRoutes(mux)
accessControlledMux, err := middleware.IPAllowlist(launcherCfg.AllowedCIDRs, mux)
if err != nil {
log.Fatalf("Invalid allowed CIDR configuration: %v", err)
}
// Apply middleware stack
handler := middleware.Recoverer(
middleware.Logger(
middleware.JSONContentType(accessControlledMux),
),
)
// Print startup banner
fmt.Print(utils.Banner)
fmt.Println()
fmt.Println(" Open the following URL in your browser:")
fmt.Println()
fmt.Printf(" >> http://localhost:%s <<\n", effectivePort)
if effectivePublic {
if ip := utils.GetLocalIP(); ip != "" {
fmt.Printf(" >> http://%s:%s <<\n", ip, effectivePort)
}
}
fmt.Println()
// Set server address for systray
serverAddr = fmt.Sprintf("http://localhost:%s", effectivePort)
// Auto-open browser will be handled by systray onReady
// Auto-start gateway after backend starts listening.
go func() {
time.Sleep(1 * time.Second)
apiHandler.TryAutoStartGateway()
}()
// Start the Server in a goroutine
server = &http.Server{Addr: addr, Handler: handler}
go func() {
log.Printf("Server listening on %s", addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Start system tray
systray.Run(onReady, onExit)
}