package gateway import ( "net/http" "path" "strings" ) // spa 用 SPA 文件服务器包裹 API mux。/api/ 与 /ws/ // 之下的请求由 API handler 服务;其他所有路径都由已配置的 // 前端文件系统服务,并回退到 index.html,使客户端路由(例如 // /chat)能在单页应用中解析。 func (s *Server) spa(api http.Handler) http.Handler { fileServer := s.spaFileServer() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if isAPIRoute(r.URL.Path) || s.isWebhookRoute(r.URL.Path) { api.ServeHTTP(w, r) return } fileServer.ServeHTTP(w, r) }) } // isWebhookRoute 报告 path 是否是已注册的外部 webhook 回调 // 路由。这类路径必须抵达 API mux(webhook handler 挂载之处) // 而不是 SPA 文件回退,使入站回调得到服务。 func (s *Server) isWebhookRoute(p string) bool { if len(s.webhooks) == 0 { return false } _, ok := s.webhooks[p] return ok } // spaFileServer 提供静态资产,对不是内嵌/外部 dist 中真实 // 文件的路径回退到 index.html。 func (s *Server) spaFileServer() http.Handler { fsrv := http.FileServer(http.FS(s.frontend)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rel := strings.TrimPrefix(path.Clean(r.URL.Path), "/") if rel != "" { if f, err := s.frontend.Open(rel); err == nil { _ = f.Close() fsrv.ServeHTTP(w, r) return } } r2 := r.Clone(r.Context()) r2.URL.Path = "/" fsrv.ServeHTTP(w, r2) }) } // isAPIRoute 报告 path 是否属于网关拥有的 REST/WS API 表面, // SPA handler 必须对之不作干预。 func isAPIRoute(p string) bool { return strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/ws/") }