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
+42 -11
View File
@@ -26,6 +26,12 @@ func NewRefreshProfileService(profiles *repo.RefreshProfileRepository, tokens *r
}
}
func (s *RefreshProfileService) SetProxy(proxy string) {
if s.adobe != nil {
s.adobe.SetProxy(proxy)
}
}
func (s *RefreshProfileService) List(ctx context.Context) ([]model.RefreshProfile, error) {
return s.profiles.List(ctx)
}
@@ -64,12 +70,11 @@ func (s *RefreshProfileService) RefreshNow(ctx context.Context, id string) error
"consecutive_failures": failures,
"next_retry_at": now.Add(time.Duration(secs) * time.Second),
})
// After repeated failures the cookie can no longer mint a token — it's
// genuinely dead (expired/revoked). Lock the pool token (disabled+dead)
// so the UI flags it red. A single failure may be a transient blip, so
// only escalate after a few in a row (mirrors Python RefreshManager).
if failures >= 3 {
_, _ = s.tokens.Update(ctx, profile.Pool, id, map[string]any{
// Mark the token dead when the cookie exchange keeps failing:
// - 5 consecutive failures → cookie is likely expired/revoked
// - ride_AdobeID_acct_actreq (Adobe requires account action) → permanently broken
if failures >= 5 || strings.Contains(msg, "ride_AdobeID_acct_actreq") {
_, _ = s.tokens.Update(ctx, "adobe", id, map[string]any{
"status": "disabled",
"dead": true,
})
@@ -84,12 +89,14 @@ func (s *RefreshProfileService) RefreshNow(ctx context.Context, id string) error
"fails": 0,
"updated_at": now,
}
email, exp := parseJWTEmailExpiry(result.AccessToken)
email, _ := parseJWTEmailExpiry(result.AccessToken)
if email != "" {
tokenPatch["account_email"] = email
}
if exp != nil {
tokenPatch["cached_quota_reset_after"] = exp.Format(time.RFC3339)
if profile.IntervalSeconds > 0 {
tokenPatch["cached_quota_reset_after"] = now.Add(time.Duration(profile.IntervalSeconds) * time.Second).Format(time.RFC3339)
} else {
tokenPatch["cached_quota_reset_after"] = now.Add(54000 * time.Second).Format(time.RFC3339)
}
if profileData, profileErr := s.adobe.FetchAccountProfile(ctx, result.AccessToken); profileErr == nil {
if email := strings.TrimSpace(stringValue(profileData["email"])); email != "" {
@@ -99,6 +106,7 @@ func (s *RefreshProfileService) RefreshNow(ctx context.Context, id string) error
tokenPatch["account_display_name"] = displayName
}
}
planKnown := false
if quotaData, quotaErr := s.adobe.FetchCreditsBalance(ctx, result.AccessToken); quotaErr == nil {
meta := datatypes.JSONMap{
"cached_quota_at": int(time.Now().Unix()),
@@ -113,10 +121,33 @@ func (s *RefreshProfileService) RefreshNow(ctx context.Context, id string) error
meta["cached_quota_total"] = total
}
tokenPatch["meta"] = meta
if resetAfter := strings.TrimSpace(stringValue(quotaData["available_until"])); resetAfter != "" {
tokenPatch["cached_quota_reset_after"] = resetAfter
planCap := strings.ToLower(strings.TrimSpace(stringValue(quotaData["plan"])))
meta["plan"] = planCap
isVIP := planCap != "" && !strings.EqualFold(planCap, "free")
planKnown = planCap != ""
// VIP: use Adobe's reset time; set concurrency 5.
// 母号/子号 身份固定,不随积分动态变化——只有降级普号才刷新身份。
if isVIP {
if resetAfter := strings.TrimSpace(stringValue(quotaData["available_until"])); resetAfter != "" {
tokenPatch["cached_quota_reset_after"] = resetAfter
}
tokenPatch["concurrency"] = 5
} else {
// Free account: concurrency 1, limit image+video
tokenPatch["concurrency"] = 1
tokenPatch["image_limited"] = true
tokenPatch["video_limited"] = true
meta["is_sub_account"] = false
}
}
// 探测不到会员身份(credits 接口失败或没返回 plan)的号既不算普号也不算会员号,
// 留在池里只会在需要会员的请求上撞 403 —— 直接置死号。
if !planKnown {
tokenPatch["status"] = "disabled"
tokenPatch["dead"] = true
}
if _, err := s.tokens.Update(ctx, "adobe", id, tokenPatch); err != nil {
return err
}