- 回退渠道 homepage/favicon 字段与代理接口(未采用) - Base URL 改为可选: 留空按供应商默认(openai/anthropic), 兼容兼容型渠道必须填; 填完整地址(含 /v1)时归一化去尾 - 网关 upstreamURL 兜底去 /v1, 避免路径重复 Co-Authored-By: Claude <noreply@anthropic.com>
118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
package api
|
||
|
||
import (
|
||
"net/http"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/openteam/server/internal/api/middleware"
|
||
"github.com/openteam/server/internal/app"
|
||
"github.com/openteam/server/internal/proxy"
|
||
)
|
||
|
||
// NewRouter 装配所有路由:
|
||
// - /v1/* 代理端点(Bearer API Key,OpenAI 格式错误体)
|
||
// - /api/v1/* 管理 API(会话 JWT)
|
||
func NewRouter(a *app.App, gw *proxy.Gateway) *gin.Engine {
|
||
if a.Cfg.Env == "production" {
|
||
gin.SetMode(gin.ReleaseMode)
|
||
}
|
||
r := gin.New()
|
||
r.Use(gin.Logger(), gin.Recovery(), middleware.CORS())
|
||
|
||
h := NewHandler(a)
|
||
|
||
// --- 代理端点(对外)---
|
||
proxyGroup := r.Group("/v1")
|
||
{
|
||
proxyGroup.Any("/chat/completions", gw.Auth, gw.Handle)
|
||
proxyGroup.Any("/responses", gw.Auth, gw.Handle)
|
||
proxyGroup.Any("/messages", gw.Auth, gw.Handle)
|
||
proxyGroup.Any("/models", gw.Auth, gw.Handle)
|
||
}
|
||
// 静态资源(前端构建产物,存在时托管)
|
||
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 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"})
|
||
})
|
||
|
||
// --- 管理 API ---
|
||
api := r.Group("/api/v1")
|
||
{
|
||
auth := api.Group("/auth")
|
||
{
|
||
auth.POST("/register", h.Register)
|
||
auth.POST("/login", h.Login)
|
||
auth.POST("/refresh", h.Refresh)
|
||
auth.POST("/logout", h.Logout)
|
||
auth.GET("/me", middleware.SessionAuth(a), h.Me)
|
||
}
|
||
|
||
user := api.Group("", middleware.SessionAuth(a))
|
||
{
|
||
user.GET("/user/profile", h.UserProfile)
|
||
user.GET("/user/balance", h.UserBalance)
|
||
user.GET("/user/models", h.UserModels)
|
||
user.GET("/usage/summary", h.UsageSummary)
|
||
user.GET("/usage/stats", h.UsageStats)
|
||
user.GET("/usage/logs", h.UsageLogs)
|
||
user.POST("/keys", h.CreateKey)
|
||
user.GET("/keys", h.ListKeys)
|
||
user.PATCH("/keys/:id", h.PatchKey)
|
||
user.DELETE("/keys/:id", h.DeleteKey)
|
||
}
|
||
|
||
admin := api.Group("/admin", middleware.SessionAuth(a), middleware.AdminOnly)
|
||
{
|
||
// 用户
|
||
admin.GET("/users", h.AdminUsers)
|
||
admin.PATCH("/users/:id", h.AdminPatchUser)
|
||
admin.POST("/users/:id/balance", h.AdminAdjustBalance)
|
||
// 渠道
|
||
admin.GET("/channels", h.AdminChannels)
|
||
admin.POST("/channels", h.AdminCreateChannel)
|
||
admin.PUT("/channels/:id", h.AdminUpdateChannel)
|
||
admin.DELETE("/channels/:id", h.AdminDeleteChannel)
|
||
admin.POST("/channels/:id/test", h.AdminTestChannel)
|
||
admin.POST("/channels/:id/models/import", h.AdminImportChannelModels)
|
||
// 模型与定价
|
||
admin.GET("/models", h.AdminModels)
|
||
admin.POST("/models", h.AdminCreateModel)
|
||
admin.PUT("/models/:id", h.AdminUpdateModel)
|
||
admin.DELETE("/models/:id", h.AdminDeleteModel)
|
||
admin.POST("/models/:id/bindings", h.AdminCreateModelBinding)
|
||
admin.DELETE("/models/:id/bindings/:bid", h.AdminDeleteModelBinding)
|
||
// 统计与用量
|
||
admin.GET("/stats/overview", h.AdminStatsOverview)
|
||
admin.GET("/usage", h.AdminUsage)
|
||
// 配置
|
||
admin.GET("/config", h.AdminConfig)
|
||
admin.PUT("/config", h.AdminPutConfig)
|
||
// 充值审核(M6 预留)
|
||
}
|
||
}
|
||
|
||
r.GET("/healthz", func(c *gin.Context) {
|
||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||
})
|
||
return r
|
||
}
|