M3 收尾: 限流/配额 + 前端骨架屏 + 单端口托管前端
- ratelimit(内存计数): 密钥级每日请求数/Token 配额、用户级每秒速率 (OT_RATELIMIT_USER_RPS), 超限返回 429 - 网关 Auth 前置配额/限流检查, finishUsage 累计密钥 token 用量 - 前端 Skeleton 组件 + Dashboard/管理总览加载态 - Go 服务托管 web/dist 静态资源(SPA 回退), 单端口即可访问前后端 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/openteam/server/internal/api/middleware"
|
||||
@@ -29,15 +31,26 @@ func NewRouter(a *app.App, gw *proxy.Gateway) *gin.Engine {
|
||||
proxyGroup.Any("/messages", gw.Auth, gw.Handle)
|
||||
proxyGroup.Any("/models", gw.Auth, gw.Handle)
|
||||
}
|
||||
// 未匹配的 /v1/* 返回 OpenAI 风格 404(需先认证)
|
||||
// 静态资源(前端构建产物,存在时托管)
|
||||
const dist = "web/dist"
|
||||
if _, err := os.Stat(dist); err == nil {
|
||||
r.Static("/assets", dist+"/assets")
|
||||
r.StaticFile("/favicon.svg", dist+"/favicon.svg")
|
||||
}
|
||||
|
||||
// 未匹配路由:/v1/* 走代理鉴权;其余回退 SPA 或 404
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
if len(c.Request.URL.Path) >= 3 && c.Request.URL.Path[:3] == "/v1" {
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1") {
|
||||
gw.Auth(c)
|
||||
if !c.IsAborted() {
|
||||
gw.Handle(c)
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(dist); err == nil {
|
||||
c.File(dist + "/index.html")
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user