@goodandready/dsh-subscriptions 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/lib/images.js +111 -0
- package/lib/index.js +42 -0
- package/lib/vendors/antigravity.js +3 -5
- package/package.json +1 -1
- package/lib/vendors/antigravity-oauth.js +0 -10
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Settings GET never returns access or refresh tokens — only
|
|
|
59
59
|
| `codex` | ChatGPT / Codex | Vendor-public Codex CLI client id |
|
|
60
60
|
| `claude` | Claude Pro/Max | Vendor-public Claude Code client id |
|
|
61
61
|
| `grok` | xAI / SuperGrok | Vendor-public Grok CLI client id |
|
|
62
|
-
| `antigravity` | Google Antigravity |
|
|
62
|
+
| `antigravity` | Google Antigravity | Your OAuth client id + secret in Config |
|
|
63
63
|
|
|
64
64
|
Override `codexClientId`, `claudeClientId`, and the other empty Config fields
|
|
65
65
|
if you register your own OAuth app.
|
|
@@ -67,16 +67,16 @@ if you register your own OAuth app.
|
|
|
67
67
|
Live requests use the vendor subscription surfaces, not API-key hosts:
|
|
68
68
|
Codex `chatgpt.com/backend-api/codex/responses`, Claude Messages with the
|
|
69
69
|
OAuth beta header, Grok `cli-chat-proxy.grok.com` with CLI identity headers,
|
|
70
|
-
and
|
|
70
|
+
and Antigravity Cloud Code Assist (`loadCodeAssist` then
|
|
71
71
|
`streamGenerateContent`). Usage endpoints, when they answer, feed the 100%
|
|
72
72
|
skip. If a live model list fails, the built-in catalog is used.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
Antigravity uses a confidential Google OAuth client: set `antigravityClientId`
|
|
74
|
+
and `antigravityClientSecret` in plugin Config (Settings) — nothing is baked
|
|
75
|
+
into the repository.
|
|
76
76
|
|
|
77
77
|
Default model catalogs are built-in lists you can replace with
|
|
78
|
-
`codexModels`, `claudeModels`, `grokModels`, `
|
|
79
|
-
|
|
78
|
+
`codexModels`, `claudeModels`, `grokModels`, `antigravityModels`
|
|
79
|
+
in the plugin Config.
|
|
80
80
|
|
|
81
81
|
## Rotation
|
|
82
82
|
|
package/lib/images.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Генерация картинок на подписке.
|
|
2
|
+
//
|
|
3
|
+
// Здесь только протокол: куда идти, что послать и как прочитать ответ. Всё
|
|
4
|
+
// остальное — сохранение файла, вложение в разговор, карточка — дело плагина
|
|
5
|
+
// генерации; этот плагин лишь одалживает свой аккаунт.
|
|
6
|
+
//
|
|
7
|
+
// Токен наружу не отдаётся: плагин объявляет службу внутри процесса, а не
|
|
8
|
+
// маршрут в сети. Харнесс на этой машине доступен без пароля, и ручка,
|
|
9
|
+
// раздающая живой токен подписки, была бы дырой пошире тех, что мы закрывали.
|
|
10
|
+
|
|
11
|
+
/** Куда уходит запрос у подписки ChatGPT. */
|
|
12
|
+
export const CODEX_URL = 'https://chatgpt.com/backend-api/codex/images/generations'
|
|
13
|
+
/** Модель, которую отдаёт этот адрес. */
|
|
14
|
+
export const CODEX_MODEL = 'gpt-image-2'
|
|
15
|
+
/** Куда уходит запрос у подписки Grok. */
|
|
16
|
+
export const GROK_URL = 'https://api.x.ai/v1/images/generations'
|
|
17
|
+
/** Модель, которую отдаёт этот адрес. */
|
|
18
|
+
export const GROK_MODEL = 'grok-imagine-image-2.0'
|
|
19
|
+
|
|
20
|
+
/** Размеры, которые понимает ChatGPT. */
|
|
21
|
+
export const SIZES = ['1024x1024', '1024x1536', '1536x1024', 'auto']
|
|
22
|
+
|
|
23
|
+
/** Grok мыслит не размерами, а соотношением сторон. */
|
|
24
|
+
const GROK_ASPECT = {
|
|
25
|
+
'1024x1024': '1:1',
|
|
26
|
+
'1024x1536': '2:3',
|
|
27
|
+
'1536x1024': '3:2',
|
|
28
|
+
auto: 'auto',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function codexBody({ prompt, size, quality }) {
|
|
32
|
+
const text = String(prompt || '').trim()
|
|
33
|
+
if (!text) throw new Error('нужен непустой запрос')
|
|
34
|
+
return {
|
|
35
|
+
prompt: text,
|
|
36
|
+
model: CODEX_MODEL,
|
|
37
|
+
...(size ? { size } : {}),
|
|
38
|
+
...(quality ? { quality } : {}),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function grokBody({ prompt, size, quality }) {
|
|
43
|
+
const text = String(prompt || '').trim()
|
|
44
|
+
if (!text) throw new Error('нужен непустой запрос')
|
|
45
|
+
// У Grok качество всего двух ступеней: высокое складывается со средним.
|
|
46
|
+
const level = quality === 'low' ? 'low'
|
|
47
|
+
: (quality === 'medium' || quality === 'high') ? 'medium'
|
|
48
|
+
: undefined
|
|
49
|
+
return {
|
|
50
|
+
prompt: text,
|
|
51
|
+
model: GROK_MODEL,
|
|
52
|
+
response_format: 'b64_json',
|
|
53
|
+
...(size && GROK_ASPECT[size] ? { aspect_ratio: GROK_ASPECT[size] } : {}),
|
|
54
|
+
...(level ? { quality: level } : {}),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Разбор ответа: обе стороны отвечают одинаково. */
|
|
59
|
+
export function parseImages(payload) {
|
|
60
|
+
const body = payload && typeof payload === 'object' ? payload : {}
|
|
61
|
+
const rows = Array.isArray(body.data) ? body.data : []
|
|
62
|
+
const images = []
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
if (!row || typeof row !== 'object') continue
|
|
65
|
+
if (typeof row.b64_json !== 'string' || !row.b64_json) continue
|
|
66
|
+
images.push({
|
|
67
|
+
b64_json: row.b64_json,
|
|
68
|
+
...(typeof row.revised_prompt === 'string' && row.revised_prompt
|
|
69
|
+
? { revisedPrompt: row.revised_prompt }
|
|
70
|
+
: {}),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
if (!images.length) throw new Error('в ответе нет картинок')
|
|
74
|
+
return images
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Один запрос к нужному адресу с заголовками этого провайдера.
|
|
79
|
+
*
|
|
80
|
+
* @param options {{provider, prompt, size, quality, session, fetchImpl, signal}}
|
|
81
|
+
* session — уже освежённый блок токенов: accessToken и, для ChatGPT, accountId.
|
|
82
|
+
*/
|
|
83
|
+
export async function generateOnce(options) {
|
|
84
|
+
const { provider, session, fetchImpl, signal } = options
|
|
85
|
+
const isCodex = provider === 'codex'
|
|
86
|
+
const url = isCodex ? CODEX_URL : GROK_URL
|
|
87
|
+
const body = isCodex ? codexBody(options) : grokBody(options)
|
|
88
|
+
const headers = isCodex
|
|
89
|
+
? {
|
|
90
|
+
authorization: `Bearer ${session.accessToken}`,
|
|
91
|
+
// ChatGPT различает аккаунты отдельным заголовком, без него отвечает отказом.
|
|
92
|
+
'chatgpt-account-id': session.accountId || '',
|
|
93
|
+
originator: 'codex_cli_rs',
|
|
94
|
+
'content-type': 'application/json',
|
|
95
|
+
accept: 'application/json',
|
|
96
|
+
}
|
|
97
|
+
: {
|
|
98
|
+
authorization: `Bearer ${session.accessToken}`,
|
|
99
|
+
'content-type': 'application/json',
|
|
100
|
+
accept: 'application/json',
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const res = await fetchImpl(url, { method: 'POST', headers, body: JSON.stringify(body), signal })
|
|
104
|
+
const payload = await res.json().catch(() => ({}))
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const detail = payload && payload.error
|
|
107
|
+
&& (payload.error.message || payload.error.code || payload.error)
|
|
108
|
+
throw new Error(`${provider} HTTP ${res.status}${detail ? ': ' + String(detail).slice(0, 200) : ''}`)
|
|
109
|
+
}
|
|
110
|
+
return parseImages(payload)
|
|
111
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { parseCallbackInput, requestOrigin, webCallbackUri } from './oauth.js'
|
|
|
5
5
|
import { writeJson, writeHtml, readBody, isTrustedSettingsRequest, queryOf } from './http.js'
|
|
6
6
|
import { getVendor } from './vendors/index.js'
|
|
7
7
|
import { SubscriptionAdapter } from './adapter.js'
|
|
8
|
+
import { generateOnce, SIZES as IMAGE_SIZES } from './images.js'
|
|
9
|
+
import { parseBlob } from './blob.js'
|
|
8
10
|
import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
|
|
9
11
|
import {
|
|
10
12
|
inspectGoogleAccount,
|
|
@@ -77,6 +79,46 @@ export function apply(ctx, config) {
|
|
|
77
79
|
|
|
78
80
|
const live = () => Config(structuredClone(getConfig() ?? {})) ?? config
|
|
79
81
|
const store = createAccountStore({ credentials: ctx.credentials, getConfig: live, fetchImpl: fetch })
|
|
82
|
+
|
|
83
|
+
// Служба генерации картинок на подписке.
|
|
84
|
+
//
|
|
85
|
+
// Наружу отдаётся действие, а не токен: маршрут в сети раздавал бы живой
|
|
86
|
+
// ключ доступа каждому, кто дотянется до харнесса, а служба живёт внутри
|
|
87
|
+
// процесса и видна только другим плагинам. Обновлением токена по-прежнему
|
|
88
|
+
// занимается один хозяин — этот плагин.
|
|
89
|
+
ctx.effect(() => ctx.provide('subscriptionImages', {
|
|
90
|
+
/** Провайдеры, у которых есть вход прямо сейчас. */
|
|
91
|
+
async available() {
|
|
92
|
+
const logged = await store.loggedInProviders()
|
|
93
|
+
return ['codex', 'grok'].filter((name) => logged && logged[name])
|
|
94
|
+
},
|
|
95
|
+
sizes: IMAGE_SIZES,
|
|
96
|
+
/**
|
|
97
|
+
* @param request {{provider, prompt, size, quality, signal}}
|
|
98
|
+
* @returns [{ b64_json, revisedPrompt? }]
|
|
99
|
+
*/
|
|
100
|
+
async generate(request) {
|
|
101
|
+
const provider = request && request.provider
|
|
102
|
+
if (provider !== 'codex' && provider !== 'grok') {
|
|
103
|
+
throw new Error(`неизвестный провайдер подписки: ${provider}`)
|
|
104
|
+
}
|
|
105
|
+
const accounts = await store.listAccounts(provider)
|
|
106
|
+
const slot = (accounts || []).find((row) => row && row.ref)
|
|
107
|
+
if (!slot) throw new Error(`нет входа в ${provider}: войдите в разделе «Подписки»`)
|
|
108
|
+
const raw = await store.resolveRaw(slot.ref)
|
|
109
|
+
if (!raw) throw new Error(`нет входа в ${provider}: войдите в разделе «Подписки»`)
|
|
110
|
+
const session = await store.ensureFresh(provider, parseBlob(raw), slot.ref)
|
|
111
|
+
return generateOnce({
|
|
112
|
+
provider,
|
|
113
|
+
prompt: request.prompt,
|
|
114
|
+
size: request.size,
|
|
115
|
+
quality: request.quality,
|
|
116
|
+
session,
|
|
117
|
+
fetchImpl: fetch,
|
|
118
|
+
signal: request.signal,
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
}), 'dsh-subscriptions: служба генерации картинок')
|
|
80
122
|
const pending = new Map()
|
|
81
123
|
const adapter = new SubscriptionAdapter({
|
|
82
124
|
listAccounts: (provider) => store.listAccounts(provider),
|
|
@@ -3,7 +3,6 @@ import { buildAuthorizeUrl } from '../oauth.js'
|
|
|
3
3
|
import { googleContents } from '../messages.js'
|
|
4
4
|
import { formTokenRequest, googleStream, throwHttpError, tokenBlobFromOAuth } from '../wire.js'
|
|
5
5
|
import { emailFromToken } from '../jwt.js'
|
|
6
|
-
import { builtinAntigravityOAuth } from './antigravity-oauth.js'
|
|
7
6
|
import {
|
|
8
7
|
CODE_ASSIST_STREAM,
|
|
9
8
|
antigravityMetadata,
|
|
@@ -32,11 +31,10 @@ export function providerInfo() {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export function defaults() {
|
|
35
|
-
const builtin = builtinAntigravityOAuth()
|
|
36
34
|
return {
|
|
37
|
-
clientId:
|
|
38
|
-
clientSecret:
|
|
39
|
-
redirectUri:
|
|
35
|
+
clientId: '',
|
|
36
|
+
clientSecret: '',
|
|
37
|
+
redirectUri: 'https://antigravity.google/oauth-callback',
|
|
40
38
|
models: ['gemini-3.5-flash-low', 'gemini-3-flash', 'gemini-2.5-flash'],
|
|
41
39
|
}
|
|
42
40
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export function builtinAntigravityOAuth() {
|
|
2
|
-
// Public Antigravity IDE OAuth app (same credentials as desktop IDE).
|
|
3
|
-
const clientId = String.fromCharCode(49,48,55,49,48,48,54,48,54,48,53,57,49,45,116,109,104,115,115,105,110,50,104,50,49,108,99,114,101,50,51,53,118,116,111,108,111,106,104,52,103,52,48,51,101,112,46,97,112,112,115,46,103,111,111,103,108,101,117,115,101,114,99,111,110,116,101,110,116,46,99,111,109)
|
|
4
|
-
const clientSecret = String.fromCharCode(71,79,67,83,80,88,45,75,53,56,70,87,82,52,56,54,76,100,76,74,49,109,76,66,56,115,88,67,52,122,54,113,68,65,102)
|
|
5
|
-
return {
|
|
6
|
-
clientId,
|
|
7
|
-
clientSecret,
|
|
8
|
-
redirectUri: 'https://antigravity.google/oauth-callback',
|
|
9
|
-
}
|
|
10
|
-
}
|