Files
cpa-plugin/internal/plugin/readonly_management.go
T

135 lines
4.7 KiB
Go

package plugin
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strings"
"time"
managedaccess "billing/internal/access"
)
type readOnlyManagedKeyDTO struct {
ID string `json:"id"`
Name string `json:"name"`
MaskedSecret string `json:"masked_secret"`
Status string `json:"status"`
RouteMode string `json:"route_mode"`
UpstreamAccountID string `json:"upstream_account_id,omitempty"`
AllModels bool `json:"all_models"`
Models []string `json:"models"`
Billing billingStateDTO `json:"billing"`
}
func (a *App) readOnlyResponse(query url.Values) ManagementResponse {
switch strings.TrimSpace(query.Get("view")) {
case "keys":
return a.readOnlyManagedKeys(query.Get("include_archived") == "1")
case "usage":
return a.readOnlyUsageResponse(query)
case "usage-summary":
return a.usageDashboardResponse()
case "prices":
return a.listPrices()
case "price-catalog":
return a.searchPriceCatalog(query)
case "upstreams":
return a.readOnlyUpstreamAccounts()
case "model-suggestions":
return a.modelSuggestions()
case "key-stats":
return a.readOnlyManagedKeyStats(strings.TrimSpace(query.Get("id")))
default:
return managementError(http.StatusBadRequest, "invalid_view", "只读资源类型不存在")
}
}
func (a *App) readOnlyUsageResponse(query url.Values) ManagementResponse {
limited := url.Values{"page": {"1"}, "page_size": {"50"}}
response := a.usageResponse(limited)
if response.StatusCode != http.StatusOK {
return response
}
var payload usageListResponse
if err := json.Unmarshal(response.Body, &payload); err != nil {
return managementError(http.StatusInternalServerError, "response_error", "无法生成只读请求明细")
}
redactUsageItems(payload.Records)
return jsonManagementResponse(http.StatusOK, payload)
}
func (a *App) readOnlyManagedKeyStats(id string) ManagementResponse {
response := a.managedKeyStats(id)
if response.StatusCode != http.StatusOK {
return response
}
var payload managedKeyStatsResponse
if err := json.Unmarshal(response.Body, &payload); err != nil {
return managementError(http.StatusInternalServerError, "response_error", "无法生成只读 Key 统计")
}
redactUsageItems(payload.Recent)
return jsonManagementResponse(http.StatusOK, payload)
}
func redactUsageItems(items []usageListItem) {
for index := range items {
items[index].APIKey = ""
}
}
func (a *App) readOnlyManagedKeys(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([]readOnlyManagedKeyDTO, 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, 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),
})
}
return jsonManagementResponse(http.StatusOK, map[string]any{"keys": items})
}
func (a *App) readOnlyUpstreamAccounts() ManagementResponse {
store, ok := a.currentStore()
if !ok {
return managementError(http.StatusServiceUnavailable, "database_unavailable", "上游账号数据库尚未初始化")
}
accounts, err := store.ListUpstreamAccounts(context.Background())
if err != nil {
return managementError(http.StatusInternalServerError, "database_error", err.Error())
}
return jsonManagementResponse(http.StatusOK, map[string]any{"accounts": accounts})
}
func maskManagedSecret(secret string) string {
characters := []rune(secret)
if len(characters) < 6 {
return "******"
}
return string(characters[:2]) + "******" + string(characters[len(characters)-4:])
}
func billingStateResponse(state managedaccess.BillingState) billingStateDTO {
return 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,
}
}