feat: Adobe积分号不锁定额度 + Nano Banana双通道 + 前端显示额度

- Adobe不再区分视频/图片额度,移除ImageLimited/VideoLimited限制
- Adobe积分号quota耗尽不锁定(status不改为quota),只记录失败用于轮转
- Nano Banana 2/Pro 保留Runway通道,新增nano-banana-2/nano-banana-pro走Adobe
- Adobe模型重命名: runway-nano-banana-2, runway-nano-banana-pro
- 前端管理后台Adobe账号显示积分余额(之前固定显示—)
- README更新模型列表
This commit is contained in:
2026-08-02 20:09:03 +08:00
parent 359d95b8c1
commit 4987717d23
8 changed files with 79 additions and 54 deletions
+13 -37
View File
@@ -1155,14 +1155,7 @@ func (s *V1Service) hasActiveProviderToken(ctx context.Context, provider, kind s
if item.Status != "active" || item.Dead || strings.TrimSpace(item.Value) == "" {
continue
}
if provider == "adobe" {
if kind == "video" && item.VideoLimited {
continue
}
if kind == "image" && item.ImageLimited {
continue
}
}
// Adobe accounts are credit-based (积分号) — no per-kind quota locks.
return true, nil
}
return false, nil
@@ -1617,11 +1610,9 @@ func (s *V1Service) generateAdobeImage(ctx context.Context, eventID string, mode
}
var active []model.TokenAccount
for _, item := range items {
// Image quota is tracked separately from video — an account whose video
// quota is exhausted (VideoLimited) is still usable for image as long as
// its image quota remains. status=="quota" means BOTH kinds are limited
// (or a legacy/full quota mark), so it's excluded for either kind.
if item.Status == "active" && !item.Dead && !item.ImageLimited && strings.TrimSpace(item.Value) != "" {
// Adobe accounts are credit-based (积分号) — no per-kind quota locks.
// Only skip accounts that are dead or disabled.
if item.Status == "active" && !item.Dead && strings.TrimSpace(item.Value) != "" {
active = append(active, item)
}
}
@@ -1677,12 +1668,9 @@ func (s *V1Service) generateAdobeVideo(ctx context.Context, eventID string, mode
}
var active []model.TokenAccount
for _, item := range items {
// Video quota is tracked separately from image — skip accounts whose
// video quota is exhausted (VideoLimited), but an image-only limit
// (ImageLimited) leaves the account usable for video. status=="quota"
// means BOTH kinds are limited (or a legacy/full quota mark), so it's
// excluded for either kind.
if item.Status == "active" && !item.Dead && !item.VideoLimited && strings.TrimSpace(item.Value) != "" {
// Adobe accounts are credit-based (积分号) — no per-kind quota locks.
// Only skip accounts that are dead or disabled.
if item.Status == "active" && !item.Dead && strings.TrimSpace(item.Value) != "" {
active = append(active, item)
}
}
@@ -3211,28 +3199,16 @@ func (s *V1Service) markTokenFailure(ctx context.Context, pool string, token mod
}
switch {
case isQuota:
// Adobe quota is per-kind: a video-quota error must not block image
// requests (and vice-versa). Flag only the failing kind, and only sink
// the account into the shared "quota" waiting status once BOTH kinds are
// limited. Other pools (chatgpt) are single-kind, so they go straight to
// "quota" as before.
// Adobe accounts are credit-based (积分号) — quota exhaustion is
// non-locking: track the failure for rotation but don't limit or
// sink the account. Other pools go straight to "quota" as before.
if pool == "adobe" {
imageLimited := token.ImageLimited
videoLimited := token.VideoLimited
if kind == "video" {
videoLimited = true
patch["video_limited"] = true
} else {
imageLimited = true
patch["image_limited"] = true
}
if imageLimited && videoLimited {
patch["status"] = "quota"
}
// No-op: just track fails (already patched above), leave
// image_limited/video_limited/status untouched.
} else {
patch["status"] = "quota"
}
if strings.TrimSpace(token.CachedQuotaResetAfter) == "" {
if pool != "adobe" && strings.TrimSpace(token.CachedQuotaResetAfter) == "" {
recoverAt := time.Unix((time.Now().Unix()/86400+1)*86400, 0).UTC()
patch["quota_recover_at"] = &recoverAt
}