后端 (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 上游, 端到端验证通过
69 lines
2.2 KiB
Go
69 lines
2.2 KiB
Go
// 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
|
|
}
|