@goodandready/dsh-subscriptions 0.1.1 → 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/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),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, and Antigravity subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",