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/README.md +44 -15
- package/README.zh.md +45 -16
- package/dist/client.d.ts +40 -29
- package/dist/client.js +565 -167
- package/dist/host.d.ts +23 -13
- package/dist/host.js +331 -72
- package/dist/models.d.ts +51 -0
- package/dist/models.js +370 -0
- package/package.json +3 -1
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
77
|
-
finish(() => reject(
|
|
329
|
+
catch {
|
|
330
|
+
finish(() => reject(new ClientFailure('request')));
|
|
78
331
|
return;
|
|
79
332
|
}
|
|
80
333
|
request.then((result) => {
|
|
81
334
|
finish(() => {
|
|
82
|
-
if (!result ||
|
|
83
|
-
|
|
84
|
-
|
|
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((
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
125
|
-
|
|
382
|
+
for (const candidate of raw.notices) {
|
|
383
|
+
const notice = recordOf(candidate);
|
|
384
|
+
if (notice === null)
|
|
126
385
|
continue;
|
|
127
|
-
const
|
|
386
|
+
const kind = notice.kind;
|
|
128
387
|
notices.push({
|
|
129
|
-
|
|
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:
|
|
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
|
|
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
|
|
147
|
-
return url.protocol === 'https:'
|
|
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
|
|
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 [
|
|
160
|
-
const [
|
|
161
|
-
const [
|
|
162
|
-
const [
|
|
163
|
-
const [
|
|
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
|
|
166
|
-
const configured = info
|
|
167
|
-
const
|
|
168
|
-
const
|
|
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
|
-
|
|
179
|
-
}).catch((
|
|
443
|
+
setNotice((current) => current?.tone === 'error' ? null : current);
|
|
444
|
+
}).catch((error) => {
|
|
180
445
|
if (!alive || controller.signal.aborted)
|
|
181
446
|
return;
|
|
182
|
-
|
|
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,
|
|
453
|
+
}, [connection, statusRevision]);
|
|
189
454
|
React.useEffect(() => {
|
|
190
|
-
if (
|
|
455
|
+
if (props.subscribeReset === undefined)
|
|
191
456
|
return;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
477
|
+
const complete = (outcome) => {
|
|
202
478
|
if (!alive)
|
|
203
479
|
return;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
setPhase('
|
|
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
|
-
|
|
217
|
-
if (poll.
|
|
218
|
-
|
|
219
|
-
if (poll.status === '
|
|
220
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
else
|
|
231
|
-
finish('ended', '授权流程已结束(未获取到结果)。');
|
|
232
|
-
});
|
|
526
|
+
complete(status.configured ? 'authorized' : 'failed');
|
|
527
|
+
}).catch(() => complete('failed'));
|
|
233
528
|
}
|
|
234
|
-
}).catch((
|
|
529
|
+
}).catch((error) => {
|
|
235
530
|
if (!alive || controller.signal.aborted)
|
|
236
531
|
return;
|
|
237
532
|
failures += 1;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
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,
|
|
253
|
-
const
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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((
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
278
|
-
setPhase('
|
|
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
|
|
284
|
-
if (phase !== '
|
|
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((
|
|
289
|
-
|
|
290
|
-
setPhase('
|
|
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
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}).catch((
|
|
307
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
});
|