@young1lin/dsh-gpt-sub 0.1.2 → 0.1.3
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/CHANGELOG.md +16 -0
- package/README.md +27 -4
- package/client.js +383 -192
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
记录用户可感知的变化;格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
|
|
4
4
|
版本号见 `package.json`。
|
|
5
5
|
|
|
6
|
+
## [0.1.3] - 2026-10-08
|
|
7
|
+
|
|
8
|
+
### 新增
|
|
9
|
+
|
|
10
|
+
- Codex 设置面板接入 DSH 中英文语言服务,按钮、提示、倒计时、文件浏览、确认与重置结果跟随语言切换;有效期按所选语言格式化。
|
|
11
|
+
|
|
12
|
+
### 调整
|
|
13
|
+
|
|
14
|
+
- 设置导航统一命名为「Codex」,使用跟随主题的 Codex 图标替代默认齿轮,插件卸载时还原宿主图标。
|
|
15
|
+
- 改善重置条目的标题和状态/有效期排版,避免操作按钮挤压换行,并适配窄面板布局。
|
|
16
|
+
- 重置次数列表改为点击懒加载:打开/重开面板、切换语言、后台轮询与普通配额刷新不查询列表;首次点击「查询重置次数」后才读取,后续仅显式刷新或确认使用后更新。
|
|
17
|
+
|
|
18
|
+
### 修复
|
|
19
|
+
|
|
20
|
+
- 保留使用重置次数后的结果提示,避免随列表刷新立即清空;已显示的提示可随语言更新。
|
|
21
|
+
|
|
6
22
|
## [0.1.2] - 2026-10-05
|
|
7
23
|
|
|
8
24
|
### 新增
|
package/README.md
CHANGED
|
@@ -192,13 +192,29 @@ provider route never reads a stale token.
|
|
|
192
192
|
|
|
193
193
|
## Settings panel diagnostics
|
|
194
194
|
|
|
195
|
-
The
|
|
195
|
+
The **Codex** section follows the language selected in Settings →
|
|
196
|
+
General (English or Chinese), using DSH's locale service like `dsh-mcp-adapter`.
|
|
197
|
+
Buttons, placeholders, countdowns, file browsing, confirmations, and existing
|
|
198
|
+
action feedback follow locale updates. While the panel remains mounted, a
|
|
199
|
+
locale update preserves an edited path or proxy URL; navigating to another
|
|
200
|
+
settings section unmounts the panel and discards unsaved edits.
|
|
201
|
+
Credit expiration dates use the selected language too; upstream diagnostic
|
|
202
|
+
messages, file paths, and account-provided titles remain unchanged.
|
|
203
|
+
|
|
204
|
+
The navigation row uses the [Codex mark from Lobe Icons](https://unpkg.com/@lobehub/icons-static-svg@1.95.1/icons/codex.svg)
|
|
205
|
+
(MIT, attribution included in [the client bundle](client.js)), not the shell's
|
|
206
|
+
default gear. Its monochrome SVG follows the theme and is restored on plugin
|
|
207
|
+
unload. Since the settings shell currently has no public icon slot, this uses
|
|
208
|
+
the same narrowly scoped, disposable DOM adaptation as `dsh-mcp-adapter`.
|
|
209
|
+
If a future shell changes its markup, the icon adaptation safely does nothing.
|
|
210
|
+
|
|
211
|
+
The section does more than render the usage bars. It shows
|
|
196
212
|
one bar per rate-limit window the account reports — the 5-hour rolling window
|
|
197
213
|
and the weekly window — each drawn Codex-style as the percent **remaining**: a
|
|
198
214
|
full bar at 100% that drains toward 0% as the allowance is spent, with its own
|
|
199
|
-
reset countdown. The countdown is exact, not rounded to one unit: `4h22m
|
|
200
|
-
`10m22s
|
|
201
|
-
fast enough to read), `2d5h
|
|
215
|
+
reset countdown. The countdown is exact, not rounded to one unit: `Resets in 4h22m`
|
|
216
|
+
(or `4小时22分后重置` in Chinese), `10m22s` under an hour (seconds only appear there,
|
|
217
|
+
where they still change fast enough to read), `2d5h` past a day, and a zero component is dropped
|
|
202
218
|
rather than padded — `4h`, never `4h0m`. An account with no 5-hour limit (pro
|
|
203
219
|
accounts currently report only a weekly window) shows a note where that bar
|
|
204
220
|
would be, so the absence reads as a fact about the plan rather than a broken
|
|
@@ -229,6 +245,13 @@ a capped window without waiting out its timer. Below the bars the panel shows:
|
|
|
229
245
|
means direct. The line under the input notes whether the current value comes
|
|
230
246
|
from cordis config or a page override.
|
|
231
247
|
|
|
248
|
+
Reset credit details are lazy-loaded: opening/reopening the panel, switching
|
|
249
|
+
languages, and background quota/status polling do not query the credit list.
|
|
250
|
+
Click **Check reset credits / 查询重置次数** for the first read; after that, only
|
|
251
|
+
the credit list's **Refresh / 刷新** button or a confirmed consumption refreshes
|
|
252
|
+
it. The header badge simply reuses the ordinary quota response, with no extra
|
|
253
|
+
credit-list request.
|
|
254
|
+
|
|
232
255
|
Rate-limit reset credits (the codex CLI's `rate-limit-reset-credits` wham API): `GET /gpt-sub/reset-credits` lists the account's redeemable credits, `POST /gpt-sub/reset-credits/consume` (body `{ "creditId": "..." }`, optional) consumes one. The host mints a fresh `redeem_request_id` per click, and the panel asks for confirmation first because consuming is destructive.
|
|
233
256
|
|
|
234
257
|
Host endpoints behind the panel: `GET /gpt-sub/quota` (the bars), `GET /gpt-sub/status`,
|
package/client.js
CHANGED
|
@@ -41,7 +41,7 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
const POLL_MS = 60_000
|
|
42
42
|
|
|
43
43
|
const css = [
|
|
44
|
-
'.dshGptSub{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}',
|
|
44
|
+
'.dshGptSub{box-sizing:border-box;container-type:inline-size;width:100%;min-width:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}',
|
|
45
45
|
'.dshGptSubHead{display:flex;align-items:center;gap:8px;margin:0 0 10px}',
|
|
46
46
|
'.dshGptSubPlan{font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
47
47
|
'.dshGptSubTrack{position:relative;height:8px;border-radius:4px;background:var(--dsw-alias-interactive-bg-hover);overflow:hidden}',
|
|
@@ -54,13 +54,20 @@ window.__ModuleLoader__.load({
|
|
|
54
54
|
'.dshGptSubWinLabel{font-size:12px;color:var(--dsw-alias-label-tertiary);margin-bottom:4px}',
|
|
55
55
|
'.dshGptSubNote{margin-top:8px;font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
56
56
|
'.dshGptSubBtn{margin-left:auto}',
|
|
57
|
-
'.dshGptSubDiag{margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-interactive-bg-hover);display:grid;grid-template-columns:max-content 1fr;gap:
|
|
58
|
-
'.dshGptSubDiagLabel{color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px}',
|
|
57
|
+
'.dshGptSubDiag{margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-interactive-bg-hover);display:grid;grid-template-columns:max-content minmax(0,1fr);gap:14px 16px;align-items:start}',
|
|
58
|
+
'.dshGptSubDiagLabel{padding-top:6px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;font-size:12px}',
|
|
59
59
|
'.dshGptSubDiagValue{min-width:0;overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary)}',
|
|
60
|
-
'.dshGptSubProxyRow{display:flex;gap:8px;align-items:center;min-width:0}',
|
|
61
|
-
'.dshGptSubProxyField{flex:1;min-width:0;display:flex}',
|
|
62
|
-
'.dshGptSubProxyField
|
|
63
|
-
'.dshGptSubProxyMeta{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
60
|
+
'.dshGptSubProxyRow{display:flex;flex-wrap:wrap;gap:8px;align-items:center;min-width:0}',
|
|
61
|
+
'.dshGptSubProxyField{flex:1 1 180px;min-width:0;display:flex}',
|
|
62
|
+
'.dshGptSubProxyField>*{flex:1;min-width:0}',
|
|
63
|
+
'.dshGptSubProxyMeta{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin-top:6px;font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
64
|
+
'.dshGptSubCreditList{display:flex;flex-direction:column;gap:12px;margin-top:12px}',
|
|
65
|
+
'.dshGptSubCreditRow{display:flex;align-items:center;justify-content:space-between;gap:16px;min-width:0}',
|
|
66
|
+
'.dshGptSubCreditText{flex:1;min-width:0;overflow-wrap:anywhere}',
|
|
67
|
+
'.dshGptSubCreditTitle{color:var(--dsw-alias-label-secondary);font-size:13px}',
|
|
68
|
+
'.dshGptSubCreditMeta{display:flex;flex-wrap:wrap;gap:2px 8px;margin-top:2px;font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
69
|
+
'@container (max-width:560px){.dshGptSubDiag{grid-template-columns:minmax(0,1fr);gap:6px}.dshGptSubDiagLabel{padding-top:8px}.dshGptSubHead{flex-wrap:wrap}}',
|
|
70
|
+
'@container (max-width:340px){.dshGptSubProxyField{flex-basis:100%}.dshGptSubCreditRow{flex-direction:column;align-items:flex-start;gap:6px}}',
|
|
64
71
|
// The credential-file picker: a small host-backed directory browser.
|
|
65
72
|
'.dshGptSubBrowseMask{position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center}',
|
|
66
73
|
'.dshGptSubBrowse{width:min(560px,92vw);max-height:72vh;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-interactive-bg-hover);border-radius:12px;color:var(--dsw-alias-label-secondary);box-shadow:0 12px 40px rgba(0,0,0,.25)}',
|
|
@@ -93,14 +100,18 @@ window.__ModuleLoader__.load({
|
|
|
93
100
|
* @param props - variant, children, and native button attributes.
|
|
94
101
|
* @returns the button element.
|
|
95
102
|
*/
|
|
96
|
-
const SmallButton = (props) =>
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
const SmallButton = (props) => {
|
|
104
|
+
// The primitive's small size does not prevent flex compression: keep
|
|
105
|
+
// action labels on one line, particularly "Use and reset".
|
|
106
|
+
const buttonProps = { ...props, style: { ...props.style, whiteSpace: 'nowrap', flexShrink: 0 } }
|
|
107
|
+
return Button !== undefined
|
|
108
|
+
? React.createElement(Button, { size: 'sm', ...buttonProps })
|
|
99
109
|
: React.createElement('button', {
|
|
100
110
|
type: 'button',
|
|
101
111
|
className: 'dshGptSubFallbackButton',
|
|
102
|
-
...
|
|
112
|
+
...buttonProps,
|
|
103
113
|
})
|
|
114
|
+
}
|
|
104
115
|
|
|
105
116
|
/**
|
|
106
117
|
* Render a pill badge through the primitives, or a styled native span.
|
|
@@ -128,57 +139,26 @@ window.__ModuleLoader__.load({
|
|
|
128
139
|
return '#ef4444'
|
|
129
140
|
}
|
|
130
141
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
* components and dropping a trailing zero: days+hours past a day,
|
|
134
|
-
* hours+minutes past an hour, minutes+seconds under one -- '4h22m',
|
|
135
|
-
* '10m22s', '2d5h', '45s', never '4h0m'. Seconds only appear under an
|
|
136
|
-
* hour, where they still change fast enough to read.
|
|
137
|
-
* @param seconds - whole remaining seconds; negative clamps to zero.
|
|
138
|
-
* @returns the compact duration.
|
|
139
|
-
*/
|
|
140
|
-
const compactDuration = (seconds) => {
|
|
142
|
+
/** Compact durations, with units from the active language dictionary. */
|
|
143
|
+
const compactDuration = (seconds, t) => {
|
|
141
144
|
const safe = Math.max(0, seconds)
|
|
142
145
|
const days = Math.floor(safe / 86400)
|
|
143
146
|
const hours = Math.floor((safe % 86400) / 3600)
|
|
144
147
|
const minutes = Math.floor((safe % 3600) / 60)
|
|
145
148
|
const rest = safe % 60
|
|
146
|
-
if (days > 0) return days + '
|
|
147
|
-
if (hours > 0) return hours + '
|
|
148
|
-
if (minutes > 0) return minutes + '
|
|
149
|
-
return rest + '
|
|
149
|
+
if (days > 0) return days + t('unitDay') + (hours > 0 ? hours + t('unitHour') : '')
|
|
150
|
+
if (hours > 0) return hours + t('unitHour') + (minutes > 0 ? minutes + t('unitMinute') : '')
|
|
151
|
+
if (minutes > 0) return minutes + t('unitMinute') + (rest > 0 ? rest + t('unitSecond') : '')
|
|
152
|
+
return rest + t('unitSecond')
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
* @param at - epoch milliseconds.
|
|
155
|
-
* @param now - epoch milliseconds.
|
|
156
|
-
* @returns '4h22m后'-style, '即将' when due, or '' when unknown.
|
|
157
|
-
*/
|
|
158
|
-
const countdownFrom = (at, now) => {
|
|
155
|
+
/** Localize the entire countdown, rather than appending Chinese suffixes. */
|
|
156
|
+
const countdownFrom = (at, now, t, kind) => {
|
|
159
157
|
if (!at) return ''
|
|
160
158
|
const seconds = Math.max(0, Math.round((at - now) / 1000))
|
|
161
|
-
|
|
162
|
-
return compactDuration(seconds) + '后'
|
|
159
|
+
return seconds === 0 ? t(kind + 'Soon') : t(kind + 'In', { duration: compactDuration(seconds, t) })
|
|
163
160
|
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Name what a countdown counts down to: '4h22m后' + '重置' reads
|
|
167
|
-
* '4h22m后重置', and '即将' becomes '即将重置'.
|
|
168
|
-
* @param base - a countdownFrom result.
|
|
169
|
-
* @param noun - what happens when the countdown ends.
|
|
170
|
-
* @returns the named string, or '' untouched.
|
|
171
|
-
*/
|
|
172
|
-
const withNoun = (base, noun) =>
|
|
173
|
-
base === '' ? '' : base === '即将' ? '即将' + noun : base.replace(/后$/, '后' + noun)
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Render a reset time as an exact countdown.
|
|
177
|
-
* @param resetAt - epoch seconds.
|
|
178
|
-
* @param now - epoch milliseconds.
|
|
179
|
-
* @returns '4h22m后重置', or an empty string when unknown.
|
|
180
|
-
*/
|
|
181
|
-
const countdown = (resetAt, now) => withNoun(countdownFrom(resetAt * 1000, now), '重置')
|
|
161
|
+
const countdown = (resetAt, now, t) => countdownFrom(resetAt * 1000, now, t, 'reset')
|
|
182
162
|
|
|
183
163
|
// The latest quota reading, plus the subscribers waiting on it.
|
|
184
164
|
let state = { phase: 'loading' }
|
|
@@ -217,10 +197,10 @@ window.__ModuleLoader__.load({
|
|
|
217
197
|
polling ??= (async () => {
|
|
218
198
|
try {
|
|
219
199
|
const response = await fetch(ENDPOINT + (force ? '?refresh=1' : ''), { cache: 'no-store' })
|
|
220
|
-
if (!response.ok) throw
|
|
200
|
+
if (!response.ok) throw endpointError(response.status)
|
|
221
201
|
publish(await response.json())
|
|
222
202
|
} catch (error) {
|
|
223
|
-
publish({ phase: 'error', message: error
|
|
203
|
+
publish({ phase: 'error', message: error })
|
|
224
204
|
}
|
|
225
205
|
})().finally(() => {
|
|
226
206
|
polling = undefined
|
|
@@ -236,10 +216,10 @@ window.__ModuleLoader__.load({
|
|
|
236
216
|
statusPolling ??= (async () => {
|
|
237
217
|
try {
|
|
238
218
|
const response = await fetch(STATUS_ENDPOINT, { cache: 'no-store' })
|
|
239
|
-
if (!response.ok) throw
|
|
219
|
+
if (!response.ok) throw endpointError(response.status)
|
|
240
220
|
publishStatus(await response.json())
|
|
241
221
|
} catch (error) {
|
|
242
|
-
publishStatus({ error
|
|
222
|
+
publishStatus({ error })
|
|
243
223
|
}
|
|
244
224
|
})().finally(() => {
|
|
245
225
|
statusPolling = undefined
|
|
@@ -260,65 +240,195 @@ window.__ModuleLoader__.load({
|
|
|
260
240
|
body: JSON.stringify(body),
|
|
261
241
|
})
|
|
262
242
|
const payload = await response.json().catch(() => ({}))
|
|
263
|
-
if (!response.ok) throw new Error(payload.message
|
|
243
|
+
if (!response.ok) throw payload.message ? new Error(payload.message) : endpointError(response.status)
|
|
264
244
|
return payload
|
|
265
245
|
}
|
|
266
246
|
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
247
|
+
// Same namespace / dictionary / slot-locale contract as dsh-mcp-adapter.
|
|
248
|
+
const NS = 'settings.gpt-sub-quota'
|
|
249
|
+
const en = {
|
|
250
|
+
nav: "Codex",
|
|
251
|
+
authSource: "Auth source",
|
|
252
|
+
nextRefresh: "Automatic refresh",
|
|
253
|
+
proxy: "HTTP proxy",
|
|
254
|
+
direct: "Direct connection",
|
|
255
|
+
test: "Test connection",
|
|
256
|
+
save: "Save",
|
|
257
|
+
saved: "Saved and applied; this proxy will still be used after restart",
|
|
258
|
+
testing: "Testing…",
|
|
259
|
+
saving: "Saving…",
|
|
260
|
+
marginNote: "Refresh the token when less than {margin} minutes remain; sync every {sync} minutes",
|
|
261
|
+
unknown: "Unknown (token has no valid exp)",
|
|
262
|
+
configOnly: "From configuration",
|
|
263
|
+
overridden: "Panel override",
|
|
264
|
+
ok: "Connected · {ms} ms",
|
|
265
|
+
fail: "Connection failed",
|
|
266
|
+
proxyPlaceholder: "http://127.0.0.1:7890; leave empty for a direct connection",
|
|
267
|
+
authFileLabel: "Auth file",
|
|
268
|
+
authPlaceholder: "~/.codex/auth.json",
|
|
269
|
+
authSaved: "Saved and applied; this path will still be used after restart",
|
|
270
|
+
authResetHint: "Clear and save to restore the configured default",
|
|
271
|
+
authUnpublished: "Switched, but token publishing failed; the next sync will retry automatically",
|
|
272
|
+
browse: "Browse…",
|
|
273
|
+
browseTitle: "Select an auth file",
|
|
274
|
+
cancel: "Cancel",
|
|
275
|
+
upLabel: "..",
|
|
276
|
+
truncatedNote: "Too many entries; only some are shown",
|
|
277
|
+
browseFail: "Cannot read this directory",
|
|
278
|
+
noFiveHour: "This account has no 5-hour limit; only the weekly limit applies",
|
|
279
|
+
noWindows: "The upstream service reported no rate-limit windows",
|
|
280
|
+
resetsLeft: "Reset credits remaining: {count}",
|
|
281
|
+
resetCredits: "Reset credits",
|
|
282
|
+
rcLoad: "Check reset credits",
|
|
283
|
+
rcLoading: "Checking…",
|
|
284
|
+
rcAvailable: "Available: {n}",
|
|
285
|
+
rcEmpty: "No reset credits are available",
|
|
286
|
+
rcUse: "Use and reset",
|
|
287
|
+
rcUsing: "Using…",
|
|
288
|
+
rcConfirm: "Use this reset credit? This will immediately reset the quota window: {title}",
|
|
289
|
+
unnamed: "Unnamed",
|
|
290
|
+
rcConsumeFail: "Use failed: {message}",
|
|
291
|
+
rcOutcomeReset: "✓ Reset complete (windows: {n})",
|
|
292
|
+
rcOutcomeNothing: "✗ No windows can be reset right now",
|
|
293
|
+
rcOutcomeNoCredit: "✗ No reset credits are available",
|
|
294
|
+
rcOutcomeRedeemed: "✗ This credit has already been used",
|
|
295
|
+
rcOutcomeUnknown: "Unexpected reset result: {code}",
|
|
296
|
+
rcStatusAvailable: "Available",
|
|
297
|
+
rcStatusRedeeming: "Redeeming",
|
|
298
|
+
rcStatusRedeemed: "Used",
|
|
299
|
+
rcStatusUnknown: "Unknown",
|
|
300
|
+
rcExpires: " (expires {text})",
|
|
301
|
+
refresh: "Refresh",
|
|
302
|
+
loading: "Loading…",
|
|
303
|
+
quotaError: "Cannot read quota: {message}",
|
|
304
|
+
unknownError: "Unknown error",
|
|
305
|
+
retry: "Retry",
|
|
306
|
+
windowDays: "{n}-day window",
|
|
307
|
+
windowHours: "{n}-hour window",
|
|
308
|
+
remaining: "{percent}% remaining",
|
|
309
|
+
stale: "Data may be stale: {message}",
|
|
310
|
+
accessToken: "Access token",
|
|
311
|
+
resetSoon: "Resets soon",
|
|
312
|
+
resetIn: "Resets in {duration}",
|
|
313
|
+
refreshSoon: "Refreshing soon",
|
|
314
|
+
refreshIn: "Refreshes in {duration}",
|
|
315
|
+
expireSoon: "Expires soon",
|
|
316
|
+
expireIn: "Expires in {duration}",
|
|
317
|
+
unitDay: "d",
|
|
318
|
+
unitHour: "h",
|
|
319
|
+
unitMinute: "m",
|
|
320
|
+
unitSecond: "s",
|
|
321
|
+
dateLocale: "en-US",
|
|
322
|
+
endpointError: "Endpoint returned HTTP {status}",
|
|
323
|
+
}
|
|
324
|
+
const zh = {
|
|
325
|
+
nav: "Codex",
|
|
326
|
+
authSource: "认证来源",
|
|
327
|
+
nextRefresh: "自动刷新",
|
|
328
|
+
proxy: "HTTP 代理",
|
|
329
|
+
direct: "直连",
|
|
330
|
+
test: "测试连接",
|
|
331
|
+
save: "保存",
|
|
332
|
+
saved: "已保存并生效,重启后仍使用该代理",
|
|
333
|
+
testing: "测试中…",
|
|
334
|
+
saving: "保存中…",
|
|
335
|
+
marginNote: "Token 剩余不足 {margin} 分钟时自动刷新;每 {sync} 分钟定期同步",
|
|
336
|
+
unknown: "未知(Token 无有效 exp)",
|
|
337
|
+
configOnly: "来自配置",
|
|
338
|
+
overridden: "页面设置",
|
|
339
|
+
ok: "连接正常 · {ms} 毫秒",
|
|
340
|
+
fail: "连接失败",
|
|
341
|
+
proxyPlaceholder: "http://127.0.0.1:7890,留空表示直连",
|
|
342
|
+
authFileLabel: "认证文件",
|
|
343
|
+
authPlaceholder: "~/.codex/auth.json",
|
|
344
|
+
authSaved: "已保存并生效,重启后仍使用该路径",
|
|
345
|
+
authResetHint: "清空并保存恢复配置默认",
|
|
346
|
+
authUnpublished: "已切换,但 Token 发布失败,将在下个同步周期自动重试",
|
|
347
|
+
browse: "浏览…",
|
|
348
|
+
browseTitle: "选择认证文件",
|
|
349
|
+
cancel: "取消",
|
|
350
|
+
upLabel: "..",
|
|
351
|
+
truncatedNote: "条目过多,仅显示部分",
|
|
352
|
+
browseFail: "无法读取目录",
|
|
353
|
+
noFiveHour: "当前账号无 5 小时限额,仅按周限额计",
|
|
354
|
+
noWindows: "上游未报告任何限额窗口",
|
|
355
|
+
resetsLeft: "还有 {count} 次重置",
|
|
356
|
+
resetCredits: "重置次数",
|
|
357
|
+
rcLoad: "查询重置次数",
|
|
358
|
+
rcLoading: "查询中…",
|
|
359
|
+
rcAvailable: "可用 {n} 次",
|
|
360
|
+
rcEmpty: "当前没有可用的重置次数",
|
|
361
|
+
rcUse: "使用并重置",
|
|
362
|
+
rcUsing: "使用中…",
|
|
363
|
+
rcConfirm: "确定使用这个重置次数吗?将立即重置配额窗口:{title}",
|
|
364
|
+
unnamed: "未命名",
|
|
365
|
+
rcConsumeFail: "使用失败:{message}",
|
|
366
|
+
rcOutcomeReset: "✓ 已重置({n} 个窗口)",
|
|
367
|
+
rcOutcomeNothing: "✗ 当前没有可重置的窗口",
|
|
368
|
+
rcOutcomeNoCredit: "✗ 没有可用的重置次数",
|
|
369
|
+
rcOutcomeRedeemed: "✗ 该次数已被使用过",
|
|
370
|
+
rcOutcomeUnknown: "未知重置结果:{code}",
|
|
371
|
+
rcStatusAvailable: "可用",
|
|
372
|
+
rcStatusRedeeming: "兑换中",
|
|
373
|
+
rcStatusRedeemed: "已使用",
|
|
374
|
+
rcStatusUnknown: "未知",
|
|
375
|
+
rcExpires: "({text} 前有效)",
|
|
376
|
+
refresh: "刷新",
|
|
377
|
+
loading: "读取中…",
|
|
378
|
+
quotaError: "无法读取配额:{message}",
|
|
379
|
+
unknownError: "未知错误",
|
|
380
|
+
retry: "重试",
|
|
381
|
+
windowDays: "{n} 天窗口",
|
|
382
|
+
windowHours: "{n} 小时窗口",
|
|
383
|
+
remaining: "剩余 {percent}%",
|
|
384
|
+
stale: "数据可能已过期:{message}",
|
|
385
|
+
accessToken: "访问令牌",
|
|
386
|
+
resetSoon: "即将重置",
|
|
387
|
+
resetIn: "{duration}后重置",
|
|
388
|
+
refreshSoon: "即将刷新",
|
|
389
|
+
refreshIn: "{duration}后刷新",
|
|
390
|
+
expireSoon: "即将过期",
|
|
391
|
+
expireIn: "{duration}后过期",
|
|
392
|
+
unitDay: "天",
|
|
393
|
+
unitHour: "小时",
|
|
394
|
+
unitMinute: "分",
|
|
395
|
+
unitSecond: "秒",
|
|
396
|
+
dateLocale: "zh-CN",
|
|
397
|
+
endpointError: "接口返回 HTTP {status}",
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const interpolate = (template, params = {}) =>
|
|
401
|
+
template.replace(/\{(\w+)\}/g, (match, key) =>
|
|
402
|
+
Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : match)
|
|
403
|
+
const fallbackT = (key, params) => interpolate(en[key] ?? key, params)
|
|
404
|
+
|
|
405
|
+
// Keep HTTP failures and action notes as data, not translated snapshots.
|
|
406
|
+
// A language switch also updates a success/error note already on screen.
|
|
407
|
+
const endpointError = (status) => Object.assign(new Error(), { endpointStatus: status })
|
|
408
|
+
const errorText = (error, t) => {
|
|
409
|
+
if (error && error.endpointStatus !== undefined) return t('endpointError', { status: error.endpointStatus })
|
|
410
|
+
if (error && error.key) return t(error.key)
|
|
411
|
+
return (error && error.message) || (error ? String(error) : t('unknownError'))
|
|
412
|
+
}
|
|
413
|
+
const noteText = (note, t) => {
|
|
414
|
+
if (!note) return ''
|
|
415
|
+
if (note.key) return t(note.key, { ...note.params, ...(note.error ? { message: errorText(note.error, t) } : {}) })
|
|
416
|
+
return '✗ ' + errorText(note.error, t)
|
|
417
|
+
}
|
|
418
|
+
const resetOutcomeKeys = {
|
|
419
|
+
reset: 'rcOutcomeReset', nothing_to_reset: 'rcOutcomeNothing',
|
|
420
|
+
no_credit: 'rcOutcomeNoCredit', already_redeemed: 'rcOutcomeRedeemed',
|
|
421
|
+
}
|
|
422
|
+
const creditStatusKeys = {
|
|
423
|
+
available: 'rcStatusAvailable', redeeming: 'rcStatusRedeeming',
|
|
424
|
+
redeemed: 'rcStatusRedeemed', unknown: 'rcStatusUnknown',
|
|
315
425
|
}
|
|
316
426
|
|
|
317
427
|
/**
|
|
318
428
|
* The proxy control block: current URL, editable, with test and save.
|
|
319
429
|
* @returns the rendered block.
|
|
320
430
|
*/
|
|
321
|
-
const ProxyControl = () => {
|
|
431
|
+
const ProxyControl = ({ t }) => {
|
|
322
432
|
const current = status && status.proxyUrl !== undefined ? status.proxyUrl : ''
|
|
323
433
|
const [draft, setDraft] = useState(current)
|
|
324
434
|
const [dirty, setDirty] = useState(false)
|
|
@@ -342,9 +452,9 @@ window.__ModuleLoader__.load({
|
|
|
342
452
|
try {
|
|
343
453
|
const probed = await postJson(PROXY_TEST_ENDPOINT, { proxyUrl: draft.trim() })
|
|
344
454
|
setResult(probed)
|
|
345
|
-
if (!probed.ok) setNote(
|
|
455
|
+
if (!probed.ok) setNote({ error: probed.message || { key: 'fail' } })
|
|
346
456
|
} catch (error) {
|
|
347
|
-
setNote(
|
|
457
|
+
setNote({ error })
|
|
348
458
|
} finally {
|
|
349
459
|
setBusy('')
|
|
350
460
|
}
|
|
@@ -361,10 +471,10 @@ window.__ModuleLoader__.load({
|
|
|
361
471
|
try {
|
|
362
472
|
await postJson(PROXY_ENDPOINT, { proxyUrl: draft.trim() })
|
|
363
473
|
setDirty(false)
|
|
364
|
-
setNote(
|
|
474
|
+
setNote({ key: 'saved' })
|
|
365
475
|
await Promise.all([pollStatus(), poll(false)])
|
|
366
476
|
} catch (error) {
|
|
367
|
-
setNote(
|
|
477
|
+
setNote({ error })
|
|
368
478
|
} finally {
|
|
369
479
|
setBusy('')
|
|
370
480
|
}
|
|
@@ -372,16 +482,17 @@ window.__ModuleLoader__.load({
|
|
|
372
482
|
|
|
373
483
|
const okPill =
|
|
374
484
|
result && result.ok
|
|
375
|
-
? React.createElement(Badge, null, '✓ ' +
|
|
485
|
+
? React.createElement(Badge, null, '✓ ' + t('ok', { ms: result.latencyMs }))
|
|
376
486
|
: null
|
|
377
487
|
const failPill =
|
|
378
488
|
result && !result.ok
|
|
379
|
-
? React.createElement(Badge, null, '✗ ' +
|
|
489
|
+
? React.createElement(Badge, null, '✗ ' + t('fail'))
|
|
380
490
|
: null
|
|
381
491
|
|
|
382
492
|
const inputProps = {
|
|
383
493
|
value: draft,
|
|
384
|
-
placeholder: '
|
|
494
|
+
placeholder: t('proxyPlaceholder'),
|
|
495
|
+
'aria-label': t('proxy'),
|
|
385
496
|
spellCheck: false,
|
|
386
497
|
disabled: busy !== '',
|
|
387
498
|
onChange: (event) => {
|
|
@@ -406,23 +517,23 @@ window.__ModuleLoader__.load({
|
|
|
406
517
|
React.createElement(
|
|
407
518
|
SmallButton,
|
|
408
519
|
{ variant: 'outline', disabled: busy !== '', onClick: onTest },
|
|
409
|
-
busy === 'test' ?
|
|
520
|
+
busy === 'test' ? t('testing') : t('test'),
|
|
410
521
|
),
|
|
411
522
|
React.createElement(
|
|
412
523
|
SmallButton,
|
|
413
524
|
{ variant: 'primary', disabled: busy !== '', onClick: onSave },
|
|
414
|
-
busy === 'save' ?
|
|
525
|
+
busy === 'save' ? t('saving') : t('save'),
|
|
415
526
|
),
|
|
416
527
|
),
|
|
417
528
|
React.createElement(
|
|
418
529
|
'div',
|
|
419
530
|
{ className: 'dshGptSubProxyMeta' },
|
|
420
|
-
React.createElement(Badge, null, current === '' ?
|
|
421
|
-
React.createElement(Badge, null, status && status.overridden ?
|
|
531
|
+
React.createElement(Badge, null, current === '' ? t('direct') : current),
|
|
532
|
+
React.createElement(Badge, null, status && status.overridden ? t('overridden') : t('configOnly')),
|
|
422
533
|
okPill,
|
|
423
534
|
failPill,
|
|
424
535
|
),
|
|
425
|
-
note ? React.createElement('div', { className: 'dshGptSubNote' }, note) : null,
|
|
536
|
+
note ? React.createElement('div', { className: 'dshGptSubNote' }, noteText(note, t)) : null,
|
|
426
537
|
)
|
|
427
538
|
}
|
|
428
539
|
|
|
@@ -435,7 +546,7 @@ window.__ModuleLoader__.load({
|
|
|
435
546
|
* picker, which is why browsing runs through the host instead.
|
|
436
547
|
* @returns the rendered block.
|
|
437
548
|
*/
|
|
438
|
-
const AuthControl = () => {
|
|
549
|
+
const AuthControl = ({ t }) => {
|
|
439
550
|
const current = status && status.authFile !== undefined ? status.authFile : ''
|
|
440
551
|
const [draft, setDraft] = useState(current)
|
|
441
552
|
const [dirty, setDirty] = useState(false)
|
|
@@ -458,10 +569,10 @@ window.__ModuleLoader__.load({
|
|
|
458
569
|
try {
|
|
459
570
|
const payload = await postJson(AUTH_ENDPOINT, { authFile: draft.trim() })
|
|
460
571
|
setDirty(false)
|
|
461
|
-
setNote(payload.published === false ?
|
|
572
|
+
setNote({ key: payload.published === false ? 'authUnpublished' : 'authSaved' })
|
|
462
573
|
await pollStatus()
|
|
463
574
|
} catch (error) {
|
|
464
|
-
setNote(
|
|
575
|
+
setNote({ error })
|
|
465
576
|
} finally {
|
|
466
577
|
setBusy(false)
|
|
467
578
|
}
|
|
@@ -478,10 +589,10 @@ window.__ModuleLoader__.load({
|
|
|
478
589
|
const query = path ? '?path=' + encodeURIComponent(path) : ''
|
|
479
590
|
const response = await fetch(AUTH_BROWSE_ENDPOINT + query, { cache: 'no-store' })
|
|
480
591
|
const payload = await response.json().catch(() => ({}))
|
|
481
|
-
if (!response.ok || !payload.ok) throw new Error(payload.message
|
|
592
|
+
if (!response.ok || !payload.ok) throw payload.message ? new Error(payload.message) : { key: 'browseFail' }
|
|
482
593
|
setBrowse(payload)
|
|
483
594
|
} catch (error) {
|
|
484
|
-
setNote(
|
|
595
|
+
setNote({ error })
|
|
485
596
|
}
|
|
486
597
|
}
|
|
487
598
|
|
|
@@ -498,7 +609,8 @@ window.__ModuleLoader__.load({
|
|
|
498
609
|
|
|
499
610
|
const inputProps = {
|
|
500
611
|
value: draft,
|
|
501
|
-
placeholder:
|
|
612
|
+
placeholder: t('authPlaceholder'),
|
|
613
|
+
'aria-label': t('authFileLabel'),
|
|
502
614
|
spellCheck: false,
|
|
503
615
|
disabled: busy,
|
|
504
616
|
onChange: (event) => {
|
|
@@ -526,21 +638,21 @@ window.__ModuleLoader__.load({
|
|
|
526
638
|
React.createElement(
|
|
527
639
|
SmallButton,
|
|
528
640
|
{ variant: 'outline', disabled: busy, onClick: () => void openBrowserAt('') },
|
|
529
|
-
|
|
641
|
+
t('browse'),
|
|
530
642
|
),
|
|
531
643
|
React.createElement(
|
|
532
644
|
SmallButton,
|
|
533
645
|
{ variant: 'primary', disabled: busy, onClick: onSave },
|
|
534
|
-
busy ?
|
|
646
|
+
busy ? t('saving') : t('save'),
|
|
535
647
|
),
|
|
536
648
|
),
|
|
537
649
|
React.createElement(
|
|
538
650
|
'div',
|
|
539
651
|
{ className: 'dshGptSubProxyMeta' },
|
|
540
|
-
React.createElement(Badge, null, status && status.authOverridden ?
|
|
541
|
-
React.createElement('span', null,
|
|
652
|
+
React.createElement(Badge, null, status && status.authOverridden ? t('overridden') : t('configOnly')),
|
|
653
|
+
React.createElement('span', null, t('authResetHint')),
|
|
542
654
|
),
|
|
543
|
-
note ? React.createElement('div', { className: 'dshGptSubNote' }, note) : null,
|
|
655
|
+
note ? React.createElement('div', { className: 'dshGptSubNote' }, noteText(note, t)) : null,
|
|
544
656
|
),
|
|
545
657
|
browse === undefined
|
|
546
658
|
? null
|
|
@@ -550,7 +662,7 @@ window.__ModuleLoader__.load({
|
|
|
550
662
|
React.createElement(
|
|
551
663
|
'div',
|
|
552
664
|
{ className: 'dshGptSubBrowse', onClick: (event) => event.stopPropagation() },
|
|
553
|
-
React.createElement('div', { className: 'dshGptSubBrowseHead' },
|
|
665
|
+
React.createElement('div', { className: 'dshGptSubBrowseHead' }, t('browseTitle')),
|
|
554
666
|
React.createElement('div', { className: 'dshGptSubBrowsePath' }, browse.dir),
|
|
555
667
|
React.createElement(
|
|
556
668
|
'ul',
|
|
@@ -564,7 +676,7 @@ window.__ModuleLoader__.load({
|
|
|
564
676
|
onClick: () => void openBrowserAt(browse.parent),
|
|
565
677
|
},
|
|
566
678
|
React.createElement('span', { className: 'dshGptSubBrowseMark' }, '↰'),
|
|
567
|
-
|
|
679
|
+
t('upLabel'),
|
|
568
680
|
)
|
|
569
681
|
: null,
|
|
570
682
|
browse.entries.map((entry) =>
|
|
@@ -585,12 +697,12 @@ window.__ModuleLoader__.load({
|
|
|
585
697
|
'div',
|
|
586
698
|
{ className: 'dshGptSubBrowseFoot' },
|
|
587
699
|
browse.truncated
|
|
588
|
-
? React.createElement('span', { className: 'dshGptSubNote' },
|
|
700
|
+
? React.createElement('span', { className: 'dshGptSubNote' }, t('truncatedNote'))
|
|
589
701
|
: null,
|
|
590
702
|
React.createElement(
|
|
591
703
|
SmallButton,
|
|
592
704
|
{ variant: 'outline', onClick: () => setBrowse(undefined) },
|
|
593
|
-
|
|
705
|
+
t('cancel'),
|
|
594
706
|
),
|
|
595
707
|
),
|
|
596
708
|
),
|
|
@@ -604,7 +716,7 @@ window.__ModuleLoader__.load({
|
|
|
604
716
|
* Consuming is destructive, so nothing here fires without a click.
|
|
605
717
|
* @returns the rendered block.
|
|
606
718
|
*/
|
|
607
|
-
const ResetCreditsControl = () => {
|
|
719
|
+
const ResetCreditsControl = ({ t }) => {
|
|
608
720
|
const [details, setDetails] = useState(undefined)
|
|
609
721
|
const [busy, setBusy] = useState(false)
|
|
610
722
|
const [consumingId, setConsumingId] = useState('')
|
|
@@ -614,25 +726,23 @@ window.__ModuleLoader__.load({
|
|
|
614
726
|
* Fetch the credit list from the host.
|
|
615
727
|
* @returns nothing; failures surface as the note.
|
|
616
728
|
*/
|
|
617
|
-
const load = async () => {
|
|
729
|
+
const load = async (preserveNote = false) => {
|
|
618
730
|
setBusy(true)
|
|
619
|
-
setNote('')
|
|
731
|
+
if (!preserveNote) setNote('')
|
|
620
732
|
try {
|
|
621
733
|
const response = await fetch(RESET_CREDITS_ENDPOINT, { cache: 'no-store' })
|
|
622
734
|
const payload = await response.json().catch(() => ({}))
|
|
623
|
-
if (!response.ok || !payload.ok) throw new Error(payload.message
|
|
735
|
+
if (!response.ok || !payload.ok) throw payload.message ? new Error(payload.message) : endpointError(response.status)
|
|
624
736
|
setDetails(payload)
|
|
625
737
|
} catch (error) {
|
|
626
|
-
setNote(
|
|
738
|
+
setNote({ error })
|
|
627
739
|
} finally {
|
|
628
740
|
setBusy(false)
|
|
629
741
|
}
|
|
630
742
|
}
|
|
631
743
|
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
void load()
|
|
635
|
-
}, [])
|
|
744
|
+
// Never query on mount, locale changes, or quota polling. The first
|
|
745
|
+
// read is user-triggered; later reads come from Refresh or consumption.
|
|
636
746
|
|
|
637
747
|
/**
|
|
638
748
|
* Consume one credit after a confirmation dialog, then refresh both the
|
|
@@ -642,18 +752,18 @@ window.__ModuleLoader__.load({
|
|
|
642
752
|
*/
|
|
643
753
|
const consume = async (credit) => {
|
|
644
754
|
const title = credit ? credit.title || credit.id : ''
|
|
645
|
-
if (!window.confirm(
|
|
755
|
+
if (!window.confirm(t('rcConfirm', { title: title || t('unnamed') }))) return
|
|
646
756
|
setConsumingId(credit ? credit.id : '*')
|
|
647
757
|
setNote('')
|
|
648
758
|
try {
|
|
649
759
|
const payload = await postJson(RESET_CREDITS_CONSUME_ENDPOINT, {
|
|
650
760
|
...(credit ? { creditId: credit.id } : {}),
|
|
651
761
|
})
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
await Promise.all([load(), poll(false)])
|
|
762
|
+
setNote({ key: resetOutcomeKeys[payload.code] || 'rcOutcomeUnknown',
|
|
763
|
+
params: { n: payload.windows_reset || 0, code: payload.code } })
|
|
764
|
+
await Promise.all([load(true), poll(false)])
|
|
655
765
|
} catch (error) {
|
|
656
|
-
setNote(
|
|
766
|
+
setNote({ key: 'rcConsumeFail', error })
|
|
657
767
|
} finally {
|
|
658
768
|
setConsumingId('')
|
|
659
769
|
}
|
|
@@ -666,9 +776,9 @@ window.__ModuleLoader__.load({
|
|
|
666
776
|
React.createElement(
|
|
667
777
|
SmallButton,
|
|
668
778
|
{ variant: 'outline', disabled: busy, onClick: () => void load() },
|
|
669
|
-
busy ?
|
|
779
|
+
busy ? t('rcLoading') : t('rcLoad'),
|
|
670
780
|
),
|
|
671
|
-
note ? React.createElement('span', null, note) : null,
|
|
781
|
+
note ? React.createElement('span', null, noteText(note, t)) : null,
|
|
672
782
|
)
|
|
673
783
|
}
|
|
674
784
|
|
|
@@ -679,31 +789,37 @@ window.__ModuleLoader__.load({
|
|
|
679
789
|
React.createElement(
|
|
680
790
|
'div',
|
|
681
791
|
{ className: 'dshGptSubProxyMeta' },
|
|
682
|
-
React.createElement(Badge, null,
|
|
792
|
+
React.createElement(Badge, null, t('rcAvailable', { n: details.available_count })),
|
|
683
793
|
React.createElement(
|
|
684
794
|
SmallButton,
|
|
685
795
|
{ variant: 'ghost', disabled: busy, onClick: () => void load() },
|
|
686
|
-
busy ?
|
|
796
|
+
busy ? t('rcLoading') : t('refresh'),
|
|
687
797
|
),
|
|
688
798
|
),
|
|
689
799
|
available.length === 0
|
|
690
|
-
? React.createElement('div', { className: 'dshGptSubNote' },
|
|
800
|
+
? React.createElement('div', { className: 'dshGptSubNote' }, t('rcEmpty'))
|
|
691
801
|
: React.createElement(
|
|
692
802
|
'div',
|
|
693
|
-
|
|
803
|
+
{ className: 'dshGptSubCreditList' },
|
|
694
804
|
available.map((credit) =>
|
|
695
805
|
React.createElement(
|
|
696
806
|
'div',
|
|
697
|
-
{ key: credit.id, className: '
|
|
807
|
+
{ key: credit.id, className: 'dshGptSubCreditRow' },
|
|
698
808
|
React.createElement(
|
|
699
|
-
'
|
|
700
|
-
|
|
701
|
-
(credit.title || credit.id)
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
:
|
|
809
|
+
'div',
|
|
810
|
+
{ className: 'dshGptSubCreditText' },
|
|
811
|
+
React.createElement('div', { className: 'dshGptSubCreditTitle' }, credit.title || credit.id),
|
|
812
|
+
React.createElement(
|
|
813
|
+
'div',
|
|
814
|
+
{ className: 'dshGptSubCreditMeta' },
|
|
815
|
+
React.createElement('span', null,
|
|
816
|
+
creditStatusKeys[credit.status] ? t(creditStatusKeys[credit.status]) : credit.status),
|
|
817
|
+
credit.expires_at
|
|
818
|
+
? React.createElement('span', null, t('rcExpires', {
|
|
819
|
+
text: new Date(credit.expires_at).toLocaleString(t('dateLocale')),
|
|
820
|
+
}))
|
|
821
|
+
: null,
|
|
822
|
+
),
|
|
707
823
|
),
|
|
708
824
|
React.createElement(
|
|
709
825
|
SmallButton,
|
|
@@ -712,12 +828,12 @@ window.__ModuleLoader__.load({
|
|
|
712
828
|
disabled: busy || consumingId !== '',
|
|
713
829
|
onClick: () => void consume(credit),
|
|
714
830
|
},
|
|
715
|
-
consumingId === credit.id ?
|
|
831
|
+
consumingId === credit.id ? t('rcUsing') : t('rcUse'),
|
|
716
832
|
),
|
|
717
833
|
),
|
|
718
834
|
),
|
|
719
835
|
),
|
|
720
|
-
note ? React.createElement('div', { className: 'dshGptSubNote' }, note) : null,
|
|
836
|
+
note ? React.createElement('div', { className: 'dshGptSubNote' }, noteText(note, t)) : null,
|
|
721
837
|
)
|
|
722
838
|
}
|
|
723
839
|
|
|
@@ -725,7 +841,7 @@ window.__ModuleLoader__.load({
|
|
|
725
841
|
* The Settings section body.
|
|
726
842
|
* @returns the rendered panel.
|
|
727
843
|
*/
|
|
728
|
-
const QuotaSection = () => {
|
|
844
|
+
const QuotaSection = ({ t = fallbackT } = {}) => {
|
|
729
845
|
ensureStyle()
|
|
730
846
|
const [reading, setReading] = useState(state)
|
|
731
847
|
const [stat, setStat] = useState(status)
|
|
@@ -743,17 +859,17 @@ window.__ModuleLoader__.load({
|
|
|
743
859
|
}, [])
|
|
744
860
|
|
|
745
861
|
if (reading.phase === 'loading') {
|
|
746
|
-
return React.createElement('div', { className: 'dshGptSub' }, '
|
|
862
|
+
return React.createElement('div', { className: 'dshGptSub' }, t('loading'))
|
|
747
863
|
}
|
|
748
864
|
if (reading.phase === 'error') {
|
|
749
865
|
return React.createElement(
|
|
750
866
|
'div',
|
|
751
867
|
{ className: 'dshGptSub' },
|
|
752
|
-
React.createElement('div', { className: 'dshGptSubNote' }, '
|
|
868
|
+
React.createElement('div', { className: 'dshGptSubNote' }, t('quotaError', { message: errorText(reading.message, t) })),
|
|
753
869
|
React.createElement(
|
|
754
870
|
'div',
|
|
755
871
|
{ className: 'dshGptSubBtn' },
|
|
756
|
-
React.createElement(SmallButton, { variant: 'outline', onClick: () => poll(true) }, '
|
|
872
|
+
React.createElement(SmallButton, { variant: 'outline', onClick: () => poll(true) }, t('retry')),
|
|
757
873
|
),
|
|
758
874
|
)
|
|
759
875
|
}
|
|
@@ -763,7 +879,8 @@ window.__ModuleLoader__.load({
|
|
|
763
879
|
* @param hours - the window's length in hours.
|
|
764
880
|
* @returns the label.
|
|
765
881
|
*/
|
|
766
|
-
const windowLabel = (hours) => (hours >= 24 ?
|
|
882
|
+
const windowLabel = (hours) => t(hours >= 24 ? 'windowDays' : 'windowHours',
|
|
883
|
+
{ n: hours >= 24 ? Math.round(hours / 24) : hours })
|
|
767
884
|
|
|
768
885
|
/**
|
|
769
886
|
* One window's block: label, a track of the percent REMAINING, and the
|
|
@@ -791,19 +908,19 @@ window.__ModuleLoader__.load({
|
|
|
791
908
|
React.createElement(
|
|
792
909
|
'div',
|
|
793
910
|
{ className: 'dshGptSubMeta' },
|
|
794
|
-
React.createElement('span', null, '
|
|
795
|
-
React.createElement('span', null, countdown(window.resetAt, now)),
|
|
911
|
+
React.createElement('span', null, t('remaining', { percent: remaining })),
|
|
912
|
+
React.createElement('span', null, countdown(window.resetAt, now, t)),
|
|
796
913
|
),
|
|
797
914
|
)
|
|
798
915
|
}
|
|
799
916
|
|
|
800
917
|
const refreshLabel = stat
|
|
801
918
|
? stat.refreshAt
|
|
802
|
-
? countdownFrom(stat.refreshAt, now) + '
|
|
803
|
-
:
|
|
919
|
+
? countdownFrom(stat.refreshAt, now, t, 'refresh') + ' · ' + t('marginNote', { margin: stat.refreshMarginMinutes, sync: stat.syncIntervalMinutes })
|
|
920
|
+
: t('unknown')
|
|
804
921
|
: ''
|
|
805
922
|
const authLabel = stat ? stat.authFile || '' : ''
|
|
806
|
-
const tokenExpiryLabel = stat && stat.tokenExpiresAt ?
|
|
923
|
+
const tokenExpiryLabel = stat && stat.tokenExpiresAt ? countdownFrom(stat.tokenExpiresAt, now, t, 'expire') : ''
|
|
807
924
|
|
|
808
925
|
return React.createElement(
|
|
809
926
|
'div',
|
|
@@ -815,12 +932,12 @@ window.__ModuleLoader__.load({
|
|
|
815
932
|
// The on-demand usage resets the account can still spend to clear
|
|
816
933
|
// a capped window without waiting out its timer.
|
|
817
934
|
reading.resetsRemaining !== undefined
|
|
818
|
-
? React.createElement(Badge, null,
|
|
935
|
+
? React.createElement(Badge, null, t('resetsLeft', { count: reading.resetsRemaining }))
|
|
819
936
|
: null,
|
|
820
937
|
React.createElement(
|
|
821
938
|
'span',
|
|
822
939
|
{ className: 'dshGptSubBtn' },
|
|
823
|
-
React.createElement(SmallButton, { variant: 'ghost', onClick: () => poll(true) }, '
|
|
940
|
+
React.createElement(SmallButton, { variant: 'ghost', onClick: () => poll(true) }, t('refresh')),
|
|
824
941
|
),
|
|
825
942
|
),
|
|
826
943
|
// The 5-hour rolling window first -- it is the one that bites first --
|
|
@@ -829,42 +946,111 @@ window.__ModuleLoader__.load({
|
|
|
829
946
|
reading.fiveHour !== undefined
|
|
830
947
|
? WindowBlock(windowLabel(reading.fiveHour.windowHours), reading.fiveHour)
|
|
831
948
|
: reading.weekly !== undefined
|
|
832
|
-
? React.createElement('div', { className: 'dshGptSubNote' },
|
|
949
|
+
? React.createElement('div', { className: 'dshGptSubNote' }, t('noFiveHour'))
|
|
833
950
|
: null,
|
|
834
951
|
reading.weekly !== undefined ? WindowBlock(windowLabel(reading.weekly.windowHours), reading.weekly) : null,
|
|
835
952
|
reading.fiveHour === undefined && reading.weekly === undefined
|
|
836
|
-
? React.createElement('div', { className: 'dshGptSubNote' },
|
|
953
|
+
? React.createElement('div', { className: 'dshGptSubNote' }, t('noWindows'))
|
|
837
954
|
: null,
|
|
838
955
|
reading.stale
|
|
839
|
-
? React.createElement('div', { className: 'dshGptSubNote' }, '
|
|
956
|
+
? React.createElement('div', { className: 'dshGptSubNote' }, t('stale', { message: reading.message ? errorText(reading.message, t) : '' }))
|
|
840
957
|
: null,
|
|
841
958
|
React.createElement(
|
|
842
959
|
'div',
|
|
843
960
|
{ className: 'dshGptSubDiag' },
|
|
844
|
-
React.createElement('span', { className: 'dshGptSubDiagLabel' },
|
|
961
|
+
React.createElement('span', { className: 'dshGptSubDiagLabel' }, t('authSource')),
|
|
845
962
|
React.createElement(
|
|
846
963
|
'span',
|
|
847
964
|
{ className: 'dshGptSubDiagValue' },
|
|
848
|
-
authLabel + (tokenExpiryLabel ? ' ·
|
|
965
|
+
authLabel + (tokenExpiryLabel ? ' · ' + t('accessToken') + ' ' + tokenExpiryLabel : ''),
|
|
849
966
|
),
|
|
850
|
-
React.createElement('span', { className: 'dshGptSubDiagLabel' },
|
|
967
|
+
React.createElement('span', { className: 'dshGptSubDiagLabel' }, t('authFileLabel')),
|
|
851
968
|
React.createElement(
|
|
852
969
|
'span',
|
|
853
970
|
{ className: 'dshGptSubDiagValue' },
|
|
854
|
-
React.createElement(AuthControl,
|
|
971
|
+
React.createElement(AuthControl, { t }),
|
|
855
972
|
),
|
|
856
|
-
React.createElement('span', { className: 'dshGptSubDiagLabel' },
|
|
973
|
+
React.createElement('span', { className: 'dshGptSubDiagLabel' }, t('nextRefresh')),
|
|
857
974
|
React.createElement('span', { className: 'dshGptSubDiagValue' }, refreshLabel),
|
|
858
|
-
React.createElement('span', { className: 'dshGptSubDiagLabel' },
|
|
859
|
-
React.createElement('span', { className: 'dshGptSubDiagValue' }, React.createElement(ProxyControl,
|
|
860
|
-
React.createElement('span', { className: 'dshGptSubDiagLabel' },
|
|
861
|
-
React.createElement('span', { className: 'dshGptSubDiagValue' }, React.createElement(ResetCreditsControl,
|
|
975
|
+
React.createElement('span', { className: 'dshGptSubDiagLabel' }, t('proxy')),
|
|
976
|
+
React.createElement('span', { className: 'dshGptSubDiagValue' }, React.createElement(ProxyControl, { t })),
|
|
977
|
+
React.createElement('span', { className: 'dshGptSubDiagLabel' }, t('resetCredits')),
|
|
978
|
+
React.createElement('span', { className: 'dshGptSubDiagValue' }, React.createElement(ResetCreditsControl, { t })),
|
|
862
979
|
),
|
|
863
980
|
)
|
|
864
981
|
}
|
|
865
982
|
|
|
866
|
-
|
|
867
|
-
|
|
983
|
+
/* Codex mark from @lobehub/icons-static-svg 1.95.1 (MIT):
|
|
984
|
+
* https://unpkg.com/@lobehub/icons-static-svg@1.95.1/icons/codex.svg
|
|
985
|
+
* Copyright (c) 2023 LobeHub
|
|
986
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
987
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
988
|
+
* in the Software without restriction, including without limitation the rights
|
|
989
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
990
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
991
|
+
* furnished to do so, subject to the following conditions:
|
|
992
|
+
* The above copyright notice and this permission notice shall be included in
|
|
993
|
+
* all copies or substantial portions of the Software.
|
|
994
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
995
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
996
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
997
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
998
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
999
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
1000
|
+
* THE SOFTWARE.
|
|
1001
|
+
*/
|
|
1002
|
+
const CODEX_MARK = 'M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z'
|
|
1003
|
+
|
|
1004
|
+
// The settings shell currently hard-codes its nav icons and ignores a
|
|
1005
|
+
// slot's icon prop. Like dsh-mcp-adapter, replace only our row's SVG, and
|
|
1006
|
+
// restore it on unload. Unknown shell markup is deliberately a no-op.
|
|
1007
|
+
const paintNavIcon = (label) => {
|
|
1008
|
+
if (typeof document === 'undefined') return () => {}
|
|
1009
|
+
const painted = new Map()
|
|
1010
|
+
const marker = 'data-gpt-sub-nav-icon'
|
|
1011
|
+
const paint = () => {
|
|
1012
|
+
for (const cell of painted.keys()) {
|
|
1013
|
+
if (cell.isConnected === false) painted.delete(cell)
|
|
1014
|
+
}
|
|
1015
|
+
const want = label().trim()
|
|
1016
|
+
if (!want) return
|
|
1017
|
+
for (const cell of document.querySelectorAll('[role="dialog"] nav button')) {
|
|
1018
|
+
if ((cell.textContent || '').trim() !== want) continue
|
|
1019
|
+
const gear = cell.querySelector('svg')
|
|
1020
|
+
if (!gear || gear.getAttribute(marker) !== null) continue
|
|
1021
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
1022
|
+
const className = gear.getAttribute('class')
|
|
1023
|
+
if (className) svg.setAttribute('class', className)
|
|
1024
|
+
svg.setAttribute('viewBox', '0 0 24 24')
|
|
1025
|
+
svg.setAttribute('width', '16')
|
|
1026
|
+
svg.setAttribute('height', '16')
|
|
1027
|
+
svg.setAttribute('fill', 'currentColor')
|
|
1028
|
+
svg.setAttribute('fill-rule', 'evenodd')
|
|
1029
|
+
svg.setAttribute('aria-hidden', 'true')
|
|
1030
|
+
svg.setAttribute('focusable', 'false')
|
|
1031
|
+
svg.setAttribute(marker, '1')
|
|
1032
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
|
1033
|
+
path.setAttribute('d', CODEX_MARK)
|
|
1034
|
+
path.setAttribute('clip-rule', 'evenodd')
|
|
1035
|
+
svg.appendChild(path)
|
|
1036
|
+
// If the shell kept the row but redrew its gear, the newest original
|
|
1037
|
+
// is the one to restore on disposal.
|
|
1038
|
+
painted.set(cell, { gear, svg })
|
|
1039
|
+
gear.replaceWith(svg)
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
paint()
|
|
1043
|
+
const observer = typeof MutationObserver === 'undefined' ? undefined : new MutationObserver(paint)
|
|
1044
|
+
observer?.observe(document.body, { childList: true, subtree: true, characterData: true })
|
|
1045
|
+
return () => {
|
|
1046
|
+
observer?.disconnect()
|
|
1047
|
+
for (const { gear, svg } of painted.values()) svg.replaceWith(gear)
|
|
1048
|
+
painted.clear()
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** Required services: slot registration and the host locale runtime. */
|
|
1053
|
+
exports.inject = ['slots', 'locale']
|
|
868
1054
|
|
|
869
1055
|
/**
|
|
870
1056
|
* Client plugin body: poll while the page is visible, and register the
|
|
@@ -872,6 +1058,10 @@ window.__ModuleLoader__.load({
|
|
|
872
1058
|
* @param ctx - client root context.
|
|
873
1059
|
*/
|
|
874
1060
|
exports.apply = function apply(ctx) {
|
|
1061
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'gpt-sub: dictionaries')
|
|
1062
|
+
const t = ctx.locale.bind(NS)
|
|
1063
|
+
ctx.effect(() => paintNavIcon(() => t('nav')), 'gpt-sub: nav icon')
|
|
1064
|
+
|
|
875
1065
|
ctx.effect(() => {
|
|
876
1066
|
void poll(false)
|
|
877
1067
|
void pollStatus()
|
|
@@ -898,8 +1088,9 @@ window.__ModuleLoader__.load({
|
|
|
898
1088
|
name: 'settings.section',
|
|
899
1089
|
id: 'gpt-sub-quota',
|
|
900
1090
|
order: 20,
|
|
901
|
-
label: () => '
|
|
902
|
-
|
|
1091
|
+
label: () => t('nav'),
|
|
1092
|
+
locale: NS,
|
|
1093
|
+
inject: () => ({ t }),
|
|
903
1094
|
}, QuotaSection))
|
|
904
1095
|
}
|
|
905
1096
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@young1lin/dsh-gpt-sub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -102,7 +102,10 @@
|
|
|
102
102
|
"patch": "./cordis.patch.yml"
|
|
103
103
|
},
|
|
104
104
|
"client": {
|
|
105
|
-
"inject": [
|
|
105
|
+
"inject": [
|
|
106
|
+
"@deepseek-ai/dsh-client-locale",
|
|
107
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
108
|
+
],
|
|
106
109
|
"platform": "web"
|
|
107
110
|
}
|
|
108
111
|
}
|