- 新增 ASRConfig,热词从 doubao 提升为 provider 无关配置 - 移除 SecurityConfig,token 移入 ServerConfig - 移除 tls_auto 配置项,TLS 始终启用(getUserMedia 要求 HTTPS) - validate() 改为基于 provider 白名单验证,增加 resource_id 校验 - 简化 main.go:移除 scheme 变量和 HTTP 降级分支 - 更新 config.example.yaml 为新结构并修正环境变量前缀
193 lines
4.8 KiB
Go
193 lines
4.8 KiB
Go
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/dist
|
||
var webFS embed.FS
|
||
var version = "dev"
|
||
|
||
func main() {
|
||
initLogger()
|
||
slog.Info("VoicePaste", "version", version)
|
||
cfg := mustLoadConfig()
|
||
stopWatch := config.WatchAndReload("")
|
||
defer func() {
|
||
if stopWatch != nil {
|
||
stopWatch()
|
||
}
|
||
}()
|
||
initClipboard()
|
||
lanIPs := mustDetectLANIPs()
|
||
lanIP := lanIPs[0]
|
||
tlsResult := mustSetupTLS(lanIP)
|
||
printBanner(cfg, tlsResult, lanIPs)
|
||
srv := createServer(cfg, lanIP, tlsResult)
|
||
runWithGracefulShutdown(srv)
|
||
}
|
||
|
||
func initLogger() {
|
||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||
Level: slog.LevelInfo,
|
||
})))
|
||
}
|
||
|
||
func mustLoadConfig() config.Config {
|
||
cfg, err := config.Load("")
|
||
if err != nil {
|
||
slog.Error("failed to load config", "error", err)
|
||
os.Exit(1)
|
||
}
|
||
return cfg
|
||
}
|
||
|
||
func initClipboard() {
|
||
if err := paste.Init(); err != nil {
|
||
slog.Warn("clipboard init failed, paste will be unavailable", "err", err)
|
||
}
|
||
}
|
||
|
||
func mustDetectLANIPs() []string {
|
||
lanIPs, err := server.GetLANIPs()
|
||
if err != nil {
|
||
slog.Error("failed to detect LAN IP", "error", err)
|
||
os.Exit(1)
|
||
}
|
||
return lanIPs
|
||
}
|
||
|
||
func mustSetupTLS(lanIP string) *vpTLS.Result {
|
||
tlsResult, err := vpTLS.GetTLSConfig(lanIP)
|
||
if err != nil {
|
||
slog.Error("TLS setup failed", "error", err)
|
||
os.Exit(1)
|
||
}
|
||
return tlsResult
|
||
}
|
||
|
||
func printBanner(cfg config.Config, tlsResult *vpTLS.Result, lanIPs []string) {
|
||
fmt.Println()
|
||
fmt.Println("╔══════════════════════════════════════╗")
|
||
fmt.Println("║ VoicePaste 就绪 ║")
|
||
fmt.Println("╚══════════════════════════════════════╝")
|
||
fmt.Println()
|
||
printAddresses(cfg, tlsResult, lanIPs)
|
||
printCertInfo(tlsResult)
|
||
printAuthInfo(cfg.Server.Token)
|
||
fmt.Println()
|
||
fmt.Println(" 在手机浏览器中打开上方地址")
|
||
fmt.Println(" 按 Ctrl+C 停止服务")
|
||
fmt.Println()
|
||
}
|
||
|
||
func printAddresses(cfg config.Config, tlsResult *vpTLS.Result, lanIPs []string) {
|
||
token := cfg.Server.Token
|
||
if len(lanIPs) == 1 {
|
||
host := lanIPHost(tlsResult, lanIPs[0])
|
||
fmt.Printf(" 地址: %s\n", buildURL(host, cfg.Server.Port, token))
|
||
return
|
||
}
|
||
fmt.Println(" 地址:")
|
||
for _, ip := range lanIPs {
|
||
host := lanIPHost(tlsResult, ip)
|
||
fmt.Printf(" - %s\n", buildURL(host, cfg.Server.Port, token))
|
||
}
|
||
}
|
||
|
||
func lanIPHost(tlsResult *vpTLS.Result, ip string) string {
|
||
if tlsResult != nil {
|
||
return vpTLS.AnyIPHost(ip)
|
||
}
|
||
return ip
|
||
}
|
||
|
||
func printCertInfo(tlsResult *vpTLS.Result) {
|
||
if tlsResult != nil {
|
||
fmt.Println(" 证书: AnyIP(浏览器信任)")
|
||
} else {
|
||
fmt.Println(" 证书: 获取失败")
|
||
}
|
||
}
|
||
|
||
func printAuthInfo(token string) {
|
||
if token != "" {
|
||
fmt.Println(" 认证: 已启用")
|
||
} else {
|
||
fmt.Println(" 认证: 未启用(无需 token)")
|
||
}
|
||
}
|
||
|
||
func createServer(cfg config.Config, lanIP string, tlsResult *vpTLS.Result) *server.Server {
|
||
webContent, _ := fs.Sub(webFS, "web/dist")
|
||
var tlsConfig *crypto_tls.Config
|
||
if tlsResult != nil {
|
||
tlsConfig = tlsResult.Config
|
||
}
|
||
srv := server.New(cfg.Server.Token, lanIP, webContent, tlsConfig)
|
||
asrFactory := buildASRFactory()
|
||
wsHandler := ws.NewHandler(cfg.Server.Token, paste.Paste, asrFactory)
|
||
wsHandler.Register(srv.App())
|
||
return srv
|
||
}
|
||
|
||
func buildASRFactory() func(chan<- ws.ServerMsg) (func([]byte), func(), error) {
|
||
return func(resultCh chan<- ws.ServerMsg) (func([]byte), func(), error) {
|
||
cfg := config.Get()
|
||
asrCfg := asr.Config{
|
||
AppID: cfg.Doubao.AppID,
|
||
AccessToken: cfg.Doubao.AccessToken,
|
||
ResourceID: cfg.Doubao.ResourceID,
|
||
Hotwords: cfg.ASR.Hotwords,
|
||
}
|
||
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
|
||
}
|
||
}
|
||
|
||
func runWithGracefulShutdown(srv *server.Server) {
|
||
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(host string, port int, token string) string {
|
||
if token != "" {
|
||
return fmt.Sprintf("https://%s:%d/?token=%s", host, port, token)
|
||
}
|
||
return fmt.Sprintf("https://%s:%d/", host, port)
|
||
}
|