forked from niuniu/llm-intelligence
fix deployment and frontend build regressions
This commit is contained in:
@@ -7,6 +7,9 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
@@ -75,7 +78,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
mux := newMux(db, fetchModels, fetchSubscriptionPlans)
|
||||
mux := newMux(db, fetchModels, fetchSubscriptionPlans, resolveFrontendDistDir())
|
||||
|
||||
log.Printf("server listening on :%s", addr)
|
||||
if err := http.ListenAndServe(":"+addr, mux); err != nil {
|
||||
@@ -83,7 +86,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func newMux(db *sql.DB, fetchModelsFn modelFetcher, fetchPlansFn subscriptionPlanFetcher) *http.ServeMux {
|
||||
func newMux(db *sql.DB, fetchModelsFn modelFetcher, fetchPlansFn subscriptionPlanFetcher, frontendDistDir string) *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
if db == nil {
|
||||
@@ -122,9 +125,65 @@ func newMux(db *sql.DB, fetchModelsFn modelFetcher, fetchPlansFn subscriptionPla
|
||||
}
|
||||
writeJSON(w, http.StatusOK, apiEnvelope{Data: plans})
|
||||
})
|
||||
if frontendDistDir != "" {
|
||||
mux.Handle("/", frontendHandler(frontendDistDir))
|
||||
}
|
||||
return mux
|
||||
}
|
||||
|
||||
func resolveFrontendDistDir() string {
|
||||
candidates := []string{}
|
||||
if custom := os.Getenv("FRONTEND_DIST_DIR"); custom != "" {
|
||||
candidates = append(candidates, custom)
|
||||
}
|
||||
|
||||
candidates = append(candidates,
|
||||
filepath.Join("frontend", "dist"),
|
||||
filepath.Join(filepath.Dir(os.Args[0]), "frontend", "dist"),
|
||||
)
|
||||
|
||||
for _, candidate := range candidates {
|
||||
indexPath := filepath.Join(candidate, "index.html")
|
||||
info, err := os.Stat(indexPath)
|
||||
if err == nil && !info.IsDir() {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func frontendHandler(frontendDistDir string) http.Handler {
|
||||
indexPath := filepath.Join(frontendDistDir, "index.html")
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
cleanPath := path.Clean("/" + r.URL.Path)
|
||||
if cleanPath == "/" {
|
||||
http.ServeFile(w, r, indexPath)
|
||||
return
|
||||
}
|
||||
|
||||
relativePath := strings.TrimPrefix(cleanPath, "/")
|
||||
assetPath := filepath.Join(frontendDistDir, filepath.FromSlash(relativePath))
|
||||
if info, err := os.Stat(assetPath); err == nil && !info.IsDir() {
|
||||
http.ServeFile(w, r, assetPath)
|
||||
return
|
||||
}
|
||||
|
||||
if filepath.Ext(relativePath) != "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFile(w, r, indexPath)
|
||||
})
|
||||
}
|
||||
|
||||
func fetchModels(ctx context.Context, db *sql.DB) ([]modelResponse, error) {
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
WITH latest_prices AS (
|
||||
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -20,7 +23,7 @@ func TestSubscriptionPlansHandlerReturnsEnvelope(t *testing.T) {
|
||||
{
|
||||
PlanFamily: "token_plan",
|
||||
PlanCode: "token-plan-lite",
|
||||
PlanName: "通用 Token Plan Lite",
|
||||
PlanName: "General Token Plan Lite",
|
||||
Tier: "Lite",
|
||||
Provider: "Tencent",
|
||||
ProviderCN: "腾讯",
|
||||
@@ -38,6 +41,7 @@ func TestSubscriptionPlansHandlerReturnsEnvelope(t *testing.T) {
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
"",
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/subscription-plans", nil)
|
||||
@@ -76,3 +80,89 @@ func TestSubscriptionPlansHandlerReturnsEnvelope(t *testing.T) {
|
||||
t.Fatalf("unexpected model scope length: %d", len(got.ModelScope))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontendHandlerServesIndexAssetsAndSpaFallback(t *testing.T) {
|
||||
distDir := t.TempDir()
|
||||
writeTestFile(t, filepath.Join(distDir, "index.html"), "<html>dashboard</html>")
|
||||
writeTestFile(t, filepath.Join(distDir, "assets", "app.js"), "console.log('ok');")
|
||||
|
||||
mux := newMux(&sql.DB{}, noOpModelsFetcher, noOpPlansFetcher, distDir)
|
||||
|
||||
t.Run("root serves index", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "dashboard") {
|
||||
t.Fatalf("expected index response, got %q", rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("asset serves file", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/assets/app.js", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "console.log") {
|
||||
t.Fatalf("expected asset response, got %q", rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("spa route falls back to index", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/explorer/detail", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "dashboard") {
|
||||
t.Fatalf("expected SPA fallback, got %q", rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing asset returns not found", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/assets/missing.js", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected status 404, got %d", rec.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("api routes keep precedence", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/models", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func noOpModelsFetcher(context.Context, *sql.DB) ([]modelResponse, error) {
|
||||
return []modelResponse{}, nil
|
||||
}
|
||||
|
||||
func noOpPlansFetcher(context.Context, *sql.DB) ([]subscriptionPlanResponse, error) {
|
||||
return []subscriptionPlanResponse{}, nil
|
||||
}
|
||||
|
||||
func writeTestFile(t *testing.T, path string, contents string) {
|
||||
t.Helper()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", path, err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
||||
t.Fatalf("write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user