mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
dea06c391c
* Improve the web launcher and gateway integration across backend and frontend. - add runtime model availability checks for local and OAuth-backed models - support launcher-driven gateway host overrides and websocket URL resolution - add gateway log clearing and keep incremental log sync consistent after resets - migrate session history APIs to JSONL metadata-backed storage with legacy fallback - expose session titles and improve chat history loading and error handling - move shared backend runtime helpers into the web utils package - avoid blocking web startup when automatic onboard initialization fails - add backend tests covering gateway readiness, host resolution, models, logs, and sessions * feat(agent): add skills and tools management APIs and UI - add backend APIs to list, view, import, and delete skills - add tool status and toggle endpoints with dependency-aware config updates - add agent skills/tools pages, routes, sidebar entries, and i18n strings - add backend tests for the new skills and tools flows * chore(frontend): upgrade shadcn to 4.0.5 and refresh lockfile * chore(web): keep backend dist placeholder tracked
170 lines
4.5 KiB
Go
170 lines
4.5 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"
|
|
|
|
"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"
|
|
)
|
|
|
|
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")
|
|
|
|
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()
|
|
|
|
// 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()
|
|
|
|
// Auto-open browser
|
|
if !*noBrowser {
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond)
|
|
url := "http://localhost:" + effectivePort
|
|
if err := utils.OpenBrowser(url); err != nil {
|
|
log.Printf("Warning: Failed to auto-open browser: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Auto-start gateway after backend starts listening.
|
|
go func() {
|
|
time.Sleep(1 * time.Second)
|
|
apiHandler.TryAutoStartGateway()
|
|
}()
|
|
|
|
// Start the Server
|
|
if err := http.ListenAndServe(addr, handler); err != nil {
|
|
log.Fatalf("Server failed to start: %v", err)
|
|
}
|
|
}
|