Jelajahi Sumber

feat: embed web file (#181)

* feat: embed web file

* fix: need wait web release done

* fix: ci lint

* fix: ci lint
zijiren 7 bulan lalu
induk
melakukan
7df27c8ee7

+ 30 - 1
.github/workflows/release.yml

@@ -23,9 +23,33 @@ env:
   ALIYUN_REPO: ${{ secrets.ALIYUN_REPO != '' && secrets.ALIYUN_REPO || secrets.ALIYUN_USERNAME != '' && format('{0}/{1}/{2}', secrets.ALIYUN_REGISTRY, secrets.ALIYUN_USERNAME, 'aiproxy') || '' }}
 
 jobs:
+  release-web:
+    name: Release Web
+    runs-on: ubuntu-24.04
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Use Node.js 22
+        uses: actions/setup-node@v4
+        with:
+          node-version: 22.x
+
+      - name: Build
+        working-directory: web
+        run: |
+          npm install
+          npm run build
+
+      - name: Upload Artifact
+        uses: actions/upload-artifact@v4
+        with:
+          name: web
+          path: web/dist
+
   release:
     name: Release AI Proxy
     runs-on: ubuntu-24.04
+    needs: release-web
     permissions:
       contents: write
     strategy:
@@ -50,6 +74,11 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v4
 
+      - uses: actions/download-artifact@v4
+        with:
+          name: web
+          path: core/public/dist
+
       - name: Download tiktoken
         working-directory: core
         run: |
@@ -58,7 +87,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v5
         with:
-          go-version: "1.23"
+          go-version: "1.24"
 
       - name: Generate Swagger
         working-directory: core

+ 16 - 15
Dockerfile

@@ -1,3 +1,16 @@
+# Frontend build stage
+FROM node:22-alpine AS frontend-builder
+
+WORKDIR /aiproxy/web
+
+COPY ./web/ ./
+
+# Install pnpm globally
+RUN npm install -g pnpm
+
+# Install dependencies and build with pnpm
+RUN pnpm install && pnpm run build
+
 FROM golang:1.24-alpine AS builder
 
 RUN apk add --no-cache curl
@@ -6,6 +19,9 @@ WORKDIR /aiproxy/core
 
 COPY ./ /aiproxy
 
+# Copy frontend dist files
+COPY --from=frontend-builder /aiproxy/web/dist /aiproxy/core/web/dist
+
 RUN sh scripts/tiktoken.sh
 
 RUN go install github.com/swaggo/swag/cmd/swag@latest
@@ -14,19 +30,6 @@ RUN sh scripts/swag.sh
 
 RUN go build -trimpath -tags "jsoniter" -ldflags "-s -w" -o aiproxy
 
-# Frontend build stage
-FROM node:23-alpine AS frontend-builder
-
-WORKDIR /aiproxy/web
-
-COPY ./web/ ./
-
-# Install pnpm globally
-RUN npm install -g pnpm
-
-# Install dependencies and build with pnpm
-RUN pnpm install && pnpm run build
-
 FROM alpine:latest
 
 RUN mkdir -p /aiproxy
@@ -39,8 +42,6 @@ RUN apk add --no-cache ca-certificates tzdata ffmpeg curl && \
     rm -rf /var/cache/apk/*
 
 COPY --from=builder /aiproxy/core/aiproxy /usr/local/bin/aiproxy
-# Copy frontend dist files
-COPY --from=frontend-builder /aiproxy/web/dist/ ./web/dist/
 
 ENV PUID=0 PGID=0 UMASK=022
 

+ 1 - 0
core/.gitignore

@@ -1,4 +1,5 @@
 aiproxy.db*
 aiproxy
 common/tiktoken/assets/*
+/public/dist/*
 !*.gitkeep

+ 2 - 0
core/common/config/config.go

@@ -18,6 +18,8 @@ var (
 var (
 	DisableAutoMigrateDB = env.Bool("DISABLE_AUTO_MIGRATE_DB", false)
 	AdminKey             = os.Getenv("ADMIN_KEY")
+	WebPath              = os.Getenv("WEB_PATH")
+	DisableWeb           = env.Bool("DISABLE_WEB", false)
 	FfmpegEnabled        = env.Bool("FFMPEG_ENABLED", false)
 )
 

+ 0 - 0
core/public/dist/.gitkeep


+ 11 - 0
core/public/public.go

@@ -0,0 +1,11 @@
+package public
+
+import (
+	"embed"
+	"io/fs"
+)
+
+//go:embed all:dist
+var dist embed.FS
+
+var Public, _ = fs.Sub(dist, "dist")

+ 0 - 5
core/router/main.go

@@ -1,15 +1,10 @@
 package router
 
 import (
-	"net/http"
-
 	"github.com/gin-gonic/gin"
 )
 
 func SetRouter(router *gin.Engine) {
-	router.GET("/ok", func(c *gin.Context) {
-		c.String(http.StatusOK, "AI Proxy is running!")
-	})
 	SetAPIRouter(router)
 	SetRelayRouter(router)
 	SetMCPRouter(router)

+ 87 - 15
core/router/static.go

@@ -1,32 +1,104 @@
 package router
 
 import (
+	"io/fs"
 	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
 	"strings"
 
 	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/common/config"
+	"github.com/labring/aiproxy/core/public"
+	"github.com/sirupsen/logrus"
 )
 
-// SetStaticFileRouter configures routes to serve frontend static files
 func SetStaticFileRouter(router *gin.Engine) {
-	// Serve static assets
-	router.Static("/assets", "./web/dist/assets")
+	if config.DisableWeb {
+		return
+	}
+	if config.WebPath == "" {
+		err := initFSRouter(router, public.Public.(fs.ReadDirFS), ".")
+		if err != nil {
+			panic(err)
+		}
+		fs := http.FS(public.Public)
+		router.NoRoute(newIndexNoRouteHandler(fs))
+	} else {
+		absPath, err := filepath.Abs(config.WebPath)
+		if err != nil {
+			panic(err)
+		}
+		logrus.Infof("frontend file path: %s", absPath)
+		err = initFSRouter(router, os.DirFS(absPath).(fs.ReadDirFS), ".")
+		if err != nil {
+			panic(err)
+		}
+		router.NoRoute(newDynamicNoRouteHandler(http.Dir(absPath)))
+	}
+}
 
-	// Serve localization files
-	router.Static("/locales", "./web/dist/locales")
+func checkNoRouteNotFound(path string) bool {
+	if strings.HasPrefix(path, "/api") ||
+		strings.HasPrefix(path, "/mcp") ||
+		strings.HasPrefix(path, "/v1") {
+		return true
+	}
+	return false
+}
 
-	// Serve other static files
-	router.StaticFile("/logo.svg", "./web/dist/logo.svg")
+func newIndexNoRouteHandler(fs http.FileSystem) func(ctx *gin.Context) {
+	return func(ctx *gin.Context) {
+		if checkNoRouteNotFound(ctx.Request.URL.Path) {
+			ctx.String(http.StatusNotFound, "404 page not found")
+			return
+		}
+		ctx.FileFromFS("", fs)
+	}
+}
 
-	// Handle non-API routes, returning the frontend entry point
-	router.NoRoute(func(c *gin.Context) {
-		// Return 404 for API requests
-		if strings.HasPrefix(c.Request.URL.Path, "/api") {
-			c.JSON(http.StatusNotFound, gin.H{"error": "API route not found"})
+func newDynamicNoRouteHandler(fs http.FileSystem) func(ctx *gin.Context) {
+	fileServer := http.StripPrefix("/", http.FileServer(fs))
+	return func(c *gin.Context) {
+		if checkNoRouteNotFound(c.Request.URL.Path) {
+			c.String(http.StatusNotFound, "404 page not found")
 			return
 		}
 
-		// Return the frontend entry file for all other routes
-		c.File("./web/dist/index.html")
-	})
+		f, err := fs.Open(c.Request.URL.Path)
+		if err != nil {
+			c.FileFromFS("", fs)
+			return
+		}
+		f.Close()
+
+		fileServer.ServeHTTP(c.Writer, c.Request)
+	}
+}
+
+type staticFileFS interface {
+	StaticFileFS(relativePath string, filepath string, fs http.FileSystem) gin.IRoutes
+}
+
+func initFSRouter(e staticFileFS, f fs.ReadDirFS, path string) error {
+	dirs, err := f.ReadDir(path)
+	if err != nil {
+		return err
+	}
+	for _, dir := range dirs {
+		u, err := url.JoinPath(path, dir.Name())
+		if err != nil {
+			return err
+		}
+		if dir.IsDir() {
+			err = initFSRouter(e, f, u)
+			if err != nil {
+				return err
+			}
+		} else {
+			e.StaticFileFS(u, u, http.FS(f))
+		}
+	}
+	return nil
 }