dsh-openai-subscription 0.1.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/dist/host.js ADDED
@@ -0,0 +1,688 @@
1
+ // Host service for ChatGPT subscription authorization in DSH.
2
+ // OAuth is delegated to the OpenAI Codex integration bundled with DSH.
3
+ import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
4
+ import { normalizeOAuthCredential } from './oauth.js';
5
+ /** Plugin-owned authorization metadata. */
6
+ const KEY = 'dsh-openai-subscription/chatgpt';
7
+ /** Credential consumed by the `openai-codex` model provider. */
8
+ const PI_AI_RECORD = 'llm-pi-ai/openai-codex';
9
+ const LOCATE_SCRIPT = [
10
+ 'set -eu',
11
+ 'CAND=""',
12
+ 'B="$(command -v pi 2>/dev/null || true)"',
13
+ 'if [ -n "$B" ]; then',
14
+ ' R="$(readlink -f "$B" 2>/dev/null || printf "%s" "$B")"',
15
+ ' D="$(dirname "$R")"',
16
+ ' while [ "$D" != "/" ] && [ -n "$D" ]; do',
17
+ ' if [ -f "$D/node_modules/@earendil-works/pi-ai/dist/auth/oauth/openai-codex.js" ]; then',
18
+ ' CAND="$D/node_modules/@earendil-works/pi-ai/dist/auth/oauth/openai-codex.js"',
19
+ ' break',
20
+ ' fi',
21
+ ' D="$(dirname "$D")"',
22
+ ' done',
23
+ 'fi',
24
+ 'if [ -z "$CAND" ] && [ -n "${HOME:-}" ] && [ -f "$HOME/.bun/install/global/node_modules/@earendil-works/pi-ai/dist/auth/oauth/openai-codex.js" ]; then',
25
+ ' CAND="$HOME/.bun/install/global/node_modules/@earendil-works/pi-ai/dist/auth/oauth/openai-codex.js"',
26
+ 'fi',
27
+ 'printf "%s" "$CAND"',
28
+ ].join('\n');
29
+ const DRIVER_DEVICE = [
30
+ "import { pathToFileURL } from 'node:url'",
31
+ "const { openaiCodexOAuth } = await import(pathToFileURL(process.argv[1]).href)",
32
+ "const out = (o) => { try { process.stdout.write(JSON.stringify(o) + '\\n') } catch {} }",
33
+ "process.stdout.on('error', () => {})",
34
+ "const signal = AbortSignal.timeout(15 * 60 * 1000)",
35
+ "const interaction = {",
36
+ " signal,",
37
+ " notify: (n) => out({ type: 'notice', ...n }),",
38
+ " prompt: async (p) => (p.type === 'select' ? 'device_code' : ''),",
39
+ "}",
40
+ "try {",
41
+ " const credential = await openaiCodexOAuth.login(interaction)",
42
+ " out({ type: 'result', credential })",
43
+ "} catch (error) {",
44
+ " out({ type: 'error', message: error instanceof Error ? error.message : String(error) })",
45
+ "}",
46
+ ].join('\n');
47
+ const DRIVER_REFRESH = [
48
+ "import { readFileSync } from 'node:fs'",
49
+ "import { pathToFileURL } from 'node:url'",
50
+ "const { openaiCodexOAuth } = await import(pathToFileURL(process.argv[1]).href)",
51
+ "const out = (o) => { try { process.stdout.write(JSON.stringify(o) + '\\n') } catch {} }",
52
+ "process.stdout.on('error', () => {})",
53
+ "const credential = JSON.parse(readFileSync(0, 'utf8') || '{}')",
54
+ "const signal = AbortSignal.timeout(90 * 1000)",
55
+ "try {",
56
+ " const refreshed = await openaiCodexOAuth.refresh(credential, signal)",
57
+ " out({ type: 'result', credential: refreshed })",
58
+ "} catch (error) {",
59
+ " out({ type: 'error', message: error instanceof Error ? error.message : String(error) })",
60
+ "}",
61
+ ].join('\n');
62
+ /** Format an unknown error for logs and user-facing messages. */
63
+ function errorMessage(error) {
64
+ const message = error?.message;
65
+ return message ? String(message) : String(error);
66
+ }
67
+ //#endregion
68
+ /** Register source-mode remote methods without decorator syntax. */
69
+ const REMOTE_METHODS = ['status', 'authorize', 'poll', 'cancel', 'logout'];
70
+ function decorateRemoteMethods(klass, methods) {
71
+ const initializers = [];
72
+ for (const name of methods) {
73
+ const context = {
74
+ kind: 'method',
75
+ name,
76
+ static: false,
77
+ private: false,
78
+ addInitializer(initializer) {
79
+ initializers.push(initializer);
80
+ },
81
+ };
82
+ Remote(undefined, context);
83
+ }
84
+ const probe = Object.create(klass.prototype);
85
+ for (const initializer of initializers)
86
+ initializer.call(probe);
87
+ }
88
+ class OpenAISubscriptionController extends TypertRemoteService {
89
+ registeredAuthorization = null;
90
+ cachedModule = null;
91
+ locatingModule = null;
92
+ pendingBridge = null;
93
+ constructor(ctx) {
94
+ super(ctx, 'openaiSubscription', { namespace: 'openaiSubscription' });
95
+ // Resolve optional services per operation so late mounts and reloads work.
96
+ }
97
+ credentials() {
98
+ return this.ctx.get('credentials');
99
+ }
100
+ shell() {
101
+ return this.ctx.get('shell');
102
+ }
103
+ timer() {
104
+ return this.ctx.get('timer');
105
+ }
106
+ authorization() {
107
+ return this.ctx.get('authorization');
108
+ }
109
+ ensureAuthorizationFlow() {
110
+ const authorization = this.authorization();
111
+ if (authorization === undefined) {
112
+ this.registeredAuthorization = null;
113
+ return;
114
+ }
115
+ if (this.registeredAuthorization === authorization)
116
+ return;
117
+ this.registerAuthorizationFlow(authorization);
118
+ }
119
+ async locateAuthModule() {
120
+ if (this.cachedModule !== null)
121
+ return this.cachedModule;
122
+ if (this.locatingModule !== null)
123
+ return this.locatingModule;
124
+ const pending = (async () => {
125
+ const shell = this.shell();
126
+ if (shell === undefined)
127
+ return '';
128
+ try {
129
+ const spec = shell.resolve({ command: LOCATE_SCRIPT, timeoutMs: 20000, stdoutMaxBytes: 4096 });
130
+ const result = await shell.run(spec);
131
+ if (result.exitCode !== 0)
132
+ return '';
133
+ return (result.stdout.text || '').trim();
134
+ }
135
+ catch (error) {
136
+ console.error('[openai-subscription] locate OpenAI auth module failed: ' + errorMessage(error));
137
+ return '';
138
+ }
139
+ })();
140
+ this.locatingModule = pending;
141
+ try {
142
+ const path = await pending;
143
+ // Retry failed lookups because the dependency may mount later.
144
+ if (path)
145
+ this.cachedModule = path;
146
+ return path;
147
+ }
148
+ finally {
149
+ if (this.locatingModule === pending)
150
+ this.locatingModule = null;
151
+ }
152
+ }
153
+ async runDevice(control, notify) {
154
+ const credentials = this.credentials();
155
+ if (credentials === undefined) {
156
+ notify({ message: 'DSH 凭证服务不可用,请重启后重试。' });
157
+ throw new Error('DSH credential service unavailable');
158
+ }
159
+ const modulePath = await this.locateAuthModule();
160
+ const shell = this.shell();
161
+ if (!modulePath || shell === undefined) {
162
+ notify({ message: '当前 DSH 环境缺少 OpenAI 登录组件,请更新 DSH 后重试。' });
163
+ throw new Error('OpenAI login component unavailable');
164
+ }
165
+ notify({ message: '正在向 OpenAI 请求设备登录码…' });
166
+ const spec = shell.resolve({
167
+ command: 'node --input-type=module --eval ' + this.shq(DRIVER_DEVICE) + ' ' + this.shq(modulePath),
168
+ timeoutMs: 16 * 60 * 1000,
169
+ stdoutMaxBytes: 262144,
170
+ signal: control.signal,
171
+ });
172
+ const proc = shell.start(spec);
173
+ let buffer = '';
174
+ let credential = null;
175
+ let failure = null;
176
+ const deadline = Date.now() + 15 * 60 * 1000;
177
+ while (credential === null && failure === null) {
178
+ if (control.aborted()) {
179
+ try {
180
+ proc.kill();
181
+ }
182
+ catch { }
183
+ await proc.done;
184
+ return null;
185
+ }
186
+ let read;
187
+ try {
188
+ read = proc.readOutput();
189
+ }
190
+ catch {
191
+ read = { delta: '' };
192
+ }
193
+ buffer += read.delta || '';
194
+ let nl;
195
+ while ((nl = buffer.indexOf('\n')) >= 0) {
196
+ const line = buffer.slice(0, nl).trim();
197
+ buffer = buffer.slice(nl + 1);
198
+ if (!line)
199
+ continue;
200
+ let msg = null;
201
+ try {
202
+ msg = JSON.parse(line);
203
+ }
204
+ catch {
205
+ continue;
206
+ }
207
+ if (typeof msg.userCode === 'string' && msg.userCode) {
208
+ notify({
209
+ message: '请打开链接,使用有 Codex 权限的 ChatGPT 账号登录并输入设备码。',
210
+ url: typeof msg.verificationUri === 'string' ? msg.verificationUri : 'https://auth.openai.com/codex/device',
211
+ code: msg.userCode,
212
+ });
213
+ }
214
+ else if (msg.type === 'result') {
215
+ credential = normalizeOAuthCredential(msg.credential);
216
+ if (credential === null)
217
+ failure = '登录模块返回了无效的授权凭证';
218
+ }
219
+ else if (msg.type === 'error') {
220
+ failure = typeof msg.message === 'string' ? msg.message : '登录流程异常结束';
221
+ }
222
+ }
223
+ if (credential === null && failure === null) {
224
+ const timer = this.timer();
225
+ if (Date.now() > deadline) {
226
+ failure = '登录超时(15 分钟)';
227
+ break;
228
+ }
229
+ if (proc.status !== 'running') {
230
+ failure = '登录进程意外退出';
231
+ break;
232
+ }
233
+ if (timer === undefined) {
234
+ failure = 'DSH 计时服务不可用';
235
+ break;
236
+ }
237
+ try {
238
+ await timer.timeout(1000);
239
+ }
240
+ catch {
241
+ failure = '登录轮询已停止';
242
+ break;
243
+ }
244
+ }
245
+ }
246
+ if (failure !== null) {
247
+ try {
248
+ proc.kill();
249
+ }
250
+ catch { }
251
+ await proc.done;
252
+ const knownFailure = failure === '登录超时(15 分钟)'
253
+ || failure === '登录进程意外退出'
254
+ || failure === 'DSH 计时服务不可用'
255
+ || failure === '登录轮询已停止'
256
+ || failure === '登录模块返回了无效的授权凭证';
257
+ const summary = knownFailure ? failure : 'OpenAI 登录请求未完成';
258
+ const hint = /404|not enabled/i.test(failure)
259
+ ? ' 请在 ChatGPT 安全设置中启用设备码授权后重试。'
260
+ : ' 请重试。';
261
+ notify({ message: summary + '。' + hint });
262
+ throw new Error('OpenAI authorization failed');
263
+ }
264
+ if (credential === null)
265
+ throw new Error('OpenAI authorization ended without credentials');
266
+ if (proc.status === 'running')
267
+ proc.kill();
268
+ await proc.done;
269
+ if (control.aborted())
270
+ return null;
271
+ const granted = credential;
272
+ await credentials.modifyRecord(KEY, async (current) => {
273
+ if (control.aborted())
274
+ return undefined;
275
+ const currentPayload = current?.kind === 'grant' && current.payload && typeof current.payload === 'object'
276
+ ? current.payload
277
+ : {};
278
+ return {
279
+ kind: 'grant',
280
+ payload: {
281
+ provider: 'openai',
282
+ loginMethod: 'device_code',
283
+ accountId: granted.accountId ?? null,
284
+ access: granted.access,
285
+ refresh: granted.refresh ?? '',
286
+ expires: granted.expires ?? null,
287
+ obtainedAt: Date.now(),
288
+ managedPiRoute: currentPayload.managedPiRoute === true,
289
+ },
290
+ };
291
+ });
292
+ if (control.aborted())
293
+ return null;
294
+ if (!(await this.mirrorToPiAi(granted, undefined, control.signal))) {
295
+ if (control.aborted())
296
+ return null;
297
+ notify({ message: '授权已完成,但模型凭证保存失败,请重试。' });
298
+ throw new Error('Provider credential write failed');
299
+ }
300
+ if (control.aborted())
301
+ return null;
302
+ if (await this.ensurePiRoute())
303
+ await this.markPiRouteManaged();
304
+ return granted;
305
+ }
306
+ async runRefresh(control, notify) {
307
+ const credentials = this.credentials();
308
+ if (credentials === undefined) {
309
+ notify({ message: 'DSH 凭证服务不可用,请重启后重试。' });
310
+ throw new Error('DSH credential service unavailable');
311
+ }
312
+ const current = await credentials.readRecord(KEY);
313
+ const adapterRecord = await credentials.readRecord(PI_AI_RECORD);
314
+ if ((current === undefined || current.kind !== 'grant') && (adapterRecord === undefined || adapterRecord.kind !== 'grant')) {
315
+ notify({ message: '尚未登录 ChatGPT 账号,请先完成设备授权。' });
316
+ throw new Error('ChatGPT authorization not found');
317
+ }
318
+ const payload = current?.kind === 'grant' && current.payload && typeof current.payload === 'object'
319
+ ? current.payload
320
+ : {};
321
+ const adapterPayload = adapterRecord?.kind === 'grant' && adapterRecord.payload && typeof adapterRecord.payload === 'object'
322
+ ? adapterRecord.payload
323
+ : {};
324
+ // Prefer the provider's token because it may rotate during model requests.
325
+ const secretPayload = typeof adapterPayload.refresh === 'string' && adapterPayload.refresh ? adapterPayload : payload;
326
+ if (typeof secretPayload.refresh !== 'string' || !secretPayload.refresh) {
327
+ notify({ message: '当前授权无法刷新,请退出后重新登录。' });
328
+ throw new Error('ChatGPT authorization cannot be refreshed');
329
+ }
330
+ const expectedMainRefresh = typeof payload.refresh === 'string' ? payload.refresh : null;
331
+ const expectedAdapterRefresh = secretPayload.refresh;
332
+ const adapterRecordRequired = adapterRecord?.kind === 'grant';
333
+ const modulePath = await this.locateAuthModule();
334
+ const shell = this.shell();
335
+ if (!modulePath || shell === undefined) {
336
+ notify({ message: '当前 DSH 环境缺少 OpenAI 登录组件,请更新 DSH 后重试。' });
337
+ throw new Error('OpenAI login component unavailable');
338
+ }
339
+ const previous = {};
340
+ if (typeof secretPayload.access === 'string' && secretPayload.access)
341
+ previous.access = secretPayload.access;
342
+ if (typeof secretPayload.refresh === 'string' && secretPayload.refresh)
343
+ previous.refresh = secretPayload.refresh;
344
+ if (typeof secretPayload.expires === 'number' && Number.isFinite(secretPayload.expires) && secretPayload.expires > 0)
345
+ previous.expires = secretPayload.expires;
346
+ if (typeof secretPayload.accountId === 'string' && secretPayload.accountId)
347
+ previous.accountId = secretPayload.accountId;
348
+ notify({ message: '正在刷新 ChatGPT 订阅授权…' });
349
+ const spec = shell.resolve({
350
+ command: 'node --input-type=module --eval ' + this.shq(DRIVER_REFRESH) + ' ' + this.shq(modulePath),
351
+ timeoutMs: 120000,
352
+ stdoutMaxBytes: 65536,
353
+ signal: control.signal,
354
+ // Keep credentials out of the child environment and process listing.
355
+ stdin: JSON.stringify(previous),
356
+ });
357
+ const result = await shell.run(spec);
358
+ if (result.aborted || control.aborted())
359
+ return null;
360
+ let msg = null;
361
+ if (result.exitCode === 0) {
362
+ const lines = (result.stdout.text || '').split('\n');
363
+ for (let i = lines.length - 1; i >= 0; i--) {
364
+ const line = (lines[i] ?? '').trim();
365
+ if (!line)
366
+ continue;
367
+ try {
368
+ msg = JSON.parse(line);
369
+ }
370
+ catch {
371
+ continue;
372
+ }
373
+ break;
374
+ }
375
+ }
376
+ if (!msg || msg.type !== 'result' || !msg.credential || typeof msg.credential !== 'object') {
377
+ notify({ message: '刷新失败,请检查网络后重试;若问题持续,请退出后重新登录。' });
378
+ throw new Error('OpenAI authorization refresh failed');
379
+ }
380
+ const next = normalizeOAuthCredential(msg.credential, previous);
381
+ if (next === null) {
382
+ notify({ message: '刷新失败:登录服务返回了无效响应。' });
383
+ throw new Error('Invalid authorization response');
384
+ }
385
+ if (!(await this.mirrorToPiAi(next, { refresh: expectedAdapterRefresh, requireExisting: adapterRecordRequired }, control.signal))) {
386
+ notify({ message: '刷新期间授权记录已变化,已保留较新的凭证。' });
387
+ throw new Error('Provider authorization changed during refresh');
388
+ }
389
+ await credentials.modifyRecord(KEY, async (latest) => {
390
+ if (control.aborted())
391
+ return undefined;
392
+ if (latest !== undefined && latest.kind !== 'grant')
393
+ throw new Error('Plugin authorization changed during refresh');
394
+ const latestPayload = latest?.kind === 'grant' && latest.payload && typeof latest.payload === 'object'
395
+ ? latest.payload
396
+ : {};
397
+ const latestRefresh = typeof latestPayload.refresh === 'string' ? latestPayload.refresh : null;
398
+ if (latestRefresh !== expectedMainRefresh)
399
+ throw new Error('Plugin authorization changed during refresh');
400
+ return {
401
+ kind: 'grant',
402
+ payload: {
403
+ provider: 'openai',
404
+ loginMethod: typeof latestPayload.loginMethod === 'string' ? latestPayload.loginMethod : 'refresh',
405
+ accountId: next.accountId ?? null,
406
+ access: next.access,
407
+ refresh: next.refresh ?? '',
408
+ expires: next.expires ?? null,
409
+ obtainedAt: typeof latestPayload.obtainedAt === 'number' ? latestPayload.obtainedAt : null,
410
+ refreshedAt: Date.now(),
411
+ managedPiRoute: latestPayload.managedPiRoute === true,
412
+ },
413
+ };
414
+ });
415
+ if (control.aborted())
416
+ return null;
417
+ if (await this.ensurePiRoute())
418
+ await this.markPiRouteManaged();
419
+ return next;
420
+ }
421
+ /** Store the credential used by the `openai-codex` provider. */
422
+ async mirrorToPiAi(credential, expected, signal) {
423
+ const credentials = this.credentials();
424
+ if (credentials === undefined)
425
+ return false;
426
+ const payload = { type: 'oauth', access: credential.access };
427
+ if (credential.refresh !== undefined)
428
+ payload.refresh = credential.refresh;
429
+ if (credential.expires !== undefined)
430
+ payload.expires = credential.expires;
431
+ if (credential.accountId !== undefined)
432
+ payload.accountId = credential.accountId;
433
+ try {
434
+ await credentials.modifyRecord(PI_AI_RECORD, async (current) => {
435
+ if (signal?.aborted)
436
+ return undefined;
437
+ if (expected !== undefined) {
438
+ if (expected.requireExisting && (current === undefined || current.kind !== 'grant')) {
439
+ throw new Error('Provider authorization was removed during refresh');
440
+ }
441
+ if (current !== undefined && current.kind === 'grant') {
442
+ const currentPayload = current.payload && typeof current.payload === 'object' ? current.payload : {};
443
+ if (currentPayload.refresh !== expected.refresh)
444
+ throw new Error('Provider authorization changed during refresh');
445
+ }
446
+ }
447
+ return { kind: 'grant', payload };
448
+ });
449
+ return signal?.aborted !== true;
450
+ }
451
+ catch (error) {
452
+ console.error('[openai-subscription] mirror to llm-pi-ai/openai-codex failed: ' + errorMessage(error));
453
+ return false;
454
+ }
455
+ }
456
+ /** Enable the default provider without replacing user configuration. */
457
+ async ensurePiRoute() {
458
+ const settings = this.ctx.get('settings');
459
+ if (settings === undefined || typeof settings.mutate !== 'function')
460
+ return false;
461
+ try {
462
+ const descriptor = settings.describe({ redactSecrets: true }).find((entry) => entry.ns === 'llm-pi-ai');
463
+ if (descriptor === undefined)
464
+ return false;
465
+ const section = (descriptor.value && typeof descriptor.value === 'object') ? descriptor.value : {};
466
+ const providers = (section.providers && typeof section.providers === 'object') ? section.providers : {};
467
+ if (providers['openai-codex'] !== undefined)
468
+ return false;
469
+ await settings.mutate('llm-pi-ai', [{ op: 'set', path: ['providers', 'openai-codex'], value: {} }], descriptor.revision);
470
+ return true;
471
+ }
472
+ catch (error) {
473
+ console.error('[openai-subscription] enable openai-codex route failed: ' + errorMessage(error));
474
+ return false;
475
+ }
476
+ }
477
+ /** Mark the default provider as plugin-managed. */
478
+ async markPiRouteManaged() {
479
+ const credentials = this.credentials();
480
+ if (credentials === undefined)
481
+ return;
482
+ try {
483
+ await credentials.modifyRecord(KEY, async (current) => {
484
+ if (current === undefined || current.kind !== 'grant')
485
+ return undefined;
486
+ const payload = (current.payload && typeof current.payload === 'object') ? current.payload : {};
487
+ return { ...current, payload: { ...payload, managedPiRoute: true } };
488
+ });
489
+ }
490
+ catch (error) {
491
+ console.error('[openai-subscription] remember managed openai-codex route failed: ' + errorMessage(error));
492
+ }
493
+ }
494
+ /** Remove only the unchanged default provider created by this plugin. */
495
+ async removePiRouteIfBare() {
496
+ const settings = this.ctx.get('settings');
497
+ if (settings === undefined || typeof settings.mutate !== 'function')
498
+ return;
499
+ try {
500
+ const descriptor = settings.describe({ redactSecrets: true }).find((entry) => entry.ns === 'llm-pi-ai');
501
+ if (descriptor === undefined)
502
+ return;
503
+ const user = (descriptor.user && typeof descriptor.user === 'object') ? descriptor.user : {};
504
+ const providers = (user.providers && typeof user.providers === 'object') ? user.providers : {};
505
+ const entry = providers['openai-codex'];
506
+ if (entry === undefined)
507
+ return;
508
+ if (!(typeof entry === 'object' && entry !== null && Object.keys(entry).length === 0))
509
+ return;
510
+ await settings.mutate('llm-pi-ai', [{ op: 'unset', path: ['providers', 'openai-codex'] }], descriptor.revision);
511
+ }
512
+ catch (error) {
513
+ console.error('[openai-subscription] disable openai-codex route failed: ' + errorMessage(error));
514
+ }
515
+ }
516
+ beginLogin(method) {
517
+ this.ensureAuthorizationFlow();
518
+ if (this.pendingBridge !== null) {
519
+ if (this.pendingBridge.done)
520
+ this.pendingBridge = null;
521
+ else
522
+ return { started: false, error: '已有一个进行中的授权流程' };
523
+ }
524
+ const controller = new AbortController();
525
+ const state = { notices: [], done: false, outcome: null, error: null, controller, task: null };
526
+ this.pendingBridge = state;
527
+ const notify = (notice) => {
528
+ state.notices.push({ message: notice.message, url: notice.url, code: notice.code });
529
+ if (state.notices.length > 50)
530
+ state.notices.shift();
531
+ };
532
+ const control = { signal: controller.signal, aborted: () => controller.signal.aborted };
533
+ const task = (async () => {
534
+ try {
535
+ let credential;
536
+ if (method === 'refresh')
537
+ credential = await this.runRefresh(control, notify);
538
+ else if (method === 'device_code')
539
+ credential = await this.runDevice(control, notify);
540
+ else
541
+ throw new Error('未知的登录方式:' + method);
542
+ state.outcome = credential === null ? 'cancelled' : 'authorized';
543
+ }
544
+ catch {
545
+ state.outcome = controller.signal.aborted ? 'cancelled' : 'failed';
546
+ state.error = controller.signal.aborted ? null : '授权失败,请根据提示重试。';
547
+ }
548
+ finally {
549
+ state.done = true;
550
+ const timer = this.timer();
551
+ if (timer !== undefined) {
552
+ void timer.timeout(30000).then(() => {
553
+ if (this.pendingBridge === state)
554
+ this.pendingBridge = null;
555
+ }).catch(() => {
556
+ // Fiber disposal cancels timer promises; the flow is already done.
557
+ });
558
+ }
559
+ }
560
+ })();
561
+ state.task = task;
562
+ void task.catch((error) => {
563
+ console.error('[openai-subscription] authorization task failed: ' + errorMessage(error));
564
+ });
565
+ return { started: true };
566
+ }
567
+ registerAuthorizationFlow(authorization) {
568
+ if (this.registeredAuthorization === authorization)
569
+ return;
570
+ try {
571
+ authorization.registerFlow({
572
+ key: KEY,
573
+ label: 'ChatGPT 订阅账号',
574
+ methods: [
575
+ { id: 'device_code', label: '使用设备码登录' },
576
+ { id: 'refresh', label: '刷新授权' },
577
+ ],
578
+ run: async (session) => {
579
+ const notify = (notice) => session.notify(notice);
580
+ const control = { signal: session.signal, aborted: () => session.signal.aborted };
581
+ if (session.method === 'refresh') {
582
+ await this.runRefresh(control, notify);
583
+ return;
584
+ }
585
+ if (session.method === 'device_code') {
586
+ await this.runDevice(control, notify);
587
+ return;
588
+ }
589
+ throw new Error('未知的登录方式:' + session.method);
590
+ },
591
+ });
592
+ this.registeredAuthorization = authorization;
593
+ }
594
+ catch (error) {
595
+ console.error('[openai-subscription] registerFlow failed: ' + errorMessage(error));
596
+ }
597
+ }
598
+ shq(value) {
599
+ return "'" + String(value).replace(/'/g, "'\\''") + "'";
600
+ }
601
+ async status() {
602
+ this.ensureAuthorizationFlow();
603
+ const modulePath = await this.locateAuthModule();
604
+ const credentials = this.credentials();
605
+ if (credentials === undefined)
606
+ return { configured: false, ready: !!modulePath };
607
+ const [record, adapterRecord] = await Promise.all([
608
+ credentials.readRecord(KEY),
609
+ credentials.readRecord(PI_AI_RECORD),
610
+ ]);
611
+ const p = record?.kind === 'grant' && record.payload && typeof record.payload === 'object'
612
+ ? record.payload
613
+ : {};
614
+ const adapter = adapterRecord?.kind === 'grant' && adapterRecord.payload && typeof adapterRecord.payload === 'object'
615
+ ? adapterRecord.payload
616
+ : {};
617
+ if (typeof adapter.access !== 'string' || !adapter.access)
618
+ return { configured: false, ready: !!modulePath };
619
+ return {
620
+ configured: true,
621
+ ready: !!modulePath,
622
+ accountId: typeof adapter.accountId === 'string' ? adapter.accountId : (typeof p.accountId === 'string' ? p.accountId : null),
623
+ expires: typeof adapter.expires === 'number' && Number.isFinite(adapter.expires) ? adapter.expires : null,
624
+ loginMethod: typeof p.loginMethod === 'string' ? p.loginMethod : null,
625
+ obtainedAt: typeof p.obtainedAt === 'number' ? p.obtainedAt : null,
626
+ refreshedAt: typeof p.refreshedAt === 'number' ? p.refreshedAt : null,
627
+ hasRefresh: typeof adapter.refresh === 'string' && adapter.refresh.length > 0,
628
+ };
629
+ }
630
+ async authorize(method) {
631
+ return this.beginLogin(typeof method === 'string' ? method : 'device_code');
632
+ }
633
+ async poll() {
634
+ if (this.pendingBridge === null)
635
+ return { status: 'idle', notices: [] };
636
+ const bridge = this.pendingBridge;
637
+ const notices = bridge.notices.splice(0);
638
+ if (bridge.done)
639
+ return { status: 'done', notices, outcome: bridge.outcome, error: bridge.error };
640
+ return { status: 'pending', notices };
641
+ }
642
+ async cancel() {
643
+ if (this.pendingBridge !== null)
644
+ this.pendingBridge.controller.abort();
645
+ const authorization = this.authorization();
646
+ if (authorization !== undefined) {
647
+ try {
648
+ authorization.cancel(KEY);
649
+ }
650
+ catch { }
651
+ }
652
+ return { ok: true };
653
+ }
654
+ /** Cancel active authorization and remove plugin-managed credentials and settings. */
655
+ async logout() {
656
+ const pending = this.pendingBridge;
657
+ if (pending !== null) {
658
+ pending.controller.abort();
659
+ if (pending.task !== null)
660
+ await pending.task.catch(() => { });
661
+ if (this.pendingBridge === pending)
662
+ this.pendingBridge = null;
663
+ }
664
+ const authorization = this.authorization();
665
+ if (authorization !== undefined) {
666
+ try {
667
+ authorization.cancel(KEY);
668
+ }
669
+ catch { }
670
+ }
671
+ const credentials = this.credentials();
672
+ if (credentials === undefined)
673
+ throw new Error('DSH credential service unavailable');
674
+ const record = await credentials.readRecord(KEY);
675
+ const payload = record?.kind === 'grant' && record.payload && typeof record.payload === 'object'
676
+ ? record.payload
677
+ : {};
678
+ const managedPiRoute = payload.managedPiRoute === true;
679
+ // Delete the provider credential before reporting a successful logout.
680
+ await credentials.deleteRecord(PI_AI_RECORD);
681
+ await credentials.deleteRecord(KEY);
682
+ if (managedPiRoute)
683
+ await this.removePiRouteIfBare();
684
+ return { ok: true };
685
+ }
686
+ }
687
+ decorateRemoteMethods(OpenAISubscriptionController, REMOTE_METHODS);
688
+ export default OpenAISubscriptionController;