package plugin import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "errors" "net/http" "net/url" "strconv" "strings" "time" "unicode" managedaccess "billing/internal/access" "billing/internal/collection" "billing/internal/repository" ) type createManagedKeyRequest struct { Name string `json:"name"` Secret string `json:"secret,omitempty"` 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"` } type updateManagedKeyRequest struct { ID string `json:"id"` Name *string `json:"name,omitempty"` Status *string `json:"status,omitempty"` 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"` } type billingSettingsRequest struct { QuotaUSD string `json:"quota_usd"` ResetPeriod string `json:"reset_period"` NextResetAt *time.Time `json:"next_reset_at,omitempty"` MaxConcurrency int `json:"max_concurrency"` } type billingStateDTO struct { QuotaUSD string `json:"quota_usd"` SpentUSD string `json:"spent_usd"` LifetimeSpentUSD string `json:"lifetime_spent_usd"` BalanceUSD string `json:"balance_usd"` ResetPeriod string `json:"reset_period"` NextResetAt *time.Time `json:"next_reset_at,omitempty"` MaxConcurrency int `json:"max_concurrency"` ActiveRequests int `json:"active_requests"` CycleStartedAt time.Time `json:"cycle_started_at"` } type managedKeyDTO struct { managedaccess.ManagedKey Billing billingStateDTO `json:"billing"` } type ledgerEntryDTO struct { ID int64 `json:"id"` Kind string `json:"kind"` AmountUSD string `json:"amount_usd"` BalanceAfterUSD string `json:"balance_after_usd"` RequestID string `json:"request_id,omitempty"` ExecutionID string `json:"execution_id,omitempty"` Model string `json:"model,omitempty"` OccurredAt time.Time `json:"occurred_at"` } type archiveManagedKeyRequest struct { ID string `json:"id"` } type managedKeyStatsResponse struct { Stats managedaccess.KeyStats `json:"stats"` Recent []usageListItem `json:"recent"` } func (a *App) listManagedKeys(includeArchived bool) ManagementResponse { store, ok := a.currentStore() if !ok { return managementError(http.StatusServiceUnavailable, "database_unavailable", "Key 数据库尚未初始化") } keys, err := store.ListManagedKeys(context.Background(), includeArchived) if err != nil { return managementError(http.StatusInternalServerError, "database_error", err.Error()) } items := make([]managedKeyDTO, 0, len(keys)) for _, key := range keys { state, stateErr := store.BillingState(context.Background(), key.ID, time.Now().UTC()) if stateErr != nil { return managementError(http.StatusInternalServerError, "database_error", stateErr.Error()) } items = append(items, managedKeyResponse(key, state)) } return jsonManagementResponse(http.StatusOK, map[string]any{"keys": items}) } func (a *App) createManagedKey(body []byte) ManagementResponse { var req createManagedKeyRequest if err := json.Unmarshal(body, &req); err != nil { return managementError(http.StatusBadRequest, "invalid_request", "无法解析 Key 请求") } store, ok := a.currentStore() if !ok { return managementError(http.StatusServiceUnavailable, "database_unavailable", "Key 数据库尚未初始化") } name := strings.TrimSpace(req.Name) if name == "" || len(name) > 64 { return managementError(http.StatusBadRequest, "invalid_name", "Key 名称必须为 1-64 个字符") } secret := strings.TrimSpace(req.Secret) if secret == "" { var generateErr error secret, generateErr = generatedToken("cpa_") if generateErr != nil { return managementError(http.StatusInternalServerError, "random_unavailable", "无法生成 Key") } } if !validManagedSecret(secret) { return managementError(http.StatusBadRequest, "invalid_key", "Key 必须为 6-256 个不含空白或控制字符的字符") } keyID, err := generatedToken("key_") if err != nil { return managementError(http.StatusInternalServerError, "random_unavailable", "无法生成 Key ID") } key := managedaccess.ManagedKey{ ID: keyID, Name: name, Secret: secret, 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 key.UpstreamAccountID = template.UpstreamAccountID key.AllModels = template.AllModels key.Models = append([]string(nil), template.Models...) } if req.RouteMode != "" { key.RouteMode = req.RouteMode } if req.UpstreamAccountID != "" { key.UpstreamAccountID = strings.TrimSpace(req.UpstreamAccountID) } 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) } if key.RouteMode == managedaccess.RouteAuto { key.UpstreamAccountID = "" } if key.AllModels { key.Models = nil } if err := validateManagedKeyRule(store, key); err != nil { return managementError(http.StatusBadRequest, "invalid_rule", err.Error()) } var requestedBilling *managedaccess.BillingSettings if req.Billing != nil { settings, settingsErr := billingSettings(*req.Billing) if settingsErr != nil { return managementError(http.StatusBadRequest, "invalid_billing", settingsErr.Error()) } requestedBilling = &settings } if err := store.CreateManagedKey(context.Background(), key); err != nil { return managementError(http.StatusConflict, "key_conflict", err.Error()) } if requestedBilling != nil { if _, settingsErr := store.UpdateBilling(context.Background(), key.ID, *requestedBilling, time.Now().UTC()); settingsErr != nil { return managementError(http.StatusBadRequest, "invalid_billing", settingsErr.Error()) } } created, _ := store.ManagedKeyByID(context.Background(), key.ID) state, _ := store.BillingState(context.Background(), key.ID, time.Now().UTC()) return jsonManagementResponse(http.StatusCreated, managedKeyResponse(created, state)) } func (a *App) updateManagedKey(body []byte) ManagementResponse { var req updateManagedKeyRequest if err := json.Unmarshal(body, &req); err != nil || strings.TrimSpace(req.ID) == "" { return managementError(http.StatusBadRequest, "invalid_request", "缺少有效的 Key ID") } store, ok := a.currentStore() if !ok { return managementError(http.StatusServiceUnavailable, "database_unavailable", "Key 数据库尚未初始化") } key, err := store.ManagedKeyByID(context.Background(), req.ID) if err != nil { return managementError(http.StatusNotFound, "key_not_found", "Key 不存在") } if req.Name != nil { key.Name = strings.TrimSpace(*req.Name) } if req.Status != nil { key.Status = strings.TrimSpace(*req.Status) } if req.RouteMode != nil { key.RouteMode = strings.TrimSpace(*req.RouteMode) } if req.UpstreamAccountID != nil { key.UpstreamAccountID = strings.TrimSpace(*req.UpstreamAccountID) } 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) } if key.RouteMode == managedaccess.RouteAuto { key.UpstreamAccountID = "" } if key.AllModels { key.Models = nil } if key.Name == "" || len(key.Name) > 64 { return managementError(http.StatusBadRequest, "invalid_name", "Key 名称必须为 1-64 个字符") } if key.Status != managedaccess.StatusActive && key.Status != managedaccess.StatusDisabled { return managementError(http.StatusBadRequest, "invalid_status", "状态只能是 active 或 disabled") } if err := validateManagedKeyRule(store, key); err != nil { return managementError(http.StatusBadRequest, "invalid_rule", err.Error()) } var requestedBilling *managedaccess.BillingSettings if req.Billing != nil { settings, settingsErr := billingSettings(*req.Billing) if settingsErr != nil { return managementError(http.StatusBadRequest, "invalid_billing", settingsErr.Error()) } requestedBilling = &settings } if err := store.UpdateManagedKey(context.Background(), key); err != nil { status := http.StatusConflict if errors.Is(err, repository.ErrManagedKeyNotFound) { status = http.StatusNotFound } return managementError(status, "update_failed", err.Error()) } if requestedBilling != nil { if _, settingsErr := store.UpdateBilling(context.Background(), key.ID, *requestedBilling, time.Now().UTC()); settingsErr != nil { return managementError(http.StatusBadRequest, "invalid_billing", settingsErr.Error()) } } updated, _ := store.ManagedKeyByID(context.Background(), key.ID) state, _ := store.BillingState(context.Background(), key.ID, time.Now().UTC()) return jsonManagementResponse(http.StatusOK, managedKeyResponse(updated, state)) } func billingSettings(request billingSettingsRequest) (managedaccess.BillingSettings, error) { quota, err := parseDecimalMicros(request.QuotaUSD) if err != nil { return managedaccess.BillingSettings{}, errors.New("额度必须是非负美元金额,最多六位小数") } period := strings.TrimSpace(request.ResetPeriod) if period == "" { period = managedaccess.ResetNone } if period != managedaccess.ResetNone && period != managedaccess.ResetDaily && period != managedaccess.ResetWeekly && period != managedaccess.ResetMonthly { return managedaccess.BillingSettings{}, errors.New("重置周期只能是 none、daily、weekly 或 monthly") } if period != managedaccess.ResetNone && request.NextResetAt != nil && !request.NextResetAt.After(time.Now()) { return managedaccess.BillingSettings{}, errors.New("下次重置时间必须晚于当前时间") } maxConcurrency := request.MaxConcurrency if maxConcurrency == 0 { maxConcurrency = 4 } if maxConcurrency < 1 || maxConcurrency > 64 { return managedaccess.BillingSettings{}, errors.New("并发上限必须为 1-64") } return managedaccess.BillingSettings{QuotaMicros: quota, ResetPeriod: period, NextResetAt: request.NextResetAt, MaxConcurrency: maxConcurrency}, nil } func managedKeyResponse(key managedaccess.ManagedKey, state managedaccess.BillingState) managedKeyDTO { return managedKeyDTO{ManagedKey: key, Billing: billingStateResponse(state)} } func (a *App) resetManagedKeyBilling(body []byte) ManagementResponse { var request struct { ID string `json:"id"` All bool `json:"all"` } if err := json.Unmarshal(body, &request); err != nil || !request.All && strings.TrimSpace(request.ID) == "" { return managementError(http.StatusBadRequest, "invalid_request", "缺少有效的 Key ID") } store, ok := a.currentStore() if !ok { return managementError(http.StatusServiceUnavailable, "database_unavailable", "额度数据库尚未初始化") } if request.All { count, err := store.ResetAllBilling(context.Background(), time.Now().UTC()) if err != nil { return managementError(http.StatusBadRequest, "reset_failed", err.Error()) } return jsonManagementResponse(http.StatusOK, map[string]int{"reset_count": count}) } state, err := store.ResetBilling(context.Background(), request.ID, time.Now().UTC()) if err != nil { return managementError(http.StatusBadRequest, "reset_failed", err.Error()) } return jsonManagementResponse(http.StatusOK, billingStateDTO{ QuotaUSD: formatMicros(state.QuotaMicros), SpentUSD: formatMicros(state.SpentMicros), LifetimeSpentUSD: formatMicros(state.LifetimeSpentMicros), BalanceUSD: formatMicros(state.BalanceMicros), ResetPeriod: state.ResetPeriod, NextResetAt: state.NextResetAt, MaxConcurrency: state.MaxConcurrency, ActiveRequests: state.ActiveRequests, CycleStartedAt: state.CycleStartedAt, }) } func (a *App) managedKeyLedger(query url.Values) ManagementResponse { keyID := strings.TrimSpace(query.Get("id")) if keyID == "" { return managementError(http.StatusBadRequest, "invalid_request", "缺少有效的 Key ID") } before, _ := strconv.ParseInt(query.Get("before"), 10, 64) limit, _ := strconv.Atoi(query.Get("limit")) store, ok := a.currentStore() if !ok { return managementError(http.StatusServiceUnavailable, "database_unavailable", "额度数据库尚未初始化") } entries, err := store.ListBillingLedger(context.Background(), keyID, before, limit) if err != nil { return managementError(http.StatusInternalServerError, "database_error", err.Error()) } items := make([]ledgerEntryDTO, 0, len(entries)) for _, entry := range entries { items = append(items, ledgerEntryDTO{ID: entry.ID, Kind: entry.Kind, AmountUSD: formatMicros(entry.AmountMicros), BalanceAfterUSD: formatMicros(entry.BalanceAfterMicros), RequestID: entry.RequestID, ExecutionID: entry.ExecutionID, Model: entry.Model, OccurredAt: entry.OccurredAt}) } return jsonManagementResponse(http.StatusOK, map[string]any{"entries": items}) } func (a *App) archiveManagedKey(body []byte) ManagementResponse { var req archiveManagedKeyRequest if err := json.Unmarshal(body, &req); err != nil || strings.TrimSpace(req.ID) == "" { return managementError(http.StatusBadRequest, "invalid_request", "缺少有效的 Key ID") } store, ok := a.currentStore() if !ok { return managementError(http.StatusServiceUnavailable, "database_unavailable", "Key 数据库尚未初始化") } if err := store.ArchiveManagedKey(context.Background(), req.ID); err != nil { return managementError(http.StatusNotFound, "key_not_found", "Key 不存在或已经归档") } return jsonManagementResponse(http.StatusOK, map[string]bool{"archived": true}) } func (a *App) managedKeyStats(id string) ManagementResponse { store, ok := a.currentStore() if !ok { return managementError(http.StatusServiceUnavailable, "database_unavailable", "Key 数据库尚未初始化") } if _, err := store.ManagedKeyByID(context.Background(), id); err != nil { return managementError(http.StatusNotFound, "key_not_found", "Key 不存在") } location, _ := time.LoadLocation("Asia/Shanghai") now := time.Now().In(location) today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location) stats, err := store.KeyStats(context.Background(), id, today) if err != nil { return managementError(http.StatusInternalServerError, "database_error", err.Error()) } records, err := store.QueryUsage(context.Background(), collection.UsageQuery{Page: 1, PageSize: 50, KeyID: id}) if err != nil { return managementError(http.StatusInternalServerError, "database_error", err.Error()) } recent := make([]usageListItem, 0, len(records.Records)) for _, record := range records.Records { recent = append(recent, usageItem(record)) } return jsonManagementResponse(http.StatusOK, managedKeyStatsResponse{Stats: stats, Recent: recent}) } func (a *App) upstreamAccounts() ManagementResponse { syncErr := a.syncUpstreamAccounts(context.Background()) store, ok := a.currentStore() if !ok { return managementError(http.StatusServiceUnavailable, "database_unavailable", "上游账号数据库尚未初始化") } if observedErr := a.flushObservedUpstreams(context.Background(), store); observedErr != nil && syncErr == nil { syncErr = observedErr } accounts, err := store.ListUpstreamAccounts(context.Background()) if err != nil { return managementError(http.StatusInternalServerError, "database_error", err.Error()) } response := map[string]any{"accounts": accounts} if syncErr != nil { response["warning"] = syncErr.Error() } return jsonManagementResponse(http.StatusOK, response) } func (a *App) modelSuggestions() ManagementResponse { store, ok := a.currentStore() if !ok { return managementError(http.StatusServiceUnavailable, "database_unavailable", "数据库尚未初始化") } models, err := store.ModelSuggestions(context.Background()) if err != nil { return managementError(http.StatusInternalServerError, "database_error", err.Error()) } return jsonManagementResponse(http.StatusOK, map[string]any{"models": models}) } func (a *App) currentStore() (*repository.SQLiteUsageRepository, bool) { a.mu.RLock() defer a.mu.RUnlock() return a.store, !a.closed && a.store != nil } func validateManagedKeyRule(store *repository.SQLiteUsageRepository, key managedaccess.ManagedKey) error { if key.RouteMode != managedaccess.RouteAuto && key.RouteMode != managedaccess.RouteStrict { return errors.New("路由模式只能是 auto 或 strict") } if key.RouteMode == managedaccess.RouteAuto { key.UpstreamAccountID = "" } else { if strings.TrimSpace(key.UpstreamAccountID) == "" { return errors.New("严格路由必须选择上游账号") } if _, err := store.UpstreamAccountByID(context.Background(), key.UpstreamAccountID); err != nil { return errors.New("选择的上游账号不存在") } } if !key.AllModels && len(normalizeModels(key.Models)) == 0 { return errors.New("未允许全部模型时至少配置一个模型") } for _, pattern := range key.Models { if len(pattern) > 128 || strings.IndexFunc(pattern, func(value rune) bool { return unicode.IsSpace(value) || unicode.IsControl(value) }) >= 0 { return errors.New("模型规则不能包含空白或控制字符,且最长 128 个字符") } } return nil } func normalizeModels(models []string) []string { seen := make(map[string]struct{}) result := make([]string, 0, len(models)) for _, raw := range models { model := strings.ToLower(strings.TrimSpace(raw)) if model == "" { continue } if _, exists := seen[model]; exists { continue } seen[model] = struct{}{} result = append(result, model) } return result } func generatedToken(prefix string) (string, error) { raw := make([]byte, 24) if _, err := rand.Read(raw); err != nil { return "", err } return prefix + base64.RawURLEncoding.EncodeToString(raw), nil } func validManagedSecret(secret string) bool { if len(secret) < 6 || len(secret) > 256 { return false } for _, value := range secret { if unicode.IsSpace(value) || unicode.IsControl(value) { return false } } return true } func managementError(status int, code, message string) ManagementResponse { return jsonManagementResponse(status, map[string]any{"error": map[string]string{"code": code, "message": message}}) }