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:
@@ -453,7 +453,7 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error)
|
||||
"description": "Adobe Flux Kontext Max",
|
||||
},
|
||||
{
|
||||
"id": "nano-banana-2",
|
||||
"id": "runway-nano-banana-2",
|
||||
"provider": "runway",
|
||||
"type": "image",
|
||||
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
|
||||
@@ -464,7 +464,7 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error)
|
||||
"description": "Runway Nano Banana 2 (图/参考图)",
|
||||
},
|
||||
{
|
||||
"id": "nano-banana-pro",
|
||||
"id": "runway-nano-banana-pro",
|
||||
"provider": "runway",
|
||||
"type": "image",
|
||||
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
|
||||
@@ -474,6 +474,28 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error)
|
||||
"reference_mode": "asset",
|
||||
"description": "Runway Nano Banana Pro (图/参考图)",
|
||||
},
|
||||
{
|
||||
"id": "nano-banana-2",
|
||||
"provider": "adobe",
|
||||
"type": "image",
|
||||
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
|
||||
"resolutions": []string{"1K", "2K", "4K"},
|
||||
"image_to_image": true,
|
||||
"max_reference_images": 6,
|
||||
"reference_mode": "asset",
|
||||
"description": "Nano Banana 2 (图/参考图)",
|
||||
},
|
||||
{
|
||||
"id": "nano-banana-pro",
|
||||
"provider": "adobe",
|
||||
"type": "image",
|
||||
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
|
||||
"resolutions": []string{"1K", "2K", "4K"},
|
||||
"image_to_image": true,
|
||||
"max_reference_images": 6,
|
||||
"reference_mode": "asset",
|
||||
"description": "Nano Banana Pro (图/参考图)",
|
||||
},
|
||||
{
|
||||
"id": "gemini-veo31",
|
||||
"provider": "adobe",
|
||||
@@ -625,7 +647,7 @@ func (h *UserGenerationHandler) publicModels() ([]gin.H, error) {
|
||||
"stub": false,
|
||||
},
|
||||
{
|
||||
"id": "nano-banana-2",
|
||||
"id": "runway-nano-banana-2",
|
||||
"provider": "runway",
|
||||
"kind": "image",
|
||||
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
|
||||
@@ -634,7 +656,7 @@ func (h *UserGenerationHandler) publicModels() ([]gin.H, error) {
|
||||
"stub": false,
|
||||
},
|
||||
{
|
||||
"id": "nano-banana-pro",
|
||||
"id": "runway-nano-banana-pro",
|
||||
"provider": "runway",
|
||||
"kind": "image",
|
||||
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
|
||||
@@ -642,6 +664,24 @@ func (h *UserGenerationHandler) publicModels() ([]gin.H, error) {
|
||||
"description": "Runway Nano Banana Pro",
|
||||
"stub": false,
|
||||
},
|
||||
{
|
||||
"id": "nano-banana-2",
|
||||
"provider": "adobe",
|
||||
"kind": "image",
|
||||
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
|
||||
"resolutions": []string{"1K", "2K", "4K"},
|
||||
"description": "Nano Banana 2",
|
||||
"stub": false,
|
||||
},
|
||||
{
|
||||
"id": "nano-banana-pro",
|
||||
"provider": "adobe",
|
||||
"kind": "image",
|
||||
"ratios": []string{"1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"},
|
||||
"resolutions": []string{"1K", "2K", "4K"},
|
||||
"description": "Nano Banana Pro",
|
||||
"stub": false,
|
||||
},
|
||||
{
|
||||
"id": "gemini-veo31",
|
||||
"provider": "adobe",
|
||||
|
||||
@@ -52,6 +52,10 @@ func ResolveModelSpec(modelID string) modelSpec {
|
||||
return modelSpec{UpstreamModelID: "gpt-image", UpstreamModelVersion: "2"}
|
||||
case "flux-kontext-max":
|
||||
return modelSpec{UpstreamModelID: "flux", UpstreamModelVersion: "fluxKontextMax"}
|
||||
case "nano-banana-2", "runway-nano-banana-2":
|
||||
return modelSpec{UpstreamModelID: "gemini-flash", UpstreamModelVersion: "nano-banana-2"}
|
||||
case "nano-banana-pro", "runway-nano-banana-pro":
|
||||
return modelSpec{UpstreamModelID: "gemini-flash", UpstreamModelVersion: "nano-banana-pro"}
|
||||
default:
|
||||
return modelSpec{UpstreamModelID: "gemini-flash", UpstreamModelVersion: "nano-banana-3"}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// GenerateImage runs a Runway gemini image text/image-to-image pipeline:
|
||||
// upload each reference image (DATASET + DATASET_PREVIEW → dataset) to obtain
|
||||
// its {assetId, url}, create a gemini image task and poll it to completion,
|
||||
// then download the rendered PNG. modelID selects the variant: "nano-banana-2"
|
||||
// then download the rendered PNG. modelID selects the variant: "runway-nano-banana-2"
|
||||
// → gemini_3_1_flash_image / gemini-3.1-flash-image-preview (with aspect_ratio),
|
||||
// anything else → workflow_gemini_image / gemini-3-pro-image-preview. imageSize
|
||||
// is the "1K"/"2K"/"4K" tier. teamID is the workspace id; if empty it's derived
|
||||
@@ -147,7 +147,7 @@ func (c *Client) createImageTask(ctx context.Context, client tlsclient.HttpClien
|
||||
"exploreMode": false,
|
||||
"creationSource": "tool-mode",
|
||||
}
|
||||
if modelID == "nano-banana-2" {
|
||||
if modelID == "runway-nano-banana-2" {
|
||||
taskType = "gemini_3_1_flash_image"
|
||||
opts["name"] = "Nano Banana 2 - " + prompt
|
||||
opts["model"] = "gemini-3.1-flash-image-preview"
|
||||
|
||||
@@ -241,8 +241,8 @@ const leonardoDailyTokens = 150
|
||||
func (r *TokenRepository) RecoverQuota(ctx context.Context) ([]model.TokenAccount, error) {
|
||||
// Also pick up accounts that are only single-kind limited (image_limited /
|
||||
// video_limited) — those keep status "active" and would otherwise never have
|
||||
// their per-kind flag cleared. Adobe resets both kinds at once, so the shared
|
||||
// reset time gates recovery for all of them.
|
||||
// their per-kind flag cleared. This is legacy support; Adobe no longer sets
|
||||
// per-kind limits.
|
||||
var items []model.TokenAccount
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("status = ? OR image_limited = ? OR video_limited = ?", "quota", true, true).
|
||||
@@ -269,8 +269,11 @@ func (r *TokenRepository) RecoverQuota(ctx context.Context) ([]model.TokenAccoun
|
||||
patch := map[string]any{
|
||||
"fails": 0,
|
||||
"quota_recover_at": nil,
|
||||
"image_limited": false,
|
||||
"video_limited": false,
|
||||
}
|
||||
// For Adobe, also clear any legacy per-kind limit flags.
|
||||
if t.Pool == "adobe" {
|
||||
patch["image_limited"] = false
|
||||
patch["video_limited"] = false
|
||||
}
|
||||
// Only flip status back to active if it was sunk to "quota" (both kinds
|
||||
// limited); a single-kind limit left status untouched.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user