feat(server): integrate web spa static assets hosting and route fallback
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -91,9 +94,56 @@ func (s *Server) Routes() http.Handler {
|
||||
mux.HandleFunc("POST /api/v1/docgov/comments", s.handleDocGovAddComment)
|
||||
mux.HandleFunc("GET /api/v1/docgov/audit-logs", s.handleDocGovListAuditLogs)
|
||||
|
||||
// 9. 现代 SPA 前端页面与静态资产托管
|
||||
mux.HandleFunc("GET /", s.serveSPA)
|
||||
|
||||
return s.corsMiddleware(mux)
|
||||
}
|
||||
|
||||
// serveSPA 托管 web/dist 静态产物并提供前端路由自动回退
|
||||
func (s *Server) serveSPA(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
"web/dist",
|
||||
"../web/dist",
|
||||
"../../web/dist",
|
||||
filepath.Join(s.cfg.DataDir, "web/dist"),
|
||||
filepath.Join(s.cfg.DataDir, "../web/dist"),
|
||||
}
|
||||
|
||||
var distDir string
|
||||
for _, dir := range candidates {
|
||||
if fi, err := os.Stat(filepath.Join(dir, "index.html")); err == nil && !fi.IsDir() {
|
||||
distDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if distDir == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
reqPath := filepath.Clean(r.URL.Path)
|
||||
if reqPath == "/" {
|
||||
http.ServeFile(w, r, filepath.Join(distDir, "index.html"))
|
||||
return
|
||||
}
|
||||
|
||||
targetFile := filepath.Join(distDir, reqPath)
|
||||
if fi, err := os.Stat(targetFile); err == nil && !fi.IsDir() {
|
||||
http.ServeFile(w, r, targetFile)
|
||||
return
|
||||
}
|
||||
|
||||
// SPA Fallback 默认回退 index.html
|
||||
http.ServeFile(w, r, filepath.Join(distDir, "index.html"))
|
||||
}
|
||||
|
||||
// Start 启动 HTTP 守护进程
|
||||
func (s *Server) Start() error {
|
||||
addr := fmt.Sprintf("%s:%d", s.cfg.HTTPHost, s.cfg.HTTPPort)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/config"
|
||||
)
|
||||
|
||||
func TestServer_WebAssetsHostingAndFallback(t *testing.T) {
|
||||
cfg, err := config.Load("")
|
||||
if err != nil {
|
||||
t.Fatalf("加载配置失败: %v", err)
|
||||
}
|
||||
cfg.HTTPPort = 8089
|
||||
|
||||
srv := NewServer(cfg, nil)
|
||||
handler := srv.Routes()
|
||||
|
||||
// 1. 测试访问根路径 "/" 应返回前端 SPA index.html
|
||||
reqRoot := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
wRoot := httptest.NewRecorder()
|
||||
handler.ServeHTTP(wRoot, reqRoot)
|
||||
|
||||
if wRoot.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200 for root SPA, got %d", wRoot.Code)
|
||||
}
|
||||
bodyStr := wRoot.Body.String()
|
||||
if bodyStr == "" {
|
||||
t.Errorf("expected non-empty SPA body")
|
||||
}
|
||||
|
||||
// 2. 测试 SPA 客户端路由回退 (如访问 /sessions/sess-001 应返回 index.html 而不是 404)
|
||||
reqClientRoute := httptest.NewRequest(http.MethodGet, "/sessions/sess-001", nil)
|
||||
wClient := httptest.NewRecorder()
|
||||
handler.ServeHTTP(wClient, reqClientRoute)
|
||||
|
||||
if wClient.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200 for client route fallback, got %d", wClient.Code)
|
||||
}
|
||||
|
||||
// 3. 测试 API 路由不受静态路由影响 (GET /api/v1/chat/sessions 依然正常工作)
|
||||
reqAPI := httptest.NewRequest(http.MethodGet, "/api/v1/chat/sessions", nil)
|
||||
wAPI := httptest.NewRecorder()
|
||||
handler.ServeHTTP(wAPI, reqAPI)
|
||||
|
||||
if wAPI.Code != http.StatusOK {
|
||||
t.Errorf("expected status 200 for /api/v1/chat/sessions, got %d", wAPI.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user