@goodandready/dsh-subscriptions 0.1.1 → 0.2.1
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/client.js +102 -29
- package/lib/images.js +111 -0
- package/lib/index.js +42 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -35,15 +35,76 @@ window.__ModuleLoader__.load({
|
|
|
35
35
|
document.head.appendChild(tag)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
// Строки карточки живут в реестре локалей: так их переводит отдельный
|
|
39
|
+
// пакет, не трогая код этого плагина. Английский — язык по умолчанию,
|
|
40
|
+
// на него же приходится откат, если перевода нет.
|
|
41
|
+
const NS = 'dsh-subscriptions'
|
|
42
|
+
const en = {
|
|
43
|
+
'notConnected': 'Not connected',
|
|
44
|
+
'verifyAccount': 'Verify account',
|
|
45
|
+
'coolingDown': 'Cooling down',
|
|
46
|
+
'usageFull': 'Usage 100%',
|
|
47
|
+
'connected': 'Connected',
|
|
48
|
+
'title': 'Subscriptions',
|
|
49
|
+
'intro': 'Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.',
|
|
50
|
+
'useOrigin': 'Use this Web UI origin as OAuth redirect_uri',
|
|
51
|
+
'useOriginHint': 'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.',
|
|
52
|
+
'reconnect': 'Reconnect',
|
|
53
|
+
'connect': 'Connect',
|
|
54
|
+
'disconnect': 'Disconnect',
|
|
55
|
+
'removeSlot': 'Remove slot',
|
|
56
|
+
'pastePlaceholder': 'Paste redirected URL or code',
|
|
57
|
+
'submitCode': 'Submit code',
|
|
58
|
+
'verifyPrefix': 'Google requires one-time account verification. ',
|
|
59
|
+
'verifyLink': 'Open verification link',
|
|
60
|
+
'verifySuffix': ' then reconnect.',
|
|
61
|
+
'addAccount': '+ Add account',
|
|
62
|
+
'save': 'Save',
|
|
63
|
+
'saved': 'Saved',
|
|
64
|
+
'loading': 'Loading\u2026',
|
|
65
|
+
'accountLabel': 'Account',
|
|
66
|
+
'plan': 'Plan',
|
|
67
|
+
'storedAs': 'Stored as',
|
|
68
|
+
}
|
|
69
|
+
const ru = {
|
|
70
|
+
'notConnected': 'Не подключено',
|
|
71
|
+
'verifyAccount': 'Требуется проверка',
|
|
72
|
+
'coolingDown': 'Пауза после лимита',
|
|
73
|
+
'usageFull': 'Лимит исчерпан',
|
|
74
|
+
'connected': 'Подключено',
|
|
75
|
+
'title': 'Подписки',
|
|
76
|
+
'intro': 'Вход по обычной пользовательской подписке. Токены остаются в хранилище учётных данных харнесса, браузер их обратно не читает. Если после «Подключить» провайдер увёл на localhost или на свою страницу, вставьте сюда адрес перехода или код.',
|
|
77
|
+
'useOrigin': 'Использовать адрес этого веб-интерфейса как redirect_uri',
|
|
78
|
+
'useOriginHint': 'Включайте, только если зарегистрировали собственного клиента OAuth на этот адрес. Штатным консольным клиентам провайдеров нужен их опубликованный адрес возврата и вставка кода вручную.',
|
|
79
|
+
'reconnect': 'Переподключить',
|
|
80
|
+
'connect': 'Подключить',
|
|
81
|
+
'disconnect': 'Отключить',
|
|
82
|
+
'removeSlot': 'Убрать место',
|
|
83
|
+
'pastePlaceholder': 'Адрес перехода или код',
|
|
84
|
+
'submitCode': 'Отправить код',
|
|
85
|
+
'verifyPrefix': 'Google требует однократной проверки аккаунта. ',
|
|
86
|
+
'verifyLink': 'Открыть страницу проверки',
|
|
87
|
+
'verifySuffix': ' затем подключитесь заново.',
|
|
88
|
+
'addAccount': '+ Добавить аккаунт',
|
|
89
|
+
'save': 'Сохранить',
|
|
90
|
+
'saved': 'Сохранено',
|
|
91
|
+
'loading': 'Загрузка…',
|
|
92
|
+
'accountLabel': 'Аккаунт',
|
|
93
|
+
'plan': 'Тариф',
|
|
94
|
+
'storedAs': 'Хранится как',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function badgeFor(account, t) {
|
|
98
|
+
if (!account || !account.configured) return t('notConnected')
|
|
99
|
+
if (account.validationUrl) return t('verifyAccount')
|
|
100
|
+
if (account.cooldownUntil && account.cooldownUntil > Date.now()) return t('coolingDown')
|
|
101
|
+
if (account.usagePercent != null && account.usagePercent >= 100) return t('usageFull')
|
|
102
|
+
return t('connected')
|
|
44
103
|
}
|
|
45
104
|
|
|
46
|
-
function SubsSection() {
|
|
105
|
+
function SubsSection(props) {
|
|
106
|
+
// Переводчик приходит от слота, потому что в его записи указан locale.
|
|
107
|
+
const t = (props && props.t) || ((key) => key)
|
|
47
108
|
const [draft, setDraft] = React.useState(null)
|
|
48
109
|
const [accounts, setAccounts] = React.useState([])
|
|
49
110
|
const [providers, setProviders] = React.useState([])
|
|
@@ -67,7 +128,7 @@ window.__ModuleLoader__.load({
|
|
|
67
128
|
return () => { alive = false }
|
|
68
129
|
}, [])
|
|
69
130
|
|
|
70
|
-
if (!draft) return React.createElement('div', { className: 'dsub-wrap' }, '
|
|
131
|
+
if (!draft) return React.createElement('div', { className: 'dsub-wrap' }, t('loading'))
|
|
71
132
|
|
|
72
133
|
const slots = Array.isArray(draft.slots) ? draft.slots : []
|
|
73
134
|
const setSlots = (next) => setDraft((d) => Object.assign({}, d, { slots: next }))
|
|
@@ -137,19 +198,19 @@ window.__ModuleLoader__.load({
|
|
|
137
198
|
|
|
138
199
|
return React.createElement('div', { className: 'dsub-wrap' },
|
|
139
200
|
React.createElement('div', { className: 'dsub-block' },
|
|
140
|
-
React.createElement('div', { className: 'dsub-h' }, '
|
|
201
|
+
React.createElement('div', { className: 'dsub-h' }, t('title')),
|
|
141
202
|
React.createElement('div', { className: 'dsub-sub' },
|
|
142
|
-
'
|
|
203
|
+
t('intro')),
|
|
143
204
|
React.createElement('label', { className: 'dsub-row' },
|
|
144
205
|
React.createElement('input', {
|
|
145
206
|
type: 'checkbox',
|
|
146
207
|
checked: !!draft.useWebCallback,
|
|
147
208
|
onChange: (e) => setDraft((d) => Object.assign({}, d, { useWebCallback: e.target.checked })),
|
|
148
209
|
}),
|
|
149
|
-
React.createElement('span', null, '
|
|
210
|
+
React.createElement('span', null, t('useOrigin')),
|
|
150
211
|
),
|
|
151
212
|
React.createElement('div', { className: 'dsub-sub' },
|
|
152
|
-
'
|
|
213
|
+
t('useOriginHint')),
|
|
153
214
|
),
|
|
154
215
|
names.map((prov) => {
|
|
155
216
|
const rows = slots
|
|
@@ -166,25 +227,25 @@ window.__ModuleLoader__.load({
|
|
|
166
227
|
React.createElement('input', {
|
|
167
228
|
className: 'dsub-grow',
|
|
168
229
|
value: row.slot.label || '',
|
|
169
|
-
placeholder: account.label || ('
|
|
230
|
+
placeholder: account.label || (t('accountLabel') + ' ' + row.slot.index),
|
|
170
231
|
onChange: (e) => {
|
|
171
232
|
const next = slots.slice()
|
|
172
233
|
next[row.i] = Object.assign({}, next[row.i], { label: e.target.value })
|
|
173
234
|
setSlots(next)
|
|
174
235
|
},
|
|
175
236
|
}),
|
|
176
|
-
React.createElement('span', { className: 'dsub-badge' + (account.validationUrl ? ' dsub-badge-warn' : (on ? ' dsub-badge-on' : '')) }, badgeFor(account)),
|
|
237
|
+
React.createElement('span', { className: 'dsub-badge' + (account.validationUrl ? ' dsub-badge-warn' : (on ? ' dsub-badge-on' : '')) }, badgeFor(account, t)),
|
|
177
238
|
React.createElement('button', {
|
|
178
239
|
type: 'button', className: 'dsub-mini',
|
|
179
240
|
onClick: () => connect(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
|
|
180
|
-
}, on ? '
|
|
241
|
+
}, on ? t('reconnect') : t('connect')),
|
|
181
242
|
React.createElement('button', {
|
|
182
243
|
type: 'button', className: 'dsub-mini',
|
|
183
244
|
disabled: !on,
|
|
184
245
|
onClick: () => logout(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
|
|
185
|
-
}, '
|
|
246
|
+
}, t('disconnect')),
|
|
186
247
|
React.createElement('button', {
|
|
187
|
-
type: 'button', className: 'dsub-mini', title: '
|
|
248
|
+
type: 'button', className: 'dsub-mini', title: t('removeSlot'),
|
|
188
249
|
onClick: () => {
|
|
189
250
|
const slot = row.slot
|
|
190
251
|
Promise.resolve(on ? logout(slot.provider, slot.index) : null)
|
|
@@ -197,49 +258,61 @@ window.__ModuleLoader__.load({
|
|
|
197
258
|
React.createElement('input', {
|
|
198
259
|
className: 'dsub-grow',
|
|
199
260
|
value: paste[key] || '',
|
|
200
|
-
placeholder: '
|
|
261
|
+
placeholder: t('pastePlaceholder'),
|
|
201
262
|
onChange: (e) => setPaste((p) => Object.assign({}, p, { [key]: e.target.value })),
|
|
202
263
|
}),
|
|
203
264
|
React.createElement('button', {
|
|
204
265
|
type: 'button', className: 'dsub-mini',
|
|
205
266
|
onClick: () => complete(row.slot.provider, row.slot.index).catch((e) => setErr(String(e.message || e))),
|
|
206
|
-
}, '
|
|
267
|
+
}, t('submitCode')),
|
|
207
268
|
),
|
|
208
269
|
account.accountNotice ? React.createElement('div', { className: 'dsub-verify' }, account.accountNotice) : null,
|
|
209
270
|
account.validationUrl ? React.createElement('div', { className: 'dsub-verify' },
|
|
210
|
-
'
|
|
211
|
-
React.createElement('a', { href: account.validationUrl, target: '_blank', rel: 'noopener noreferrer' }, '
|
|
212
|
-
'
|
|
271
|
+
t('verifyPrefix'),
|
|
272
|
+
React.createElement('a', { href: account.validationUrl, target: '_blank', rel: 'noopener noreferrer' }, t('verifyLink')),
|
|
273
|
+
t('verifySuffix'),
|
|
213
274
|
) : null,
|
|
214
|
-
account.paidTierName ? React.createElement('span', { className: 'dsub-sub' }, '
|
|
215
|
-
account.ref ? React.createElement('span', { className: 'dsub-sub' }, '
|
|
275
|
+
account.paidTierName ? React.createElement('span', { className: 'dsub-sub' }, t('plan') + ': ' + account.paidTierName) : null,
|
|
276
|
+
account.ref ? React.createElement('span', { className: 'dsub-sub' }, t('storedAs') + ' ' + account.ref) : null,
|
|
216
277
|
)
|
|
217
278
|
}),
|
|
218
279
|
React.createElement('button', {
|
|
219
280
|
type: 'button', className: 'dsub-mini',
|
|
220
281
|
onClick: () => addSlot(prov.id),
|
|
221
|
-
}, '
|
|
282
|
+
}, t('addAccount')),
|
|
222
283
|
)
|
|
223
284
|
}),
|
|
224
285
|
React.createElement('div', { className: 'dsub-row' },
|
|
225
286
|
React.createElement('button', {
|
|
226
287
|
type: 'button', className: 'dsub-save',
|
|
227
288
|
onClick: () => save().catch((e) => setErr(String(e.message || e))),
|
|
228
|
-
}, '
|
|
229
|
-
saved ? React.createElement('span', { className: 'dsub-ok' }, '
|
|
289
|
+
}, t('save')),
|
|
290
|
+
saved ? React.createElement('span', { className: 'dsub-ok' }, t('saved')) : null,
|
|
230
291
|
err ? React.createElement('span', { className: 'dsub-bad' }, err) : null,
|
|
231
292
|
),
|
|
232
293
|
)
|
|
233
294
|
}
|
|
234
295
|
|
|
235
296
|
function registerSettings(ctx) {
|
|
297
|
+
ctx.effect(() => ctx.locale.register(NS, { en, ru }), 'dsh-subscriptions: словари')
|
|
298
|
+
// Подпись раздела рисует боковой список, а не наш компонент: props.t
|
|
299
|
+
// туда не доходит, поэтому берём переводчик, привязанный к namespace.
|
|
300
|
+
const t = ctx.locale.bind(NS)
|
|
236
301
|
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
237
|
-
{
|
|
302
|
+
{
|
|
303
|
+
name: 'settings.section',
|
|
304
|
+
id: '@goodandready/dsh-subscriptions',
|
|
305
|
+
order: 28,
|
|
306
|
+
// locale в записи слота — то, из-за чего компонент получает props.t.
|
|
307
|
+
locale: NS,
|
|
308
|
+
label: () => t('title'),
|
|
309
|
+
inject: () => ({ ctx: ctx }),
|
|
310
|
+
},
|
|
238
311
|
SubsSection,
|
|
239
312
|
))
|
|
240
313
|
}
|
|
241
314
|
|
|
242
|
-
exports.inject = ['slots']
|
|
315
|
+
exports.inject = ['slots', 'locale']
|
|
243
316
|
exports.apply = function apply(ctx) {
|
|
244
317
|
registerSettings(ctx)
|
|
245
318
|
}
|
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