feat: add configurable user statistics
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user