diff --git a/backend/internal/http/handler/user_generation.go b/backend/internal/http/handler/user_generation.go index 2421d88..6bf44bd 100644 --- a/backend/internal/http/handler/user_generation.go +++ b/backend/internal/http/handler/user_generation.go @@ -339,26 +339,57 @@ func (h *UserGenerationHandler) VideoPresets(c *gin.Context) { "data": []gin.H{ { "key": "gemini-veo31", - "label": "Veo31", + "label": "Veo31 Fast", "type": "video", "provider": "adobe", "durations": []string{"4s", "6s", "8s"}, - "ratios": []string{"16x9", "9x16"}, + "ratios": []string{"16:9", "9:16"}, "resolutions": []string{"720p", "1080p"}, "max_reference_images": 2, "reference_mode": "frame", }, { - "key": "firefly-ray", - "label": "Luma Ray", + "key": "gemini-veo3.1-fast", + "label": "Veo 3.1 Fast", "type": "video", "provider": "adobe", - "durations": []string{"5s", "10s"}, - "ratios": []string{"21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "9:21"}, - "resolutions": []string{"720p"}, + "durations": []string{"4s", "6s", "8s"}, + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p", "1080p"}, "max_reference_images": 2, "reference_mode": "frame", }, + { + "key": "gemini-veo3.1", + "label": "Veo 3.1", + "type": "video", + "provider": "adobe", + "durations": []string{"4s", "6s", "8s"}, + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p"}, + }, + { + "key": "seedance-fast", + "label": "Seedance 2.0 Fast", + "type": "video", + "provider": "adobe", + "durations": []string{"4s", "5s", "6s", "7s", "8s", "9s", "10s", "11s", "12s", "13s", "14s", "15s"}, + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p"}, + "max_reference_images": 9, + "reference_mode": "frame", + }, + { + "key": "seedance-2.0", + "label": "Seedance 2.0", + "type": "video", + "provider": "adobe", + "durations": []string{"4s", "5s", "6s", "7s", "8s", "9s", "10s", "11s", "12s", "13s", "14s", "15s"}, + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p", "1080p"}, + "max_reference_images": 9, + "reference_mode": "frame", + }, { "key": "firefly-video", "label": "Firefly Video", @@ -442,16 +473,6 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error) "image_to_image": true, "description": "Adobe Firefly Image 5", }, - { - "id": "flux-kontext-max", - "provider": "adobe", - "type": "image", - "ratios": []string{"1:1", "16:9", "9:16", "4:3", "3:4"}, - "resolutions": []string{"1K"}, - "image_to_image": true, - "max_reference_images": 4, - "description": "Adobe Flux Kontext Max", - }, { "id": "runway-nano-banana-2", "provider": "runway", @@ -497,15 +518,25 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error) "description": "Nano Banana Pro (图/参考图)", }, { - "id": "gemini-veo31", + "id": "gemini-veo3.1-fast", "provider": "adobe", "type": "video", - "ratios": []string{"16x9", "9x16"}, + "ratios": []string{"16:9", "9:16"}, "resolutions": []string{"720p", "1080p"}, "durations": []string{"4s", "6s", "8s"}, "max_reference_images": 2, "reference_mode": "frame", - "description": "Veo31 video", + "description": "Veo 3.1 Fast", + }, + { + "id": "gemini-veo3.1", + "provider": "adobe", + "type": "video", + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p"}, + "durations": []string{"4s", "6s", "8s"}, + "max_reference_images": 0, + "description": "Veo 3.1", }, { "id": "firefly-ray", @@ -529,6 +560,28 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error) "reference_mode": "frame", "description": "Adobe Firefly Video", }, + { + "id": "seedance-fast", + "provider": "adobe", + "type": "video", + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p"}, + "durations": []string{"4s", "5s", "6s", "7s", "8s", "9s", "10s", "11s", "12s", "13s", "14s", "15s"}, + "max_reference_images": 9, + "reference_mode": "frame", + "description": "Seedance 2.0 Fast", + }, + { + "id": "seedance-2.0", + "provider": "adobe", + "type": "video", + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p", "1080p"}, + "durations": []string{"4s", "5s", "6s", "7s", "8s", "9s", "10s", "11s", "12s", "13s", "14s", "15s"}, + "max_reference_images": 9, + "reference_mode": "frame", + "description": "Seedance 2.0", + }, { "id": "runway-gen4-turbo", "provider": "runway", @@ -637,15 +690,6 @@ func (h *UserGenerationHandler) publicModels() ([]gin.H, error) { "description": "Adobe Firefly Image 5", "stub": false, }, - { - "id": "flux-kontext-max", - "provider": "adobe", - "kind": "image", - "ratios": []string{"1:1", "16:9", "9:16", "4:3", "3:4"}, - "resolutions": []string{"1K"}, - "description": "Adobe Flux Kontext Max", - "stub": false, - }, { "id": "runway-nano-banana-2", "provider": "runway", @@ -683,12 +727,21 @@ func (h *UserGenerationHandler) publicModels() ([]gin.H, error) { "stub": false, }, { - "id": "gemini-veo31", + "id": "gemini-veo3.1-fast", "provider": "adobe", "kind": "video", - "ratios": []string{"16x9", "9x16"}, + "ratios": []string{"16:9", "9:16"}, "resolutions": []string{"720p", "1080p"}, - "description": "Veo31 video", + "description": "Veo 3.1 Fast", + "stub": false, + }, + { + "id": "gemini-veo3.1", + "provider": "adobe", + "kind": "video", + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p"}, + "description": "Veo 3.1", "stub": false, }, { @@ -709,6 +762,24 @@ func (h *UserGenerationHandler) publicModels() ([]gin.H, error) { "description": "Adobe Firefly Video", "stub": false, }, + { + "id": "seedance-fast", + "provider": "adobe", + "kind": "video", + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p"}, + "description": "Seedance 2.0 Fast", + "stub": false, + }, + { + "id": "seedance-2.0", + "provider": "adobe", + "kind": "video", + "ratios": []string{"16:9", "9:16"}, + "resolutions": []string{"720p", "1080p"}, + "description": "Seedance 2.0", + "stub": false, + }, { "id": "runway-gen4-turbo", "provider": "runway", diff --git a/backend/internal/provider/adobe/client.go b/backend/internal/provider/adobe/client.go index 71c30e9..bf6082c 100644 --- a/backend/internal/provider/adobe/client.go +++ b/backend/internal/provider/adobe/client.go @@ -37,6 +37,7 @@ var ( ErrQuotaExhausted = errors.New("adobe quota exhausted") ErrTemporaryUpstream = errors.New("adobe upstream temporary error") ErrDeadUpstream = errors.New("adobe upstream fatal error") + ErrRateLimited = errors.New("adobe rate limited") // ErrContentRejected is Adobe's content-safety filter refusing the prompt or // the generated image (HTTP 451 image_unsafe). It is the prompt's fault, not // the account's — every account rejects the same content — so the caller must @@ -178,7 +179,10 @@ func (c *Client) uploadImageOnce(ctx context.Context, token string, content []by if resp.StatusCode == 401 || resp.StatusCode == 403 { return nil, fmt.Errorf("%w (upload %d %s: %s)", ErrAuth, resp.StatusCode, resp.Header.Get("x-access-error"), clip(body, 300)), false } - if resp.StatusCode == 429 || resp.StatusCode == 451 || resp.StatusCode >= 500 { + if resp.StatusCode == 429 { + return nil, fmt.Errorf("%w (upload %d): %s", ErrRateLimited, resp.StatusCode, clip(body, 300)), false + } + if resp.StatusCode == 451 || resp.StatusCode >= 500 { return nil, fmt.Errorf("adobe upload failed: %d %s", resp.StatusCode, clip(body, 300)), true } if resp.StatusCode != 200 { diff --git a/backend/internal/provider/adobe/payloads.go b/backend/internal/provider/adobe/payloads.go index 61c90a6..4068ba7 100644 --- a/backend/internal/provider/adobe/payloads.go +++ b/backend/internal/provider/adobe/payloads.go @@ -2,8 +2,8 @@ package adobe import ( "encoding/json" + "math/rand" "strings" - "time" ) type modelSpec struct { @@ -68,7 +68,7 @@ func ResolveModelSpec(modelID string) modelSpec { func buildImage5Payload(prompt, aspectRatio, resolution string, blobIDs []string) map[string]any { p := map[string]any{ "n": 1, - "seeds": []int{int(time.Now().Unix()) % 999999}, + "seeds": []int{rand.Intn(999999)}, "output": map[string]any{"storeInputs": true}, "prompt": prompt, "referenceBlobs": []any{}, @@ -125,7 +125,7 @@ func buildGPTImagePayloads(spec modelSpec, prompt, ratio, resolution string, blo "modelVersion": spec.UpstreamModelVersion, "n": 1, "prompt": prompt, - "seeds": []int{int(time.Now().Unix()) % 999999}, + "seeds": []int{rand.Intn(999999)}, "output": map[string]any{"storeInputs": true}, "referenceBlobs": []any{}, "generationMetadata": map[string]any{"module": "text2image", "submodule": "ff-image-generate"}, @@ -151,7 +151,7 @@ func buildFluxPayloads(spec modelSpec, prompt, ratio string, blobIDs []string) [ "n": 1, "prompt": prompt, "size": map[string]any{"width": size[0], "height": size[1]}, - "seeds": []int{int(time.Now().Unix()) % 999999}, + "seeds": []int{rand.Intn(999999)}, "output": map[string]any{"storeInputs": true}, "referenceBlobs": []any{}, "modelSpecificPayload": map[string]any{ @@ -182,7 +182,7 @@ func buildDefaultPayloads(spec modelSpec, prompt, ratio, resolution string, blob "n": 1, "prompt": prompt, "size": map[string]any{"width": size[0], "height": size[1]}, - "seeds": []int{int(time.Now().Unix()) % 999999}, + "seeds": []int{rand.Intn(999999)}, "groundSearch": false, "output": map[string]any{"storeInputs": true}, "generationMetadata": map[string]any{ @@ -252,7 +252,7 @@ func cloneMap(in map[string]any) map[string]any { } func BuildVideoPayload(engine, prompt, aspectRatio string, durationSeconds int, resolution, referenceMode, upstreamModel string, blobIDs []string) map[string]any { - seedVal := int(time.Now().Unix()) % 999999 + seedVal := rand.Intn(999999) engine = defaultString(engine, "sora2") resolution = defaultString(resolution, "720p") aspectRatio = defaultString(aspectRatio, "16:9") @@ -299,9 +299,6 @@ func BuildVideoPayload(engine, prompt, aspectRatio string, durationSeconds int, if engine == "veo31-standard" { modelVersion = "3.1-generate" } - // Shape mirrors a captured working firefly.adobe.com video request: flat - // top-level duration / negativePrompt / generateAudio, submodule set, and - // NO `n` / NO modelSpecificPayload (sending those got 403). payload := map[string]any{ "modelId": "veo", "modelVersion": modelVersion, @@ -310,7 +307,7 @@ func BuildVideoPayload(engine, prompt, aspectRatio string, durationSeconds int, "prompt": prompt, "negativePrompt": "", "duration": durationSeconds, - "generateAudio": false, + "generateAudio": true, "generationMetadata": map[string]any{ "module": "text2video", "submodule": "ff-video-generate", @@ -320,9 +317,49 @@ func BuildVideoPayload(engine, prompt, aspectRatio string, durationSeconds int, } if len(blobIDs) > 0 { payload["generationMetadata"] = map[string]any{"module": "image2video", "submodule": "ff-video-generate"} - refs := make([]any, 0, min(len(blobIDs), 2)) - for idx, id := range blobIDs[:min(len(blobIDs), 2)] { - refs = append(refs, map[string]any{"id": id, "usage": "general", "promptReference": idx + 1}) + refs := make([]any, 0, min(len(blobIDs), 3)) + if referenceMode == "frame" { + for idx, id := range blobIDs[:min(len(blobIDs), 3)] { + refs = append(refs, map[string]any{"id": id, "usage": "frame", "order": idx + 1}) + } + } else { + for _, id := range blobIDs[:min(len(blobIDs), 3)] { + refs = append(refs, map[string]any{"id": id, "usage": "asset"}) + } + } + payload["referenceBlobs"] = refs + } + return payload + case "seedance-fast", "seedance-2.0": + modelVersion := "seedance_2.0" + if engine == "seedance-fast" { + modelVersion = "seedance_2.0_fast" + } + payload := map[string]any{ + "modelId": "seedance", + "modelVersion": modelVersion, + "size": videoSize(aspectRatio, resolution), + "seeds": []int{seedVal}, + "prompt": prompt, + "negativePrompt": "", + "duration": durationSeconds, + "generateAudio": true, + "generationSettings": map[string]any{"aspectRatio": aspectRatio}, + "generationMetadata": map[string]any{"module": "text2video", "submodule": "ff-video-generate"}, + "output": map[string]any{"storeInputs": true}, + "referenceBlobs": []any{}, + } + if len(blobIDs) > 0 { + payload["generationMetadata"] = map[string]any{"module": "image2video", "submodule": "ff-video-generate"} + refs := make([]any, 0, min(len(blobIDs), 9)) + if referenceMode == "frame" { + for idx, id := range blobIDs[:min(len(blobIDs), 9)] { + refs = append(refs, map[string]any{"id": id, "usage": "frame", "order": idx + 1}) + } + } else { + for _, id := range blobIDs[:min(len(blobIDs), 9)] { + refs = append(refs, map[string]any{"id": id, "usage": "asset"}) + } } payload["referenceBlobs"] = refs } diff --git a/backend/internal/service/tokens.go b/backend/internal/service/tokens.go index 0c93b39..d45786b 100644 --- a/backend/internal/service/tokens.go +++ b/backend/internal/service/tokens.go @@ -716,6 +716,9 @@ func (s *TokenService) checkPendingAdobe(tokenID, cookie string) { quotaMeta["cached_quota_at"] = int(time.Now().Unix()) if rem, ok := cb["remaining"].(int); ok { quotaMeta["cached_quota_remaining"] = rem + if rem <= 4000 { + quotaMeta["is_sub_account"] = true + } } } if prof, e := s.adobe.FetchAccountProfile(ctx, result.AccessToken); e == nil { @@ -1273,6 +1276,7 @@ func (s *TokenService) Quota(ctx context.Context, pool, id string) (map[string]a meta["cached_quota_at"] = int(time.Now().Unix()) if remaining, ok := data["remaining"].(int); ok { meta["cached_quota_remaining"] = remaining + meta["is_sub_account"] = remaining <= 4000 } if used, ok := data["used"].(int); ok { meta["cached_quota_used"] = used @@ -1643,6 +1647,7 @@ func accountRow(item model.TokenAccount, inFlight int64) map[string]any { remaining, hasRemaining := jsonMapInt(item.Meta, "cached_quota_remaining") quotaAt, _ := jsonMapInt(item.Meta, "cached_quota_at") pending, _ := jsonMapBool(item.Meta, "pending_check") + subAccount, _ := jsonMapBool(item.Meta, "is_sub_account") // OpenAI email lives in the token's JWT (nested profile claim). Decode it at // render time like the Python reference (_account_row) so accounts imported // before the email was persisted still show a name; fall back to the cached @@ -1680,6 +1685,7 @@ func accountRow(item model.TokenAccount, inFlight int64) map[string]any { "image_limited": item.ImageLimited, "video_limited": item.VideoLimited, "pending": pending, + "sub_account": subAccount, "quota_supported": hasQuota, "needs_reset_fetch": typeLabel == "adobe" && item.Status == "active" && strings.TrimSpace(item.CachedQuotaResetAfter) == "", "weight": item.Weight, diff --git a/backend/internal/service/v1.go b/backend/internal/service/v1.go index d6254b5..3a82299 100644 --- a/backend/internal/service/v1.go +++ b/backend/internal/service/v1.go @@ -1588,7 +1588,7 @@ func (s *V1Service) tryAccount(ctx context.Context, eventID, pool string, token } func adobeErrClass(e error) (bool, bool, bool, bool) { - return errors.Is(e, adobe.ErrAuth), errors.Is(e, adobe.ErrQuotaExhausted), errors.Is(e, adobe.ErrTemporaryUpstream), errors.Is(e, adobe.ErrDeadUpstream) + return errors.Is(e, adobe.ErrAuth), errors.Is(e, adobe.ErrQuotaExhausted), errors.Is(e, adobe.ErrTemporaryUpstream) || errors.Is(e, adobe.ErrRateLimited), errors.Is(e, adobe.ErrDeadUpstream) } // noStore url-only mode: adobe returns a presigned image URL (meta["image_url"]); @@ -1668,11 +1668,13 @@ func (s *V1Service) generateAdobeVideo(ctx context.Context, eventID string, mode } var active []model.TokenAccount for _, item := range items { - // 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) + if item.Status != "active" || item.Dead || strings.TrimSpace(item.Value) == "" { + continue } + if isSeedanceModel(modelItem.ID) && isSubAccount(item.Meta) { + continue + } + active = append(active, item) } active = pinTestAccount(items, active, in.AccountID) if len(active) == 0 { @@ -3070,7 +3072,20 @@ func modelPrice(item *model.ModelConfig, kind, resolution, duration string, agen } if kind == "video" { rv, rok := tierPrice(item.Prices, item.PricesAgent, resolution) - dv, dok := tierPrice(item.DurationPrices, item.DurationPricesAgent, duration) + var dv float64 + var dok bool + if pps, hasPerSec := jsonMapFloat(item.DurationPrices, "per_second"); hasPerSec { + secs := parseDurationSeconds(duration) + dv = pps * float64(secs) + dok = true + if agent { + if av, aok := jsonMapFloat(item.DurationPricesAgent, "per_second"); aok { + dv = av * float64(secs) + } + } + } else { + dv, dok = tierPrice(item.DurationPrices, item.DurationPricesAgent, duration) + } if !rok || !dok { return 0, false } @@ -3143,12 +3158,14 @@ func parseDurationSeconds(raw string) int { func resolveAdobeVideoEngine(modelID string) (string, string) { switch strings.ToLower(strings.TrimSpace(modelID)) { - case "gemini-veo31", "firefly-veo31": - // Use the fast tier — it's the only Veo 3.1 version this account is - // entitled to (standard "3.1-generate" returns 403 user_not_entitled). - // "firefly-veo31" is the legacy id, kept for back-compat with historical - // rows/logs; the model is branded "gemini-veo31" now. + case "gemini-veo3.1-fast", "gemini-veo31", "firefly-veo31": return "veo31-fast", "" + case "gemini-veo3.1": + return "veo31-standard", "" + case "seedance-fast": + return "seedance-fast", "" + case "seedance-2.0": + return "seedance-2.0", "" case "firefly-ray": return "luma", "" case "firefly-video": @@ -3311,3 +3328,19 @@ func (s *V1Service) rotateRoundRobin(pool string, items []model.TokenAccount) { i = j } } + +func isSeedanceModel(modelID string) bool { + return modelID == "seedance-fast" || modelID == "seedance-2.0" +} + +func isSubAccount(meta map[string]interface{}) bool { + if meta == nil { + return false + } + v, ok := meta["is_sub_account"] + if !ok { + return false + } + b, _ := v.(bool) + return b +} diff --git a/frontend/src/components/ModelFormModal.vue b/frontend/src/components/ModelFormModal.vue index b702cb6..7ce8c21 100644 --- a/frontend/src/components/ModelFormModal.vue +++ b/frontend/src/components/ModelFormModal.vue @@ -40,12 +40,15 @@ const isVideo = computed(() => entry.value?.type === 'video') // Display tiers in canonical ascending order (720p before 1080p; 1K<2K<4K) // regardless of how the catalog/stored record happens to list them. const resolutions = computed(() => sortResolutions(entry.value?.resolutions || [])) -// Duration tiers for the price inputs. Prefer the declared `durations`; fall -// back to the keys of any stored duration_prices so a model saved without a -// `durations` array (the legacy sync bug) is still viewable/editable. +const isPerSecond = computed(() => { + const id = entry.value?.id || '' + return id.startsWith('seedance') +}) + const durationTiers = computed(() => { const e = entry.value if (!e) return [] + if (isPerSecond.value) return ['per_second'] const ds = e.durations || [] return ds.length ? ds : Object.keys(e.duration_prices || {}) }) @@ -67,7 +70,12 @@ function resetPrices(e) { // (real video price = resolution price + duration price). for (const r of (e.resolutions || [])) { imagePrices.value[r] = ''; imagePricesAgent.value[r] = '' } if (e.type === 'video') { - for (const d of (e.durations || [])) { videoPrices.value[d] = ''; videoPricesAgent.value[d] = '' } + if (isPerSecond.value) { + videoPrices.value['per_second'] = '' + videoPricesAgent.value['per_second'] = '' + } else { + for (const d of (e.durations || [])) { videoPrices.value[d] = ''; videoPricesAgent.value[d] = '' } + } } } @@ -88,10 +96,15 @@ onMounted(async () => { imagePricesAgent.value[r] = m.prices_agent?.[r] ?? '' } if (m.type === 'video') { - const durs = (m.durations && m.durations.length) ? m.durations : Object.keys(m.duration_prices || {}) - for (const d of durs) { - videoPrices.value[d] = m.duration_prices?.[d] ?? '' - videoPricesAgent.value[d] = m.duration_prices_agent?.[d] ?? '' + if (m.duration_prices?.per_second != null) { + videoPrices.value['per_second'] = m.duration_prices.per_second + videoPricesAgent.value['per_second'] = m.duration_prices_agent?.per_second ?? '' + } else { + const durs = (m.durations && m.durations.length) ? m.durations : Object.keys(m.duration_prices || {}) + for (const d of durs) { + videoPrices.value[d] = m.duration_prices?.[d] ?? '' + videoPricesAgent.value[d] = m.duration_prices_agent?.[d] ?? '' + } } } } @@ -116,12 +129,8 @@ async function save() { let payload if (e.type === 'video') { - // Real video price = resolution price + duration price. Both tiers are - // priced independently; a blank tier on either axis = unsupported. Charge - // happens server-side as prices[res] + duration_prices[dur]. const prices = collect(imagePrices.value, e.resolutions || []) const duration_prices = collect(videoPrices.value, durationTiers.value) - // 代理价:可选覆盖,留空的档跟随普通价。 const prices_agent = collect(imagePricesAgent.value, e.resolutions || []) const duration_prices_agent = collect(videoPricesAgent.value, durationTiers.value) if (!Object.keys(prices).length) { error.value = '至少填写一个分辨率价格'; return } @@ -132,10 +141,7 @@ async function save() { ratios: e.ratios || [], resolutions: e.resolutions || [], prices, - // durations MUST track duration_prices — the model list / docs iterate - // `durations` to render the per-second price chips. Persisting prices - // without the matching durations array hides them. Keep them in sync. - durations: Object.keys(duration_prices), + durations: isPerSecond.value ? (e.durations || []) : Object.keys(duration_prices), duration_prices, prices_agent, duration_prices_agent, @@ -282,7 +288,7 @@ async function save() { - +
@@ -291,7 +297,7 @@ async function save() { 代理价
-
{{ d }}
+
{{ d === 'per_second' ? '/s' : d }}
积分 @@ -305,7 +311,8 @@ async function save() {

该模型未声明时长档位

-

实付 = 分辨率价 + 时长价(例:720p 50 + 5s 30 = 80 积分)

+

实付 = 分辨率价 + 时长价(例:720p 50 + 5s 30 = 80 积分)

+

实付 = 分辨率价 + 每秒价 × 秒数

diff --git a/frontend/src/views/AccountsView.vue b/frontend/src/views/AccountsView.vue index 1d62220..9d0f550 100644 --- a/frontend/src/views/AccountsView.vue +++ b/frontend/src/views/AccountsView.vue @@ -453,6 +453,9 @@ onMounted(() => { loadAccounts(); loadModelList() }) case. -->
{{ a.email || '-' }} + 子号 图片限额 diff --git a/frontend/src/views/PlaygroundView.vue b/frontend/src/views/PlaygroundView.vue index 91ad3b6..9369099 100644 --- a/frontend/src/views/PlaygroundView.vue +++ b/frontend/src/views/PlaygroundView.vue @@ -725,7 +725,7 @@ onUnmounted(() => {