From 4987717d235cd395efe8b0f2c7fa0908326a5ffe Mon Sep 17 00:00:00 2001 From: chiyi Date: Sun, 2 Aug 2026 20:09:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Adobe=E7=A7=AF=E5=88=86=E5=8F=B7?= =?UTF-8?q?=E4=B8=8D=E9=94=81=E5=AE=9A=E9=A2=9D=E5=BA=A6=20+=20Nano=20Bana?= =?UTF-8?q?na=E5=8F=8C=E9=80=9A=E9=81=93=20+=20=E5=89=8D=E7=AB=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=A2=9D=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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更新模型列表 --- README.en.md | 4 +- README.md | 4 +- .../internal/http/handler/user_generation.go | 48 ++++++++++++++++-- backend/internal/provider/adobe/payloads.go | 4 ++ backend/internal/provider/runway/image.go | 4 +- backend/internal/repo/token_repo.go | 11 ++-- backend/internal/service/v1.go | 50 +++++-------------- frontend/src/views/AccountsView.vue | 8 +-- 8 files changed, 79 insertions(+), 54 deletions(-) diff --git a/README.en.md b/README.en.md index d8ff705..b111f94 100644 --- a/README.en.md +++ b/README.en.md @@ -130,9 +130,9 @@ It's more than an API proxy: it ships with **credit billing, CDK top-ups, referr | Provider | Models (examples) | Type | |---|---|---| -| **Adobe Firefly** | firefly-image-5 · firefly-gpt-image-2 · flux-kontext-max · firefly-video · firefly-ray · gemini-veo31 | Image / Video | +| **Adobe Firefly** | firefly-image-5 · firefly-gpt-image-2 · flux-kontext-max · nano-banana-2 · nano-banana-pro · firefly-video · firefly-ray · gemini-veo31 | Image / Video | | **OpenAI** | gpt-image-2 | Image | -| **Runway** | runway-gen4-turbo · nano-banana-2 (Nano Banana 2) | Video / Image | +| **Runway** | runway-gen4-turbo · runway-nano-banana-2 (Nano Banana 2) · runway-nano-banana-pro (Nano Banana Pro) | Video / Image | | **Grok (grok.com)** | grok-video (imagine text/image-to-video) | Video | | **Leonardo.ai** | seedream-4.5 | Image | | **Krea.ai** | flux-klein-2 | Image | diff --git a/README.md b/README.md index f10d31a..135815b 100644 --- a/README.md +++ b/README.md @@ -137,9 +137,9 @@ | 供应商 | 模型(示例) | 类型 | |---|---|---| -| **Adobe Firefly** | firefly-image-5 · firefly-gpt-image-2 · flux-kontext-max · firefly-video · firefly-ray · gemini-veo31 | 图像 / 视频 | +| **Adobe Firefly** | firefly-image-5 · firefly-gpt-image-2 · flux-kontext-max · nano-banana-2 · nano-banana-pro · firefly-video · firefly-ray · gemini-veo31 | 图像 / 视频 | | **OpenAI** | gpt-image-2 | 图像 | -| **Runway** | runway-gen4-turbo · nano-banana-2(Nano Banana 2) | 视频 / 图像 | +| **Runway** | runway-gen4-turbo · runway-nano-banana-2 (Nano Banana 2) · runway-nano-banana-pro (Nano Banana Pro) | 视频 / 图像 | | **Grok(grok.com)** | grok-video(imagine 文生 / 图生视频) | 视频 | | **Leonardo.ai** | seedream-4.5 | 图像 | | **Krea.ai** | flux-klein-2 | 图像 | diff --git a/backend/internal/http/handler/user_generation.go b/backend/internal/http/handler/user_generation.go index ec5d02f..2421d88 100644 --- a/backend/internal/http/handler/user_generation.go +++ b/backend/internal/http/handler/user_generation.go @@ -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", diff --git a/backend/internal/provider/adobe/payloads.go b/backend/internal/provider/adobe/payloads.go index ff884cb..f5687f0 100644 --- a/backend/internal/provider/adobe/payloads.go +++ b/backend/internal/provider/adobe/payloads.go @@ -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"} } diff --git a/backend/internal/provider/runway/image.go b/backend/internal/provider/runway/image.go index a8292b6..0407566 100644 --- a/backend/internal/provider/runway/image.go +++ b/backend/internal/provider/runway/image.go @@ -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" diff --git a/backend/internal/repo/token_repo.go b/backend/internal/repo/token_repo.go index 8b3af46..a58a619 100644 --- a/backend/internal/repo/token_repo.go +++ b/backend/internal/repo/token_repo.go @@ -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. diff --git a/backend/internal/service/v1.go b/backend/internal/service/v1.go index d9e69bf..d6254b5 100644 --- a/backend/internal/service/v1.go +++ b/backend/internal/service/v1.go @@ -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 } diff --git a/frontend/src/views/AccountsView.vue b/frontend/src/views/AccountsView.vue index c7c2f36..1d62220 100644 --- a/frontend/src/views/AccountsView.vue +++ b/frontend/src/views/AccountsView.vue @@ -189,8 +189,10 @@ async function reconcile() { row._unknown = true updates++ } else if (result && result.unchanged === false) { - if (row.type === 'adobe') row.reset_after = result.reset_after - else applyQuota(row, result) + if (row.type === 'adobe') { + row.reset_after = result.reset_after + applyQuota(row, result) + } else applyQuota(row, result) updates++ } bump() @@ -468,7 +470,7 @@ onMounted(() => { loadAccounts(); loadModelList() }) - {{ a.remaining }}{{ a.type === 'grok' ? '%' : '' }}