4563979a15
- 迁移 000035-000037(权限等级/环境变量/渠道) - 新增 internal/channel 渠道抽象层(webhook/企微/钉钉/飞书) - 全部功能端到端验证通过(25 包单测)
127 lines
5.4 KiB
Go
127 lines
5.4 KiB
Go
package operations
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"runtime"
|
|
"time"
|
|
|
|
"aigateway.local/core/internal/identity"
|
|
"aigateway.local/core/internal/platform/apiresponse"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type AdminHTTPHandler struct {
|
|
pool *pgxpool.Pool
|
|
identity *identity.Service
|
|
version string
|
|
startedAt time.Time
|
|
reload func(context.Context) error
|
|
mux *http.ServeMux
|
|
}
|
|
|
|
func NewAdminHTTPHandler(pool *pgxpool.Pool, identityService *identity.Service, version string, startedAt time.Time, reload func(context.Context) error) *AdminHTTPHandler {
|
|
h := &AdminHTTPHandler{pool: pool, identity: identityService, version: version, startedAt: startedAt, reload: reload, mux: http.NewServeMux()}
|
|
h.mux.HandleFunc("GET /api/v1/admin/system-info", h.systemInfo)
|
|
h.mux.HandleFunc("GET /api/v1/admin/monitoring/overview", h.overview)
|
|
h.mux.HandleFunc("GET /api/v1/admin/tenants/overview", h.tenantsOverview)
|
|
h.mux.HandleFunc("POST /api/v1/admin/reload", h.reloadSnapshots)
|
|
return h
|
|
}
|
|
|
|
func (h *AdminHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) }
|
|
|
|
func (h *AdminHTTPHandler) account(w http.ResponseWriter, r *http.Request) (identity.Account, bool) {
|
|
account, err := h.identity.Authenticate(r.Context(), identity.KindAdmin, r.Header.Get("Authorization"))
|
|
if err != nil {
|
|
apiresponse.Error(w, http.StatusUnauthorized, "登录状态无效")
|
|
return identity.Account{}, false
|
|
}
|
|
return account, true
|
|
}
|
|
|
|
func (h *AdminHTTPHandler) systemInfo(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := h.account(w, r); !ok {
|
|
return
|
|
}
|
|
var providers, models, keys int64
|
|
if err := h.pool.QueryRow(r.Context(), `SELECT (SELECT count(*) FROM gateway.providers WHERE enabled),(SELECT count(*) FROM gateway.provider_models WHERE enabled),(SELECT count(*) FROM gateway.api_keys WHERE enabled)`).Scan(&providers, &models, &keys); err != nil {
|
|
apiresponse.Error(w, http.StatusServiceUnavailable, "系统信息查询失败")
|
|
return
|
|
}
|
|
apiresponse.OK(w, map[string]any{"version": h.version, "go_version": runtime.Version(), "uptime_seconds": int64(time.Since(h.startedAt).Seconds()), "database": "postgresql", "object_storage": true, "clickhouse": false, "enabled_providers": providers, "enabled_models": models, "enabled_api_keys": keys})
|
|
}
|
|
|
|
func (h *AdminHTTPHandler) overview(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := h.account(w, r); !ok {
|
|
return
|
|
}
|
|
var requests, failures, promptTokens, completionTokens, cost int64
|
|
var latency *float64
|
|
err := h.pool.QueryRow(r.Context(), `SELECT count(*),count(*) FILTER(WHERE coalesce(status_code,500)>=400),coalesce(sum(prompt_tokens),0),coalesce(sum(completion_tokens),0),coalesce(sum(cost_microunits),0),avg(latency_ms) FROM gateway.audit_events WHERE recorded_at>=clock_timestamp()-interval '24 hours'`).Scan(&requests, &failures, &promptTokens, &completionTokens, &cost, &latency)
|
|
if err != nil {
|
|
apiresponse.Error(w, http.StatusServiceUnavailable, "监控汇总查询失败")
|
|
return
|
|
}
|
|
apiresponse.OK(w, map[string]any{"window": "24h", "requests": requests, "failed_requests": failures, "prompt_tokens": promptTokens, "completion_tokens": completionTokens, "cost_microunits": cost, "avg_latency_ms": latency})
|
|
}
|
|
|
|
func (h *AdminHTTPHandler) reloadSnapshots(w http.ResponseWriter, r *http.Request) {
|
|
account, ok := h.account(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if !identity.HasPermission(account, identity.PermissionProviderManage) {
|
|
apiresponse.Error(w, http.StatusForbidden, "缺少运行时配置管理权限")
|
|
return
|
|
}
|
|
if h.reload != nil {
|
|
if err := h.reload(r.Context()); err != nil {
|
|
apiresponse.Error(w, http.StatusServiceUnavailable, "运行时快照刷新不完整: "+err.Error())
|
|
return
|
|
}
|
|
}
|
|
apiresponse.OK(w, map[string]bool{"reloaded": true})
|
|
}
|
|
|
|
|
|
// tenantsOverview 以部门为租户维度,汇总各租户的账号/Key/用量。
|
|
func (h *AdminHTTPHandler) tenantsOverview(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := h.account(w, r); !ok {
|
|
return
|
|
}
|
|
rows, err := h.pool.Query(r.Context(), `SELECT d.id::text,d.name,
|
|
(SELECT count(*) FROM gateway.portal_users u WHERE u.department_id=d.id),
|
|
(SELECT count(*) FROM gateway.api_keys k WHERE k.tenant_id=d.id AND k.enabled),
|
|
(SELECT count(*) FROM gateway.audit_events a WHERE a.tenant_id=d.id AND a.recorded_at>=date_trunc('day',now())),
|
|
(SELECT COALESCE(sum(a.prompt_tokens+a.completion_tokens),0) FROM gateway.audit_events a WHERE a.tenant_id=d.id AND a.recorded_at>=date_trunc('day',now()))
|
|
FROM gateway.departments d WHERE d.active ORDER BY d.name`)
|
|
if err != nil {
|
|
apiresponse.Error(w, http.StatusServiceUnavailable, "租户概览查询失败")
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
type tenantRow struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
PortalUsers int64 `json:"portal_users"`
|
|
EnabledKeys int64 `json:"enabled_api_keys"`
|
|
TodayRequests int64 `json:"today_requests"`
|
|
TodayTokens int64 `json:"today_tokens"`
|
|
}
|
|
items := []tenantRow{}
|
|
for rows.Next() {
|
|
var item tenantRow
|
|
if err := rows.Scan(&item.ID, &item.Name, &item.PortalUsers, &item.EnabledKeys, &item.TodayRequests, &item.TodayTokens); err != nil {
|
|
apiresponse.Error(w, http.StatusServiceUnavailable, "租户概览查询失败")
|
|
return
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
apiresponse.Error(w, http.StatusServiceUnavailable, "租户概览查询失败")
|
|
return
|
|
}
|
|
apiresponse.OK(w, map[string]any{"tenants": items})
|
|
}
|