feat: veo3.1 拆分 fast/standard, seedance双模型, per_second定价, 子号过滤, 随机种子, UI优化

This commit is contained in:
2026-08-03 20:27:20 +08:00
parent e90aeecea3
commit 7f4701d362
8 changed files with 238 additions and 77 deletions
+6
View File
@@ -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,
+44 -11
View File
@@ -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
}