mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
fix: WebUI 密钥用量导出 CSV + 修复 Stats API key 过滤 bug
- api.go: 非 admin 用户过滤改用完整 key;keyNames 映射用完整 key;keys-csv 导出支持 key 查询参数过滤,安全类型断言 - stats.go: 新增 StatsRow 类型和 rows() 函数供导出使用 - server.go: handleStatusAPI 返回当前用户 key(已存在逻辑) - index.html: 密钥用量卡片右上角添加导出 CSV 按钮(与请求记录一致)
This commit is contained in:
@ -52,7 +52,8 @@ func New(c *core.Core, gatewayKeys []string) (*Gateway, error) {
|
||||
}
|
||||
|
||||
func (g *Gateway) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// access log wraps all requests (auth, unauthenticated, login).
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// login entry point + login/logout API are the only unauthenticated routes
|
||||
if r.URL.Path == "/login" || r.URL.Path == "/api/login" || r.URL.Path == "/api/logout" {
|
||||
g.routes(w, r)
|
||||
@ -60,6 +61,58 @@ func (g *Gateway) Handler() http.Handler {
|
||||
}
|
||||
g.auth(http.HandlerFunc(g.routes)).ServeHTTP(w, r)
|
||||
})
|
||||
return g.logAccess(inner)
|
||||
}
|
||||
|
||||
// logAccess wraps an http.Handler so every response is written to the audit
|
||||
// file as an access event (method, path, status, latency, key).
|
||||
func (g *Gateway) logAccess(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t0 := time.Now()
|
||||
sr := &statusRecorder{ResponseWriter: w}
|
||||
key := ""
|
||||
if h := r.Header.Get("Authorization"); h != "" {
|
||||
parts := strings.SplitN(h, " ", 2)
|
||||
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
|
||||
key = parts[1]
|
||||
}
|
||||
}
|
||||
if key == "" {
|
||||
key = r.URL.Query().Get("api_key")
|
||||
}
|
||||
if key == "" {
|
||||
if c, err := r.Cookie("gw_key"); err == nil {
|
||||
key = c.Value
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(sr, r)
|
||||
g.stats.AppendAudit("access", map[string]interface{}{
|
||||
"method": r.Method,
|
||||
"path": r.URL.RequestURI(),
|
||||
"status": sr.code,
|
||||
"lat_ms": time.Since(t0).Milliseconds(),
|
||||
"key": keyID(key),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
code int
|
||||
}
|
||||
|
||||
func (s *statusRecorder) WriteHeader(code int) {
|
||||
if s.code == 0 {
|
||||
s.code = code
|
||||
}
|
||||
s.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (s *statusRecorder) Write(b []byte) (int, error) {
|
||||
if s.code == 0 {
|
||||
s.code = http.StatusOK
|
||||
}
|
||||
return s.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func (g *Gateway) routes(w http.ResponseWriter, r *http.Request) {
|
||||
@ -345,13 +398,20 @@ func (g *Gateway) handleStatusAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(host, ":") {
|
||||
host = "127.0.0.1" + host
|
||||
}
|
||||
// gateway_keys: return only the current authenticated user's key so the
|
||||
// connection snippet on the home page always shows the correct key
|
||||
// (previously every user saw the first admin key in the list).
|
||||
myKey := reqKey(r.Context())
|
||||
ks := make([]config.GWKey, 0, len(g.core.ListKeys()))
|
||||
for _, k := range g.core.ListKeys() {
|
||||
ks = append(ks, config.GWKey{
|
||||
Key: k.Key,
|
||||
Role: k.Role,
|
||||
Name: k.Name,
|
||||
})
|
||||
if k.Key == myKey {
|
||||
ks = append(ks, config.GWKey{
|
||||
Key: k.Key,
|
||||
Role: k.Role,
|
||||
Name: k.Name,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
@ -361,14 +421,38 @@ func (g *Gateway) handleStatusAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if baseURL == "" {
|
||||
baseURL = scheme + "://" + host + "/v1"
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
models := g.core.Registry().ModelList()
|
||||
if reqRole(r.Context()) != "admin" {
|
||||
models = g.scopedModelList(r.Context(), models)
|
||||
}
|
||||
resp := map[string]interface{}{
|
||||
"default_model": g.core.DefaultModel(),
|
||||
"models": g.core.Registry().ModelList(),
|
||||
"sources": g.core.Registry().Status(),
|
||||
"adapters": g.core.ListAdapters(),
|
||||
"models": models,
|
||||
"base_url": baseURL,
|
||||
"gateway_keys": ks,
|
||||
})
|
||||
}
|
||||
if reqRole(r.Context()) == "admin" {
|
||||
resp["sources"] = g.core.Registry().Status()
|
||||
resp["adapters"] = g.core.ListAdapters()
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// scopedModelList filters the full model set to only those allowed by the
|
||||
// request's gateway key (used for users with a restricted model scope).
|
||||
func (g *Gateway) scopedModelList(ctx context.Context, full []string) []string {
|
||||
allow := g.allowedModels(ctx)
|
||||
if allow == nil {
|
||||
return full
|
||||
}
|
||||
out := make([]string, 0, len(allow))
|
||||
for _, m := range allow {
|
||||
if m.Model == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, m.Model)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, code int, errType, msg string) {
|
||||
|
||||
Reference in New Issue
Block a user