package main import ( crypto_tls "crypto/tls" "embed" "fmt" "io/fs" "log/slog" "os" "os/signal" "syscall" "github.com/imbytecat/voicepaste/internal/asr" "github.com/imbytecat/voicepaste/internal/config" "github.com/imbytecat/voicepaste/internal/paste" "github.com/imbytecat/voicepaste/internal/server" vpTLS "github.com/imbytecat/voicepaste/internal/tls" "github.com/imbytecat/voicepaste/internal/ws" ) //go:embed all:web var webFS embed.FS var version = "dev" func main() { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: slog.LevelInfo, }))) slog.Info("VoicePaste", "version", version) // Load config cfg, err := config.Load("") if err != nil { slog.Error("failed to load config", "error", err) os.Exit(1) } // Start config hot-reload watcher config.WatchAndReload("") // Initialize clipboard if err := paste.Init(); err != nil { slog.Warn("clipboard init failed, paste will be unavailable", "err", err) } // Detect LAN IPs lanIPs, err := server.GetLANIPs() if err != nil { slog.Error("failed to detect LAN IP", "error", err) os.Exit(1) } lanIP := lanIPs[0] // Use first IP for TLS and server binding // Read token from config (empty = no auth required) token := cfg.Security.Token // TLS setup var tlsResult *vpTLS.Result scheme := "http" host := lanIP if cfg.Server.TLSAuto { var err error tlsResult, err = vpTLS.GetTLSConfig(lanIP) if err != nil { slog.Error("TLS setup failed", "error", err) os.Exit(1) } scheme = "https" host = tlsResult.Host } // Print connection info fmt.Println() fmt.Println("╔══════════════════════════════════════╗") fmt.Println("║ VoicePaste 就绪 ║") fmt.Println("╚══════════════════════════════════════╝") fmt.Println() // Print all accessible addresses if len(lanIPs) == 1 { fmt.Printf(" 地址: %s\n", buildURL(scheme, host, cfg.Server.Port, token)) } else { fmt.Println(" 地址:") for _, ip := range lanIPs { h := ip if tlsResult != nil && tlsResult.AnyIP { h = vpTLS.AnyIPHost(ip) } fmt.Printf(" - %s\n", buildURL(scheme, h, cfg.Server.Port, token)) } } if tlsResult != nil && tlsResult.AnyIP { fmt.Println(" 证书: AnyIP(浏览器信任)") } else if cfg.Server.TLSAuto { fmt.Println(" 证书: 自签名(浏览器会警告)") } if token != "" { fmt.Println(" 认证: 已启用") } else { fmt.Println(" 认证: 未启用(无需 token)") } fmt.Println() fmt.Println(" 在手机浏览器中打开上方地址") fmt.Println(" 按 Ctrl+C 停止服务") fmt.Println() // Create and start server webContent, _ := fs.Sub(webFS, "web") var serverTLSCfg *crypto_tls.Config if tlsResult != nil { serverTLSCfg = tlsResult.Config } srv := server.New(token, lanIP, webContent, serverTLSCfg) // Build ASR factory from config asrCfg := asr.Config{ AppKey: cfg.Doubao.AppKey, AccessKey: cfg.Doubao.AccessKey, ResourceID: cfg.Doubao.ResourceID, } asrFactory := func(resultCh chan<- ws.ServerMsg) (func([]byte), func(), error) { client, err := asr.Dial(asrCfg, resultCh) if err != nil { return nil, nil, err } sendAudio := func(pcm []byte) { if err := client.SendAudio(pcm, false); err != nil { slog.Warn("send audio to asr", "err", err) } } cleanup := func() { client.Finish() } return sendAudio, cleanup, nil } // Register WebSocket handler wsHandler := ws.NewHandler(token, paste.Paste, asrFactory) wsHandler.Register(srv.App()) // Graceful shutdown go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh slog.Info("shutting down...") srv.Shutdown() }() if err := srv.Start(); err != nil { slog.Error("server error", "error", err) os.Exit(1) } } func buildURL(scheme, host string, port int, token string) string { if token != "" { return fmt.Sprintf("%s://%s:%d/?token=%s", scheme, host, port, token) } return fmt.Sprintf("%s://%s:%d/", scheme, host, port) }