refactor(plugins): restructure admin and message_gateway into standard layered sub-packages

This commit is contained in:
ryan
2026-08-28 22:33:26 +08:00
parent 85b383a4e0
commit f4975d6732
128 changed files with 12050 additions and 9901 deletions
@@ -0,0 +1,92 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package driver_http
import (
"io"
"io/fs"
"net/http"
"path"
"strings"
"github.com/gin-gonic/gin"
)
const indexFile = "index.html"
// serverOwnedPrefixes are backend-owned namespaces. A miss there must keep Gin's default
// 404 instead of silently returning the frontend shell, which would mask broken API links.
var serverOwnedPrefixes = []string{"/api/", "/f/"}
// registerFrontend mounts assets as the NoRoute fallback so client-side routes resolve.
// It is a no-op when assets is nil, i.e. the binary was built without the embed_frontend tag.
func registerFrontend(engine *gin.Engine, assets fs.FS) {
if assets == nil {
return
}
engine.NoRoute(func(c *gin.Context) {
if isServerOwned(c.Request.URL.Path) {
return
}
if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead {
c.JSON(http.StatusMethodNotAllowed, gin.H{"error_msg": "Method not allowed"})
return
}
for _, candidate := range assetCandidates(c.Request.URL.Path) {
if serveAsset(c, assets, candidate) {
return
}
}
})
}
func isServerOwned(urlPath string) bool {
for _, prefix := range serverOwnedPrefixes {
if strings.HasPrefix(urlPath, prefix) {
return true
}
}
return false
}
// assetCandidates maps a request path onto the static export layout: exact file,
// Next.js clean-URL siblings, then the single-page-app shell.
func assetCandidates(urlPath string) []string {
clean := strings.TrimPrefix(path.Join("/", urlPath), "/")
if clean == "" || clean == "." {
return []string{indexFile}
}
candidates := []string{clean}
if !strings.Contains(clean, ".") {
candidates = append(candidates, clean+".html", path.Join(clean, indexFile))
}
return append(candidates, indexFile)
}
// serveAsset writes name when it exists as a regular file, reporting whether it served.
func serveAsset(c *gin.Context, assets fs.FS, name string) bool {
file, err := assets.Open(name)
if err != nil {
return false
}
defer func() { _ = file.Close() }()
info, err := file.Stat()
if err != nil || info.IsDir() {
return false
}
seeker, ok := file.(io.ReadSeeker)
if !ok {
return false
}
http.ServeContent(c.Writer, c.Request, name, info.ModTime(), seeker)
return true
}
@@ -0,0 +1,12 @@
//go:build !embed_frontend
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package driver_http
import "io/fs"
// frontendAssets returns nil: this binary was built without the embedded frontend bundle,
// so unmatched routes keep Gin's default 404 and the frontend is served separately.
func frontendAssets() fs.FS { return nil }
@@ -0,0 +1,24 @@
//go:build embed_frontend
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package driver_http
import (
"embed"
"io/fs"
)
//go:embed all:dist
var frontendFS embed.FS
// frontendAssets returns the exported Next.js bundle shipped inside this binary.
func frontendAssets() fs.FS {
sub, err := fs.Sub(frontendFS, "dist")
if err != nil {
panic("driver_http: embedded frontend bundle unavailable: " + err.Error())
}
return sub
}
@@ -162,6 +162,8 @@ func (p *Plugin) Start(ctx context.Context) error {
}
}
registerFrontend(p.engine, frontendAssets())
p.server = &http.Server{
Addr: p.addr,
Handler: p.engine,