dsh-openai-subscription 0.1.1 → 0.1.2

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/client.js CHANGED
@@ -2,10 +2,128 @@
2
2
  // Web settings UI for ChatGPT subscription authorization.
3
3
  // This remains a global script for the DSH client module loader.
4
4
  //#endregion
5
- /** Stringify an unknown thrown value for display. */
6
- function messageOf(error) {
7
- const message = error?.message;
8
- return message ? String(message) : String(error);
5
+ const NS = 'settings.openaiSubscription';
6
+ const ZH = {
7
+ nav: 'ChatGPT 订阅',
8
+ title: 'ChatGPT 订阅',
9
+ subtitle: '连接具备 Codex 权限的账号,并在本机安全管理授权与可用模型。',
10
+ 'status.loading': '正在读取连接状态',
11
+ 'status.connected': '已连接',
12
+ 'status.disconnected': '未连接',
13
+ 'status.unavailable': '登录组件不可用',
14
+ 'status.connected.detail': 'DSH 已可使用此 ChatGPT 订阅。',
15
+ 'status.disconnected.detail': '连接后会自动同步账号可见模型。',
16
+ 'status.unavailable.detail': '请更新或重启 DSH 后再试。',
17
+ 'model.synced': '模型已同步',
18
+ 'model.attention': '模型需要同步',
19
+ 'model.count': '{count} 个可用模型',
20
+ 'model.synced.detail': '账号目录、内置目录和本地编辑已安全合并。',
21
+ 'model.attention.detail': '确认同步后会保留本地新增项与已编辑字段。',
22
+ 'action.connect': '连接 ChatGPT',
23
+ 'action.connecting': '正在连接…',
24
+ 'action.refresh': '刷新授权',
25
+ 'action.reconnect': '重新连接',
26
+ 'action.refreshing': '正在刷新…',
27
+ 'action.sync': '同步模型',
28
+ 'action.updateModels': '更新模型',
29
+ 'action.syncing': '正在同步…',
30
+ 'action.disconnect': '断开连接',
31
+ 'action.disconnecting': '正在断开…',
32
+ 'action.cancel': '取消',
33
+ 'action.cancelling': '正在取消…',
34
+ 'action.retry': '重试',
35
+ 'action.copy': '复制代码',
36
+ 'action.open': '打开验证页面',
37
+ 'action.continue': '确认并同步',
38
+ 'progress.requesting-code': '正在准备安全登录…',
39
+ 'progress.enter-code': '等待你在 ChatGPT 完成确认…',
40
+ 'progress.refreshing': '正在安全刷新授权…',
41
+ 'progress.authorizing': '正在等待 ChatGPT 授权…',
42
+ 'device.title': '完成设备验证',
43
+ 'device.detail': '打开验证页面并输入以下一次性代码。',
44
+ 'toast.connected': '连接成功,授权已安全保存。',
45
+ 'toast.cancelled': '连接已取消。',
46
+ 'toast.synced': '模型目录已更新,共 {count} 个可用模型。',
47
+ 'toast.sync-pending': '连接已保存,可稍后手动同步模型。',
48
+ 'toast.disconnected': '已移除本机上的 ChatGPT 连接。',
49
+ 'toast.copied': '代码已复制。',
50
+ 'error.status': '暂时无法读取连接状态。',
51
+ 'error.request': '请求未完成,请稍后重试。',
52
+ 'error.timeout': '请求超时,请检查连接后重试。',
53
+ 'error.poll': '授权状态暂时中断,正在自动重试。',
54
+ 'error.start': '无法启动授权,请稍后重试。',
55
+ 'error.cancel': '无法取消授权,请重试。',
56
+ 'error.sync': '模型同步失败,现有模型设置未被覆盖。',
57
+ 'error.disconnect': '无法断开连接,请重试。',
58
+ 'error.clipboard': '无法复制,请手动选择代码。',
59
+ 'dialog.sync.title': '合并并同步模型?',
60
+ 'dialog.sync.detail': '账号可见模型和 DSH 内置模型将合并到现有列表;你的新增模型、字段编辑和删除选择会被保留。',
61
+ 'dialog.disconnect.title': '断开 ChatGPT 连接?',
62
+ 'dialog.disconnect.detail': '这只会删除本机授权和插件管理的未修改模型项,不会注销或删除你的 ChatGPT 账号。',
63
+ };
64
+ const EN = {
65
+ nav: 'ChatGPT Subscription',
66
+ title: 'ChatGPT Subscription',
67
+ subtitle: 'Connect an account with Codex access and manage authorization and available models locally.',
68
+ 'status.loading': 'Reading connection status',
69
+ 'status.connected': 'Connected',
70
+ 'status.disconnected': 'Not connected',
71
+ 'status.unavailable': 'Sign-in component unavailable',
72
+ 'status.connected.detail': 'DSH can use this ChatGPT subscription.',
73
+ 'status.disconnected.detail': 'Connecting automatically syncs account-visible models.',
74
+ 'status.unavailable.detail': 'Update or restart DSH, then try again.',
75
+ 'model.synced': 'Models synced',
76
+ 'model.attention': 'Models need syncing',
77
+ 'model.count': '{count} models available',
78
+ 'model.synced.detail': 'Account, built-in, and locally edited catalogs are safely merged.',
79
+ 'model.attention.detail': 'Syncing preserves local additions and fields you edited.',
80
+ 'action.connect': 'Connect ChatGPT',
81
+ 'action.connecting': 'Connecting…',
82
+ 'action.refresh': 'Refresh authorization',
83
+ 'action.reconnect': 'Reconnect',
84
+ 'action.refreshing': 'Refreshing…',
85
+ 'action.sync': 'Sync models',
86
+ 'action.updateModels': 'Update models',
87
+ 'action.syncing': 'Syncing…',
88
+ 'action.disconnect': 'Disconnect',
89
+ 'action.disconnecting': 'Disconnecting…',
90
+ 'action.cancel': 'Cancel',
91
+ 'action.cancelling': 'Cancelling…',
92
+ 'action.retry': 'Retry',
93
+ 'action.copy': 'Copy code',
94
+ 'action.open': 'Open verification page',
95
+ 'action.continue': 'Confirm and sync',
96
+ 'progress.requesting-code': 'Preparing secure sign-in…',
97
+ 'progress.enter-code': 'Waiting for confirmation in ChatGPT…',
98
+ 'progress.refreshing': 'Securely refreshing authorization…',
99
+ 'progress.authorizing': 'Waiting for ChatGPT authorization…',
100
+ 'device.title': 'Complete device verification',
101
+ 'device.detail': 'Open the verification page and enter this one-time code.',
102
+ 'toast.connected': 'Connected and authorization saved securely.',
103
+ 'toast.cancelled': 'Connection cancelled.',
104
+ 'toast.synced': 'Model catalog updated with {count} available models.',
105
+ 'toast.sync-pending': 'Connection saved. Models can be synced manually later.',
106
+ 'toast.disconnected': 'The local ChatGPT connection was removed.',
107
+ 'toast.copied': 'Code copied.',
108
+ 'error.status': 'Connection status is temporarily unavailable.',
109
+ 'error.request': 'The request did not complete. Try again shortly.',
110
+ 'error.timeout': 'The request timed out. Check the connection and retry.',
111
+ 'error.poll': 'Authorization status was interrupted. Retrying automatically.',
112
+ 'error.start': 'Could not start authorization. Try again shortly.',
113
+ 'error.cancel': 'Could not cancel authorization. Try again.',
114
+ 'error.sync': 'Model sync failed. Existing model settings were not overwritten.',
115
+ 'error.disconnect': 'Could not disconnect. Try again.',
116
+ 'error.clipboard': 'Could not copy. Select the code manually.',
117
+ 'dialog.sync.title': 'Merge and sync models?',
118
+ '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
+ 'dialog.disconnect.title': 'Disconnect ChatGPT?',
120
+ '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.',
121
+ };
122
+ function fallbackTranslate(key, params = {}) {
123
+ const language = typeof navigator === 'undefined' ? 'zh' : navigator.language.toLowerCase();
124
+ const dictionary = language.startsWith('zh') ? ZH : EN;
125
+ const template = dictionary[key] ?? EN[key] ?? key;
126
+ return template.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match);
9
127
  }
