- 系统配置键 log_raw_requests:开启后,仅管理员账号的每次请求 在用量明细中保存客户端原始请求体与上游原始响应体 (流式含全部 SSE 事件),用于排障 - UsageLog 新增 raw_request / raw_response 字段(type:text) - AuthLLM 附带 user_role 供网关判断管理员 - gateway:10s TTL 缓存开关;streamResponse/bufferResponse 支持累积上游原始响应;recordUsage 填充原始字段 - 前端 SystemConfig 新增开关(会显著增加存储的提示) - 新增 doc/flow.md 网关调用流程示意图
239 lines
9.9 KiB
Go
239 lines
9.9 KiB
Go
package store
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 角色 / 状态枚举
|
|
const (
|
|
RoleUser = "user"
|
|
RoleAdmin = "admin"
|
|
|
|
UserStatusActive = "active"
|
|
UserStatusDisabled = "disabled"
|
|
|
|
KeyStatusActive = "active"
|
|
KeyStatusRevoked = "revoked"
|
|
|
|
ChannelProviderOpenAI = "openai"
|
|
ChannelProviderAnthropic = "anthropic"
|
|
ChannelProviderCompatible = "compatible"
|
|
ChannelHealthHealthy = "healthy"
|
|
ChannelHealthDegraded = "degraded"
|
|
ChannelHealthCooldown = "cooldown"
|
|
|
|
FormatChat = "chat"
|
|
FormatResponses = "responses"
|
|
FormatMessages = "messages"
|
|
|
|
UsageStatusSuccess = "success"
|
|
UsageStatusError = "error"
|
|
UsageStatusCanceled = "canceled"
|
|
)
|
|
|
|
// User 用户
|
|
type User struct {
|
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
|
Username string `gorm:"uniqueIndex;size:64;not null" json:"username"`
|
|
Email string `gorm:"uniqueIndex;size:255;not null" json:"email"`
|
|
PasswordHash string `gorm:"size:255;not null" json:"-"`
|
|
Role string `gorm:"size:16;not null;default:user" json:"role"`
|
|
Balance float64 `gorm:"type:numeric(20,8);not null;default:0" json:"balance"`
|
|
Status string `gorm:"size:16;not null;default:active" json:"status"`
|
|
AllowedModels []string `gorm:"type:jsonb;serializer:json" json:"allowed_models,omitempty"`
|
|
DeniedModels []string `gorm:"type:jsonb;serializer:json" json:"denied_models,omitempty"`
|
|
InviteCode *string `json:"invite_code,omitempty"`
|
|
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// APIKey 密钥(SHA-256 hash 存储)
|
|
type APIKey struct {
|
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
|
UserID uint64 `gorm:"index;not null" json:"user_id"`
|
|
Name string `gorm:"size:64;not null" json:"name"`
|
|
KeyHash string `gorm:"uniqueIndex;size:64;not null" json:"-"`
|
|
KeyPlain string `gorm:"size:255;not null" json:"key_plain"`
|
|
KeyPrefix string `gorm:"size:32;not null" json:"key_prefix"`
|
|
QuotaTokensPerDay *int64 `json:"quota_tokens_per_day,omitempty"`
|
|
QuotaRequestsPerDay *int `json:"quota_requests_per_day,omitempty"`
|
|
AllowedModels []string `gorm:"type:jsonb;serializer:json" json:"allowed_models,omitempty"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
Status string `gorm:"size:16;not null;default:active" json:"status"`
|
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// Channel 上游渠道
|
|
type Channel struct {
|
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
|
Name string `gorm:"uniqueIndex;size:64;not null" json:"name"`
|
|
Provider string `gorm:"size:16;not null" json:"provider"`
|
|
Formats []string `gorm:"type:jsonb;serializer:json" json:"formats,omitempty"`
|
|
BaseURL string `gorm:"size:255;not null" json:"base_url"`
|
|
BaseURLs map[string]string `gorm:"type:jsonb;serializer:json" json:"base_urls,omitempty"`
|
|
APIKeyEnc string `gorm:"size:1024;not null" json:"-"`
|
|
// Weight 为 0 表示不参与加权随机选择(探活/回退语义),因此不能加 gorm
|
|
// default 标签 —— 零值字段会被 default 值覆盖,导致 0 被静默改写为 1。
|
|
Weight int `gorm:"not null" json:"weight"`
|
|
Priority int `gorm:"not null;default:0" json:"priority"`
|
|
TimeoutMS int `gorm:"not null;default:120000" json:"timeout_ms"`
|
|
MaxConcurrency int `gorm:"not null;default:16" json:"max_concurrency"`
|
|
HealthStatus string `gorm:"size:16;not null;default:healthy" json:"health_status"`
|
|
Enabled bool `gorm:"not null;default:true" json:"enabled"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// FormatsEffective 返回渠道实际支持的原生协议
|
|
func (c *Channel) FormatsEffective() []string {
|
|
if len(c.Formats) > 0 {
|
|
return c.Formats
|
|
}
|
|
switch c.Provider {
|
|
case ChannelProviderAnthropic:
|
|
return []string{FormatMessages}
|
|
case ChannelProviderOpenAI:
|
|
return []string{FormatChat, FormatResponses}
|
|
default:
|
|
return []string{FormatChat}
|
|
}
|
|
}
|
|
|
|
var versionSegRe = regexp.MustCompile(`/v[0-9]+/?$`)
|
|
|
|
// UpstreamURL 按协议选 base_url,拼资源路径
|
|
func (c *Channel) UpstreamURL(proto, path string) string {
|
|
base := c.BaseURL
|
|
if len(c.BaseURLs) > 0 && c.BaseURLs[proto] != "" {
|
|
base = c.BaseURLs[proto]
|
|
}
|
|
base = strings.TrimRight(base, "/")
|
|
if base == "" {
|
|
return path
|
|
}
|
|
if strings.HasSuffix(base, path) {
|
|
return base
|
|
}
|
|
if versionSegRe.MatchString(base) {
|
|
return base + path
|
|
}
|
|
return base + "/v1" + path
|
|
}
|
|
|
|
// Model 全局模型 + 定价(价格按每百万 token,USD)
|
|
type Model struct {
|
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
|
Name string `gorm:"uniqueIndex;size:128;not null" json:"name"`
|
|
DisplayName string `gorm:"size:128" json:"display_name"`
|
|
InputPrice float64 `gorm:"type:numeric(20,8);not null;default:0" json:"input_price"`
|
|
OutputPrice float64 `gorm:"type:numeric(20,8);not null;default:0" json:"output_price"`
|
|
CacheReadPrice float64 `gorm:"type:numeric(20,8);not null;default:0" json:"cache_read_price"`
|
|
Enabled bool `gorm:"not null;default:true" json:"enabled"`
|
|
Sort int `gorm:"not null;default:0" json:"sort"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ChannelModelBinding 渠道↔模型绑定(多对多)
|
|
type ChannelModelBinding struct {
|
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
|
ChannelID uint64 `gorm:"index:idx_ch_model,unique;not null" json:"channel_id"`
|
|
ModelID uint64 `gorm:"index:idx_ch_model,unique;not null" json:"model_id"`
|
|
UpstreamModel string `gorm:"size:255;not null" json:"upstream_model"`
|
|
Weight int `gorm:"not null;default:1" json:"weight"`
|
|
Channel Channel `gorm:"foreignKey:ChannelID" json:"-"`
|
|
Model Model `gorm:"foreignKey:ModelID" json:"-"`
|
|
}
|
|
|
|
// UsageLog 请求级用量明细
|
|
type UsageLog struct {
|
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
|
RequestID string `gorm:"size:128" json:"request_id"`
|
|
TraceID string `gorm:"size:64;index" json:"trace_id"`
|
|
UserID uint64 `gorm:"index:idx_user_created;not null" json:"user_id"`
|
|
KeyID uint64 `json:"key_id"`
|
|
ChannelID uint64 `json:"channel_id"`
|
|
ModelID uint64 `json:"model_id"`
|
|
ModelName string `gorm:"size:128" json:"model_name"`
|
|
Protocol string `gorm:"size:32" json:"protocol"`
|
|
InputTokens int64 `json:"input_tokens"`
|
|
OutputTokens int64 `json:"output_tokens"`
|
|
CacheReadTokens int64 `json:"cache_read_tokens"`
|
|
CacheCreationTokens int64 `json:"cache_creation_tokens"`
|
|
InputPrice float64 `gorm:"type:numeric(20,8)" json:"input_price"`
|
|
OutputPrice float64 `gorm:"type:numeric(20,8)" json:"output_price"`
|
|
CacheReadPrice float64 `gorm:"type:numeric(20,8)" json:"cache_read_price"`
|
|
Cost float64 `gorm:"type:numeric(20,8)" json:"cost"`
|
|
LatencyMS int `json:"latency_ms"`
|
|
Status string `gorm:"size:16;not null" json:"status"`
|
|
ErrorCode *string `json:"error_code,omitempty"`
|
|
RawRequest string `gorm:"type:text" json:"raw_request,omitempty"` // 客户端原始请求体(未转换;仅管理员+开关开启时记录)
|
|
RawResponse string `gorm:"type:text" json:"raw_response,omitempty"` // 上游原始响应(未转换;流式为全部 SSE 事件)
|
|
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
|
}
|
|
|
|
// UsageDaily 日粒度预聚合
|
|
type UsageDaily struct {
|
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
|
UserID uint64 `gorm:"index:idx_daily_user_model,unique" json:"user_id"`
|
|
ModelID uint64 `gorm:"index:idx_daily_user_model,unique" json:"model_id"`
|
|
Date string `gorm:"size:10;index:idx_daily_user_model,unique" json:"date"`
|
|
Requests int64 `json:"requests"`
|
|
InputTokens int64 `json:"input_tokens"`
|
|
OutputTokens int64 `json:"output_tokens"`
|
|
CacheReadTokens int64 `json:"cache_read_tokens"`
|
|
Cost float64 `gorm:"type:numeric(20,8)" json:"cost"`
|
|
}
|
|
|
|
// Passkey WebAuthn 凭据
|
|
type Passkey struct {
|
|
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
|
UserID uint64 `gorm:"index;not null" json:"user_id"`
|
|
Name string `gorm:"size:64" json:"name"`
|
|
CredentialID string `gorm:"size:255;not null" json:"-"`
|
|
PublicKey string `gorm:"size:512;not null" json:"-"`
|
|
AttestationType string `gorm:"size:64" json:"-"`
|
|
AAGUID string `gorm:"size:64" json:"-"`
|
|
SignCount uint64 `json:"-"`
|
|
DeviceType string `gorm:"size:255" json:"device_type,omitempty"`
|
|
LastUsedAt int64 `json:"last_used_at,omitempty"`
|
|
BackupEligible bool `json:"-"`
|
|
BackupState bool `json:"-"`
|
|
Transport string `gorm:"size:32" json:"-"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// SystemConfig 系统配置
|
|
type SystemConfig struct {
|
|
Key string `gorm:"primaryKey;size:64" json:"key"`
|
|
Value string `gorm:"type:jsonb;not null" json:"value"`
|
|
}
|
|
|
|
// AllModels 返回所有需要迁移的模型
|
|
func AllModels() []any {
|
|
return []any{
|
|
&User{},
|
|
&APIKey{},
|
|
&Channel{},
|
|
&Model{},
|
|
&ChannelModelBinding{},
|
|
&UsageLog{},
|
|
&UsageDaily{},
|
|
&Passkey{},
|
|
&SystemConfig{},
|
|
}
|
|
}
|
|
|
|
// HashAPIKey hashes an API key using SHA-256
|
|
func HashAPIKey(key string) string {
|
|
h := sha256.Sum256([]byte(key))
|
|
return hex.EncodeToString(h[:])
|
|
}
|