2026-01-04 19:27:53 +08:00
|
|
|
|
// Package routes provides HTTP route registration and handlers.
|
2025-12-26 10:42:08 +08:00
|
|
|
|
package routes
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
|
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// RegisterAdminRoutes 注册管理员路由
|
|
|
|
|
|
func RegisterAdminRoutes(
|
|
|
|
|
|
v1 *gin.RouterGroup,
|
|
|
|
|
|
h *handler.Handlers,
|
|
|
|
|
|
adminAuth middleware.AdminAuthMiddleware,
|
|
|
|
|
|
) {
|
|
|
|
|
|
admin := v1.Group("/admin")
|
|
|
|
|
|
admin.Use(gin.HandlerFunc(adminAuth))
|
|
|
|
|
|
{
|
|
|
|
|
|
// 仪表盘
|
|
|
|
|
|
registerDashboardRoutes(admin, h)
|
|
|
|
|
|
|
|
|
|
|
|
// 用户管理
|
|
|
|
|
|
registerUserManagementRoutes(admin, h)
|
|
|
|
|
|
|
|
|
|
|
|
// 分组管理
|
|
|
|
|
|
registerGroupRoutes(admin, h)
|
|
|
|
|
|
|
|
|
|
|
|
// 账号管理
|
|
|
|
|
|
registerAccountRoutes(admin, h)
|
|
|
|
|
|
|
2026-01-30 16:45:04 +08:00
|
|
|
|
// 公告管理
|
|
|
|
|
|
registerAnnouncementRoutes(admin, h)
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
// OpenAI OAuth
|
|
|
|
|
|
registerOpenAIOAuthRoutes(admin, h)
|
|
|
|
|
|
|
2025-12-26 00:17:55 -08:00
|
|
|
|
// Gemini OAuth
|
|
|
|
|
|
registerGeminiOAuthRoutes(admin, h)
|
|
|
|
|
|
|
2025-12-29 00:44:07 +08:00
|
|
|
|
// Antigravity OAuth
|
|
|
|
|
|
registerAntigravityOAuthRoutes(admin, h)
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
// 代理管理
|
|
|
|
|
|
registerProxyRoutes(admin, h)
|
|
|
|
|
|
|
|
|
|
|
|
// 卡密管理
|
|
|
|
|
|
registerRedeemCodeRoutes(admin, h)
|
|
|
|
|
|
|
2026-01-10 13:14:35 +08:00
|
|
|
|
// 优惠码管理
|
|
|
|
|
|
registerPromoCodeRoutes(admin, h)
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
// 系统设置
|
|
|
|
|
|
registerSettingsRoutes(admin, h)
|
|
|
|
|
|
|
2026-02-28 15:01:20 +08:00
|
|
|
|
// 数据管理
|
|
|
|
|
|
registerDataManagementRoutes(admin, h)
|
|
|
|
|
|
|
2026-03-13 10:38:19 +08:00
|
|
|
|
// 数据库备份恢复
|
|
|
|
|
|
registerBackupRoutes(admin, h)
|
|
|
|
|
|
|
2026-01-09 20:55:12 +08:00
|
|
|
|
// 运维监控(Ops)
|
|
|
|
|
|
registerOpsRoutes(admin, h)
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
// 系统管理
|
|
|
|
|
|
registerSystemRoutes(admin, h)
|
|
|
|
|
|
|
|
|
|
|
|
// 订阅管理
|
|
|
|
|
|
registerSubscriptionRoutes(admin, h)
|
|
|
|
|
|
|
|
|
|
|
|
// 使用记录管理
|
|
|
|
|
|
registerUsageRoutes(admin, h)
|
2026-01-01 18:58:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 用户属性管理
|
|
|
|
|
|
registerUserAttributeRoutes(admin, h)
|
2026-02-05 21:52:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 错误透传规则管理
|
|
|
|
|
|
registerErrorPassthroughRoutes(admin, h)
|
2026-02-28 00:07:44 +08:00
|
|
|
|
|
2026-03-27 14:23:28 +08:00
|
|
|
|
// TLS 指纹模板管理
|
|
|
|
|
|
registerTLSFingerprintProfileRoutes(admin, h)
|
|
|
|
|
|
|
2026-02-28 00:07:44 +08:00
|
|
|
|
// API Key 管理
|
|
|
|
|
|
registerAdminAPIKeyRoutes(admin, h)
|
2026-03-05 16:06:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 定时测试计划
|
|
|
|
|
|
registerScheduledTestRoutes(admin, h)
|
2026-04-04 11:00:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 渠道管理
|
|
|
|
|
|
registerChannelRoutes(admin, h)
|
2026-04-20 20:21:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 渠道监控
|
|
|
|
|
|
registerChannelMonitorRoutes(admin, h)
|
2026-04-25 19:14:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 邀请返利(专属用户管理)
|
|
|
|
|
|
registerAffiliateRoutes(admin, h)
|
2026-02-28 00:07:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func registerAdminAPIKeyRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
apiKeys := admin.Group("/api-keys")
|
|
|
|
|
|
{
|
|
|
|
|
|
apiKeys.PUT("/:id", h.Admin.APIKey.UpdateGroup)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 20:55:12 +08:00
|
|
|
|
func registerOpsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
ops := admin.Group("/ops")
|
|
|
|
|
|
{
|
|
|
|
|
|
// Realtime ops signals
|
|
|
|
|
|
ops.GET("/concurrency", h.Admin.Ops.GetConcurrencyStats)
|
2026-02-07 12:31:10 +08:00
|
|
|
|
ops.GET("/user-concurrency", h.Admin.Ops.GetUserConcurrencyStats)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
ops.GET("/account-availability", h.Admin.Ops.GetAccountAvailability)
|
2026-01-12 14:17:58 +08:00
|
|
|
|
ops.GET("/realtime-traffic", h.Admin.Ops.GetRealtimeTrafficSummary)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
|
|
|
|
|
|
// Alerts (rules + events)
|
|
|
|
|
|
ops.GET("/alert-rules", h.Admin.Ops.ListAlertRules)
|
|
|
|
|
|
ops.POST("/alert-rules", h.Admin.Ops.CreateAlertRule)
|
|
|
|
|
|
ops.PUT("/alert-rules/:id", h.Admin.Ops.UpdateAlertRule)
|
|
|
|
|
|
ops.DELETE("/alert-rules/:id", h.Admin.Ops.DeleteAlertRule)
|
|
|
|
|
|
ops.GET("/alert-events", h.Admin.Ops.ListAlertEvents)
|
2026-01-14 09:03:35 +08:00
|
|
|
|
ops.GET("/alert-events/:id", h.Admin.Ops.GetAlertEvent)
|
|
|
|
|
|
ops.PUT("/alert-events/:id/status", h.Admin.Ops.UpdateAlertEventStatus)
|
|
|
|
|
|
ops.POST("/alert-silences", h.Admin.Ops.CreateAlertSilence)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
|
|
|
|
|
|
// Email notification config (DB-backed)
|
|
|
|
|
|
ops.GET("/email-notification/config", h.Admin.Ops.GetEmailNotificationConfig)
|
|
|
|
|
|
ops.PUT("/email-notification/config", h.Admin.Ops.UpdateEmailNotificationConfig)
|
|
|
|
|
|
|
|
|
|
|
|
// Runtime settings (DB-backed)
|
|
|
|
|
|
runtime := ops.Group("/runtime")
|
|
|
|
|
|
{
|
|
|
|
|
|
runtime.GET("/alert", h.Admin.Ops.GetAlertRuntimeSettings)
|
|
|
|
|
|
runtime.PUT("/alert", h.Admin.Ops.UpdateAlertRuntimeSettings)
|
2026-02-12 16:27:29 +08:00
|
|
|
|
runtime.GET("/logging", h.Admin.Ops.GetRuntimeLogConfig)
|
|
|
|
|
|
runtime.PUT("/logging", h.Admin.Ops.UpdateRuntimeLogConfig)
|
|
|
|
|
|
runtime.POST("/logging/reset", h.Admin.Ops.ResetRuntimeLogConfig)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-11 19:51:18 +08:00
|
|
|
|
// Advanced settings (DB-backed)
|
|
|
|
|
|
ops.GET("/advanced-settings", h.Admin.Ops.GetAdvancedSettings)
|
|
|
|
|
|
ops.PUT("/advanced-settings", h.Admin.Ops.UpdateAdvancedSettings)
|
|
|
|
|
|
|
2026-01-12 11:42:56 +08:00
|
|
|
|
// Settings group (DB-backed)
|
|
|
|
|
|
settings := ops.Group("/settings")
|
|
|
|
|
|
{
|
|
|
|
|
|
settings.GET("/metric-thresholds", h.Admin.Ops.GetMetricThresholds)
|
|
|
|
|
|
settings.PUT("/metric-thresholds", h.Admin.Ops.UpdateMetricThresholds)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 20:55:12 +08:00
|
|
|
|
// WebSocket realtime (QPS/TPS)
|
|
|
|
|
|
ws := ops.Group("/ws")
|
|
|
|
|
|
{
|
|
|
|
|
|
ws.GET("/qps", h.Admin.Ops.QPSWSHandler)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 14:29:01 +08:00
|
|
|
|
// Error logs (legacy)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
ops.GET("/errors", h.Admin.Ops.GetErrorLogs)
|
|
|
|
|
|
ops.GET("/errors/:id", h.Admin.Ops.GetErrorLogByID)
|
2026-01-14 09:03:35 +08:00
|
|
|
|
ops.GET("/errors/:id/retries", h.Admin.Ops.ListRetryAttempts)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
ops.POST("/errors/:id/retry", h.Admin.Ops.RetryErrorRequest)
|
2026-01-14 09:03:35 +08:00
|
|
|
|
ops.PUT("/errors/:id/resolve", h.Admin.Ops.UpdateErrorResolution)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
|
2026-01-14 14:29:01 +08:00
|
|
|
|
// Request errors (client-visible failures)
|
|
|
|
|
|
ops.GET("/request-errors", h.Admin.Ops.ListRequestErrors)
|
|
|
|
|
|
ops.GET("/request-errors/:id", h.Admin.Ops.GetRequestError)
|
2026-01-14 23:16:01 +08:00
|
|
|
|
ops.GET("/request-errors/:id/upstream-errors", h.Admin.Ops.ListRequestErrorUpstreamErrors)
|
2026-01-14 14:29:01 +08:00
|
|
|
|
ops.POST("/request-errors/:id/retry-client", h.Admin.Ops.RetryRequestErrorClient)
|
|
|
|
|
|
ops.POST("/request-errors/:id/upstream-errors/:idx/retry", h.Admin.Ops.RetryRequestErrorUpstreamEvent)
|
|
|
|
|
|
ops.PUT("/request-errors/:id/resolve", h.Admin.Ops.ResolveRequestError)
|
|
|
|
|
|
|
|
|
|
|
|
// Upstream errors (independent upstream failures)
|
|
|
|
|
|
ops.GET("/upstream-errors", h.Admin.Ops.ListUpstreamErrors)
|
|
|
|
|
|
ops.GET("/upstream-errors/:id", h.Admin.Ops.GetUpstreamError)
|
|
|
|
|
|
ops.POST("/upstream-errors/:id/retry", h.Admin.Ops.RetryUpstreamError)
|
|
|
|
|
|
ops.PUT("/upstream-errors/:id/resolve", h.Admin.Ops.ResolveUpstreamError)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
|
|
|
|
|
|
// Request drilldown (success + error)
|
|
|
|
|
|
ops.GET("/requests", h.Admin.Ops.ListRequestDetails)
|
|
|
|
|
|
|
2026-02-12 16:27:29 +08:00
|
|
|
|
// Indexed system logs
|
|
|
|
|
|
ops.GET("/system-logs", h.Admin.Ops.ListSystemLogs)
|
|
|
|
|
|
ops.POST("/system-logs/cleanup", h.Admin.Ops.CleanupSystemLogs)
|
|
|
|
|
|
ops.GET("/system-logs/health", h.Admin.Ops.GetSystemLogIngestionHealth)
|
|
|
|
|
|
|
2026-01-09 20:55:12 +08:00
|
|
|
|
// Dashboard (vNext - raw path for MVP)
|
2026-03-04 13:45:49 +08:00
|
|
|
|
ops.GET("/dashboard/snapshot-v2", h.Admin.Ops.GetDashboardSnapshotV2)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
ops.GET("/dashboard/overview", h.Admin.Ops.GetDashboardOverview)
|
|
|
|
|
|
ops.GET("/dashboard/throughput-trend", h.Admin.Ops.GetDashboardThroughputTrend)
|
|
|
|
|
|
ops.GET("/dashboard/latency-histogram", h.Admin.Ops.GetDashboardLatencyHistogram)
|
|
|
|
|
|
ops.GET("/dashboard/error-trend", h.Admin.Ops.GetDashboardErrorTrend)
|
|
|
|
|
|
ops.GET("/dashboard/error-distribution", h.Admin.Ops.GetDashboardErrorDistribution)
|
2026-02-12 14:20:14 +08:00
|
|
|
|
ops.GET("/dashboard/openai-token-stats", h.Admin.Ops.GetDashboardOpenAITokenStats)
|
2026-01-09 20:55:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
func registerDashboardRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
dashboard := admin.Group("/dashboard")
|
|
|
|
|
|
{
|
2026-03-04 13:45:49 +08:00
|
|
|
|
dashboard.GET("/snapshot-v2", h.Admin.Dashboard.GetSnapshotV2)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
dashboard.GET("/stats", h.Admin.Dashboard.GetStats)
|
|
|
|
|
|
dashboard.GET("/realtime", h.Admin.Dashboard.GetRealtimeMetrics)
|
|
|
|
|
|
dashboard.GET("/trend", h.Admin.Dashboard.GetUsageTrend)
|
|
|
|
|
|
dashboard.GET("/models", h.Admin.Dashboard.GetModelStats)
|
2026-03-01 20:10:51 +08:00
|
|
|
|
dashboard.GET("/groups", h.Admin.Dashboard.GetGroupStats)
|
2026-01-04 19:27:53 +08:00
|
|
|
|
dashboard.GET("/api-keys-trend", h.Admin.Dashboard.GetAPIKeyUsageTrend)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
dashboard.GET("/users-trend", h.Admin.Dashboard.GetUserUsageTrend)
|
2026-03-13 03:41:29 +08:00
|
|
|
|
dashboard.GET("/users-ranking", h.Admin.Dashboard.GetUserSpendingRanking)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
dashboard.POST("/users-usage", h.Admin.Dashboard.GetBatchUsersUsage)
|
2026-01-04 19:27:53 +08:00
|
|
|
|
dashboard.POST("/api-keys-usage", h.Admin.Dashboard.GetBatchAPIKeysUsage)
|
feat(dashboard): add per-user drill-down for group, model, and endpoint distributions
Click on a group name, model name, or endpoint name in the distribution
tables to expand and show per-user usage breakdown (requests, tokens,
actual cost, standard cost).
Backend: new GET /admin/dashboard/user-breakdown API with group_id,
model, endpoint, endpoint_type filters.
Frontend: clickable rows with expand/collapse sub-table in all three
distribution charts.
2026-03-16 21:31:52 +08:00
|
|
|
|
dashboard.GET("/user-breakdown", h.Admin.Dashboard.GetUserBreakdown)
|
2026-01-11 16:01:35 +08:00
|
|
|
|
dashboard.POST("/aggregation/backfill", h.Admin.Dashboard.BackfillAggregation)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func registerUserManagementRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
users := admin.Group("/users")
|
|
|
|
|
|
{
|
|
|
|
|
|
users.GET("", h.Admin.User.List)
|
|
|
|
|
|
users.GET("/:id", h.Admin.User.GetByID)
|
2026-04-20 22:22:14 +08:00
|
|
|
|
users.POST("/:id/auth-identities", h.Admin.User.BindAuthIdentity)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
users.POST("", h.Admin.User.Create)
|
|
|
|
|
|
users.PUT("/:id", h.Admin.User.Update)
|
|
|
|
|
|
users.DELETE("/:id", h.Admin.User.Delete)
|
|
|
|
|
|
users.POST("/:id/balance", h.Admin.User.UpdateBalance)
|
|
|
|
|
|
users.GET("/:id/api-keys", h.Admin.User.GetUserAPIKeys)
|
|
|
|
|
|
users.GET("/:id/usage", h.Admin.User.GetUserUsage)
|
2026-02-03 00:16:10 +08:00
|
|
|
|
users.GET("/:id/balance-history", h.Admin.User.GetBalanceHistory)
|
2026-03-18 23:28:11 +08:00
|
|
|
|
users.POST("/:id/replace-group", h.Admin.User.ReplaceGroup)
|
2026-04-23 03:33:52 +08:00
|
|
|
|
users.GET("/:id/rpm-status", h.Admin.User.GetUserRPMStatus)
|
2026-01-01 18:58:34 +08:00
|
|
|
|
|
|
|
|
|
|
// User attribute values
|
|
|
|
|
|
users.GET("/:id/attributes", h.Admin.UserAttribute.GetUserAttributes)
|
|
|
|
|
|
users.PUT("/:id/attributes", h.Admin.UserAttribute.UpdateUserAttributes)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func registerGroupRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
groups := admin.Group("/groups")
|
|
|
|
|
|
{
|
|
|
|
|
|
groups.GET("", h.Admin.Group.List)
|
|
|
|
|
|
groups.GET("/all", h.Admin.Group.GetAll)
|
2026-03-17 22:09:28 +08:00
|
|
|
|
groups.GET("/usage-summary", h.Admin.Group.GetUsageSummary)
|
2026-03-18 01:41:53 +08:00
|
|
|
|
groups.GET("/capacity-summary", h.Admin.Group.GetCapacitySummary)
|
2026-02-08 16:53:45 +08:00
|
|
|
|
groups.PUT("/sort-order", h.Admin.Group.UpdateSortOrder)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
groups.GET("/:id", h.Admin.Group.GetByID)
|
|
|
|
|
|
groups.POST("", h.Admin.Group.Create)
|
|
|
|
|
|
groups.PUT("/:id", h.Admin.Group.Update)
|
|
|
|
|
|
groups.DELETE("/:id", h.Admin.Group.Delete)
|
|
|
|
|
|
groups.GET("/:id/stats", h.Admin.Group.GetStats)
|
2026-03-12 23:37:36 +08:00
|
|
|
|
groups.GET("/:id/rate-multipliers", h.Admin.Group.GetGroupRateMultipliers)
|
|
|
|
|
|
groups.PUT("/:id/rate-multipliers", h.Admin.Group.BatchSetGroupRateMultipliers)
|
|
|
|
|
|
groups.DELETE("/:id/rate-multipliers", h.Admin.Group.ClearGroupRateMultipliers)
|
2026-04-23 03:33:52 +08:00
|
|
|
|
groups.PUT("/:id/rpm-overrides", h.Admin.Group.BatchSetGroupRPMOverrides)
|
|
|
|
|
|
groups.DELETE("/:id/rpm-overrides", h.Admin.Group.ClearGroupRPMOverrides)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
groups.GET("/:id/api-keys", h.Admin.Group.GetGroupAPIKeys)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
accounts := admin.Group("/accounts")
|
|
|
|
|
|
{
|
|
|
|
|
|
accounts.GET("", h.Admin.Account.List)
|
|
|
|
|
|
accounts.GET("/:id", h.Admin.Account.GetByID)
|
|
|
|
|
|
accounts.POST("", h.Admin.Account.Create)
|
2026-02-24 17:11:14 +08:00
|
|
|
|
accounts.POST("/check-mixed-channel", h.Admin.Account.CheckMixedChannel)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS)
|
2026-02-09 10:38:26 +08:00
|
|
|
|
accounts.POST("/sync/crs/preview", h.Admin.Account.PreviewFromCRS)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
accounts.PUT("/:id", h.Admin.Account.Update)
|
|
|
|
|
|
accounts.DELETE("/:id", h.Admin.Account.Delete)
|
|
|
|
|
|
accounts.POST("/:id/test", h.Admin.Account.Test)
|
2026-03-08 06:59:53 +08:00
|
|
|
|
accounts.POST("/:id/recover-state", h.Admin.Account.RecoverState)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
accounts.POST("/:id/refresh", h.Admin.Account.Refresh)
|
2026-03-25 13:05:47 +08:00
|
|
|
|
accounts.POST("/:id/set-privacy", h.Admin.Account.SetPrivacy)
|
feat(gemini): 添加 Google One 存储空间推断 Tier 功能
## 功能概述
通过 Google Drive API 获取存储空间配额来推断 Google One 订阅等级,并优化统一的配额显示系统。
## 后端改动
- 新增 Drive API 客户端 (drive_client.go)
- 支持代理和指数退避重试
- 处理 403/429 错误
- 添加 Tier 推断逻辑 (inferGoogleOneTier)
- 支持 6 种 tier 类型:AI_PREMIUM, GOOGLE_ONE_STANDARD, GOOGLE_ONE_BASIC, FREE, GOOGLE_ONE_UNKNOWN, GOOGLE_ONE_UNLIMITED
- 集成到 OAuth 流程
- ExchangeCode: 授权时自动获取 tier
- RefreshAccountToken: Token 刷新时更新 tier (24小时缓存)
- 新增管理 API 端点
- POST /api/v1/admin/accounts/:id/refresh-tier (单个账号刷新)
- POST /api/v1/admin/accounts/batch-refresh-tier (批量刷新)
## 前端改动
- 更新 AccountQuotaInfo.vue
- 添加 Google One tier 标签映射
- 添加 tier 颜色样式 (紫色/蓝色/绿色/灰色/琥珀色)
- 更新 AccountUsageCell.vue
- 添加 Google One tier 显示逻辑
- 根据 oauth_type 区分显示方式
- 添加国际化翻译 (en.ts, zh.ts)
- aiPremium, standard, basic, free, personal, unlimited
## Tier 推断规则
- >= 2TB: AI Premium
- >= 200GB: Google One Standard
- >= 100GB: Google One Basic
- >= 15GB: Free
- > 100TB: Unlimited (G Suite legacy)
- 其他/失败: Unknown (显示为 Personal)
## 优雅降级
- Drive API 失败时使用 GOOGLE_ONE_UNKNOWN
- 不阻断 OAuth 流程
- 24小时缓存避免频繁调用
## 测试
- ✅ 后端编译成功
- ✅ 前端构建成功
- ✅ 所有代码符合现有规范
2025-12-31 21:45:24 -08:00
|
|
|
|
accounts.POST("/:id/refresh-tier", h.Admin.Account.RefreshTier)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
accounts.GET("/:id/stats", h.Admin.Account.GetStats)
|
|
|
|
|
|
accounts.POST("/:id/clear-error", h.Admin.Account.ClearError)
|
|
|
|
|
|
accounts.GET("/:id/usage", h.Admin.Account.GetUsage)
|
|
|
|
|
|
accounts.GET("/:id/today-stats", h.Admin.Account.GetTodayStats)
|
2026-02-28 15:01:20 +08:00
|
|
|
|
accounts.POST("/today-stats/batch", h.Admin.Account.GetBatchTodayStats)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
accounts.POST("/:id/clear-rate-limit", h.Admin.Account.ClearRateLimit)
|
2026-03-05 20:54:37 +08:00
|
|
|
|
accounts.POST("/:id/reset-quota", h.Admin.Account.ResetQuota)
|
2026-01-03 06:34:00 -08:00
|
|
|
|
accounts.GET("/:id/temp-unschedulable", h.Admin.Account.GetTempUnschedulable)
|
|
|
|
|
|
accounts.DELETE("/:id/temp-unschedulable", h.Admin.Account.ClearTempUnschedulable)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
accounts.POST("/:id/schedulable", h.Admin.Account.SetSchedulable)
|
|
|
|
|
|
accounts.GET("/:id/models", h.Admin.Account.GetAvailableModels)
|
|
|
|
|
|
accounts.POST("/batch", h.Admin.Account.BatchCreate)
|
2026-02-05 17:46:08 +08:00
|
|
|
|
accounts.GET("/data", h.Admin.Account.ExportData)
|
|
|
|
|
|
accounts.POST("/data", h.Admin.Account.ImportData)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
accounts.POST("/batch-update-credentials", h.Admin.Account.BatchUpdateCredentials)
|
feat(gemini): 添加 Google One 存储空间推断 Tier 功能
## 功能概述
通过 Google Drive API 获取存储空间配额来推断 Google One 订阅等级,并优化统一的配额显示系统。
## 后端改动
- 新增 Drive API 客户端 (drive_client.go)
- 支持代理和指数退避重试
- 处理 403/429 错误
- 添加 Tier 推断逻辑 (inferGoogleOneTier)
- 支持 6 种 tier 类型:AI_PREMIUM, GOOGLE_ONE_STANDARD, GOOGLE_ONE_BASIC, FREE, GOOGLE_ONE_UNKNOWN, GOOGLE_ONE_UNLIMITED
- 集成到 OAuth 流程
- ExchangeCode: 授权时自动获取 tier
- RefreshAccountToken: Token 刷新时更新 tier (24小时缓存)
- 新增管理 API 端点
- POST /api/v1/admin/accounts/:id/refresh-tier (单个账号刷新)
- POST /api/v1/admin/accounts/batch-refresh-tier (批量刷新)
## 前端改动
- 更新 AccountQuotaInfo.vue
- 添加 Google One tier 标签映射
- 添加 tier 颜色样式 (紫色/蓝色/绿色/灰色/琥珀色)
- 更新 AccountUsageCell.vue
- 添加 Google One tier 显示逻辑
- 根据 oauth_type 区分显示方式
- 添加国际化翻译 (en.ts, zh.ts)
- aiPremium, standard, basic, free, personal, unlimited
## Tier 推断规则
- >= 2TB: AI Premium
- >= 200GB: Google One Standard
- >= 100GB: Google One Basic
- >= 15GB: Free
- > 100TB: Unlimited (G Suite legacy)
- 其他/失败: Unknown (显示为 Personal)
## 优雅降级
- Drive API 失败时使用 GOOGLE_ONE_UNKNOWN
- 不阻断 OAuth 流程
- 24小时缓存避免频繁调用
## 测试
- ✅ 后端编译成功
- ✅ 前端构建成功
- ✅ 所有代码符合现有规范
2025-12-31 21:45:24 -08:00
|
|
|
|
accounts.POST("/batch-refresh-tier", h.Admin.Account.BatchRefreshTier)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
accounts.POST("/bulk-update", h.Admin.Account.BulkUpdate)
|
2026-03-09 17:47:30 +08:00
|
|
|
|
accounts.POST("/batch-clear-error", h.Admin.Account.BatchClearError)
|
|
|
|
|
|
accounts.POST("/batch-refresh", h.Admin.Account.BatchRefresh)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
|
2026-02-07 12:31:10 +08:00
|
|
|
|
// Antigravity 默认模型映射
|
|
|
|
|
|
accounts.GET("/antigravity/default-model-mapping", h.Admin.Account.GetAntigravityDefaultModelMapping)
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
// Claude OAuth routes
|
|
|
|
|
|
accounts.POST("/generate-auth-url", h.Admin.OAuth.GenerateAuthURL)
|
|
|
|
|
|
accounts.POST("/generate-setup-token-url", h.Admin.OAuth.GenerateSetupTokenURL)
|
|
|
|
|
|
accounts.POST("/exchange-code", h.Admin.OAuth.ExchangeCode)
|
|
|
|
|
|
accounts.POST("/exchange-setup-token-code", h.Admin.OAuth.ExchangeSetupTokenCode)
|
|
|
|
|
|
accounts.POST("/cookie-auth", h.Admin.OAuth.CookieAuth)
|
|
|
|
|
|
accounts.POST("/setup-token-cookie-auth", h.Admin.OAuth.SetupTokenCookieAuth)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 16:45:04 +08:00
|
|
|
|
func registerAnnouncementRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
announcements := admin.Group("/announcements")
|
|
|
|
|
|
{
|
|
|
|
|
|
announcements.GET("", h.Admin.Announcement.List)
|
|
|
|
|
|
announcements.POST("", h.Admin.Announcement.Create)
|
|
|
|
|
|
announcements.GET("/:id", h.Admin.Announcement.GetByID)
|
|
|
|
|
|
announcements.PUT("/:id", h.Admin.Announcement.Update)
|
|
|
|
|
|
announcements.DELETE("/:id", h.Admin.Announcement.Delete)
|
|
|
|
|
|
announcements.GET("/:id/read-status", h.Admin.Announcement.ListReadStatus)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
func registerOpenAIOAuthRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
openai := admin.Group("/openai")
|
|
|
|
|
|
{
|
|
|
|
|
|
openai.POST("/generate-auth-url", h.Admin.OpenAIOAuth.GenerateAuthURL)
|
|
|
|
|
|
openai.POST("/exchange-code", h.Admin.OpenAIOAuth.ExchangeCode)
|
|
|
|
|
|
openai.POST("/refresh-token", h.Admin.OpenAIOAuth.RefreshToken)
|
|
|
|
|
|
openai.POST("/accounts/:id/refresh", h.Admin.OpenAIOAuth.RefreshAccountToken)
|
|
|
|
|
|
openai.POST("/create-from-oauth", h.Admin.OpenAIOAuth.CreateAccountFromOAuth)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 00:17:55 -08:00
|
|
|
|
func registerGeminiOAuthRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
gemini := admin.Group("/gemini")
|
|
|
|
|
|
{
|
|
|
|
|
|
gemini.POST("/oauth/auth-url", h.Admin.GeminiOAuth.GenerateAuthURL)
|
|
|
|
|
|
gemini.POST("/oauth/exchange-code", h.Admin.GeminiOAuth.ExchangeCode)
|
|
|
|
|
|
gemini.GET("/oauth/capabilities", h.Admin.GeminiOAuth.GetCapabilities)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-28 15:54:42 +08:00
|
|
|
|
func registerAntigravityOAuthRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
antigravity := admin.Group("/antigravity")
|
|
|
|
|
|
{
|
|
|
|
|
|
antigravity.POST("/oauth/auth-url", h.Admin.AntigravityOAuth.GenerateAuthURL)
|
|
|
|
|
|
antigravity.POST("/oauth/exchange-code", h.Admin.AntigravityOAuth.ExchangeCode)
|
2026-02-10 23:57:18 +08:00
|
|
|
|
antigravity.POST("/oauth/refresh-token", h.Admin.AntigravityOAuth.RefreshToken)
|
2025-12-28 15:54:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
func registerProxyRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
proxies := admin.Group("/proxies")
|
|
|
|
|
|
{
|
|
|
|
|
|
proxies.GET("", h.Admin.Proxy.List)
|
|
|
|
|
|
proxies.GET("/all", h.Admin.Proxy.GetAll)
|
2026-02-05 17:46:08 +08:00
|
|
|
|
proxies.GET("/data", h.Admin.Proxy.ExportData)
|
2026-02-05 18:23:49 +08:00
|
|
|
|
proxies.POST("/data", h.Admin.Proxy.ImportData)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
proxies.GET("/:id", h.Admin.Proxy.GetByID)
|
|
|
|
|
|
proxies.POST("", h.Admin.Proxy.Create)
|
|
|
|
|
|
proxies.PUT("/:id", h.Admin.Proxy.Update)
|
|
|
|
|
|
proxies.DELETE("/:id", h.Admin.Proxy.Delete)
|
|
|
|
|
|
proxies.POST("/:id/test", h.Admin.Proxy.Test)
|
2026-02-19 21:18:35 +08:00
|
|
|
|
proxies.POST("/:id/quality-check", h.Admin.Proxy.CheckQuality)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
proxies.GET("/:id/stats", h.Admin.Proxy.GetStats)
|
|
|
|
|
|
proxies.GET("/:id/accounts", h.Admin.Proxy.GetProxyAccounts)
|
2026-01-14 19:45:29 +08:00
|
|
|
|
proxies.POST("/batch-delete", h.Admin.Proxy.BatchDelete)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
proxies.POST("/batch", h.Admin.Proxy.BatchCreate)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func registerRedeemCodeRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
codes := admin.Group("/redeem-codes")
|
|
|
|
|
|
{
|
|
|
|
|
|
codes.GET("", h.Admin.Redeem.List)
|
|
|
|
|
|
codes.GET("/stats", h.Admin.Redeem.GetStats)
|
|
|
|
|
|
codes.GET("/export", h.Admin.Redeem.Export)
|
|
|
|
|
|
codes.GET("/:id", h.Admin.Redeem.GetByID)
|
2026-03-01 00:41:38 +08:00
|
|
|
|
codes.POST("/create-and-redeem", h.Admin.Redeem.CreateAndRedeem)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
codes.POST("/generate", h.Admin.Redeem.Generate)
|
|
|
|
|
|
codes.DELETE("/:id", h.Admin.Redeem.Delete)
|
|
|
|
|
|
codes.POST("/batch-delete", h.Admin.Redeem.BatchDelete)
|
|
|
|
|
|
codes.POST("/:id/expire", h.Admin.Redeem.Expire)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 13:14:35 +08:00
|
|
|
|
func registerPromoCodeRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
promoCodes := admin.Group("/promo-codes")
|
|
|
|
|
|
{
|
|
|
|
|
|
promoCodes.GET("", h.Admin.Promo.List)
|
|
|
|
|
|
promoCodes.GET("/:id", h.Admin.Promo.GetByID)
|
|
|
|
|
|
promoCodes.POST("", h.Admin.Promo.Create)
|
|
|
|
|
|
promoCodes.PUT("/:id", h.Admin.Promo.Update)
|
|
|
|
|
|
promoCodes.DELETE("/:id", h.Admin.Promo.Delete)
|
|
|
|
|
|
promoCodes.GET("/:id/usages", h.Admin.Promo.GetUsages)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
adminSettings := admin.Group("/settings")
|
|
|
|
|
|
{
|
|
|
|
|
|
adminSettings.GET("", h.Admin.Setting.GetSettings)
|
|
|
|
|
|
adminSettings.PUT("", h.Admin.Setting.UpdateSettings)
|
2026-01-04 19:27:53 +08:00
|
|
|
|
adminSettings.POST("/test-smtp", h.Admin.Setting.TestSMTPConnection)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail)
|
|
|
|
|
|
// Admin API Key 管理
|
2026-01-04 19:27:53 +08:00
|
|
|
|
adminSettings.GET("/admin-api-key", h.Admin.Setting.GetAdminAPIKey)
|
|
|
|
|
|
adminSettings.POST("/admin-api-key/regenerate", h.Admin.Setting.RegenerateAdminAPIKey)
|
|
|
|
|
|
adminSettings.DELETE("/admin-api-key", h.Admin.Setting.DeleteAdminAPIKey)
|
2026-03-18 16:22:19 +08:00
|
|
|
|
// 529过载冷却配置
|
|
|
|
|
|
adminSettings.GET("/overload-cooldown", h.Admin.Setting.GetOverloadCooldownSettings)
|
|
|
|
|
|
adminSettings.PUT("/overload-cooldown", h.Admin.Setting.UpdateOverloadCooldownSettings)
|
2026-01-11 21:54:52 -08:00
|
|
|
|
// 流超时处理配置
|
|
|
|
|
|
adminSettings.GET("/stream-timeout", h.Admin.Setting.GetStreamTimeoutSettings)
|
|
|
|
|
|
adminSettings.PUT("/stream-timeout", h.Admin.Setting.UpdateStreamTimeoutSettings)
|
2026-03-07 21:45:18 +08:00
|
|
|
|
// 请求整流器配置
|
|
|
|
|
|
adminSettings.GET("/rectifier", h.Admin.Setting.GetRectifierSettings)
|
|
|
|
|
|
adminSettings.PUT("/rectifier", h.Admin.Setting.UpdateRectifierSettings)
|
2026-03-10 11:14:17 +08:00
|
|
|
|
// Beta 策略配置
|
|
|
|
|
|
adminSettings.GET("/beta-policy", h.Admin.Setting.GetBetaPolicySettings)
|
|
|
|
|
|
adminSettings.PUT("/beta-policy", h.Admin.Setting.UpdateBetaPolicySettings)
|
feat(gateway): add web search emulation for Anthropic API Key accounts
Inject web search capability for Claude Console (API Key) accounts that
don't natively support Anthropic's web_search tool. When a pure
web_search request is detected, the gateway calls Brave Search or Tavily
API directly and constructs an Anthropic-protocol-compliant SSE/JSON
response without forwarding to upstream.
Backend:
- New `pkg/websearch/` SDK: Brave and Tavily provider implementations
with io.LimitReader, proxy support, and Redis-based quota tracking
(Lua atomic INCR + TTL, DECR rollback on failure)
- Global config via `settings.web_search_emulation_config` (JSON) with
in-process cache + singleflight, input validation, API key merge on
save, and sanitized API responses
- Channel-level toggle via `channels.features_config` JSONB column
(DB migration 101)
- Account-level toggle via `accounts.extra.web_search_emulation`
- Request interception in `Forward()` with SSE streaming response
construction using json.Marshal (no manual string concatenation)
- Manager hot-reload: `RebuildWebSearchManager()` called on config save
and startup via `SetWebSearchRedisClient()`
- 70 unit tests covering providers, manager, config validation,
sanitization, tool detection, query extraction, and response building
Frontend:
- Settings → Gateway tab: Web Search Emulation config card with global
toggle, provider list (add/remove, API key, priority, quota, proxy)
- Channels → Anthropic tab: web search emulation toggle with global
state linkage (disabled when global off)
- Account Create/Edit modals: web search emulation toggle for API Key
type with Toggle component
- Full i18n coverage (zh + en)
2026-04-12 00:02:26 +08:00
|
|
|
|
// Web Search 模拟配置
|
|
|
|
|
|
adminSettings.GET("/web-search-emulation", h.Admin.Setting.GetWebSearchEmulationConfig)
|
|
|
|
|
|
adminSettings.PUT("/web-search-emulation", h.Admin.Setting.UpdateWebSearchEmulationConfig)
|
2026-04-12 13:11:46 +08:00
|
|
|
|
adminSettings.POST("/web-search-emulation/test", h.Admin.Setting.TestWebSearchEmulation)
|
2026-04-14 08:03:27 +08:00
|
|
|
|
adminSettings.POST("/web-search-emulation/reset-usage", h.Admin.Setting.ResetWebSearchUsage)
|
2026-02-28 15:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func registerDataManagementRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
dataManagement := admin.Group("/data-management")
|
|
|
|
|
|
{
|
|
|
|
|
|
dataManagement.GET("/agent/health", h.Admin.DataManagement.GetAgentHealth)
|
|
|
|
|
|
dataManagement.GET("/config", h.Admin.DataManagement.GetConfig)
|
|
|
|
|
|
dataManagement.PUT("/config", h.Admin.DataManagement.UpdateConfig)
|
|
|
|
|
|
dataManagement.GET("/sources/:source_type/profiles", h.Admin.DataManagement.ListSourceProfiles)
|
|
|
|
|
|
dataManagement.POST("/sources/:source_type/profiles", h.Admin.DataManagement.CreateSourceProfile)
|
|
|
|
|
|
dataManagement.PUT("/sources/:source_type/profiles/:profile_id", h.Admin.DataManagement.UpdateSourceProfile)
|
|
|
|
|
|
dataManagement.DELETE("/sources/:source_type/profiles/:profile_id", h.Admin.DataManagement.DeleteSourceProfile)
|
|
|
|
|
|
dataManagement.POST("/sources/:source_type/profiles/:profile_id/activate", h.Admin.DataManagement.SetActiveSourceProfile)
|
|
|
|
|
|
dataManagement.POST("/s3/test", h.Admin.DataManagement.TestS3)
|
|
|
|
|
|
dataManagement.GET("/s3/profiles", h.Admin.DataManagement.ListS3Profiles)
|
|
|
|
|
|
dataManagement.POST("/s3/profiles", h.Admin.DataManagement.CreateS3Profile)
|
|
|
|
|
|
dataManagement.PUT("/s3/profiles/:profile_id", h.Admin.DataManagement.UpdateS3Profile)
|
|
|
|
|
|
dataManagement.DELETE("/s3/profiles/:profile_id", h.Admin.DataManagement.DeleteS3Profile)
|
|
|
|
|
|
dataManagement.POST("/s3/profiles/:profile_id/activate", h.Admin.DataManagement.SetActiveS3Profile)
|
|
|
|
|
|
dataManagement.POST("/backups", h.Admin.DataManagement.CreateBackupJob)
|
|
|
|
|
|
dataManagement.GET("/backups", h.Admin.DataManagement.ListBackupJobs)
|
|
|
|
|
|
dataManagement.GET("/backups/:job_id", h.Admin.DataManagement.GetBackupJob)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 10:38:19 +08:00
|
|
|
|
func registerBackupRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
backup := admin.Group("/backups")
|
|
|
|
|
|
{
|
|
|
|
|
|
// S3 存储配置
|
|
|
|
|
|
backup.GET("/s3-config", h.Admin.Backup.GetS3Config)
|
|
|
|
|
|
backup.PUT("/s3-config", h.Admin.Backup.UpdateS3Config)
|
|
|
|
|
|
backup.POST("/s3-config/test", h.Admin.Backup.TestS3Connection)
|
|
|
|
|
|
|
|
|
|
|
|
// 定时备份配置
|
|
|
|
|
|
backup.GET("/schedule", h.Admin.Backup.GetSchedule)
|
|
|
|
|
|
backup.PUT("/schedule", h.Admin.Backup.UpdateSchedule)
|
|
|
|
|
|
|
|
|
|
|
|
// 备份操作
|
|
|
|
|
|
backup.POST("", h.Admin.Backup.CreateBackup)
|
|
|
|
|
|
backup.GET("", h.Admin.Backup.ListBackups)
|
|
|
|
|
|
backup.GET("/:id", h.Admin.Backup.GetBackup)
|
|
|
|
|
|
backup.DELETE("/:id", h.Admin.Backup.DeleteBackup)
|
|
|
|
|
|
backup.GET("/:id/download-url", h.Admin.Backup.GetDownloadURL)
|
|
|
|
|
|
|
|
|
|
|
|
// 恢复操作
|
|
|
|
|
|
backup.POST("/:id/restore", h.Admin.Backup.RestoreBackup)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 10:42:08 +08:00
|
|
|
|
func registerSystemRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
system := admin.Group("/system")
|
|
|
|
|
|
{
|
|
|
|
|
|
system.GET("/version", h.Admin.System.GetVersion)
|
|
|
|
|
|
system.GET("/check-updates", h.Admin.System.CheckUpdates)
|
|
|
|
|
|
system.POST("/update", h.Admin.System.PerformUpdate)
|
|
|
|
|
|
system.POST("/rollback", h.Admin.System.Rollback)
|
|
|
|
|
|
system.POST("/restart", h.Admin.System.RestartService)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func registerSubscriptionRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
subscriptions := admin.Group("/subscriptions")
|
|
|
|
|
|
{
|
|
|
|
|
|
subscriptions.GET("", h.Admin.Subscription.List)
|
|
|
|
|
|
subscriptions.GET("/:id", h.Admin.Subscription.GetByID)
|
|
|
|
|
|
subscriptions.GET("/:id/progress", h.Admin.Subscription.GetProgress)
|
|
|
|
|
|
subscriptions.POST("/assign", h.Admin.Subscription.Assign)
|
|
|
|
|
|
subscriptions.POST("/bulk-assign", h.Admin.Subscription.BulkAssign)
|
|
|
|
|
|
subscriptions.POST("/:id/extend", h.Admin.Subscription.Extend)
|
2026-03-10 11:21:11 +08:00
|
|
|
|
subscriptions.POST("/:id/reset-quota", h.Admin.Subscription.ResetQuota)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
subscriptions.DELETE("/:id", h.Admin.Subscription.Revoke)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分组下的订阅列表
|
|
|
|
|
|
admin.GET("/groups/:id/subscriptions", h.Admin.Subscription.ListByGroup)
|
|
|
|
|
|
|
|
|
|
|
|
// 用户下的订阅列表
|
|
|
|
|
|
admin.GET("/users/:id/subscriptions", h.Admin.Subscription.ListByUser)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func registerUsageRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
usage := admin.Group("/usage")
|
|
|
|
|
|
{
|
|
|
|
|
|
usage.GET("", h.Admin.Usage.List)
|
|
|
|
|
|
usage.GET("/stats", h.Admin.Usage.Stats)
|
|
|
|
|
|
usage.GET("/search-users", h.Admin.Usage.SearchUsers)
|
2026-01-04 19:27:53 +08:00
|
|
|
|
usage.GET("/search-api-keys", h.Admin.Usage.SearchAPIKeys)
|
2026-01-18 10:52:18 +08:00
|
|
|
|
usage.GET("/cleanup-tasks", h.Admin.Usage.ListCleanupTasks)
|
|
|
|
|
|
usage.POST("/cleanup-tasks", h.Admin.Usage.CreateCleanupTask)
|
|
|
|
|
|
usage.POST("/cleanup-tasks/:id/cancel", h.Admin.Usage.CancelCleanupTask)
|
2025-12-26 10:42:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-01 18:58:34 +08:00
|
|
|
|
|
|
|
|
|
|
func registerUserAttributeRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
attrs := admin.Group("/user-attributes")
|
|
|
|
|
|
{
|
|
|
|
|
|
attrs.GET("", h.Admin.UserAttribute.ListDefinitions)
|
|
|
|
|
|
attrs.POST("", h.Admin.UserAttribute.CreateDefinition)
|
|
|
|
|
|
attrs.POST("/batch", h.Admin.UserAttribute.GetBatchUserAttributes)
|
|
|
|
|
|
attrs.PUT("/reorder", h.Admin.UserAttribute.ReorderDefinitions)
|
|
|
|
|
|
attrs.PUT("/:id", h.Admin.UserAttribute.UpdateDefinition)
|
|
|
|
|
|
attrs.DELETE("/:id", h.Admin.UserAttribute.DeleteDefinition)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-05 21:52:54 +08:00
|
|
|
|
|
2026-03-05 16:06:05 +08:00
|
|
|
|
func registerScheduledTestRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
plans := admin.Group("/scheduled-test-plans")
|
|
|
|
|
|
{
|
|
|
|
|
|
plans.POST("", h.Admin.ScheduledTest.Create)
|
|
|
|
|
|
plans.PUT("/:id", h.Admin.ScheduledTest.Update)
|
|
|
|
|
|
plans.DELETE("/:id", h.Admin.ScheduledTest.Delete)
|
|
|
|
|
|
plans.GET("/:id/results", h.Admin.ScheduledTest.ListResults)
|
|
|
|
|
|
}
|
|
|
|
|
|
// Nested under accounts
|
|
|
|
|
|
admin.GET("/accounts/:id/scheduled-test-plans", h.Admin.ScheduledTest.ListByAccount)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 21:52:54 +08:00
|
|
|
|
func registerErrorPassthroughRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
rules := admin.Group("/error-passthrough-rules")
|
|
|
|
|
|
{
|
|
|
|
|
|
rules.GET("", h.Admin.ErrorPassthrough.List)
|
|
|
|
|
|
rules.GET("/:id", h.Admin.ErrorPassthrough.GetByID)
|
|
|
|
|
|
rules.POST("", h.Admin.ErrorPassthrough.Create)
|
|
|
|
|
|
rules.PUT("/:id", h.Admin.ErrorPassthrough.Update)
|
|
|
|
|
|
rules.DELETE("/:id", h.Admin.ErrorPassthrough.Delete)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-27 14:23:28 +08:00
|
|
|
|
|
|
|
|
|
|
func registerTLSFingerprintProfileRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
profiles := admin.Group("/tls-fingerprint-profiles")
|
|
|
|
|
|
{
|
|
|
|
|
|
profiles.GET("", h.Admin.TLSFingerprintProfile.List)
|
|
|
|
|
|
profiles.GET("/:id", h.Admin.TLSFingerprintProfile.GetByID)
|
|
|
|
|
|
profiles.POST("", h.Admin.TLSFingerprintProfile.Create)
|
|
|
|
|
|
profiles.PUT("/:id", h.Admin.TLSFingerprintProfile.Update)
|
|
|
|
|
|
profiles.DELETE("/:id", h.Admin.TLSFingerprintProfile.Delete)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-04 11:00:55 +08:00
|
|
|
|
|
|
|
|
|
|
func registerChannelRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
channels := admin.Group("/channels")
|
|
|
|
|
|
{
|
|
|
|
|
|
channels.GET("", h.Admin.Channel.List)
|
2026-03-30 16:11:49 +08:00
|
|
|
|
channels.GET("/model-pricing", h.Admin.Channel.GetModelDefaultPricing)
|
2026-04-04 11:00:55 +08:00
|
|
|
|
channels.GET("/:id", h.Admin.Channel.GetByID)
|
|
|
|
|
|
channels.POST("", h.Admin.Channel.Create)
|
|
|
|
|
|
channels.PUT("/:id", h.Admin.Channel.Update)
|
|
|
|
|
|
channels.DELETE("/:id", h.Admin.Channel.Delete)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-20 20:21:02 +08:00
|
|
|
|
|
|
|
|
|
|
func registerChannelMonitorRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
monitors := admin.Group("/channel-monitors")
|
|
|
|
|
|
{
|
|
|
|
|
|
monitors.GET("", h.Admin.ChannelMonitor.List)
|
|
|
|
|
|
monitors.POST("", h.Admin.ChannelMonitor.Create)
|
|
|
|
|
|
monitors.GET("/:id", h.Admin.ChannelMonitor.Get)
|
|
|
|
|
|
monitors.PUT("/:id", h.Admin.ChannelMonitor.Update)
|
|
|
|
|
|
monitors.DELETE("/:id", h.Admin.ChannelMonitor.Delete)
|
|
|
|
|
|
monitors.POST("/:id/run", h.Admin.ChannelMonitor.Run)
|
|
|
|
|
|
monitors.GET("/:id/history", h.Admin.ChannelMonitor.History)
|
|
|
|
|
|
}
|
feat(channel-monitor): request templates with snapshot apply + headers/body override
Problem:
Upstream channels can reject monitor probes based on client fingerprint
(e.g. "only Claude Code clients allowed"). The monitor had no way to
customize the outgoing request to bypass such restrictions.
Solution:
Introduce reusable request templates that carry extra_headers plus an
optional body override; monitors reference a template and receive a
snapshot copy on apply. Template edits do NOT auto-propagate — users
must click "apply to associated monitors" to refresh snapshots, so a
bad template edit cannot instantly break all production monitors.
Data model (migration 112):
- channel_monitor_request_templates: id, name, provider, description,
extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'),
body_override jsonb. Unique (provider, name).
- channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers,
+body_override_mode, +body_override (the three runtime snapshot fields).
Checker (channel_monitor_checker.go):
- callProvider + runCheckForModel accept a CheckOptions carrying the
snapshot fields. mergeHeaders applies user headers on top of adapter
defaults (forbidden list: Host / Content-Length / Transfer-Encoding /
Connection / Content-Encoding).
- buildRequestBody:
off -> adapter default body
merge -> shallow-merge over default; per-provider deny list
(model/messages/contents) protects the challenge contract
replace -> user body verbatim
- Replace mode skips challenge validation; instead HTTP 2xx + non-empty
extracted response text = operational, empty = failed.
- 4 new unit tests cover all three modes + replace/empty-response case.
Admin API:
- /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot
on all template_id=id monitors, returns affected count).
- channel_monitor request/response DTOs gain the 4 new fields.
Frontend:
- channelMonitorTemplate.ts API client.
- MonitorAdvancedRequestConfig.vue shared component for headers textarea
+ body mode radio + body JSON editor; used by both template and monitor
forms.
- MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/
delete/apply, live "associated monitors" count per row.
- MonitorFiltersBar: new 模板管理 button next to 新增监控.
- MonitorFormDialog: collapsible 高级 section with template dropdown
(filtered by form.provider, clears on provider change) + embedded
AdvancedRequestConfig. Picking a template copies its fields into the
form (snapshot semantics mirrored on the client).
- i18n zh/en entries for all new copy.
chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
|
|
|
|
|
|
|
|
|
|
templates := admin.Group("/channel-monitor-templates")
|
|
|
|
|
|
{
|
|
|
|
|
|
templates.GET("", h.Admin.ChannelMonitorTemplate.List)
|
|
|
|
|
|
templates.POST("", h.Admin.ChannelMonitorTemplate.Create)
|
|
|
|
|
|
templates.GET("/:id", h.Admin.ChannelMonitorTemplate.Get)
|
|
|
|
|
|
templates.PUT("/:id", h.Admin.ChannelMonitorTemplate.Update)
|
|
|
|
|
|
templates.DELETE("/:id", h.Admin.ChannelMonitorTemplate.Delete)
|
2026-04-21 14:39:19 +08:00
|
|
|
|
templates.GET("/:id/monitors", h.Admin.ChannelMonitorTemplate.AssociatedMonitors)
|
feat(channel-monitor): request templates with snapshot apply + headers/body override
Problem:
Upstream channels can reject monitor probes based on client fingerprint
(e.g. "only Claude Code clients allowed"). The monitor had no way to
customize the outgoing request to bypass such restrictions.
Solution:
Introduce reusable request templates that carry extra_headers plus an
optional body override; monitors reference a template and receive a
snapshot copy on apply. Template edits do NOT auto-propagate — users
must click "apply to associated monitors" to refresh snapshots, so a
bad template edit cannot instantly break all production monitors.
Data model (migration 112):
- channel_monitor_request_templates: id, name, provider, description,
extra_headers jsonb, body_override_mode ('off'|'merge'|'replace'),
body_override jsonb. Unique (provider, name).
- channel_monitors: +template_id (FK, ON DELETE SET NULL), +extra_headers,
+body_override_mode, +body_override (the three runtime snapshot fields).
Checker (channel_monitor_checker.go):
- callProvider + runCheckForModel accept a CheckOptions carrying the
snapshot fields. mergeHeaders applies user headers on top of adapter
defaults (forbidden list: Host / Content-Length / Transfer-Encoding /
Connection / Content-Encoding).
- buildRequestBody:
off -> adapter default body
merge -> shallow-merge over default; per-provider deny list
(model/messages/contents) protects the challenge contract
replace -> user body verbatim
- Replace mode skips challenge validation; instead HTTP 2xx + non-empty
extracted response text = operational, empty = failed.
- 4 new unit tests cover all three modes + replace/empty-response case.
Admin API:
- /admin/channel-monitor-templates CRUD + /:id/apply (overwrite snapshot
on all template_id=id monitors, returns affected count).
- channel_monitor request/response DTOs gain the 4 new fields.
Frontend:
- channelMonitorTemplate.ts API client.
- MonitorAdvancedRequestConfig.vue shared component for headers textarea
+ body mode radio + body JSON editor; used by both template and monitor
forms.
- MonitorTemplateManagerDialog.vue: provider tabs, list/create/edit/
delete/apply, live "associated monitors" count per row.
- MonitorFiltersBar: new 模板管理 button next to 新增监控.
- MonitorFormDialog: collapsible 高级 section with template dropdown
(filtered by form.provider, clears on provider change) + embedded
AdvancedRequestConfig. Picking a template copies its fields into the
form (snapshot semantics mirrored on the client).
- i18n zh/en entries for all new copy.
chore: bump version to 0.1.114.32
2026-04-21 14:14:49 +08:00
|
|
|
|
templates.POST("/:id/apply", h.Admin.ChannelMonitorTemplate.Apply)
|
|
|
|
|
|
}
|
2026-04-20 20:21:02 +08:00
|
|
|
|
}
|
2026-04-25 19:14:34 +08:00
|
|
|
|
|
|
|
|
|
|
// registerAffiliateRoutes 注册邀请返利的管理端路由(专属用户配置)
|
|
|
|
|
|
func registerAffiliateRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
|
|
|
|
affiliates := admin.Group("/affiliates")
|
|
|
|
|
|
{
|
2026-05-03 15:43:56 +08:00
|
|
|
|
affiliates.GET("/invites", h.Admin.Affiliate.ListInviteRecords)
|
|
|
|
|
|
affiliates.GET("/rebates", h.Admin.Affiliate.ListRebateRecords)
|
|
|
|
|
|
affiliates.GET("/transfers", h.Admin.Affiliate.ListTransferRecords)
|
|
|
|
|
|
|
2026-04-25 19:14:34 +08:00
|
|
|
|
users := affiliates.Group("/users")
|
|
|
|
|
|
{
|
|
|
|
|
|
users.GET("", h.Admin.Affiliate.ListUsers)
|
|
|
|
|
|
users.GET("/lookup", h.Admin.Affiliate.LookupUsers)
|
|
|
|
|
|
users.POST("/batch-rate", h.Admin.Affiliate.BatchSetRate)
|
2026-05-03 15:43:56 +08:00
|
|
|
|
users.GET("/:user_id/overview", h.Admin.Affiliate.GetUserOverview)
|
2026-04-25 19:14:34 +08:00
|
|
|
|
users.PUT("/:user_id", h.Admin.Affiliate.UpdateUserSettings)
|
|
|
|
|
|
users.DELETE("/:user_id", h.Admin.Affiliate.ClearUserSettings)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|