328 lines
11 KiB
Go
328 lines
11 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
managedaccess "cpa-ext/internal/access"
|
|
"cpa-ext/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"`
|
|
Models *[]string `json:"models,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"`
|
|
Models *[]string `json:"models,omitempty"`
|
|
}
|
|
|
|
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())
|
|
}
|
|
return jsonManagementResponse(http.StatusOK, map[string]any{"keys": keys})
|
|
}
|
|
|
|
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 必须为 1-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,
|
|
}
|
|
a.mu.RLock()
|
|
defaultName := a.config.BootstrapName
|
|
a.mu.RUnlock()
|
|
if template, err := store.ManagedKeyByName(context.Background(), defaultName); 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.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())
|
|
}
|
|
if err := store.CreateManagedKey(context.Background(), key); err != nil {
|
|
return managementError(http.StatusConflict, "key_conflict", err.Error())
|
|
}
|
|
created, _ := store.ManagedKeyByID(context.Background(), key.ID)
|
|
return jsonManagementResponse(http.StatusCreated, created)
|
|
}
|
|
|
|
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.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())
|
|
}
|
|
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())
|
|
}
|
|
updated, _ := store.ManagedKeyByID(context.Background(), key.ID)
|
|
return jsonManagementResponse(http.StatusOK, updated)
|
|
}
|
|
|
|
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.ListRecent(context.Background(), 1000)
|
|
if err != nil {
|
|
return managementError(http.StatusInternalServerError, "database_error", err.Error())
|
|
}
|
|
recent := make([]usageListItem, 0, 50)
|
|
for _, record := range records {
|
|
if record.ManagedKeyID == id {
|
|
recent = append(recent, usageItem(record))
|
|
if len(recent) == 50 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
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("未允许全部模型时至少配置一个模型")
|
|
}
|
|
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 secret == "" || 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}})
|
|
}
|