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
+13 -4
View File
@@ -259,6 +259,8 @@ function applyQuota(row, result) {
if (result.unknown && result.remaining === null) { row.remaining = null; row._unknown = true; return }
row._unknown = false
row.remaining = result.remaining
if (result.image_remaining !== undefined) row.image_remaining = result.image_remaining
if (result.video_remaining !== undefined) row.video_remaining = result.video_remaining
row.reset_after = result.reset_after
// 会员身份随额度一起回写:改过会员/掉了会员的号,查一次额度徽章就跟着更新。
if (result.plan !== undefined) row.plan = result.plan
@@ -391,7 +393,7 @@ onMounted(() => { loadAccounts(); loadModelList() })
<button @click="setFilter(() => planFilter = '')" class="fp" :class="planFilter === '' && 'fp-on'">全部账号</button>
<button @click="setFilter(() => planFilter = 'free')" class="fp" :class="planFilter === 'free' && 'fp-slate'">普号</button>
<button @click="setFilter(() => planFilter = 'sub')" class="fp" :class="planFilter === 'sub' && 'fp-sky'">子号</button>
<button @click="setFilter(() => planFilter = 'master')" class="fp" :class="planFilter === 'master' && 'fp-purple'"></button>
<button @click="setFilter(() => planFilter = 'master')" class="fp" :class="planFilter === 'master' && 'fp-purple'">会员</button>
</div>
</template>
<div class="flex-1 min-w-[200px]">
@@ -480,7 +482,7 @@ onMounted(() => { loadAccounts(); loadModelList() })
<span v-if="a.sub_account" class="acct-tag acct-tag-sub"
:title="'子号 · 会员 (剩余 ' + (a.remaining ?? '—') + ' 积分)'">子号</span>
<span v-else-if="a.plan && a.plan !== 'free'" class="acct-tag acct-tag-master"
:title="'母号 · 会员 (' + a.plan + ')'"></span>
:title="'会员 (' + a.plan + ')'">会员</span>
<span v-else-if="a.plan === 'free'" class="acct-tag acct-tag-free"
title="普号 (无会员)">普号</span>
<span v-if="a.image_limited && a.status !== 'quota' && a.plan !== 'free'" class="acct-tag acct-tag-limit"
@@ -498,9 +500,16 @@ onMounted(() => { loadAccounts(); loadModelList() })
<td class="px-3 py-3.5 align-middle text-right text-sm tabular-nums whitespace-nowrap">
<!-- quota column: 数字 / (never "未知"/"失败"/"检测中") -->
<!-- remaining === -1 is the provider "unlimited" sentinel show not a scary red -1 -->
<span v-if="(a.type === 'openai' || a.type === 'adobe' || a.type === 'runway' || a.type === 'leonardo' || a.type === 'krea' || a.type === 'imagine' || a.type === 'grok') && a.remaining != null && a.remaining !== -1"
<!-- grok 额度是本地写死的两份计数 / 视频用完即判死没有恢复时间 -->
<span v-if="a.type === 'grok' && a.image_remaining != null"
class="font-mono font-semibold" title="图片剩余 / 视频剩余">
<span :class="a.image_remaining > 0 ? 'text-emerald-300' : 'text-rose-300'">{{ a.image_remaining }}</span>
<span class="text-white/20">/</span>
<span :class="a.video_remaining > 0 ? 'text-emerald-300' : 'text-rose-300'">{{ a.video_remaining }}</span>
</span>
<span v-else-if="(a.type === 'openai' || a.type === 'adobe' || a.type === 'runway' || a.type === 'leonardo' || a.type === 'krea' || a.type === 'imagine') && a.remaining != null && a.remaining !== -1"
class="font-mono font-semibold"
:class="a.remaining > 0 ? 'text-emerald-300' : 'text-rose-300'">{{ a.remaining }}{{ a.type === 'grok' ? '%' : '' }}</span>
:class="a.remaining > 0 ? 'text-emerald-300' : 'text-rose-300'">{{ a.remaining }}</span>
<span v-else class="text-white/25" :title="a._quotaError || ''"></span>
</td>
<!-- weight (edit via modal) -->
+10 -1
View File
@@ -556,14 +556,23 @@ async function fireOne() {
payload.reference_images = refs.filter(Boolean)
}
// 同一个任务带固定的 Idempotency-Key:长请求期间连接被重置、浏览器/网关透明
// 重发 POST 时,后端不会再生成一次、再扣一次积分。
const opts = jsonBody('POST', payload)
opts.headers['Idempotency-Key'] = task.id + '-' + task.ts
try {
const r = await api('/generate', jsonBody('POST', payload))
const r = await api('/generate', opts)
if (r.ok && r.data?.url) {
task.status = 'done'
task.url = r.data.url
task.elapsed_ms = r.data.elapsed_ms
task.charged = r.data.charged ?? chargedPrice
if (auth.user && r.data.credits != null) auth.user.credits = r.data.credits
} else if (r.status === 409 && String(r.data?.detail || '').includes('重复提交')) {
// 这次响应来自被透明重发的那个 POST —— 原任务还在生成,保持 running,
// loadHistory() 在结果落库后认领它。
task.status = 'running'
} else if (GATEWAY_TIMEOUT.has(r.status)) {
// CDN/代理回源超时(如 EdgeOne 524)—— 后端仍在生成。保持 running,
// loadHistory() 在结果落库后认领它。