- 新增 channels/channel_models:上游 URL + AES-GCM 加密 key +
并发/权重/本地统计,模型经渠道 /v1/models 导入绑定
- internal/upstream:vivid 协议客户端(NDJSON 流 + jobId 断流重连 +
uploads 预签名 + models + balance + 错误分类)
- service/v1 重写:权重分组调度、Redis 并发槽 conc:c:、故障转移,
站内/测试/对外统一走渠道执行器
- 对外端点换成 vivid 四件套:POST /v1/generate(NDJSON+jobId 重连)、
POST /v1/uploads(SigV4 预签名 PUT)、GET /v1/models、GET /v1/balance,
错误包 {"error":{"code","message"}}
- API key 新发改 vk_+48hex;删除全部内置 provider 与 token 池
- 管理端:渠道 CRUD/拉模型/导入/余额端点,前端账号页换渠道页,
概览/文档/测试弹窗适配
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
139 lines
3.2 KiB
Go
139 lines
3.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"backend/internal/model"
|
|
"backend/internal/repo"
|
|
)
|
|
|
|
type APIKeyService struct {
|
|
keys *repo.APIKeyRepository
|
|
}
|
|
|
|
func NewAPIKeyService(keys *repo.APIKeyRepository) *APIKeyService {
|
|
return &APIKeyService{keys: keys}
|
|
}
|
|
|
|
func (s *APIKeyService) Current(ctx context.Context, userID string) (map[string]any, error) {
|
|
keys, err := s.keys.ListByUserID(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(keys) == 0 {
|
|
return map[string]any{"key": nil}, nil
|
|
}
|
|
key := keys[0]
|
|
return map[string]any{
|
|
"key": map[string]any{
|
|
"id": key.ID,
|
|
"name": key.Name,
|
|
"key_preview": key.KeyPreview,
|
|
"created_at": key.CreatedAt,
|
|
"last_used_at": key.LastUsedAt,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *APIKeyService) Mint(ctx context.Context, userID string) (map[string]any, error) {
|
|
plain, err := generatePlainAPIKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key := &model.APIKey{
|
|
ID: "k-" + time.Now().Format("150405") + randomSuffix(2),
|
|
UserID: userID,
|
|
Name: "default",
|
|
KeyPreview: previewAPIKey(plain),
|
|
KeyHash: hashAPIKey(plain),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
if err := s.keys.ReplaceForUser(ctx, userID, key); err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{
|
|
"ok": true,
|
|
"key": plain,
|
|
"preview": key.KeyPreview,
|
|
}, nil
|
|
}
|
|
|
|
func (s *APIKeyService) Revoke(ctx context.Context, userID string) error {
|
|
return s.keys.DeleteByUserID(ctx, userID)
|
|
}
|
|
|
|
func (s *APIKeyService) MintNamed(ctx context.Context, userID, name string, replace bool) (map[string]any, error) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
name = "default"
|
|
}
|
|
plain, err := generatePlainAPIKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key := &model.APIKey{
|
|
ID: "k-" + time.Now().Format("150405") + randomSuffix(2),
|
|
UserID: userID,
|
|
Name: name,
|
|
KeyPreview: previewAPIKey(plain),
|
|
KeyHash: hashAPIKey(plain),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
if replace {
|
|
if err := s.keys.ReplaceForUser(ctx, userID, key); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if err := s.keys.Create(ctx, key); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"ok": true,
|
|
"key": plain,
|
|
"preview": key.KeyPreview,
|
|
"id": key.ID,
|
|
"name": key.Name,
|
|
}, nil
|
|
}
|
|
|
|
func (s *APIKeyService) DeleteOne(ctx context.Context, userID, keyID string) error {
|
|
if strings.TrimSpace(keyID) == "" {
|
|
return errors.New("key id required")
|
|
}
|
|
return s.keys.DeleteByID(ctx, userID, keyID)
|
|
}
|
|
|
|
// generatePlainAPIKey mints a vivid-shaped key: vk_ + 48 lowercase hex chars
|
|
// (upstream channel api_keys use the same shape, so one key format rules
|
|
// everything — no per-direction prefixes).
|
|
func generatePlainAPIKey() (string, error) {
|
|
b := make([]byte, 24)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return "vk_" + hex.EncodeToString(b), nil
|
|
}
|
|
|
|
func previewAPIKey(plain string) string {
|
|
if len(plain) <= 4 {
|
|
return strings.Repeat("•", len(plain))
|
|
}
|
|
return "…" + plain[len(plain)-4:]
|
|
}
|
|
|
|
func hashAPIKey(plain string) string {
|
|
sum := sha256.Sum256([]byte(plain))
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func randomSuffix(n int) string {
|
|
return randomUpper(n)
|
|
}
|