feat: 二次元前端改版 + 后端账号plan探测/调度修复

This commit is contained in:
2026-08-06 10:11:05 +08:00
parent 7f4701d362
commit f76ecd5769
37 changed files with 2484 additions and 813 deletions
+63 -5
View File
@@ -709,17 +709,44 @@ func (s *TokenService) checkPendingAdobe(tokenID, cookie string) {
_, _ = s.tokens.Update(ctx, "adobe", tokenID, seed)
quotaMeta := map[string]any{}
planKnown := false
if cb, e := s.adobe.FetchCreditsBalance(ctx, result.AccessToken); e == nil {
if ra := strings.TrimSpace(stringValue(cb["available_until"])); ra != "" {
_, _ = s.tokens.Update(ctx, "adobe", tokenID, map[string]any{"cached_quota_reset_after": ra})
}
quotaMeta["cached_quota_at"] = int(time.Now().Unix())
planCap := strings.ToLower(strings.TrimSpace(stringValue(cb["plan"])))
quotaMeta["plan"] = planCap
isVIP := planCap != "" && planCap != "free"
planKnown = planCap != ""
if rem, ok := cb["remaining"].(int); ok {
quotaMeta["cached_quota_remaining"] = rem
if rem <= 4000 {
if isVIP && rem > 0 && rem <= 4000 {
quotaMeta["is_sub_account"] = true
}
}
// Set concurrency + limits based on plan
concurrency := 1
imageLimit := true
videoLimit := true
if isVIP {
concurrency = 5
imageLimit = false
videoLimit = false
if ra := strings.TrimSpace(stringValue(cb["available_until"])); ra != "" {
_, _ = s.tokens.Update(ctx, "adobe", tokenID, map[string]any{"cached_quota_reset_after": ra})
}
}
patch := map[string]any{
"concurrency": concurrency,
"image_limited": imageLimit,
"video_limited": videoLimit,
"meta": quotaMeta,
}
if planKnown {
patch["status"] = "active"
patch["dead"] = false
}
_, _ = s.tokens.Update(ctx, "adobe", tokenID, patch)
}
if prof, e := s.adobe.FetchAccountProfile(ctx, result.AccessToken); e == nil {
p := map[string]any{}
@@ -734,6 +761,13 @@ func (s *TokenService) checkPendingAdobe(tokenID, cookie string) {
}
}
// 探测不到会员身份(credits 接口失败或没返回 plan)的号既不算普号也不算会员号,
// 留在池里只会在需要会员的请求上撞 403 —— 直接置死号,等重新探测到 plan 再启用。
if !planKnown {
log.Printf("token import: adobe %s plan unknown, marking dead", tokenID)
s.finishPending(ctx, "adobe", tokenID, "disabled", true, quotaMeta)
return
}
s.finishPending(ctx, "adobe", tokenID, "active", false, quotaMeta)
_, _ = s.refresh.Update(ctx, tokenID, map[string]any{
"last_attempt_at": time.Now(),
@@ -1274,9 +1308,21 @@ func (s *TokenService) Quota(ctx context.Context, pool, id string) (map[string]a
patch := map[string]any{}
meta := cloneJSONMap(item.Meta)
meta["cached_quota_at"] = int(time.Now().Unix())
plan := strings.ToLower(strings.TrimSpace(stringValue(data["plan"])))
if plan != "" {
meta["plan"] = plan
}
if remaining, ok := data["remaining"].(int); ok {
meta["cached_quota_remaining"] = remaining
meta["is_sub_account"] = remaining <= 4000
// is_sub_account 仅在导入时写入,刷新配额时不覆盖
if _, hasFlag := meta["is_sub_account"]; !hasFlag && plan != "free" {
meta["is_sub_account"] = remaining > 0 && remaining <= 4000
}
}
// 子号必然是会员号:plan 查回 free 说明这个号没有会员,不能再挂子号标记
// (否则同一行会同时显示 子号 和 普号)。
if plan == "free" {
meta["is_sub_account"] = false
}
if used, ok := data["used"].(int); ok {
meta["cached_quota_used"] = used
@@ -1285,6 +1331,12 @@ func (s *TokenService) Quota(ctx context.Context, pool, id string) (map[string]a
meta["cached_quota_total"] = total
}
patch["meta"] = meta
// 探测不到会员身份(新旧 plan 都为空)的号既不算普号也不算会员号,
// 留在池里只会在需要会员的请求上撞 403 —— 直接置死号。
if strings.TrimSpace(stringValue(meta["plan"])) == "" {
patch["status"] = "disabled"
patch["dead"] = true
}
if resetAfter := strings.TrimSpace(stringValue(data["available_until"])); resetAfter != "" {
patch["cached_quota_reset_after"] = resetAfter
item.CachedQuotaResetAfter = resetAfter
@@ -1294,6 +1346,7 @@ func (s *TokenService) Quota(ctx context.Context, pool, id string) (map[string]a
item = updated
}
}
subAccount, _ := jsonMapBool(meta, "is_sub_account")
return map[string]any{
"supported": true,
"remaining": data["remaining"],
@@ -1304,6 +1357,9 @@ func (s *TokenService) Quota(ctx context.Context, pool, id string) (map[string]a
"unchanged": false,
"unknown": boolValueWithDefault(data["unknown"], false),
"error": data["error"],
// 会员身份跟着额度一起返回,前端查一次额度就能刷新 子号/母号/普号 徽章。
"plan": emptyToNil(strings.ToLower(strings.TrimSpace(stringValue(meta["plan"])))),
"sub_account": subAccount,
}, nil
}
if poolToType(item.Pool) == "krea" && s.krea != nil {
@@ -1648,6 +1704,7 @@ func accountRow(item model.TokenAccount, inFlight int64) map[string]any {
quotaAt, _ := jsonMapInt(item.Meta, "cached_quota_at")
pending, _ := jsonMapBool(item.Meta, "pending_check")
subAccount, _ := jsonMapBool(item.Meta, "is_sub_account")
plan, _ := item.Meta["plan"]
// 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
@@ -1686,6 +1743,7 @@ func accountRow(item model.TokenAccount, inFlight int64) map[string]any {
"video_limited": item.VideoLimited,
"pending": pending,
"sub_account": subAccount,
"plan": emptyToNil(strings.ToLower(strings.TrimSpace(stringValue(plan)))),
"quota_supported": hasQuota,
"needs_reset_fetch": typeLabel == "adobe" && item.Status == "active" && strings.TrimSpace(item.CachedQuotaResetAfter) == "",
"weight": item.Weight,