10
128
  window.__ModuleLoader__.load({
11
129
  id: 'dsh-openai-subscription',
@@ -14,31 +132,166 @@ window.__ModuleLoader__.load({
14
132
  const exports = module.exports;
15
133
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
16
134
  const React = require('react');
17
- const CSS = [
18
- '.oasub-wrap { display: flex; flex-direction: column; gap: 12px; max-width: 620px; }',
19
- '.oasub-title { font-size: 15px; font-weight: 600; }',
20
- '.oasub-desc { font-size: 13px; line-height: 1.6; opacity: .8; }',
21
- '.oasub-card { border: 1px solid rgba(128,128,128,.28); border-radius: 10px; padding: 12px 14px; display: flex; flex-direction: column; gap: 6px; }',
22
- '.oasub-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }',
23
- '.oasub-code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 18px; font-weight: 700; letter-spacing: 2px; padding: 8px 14px; border-radius: 8px; background: rgba(128,128,128,.16); user-select: all; }',
24
- '.oasub-link { color: inherit; }',
25
- '.oasub-btn { padding: 7px 14px; border-radius: 8px; border: 1px solid rgba(128,128,128,.4); background: transparent; color: inherit; cursor: pointer; font-size: 13px; }',
26
- '.oasub-btn:disabled { opacity: .45; cursor: default; }',
27
- '.oasub-btn.primary { background: #087c61; border-color: #087c61; color: #fff; }',
28
- '.oasub-btn:focus-visible, .oasub-link:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }',
29
- '.oasub-err { color: #c52b32; font-size: 13px; }',
30
- '.oasub-ok { color: #087c61; font-size: 13px; }',
31
- ].join('\n');
135
+ const CSS = `
136
+ .oasub-wrap {
137
+ box-sizing: border-box;
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 16px;
141
+ width: min(100%, 720px);
142
+ color: var(--dsw-alias-label-primary, #0f1115);
143
+ }
144
+ .oasub-wrap, .oasub-wrap * { box-sizing: border-box; }
145
+ .oasub-header { display: flex; align-items: center; gap: 12px; }
146
+ .oasub-mark {
147
+ display: grid;
148
+ place-items: center;
149
+ flex: 0 0 40px;
150
+ width: 40px;
151
+ height: 40px;
152
+ border-radius: 13px;
153
+ color: var(--dsw-alias-label-primary-inverted, #fff);
154
+ background: var(--dsw-alias-brand-primary, #4176e6);
155
+ font-size: 20px;
156
+ font-weight: 700;
157
+ line-height: 1;
158
+ }
159
+ .oasub-heading { min-width: 0; }
160
+ .oasub-title { margin: 0; font-size: 18px; font-weight: 600; line-height: 26px; }
161
+ .oasub-subtitle { margin: 2px 0 0; color: var(--dsw-alias-label-tertiary, #81858c); font-size: 13px; line-height: 20px; }
162
+ .oasub-card {
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: 16px;
166
+ padding: 20px;
167
+ border: .5px solid var(--dsw-alias-border-l2, rgba(15, 17, 21, .14));
168
+ border-radius: 16px;
169
+ background: var(--dsw-alias-bg-layer-1, #fff);
170
+ box-shadow: var(--dsw-shadow-lv1, 0 2px 8px rgba(0, 0, 0, .04));
171
+ }
172
+ .oasub-status-line { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
173
+ .oasub-status-copy { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
174
+ .oasub-status-title { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; line-height: 22px; }
175
+ .oasub-status-detail { color: var(--dsw-alias-label-tertiary, #81858c); font-size: 12px; line-height: 18px; }
176
+ .oasub-dot { width: 8px; height: 8px; border-radius: 999px; background: var(--dsw-alias-label-tertiary, #81858c); }
177
+ .oasub-dot.success { background: var(--dsw-alias-state-success-primary, #22c55e); }
178
+ .oasub-dot.warning { background: var(--dsw-alias-state-warn-primary, #f59e0b); }
179
+ .oasub-pill {
180
+ flex: 0 0 auto;
181
+ padding: 4px 9px;
182
+ border-radius: 999px;
183
+ color: var(--dsw-alias-label-secondary, #61666b);
184
+ background: var(--dsw-alias-bg-layer-2, #f5f6f7);
185
+ font-size: 11px;
186
+ font-weight: 500;
187
+ line-height: 16px;
188
+ }
189
+ .oasub-model {
190
+ display: flex;
191
+ align-items: center;
192
+ gap: 10px;
193
+ padding: 12px 14px;
194
+ border-radius: 12px;
195
+ background: var(--dsw-alias-bg-module-platform, var(--dsw-alias-bg-layer-2, #f5f6f7));
196
+ }
197
+ .oasub-model-icon { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 9px; background: var(--dsw-alias-bg-layer-1, #fff); font-size: 14px; }
198
+ .oasub-model-copy { display: flex; flex: 1; flex-direction: column; gap: 1px; min-width: 0; }
199
+ .oasub-model-title { font-size: 13px; font-weight: 500; line-height: 19px; }
200
+ .oasub-model-detail { color: var(--dsw-alias-label-tertiary, #81858c); font-size: 11px; line-height: 17px; }
201
+ .oasub-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
202
+ .oasub-button {
203
+ appearance: none;
204
+ min-height: 36px;
205
+ padding: 7px 15px;
206
+ border: .5px solid var(--dsw-alias-border-l1, rgba(15, 17, 21, .2));
207
+ border-radius: 999px;
208
+ color: var(--dsw-alias-label-primary, #0f1115);
209
+ background: transparent;
210
+ font: inherit;
211
+ font-size: 13px;
212
+ font-weight: 500;
213
+ line-height: 20px;
214
+ cursor: pointer;
215
+ transition: background .16s ease, border-color .16s ease, opacity .16s ease, transform .16s ease;
216
+ }
217
+ .oasub-button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(15, 17, 21, .06)); }
218
+ .oasub-button:active:not(:disabled) { transform: translateY(1px); }
219
+ .oasub-button:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #4176e6); outline-offset: 2px; }
220
+ .oasub-button:disabled { cursor: default; opacity: .45; }
221
+ .oasub-button.primary { border-color: var(--dsw-alias-brand-primary, #4176e6); color: var(--dsw-alias-label-primary-inverted, #fff); background: var(--dsw-alias-brand-primary, #4176e6); }
222
+ .oasub-button.primary:hover:not(:disabled) { filter: brightness(.96); }
223
+ .oasub-button.danger { border-color: var(--dsw-alias-state-error-primary, #dc2626); color: var(--dsw-alias-label-primary-inverted, #fff); background: var(--dsw-alias-state-error-primary, #dc2626); }
224
+ .oasub-button.danger:hover:not(:disabled) { filter: brightness(.94); }
225
+ .oasub-button.quiet { margin-left: auto; }
226
+ .oasub-notice {
227
+ display: flex;
228
+ align-items: flex-start;
229
+ gap: 10px;
230
+ padding: 12px 14px;
231
+ border: .5px solid var(--dsw-alias-border-l2, rgba(15, 17, 21, .14));
232
+ border-radius: 12px;
233
+ color: var(--dsw-alias-label-secondary, #61666b);
234
+ background: var(--dsw-alias-bg-layer-2, #f5f6f7);
235
+ font-size: 12px;
236
+ line-height: 18px;
237
+ }
238
+ .oasub-notice.success { border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #22c55e) 40%, transparent); }
239
+ .oasub-notice.warning { border-color: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 45%, transparent); }
240
+ .oasub-notice.error { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #dc2626) 45%, transparent); color: var(--dsw-alias-state-error-primary, #b91c1c); }
241
+ .oasub-device { gap: 14px; }
242
+ .oasub-device-head { display: flex; flex-direction: column; gap: 3px; }
243
+ .oasub-code {
244
+ align-self: flex-start;
245
+ margin: 0;
246
+ padding: 10px 14px;
247
+ border-radius: 10px;
248
+ color: var(--dsw-alias-label-primary, #0f1115);
249
+ background: var(--dsw-alias-bg-layer-2, #f5f6f7);
250
+ font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
251
+ font-size: 18px;
252
+ font-weight: 650;
253
+ letter-spacing: .12em;
254
+ user-select: all;
255
+ }
256
+ .oasub-skeleton { overflow: hidden; min-height: 112px; position: relative; }
257
+ .oasub-skeleton::after { content: ''; position: absolute; inset: 0; background: linear-gradient(105deg, transparent 35%, rgba(127, 130, 135, .12) 50%, transparent 65%); animation: oasub-shimmer 1.2s infinite linear; transform: translateX(-100%); }
258
+ @keyframes oasub-shimmer { to { transform: translateX(100%); } }
259
+ .oasub-dialog-backdrop { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 20px; background: rgba(0, 0, 0, .38); backdrop-filter: blur(2px); }
260
+ .oasub-dialog { width: min(100%, 430px); padding: 20px; border: .5px solid var(--dsw-alias-border-l2, rgba(15, 17, 21, .16)); border-radius: 16px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: var(--dsw-elevation-prominent, 0 14px 40px rgba(0, 0, 0, .18)); }
261
+ .oasub-dialog h3 { margin: 0; font-size: 16px; line-height: 24px; }
262
+ .oasub-dialog p { margin: 8px 0 18px; color: var(--dsw-alias-label-secondary, #61666b); font-size: 13px; line-height: 20px; }
263
+ .oasub-dialog .oasub-actions { justify-content: flex-end; }
264
+ @media (max-width: 520px) {
265
+ .oasub-card { padding: 16px; }
266
+ .oasub-status-line { flex-direction: column; }
267
+ .oasub-button { flex: 1 1 auto; }
268
+ .oasub-button.quiet { margin-left: 0; }
269
+ }
270
+ @media (prefers-reduced-motion: reduce) {
271
+ .oasub-button { transition: none; }
272
+ .oasub-skeleton::after { animation: none; display: none; }
273
+ }
274
+ `;
32
275
  function ensureCss() {
33
276
  const tagId = 'dsh-openai-subscription/settings.css';
34
- if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {
35
- const tag = document.createElement('style');
277
+ if (typeof document === 'undefined')
278
+ return;
279
+ let tag = document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']');
280
+ if (tag === null) {
281
+ tag = document.createElement('style');
36
282
  tag.setAttribute('data-plugin-css', tagId);
37
- tag.textContent = CSS;
38
283
  document.head.appendChild(tag);
39
284
  }
285
+ tag.textContent = CSS;
40
286
  }
41
- const RPC_TIMEOUT_MS = 12000;
287
+ class ClientFailure extends Error {
288
+ code;
289
+ constructor(code) {
290
+ super(code);
291
+ this.code = code;
292
+ }
293
+ }
294
+ const RPC_TIMEOUT_MS = 15_000;
42
295
  function remoteCall(connection, method, args, signal, timeoutMs = RPC_TIMEOUT_MS) {
43
296
  return new Promise((resolve, reject) => {
44
297
  const controller = new AbortController();
@@ -57,8 +310,8 @@ window.__ModuleLoader__.load({
57
310
  callback();
58
311
  };
59
312
  const onAbort = () => {
60
- controller.abort();
61
- finish(() => reject(new Error('请求已取消')));
313
+ controller.abort(signal?.reason);
314
+ finish(() => reject(new ClientFailure('cancelled')));
62
315
  };
63
316
  if (signal?.aborted) {
64
317
  onAbort();
@@ -67,66 +320,74 @@ window.__ModuleLoader__.load({
67
320
  signal?.addEventListener('abort', onAbort, { once: true });
68
321
  timeout = setTimeout(() => {
69
322
  controller.abort();
70
- finish(() => reject(new Error('请求超时,请检查连接后重试')));
323
+ finish(() => reject(new ClientFailure('timeout')));
71
324
  }, timeoutMs);
72
325
  let request;
73
326
  try {
74
327
  request = connection.rpc.call('/api', 'openaiSubscription/' + method, { args }, controller.signal);
75
328
  }
76
- catch (error) {
77
- finish(() => reject(error));
329
+ catch {
330
+ finish(() => reject(new ClientFailure('request')));
78
331
  return;
79
332
  }
80
333
  request.then((result) => {
81
334
  finish(() => {
82
- if (!result || !result.ok) {
83
- reject(new Error('DSH 服务请求失败,请重试'));
84
- return;
85
- }
86
- if (result.value === undefined) {
87
- reject(new Error('服务返回了无效结果'));
335
+ if (!result || result.ok !== true || result.value === undefined) {
336
+ console.warn('[openai-subscription] remote call failed:', method, result?.error?.code ?? 'invalid-result');
337
+ reject(new ClientFailure('request'));
88
338
  return;
89
339
  }
90
340
  resolve(result.value);
91
341
  });
92
- }).catch((error) => finish(() => reject(error)));
342
+ }).catch(() => finish(() => reject(new ClientFailure('request'))));
93
343
  });
94
344
  }
345
+ function errorKey(error, fallback) {
346
+ return error instanceof ClientFailure && error.code === 'timeout' ? 'error.timeout' : fallback;
347
+ }
348
+ function recordOf(value) {
349
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
350
+ ? value
351
+ : null;
352
+ }
95
353
  function parseStatus(value) {
96
- if (typeof value !== 'object' || value === null)
97
- throw new Error('登录状态响应无效');
98
- const raw = value;
99
- if (typeof raw.configured !== 'boolean' || typeof raw.ready !== 'boolean')
100
- throw new Error('登录状态响应不完整');
101
- const status = { configured: raw.configured, ready: raw.ready };
102
- if (typeof raw.accountId === 'string')
103
- status.accountId = raw.accountId;
104
- if (typeof raw.expires === 'number' && Number.isFinite(raw.expires) && raw.expires > 0)
105
- status.expires = raw.expires;
106
- return status;
354
+ const raw = recordOf(value);
355
+ if (raw === null || typeof raw.configured !== 'boolean' || typeof raw.ready !== 'boolean')
356
+ throw new ClientFailure('request');
357
+ if (!raw.configured) {
358
+ return { configured: false, ready: raw.ready, refreshable: false, modelsSynced: false, modelCount: 0 };
359
+ }
360
+ return {
361
+ configured: true,
362
+ 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
+ : 0,
368
+ };
107
369
  }
108
370
  function parseAuthorize(value) {
109
- if (typeof value !== 'object' || value === null)
110
- throw new Error('启动授权响应无效');
111
- const raw = value;
112
- if (typeof raw.started !== 'boolean')
113
- throw new Error('启动授权响应不完整');
114
- return { started: raw.started, error: typeof raw.error === 'string' ? raw.error : undefined };
371
+ const raw = recordOf(value);
372
+ if (raw === null || typeof raw.started !== 'boolean')
373
+ throw new ClientFailure('request');
374
+ return { started: raw.started };
115
375
  }
116
376
  function parsePoll(value) {
117
- if (typeof value !== 'object' || value === null)
118
- throw new Error('授权状态响应无效');
119
- const raw = value;
120
- if (raw.status !== 'idle' && raw.status !== 'pending' && raw.status !== 'done')
121
- throw new Error('授权状态响应不完整');
377
+ const raw = recordOf(value);
378
+ if (raw === null || (raw.status !== 'idle' && raw.status !== 'pending' && raw.status !== 'done'))
379
+ throw new ClientFailure('request');
122
380
  const notices = [];
123
381
  if (Array.isArray(raw.notices)) {
124
- for (const item of raw.notices) {
125
- if (typeof item !== 'object' || item === null)
382
+ for (const candidate of raw.notices) {
383
+ const notice = recordOf(candidate);
384
+ if (notice === null)
126
385
  continue;
127
- const notice = item;
386
+ const kind = notice.kind;
128
387
  notices.push({
129
- message: typeof notice.message === 'string' ? notice.message : undefined,
388
+ kind: kind === 'requesting-code' || kind === 'enter-code' || kind === 'refreshing' || kind === 'models-synced' || kind === 'models-sync-failed'
389
+ ? kind
390
+ : undefined,
130
391
  url: typeof notice.url === 'string' ? notice.url : undefined,
131
392
  code: typeof notice.code === 'string' ? notice.code : undefined,
132
393
  });
@@ -135,16 +396,22 @@ window.__ModuleLoader__.load({
135
396
  return {
136
397
  status: raw.status,
137
398
  notices,
138
- outcome: typeof raw.outcome === 'string' ? raw.outcome : null,
139
- error: typeof raw.error === 'string' ? raw.error : null,
399
+ outcome: raw.outcome === 'authorized' || raw.outcome === 'cancelled' || raw.outcome === 'failed' ? raw.outcome : null,
140
400
  };
141
401
  }
142
- function safeHttpUrl(value) {
402
+ function parseModelSync(value) {
403
+ const raw = recordOf(value);
404
+ if (raw === null || raw.synced !== true || typeof raw.count !== 'number' || !Number.isSafeInteger(raw.count) || raw.count < 0) {
405
+ throw new ClientFailure('request');
406
+ }
407
+ return { synced: true, count: raw.count };
408
+ }
409
+ function safeVerificationUrl(value) {
143
410
  if (typeof value !== 'string')
144
411
  return null;
145
412
  try {
146
- const url = new URL(value, window.location.href);
147
- return url.protocol === 'https:' || url.protocol === 'http:' ? url.href : null;
413
+ const url = new URL(value);
414
+ return url.protocol === 'https:' ? url.href : null;
148
415
  }
149
416
  catch {
150
417
  return null;
@@ -152,58 +419,93 @@ window.__ModuleLoader__.load({
152
419
  }
153
420
  function Section(props) {
154
421
  const el = React.createElement;
155
- const connection = props.connection;
156
- const timer = props.timer;
422
+ const { connection, timer, t } = props;
157
423
  const [info, setInfo] = React.useState(null);
158
424
  const [phase, setPhase] = React.useState('idle');
159
- const [notices, setNotices] = React.useState([]);
160
- const [error, setError] = React.useState('');
161
- const [statusError, setStatusError] = React.useState('');
162
- const [statusRetry, setStatusRetry] = React.useState(0);
163
- const [result, setResult] = React.useState(null);
425
+ const [flowStage, setFlowStage] = React.useState(undefined);
426
+ const [flowMethod, setFlowMethod] = React.useState('device_code');
427
+ const [device, setDevice] = React.useState(null);
428
+ const [notice, setNotice] = React.useState(null);
429
+ const [confirm, setConfirm] = React.useState(null);
430
+ const [statusRevision, setStatusRevision] = React.useState(0);
164
431
  const actionLock = React.useRef(false);
165
- const ready = info ? !!info.ready : true;
166
- const configured = info ? !!info.configured : false;
167
- const busy = phase !== 'idle' && phase !== 'done';
168
- const authBusy = phase === 'starting' || phase === 'pending' || phase === 'cancelling';
432
+ const busy = phase !== 'idle';
433
+ const configured = info?.configured === true;
434
+ const ready = info?.ready !== false;
435
+ const reloadStatus = () => setStatusRevision((revision) => revision + 1);
169
436
  React.useEffect(() => {
170
- if (phase !== 'idle' && phase !== 'done')
171
- return;
172
437
  let alive = true;
173
438
  const controller = new AbortController();
174
439
  remoteCall(connection, 'status', {}, controller.signal).then(parseStatus).then((status) => {
175
440
  if (!alive)
176
441
  return;
177
442
  setInfo(status);
178
- setStatusError('');
179
- }).catch((cause) => {
443
+ setNotice((current) => current?.tone === 'error' ? null : current);
444
+ }).catch((error) => {
180
445
  if (!alive || controller.signal.aborted)
181
446
  return;
182
- setStatusError('无法读取登录状态:' + messageOf(cause));
447
+ setNotice({ tone: 'error', key: errorKey(error, 'error.status') });
183
448
  });
184
449
  return () => {
185
450
  alive = false;
186
451
  controller.abort();
187
452
  };
188
- }, [connection, phase, statusRetry]);
453
+ }, [connection, statusRevision]);
189
454
  React.useEffect(() => {
190
- if (phase !== 'pending' && phase !== 'cancelling')
455
+ if (props.subscribeReset === undefined)
191
456
  return;
192
- if (timer === undefined || typeof timer.interval !== 'function') {
193
- setError('授权状态服务不可用,请重启 DSH 后重试。');
194
- setPhase('done');
457
+ return props.subscribeReset(() => reloadStatus());
458
+ }, [props.subscribeReset]);
459
+ React.useEffect(() => {
460
+ if (confirm === null)
461
+ return;
462
+ const closeOnEscape = (event) => {
463
+ if (event.key === 'Escape' && !busy)
464
+ setConfirm(null);
465
+ };
466
+ document.addEventListener('keydown', closeOnEscape);
467
+ return () => document.removeEventListener('keydown', closeOnEscape);
468
+ }, [confirm, busy]);
469
+ React.useEffect(() => {
470
+ if (phase !== 'authorizing' && phase !== 'cancelling')
195
471
  return;
196
- }
197
472
  let alive = true;
198
473
  let inFlight = false;
199
474
  let failures = 0;
475
+ let modelSyncPending = false;
200
476
  const controller = new AbortController();
201
- const finish = (nextResult, nextError = '') => {
477
+ const complete = (outcome) => {
202
478
  if (!alive)
203
479
  return;
204
- setResult(nextResult);
205
- setError(nextError);
206
- setPhase('done');
480
+ setDevice(null);
481
+ setFlowStage(undefined);
482
+ setPhase('idle');
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
+ }
207
509
  };
208
510
  const pollOnce = () => {
209
511
  if (!alive || inFlight)
@@ -213,123 +515,219 @@ window.__ModuleLoader__.load({
213
515
  if (!alive)
214
516
  return;
215
517
  failures = 0;
216
- setError((previous) => previous.startsWith('授权状态连接暂时中断') ? '' : previous);
217
- if (poll.notices && poll.notices.length)
218
- setNotices((previous) => previous.concat(poll.notices ?? []).slice(-8));
219
- if (poll.status === 'done') {
220
- finish(poll.outcome || 'done', poll.error || '');
221
- return;
222
- }
223
- if (poll.status === 'idle') {
224
- return remoteCall(connection, 'status', {}, controller.signal).then(parseStatus).then((status) => {
518
+ applyNotices(poll.notices);
519
+ if (poll.status === 'done')
520
+ complete(poll.outcome);
521
+ else if (poll.status === 'idle') {
522
+ void remoteCall(connection, 'status', {}, controller.signal).then(parseStatus).then((status) => {
225
523
  if (!alive)
226
524
  return;
227
525
  setInfo(status);
228
- if (status.configured)
229
- finish('authorized');
230
- else
231
- finish('ended', '授权流程已结束(未获取到结果)。');
232
- });
526
+ complete(status.configured ? 'authorized' : 'failed');
527
+ }).catch(() => complete('failed'));
233
528
  }
234
- }).catch((cause) => {
529
+ }).catch((error) => {
235
530
  if (!alive || controller.signal.aborted)
236
531
  return;
237
532
  failures += 1;
238
- const suffix = failures > 1 ? '(已重试 ' + failures + ' 次)' : '';
239
- setError('授权状态连接暂时中断,正在自动重试…' + suffix);
240
- console.warn('[openai-subscription] poll failed: ' + messageOf(cause));
241
- }).finally(() => {
242
- inFlight = false;
243
- });
533
+ if (failures >= 2)
534
+ setNotice({ tone: 'warning', key: errorKey(error, 'error.poll') });
535
+ }).finally(() => { inFlight = false; });
244
536
  };
245
537
  pollOnce();
246
- const stop = timer.interval(pollOnce, 1000);
538
+ const stop = timer?.interval
539
+ ? timer.interval(pollOnce, 1_000)
540
+ : (() => {
541
+ const id = setInterval(pollOnce, 1_000);
542
+ return () => clearInterval(id);
543
+ })();
247
544
  return () => {
248
545
  alive = false;
249
546
  controller.abort();
250
547
  stop();
251
548
  };
252
- }, [phase, timer, connection]);
253
- const start = (method) => {
549
+ }, [phase, connection, timer]);
550
+ const startAuthorization = (method) => {
254
551
  if (busy || actionLock.current)
255
552
  return;
256
- if (timer === undefined || typeof timer.interval !== 'function') {
257
- setError('授权状态服务不可用,请重启 DSH 后重试。');
258
- setPhase('done');
259
- return;
260
- }
261
553
  actionLock.current = true;
262
- setNotices([]);
263
- setError('');
264
- setResult(null);
554
+ setNotice(null);
555
+ setDevice(null);
556
+ setFlowMethod(method);
557
+ setFlowStage(method === 'refresh' ? 'refreshing' : 'requesting-code');
265
558
  setPhase('starting');
266
- remoteCall(connection, 'authorize', { method }).then(parseAuthorize).then((reply) => {
267
- if (reply.started)
268
- setPhase('pending');
269
- else {
270
- setError(reply.error || '无法启动授权流程');
271
- setPhase('done');
559
+ remoteCall(connection, 'authorize', { method }).then(parseAuthorize).then((result) => {
560
+ if (!result.started) {
561
+ setNotice({ tone: 'error', key: 'error.start' });
562
+ setPhase('idle');
563
+ return;
272
564
  }
273
- }).catch((cause) => {
274
- // The request might have reached the host before its acknowledgement
275
- // was lost, so cancel best-effort to avoid an orphaned flow.
565
+ setPhase('authorizing');
566
+ }).catch((error) => {
276
567
  void remoteCall(connection, 'cancel', {}).catch(() => { });
277
- setError(messageOf(cause));
278
- setPhase('done');
279
- }).finally(() => {
280
- actionLock.current = false;
281
- });
568
+ setNotice({ tone: 'error', key: errorKey(error, 'error.start') });
569
+ setPhase('idle');
570
+ }).finally(() => { actionLock.current = false; });
282
571
  };
283
- const cancel = () => {
284
- if (phase !== 'pending' || actionLock.current)
572
+ const cancelAuthorization = () => {
573
+ if ((phase !== 'authorizing' && phase !== 'starting') || actionLock.current)
285
574
  return;
286
575
  actionLock.current = true;
287
576
  setPhase('cancelling');
288
- remoteCall(connection, 'cancel', {}).catch((cause) => {
289
- setError('取消失败:' + messageOf(cause));
290
- setPhase('pending');
577
+ remoteCall(connection, 'cancel', {}).catch((error) => {
578
+ setNotice({ tone: 'error', key: errorKey(error, 'error.cancel') });
579
+ setPhase('authorizing');
580
+ }).finally(() => { actionLock.current = false; });
581
+ };
582
+ const synchronizeModels = () => {
583
+ if (busy || actionLock.current)
584
+ return;
585
+ actionLock.current = true;
586
+ setConfirm(null);
587
+ setNotice(null);
588
+ setPhase('syncing');
589
+ remoteCall(connection, 'syncModels', {}, undefined, 25_000).then(parseModelSync).then((result) => {
590
+ setNotice({ tone: 'success', key: 'toast.synced', params: { count: result.count } });
591
+ reloadStatus();
592
+ }).catch((error) => {
593
+ setNotice({ tone: 'error', key: errorKey(error, 'error.sync') });
291
594
  }).finally(() => {
292
595
  actionLock.current = false;
596
+ setPhase('idle');
293
597
  });
294
598
  };
295
- const logout = () => {
599
+ const requestModelSync = () => {
600
+ if (info?.modelsSynced)
601
+ synchronizeModels();
602
+ else
603
+ setConfirm('sync');
604
+ };
605
+ const disconnect = () => {
296
606
  if (busy || actionLock.current)
297
607
  return;
298
608
  actionLock.current = true;
299
- setError('');
300
- setPhase('logging-out');
301
- remoteCall(connection, 'logout', {}).then(() => {
302
- setInfo({ configured: false, ready });
303
- setNotices([]);
304
- setResult('logged-out');
305
- setPhase('done');
306
- }).catch((cause) => {
307
- setError('退出失败:' + messageOf(cause));
308
- setPhase('done');
609
+ setConfirm(null);
610
+ setNotice(null);
611
+ setPhase('disconnecting');
612
+ remoteCall(connection, 'logout', {}, undefined, 25_000).then(() => {
613
+ setInfo({ configured: false, ready, refreshable: false, modelsSynced: false, modelCount: 0 });
614
+ setDevice(null);
615
+ setNotice({ tone: 'success', key: 'toast.disconnected' });
616
+ }).catch((error) => {
617
+ setNotice({ tone: 'error', key: errorKey(error, 'error.disconnect') });
309
618
  }).finally(() => {
310
619
  actionLock.current = false;
620
+ setPhase('idle');
311
621
  });
312
622
  };
313
- return el('div', { className: 'oasub-wrap' }, el('div', { className: 'oasub-title', role: 'heading', 'aria-level': 2 }, 'ChatGPT 订阅登录'), el('div', { className: 'oasub-desc' }, '使用有 Codex 权限的 ChatGPT 账号授权 DSH。登录后,插件会尝试自动启用受支持的 GPT 模型,凭证保存在本机。'), !ready ? el('div', { className: 'oasub-err', role: 'alert' }, '当前 DSH 环境缺少 OpenAI 登录组件,请更新 DSH 后重试。') : null, info === null
314
- ? (statusError ? null : el('div', { className: 'oasub-card', role: 'status', 'aria-live': 'polite' }, '正在读取登录状态…'))
315
- : el('div', { className: 'oasub-card' }, configured ? el('div', { className: 'oasub-ok' }, '已登录 ChatGPT 订阅账号') : el('div', { className: 'oasub-desc' }, '尚未登录 ChatGPT 订阅账号'), configured && info.accountId ? el('div', { className: 'oasub-desc' }, '账号:' + info.accountId) : null, configured && info.expires ? el('div', { className: 'oasub-desc' }, '授权有效期至:' + new Date(info.expires).toLocaleString()) : null), notices.map((notice, index) => {
316
- const url = safeHttpUrl(notice.url);
317
- return el('div', { key: 'n' + index, className: 'oasub-card', role: 'status', 'aria-live': 'polite' }, notice.message ? el('div', { className: 'oasub-desc' }, notice.message) : null, notice.code ? el('div', { className: 'oasub-row' }, el('code', { className: 'oasub-code' }, notice.code), url ? el('a', { className: 'oasub-link', href: url, target: '_blank', rel: 'noreferrer' }, '打开登录页(新窗口)') : null) : null);
318
- }), statusError ? el('div', { className: 'oasub-row' }, el('div', { className: 'oasub-err', role: 'alert' }, statusError), el('button', { type: 'button', className: 'oasub-btn', disabled: busy, onClick: () => setStatusRetry((value) => value + 1) }, '重试')) : null, error ? el('div', { className: 'oasub-err', role: 'alert' }, error) : null, phase === 'done' && result === 'authorized' ? el('div', { className: 'oasub-ok', role: 'status', 'aria-live': 'polite' }, '授权成功,凭证已保存。') : null, phase === 'done' && result === 'cancelled' ? el('div', { className: 'oasub-desc', role: 'status', 'aria-live': 'polite' }, '授权已取消。') : null, phase === 'done' && result === 'logged-out' ? el('div', { className: 'oasub-ok', role: 'status', 'aria-live': 'polite' }, '已退出 ChatGPT 订阅账号。') : null, info === null ? null : el('div', { className: 'oasub-row' }, configured
319
- ? el('button', { type: 'button', className: 'oasub-btn', disabled: busy || !ready, onClick: () => start('refresh') }, authBusy && phase !== 'cancelling' ? '刷新中…' : '刷新授权')
320
- : el('button', { type: 'button', className: 'oasub-btn primary', disabled: busy || !ready, onClick: () => start('device_code') }, authBusy && phase !== 'cancelling' ? '登录中…' : '使用 ChatGPT 账号登录'), configured ? el('button', { type: 'button', className: 'oasub-btn', disabled: busy, onClick: logout }, phase === 'logging-out' ? '退出中…' : '退出登录') : null, phase === 'pending' || phase === 'cancelling'
321
- ? el('button', { type: 'button', className: 'oasub-btn', disabled: phase === 'cancelling', onClick: cancel }, phase === 'cancelling' ? '取消中…' : '取消')
322
- : null));
623
+ const copyCode = () => {
624
+ if (device === null || typeof navigator.clipboard?.writeText !== 'function') {
625
+ setNotice({ tone: 'error', key: 'error.clipboard' });
626
+ return;
627
+ }
628
+ navigator.clipboard.writeText(device.code).then(() => {
629
+ setNotice({ tone: 'success', key: 'toast.copied' });
630
+ }).catch(() => setNotice({ tone: 'error', key: 'error.clipboard' }));
631
+ };
632
+ const progressKey = flowStage === 'enter-code'
633
+ ? 'progress.enter-code'
634
+ : flowStage === 'refreshing' || flowMethod === 'refresh'
635
+ ? 'progress.refreshing'
636
+ : flowStage === 'requesting-code'
637
+ ? 'progress.requesting-code'
638
+ : 'progress.authorizing';
639
+ 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', 'aria-busy': busy }, el('div', { className: 'oasub-status-line' }, el('div', { className: 'oasub-status-copy' }, el('div', { className: 'oasub-status-title' }, el('span', { className: 'oasub-dot ' + (configured ? 'success' : ready ? '' : 'warning'), 'aria-hidden': true }), t(configured ? 'status.connected' : ready ? 'status.disconnected' : 'status.unavailable')), el('div', { className: 'oasub-status-detail' }, t(configured ? 'status.connected.detail' : ready ? 'status.disconnected.detail' : 'status.unavailable.detail'))), configured && info.modelsSynced
642
+ ? el('span', { className: 'oasub-pill' }, t('model.count', { count: info.modelCount }))
643
+ : null), configured
644
+ ? 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'))))
645
+ : null, el('div', { className: 'oasub-actions' }, configured
646
+ ? el('button', {
647
+ type: 'button',
648
+ className: 'oasub-button ' + (info.modelsSynced ? '' : 'primary'),
649
+ disabled: busy,
650
+ onClick: requestModelSync,
651
+ }, phase === 'syncing' ? t('action.syncing') : t(info.modelsSynced ? 'action.updateModels' : 'action.sync'))
652
+ : el('button', {
653
+ type: 'button',
654
+ className: 'oasub-button primary',
655
+ disabled: busy || !ready,
656
+ onClick: () => startAuthorization('device_code'),
657
+ }, phase === 'starting' || phase === 'authorizing' ? t('action.connecting') : t('action.connect')), configured
658
+ ? el('button', {
659
+ type: 'button',
660
+ className: 'oasub-button',
661
+ disabled: busy || !ready,
662
+ onClick: () => startAuthorization(info.refreshable ? 'refresh' : 'device_code'),
663
+ }, phase === 'starting' || phase === 'authorizing'
664
+ ? t('action.refreshing')
665
+ : t(info.refreshable ? 'action.refresh' : 'action.reconnect'))
666
+ : null, configured
667
+ ? el('button', {
668
+ type: 'button',
669
+ className: 'oasub-button danger quiet',
670
+ disabled: busy,
671
+ onClick: () => setConfirm('disconnect'),
672
+ }, 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
+ : 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, device !== null
682
+ ? el('section', { className: 'oasub-card oasub-device', role: 'status', 'aria-live': 'polite' }, 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('code', { className: 'oasub-code' }, device.code), el('div', { className: 'oasub-actions' }, device.url !== null
683
+ ? el('button', { type: 'button', className: 'oasub-button primary', onClick: () => window.open(device.url ?? '', '_blank', 'noopener,noreferrer') }, t('action.open'))
684
+ : null, el('button', { type: 'button', className: 'oasub-button', onClick: copyCode }, t('action.copy')), el('button', { type: 'button', className: 'oasub-button quiet', onClick: cancelAuthorization }, t('action.cancel'))))
685
+ : phase === 'authorizing' || phase === 'starting' || phase === 'cancelling'
686
+ ? el('div', { className: 'oasub-notice', role: 'status', 'aria-live': 'polite' }, el('span', { className: 'oasub-dot warning', 'aria-hidden': true }), t(progressKey))
687
+ : null, notice !== null
688
+ ? el('div', { className: 'oasub-notice ' + notice.tone, role: notice.tone === 'error' ? 'alert' : 'status', 'aria-live': 'polite' }, el('span', { 'aria-hidden': true }, notice.tone === 'success' ? '✓' : notice.tone === 'error' ? '!' : '•'), t(notice.key, notice.params), notice.key === 'error.status'
689
+ ? el('button', { type: 'button', className: 'oasub-button quiet', disabled: busy, onClick: reloadStatus }, t('action.retry'))
690
+ : null)
691
+ : null, confirm !== null
692
+ ? el('div', { className: 'oasub-dialog-backdrop', role: 'presentation', onMouseDown: (event) => {
693
+ if (!busy && event.target === event.currentTarget)
694
+ setConfirm(null);
695
+ } }, el('div', { className: 'oasub-dialog', role: 'dialog', 'aria-modal': true, 'aria-labelledby': 'oasub-dialog-title' }, el('h3', { id: 'oasub-dialog-title' }, t(confirm === 'sync' ? 'dialog.sync.title' : 'dialog.disconnect.title')), el('p', null, 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
+ type: 'button',
697
+ className: 'oasub-button ' + (confirm === 'sync' ? 'primary' : 'danger'),
698
+ disabled: busy,
699
+ onClick: confirm === 'sync' ? synchronizeModels : disconnect,
700
+ }, t(confirm === 'sync' ? 'action.continue' : 'action.disconnect')))))
701
+ : null);
323
702
  }
324
703
  function apply(ctx) {
325
704
  const slots = ctx.get('slots');
326
705
  if (slots === undefined)
327
706
  return;
328
707
  ensureCss();
329
- slots.inject('settings.section', () => slots.register({ name: 'settings.section', id: 'openai-subscription', order: 25, label: 'ChatGPT 订阅登录' }, () => React.createElement(Section, { connection: ctx.connection, timer: ctx.timer })));
708
+ const locale = ctx.locale ?? ctx.get('locale');
709
+ if (locale !== undefined) {
710
+ if (typeof ctx.effect === 'function') {
711
+ ctx.effect(() => locale.register(NS, { zh: ZH, en: EN }), 'openai-subscription: dictionaries');
712
+ }
713
+ else {
714
+ locale.register(NS, { zh: ZH, en: EN });
715
+ }
716
+ }
717
+ const t = locale?.bind(NS) ?? fallbackTranslate;
718
+ const subscribeReset = typeof ctx.on === 'function'
719
+ ? (listener) => ctx.on?.('connection/reset', listener) ?? (() => { })
720
+ : undefined;
721
+ slots.inject('settings.section', () => slots.register({
722
+ name: 'settings.section',
723
+ id: 'openai-subscription',
724
+ order: 25,
725
+ label: () => t('nav'),
726
+ locale: NS,
727
+ }, () => React.createElement(Section, { connection: ctx.connection, timer: ctx.timer, t, subscribeReset })));
330
728
  }
331
729
  exports.apply = apply;
332
- exports.inject = ['connection', 'timer'];
730
+ exports.inject = ['connection', 'timer', 'slots', 'locale'];
333
731
  return module.exports;
334
- }
732
+ },
335
733
  });