dsh-openai-subscription 0.1.2 → 0.1.4
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 +36 -12
- package/README.zh.md +36 -12
- package/dist/client.d.ts +8 -0
- package/dist/client.js +507 -170
- package/dist/driver.d.ts +12 -0
- package/dist/driver.js +136 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +58 -0
- package/dist/host.d.ts +32 -14
- package/dist/host.js +242 -312
- package/dist/models.js +91 -55
- package/dist/oauth.js +2 -2
- package/dist/platform.d.ts +17 -0
- package/dist/platform.js +279 -0
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -53,13 +53,58 @@ const ZH = {
|
|
|
53
53
|
'error.poll': '授权状态暂时中断,正在自动重试。',
|
|
54
54
|
'error.start': '无法启动授权,请稍后重试。',
|
|
55
55
|
'error.cancel': '无法取消授权,请重试。',
|
|
56
|
-
'error.sync': '
|
|
56
|
+
'error.sync': '模型同步未完成,请重新读取状态并检查模型设置后重试。',
|
|
57
57
|
'error.disconnect': '无法断开连接,请重试。',
|
|
58
58
|
'error.clipboard': '无法复制,请手动选择代码。',
|
|
59
59
|
'dialog.sync.title': '合并并同步模型?',
|
|
60
60
|
'dialog.sync.detail': '账号可见模型和 DSH 内置模型将合并到现有列表;你的新增模型、字段编辑和删除选择会被保留。',
|
|
61
61
|
'dialog.disconnect.title': '断开 ChatGPT 连接?',
|
|
62
62
|
'dialog.disconnect.detail': '这只会删除本机授权和插件管理的未修改模型项,不会注销或删除你的 ChatGPT 账号。',
|
|
63
|
+
'status.expired': '授权已过期',
|
|
64
|
+
'status.expired.detail': '已保存的授权已过期,请刷新授权或重新连接后再使用。',
|
|
65
|
+
'status.unknown': '已保存连接',
|
|
66
|
+
'status.unknown.detail': '尚无法确认授权是否有效。若请求失败,请刷新授权或重新连接。',
|
|
67
|
+
'status.failed': '无法读取连接状态',
|
|
68
|
+
'status.reloading': '正在重新读取状态…',
|
|
69
|
+
'status.stale': '显示的是上次读取的状态。',
|
|
70
|
+
'action.reload': '重新读取状态',
|
|
71
|
+
'action.retryPoll': '重试检查授权',
|
|
72
|
+
'action.select': '选择代码',
|
|
73
|
+
'device.manual': '代码已选中,请按 Ctrl+C 或 ⌘C 复制;也可长按代码复制。',
|
|
74
|
+
'device.code': '一次性设备代码',
|
|
75
|
+
'device.newTab': '在新标签页打开',
|
|
76
|
+
'error.unsafe-url': '验证地址不符合安全要求,已阻止打开。请取消后重试或更新 DSH。',
|
|
77
|
+
'error.poll-paused': '已暂停自动检查,设备授权可能仍在继续。请重试检查或取消授权。',
|
|
78
|
+
'error.poll-idle': '未找到本次授权记录,无法确认是否完成。请重新读取状态或取消后重新连接。',
|
|
79
|
+
'error.poll-unconfirmed': '无法确认保存的授权结果是否属于本次尝试。请重新读取状态,或取消后重新连接。',
|
|
80
|
+
'error.long-action': '请求超时,但本机操作可能仍在进行。请先重新读取状态,确认后再重试。',
|
|
81
|
+
'error.credentials-unavailable': '凭据服务不可用,请重启或更新 DSH。',
|
|
82
|
+
'error.shell-unavailable': '本机进程服务不可用,请重启或更新 DSH。',
|
|
83
|
+
'error.timer-unavailable': '计时服务不可用,请重启或更新 DSH。',
|
|
84
|
+
'error.component-unavailable': '登录组件不可用,请更新或重启 DSH。',
|
|
85
|
+
'error.runtime-unsupported': '当前运行环境不支持此登录方式,请更新 DSH。',
|
|
86
|
+
'error.busy': '另一项操作正在进行,请稍后重新读取状态。',
|
|
87
|
+
'error.invalid-method': '登录方式不受支持,请更新 DSH 后重新连接。',
|
|
88
|
+
'error.not-connected': '未找到本机授权,请连接 ChatGPT。',
|
|
89
|
+
'error.not-refreshable': '授权无法刷新,请重新连接 ChatGPT。',
|
|
90
|
+
'error.device-auth-disabled': '账号未启用设备授权,请在 ChatGPT 安全设置中启用后重试。',
|
|
91
|
+
'error.access-denied': '授权被拒绝,请检查账号权限后重试。',
|
|
92
|
+
'error.authorization-expired': '设备授权已过期,请重新连接以获取新代码。',
|
|
93
|
+
'error.rate-limited': '请求过于频繁,请稍后重试。',
|
|
94
|
+
'error.network': '无法连接服务,请检查网络后重试。',
|
|
95
|
+
'error.invalid-response': '服务返回了无效响应,请重试或更新 DSH。',
|
|
96
|
+
'error.process-exited': '登录进程提前退出,请重新连接或更新 DSH。',
|
|
97
|
+
'error.credential-write-failed': '无法保存本机授权,请检查本机权限后重试;也可断开以清理残留数据。',
|
|
98
|
+
'error.credential-changed': '本机授权已被其他操作更改,请重新读取状态后重试。',
|
|
99
|
+
'error.settings-unavailable': '设置服务不可用,请重启 DSH 后重试。',
|
|
100
|
+
'error.models-unavailable': '暂时无法获取模型目录,请稍后重试同步。',
|
|
101
|
+
'error.models-empty': '未获取到可用模型,请检查账号权限后重试。',
|
|
102
|
+
'error.models-confirmation-required': '现有模型列表需要确认合并,请确认后再同步。',
|
|
103
|
+
'error.settings-conflict': '模型设置已被其他操作更改,请重新读取状态后重试。',
|
|
104
|
+
'error.settings-write-failed': '无法保存模型设置,请检查本机权限后重试。',
|
|
105
|
+
'error.ownership-save-failed': '模型可能已更新,但清理记录未能保存。请重试同步并检查模型设置。',
|
|
106
|
+
'error.cancelled': '操作已取消。',
|
|
107
|
+
'error.unknown': '操作未完成,请重新读取状态后重试。',
|
|
63
108
|
};
|
|
64
109
|
const EN = {
|
|
65
110
|
nav: 'ChatGPT Subscription',
|
|
@@ -111,13 +156,58 @@ const EN = {
|
|
|
111
156
|
'error.poll': 'Authorization status was interrupted. Retrying automatically.',
|
|
112
157
|
'error.start': 'Could not start authorization. Try again shortly.',
|
|
113
158
|
'error.cancel': 'Could not cancel authorization. Try again.',
|
|
114
|
-
'error.sync': 'Model sync
|
|
159
|
+
'error.sync': 'Model sync did not complete. Reload status and review model settings before retrying.',
|
|
115
160
|
'error.disconnect': 'Could not disconnect. Try again.',
|
|
116
161
|
'error.clipboard': 'Could not copy. Select the code manually.',
|
|
117
162
|
'dialog.sync.title': 'Merge and sync models?',
|
|
118
163
|
'dialog.sync.detail': 'Account-visible and DSH built-in models will be merged into the current list. Local additions, field edits, and deletion choices are preserved.',
|
|
119
164
|
'dialog.disconnect.title': 'Disconnect ChatGPT?',
|
|
120
165
|
'dialog.disconnect.detail': 'This only removes local authorization and unchanged plugin-managed model entries. It does not sign out of or delete your ChatGPT account.',
|
|
166
|
+
'status.expired': 'Authorization expired',
|
|
167
|
+
'status.expired.detail': 'Saved authorization has expired. Refresh or reconnect before using this subscription.',
|
|
168
|
+
'status.unknown': 'Connection saved',
|
|
169
|
+
'status.unknown.detail': 'Authorization validity is not confirmed. Refresh or reconnect if requests fail.',
|
|
170
|
+
'status.failed': 'Could not read connection status',
|
|
171
|
+
'status.reloading': 'Reading status again…',
|
|
172
|
+
'status.stale': 'Showing the last known status.',
|
|
173
|
+
'action.reload': 'Reload status',
|
|
174
|
+
'action.retryPoll': 'Retry authorization check',
|
|
175
|
+
'action.select': 'Select code',
|
|
176
|
+
'device.manual': 'Code selected. Press Ctrl+C or ⌘C to copy, or touch and hold the code.',
|
|
177
|
+
'device.code': 'One-time device code',
|
|
178
|
+
'device.newTab': 'Opens in a new tab',
|
|
179
|
+
'error.unsafe-url': 'The verification address did not pass safety checks and was blocked. Cancel and retry, or update DSH.',
|
|
180
|
+
'error.poll-paused': 'Automatic checks paused. Device authorization may still be running. Retry the check or cancel authorization.',
|
|
181
|
+
'error.poll-idle': 'No record of this authorization was found, so completion cannot be confirmed. Reload status, or cancel and reconnect.',
|
|
182
|
+
'error.poll-unconfirmed': 'The saved authorization result could not be linked to this attempt. Reload status, or cancel and reconnect.',
|
|
183
|
+
'error.long-action': 'The request timed out, but the local operation may still be running. Reload status before deciding whether to retry.',
|
|
184
|
+
'error.credentials-unavailable': 'The credentials service is unavailable. Restart or update DSH.',
|
|
185
|
+
'error.shell-unavailable': 'The local process service is unavailable. Restart or update DSH.',
|
|
186
|
+
'error.timer-unavailable': 'The timer service is unavailable. Restart or update DSH.',
|
|
187
|
+
'error.component-unavailable': 'The sign-in component is unavailable. Update or restart DSH.',
|
|
188
|
+
'error.runtime-unsupported': 'This runtime does not support sign-in. Update DSH.',
|
|
189
|
+
'error.busy': 'Another operation is running. Wait, then reload status.',
|
|
190
|
+
'error.invalid-method': 'This sign-in method is unsupported. Update DSH and reconnect.',
|
|
191
|
+
'error.not-connected': 'No local authorization was found. Connect ChatGPT.',
|
|
192
|
+
'error.not-refreshable': 'Authorization cannot be refreshed. Reconnect ChatGPT.',
|
|
193
|
+
'error.device-auth-disabled': 'Device authorization is disabled for this account. Enable it in ChatGPT security settings and retry.',
|
|
194
|
+
'error.access-denied': 'Authorization was denied. Check account access and retry.',
|
|
195
|
+
'error.authorization-expired': 'Device authorization expired. Reconnect to get a new code.',
|
|
196
|
+
'error.rate-limited': 'Too many requests. Wait before retrying.',
|
|
197
|
+
'error.network': 'The service could not be reached. Check the network and retry.',
|
|
198
|
+
'error.invalid-response': 'The service returned an invalid response. Retry or update DSH.',
|
|
199
|
+
'error.process-exited': 'The sign-in process exited early. Reconnect or update DSH.',
|
|
200
|
+
'error.credential-write-failed': 'Local authorization could not be saved. Check local permissions and retry, or disconnect to clean up remaining data.',
|
|
201
|
+
'error.credential-changed': 'Another operation changed local authorization. Reload status and retry.',
|
|
202
|
+
'error.settings-unavailable': 'The settings service is unavailable. Restart DSH and retry.',
|
|
203
|
+
'error.models-unavailable': 'The model catalog is temporarily unavailable. Retry model sync later.',
|
|
204
|
+
'error.models-empty': 'No available models were returned. Check account access and retry.',
|
|
205
|
+
'error.models-confirmation-required': 'Merging the existing model list needs confirmation. Confirm before syncing.',
|
|
206
|
+
'error.settings-conflict': 'Another operation changed model settings. Reload status and retry.',
|
|
207
|
+
'error.settings-write-failed': 'Model settings could not be saved. Check local permissions and retry.',
|
|
208
|
+
'error.ownership-save-failed': 'Models may have updated, but cleanup records could not be saved. Retry sync and review model settings.',
|
|
209
|
+
'error.cancelled': 'The operation was cancelled.',
|
|
210
|
+
'error.unknown': 'The operation did not complete. Reload status and retry.',
|
|
121
211
|
};
|
|
122
212
|
function fallbackTranslate(key, params = {}) {
|
|
123
213
|
const language = typeof navigator === 'undefined' ? 'zh' : navigator.language.toLowerCase();
|
|
@@ -212,6 +302,8 @@ window.__ModuleLoader__.load({
|
|
|
212
302
|
font-weight: 500;
|
|
213
303
|
line-height: 20px;
|
|
214
304
|
cursor: pointer;
|
|
305
|
+
text-decoration: none;
|
|
306
|
+
text-align: center;
|
|
215
307
|
transition: background .16s ease, border-color .16s ease, opacity .16s ease, transform .16s ease;
|
|
216
308
|
}
|
|
217
309
|
.oasub-button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(15, 17, 21, .06)); }
|
|
@@ -242,6 +334,9 @@ window.__ModuleLoader__.load({
|
|
|
242
334
|
.oasub-device-head { display: flex; flex-direction: column; gap: 3px; }
|
|
243
335
|
.oasub-code {
|
|
244
336
|
align-self: flex-start;
|
|
337
|
+
width: min(100%, 24ch);
|
|
338
|
+
min-width: 0;
|
|
339
|
+
border: 1px solid var(--dsw-alias-border-l1, rgba(15, 17, 21, .2));
|
|
245
340
|
margin: 0;
|
|
246
341
|
padding: 10px 14px;
|
|
247
342
|
border-radius: 10px;
|
|
@@ -284,14 +379,45 @@ window.__ModuleLoader__.load({
|
|
|
284
379
|
}
|
|
285
380
|
tag.textContent = CSS;
|
|
286
381
|
}
|
|
382
|
+
// Never translate or render arbitrary host diagnostics. Only these machine codes
|
|
383
|
+
// (or an exact RPC message envelope containing one) cross into the UI.
|
|
384
|
+
const ERROR_CODES = new Set([
|
|
385
|
+
'credentials-unavailable', 'shell-unavailable', 'timer-unavailable', 'component-unavailable',
|
|
386
|
+
'runtime-unsupported', 'busy', 'invalid-method', 'not-connected', 'not-refreshable',
|
|
387
|
+
'device-auth-disabled', 'access-denied', 'authorization-expired', 'rate-limited', 'network',
|
|
388
|
+
'timeout', 'invalid-response', 'process-exited', 'credential-write-failed', 'credential-changed',
|
|
389
|
+
'settings-unavailable', 'models-unavailable', 'models-empty', 'models-confirmation-required',
|
|
390
|
+
'settings-conflict', 'settings-write-failed', 'ownership-save-failed', 'cancelled', 'unknown',
|
|
391
|
+
]);
|
|
392
|
+
function allowedCode(value) {
|
|
393
|
+
return typeof value === 'string' && ERROR_CODES.has(value) ? value : undefined;
|
|
394
|
+
}
|
|
395
|
+
function recordOf(value) {
|
|
396
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
397
|
+
? value
|
|
398
|
+
: null;
|
|
399
|
+
}
|
|
400
|
+
function failureCode(error) {
|
|
401
|
+
const raw = recordOf(error);
|
|
402
|
+
const code = allowedCode(raw?.code) ?? allowedCode(raw?.errorCode);
|
|
403
|
+
if (code !== undefined)
|
|
404
|
+
return code;
|
|
405
|
+
const match = typeof raw?.message === 'string'
|
|
406
|
+
? /^\[openai-subscription:([a-z-]+)\]$/.exec(raw.message)
|
|
407
|
+
: null;
|
|
408
|
+
return allowedCode(match?.[1]);
|
|
409
|
+
}
|
|
287
410
|
class ClientFailure extends Error {
|
|
288
411
|
code;
|
|
289
412
|
constructor(code) {
|
|
290
|
-
super(
|
|
291
|
-
this.code = code;
|
|
413
|
+
super('Request failed');
|
|
414
|
+
this.code = allowedCode(code) ?? 'unknown';
|
|
292
415
|
}
|
|
293
416
|
}
|
|
294
417
|
const RPC_TIMEOUT_MS = 15_000;
|
|
418
|
+
const MUTATION_TIMEOUT_MS = 75_000;
|
|
419
|
+
const MAX_POLL_FAILURES = 4;
|
|
420
|
+
const MAX_POLL_DURATION_MS = 15 * 60_000;
|
|
295
421
|
function remoteCall(connection, method, args, signal, timeoutMs = RPC_TIMEOUT_MS) {
|
|
296
422
|
return new Promise((resolve, reject) => {
|
|
297
423
|
const controller = new AbortController();
|
|
@@ -310,8 +436,8 @@ window.__ModuleLoader__.load({
|
|
|
310
436
|
callback();
|
|
311
437
|
};
|
|
312
438
|
const onAbort = () => {
|
|
313
|
-
controller.abort(signal?.reason);
|
|
314
439
|
finish(() => reject(new ClientFailure('cancelled')));
|
|
440
|
+
controller.abort();
|
|
315
441
|
};
|
|
316
442
|
if (signal?.aborted) {
|
|
317
443
|
onAbort();
|
|
@@ -319,64 +445,63 @@ window.__ModuleLoader__.load({
|
|
|
319
445
|
}
|
|
320
446
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
321
447
|
timeout = setTimeout(() => {
|
|
322
|
-
controller.abort();
|
|
323
448
|
finish(() => reject(new ClientFailure('timeout')));
|
|
449
|
+
controller.abort();
|
|
324
450
|
}, timeoutMs);
|
|
325
451
|
let request;
|
|
326
452
|
try {
|
|
327
453
|
request = connection.rpc.call('/api', 'openaiSubscription/' + method, { args }, controller.signal);
|
|
328
454
|
}
|
|
329
|
-
catch {
|
|
330
|
-
finish(() => reject(new ClientFailure('
|
|
455
|
+
catch (error) {
|
|
456
|
+
finish(() => reject(new ClientFailure(failureCode(error) ?? 'unknown')));
|
|
331
457
|
return;
|
|
332
458
|
}
|
|
333
|
-
request.then((result) => {
|
|
459
|
+
Promise.resolve(request).then((result) => {
|
|
334
460
|
finish(() => {
|
|
335
|
-
if (!result || result.ok !== true
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
461
|
+
if (!result || result.ok !== true) {
|
|
462
|
+
reject(new ClientFailure(failureCode(result?.error) ?? 'unknown'));
|
|
463
|
+
}
|
|
464
|
+
else if (result.value === undefined) {
|
|
465
|
+
reject(new ClientFailure('invalid-response'));
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
resolve(result.value);
|
|
339
469
|
}
|
|
340
|
-
resolve(result.value);
|
|
341
470
|
});
|
|
342
|
-
}).catch(() => finish(() => reject(new ClientFailure('
|
|
471
|
+
}).catch((error) => finish(() => reject(new ClientFailure(failureCode(error) ?? 'unknown'))));
|
|
343
472
|
});
|
|
344
473
|
}
|
|
345
474
|
function errorKey(error, fallback) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
function recordOf(value) {
|
|
349
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
350
|
-
? value
|
|
351
|
-
: null;
|
|
475
|
+
const code = failureCode(error);
|
|
476
|
+
return code === undefined || code === 'unknown' ? fallback : 'error.' + code;
|
|
352
477
|
}
|
|
353
478
|
function parseStatus(value) {
|
|
354
479
|
const raw = recordOf(value);
|
|
355
480
|
if (raw === null || typeof raw.configured !== 'boolean' || typeof raw.ready !== 'boolean')
|
|
356
|
-
throw new ClientFailure('
|
|
357
|
-
if (!raw.configured) {
|
|
358
|
-
return { configured: false, ready: raw.ready, refreshable: false, modelsSynced: false, modelCount: 0 };
|
|
359
|
-
}
|
|
481
|
+
throw new ClientFailure('invalid-response');
|
|
360
482
|
return {
|
|
361
|
-
configured:
|
|
483
|
+
configured: raw.configured,
|
|
362
484
|
ready: raw.ready,
|
|
363
|
-
refreshable: raw.refreshable === true,
|
|
364
|
-
modelsSynced: raw.modelsSynced === true,
|
|
365
|
-
modelCount: typeof raw.modelCount === 'number' && Number.isSafeInteger(raw.modelCount) && raw.modelCount >= 0
|
|
366
|
-
? raw.modelCount
|
|
367
|
-
|
|
485
|
+
refreshable: raw.configured && raw.refreshable === true,
|
|
486
|
+
modelsSynced: raw.configured && raw.modelsSynced === true,
|
|
487
|
+
modelCount: raw.configured && typeof raw.modelCount === 'number' && Number.isSafeInteger(raw.modelCount) && raw.modelCount >= 0
|
|
488
|
+
? raw.modelCount : 0,
|
|
489
|
+
unavailableReason: allowedCode(raw.unavailableReason),
|
|
490
|
+
credentialState: raw.credentialState === 'valid' || raw.credentialState === 'expired' ? raw.credentialState : 'unknown',
|
|
491
|
+
cleanupAvailable: raw.cleanupAvailable === true || raw.configured,
|
|
492
|
+
flowPending: raw.flowPending === true,
|
|
368
493
|
};
|
|
369
494
|
}
|
|
370
495
|
function parseAuthorize(value) {
|
|
371
496
|
const raw = recordOf(value);
|
|
372
497
|
if (raw === null || typeof raw.started !== 'boolean')
|
|
373
|
-
throw new ClientFailure('
|
|
374
|
-
return { started: raw.started };
|
|
498
|
+
throw new ClientFailure('invalid-response');
|
|
499
|
+
return { started: raw.started, errorCode: allowedCode(raw.errorCode) };
|
|
375
500
|
}
|
|
376
501
|
function parsePoll(value) {
|
|
377
502
|
const raw = recordOf(value);
|
|
378
503
|
if (raw === null || (raw.status !== 'idle' && raw.status !== 'pending' && raw.status !== 'done'))
|
|
379
|
-
throw new ClientFailure('
|
|
504
|
+
throw new ClientFailure('invalid-response');
|
|
380
505
|
const notices = [];
|
|
381
506
|
if (Array.isArray(raw.notices)) {
|
|
382
507
|
for (const candidate of raw.notices) {
|
|
@@ -386,10 +511,12 @@ window.__ModuleLoader__.load({
|
|
|
386
511
|
const kind = notice.kind;
|
|
387
512
|
notices.push({
|
|
388
513
|
kind: kind === 'requesting-code' || kind === 'enter-code' || kind === 'refreshing' || kind === 'models-synced' || kind === 'models-sync-failed'
|
|
389
|
-
? kind
|
|
390
|
-
: undefined,
|
|
514
|
+
? kind : undefined,
|
|
391
515
|
url: typeof notice.url === 'string' ? notice.url : undefined,
|
|
392
|
-
|
|
516
|
+
// Device codes are not arbitrary log messages or credentials.
|
|
517
|
+
code: typeof notice.code === 'string' && /^[A-Za-z0-9-]{3,32}$/.test(notice.code)
|
|
518
|
+
? notice.code : undefined,
|
|
519
|
+
errorCode: allowedCode(notice.errorCode),
|
|
393
520
|
});
|
|
394
521
|
}
|
|
395
522
|
}
|
|
@@ -397,21 +524,25 @@ window.__ModuleLoader__.load({
|
|
|
397
524
|
status: raw.status,
|
|
398
525
|
notices,
|
|
399
526
|
outcome: raw.outcome === 'authorized' || raw.outcome === 'cancelled' || raw.outcome === 'failed' ? raw.outcome : null,
|
|
527
|
+
errorCode: allowedCode(raw.errorCode),
|
|
400
528
|
};
|
|
401
529
|
}
|
|
402
530
|
function parseModelSync(value) {
|
|
403
531
|
const raw = recordOf(value);
|
|
404
532
|
if (raw === null || raw.synced !== true || typeof raw.count !== 'number' || !Number.isSafeInteger(raw.count) || raw.count < 0) {
|
|
405
|
-
throw new ClientFailure('
|
|
533
|
+
throw new ClientFailure('invalid-response');
|
|
406
534
|
}
|
|
407
|
-
return { synced: true, count: raw.count };
|
|
535
|
+
return { synced: true, count: raw.count, warningCode: raw.warningCode == null ? undefined : allowedCode(raw.warningCode) ?? 'unknown' };
|
|
408
536
|
}
|
|
409
537
|
function safeVerificationUrl(value) {
|
|
410
|
-
if (typeof value !== 'string')
|
|
538
|
+
if (typeof value !== 'string' || value !== value.trim())
|
|
411
539
|
return null;
|
|
412
540
|
try {
|
|
413
541
|
const url = new URL(value);
|
|
414
|
-
return url.
|
|
542
|
+
return url.origin === 'https://auth.openai.com' && !url.username && !url.password &&
|
|
543
|
+
(url.pathname === '/codex/device' || url.pathname === '/codex/device/') && !url.search && !url.hash &&
|
|
544
|
+
/^https:\/\/auth\.openai\.com(?::443)?\/codex\/device\/?$/i.test(value)
|
|
545
|
+
? url.href : null;
|
|
415
546
|
}
|
|
416
547
|
catch {
|
|
417
548
|
return null;
|
|
@@ -426,26 +557,67 @@ window.__ModuleLoader__.load({
|
|
|
426
557
|
const [flowMethod, setFlowMethod] = React.useState('device_code');
|
|
427
558
|
const [device, setDevice] = React.useState(null);
|
|
428
559
|
const [notice, setNotice] = React.useState(null);
|
|
560
|
+
const [statusError, setStatusError] = React.useState(null);
|
|
561
|
+
const [statusLoading, setStatusLoading] = React.useState(true);
|
|
562
|
+
const [pollError, setPollError] = React.useState(null);
|
|
563
|
+
const [flowWarning, setFlowWarning] = React.useState(null);
|
|
564
|
+
const [copyNotice, setCopyNotice] = React.useState(null);
|
|
565
|
+
const [cancelPending, setCancelPending] = React.useState(false);
|
|
429
566
|
const [confirm, setConfirm] = React.useState(null);
|
|
430
567
|
const [statusRevision, setStatusRevision] = React.useState(0);
|
|
431
568
|
const actionLock = React.useRef(false);
|
|
569
|
+
const cancelAcknowledged = React.useRef(false);
|
|
570
|
+
const flowObserved = React.useRef(false);
|
|
571
|
+
const reloadRef = React.useRef(null);
|
|
572
|
+
const lifetime = React.useRef(null);
|
|
573
|
+
const phaseRef = React.useRef(phase);
|
|
574
|
+
const dialogRef = React.useRef(null);
|
|
575
|
+
const codeRef = React.useRef(null);
|
|
576
|
+
const returnFocus = React.useRef(null);
|
|
577
|
+
phaseRef.current = phase;
|
|
432
578
|
const busy = phase !== 'idle';
|
|
579
|
+
const polling = phase === 'authorizing';
|
|
433
580
|
const configured = info?.configured === true;
|
|
434
|
-
const ready = info?.ready
|
|
581
|
+
const ready = info?.ready === true;
|
|
435
582
|
const reloadStatus = () => setStatusRevision((revision) => revision + 1);
|
|
583
|
+
const changePhase = (next) => {
|
|
584
|
+
phaseRef.current = next;
|
|
585
|
+
setPhase(next);
|
|
586
|
+
};
|
|
587
|
+
React.useEffect(() => {
|
|
588
|
+
const controller = new AbortController();
|
|
589
|
+
lifetime.current = controller;
|
|
590
|
+
actionLock.current = false;
|
|
591
|
+
cancelAcknowledged.current = false;
|
|
592
|
+
flowObserved.current = false;
|
|
593
|
+
setCancelPending(false);
|
|
594
|
+
setDevice(null);
|
|
595
|
+
setCopyNotice(null);
|
|
596
|
+
changePhase('idle');
|
|
597
|
+
return () => { controller.abort(); };
|
|
598
|
+
}, [connection]);
|
|
436
599
|
React.useEffect(() => {
|
|
437
600
|
let alive = true;
|
|
438
601
|
const controller = new AbortController();
|
|
602
|
+
setStatusLoading(true);
|
|
439
603
|
remoteCall(connection, 'status', {}, controller.signal).then(parseStatus).then((status) => {
|
|
440
604
|
if (!alive)
|
|
441
605
|
return;
|
|
442
606
|
setInfo(status);
|
|
443
|
-
|
|
607
|
+
// Status owns only its own feedback; action failures survive reloads.
|
|
608
|
+
setStatusError(null);
|
|
609
|
+
if (status.flowPending)
|
|
610
|
+
flowObserved.current = true;
|
|
611
|
+
if (status.flowPending && phaseRef.current === 'idle') {
|
|
612
|
+
setFlowStage(undefined);
|
|
613
|
+
changePhase('authorizing');
|
|
614
|
+
}
|
|
444
615
|
}).catch((error) => {
|
|
445
616
|
if (!alive || controller.signal.aborted)
|
|
446
617
|
return;
|
|
447
|
-
|
|
448
|
-
})
|
|
618
|
+
setStatusError(errorKey(error, 'error.status'));
|
|
619
|
+
}).finally(() => { if (alive)
|
|
620
|
+
setStatusLoading(false); });
|
|
449
621
|
return () => {
|
|
450
622
|
alive = false;
|
|
451
623
|
controller.abort();
|
|
@@ -454,180 +626,344 @@ window.__ModuleLoader__.load({
|
|
|
454
626
|
React.useEffect(() => {
|
|
455
627
|
if (props.subscribeReset === undefined)
|
|
456
628
|
return;
|
|
457
|
-
return props.subscribeReset(
|
|
629
|
+
return props.subscribeReset(reloadStatus);
|
|
458
630
|
}, [props.subscribeReset]);
|
|
459
631
|
React.useEffect(() => {
|
|
460
|
-
if (confirm === null)
|
|
632
|
+
if (confirm === null || typeof document === 'undefined')
|
|
461
633
|
return;
|
|
462
|
-
const
|
|
463
|
-
|
|
634
|
+
const dialog = dialogRef.current;
|
|
635
|
+
const previous = returnFocus.current ?? document.activeElement;
|
|
636
|
+
const focusable = () => Array.from(dialog?.querySelectorAll('button:not(:disabled), a[href], input:not(:disabled), [tabindex="0"]') ?? []);
|
|
637
|
+
const focusFirst = () => (focusable()[0] ?? dialog)?.focus();
|
|
638
|
+
focusFirst(); // Start on Cancel, never the destructive confirmation.
|
|
639
|
+
const onKey = (event) => {
|
|
640
|
+
if (event.key === 'Escape') {
|
|
641
|
+
event.preventDefault();
|
|
642
|
+
event.stopPropagation();
|
|
464
643
|
setConfirm(null);
|
|
644
|
+
}
|
|
645
|
+
else if (event.key === 'Tab') {
|
|
646
|
+
const items = focusable();
|
|
647
|
+
const first = items[0];
|
|
648
|
+
const last = items[items.length - 1];
|
|
649
|
+
if (!first || !last) {
|
|
650
|
+
event.preventDefault();
|
|
651
|
+
dialog?.focus();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (!dialog?.contains(document.activeElement) || (event.shiftKey ? document.activeElement === first : document.activeElement === last)) {
|
|
655
|
+
event.preventDefault();
|
|
656
|
+
(event.shiftKey ? last : first).focus();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
const onFocus = (event) => {
|
|
661
|
+
if (dialog && !dialog.contains(event.target))
|
|
662
|
+
focusFirst();
|
|
663
|
+
};
|
|
664
|
+
document.addEventListener('keydown', onKey);
|
|
665
|
+
document.addEventListener('focusin', onFocus);
|
|
666
|
+
return () => {
|
|
667
|
+
document.removeEventListener('keydown', onKey);
|
|
668
|
+
document.removeEventListener('focusin', onFocus);
|
|
669
|
+
if (previous?.isConnected && !previous.matches(':disabled'))
|
|
670
|
+
previous.focus();
|
|
671
|
+
else
|
|
672
|
+
reloadRef.current?.focus();
|
|
673
|
+
returnFocus.current = null;
|
|
465
674
|
};
|
|
466
|
-
|
|
467
|
-
return () => document.removeEventListener('keydown', closeOnEscape);
|
|
468
|
-
}, [confirm, busy]);
|
|
675
|
+
}, [confirm]);
|
|
469
676
|
React.useEffect(() => {
|
|
470
|
-
if (
|
|
677
|
+
if (!polling)
|
|
471
678
|
return;
|
|
472
679
|
let alive = true;
|
|
680
|
+
let stopped = false;
|
|
473
681
|
let inFlight = false;
|
|
474
682
|
let failures = 0;
|
|
475
|
-
let
|
|
683
|
+
let nextPollAt = 0;
|
|
684
|
+
const startedAt = Date.now();
|
|
476
685
|
const controller = new AbortController();
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (outcome === 'authorized') {
|
|
484
|
-
setNotice(modelSyncPending
|
|
485
|
-
? { tone: 'warning', key: 'toast.sync-pending' }
|
|
486
|
-
: { tone: 'success', key: 'toast.connected' });
|
|
487
|
-
}
|
|
488
|
-
else if (outcome === 'cancelled')
|
|
489
|
-
setNotice({ tone: 'neutral', key: 'toast.cancelled' });
|
|
490
|
-
else
|
|
491
|
-
setNotice({ tone: 'error', key: 'error.start' });
|
|
492
|
-
reloadStatus();
|
|
493
|
-
};
|
|
494
|
-
const applyNotices = (notices) => {
|
|
495
|
-
for (const item of notices) {
|
|
496
|
-
if (item.kind !== undefined)
|
|
497
|
-
setFlowStage(item.kind);
|
|
498
|
-
if (item.kind === 'enter-code' && item.code) {
|
|
499
|
-
setDevice({ code: item.code, url: safeVerificationUrl(item.url) });
|
|
500
|
-
}
|
|
501
|
-
else if (item.kind === 'models-synced') {
|
|
502
|
-
setNotice({ tone: 'success', key: 'toast.connected' });
|
|
503
|
-
}
|
|
504
|
-
else if (item.kind === 'models-sync-failed') {
|
|
505
|
-
modelSyncPending = true;
|
|
506
|
-
setNotice({ tone: 'warning', key: 'toast.sync-pending' });
|
|
507
|
-
}
|
|
508
|
-
}
|
|
686
|
+
let stopTimer;
|
|
687
|
+
const pause = (key) => {
|
|
688
|
+
stopped = true;
|
|
689
|
+
stopTimer?.();
|
|
690
|
+
setPollError(key);
|
|
691
|
+
changePhase('paused');
|
|
509
692
|
};
|
|
510
693
|
const pollOnce = () => {
|
|
511
|
-
if (!alive ||
|
|
694
|
+
if (!alive || stopped)
|
|
695
|
+
return;
|
|
696
|
+
if (Date.now() - startedAt >= MAX_POLL_DURATION_MS) {
|
|
697
|
+
pause('error.poll-paused');
|
|
698
|
+
controller.abort();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (inFlight || Date.now() < nextPollAt)
|
|
512
702
|
return;
|
|
513
703
|
inFlight = true;
|
|
514
704
|
remoteCall(connection, 'poll', {}, controller.signal).then(parsePoll).then((poll) => {
|
|
515
|
-
if (!alive)
|
|
705
|
+
if (!alive || stopped)
|
|
516
706
|
return;
|
|
517
707
|
failures = 0;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
708
|
+
nextPollAt = 0;
|
|
709
|
+
setPollError(null);
|
|
710
|
+
if (poll.status === 'pending')
|
|
711
|
+
flowObserved.current = true;
|
|
712
|
+
if ((poll.status === 'idle' || !flowObserved.current) && cancelAcknowledged.current) {
|
|
713
|
+
stopped = true;
|
|
714
|
+
stopTimer?.();
|
|
715
|
+
setDevice(null);
|
|
716
|
+
setCopyNotice(null);
|
|
717
|
+
setFlowWarning(null);
|
|
718
|
+
setFlowStage(undefined);
|
|
719
|
+
setNotice({ tone: 'neutral', key: 'toast.cancelled' });
|
|
720
|
+
changePhase('idle');
|
|
721
|
+
reloadStatus();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (poll.status === 'idle' || (poll.status === 'done' && poll.outcome === null)) {
|
|
725
|
+
// Configured may describe an older account. It proves nothing about this attempt.
|
|
726
|
+
pause(poll.status === 'idle' ? 'error.poll-idle' : 'error.invalid-response');
|
|
727
|
+
reloadStatus();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (poll.status === 'done' && !flowObserved.current) {
|
|
731
|
+
// A retained terminal result may predate a timed-out authorize RPC.
|
|
732
|
+
pause('error.poll-unconfirmed');
|
|
733
|
+
reloadStatus();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
let warning = null;
|
|
737
|
+
for (const item of poll.notices) {
|
|
738
|
+
if (item.kind === 'requesting-code' || item.kind === 'enter-code' || item.kind === 'refreshing')
|
|
739
|
+
setFlowStage(item.kind);
|
|
740
|
+
if (item.kind === 'enter-code') {
|
|
741
|
+
const url = safeVerificationUrl(item.url);
|
|
742
|
+
if (item.code) {
|
|
743
|
+
const code = item.code;
|
|
744
|
+
setDevice((current) => current && current.code === code && current.url === url ? current : { code, url });
|
|
745
|
+
}
|
|
746
|
+
else
|
|
747
|
+
setDevice(null);
|
|
748
|
+
if (!url)
|
|
749
|
+
warning = 'error.unsafe-url';
|
|
750
|
+
else if (!item.code)
|
|
751
|
+
warning = 'error.invalid-response';
|
|
752
|
+
}
|
|
753
|
+
if (item.kind === 'models-sync-failed')
|
|
754
|
+
warning = item.errorCode ? 'error.' + item.errorCode : 'toast.sync-pending';
|
|
755
|
+
else if (item.errorCode)
|
|
756
|
+
warning = 'error.' + item.errorCode;
|
|
757
|
+
}
|
|
758
|
+
setFlowWarning(warning);
|
|
759
|
+
if (poll.status === 'done') {
|
|
760
|
+
stopped = true;
|
|
761
|
+
stopTimer?.();
|
|
762
|
+
setDevice(null);
|
|
763
|
+
setCopyNotice(null);
|
|
764
|
+
setFlowStage(undefined);
|
|
765
|
+
changePhase('idle');
|
|
766
|
+
if (poll.outcome === 'authorized') {
|
|
767
|
+
setNotice({ tone: warning ? 'warning' : 'success', key: warning ?? 'toast.connected' });
|
|
768
|
+
}
|
|
769
|
+
else if (poll.outcome === 'cancelled') {
|
|
770
|
+
setNotice({ tone: 'neutral', key: 'toast.cancelled' });
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
const code = poll.errorCode ?? poll.notices.map((item) => item.errorCode).filter(Boolean).pop();
|
|
774
|
+
setNotice({ tone: 'error', key: code ? 'error.' + code : 'error.start' });
|
|
775
|
+
}
|
|
776
|
+
setFlowWarning(null);
|
|
777
|
+
reloadStatus();
|
|
528
778
|
}
|
|
529
779
|
}).catch((error) => {
|
|
530
|
-
if (!alive || controller.signal.aborted)
|
|
780
|
+
if (!alive || stopped || controller.signal.aborted)
|
|
531
781
|
return;
|
|
532
782
|
failures += 1;
|
|
533
|
-
if (failures >=
|
|
534
|
-
|
|
783
|
+
if (failures >= MAX_POLL_FAILURES)
|
|
784
|
+
pause('error.poll-paused');
|
|
785
|
+
else {
|
|
786
|
+
setPollError(errorKey(error, 'error.poll'));
|
|
787
|
+
nextPollAt = Date.now() + Math.min(8_000, 1_000 * 2 ** (failures - 1));
|
|
788
|
+
}
|
|
535
789
|
}).finally(() => { inFlight = false; });
|
|
536
790
|
};
|
|
537
791
|
pollOnce();
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
792
|
+
// Host timers are preferred, but browser timers also support settings-only mounts.
|
|
793
|
+
try {
|
|
794
|
+
stopTimer = timer?.interval(pollOnce, 1_000);
|
|
795
|
+
}
|
|
796
|
+
catch { /* use browser timer below */ }
|
|
797
|
+
if (!stopTimer) {
|
|
798
|
+
const id = setInterval(pollOnce, 1_000);
|
|
799
|
+
stopTimer = () => clearInterval(id);
|
|
800
|
+
}
|
|
544
801
|
return () => {
|
|
545
802
|
alive = false;
|
|
546
803
|
controller.abort();
|
|
547
|
-
|
|
804
|
+
stopTimer?.();
|
|
548
805
|
};
|
|
549
|
-
}, [
|
|
550
|
-
const
|
|
806
|
+
}, [polling, connection, timer]);
|
|
807
|
+
const retryPolling = () => {
|
|
808
|
+
if (phaseRef.current !== 'paused' || actionLock.current)
|
|
809
|
+
return;
|
|
810
|
+
setPollError(null);
|
|
811
|
+
changePhase('authorizing');
|
|
812
|
+
reloadStatus();
|
|
813
|
+
};
|
|
814
|
+
const openConfirm = (action) => {
|
|
551
815
|
if (busy || actionLock.current)
|
|
552
816
|
return;
|
|
817
|
+
returnFocus.current = typeof document === 'undefined' ? null : document.activeElement;
|
|
818
|
+
setConfirm(action);
|
|
819
|
+
};
|
|
820
|
+
const startAuthorization = (method) => {
|
|
821
|
+
const signal = lifetime.current?.signal;
|
|
822
|
+
if (phaseRef.current !== 'idle' || actionLock.current || !signal || signal.aborted)
|
|
823
|
+
return;
|
|
553
824
|
actionLock.current = true;
|
|
825
|
+
cancelAcknowledged.current = false;
|
|
826
|
+
flowObserved.current = false;
|
|
554
827
|
setNotice(null);
|
|
828
|
+
setPollError(null);
|
|
829
|
+
setFlowWarning(null);
|
|
830
|
+
setCopyNotice(null);
|
|
555
831
|
setDevice(null);
|
|
556
832
|
setFlowMethod(method);
|
|
557
833
|
setFlowStage(method === 'refresh' ? 'refreshing' : 'requesting-code');
|
|
558
|
-
|
|
559
|
-
remoteCall(connection, 'authorize', { method }).then(parseAuthorize).then((result) => {
|
|
834
|
+
changePhase('starting');
|
|
835
|
+
remoteCall(connection, 'authorize', { method }, signal).then(parseAuthorize).then((result) => {
|
|
836
|
+
if (signal.aborted)
|
|
837
|
+
return;
|
|
560
838
|
if (!result.started) {
|
|
561
|
-
setNotice({ tone: 'error', key: 'error.start' });
|
|
562
|
-
|
|
839
|
+
setNotice({ tone: 'error', key: result.errorCode ? 'error.' + result.errorCode : 'error.start' });
|
|
840
|
+
changePhase('idle');
|
|
841
|
+
reloadStatus();
|
|
563
842
|
return;
|
|
564
843
|
}
|
|
565
|
-
|
|
844
|
+
flowObserved.current = true;
|
|
845
|
+
changePhase('authorizing');
|
|
566
846
|
}).catch((error) => {
|
|
567
|
-
|
|
847
|
+
if (signal.aborted)
|
|
848
|
+
return;
|
|
849
|
+
// A timeout does not prove the host failed to start. Never cancel implicitly.
|
|
568
850
|
setNotice({ tone: 'error', key: errorKey(error, 'error.start') });
|
|
569
|
-
|
|
570
|
-
|
|
851
|
+
const code = failureCode(error);
|
|
852
|
+
changePhase(code === 'timeout' || code === 'network' || code === 'unknown' || code === 'invalid-response' ? 'paused' : 'idle');
|
|
853
|
+
reloadStatus();
|
|
854
|
+
}).finally(() => { if (!signal.aborted)
|
|
855
|
+
actionLock.current = false; });
|
|
571
856
|
};
|
|
572
857
|
const cancelAuthorization = () => {
|
|
573
|
-
|
|
858
|
+
const signal = lifetime.current?.signal;
|
|
859
|
+
if ((phaseRef.current !== 'authorizing' && phaseRef.current !== 'paused') || actionLock.current || !signal || signal.aborted)
|
|
574
860
|
return;
|
|
575
861
|
actionLock.current = true;
|
|
576
|
-
|
|
577
|
-
remoteCall(connection, 'cancel', {}).
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
862
|
+
setCancelPending(true);
|
|
863
|
+
remoteCall(connection, 'cancel', {}, signal).then(() => {
|
|
864
|
+
if (signal.aborted)
|
|
865
|
+
return;
|
|
866
|
+
// Idle after acknowledged cancellation is safe to dismiss; never infer authorization.
|
|
867
|
+
cancelAcknowledged.current = true;
|
|
868
|
+
if (phaseRef.current === 'paused') {
|
|
869
|
+
setPollError(null);
|
|
870
|
+
changePhase('authorizing');
|
|
871
|
+
}
|
|
872
|
+
}).catch((error) => {
|
|
873
|
+
if (!signal.aborted)
|
|
874
|
+
setNotice({ tone: 'error', key: errorKey(error, 'error.cancel') });
|
|
875
|
+
}).finally(() => {
|
|
876
|
+
if (!signal.aborted) {
|
|
877
|
+
actionLock.current = false;
|
|
878
|
+
setCancelPending(false);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
581
881
|
};
|
|
582
|
-
const synchronizeModels = () => {
|
|
583
|
-
|
|
882
|
+
const synchronizeModels = (confirmed = false) => {
|
|
883
|
+
const signal = lifetime.current?.signal;
|
|
884
|
+
if (phaseRef.current !== 'idle' || actionLock.current || !signal || signal.aborted)
|
|
584
885
|
return;
|
|
585
886
|
actionLock.current = true;
|
|
586
887
|
setConfirm(null);
|
|
587
888
|
setNotice(null);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
889
|
+
changePhase('syncing');
|
|
890
|
+
let needsConfirmation = false;
|
|
891
|
+
remoteCall(connection, 'syncModels', confirmed ? { confirmed: true } : {}, signal, MUTATION_TIMEOUT_MS).then(parseModelSync).then((result) => {
|
|
892
|
+
if (signal.aborted)
|
|
893
|
+
return;
|
|
894
|
+
setNotice(result.warningCode
|
|
895
|
+
? { tone: 'warning', key: 'error.' + result.warningCode }
|
|
896
|
+
: { tone: 'success', key: 'toast.synced', params: { count: result.count } });
|
|
592
897
|
}).catch((error) => {
|
|
593
|
-
|
|
898
|
+
if (signal.aborted)
|
|
899
|
+
return;
|
|
900
|
+
setNotice({ tone: 'error', key: failureCode(error) === 'timeout' ? 'error.long-action' : errorKey(error, 'error.sync') });
|
|
901
|
+
needsConfirmation = failureCode(error) === 'models-confirmation-required';
|
|
594
902
|
}).finally(() => {
|
|
595
|
-
|
|
596
|
-
|
|
903
|
+
if (!signal.aborted) {
|
|
904
|
+
actionLock.current = false;
|
|
905
|
+
changePhase('idle');
|
|
906
|
+
if (needsConfirmation) {
|
|
907
|
+
returnFocus.current = typeof document === 'undefined' ? null : document.activeElement;
|
|
908
|
+
setConfirm('sync');
|
|
909
|
+
}
|
|
910
|
+
reloadStatus();
|
|
911
|
+
}
|
|
597
912
|
});
|
|
598
913
|
};
|
|
599
914
|
const requestModelSync = () => {
|
|
600
915
|
if (info?.modelsSynced)
|
|
601
916
|
synchronizeModels();
|
|
602
917
|
else
|
|
603
|
-
|
|
918
|
+
openConfirm('sync');
|
|
604
919
|
};
|
|
605
920
|
const disconnect = () => {
|
|
606
|
-
|
|
921
|
+
const signal = lifetime.current?.signal;
|
|
922
|
+
if (phaseRef.current !== 'idle' || actionLock.current || !signal || signal.aborted)
|
|
607
923
|
return;
|
|
608
924
|
actionLock.current = true;
|
|
609
925
|
setConfirm(null);
|
|
610
926
|
setNotice(null);
|
|
611
|
-
|
|
612
|
-
remoteCall(connection, 'logout', {},
|
|
613
|
-
|
|
927
|
+
changePhase('disconnecting');
|
|
928
|
+
remoteCall(connection, 'logout', {}, signal, MUTATION_TIMEOUT_MS).then(() => {
|
|
929
|
+
if (signal.aborted)
|
|
930
|
+
return;
|
|
614
931
|
setDevice(null);
|
|
615
932
|
setNotice({ tone: 'success', key: 'toast.disconnected' });
|
|
616
933
|
}).catch((error) => {
|
|
617
|
-
|
|
934
|
+
if (!signal.aborted)
|
|
935
|
+
setNotice({ tone: 'error', key: failureCode(error) === 'timeout' ? 'error.long-action' : errorKey(error, 'error.disconnect') });
|
|
618
936
|
}).finally(() => {
|
|
619
|
-
|
|
620
|
-
|
|
937
|
+
if (!signal.aborted) {
|
|
938
|
+
actionLock.current = false;
|
|
939
|
+
changePhase('idle');
|
|
940
|
+
reloadStatus();
|
|
941
|
+
}
|
|
621
942
|
});
|
|
622
943
|
};
|
|
944
|
+
const selectCode = () => {
|
|
945
|
+
codeRef.current?.focus();
|
|
946
|
+
codeRef.current?.select();
|
|
947
|
+
setCopyNotice('device.manual');
|
|
948
|
+
};
|
|
623
949
|
const copyCode = () => {
|
|
624
|
-
if (device === null
|
|
625
|
-
setNotice({ tone: 'error', key: 'error.clipboard' });
|
|
950
|
+
if (device === null)
|
|
626
951
|
return;
|
|
952
|
+
const signal = lifetime.current?.signal;
|
|
953
|
+
try {
|
|
954
|
+
if (typeof navigator === 'undefined' || typeof navigator.clipboard?.writeText !== 'function') {
|
|
955
|
+
selectCode();
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
Promise.resolve(navigator.clipboard.writeText(device.code)).then(() => {
|
|
959
|
+
if (!signal?.aborted)
|
|
960
|
+
setCopyNotice('toast.copied');
|
|
961
|
+
}).catch(() => { if (!signal?.aborted)
|
|
962
|
+
selectCode(); });
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
selectCode();
|
|
627
966
|
}
|
|
628
|
-
navigator.clipboard.writeText(device.code).then(() => {
|
|
629
|
-
setNotice({ tone: 'success', key: 'toast.copied' });
|
|
630
|
-
}).catch(() => setNotice({ tone: 'error', key: 'error.clipboard' }));
|
|
631
967
|
};
|
|
632
968
|
const progressKey = flowStage === 'enter-code'
|
|
633
969
|
? 'progress.enter-code'
|
|
@@ -636,9 +972,14 @@ window.__ModuleLoader__.load({
|
|
|
636
972
|
: flowStage === 'requesting-code'
|
|
637
973
|
? 'progress.requesting-code'
|
|
638
974
|
: 'progress.authorizing';
|
|
975
|
+
const statusKey = configured
|
|
976
|
+
? info?.credentialState === 'expired' ? 'status.expired' : info?.credentialState === 'valid' ? 'status.connected' : 'status.unknown'
|
|
977
|
+
: ready ? 'status.disconnected' : 'status.unavailable';
|
|
639
978
|
const statusCard = info === null
|
|
640
|
-
? el('div', { className: 'oasub-card oasub-skeleton', role: 'status', 'aria-live': 'polite' }, el('div', { className: 'oasub-status-title' }, t('status.loading')))
|
|
641
|
-
: el('section', { className: 'oasub-card'
|
|
979
|
+
? el('div', { className: 'oasub-card' + (statusLoading ? ' oasub-skeleton' : ''), role: 'status', 'aria-live': 'polite' }, el('div', { className: 'oasub-status-title' }, t(statusLoading ? 'status.loading' : 'status.failed')))
|
|
980
|
+
: el('section', { className: 'oasub-card' }, el('div', { className: 'oasub-status-line' }, el('div', { className: 'oasub-status-copy' }, el('div', { className: 'oasub-status-title' }, el('span', { className: 'oasub-dot ' + (statusKey === 'status.connected' ? 'success' : statusKey === 'status.disconnected' ? '' : 'warning'), 'aria-hidden': true }), t(statusKey)), el('div', { className: 'oasub-status-detail' }, t(statusKey + '.detail')), !ready && configured
|
|
981
|
+
? el('div', { className: 'oasub-status-detail' }, t('status.unavailable')) : null, !ready && info.unavailableReason
|
|
982
|
+
? el('div', { className: 'oasub-status-detail' }, t('error.' + info.unavailableReason)) : null), configured && info.modelsSynced
|
|
642
983
|
? el('span', { className: 'oasub-pill' }, t('model.count', { count: info.modelCount }))
|
|
643
984
|
: null), configured
|
|
644
985
|
? el('div', { className: 'oasub-model' }, el('span', { className: 'oasub-model-icon', 'aria-hidden': true }, info.modelsSynced ? '✓' : '↻'), el('div', { className: 'oasub-model-copy' }, el('div', { className: 'oasub-model-title' }, t(info.modelsSynced ? 'model.synced' : 'model.attention')), el('div', { className: 'oasub-model-detail' }, t(info.modelsSynced ? 'model.synced.detail' : 'model.attention.detail'))))
|
|
@@ -663,40 +1004,36 @@ window.__ModuleLoader__.load({
|
|
|
663
1004
|
}, phase === 'starting' || phase === 'authorizing'
|
|
664
1005
|
? t('action.refreshing')
|
|
665
1006
|
: t(info.refreshable ? 'action.refresh' : 'action.reconnect'))
|
|
666
|
-
: null,
|
|
1007
|
+
: null, info.cleanupAvailable
|
|
667
1008
|
? el('button', {
|
|
668
1009
|
type: 'button',
|
|
669
1010
|
className: 'oasub-button danger quiet',
|
|
670
1011
|
disabled: busy,
|
|
671
|
-
onClick: () =>
|
|
1012
|
+
onClick: () => openConfirm('disconnect'),
|
|
672
1013
|
}, phase === 'disconnecting' ? t('action.disconnecting') : t('action.disconnect'))
|
|
673
|
-
: null, phase === 'authorizing' || phase === 'starting' || phase === 'cancelling'
|
|
674
|
-
? el('button', {
|
|
675
|
-
type: 'button',
|
|
676
|
-
className: 'oasub-button quiet',
|
|
677
|
-
disabled: phase === 'cancelling',
|
|
678
|
-
onClick: cancelAuthorization,
|
|
679
|
-
}, t(phase === 'cancelling' ? 'action.cancelling' : 'action.cancel'))
|
|
680
1014
|
: null));
|
|
681
|
-
return el('div', { className: 'oasub-wrap' }, el('header', { className: 'oasub-header' }, el('div', { className: 'oasub-mark', 'aria-hidden': true }, '✦'), el('div', { className: 'oasub-heading' }, el('h2', { className: 'oasub-title' }, t('title')), el('p', { className: 'oasub-subtitle' }, t('subtitle')))), statusCard,
|
|
682
|
-
? el('
|
|
683
|
-
|
|
684
|
-
:
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
: null,
|
|
688
|
-
? el('div', { className: 'oasub-notice
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
:
|
|
1015
|
+
return el('div', { className: 'oasub-wrap' }, el('header', { className: 'oasub-header' }, el('div', { className: 'oasub-mark', 'aria-hidden': true }, '✦'), el('div', { className: 'oasub-heading' }, el('h2', { className: 'oasub-title' }, t('title')), el('p', { className: 'oasub-subtitle' }, t('subtitle')))), statusCard, statusError !== null
|
|
1016
|
+
? el('div', { className: 'oasub-notice error', role: 'alert' }, t(statusError), info !== null ? ' ' + t('status.stale') : null, el('button', { type: 'button', className: 'oasub-button quiet', disabled: statusLoading, onClick: reloadStatus }, t('action.retry'))) : null, el('div', { className: 'oasub-actions' }, el('button', { ref: reloadRef, type: 'button', className: 'oasub-button', disabled: statusLoading, onClick: reloadStatus }, t(statusLoading ? 'status.reloading' : 'action.reload'))), device !== null
|
|
1017
|
+
? el('section', { className: 'oasub-card oasub-device', 'aria-label': t('device.title') }, el('div', { className: 'oasub-device-head' }, el('div', { className: 'oasub-status-title' }, t('device.title')), el('div', { className: 'oasub-status-detail' }, t('device.detail'))), el('input', { ref: codeRef, className: 'oasub-code', type: 'text', readOnly: true, value: device.code,
|
|
1018
|
+
'aria-label': t('device.code'), autoComplete: 'off', spellCheck: false, onFocus: (event) => event.currentTarget.select() }), el('div', { className: 'oasub-actions' }, device.url !== null
|
|
1019
|
+
? el('a', { className: 'oasub-button primary', href: device.url, target: '_blank', rel: 'noopener noreferrer',
|
|
1020
|
+
'aria-label': t('action.open') + ' — ' + t('device.newTab') }, t('action.open'))
|
|
1021
|
+
: null, el('button', { type: 'button', className: 'oasub-button', onClick: copyCode }, t('action.copy')), el('button', { type: 'button', className: 'oasub-button', onClick: selectCode }, t('action.select'))), copyNotice ? el('div', { role: 'status', 'aria-live': 'polite', className: 'oasub-status-detail' }, t(copyNotice)) : null) : null, phase === 'authorizing' || phase === 'starting'
|
|
1022
|
+
? el('div', { className: 'oasub-notice', role: 'status', 'aria-live': 'polite' }, el('span', { className: 'oasub-dot warning', 'aria-hidden': true }), t(cancelPending ? 'action.cancelling' : progressKey)) : null, flowWarning
|
|
1023
|
+
? el('div', { className: 'oasub-notice warning', role: 'status' }, t(flowWarning)) : null, pollError || phase === 'paused'
|
|
1024
|
+
? el('div', { className: 'oasub-notice warning', role: 'status', 'aria-live': 'polite' }, t(pollError ?? 'error.poll-paused'), polling && pollError !== 'error.poll' ? ' ' + t('error.poll') : null) : null, phase === 'authorizing' || phase === 'paused'
|
|
1025
|
+
? el('div', { className: 'oasub-actions' }, phase === 'paused'
|
|
1026
|
+
? el('button', { type: 'button', className: 'oasub-button primary', disabled: cancelPending, onClick: retryPolling }, t('action.retryPoll')) : null, el('button', { type: 'button', className: 'oasub-button', disabled: cancelPending, onClick: cancelAuthorization }, t(cancelPending ? 'action.cancelling' : 'action.cancel'))) : null, notice !== null
|
|
1027
|
+
? el('div', { className: 'oasub-notice ' + notice.tone, role: notice.tone === 'error' ? 'alert' : 'status' }, el('span', { 'aria-hidden': true }, notice.tone === 'success' ? '✓' : notice.tone === 'error' ? '!' : '•'), t(notice.key, notice.params)) : null, confirm !== null
|
|
692
1028
|
? el('div', { className: 'oasub-dialog-backdrop', role: 'presentation', onMouseDown: (event) => {
|
|
693
1029
|
if (!busy && event.target === event.currentTarget)
|
|
694
1030
|
setConfirm(null);
|
|
695
|
-
} }, el('div', {
|
|
1031
|
+
} }, el('div', { ref: dialogRef, tabIndex: -1, className: 'oasub-dialog', role: 'dialog', 'aria-modal': true,
|
|
1032
|
+
'aria-labelledby': 'oasub-dialog-title', 'aria-describedby': 'oasub-dialog-detail' }, el('h3', { id: 'oasub-dialog-title' }, t(confirm === 'sync' ? 'dialog.sync.title' : 'dialog.disconnect.title')), el('p', { id: 'oasub-dialog-detail' }, t(confirm === 'sync' ? 'dialog.sync.detail' : 'dialog.disconnect.detail')), el('div', { className: 'oasub-actions' }, el('button', { type: 'button', className: 'oasub-button', disabled: busy, onClick: () => setConfirm(null) }, t('action.cancel')), el('button', {
|
|
696
1033
|
type: 'button',
|
|
697
1034
|
className: 'oasub-button ' + (confirm === 'sync' ? 'primary' : 'danger'),
|
|
698
1035
|
disabled: busy,
|
|
699
|
-
onClick: confirm === 'sync' ? synchronizeModels : disconnect,
|
|
1036
|
+
onClick: confirm === 'sync' ? () => synchronizeModels(true) : disconnect,
|
|
700
1037
|
}, t(confirm === 'sync' ? 'action.continue' : 'action.disconnect')))))
|
|
701
1038
|
: null);
|
|
702
1039
|
}
|