// 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/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() // Share the local URL with the launcher runtime. serverAddr = fmt.Sprintf("http://localhost:%s", effectivePort) // Auto-open browser will be handled by the launcher runtime. // 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) } }() runTray() }