feat(grok): 视频改走 Grok Console 并新增图片模型

- grok 视频/图片统一走 console.x.ai:sso 换 DPoP 短期 token,绕开 grok.com 的 statsig 反爬

- 新增 grok-image:不带参考图走 /v1/images/generations,带图自动走 quality 上游 + /v1/images/edits

- grok 额度改本地写死 图5/视频2,成功各扣一次,两份归零直接判死,无恢复时间

- 视频渲染预算统一 20 分钟(adobe/runway/grok/custom)

- 删除参考图人脸打码(facemask + onnxruntime 依赖 + 前台开关)
This commit is contained in:
2026-08-08 11:15:42 +08:00
parent 554dea6252
commit ebcf3c5779
16 changed files with 956 additions and 446 deletions
+45 -85
View File
@@ -964,30 +964,13 @@ func (s *TokenService) checkPendingGrok(tokenID, ssoToken string) {
} else if strings.TrimSpace(email) != "" {
_, _ = s.tokens.Update(ctx, "grok", tokenID, map[string]any{"account_email": strings.TrimSpace(email)})
}
data, err := s.grok.FetchCreditsBalance(ctx, ssoToken)
if err != nil {
if errors.Is(err, grok.ErrAuth) {
s.finishPending(ctx, "grok", tokenID, "disabled", true, nil)
return
}
s.finishPending(ctx, "grok", tokenID, "active", false, nil)
return
}
quotaMeta := map[string]any{}
if rem, ok := data["remaining"].(int); ok {
quotaMeta["cached_quota_remaining"] = rem
quotaMeta["cached_quota_at"] = int(time.Now().Unix())
}
if used, ok := data["used"].(int); ok {
quotaMeta["cached_quota_used"] = used
}
if total, ok := data["total"].(int); ok {
quotaMeta["cached_quota_total"] = total
}
if reset := strings.TrimSpace(stringValue(data["reset_after"])); reset != "" {
_, _ = s.tokens.Update(ctx, "grok", tokenID, map[string]any{"cached_quota_reset_after": reset})
}
s.finishPending(ctx, "grok", tokenID, "active", false, quotaMeta)
// Console 没有额度接口,所以额度不查上游:导入即写死 图 5 / 视频 2,生成时各扣各的,
// 两个都归零就判死;没有恢复时间。
s.finishPending(ctx, "grok", tokenID, "active", false, map[string]any{
repo.GrokImageQuotaKey: repo.GrokImageQuota,
repo.GrokVideoQuotaKey: repo.GrokVideoQuota,
"cached_quota_at": int(time.Now().Unix()),
})
}
// RefreshGrokLiveness re-validates every live grok account each maintenance tick.
@@ -1039,28 +1022,15 @@ func (s *TokenService) RefreshGrokLiveness(ctx context.Context) {
_, _ = s.tokens.Update(ctx, "grok", it.ID, map[string]any{"status": "disabled", "dead": true})
continue
}
data, derr := s.grok.FetchCreditsBalance(ctx, it.Value)
if derr != nil {
// Same policy as the subscription probe: a credits-balance 401/403 is
// transient, never a reason to kill a live account. Skip and retry.
// 额度不查上游(Console 无额度接口):只给老号补齐写死的 图 5 / 视频 2。
if _, ok := jsonMapInt(it.Meta, repo.GrokImageQuotaKey); ok {
continue
}
meta := cloneJSONMap(it.Meta)
meta[repo.GrokImageQuotaKey] = repo.GrokImageQuota
meta[repo.GrokVideoQuotaKey] = repo.GrokVideoQuota
meta["cached_quota_at"] = int(time.Now().Unix())
if rem, ok := data["remaining"].(int); ok {
meta["cached_quota_remaining"] = rem
}
if used, ok := data["used"].(int); ok {
meta["cached_quota_used"] = used
}
if total, ok := data["total"].(int); ok {
meta["cached_quota_total"] = total
}
patch := map[string]any{"meta": meta}
if reset := strings.TrimSpace(stringValue(data["reset_after"])); reset != "" {
patch["cached_quota_reset_after"] = reset
}
_, _ = s.tokens.Update(ctx, "grok", it.ID, patch)
_, _ = s.tokens.Update(ctx, "grok", it.ID, map[string]any{"meta": meta})
}
}
@@ -1567,53 +1537,28 @@ func (s *TokenService) Quota(ctx context.Context, pool, id string) (map[string]a
"error": data["error"],
}, nil
}
if poolToType(item.Pool) == "grok" && s.grok != nil {
data, err := s.grok.FetchCreditsBalance(ctx, item.Value)
if err != nil {
if errors.Is(err, grok.ErrAuth) {
_, _ = s.tokens.Update(ctx, item.Pool, item.ID, map[string]any{
"status": "disabled",
"dead": true,
"fails": gorm.Expr("fails + 1"),
})
}
return nil, err
if poolToType(item.Pool) == "grok" {
// 本地写死的额度(图 5 / 视频 2),没有上游接口可查,所以刷新只是回读本地计数。
images, ok := jsonMapInt(item.Meta, repo.GrokImageQuotaKey)
if !ok {
images = repo.GrokImageQuota
}
patch := map[string]any{}
meta := cloneJSONMap(item.Meta)
meta["cached_quota_at"] = int(time.Now().Unix())
if remaining, ok := data["remaining"].(int); ok {
// Refresh only updates the displayed credit number; never flips status.
// Out-of-credits is judged at generation time (dead/401, no renewal).
meta["cached_quota_remaining"] = remaining
}
if used, ok := data["used"].(int); ok {
meta["cached_quota_used"] = used
}
if total, ok := data["total"].(int); ok {
meta["cached_quota_total"] = total
}
patch["meta"] = meta
// Recovery time is the credits' weekly reset (when the grant refills) —
// purely informational, NOT a death deadline (liveness is judged by the
// subscriptions sweep / real 401s), so it's safe to refresh every time.
if reset := strings.TrimSpace(stringValue(data["reset_after"])); reset != "" {
patch["cached_quota_reset_after"] = reset
item.CachedQuotaResetAfter = reset
}
if updated, updateErr := s.tokens.Update(ctx, item.Pool, item.ID, patch); updateErr == nil {
item = updated
videos, ok := jsonMapInt(item.Meta, repo.GrokVideoQuotaKey)
if !ok {
videos = repo.GrokVideoQuota
}
return map[string]any{
"supported": true,
"remaining": data["remaining"],
"used": data["used"],
"total": data["total"],
"reset_after": emptyToNil(item.CachedQuotaResetAfter),
"quota_cached_at": meta["cached_quota_at"],
"unchanged": false,
"unknown": boolValueWithDefault(data["unknown"], false),
"error": data["error"],
"remaining": images + videos,
"image_remaining": images,
"video_remaining": videos,
"used": nil,
"total": nil,
"reset_after": nil,
"quota_cached_at": item.Meta["cached_quota_at"],
"unchanged": true,
"unknown": false,
"error": nil,
}, nil
}
remaining, hasRemaining := jsonMapInt(item.Meta, "cached_quota_remaining")
@@ -1734,6 +1679,19 @@ func accountRow(item model.TokenAccount, inFlight int64) map[string]any {
if item.Meta != nil {
teamID = strings.TrimSpace(stringValue(item.Meta["team_id"]))
}
// grok 额度是本地写死的两个计数(图 / 视频),前台单独一列展示成 "5/2"。
var grokImages, grokVideos any
if typeLabel == "grok" {
images, ok := jsonMapInt(item.Meta, repo.GrokImageQuotaKey)
if !ok {
images = repo.GrokImageQuota
}
videos, ok := jsonMapInt(item.Meta, repo.GrokVideoQuotaKey)
if !ok {
videos = repo.GrokVideoQuota
}
grokImages, grokVideos = images, videos
}
hasQuota := typeLabel == "openai" || typeLabel == "adobe" || typeLabel == "runway" || typeLabel == "leonardo" || typeLabel == "krea" || typeLabel == "imagine" || typeLabel == "grok"
return map[string]any{
"id": item.ID,
@@ -1742,6 +1700,8 @@ func accountRow(item model.TokenAccount, inFlight int64) map[string]any {
"email": emptyToNil(email),
"team_id": emptyToNil(teamID),
"remaining": valueOrNil(hasQuota && hasRemaining, remaining),
"image_remaining": grokImages,
"video_remaining": grokVideos,
"reset_after": emptyToNil(item.CachedQuotaResetAfter),
"quota_cached_at": valueOrNil(quotaAt != 0, quotaAt),
"created_at": unixOrNil(item.AddedAt),