M0+M1: 基建 + 用户/密钥/核心代理
后端 (Go/Gin/GORM): - 配置(viper+env)、SQLite/Postgres 迁移、argon2id、AES-GCM 渠道密钥、JWT+refresh cookie - 用户注册/登录/刷新/登出、API Key CRUD(仅存哈希、明文一次展示) - 代理网关: /v1/chat/completions、/v1/responses、/v1/models 直通 OpenAI 渠道 非流式+流式(SSE 零缓冲转发), 用量捕获(chat 末块/responses completed 嵌套), OpenAI 错误格式(401/402/404/502), 余额检查 - 异步批量记账 + 余额流水 + 日聚合, admin 用户/余额/配置 API - 单测: crypto/jwt/apikey/流式 usage 提取 前端 (Vue3+TS+Vite+Tailwind v4): - taste-skill 设计 tokens: 深色仪表盘, 石墨+信号铜色, Outfit+JetBrains Mono - Landing/登录/注册, 控制台(仪表盘图表/密钥管理/用量明细) - 基础组件 Button/Input/Badge/Modal, ECharts 用量图 部署: docker-compose(nginx+api+postgres), 双 Dockerfile, nginx SSE 反代 联调: scripts/mockupstream 本地 mock 上游, 端到端验证通过
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// Package channel 渠道仓储:选择、加解密、健康过滤。
|
||||
// M1 阶段实现最小选择逻辑(按优先级+权重取第一个健康启用的渠道),
|
||||
// 负载均衡/健康检查/故障转移在 M4 完善。
|
||||
package channel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/openteam/server/internal/pkg/crypto"
|
||||
"github.com/openteam/server/internal/store"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var ErrNoChannel = errors.New("no available channel")
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
enc *crypto.Encryptor
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB, enc *crypto.Encryptor) *Service {
|
||||
return &Service{db: db, enc: enc}
|
||||
}
|
||||
|
||||
// Select 选择处理请求的渠道:启用 + 健康,按 priority 升序、weight 降序。
|
||||
func (s *Service) Select() (*store.Channel, error) {
|
||||
var chs []store.Channel
|
||||
if err := s.db.Where("enabled = ? AND health_status = ?", true, store.ChannelHealthHealthy).
|
||||
Order("priority ASC, weight DESC, id ASC").Find(&chs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(chs) == 0 {
|
||||
return nil, ErrNoChannel
|
||||
}
|
||||
return &chs[0], nil
|
||||
}
|
||||
|
||||
// UpstreamKey 解密渠道上游密钥。
|
||||
func (s *Service) UpstreamKey(ch *store.Channel) (string, error) {
|
||||
return s.enc.Decrypt(ch.APIKeyEnc)
|
||||
}
|
||||
|
||||
// ResolveModel 按全局模型名找到绑定渠道;M1 简化:返回绑定该模型的第一个健康渠道。
|
||||
func (s *Service) ResolveModel(modelName string) (*store.Channel, *store.ChannelModelBinding, error) {
|
||||
var m store.Model
|
||||
if err := s.db.Where("name = ? AND enabled = ?", modelName, true).First(&m).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var b store.ChannelModelBinding
|
||||
if err := s.db.Where("model_id = ?", m.ID).
|
||||
Joins("JOIN channels ON channels.id = channel_model_bindings.channel_id AND channels.enabled = ? AND channels.health_status = ?", true, store.ChannelHealthHealthy).
|
||||
Order("channel_model_bindings.weight DESC").
|
||||
First(&b).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ch, err := s.Select()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// 用绑定里的渠道(如果健康),否则回退默认渠道
|
||||
if b.ChannelID != ch.ID {
|
||||
var bound store.Channel
|
||||
if err := s.db.First(&bound, b.ChannelID).Error; err == nil && bound.Enabled && bound.HealthStatus == store.ChannelHealthHealthy {
|
||||
return &bound, &b, nil
|
||||
}
|
||||
}
|
||||
return ch, &b, nil
|
||||
}
|
||||
Reference in New Issue
Block a user