feat: add configurable user statistics

This commit is contained in:
chuan
2026-08-22 22:42:06 +08:00
parent a9727e9b96
commit 74b5994180
18 changed files with 210 additions and 33 deletions
+30
View File
@@ -197,6 +197,36 @@ func TestCreateKeyRejectsShortSecret(t *testing.T) {
}
}
func TestManagedKeyStatsVisibilityCanBeConfigured(t *testing.T) {
app := NewApp()
if _, err := app.HandleMethod(MethodPluginRegister, lifecycleRequest(t, SchemaVersion, testConfig(t, "enabled: true\n"))); err != nil {
t.Fatal(err)
}
response := managementCallBody(t, app, http.MethodPost, managementBase+routeKeys, []byte(`{"name":"hidden","secret":"hidden-000000","show_in_stats":false}`))
if response.StatusCode != http.StatusCreated {
t.Fatalf("create status=%d body=%s", response.StatusCode, response.Body)
}
var created managedaccess.ManagedKey
if err := json.Unmarshal(response.Body, &created); err != nil {
t.Fatal(err)
}
if created.ShowInStats {
t.Fatalf("created key unexpectedly visible in stats: %+v", created)
}
summary := managementCall(t, app, http.MethodGet, managementBase+routeUsageSummary)
if strings.Contains(string(summary.Body), `"key_alias":"hidden"`) {
t.Fatalf("hidden key remains in dashboard: %s", summary.Body)
}
response = managementCallBody(t, app, http.MethodPatch, managementBase+routeKeys, []byte(`{"id":"`+created.ID+`","show_in_stats":true}`))
if response.StatusCode != http.StatusOK {
t.Fatalf("update status=%d body=%s", response.StatusCode, response.Body)
}
summary = managementCall(t, app, http.MethodGet, managementBase+routeUsageSummary)
if !strings.Contains(string(summary.Body), `"key_alias":"hidden"`) {
t.Fatalf("visible key missing from dashboard: %s", summary.Body)
}
}
func TestManagedSecretValidation(t *testing.T) {
for secret, want := range map[string]bool{
"12345": false, "000000": true, "alice-000000": true, "with space": false, strings.Repeat("x", 257): false,
+9 -1
View File
@@ -24,6 +24,7 @@ type createManagedKeyRequest struct {
RouteMode string `json:"route_mode,omitempty"`
UpstreamAccountID string `json:"upstream_account_id,omitempty"`
AllModels *bool `json:"all_models,omitempty"`
ShowInStats *bool `json:"show_in_stats,omitempty"`
Models *[]string `json:"models,omitempty"`
Billing *billingSettingsRequest `json:"billing,omitempty"`
}
@@ -35,6 +36,7 @@ type updateManagedKeyRequest struct {
RouteMode *string `json:"route_mode,omitempty"`
UpstreamAccountID *string `json:"upstream_account_id,omitempty"`
AllModels *bool `json:"all_models,omitempty"`
ShowInStats *bool `json:"show_in_stats,omitempty"`
Models *[]string `json:"models,omitempty"`
Billing *billingSettingsRequest `json:"billing,omitempty"`
}
@@ -133,7 +135,7 @@ func (a *App) createManagedKey(body []byte) ManagementResponse {
}
key := managedaccess.ManagedKey{
ID: keyID, Name: name, Secret: secret,
Status: managedaccess.StatusActive, RouteMode: managedaccess.RouteAuto, AllModels: true,
Status: managedaccess.StatusActive, RouteMode: managedaccess.RouteAuto, AllModels: true, ShowInStats: true,
}
if template, err := store.ManagedKeyByID(context.Background(), "key_default"); err == nil {
key.RouteMode = template.RouteMode
@@ -150,6 +152,9 @@ func (a *App) createManagedKey(body []byte) ManagementResponse {
if req.AllModels != nil {
key.AllModels = *req.AllModels
}
if req.ShowInStats != nil {
key.ShowInStats = *req.ShowInStats
}
if req.Models != nil {
key.Models = normalizeModels(*req.Models)
}
@@ -211,6 +216,9 @@ func (a *App) updateManagedKey(body []byte) ManagementResponse {
if req.AllModels != nil {
key.AllModels = *req.AllModels
}
if req.ShowInStats != nil {
key.ShowInStats = *req.ShowInStats
}
if req.Models != nil {
key.Models = normalizeModels(*req.Models)
}
+2 -2
View File
@@ -665,7 +665,7 @@ func TestUsageResourceServesFeatureModules(t *testing.T) {
if pageResponse.StatusCode != http.StatusOK || !strings.Contains(page, `data-view="logs">日志`) {
t.Fatalf("unexpected UI response: status=%d", pageResponse.StatusCode)
}
for _, feature := range []string{`<body class="read-only">`, `<base href="/v0/resource/plugins/billing/">`, `id="demo-perspective" class="demo-perspective hidden"`, `data-perspective="admin">管理员示教`, `data-perspective="user">普通用户视角`, `data-view="stats">统计`, `data-view="users">用户`, `id="view-users" class="view admin-only"`, `id="quota-chart"`, `id="page-buttons" class="page-buttons admin-only"`, `id="user-usage"`, `id="usage-filter-panel" class="usage-filter-panel admin-only"`, `id="refresh" class="admin-only"`, `id="editor-quota"`, `id="billing-ledger"`, `id="key-management" class="user-management"`, `id="reset-all-billing"`, `id="key-rows" class="user-card-grid"`, `class="surface price-panel price-layout"`, `src="./ui-config.js"`, `type="module" src="./app/main.js"`, `href="./styles/base.css"`, `href="./styles/keys.css"`, `href="./styles/usage.css"`, `href="./styles/pricing.css"`} {
for _, feature := range []string{`<body class="read-only">`, `<base href="/v0/resource/plugins/billing/">`, `id="demo-perspective" class="demo-perspective hidden"`, `data-perspective="admin">管理员示教`, `data-perspective="user">普通用户视角`, `data-view="stats">统计`, `data-view="users">用户`, `id="view-users" class="view admin-only"`, `id="quota-chart"`, `id="quota-health"`, `id="page-buttons" class="page-buttons admin-only"`, `id="user-usage"`, `id="usage-filter-panel" class="usage-filter-panel admin-only"`, `id="refresh" class="admin-only"`, `id="editor-show-stats"`, `id="editor-quota"`, `id="billing-ledger"`, `id="key-management" class="user-management"`, `id="reset-all-billing"`, `id="key-rows" class="user-card-grid"`, `class="surface price-panel price-layout"`, `src="./ui-config.js"`, `type="module" src="./app/main.js"`, `href="./styles/base.css"`, `href="./styles/keys.css"`, `href="./styles/usage.css"`, `href="./styles/pricing.css"`} {
if !strings.Contains(page, feature) {
t.Fatalf("UI does not contain HTML feature %q", feature)
}
@@ -694,7 +694,7 @@ func TestUsageResourceServesFeatureModules(t *testing.T) {
}
javascript.Write(response.Body)
}
for _, feature := range []string{"initializeRuntime", "dataFetch", "isManagementAuthorized", "adminPageSize = 100", "userPageSize = 50", `row.setAttribute("role", "button")`, "syncLongSectionVisibility", `return "compact"`, "isCompactEndpoint(record.endpoint)", `identity.title = "点击复制 Key"`, `return "自由选择"`, "剩余额度", "今日使用 / 剩余额度", "quotaUsage(item.cost, item.balance)", "resetTime(item)", `document.body.classList.toggle("read-only"`, "compactTokens(price.long_context.threshold_input_tokens)", "appendPriceRow(table, \"长上下文\", price.long_context, true)", `删除 ${model} 的真实价格配置`, `发现 ${changed.length} 个已关联价格发生变化`} {
for _, feature := range []string{"initializeRuntime", "dataFetch", "isManagementAuthorized", "adminPageSize = 100", "userPageSize = 50", `row.setAttribute("role", "button")`, "syncLongSectionVisibility", `return "compact"`, "isCompactEndpoint(record.endpoint)", `identity.title = "点击复制 Key"`, `return "自由选择"`, "今日使用", "剩余额度", "quotaAmount(item.cost)", "quotaAmount(item.balance)", "resetTime(item)", "show_in_stats", "renderQuotaHealth", `document.body.classList.toggle("read-only"`, "compactTokens(price.long_context.threshold_input_tokens)", "appendPriceRow(table, \"长上下文\", price.long_context, true)", `删除 ${model} 的真实价格配置`, `发现 ${changed.length} 个已关联价格发生变化`} {
if !strings.Contains(javascript.String(), feature) {
t.Fatalf("UI modules do not contain feature %q", feature)
}
+2 -1
View File
@@ -19,6 +19,7 @@ type readOnlyManagedKeyDTO struct {
RouteMode string `json:"route_mode"`
UpstreamAccountID string `json:"upstream_account_id,omitempty"`
AllModels bool `json:"all_models"`
ShowInStats bool `json:"show_in_stats"`
Models []string `json:"models"`
Billing billingStateDTO `json:"billing"`
}
@@ -99,7 +100,7 @@ func (a *App) readOnlyManagedKeys(includeArchived bool) ManagementResponse {
items = append(items, readOnlyManagedKeyDTO{
ID: key.ID, Name: key.Name, MaskedSecret: maskManagedSecret(key.Secret), Status: key.Status,
RouteMode: key.RouteMode, UpstreamAccountID: key.UpstreamAccountID,
AllModels: key.AllModels, Models: key.Models, Billing: billingStateResponse(state),
AllModels: key.AllModels, ShowInStats: key.ShowInStats, Models: key.Models, Billing: billingStateResponse(state),
})
}
return jsonManagementResponse(http.StatusOK, map[string]any{"keys": items})