feat!: 账号池改渠道网关,对外协议换成 vivid /v1
- 新增 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>
This commit is contained in:
@@ -28,18 +28,18 @@ type AdminWriteService struct {
|
||||
models *repo.ModelRepository
|
||||
events *repo.EventRepository
|
||||
apiKeys *repo.APIKeyRepository
|
||||
tokens *repo.TokenRepository
|
||||
channels *repo.ChannelRepository
|
||||
orders *repo.OrderRepository
|
||||
}
|
||||
|
||||
func NewAdminWriteService(users *repo.UserRepository, showcase *repo.ShowcaseRepository, models *repo.ModelRepository, events *repo.EventRepository, apiKeys *repo.APIKeyRepository, tokens *repo.TokenRepository, orders *repo.OrderRepository) *AdminWriteService {
|
||||
func NewAdminWriteService(users *repo.UserRepository, showcase *repo.ShowcaseRepository, models *repo.ModelRepository, events *repo.EventRepository, apiKeys *repo.APIKeyRepository, channels *repo.ChannelRepository, orders *repo.OrderRepository) *AdminWriteService {
|
||||
return &AdminWriteService{
|
||||
users: users,
|
||||
showcase: showcase,
|
||||
models: models,
|
||||
events: events,
|
||||
apiKeys: apiKeys,
|
||||
tokens: tokens,
|
||||
channels: channels,
|
||||
orders: orders,
|
||||
}
|
||||
}
|
||||
@@ -371,7 +371,7 @@ func (s *AdminWriteService) CreateModel(ctx context.Context, body map[string]any
|
||||
return nil, errors.New("type must be image or video")
|
||||
}
|
||||
if provider == "" {
|
||||
return nil, errors.New("provider required")
|
||||
provider = "channel"
|
||||
}
|
||||
if err := s.validateModelNameSpace(ctx, "", modelID, alias); err != nil {
|
||||
return nil, err
|
||||
@@ -436,10 +436,9 @@ func (s *AdminWriteService) UpdateModel(ctx context.Context, modelID string, bod
|
||||
}
|
||||
if _, ok := body["provider"]; ok {
|
||||
provider := strings.TrimSpace(stringValue(body["provider"]))
|
||||
if provider == "" {
|
||||
return nil, errors.New("provider required")
|
||||
if provider != "" {
|
||||
patch["provider"] = provider
|
||||
}
|
||||
patch["provider"] = provider
|
||||
}
|
||||
// Only touch `enabled` when the caller explicitly sends a non-null value;
|
||||
// mirrors Python models_store.update ("enabled" in fields and is not None).
|
||||
@@ -488,6 +487,18 @@ func (s *AdminWriteService) UpdateModel(ctx context.Context, modelID string, bod
|
||||
if _, ok := body["reference_mode"]; ok {
|
||||
patch["reference_mode"] = defaultString(strings.TrimSpace(stringValue(body["reference_mode"])), "none")
|
||||
}
|
||||
if _, ok := body["duration_min"]; ok {
|
||||
patch["duration_min"] = intValue(body["duration_min"])
|
||||
}
|
||||
if _, ok := body["duration_max"]; ok {
|
||||
patch["duration_max"] = intValue(body["duration_max"])
|
||||
}
|
||||
if _, ok := body["max_ref_video_seconds"]; ok {
|
||||
patch["max_ref_video_seconds"] = intValue(body["max_ref_video_seconds"])
|
||||
}
|
||||
if _, ok := body["max_ref_audio_seconds"]; ok {
|
||||
patch["max_ref_audio_seconds"] = intValue(body["max_ref_audio_seconds"])
|
||||
}
|
||||
if _, ok := body["weight"]; ok {
|
||||
patch["weight"] = intValue(body["weight"])
|
||||
}
|
||||
@@ -532,52 +543,14 @@ func (s *AdminWriteService) DeleteModel(ctx context.Context, modelID string) err
|
||||
if rows == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
// Clean up: strip this model id from every custom upstream account's
|
||||
// supported-models list so no orphan reference is left behind.
|
||||
s.removeModelFromUpstreams(ctx, modelID)
|
||||
// Clean up: drop every channel binding pointing at the deleted model so no
|
||||
// orphan reference is left behind.
|
||||
if s.channels != nil {
|
||||
_ = s.channels.DeleteBindingsByModel(ctx, modelID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeModelFromUpstreams drops modelID from the CSV in each custom account's
|
||||
// Meta["models"]. Best-effort — a failure here doesn't undo the model delete.
|
||||
func (s *AdminWriteService) removeModelFromUpstreams(ctx context.Context, modelID string) {
|
||||
if s.tokens == nil {
|
||||
return
|
||||
}
|
||||
items, err := s.tokens.ListByPool(ctx, "custom")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, it := range items {
|
||||
raw, _ := it.Meta["models"].(string)
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
continue
|
||||
}
|
||||
kept := make([]string, 0, len(strings.Split(raw, ",")))
|
||||
changed := false
|
||||
for _, p := range strings.Split(raw, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if p == modelID {
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
kept = append(kept, p)
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
meta := datatypes.JSONMap{}
|
||||
for k, v := range it.Meta {
|
||||
meta[k] = v
|
||||
}
|
||||
meta["models"] = strings.Join(kept, ",")
|
||||
_, _ = s.tokens.Update(ctx, "custom", it.ID, map[string]any{"meta": meta})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AdminWriteService) ClearLogs(ctx context.Context) (int64, error) {
|
||||
return s.events.DeleteAll(ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user