diff --git a/README.en.md b/README.en.md index 2889a7c..ee71977 100644 --- a/README.en.md +++ b/README.en.md @@ -2,7 +2,7 @@
{"token","refreshToken","email","parentId"})、
+ Grok SSO(grok.com 的 sso 值,仅含 session_id,自动与 ChatGPT/Runway 区分)、
多个 JWT(换行分隔)。
全粘进来即可,无需任何前缀。
@@ -121,6 +125,9 @@ async function doSmartImport() {
Imagine · {{ detected.imagine }}
+
+ Grok · {{ detected.grok }}
+
未识别到任何 Cookie 或 JWT
diff --git a/frontend/src/utils/import.js b/frontend/src/utils/import.js
index 62f670f..1338902 100644
--- a/frontend/src/utils/import.js
+++ b/frontend/src/utils/import.js
@@ -28,6 +28,16 @@ export function looksLikeRunwayJwt(s) {
return 'sso' in claims && claims.id != null
}
+// Grok website "sso" JWTs carry ONLY a session_id claim (no openai claims, no
+// runway id/sso) — that's what tells them apart from a ChatGPT/Runway JWT.
+export function looksLikeGrokJwt(s) {
+ const claims = decodeJwtPayload(s)
+ if (!claims || typeof claims !== 'object') return false
+ if (Object.keys(claims).some((k) => k.startsWith('https://api.openai.com/'))) return false
+ if ('sso' in claims || claims.id != null) return false
+ return 'session_id' in claims
+}
+
// Leonardo cookies carry the better-auth session cookie — that's what tells them
// apart from an Adobe cookie (both are otherwise opaque cookie strings).
export function looksLikeLeonardoCookie(s) {
@@ -104,11 +114,15 @@ export function parseImportInput(text) {
// treated as an Adobe cookie string.
const lines = text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
return lines.map((line) => {
- if (looksLikeJwt(line)) {
- const value = line.replace(/^Bearer\s+/i, '')
+ // Accept a bare JWT or one with a leading `sso=` (grok cookie value form).
+ const stripped = line.replace(/^Bearer\s+/i, '').replace(/^sso=/, '')
+ if (looksLikeJwt(stripped)) {
+ const value = stripped
return looksLikeRunwayJwt(value)
? { type: 'runway', value }
- : { type: 'openai', value }
+ : looksLikeGrokJwt(value)
+ ? { type: 'grok', value }
+ : { type: 'openai', value }
}
return { type: cookieType(line), value: line }
})
diff --git a/frontend/src/views/AccountsView.vue b/frontend/src/views/AccountsView.vue
index 9da9828..91761f5 100644
--- a/frontend/src/views/AccountsView.vue
+++ b/frontend/src/views/AccountsView.vue
@@ -35,6 +35,7 @@ const stats = computed(() => {
total: rows.value.length,
openai: by('openai'), adobe: by('adobe'), runway: by('runway'),
leonardo: by('leonardo'), krea: by('krea'), imagine: by('imagine'),
+ grok: by('grok'),
}
})
@@ -135,7 +136,7 @@ async function reconcile() {
// probed here — the pending poll just reads the store until the worker writes
// their quota/email. OLD accounts (active) get a real live /quota probe for
// up-to-date remaining + refresh time.
- const quotaRows = visible.filter((r) => !r.pending && r.status === 'active' && (r.type === 'openai' || r.type === 'adobe' || r.type === 'runway' || r.type === 'leonardo' || r.type === 'krea' || r.type === 'imagine'))
+ const quotaRows = visible.filter((r) => !r.pending && r.status === 'active' && (r.type === 'openai' || r.type === 'adobe' || r.type === 'runway' || r.type === 'leonardo' || r.type === 'krea' || r.type === 'imagine' || r.type === 'grok'))
const adobeNeedEmail = visible.filter((r) => !r.pending && r.type === 'adobe' && !r.email)
const total = quotaRows.length + adobeNeedEmail.length
if (total === 0) { quotaStatus.value = ''; return }
@@ -296,7 +297,7 @@ onMounted(loadAccounts)