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:
+2
-2
@@ -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 |
|
||||
|
||||
@@ -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 | 图像 |
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() })
|
||||
<td class="px-3 py-3.5 align-middle text-right text-sm tabular-nums whitespace-nowrap">
|
||||
<!-- quota column: 数字 / — (never "未知"/"失败"/"检测中") -->
|
||||
<!-- remaining === -1 is the provider "unlimited" sentinel → show — not a scary red -1 -->
|
||||
<span v-if="(a.type === 'openai' || a.type === 'runway' || a.type === 'leonardo' || a.type === 'krea' || a.type === 'imagine' || a.type === 'grok') && a.remaining != null && a.remaining !== -1"
|
||||
<span v-if="(a.type === 'openai' || a.type === 'adobe' || a.type === 'runway' || a.type === 'leonardo' || a.type === 'krea' || a.type === 'imagine' || a.type === 'grok') && a.remaining != null && a.remaining !== -1"
|
||||
class="font-mono font-semibold"
|
||||
:class="a.remaining > 0 ? 'text-emerald-300' : 'text-rose-300'">{{ a.remaining }}{{ a.type === 'grok' ? '%' : '' }}</span>
|
||||
<span v-else class="text-white/25" :title="a._quotaError || ''">—</span>
|
||||
|
||||
Reference in New Issue
Block a user