dsh-magpie-connector 0.6.2 → 0.6.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 +11 -0
- package/README.md +3 -3
- package/README.zh.md +2 -2
- package/lib/client/SettingsPage.d.ts +8 -0
- package/lib/client/SettingsPage.js +137 -6
- package/lib/client/UsageCard.d.ts +8 -1
- package/lib/client/UsageCard.js +37 -13
- package/lib/client/api.d.ts +1 -1
- package/lib/client/api.js +7 -1
- package/lib/client/locales.d.ts +27 -0
- package/lib/client/locales.js +54 -0
- package/lib/client/styles.js +15 -3
- package/lib/client/view-model.d.ts +37 -1
- package/lib/client/view-model.js +54 -0
- package/lib/client.js +380 -20
- package/lib/host/cards.d.ts +34 -1
- package/lib/host/cards.js +87 -29
- package/lib/host/reset.d.ts +68 -0
- package/lib/host/reset.js +243 -0
- package/lib/host/routes.d.ts +3 -1
- package/lib/host/routes.js +12 -2
- package/lib/host/transport.js +20 -3
- package/package.json +1 -1
- package/screenshots.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.3 — 2026-11-04
|
|
4
|
+
|
|
5
|
+
- **用上 Codex 重置卡(首个写操作)。** 卡片展开区里最早到期的那一项带「使用…」;周窗口(`7 days`)用量达到 **80%**、且读数新鲜时才可用。host 在准备和提交前各重新读一次该卡并独立校验,不信任浏览器的判断。
|
|
6
|
+
- **只用最早到期的那张。** host 从刚读到的 `resets.each[]` 里挑 `until` 最早、仍可用的一张,把它的 `id` 记进票据;浏览器拿不到也指定不了 `id`。提交前那张已不在可用列表时直接拒绝,不会改用别的卡。
|
|
7
|
+
- **两段确认 + 全插件单飞。** 「使用…」→ 60 秒一次性票据 → 对话框(账号、周用量、所选卡到期时间、不可撤销警告)→ 必须勾选才能提交。同一时刻只允许一个准备/消费;新的准备会作废旧票据。
|
|
8
|
+
- **结果判定。** `reset` 成功并用刷新响应更新卡片;`nothing_to_reset`/`no_credit`/`already_redeemed` 各有文案;4xx(权限不足、409 并发、入口白名单拒绝)说明没花掉,可重来;网络错误/超时/5xx/无法解析一律按「结果不确定」锁定该账号到 host 重启,不自动重试。
|
|
9
|
+
- **传输边界。** 白名单新增 `POST /magpie/quotas/codex-reset`,query 只允许且必须带 `user`、`credit`;其它组合在发出前拒绝。
|
|
10
|
+
- **UI 对齐 subscriptions。** 重置折叠块的箭头从 16px 字符换成 12px SVG(间距 6px),展开条目缩进 18px 与标题文字对齐,行高 28px,「使用…」紧贴「完整重置」。
|
|
11
|
+
- 修复 README(中/英)里 0.5.2 遗留的重复行。规范:`openspec/changes/magpie-codex-reset-redeem/`。
|
|
12
|
+
- 部署提示:线上入口(Cloudflare + 自建 nginx 白名单)放行 `codex-reset` 之前,该功能会如实显示网关的 403/404,卡片与生成不受影响;gateway key 需具备完整权限(只读/受限 key 被网关拒绝)。
|
|
13
|
+
|
|
3
14
|
## 0.6.2 — 2026-11-03
|
|
4
15
|
|
|
5
16
|
- **Accordion instead of an overlay.** Expanding the sidebar usage card no longer covers the session list: the detail grows in flow inside the same card and the list gives up height from the bottom (the list keeps its top anchor, so rows do not jump). No divider line; the card surface and shadow carry the separation.
|
package/README.md
CHANGED
|
@@ -25,8 +25,8 @@ English · [简体中文](README.zh.md)
|
|
|
25
25
|
| **Grouped by upstream provider** (signature) | Works on stock DSH, no Host extension needed: the plugin registers one real provider route per gateway `owned_by` (`dsh-magpie-connector-claude`, `-codex`, `-opencode-go`, …; missing or invalid ownership goes to `-other`), so the official model picker shows `Magpie (Claude)`, `Magpie (Codex)`, `Magpie (Opencode-go)`, `Magpie (Other)` groups. Settings and usage cards use the same grouping, and a provider that only appears in usage is never dropped. |
|
|
26
26
|
| **Line panel and latency** | Configured lines start collapsed; click a row header to edit it, and reorder/delete are always-visible DSH product-icon buttons with tooltips. Opening Settings measures every saved line with a parseable address once (parallel TCP connect time, ≤600ms per line, no HTTP, no credentials) and shows a 4-bar signal plus milliseconds; timeouts show a timeout reading and failures or older hosts degrade to a dash. Display-only: it never affects automatic line selection, is not persisted, and never polls. |
|
|
27
27
|
| **Per-line Cloudflare Access** | Each line independently chooses Direct or Cloudflare Access. Access-enabled line requests carry the service token pair plus the bearer key; the token is never sent to a plain-HTTP line. |
|
|
28
|
-
| **
|
|
29
|
-
| **
|
|
28
|
+
| **Usage cards** | Each upstream account shows plan, expiry, window readings and, when the gateway provides them, its Codex reset credits: a folded `Usage limit resets · N available` line that expands to each credit with its expiry and how far off the soonest is (`expires in N days`); plans that hand out one kind per window show `2 five-hour · 1 weekly` in the summary instead. Cards are read once when settings open and only re-read when you press **Refresh** on that card; bars turn orange above 70% and red at 90%. |
|
|
29
|
+
| **Use a Codex reset** (0.6.3) | The credit that expires first carries a **Use…** button. It stays inert until the account's weekly usage is at least 80% and the reading is fresh, and the host revalidates against a fresh gateway read before and after you confirm. Confirming spends one credit: a 60-second one-shot ticket, a dialog showing the account, weekly usage and expiry, and a box you must tick first. One reset runs at a time plugin-wide, a 4xx from the gateway means nothing was spent, and an unreadable answer locks that account until the host restarts (check Magpie, do not retry). Needs a full-access gateway key and, behind a self-hosted ingress, an allowlist entry for `POST /magpie/quotas/codex-reset`. |
|
|
30
30
|
| **Sidebar usage card** (0.6.0) | Settings → Magpie has a 「Sidebar usage」 switch (host setting, default off) and an auto-refresh interval (minutes, `0` = off, default 5). When on, a dense card sits above Settings: one weekly (`7 days`) row per upstream with its reading and bar, at most five rows with the rest scrolling inside and a overflow chevron when more exist, and one refresh for the whole card. Clicking the header row toggles an in-flow detail list (every window, reset time/chances, balance, error) that shrinks the session list instead of covering it; in rail mode a single Magpie icon opens the same content as a 320px popover. Reads reuse the existing usage endpoint with a 60s cache; only the configured interval polls, silently and paused while the page is hidden. |
|
|
31
31
|
| **Model visibility and default effort** | Per provider, leave visibility untouched to follow the gateway catalog, or save an explicit allowlist so new models stay hidden. Per model, save a default reasoning effort; DSH applies it on the next request. |
|
|
32
32
|
| **Per-session fast mode** | Models advertising the `priority` service tier get a standard/fast switch next to the composer. The choice is per session and in host memory only; it adds `service_tier: "priority"` to requests. |
|
|
@@ -71,7 +71,7 @@ Need another method — custom headers, mTLS, another zero-trust gateway? **Issu
|
|
|
71
71
|
- **Only silence ends a stream.** Headers, SSE events and heartbeats all count as liveness; 300 seconds without any data ends the request with `TIMEOUT` (message contains `MAGPIE_STREAM_TIMEOUT`). Long reasoning survives as long as the gateway keeps the stream alive.
|
|
72
72
|
- **Connection changes do not abort work.** A new address or key immediately hides the old catalog and cards, but generations already prepared or running finish with their captured address and key. User cancellation and plugin disposal still stop them.
|
|
73
73
|
- **The transport is deliberately narrow.** Host requests go through a private `undici` dispatcher straight to the configured gateway, never through a global proxy dispatcher. Redirects are rejected; only `GET /models`, `POST /responses`, `POST /chat/completions`, `GET /magpie/quotas/cards` and `POST /magpie/quotas/refresh` are allowed; Magpie GUI `/api/*` management endpoints are never called. Provider egress proxies belong in Magpie.
|
|
74
|
-
- **Not supported:** `stop` sequences fail before any network access (`MAGPIE_STOP_UNSUPPORTED`) because Magpie cannot preserve them across every upstream translation. Usage cards
|
|
74
|
+
- **Not supported:** `stop` sequences fail before any network access (`MAGPIE_STOP_UNSUPPORTED`) because Magpie cannot preserve them across every upstream translation. Usage cards are read-only. Spending a Codex reset is the plugin's only write to a Magpie account: it is manual, two-step, limited to the credit that expires first, and it never touches account-management endpoints.
|
|
75
75
|
- **Secrets stay out of responses and logs.** Gateway-reflected text is redacted with the current key before it can reach the settings API or logs. Files are written `0600`; the settings API reports only `configured` / `writable`.
|
|
76
76
|
- **The interface follows the DSH UI language.** Since 0.5.0 the settings page and the speed menu ship English and Chinese dictionaries and render in the active DSH language (`ctx.locale`); before 0.5.0 the copy was Chinese-only. A host without a locale service (or a unit test) falls back to the Chinese reference set, so stored settings and behaviour are untouched.
|
|
77
77
|
|
package/README.zh.md
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
| **按上游 provider 分组**(特色) | 原版 DSH 即可使用,无需 Host 扩展:插件按网关 `owned_by` 为每个归属注册一个真实 provider route(`dsh-magpie-connector-claude`、`-codex`、`-opencode-go` 等;缺失或不合法归入 `-other`),官方模型选择器因此显示 `Magpie (Claude)`、`Magpie (Codex)`、`Magpie (Opencode-go)`、`Magpie (Other)` 分组。设置页和用量卡片使用同一套分组,只出现在用量里的 provider 也不会被丢弃。 |
|
|
24
24
|
| **线路面板与延迟** | 已配置的线路默认全部收起,点行头部展开编辑;上移 / 下移 / 删除是常驻的 DSH 产品图标按钮(带 tooltip)。打开设置页对每条已保存且地址可解析的线路做一次 TCP 建连测速(并行、单条 ≤600ms、不发 HTTP、不带凭据),用 4 格信号条 + 毫秒显示;超时显示「超时」,旧版宿主或失败显示「—」。只展示当时的网络质量,不参与自动选线、不持久化、不轮询。 |
|
|
25
25
|
| **按线路的 Cloudflare Access** | 每条线路可单独选择「直连」或「Cloudflare Access」。启用后该线路的请求带上服务令牌两项并保留 bearer key;服务令牌永远不会发往明文 HTTP 线路。 |
|
|
26
|
-
|
|
|
27
|
-
|
|
|
26
|
+
| **用量卡片** | 每个上游账号展示计划、到期时间、各窗口读数和 Codex 重置卡(网关提供时):折叠行「用量限制重置 · 可用 N 次」,展开逐张看到期时间与最早一张还剩多久(`N 天后过期`);按窗口发放的计划摘要显示「5 小时 N 次 · 每周 M 次」。打开设置页只读一次,之后只有点单张卡片的「刷新」才会重新读取;用量条超过 70% 变橙、达到 90% 变红。 |
|
|
27
|
+
| **使用 Codex 重置卡**(0.6.3) | 最早到期的那张带「使用…」按钮。周用量 ≥ 80% 且读数新鲜时才可点;确认前后 host 都会重新读一次网关再校验。确认会消耗一次额度:60 秒一次性票据 + 对话框(账号、周用量、到期时间),必须先勾选才能提交。全插件同一时刻只允许一个;网关 4xx 说明没花掉;应答无法解析时该账号锁定到 host 重启(先去 Magpie 核对,不要重试)。需要完整权限的 gateway key;自建入口还需为 `POST /magpie/quotas/codex-reset` 放行。 |
|
|
28
28
|
| **侧栏额度卡**(0.6.0) | 设置 → Magpie 新增「侧边栏额度」开关(host 持久化,默认关闭)与自动刷新间隔(分钟,`0` = 关闭,默认 5)。打开后左侧栏底部常驻一张密集卡片:每个上游一行周窗口(`7 days`)读数与进度条,最多 5 行、多出的在卡内滚动并显示溢出箭头,整张卡只有一个刷新。点标题行在手风琴里展开每个上游的全部窗口、重置时间/机会、余额与错误,占列表高度而不是盖住列表;侧栏收起时只剩一个 Magpie icon,点开是 320px 浮层。读取复用现有用量接口,客户端 60s 缓存,只有配置的间隔会静默轮询,页面隐藏时暂停。 |
|
|
29
29
|
| **模型可见性与默认 effort** | 每个 provider 不设置可见性时跟随网关目录;保存过选择后只显示勾选项,新上架模型默认隐藏。每个模型可保存一个默认推理 effort,DSH 在下一次请求应用。 |
|
|
30
30
|
| **会话级快速档** | 网关声明 `priority` 服务档的模型,输入框旁会出现标准/快速切换。选择按会话保存在 host 内存,快速档会给请求带上 `service_tier: "priority"`。 |
|
|
@@ -16,3 +16,11 @@ export declare function MagpieSettings({ api }: {
|
|
|
16
16
|
}): import("react").DetailedReactHTMLElement<{
|
|
17
17
|
className: string;
|
|
18
18
|
}, HTMLElement>;
|
|
19
|
+
/**
|
|
20
|
+
* Why the host or the gateway said no, in the page's words. The host's codes are
|
|
21
|
+
* fixed strings; anything else falls back to its (already redacted) message.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resetErrorText(response: {
|
|
24
|
+
status: number;
|
|
25
|
+
body: unknown;
|
|
26
|
+
}): string;
|
|
@@ -6,7 +6,7 @@ import { settingsCss } from './styles.js';
|
|
|
6
6
|
import { UsageCard } from './UsageCard.js';
|
|
7
7
|
import { t } from './locales.js';
|
|
8
8
|
import { DEFAULT_REFRESH_MINUTES, MAX_REFRESH_MINUTES, setSidebarPreference } from './sidebar-preference.js';
|
|
9
|
-
import { cacheIds, cardKey, checkDiff, checkSignature, formatTime, parseCards, parseCheck, providerOrder, readTime, sameTarget, } from './view-model.js';
|
|
9
|
+
import { cacheIds, cardKey, checkDiff, checkSignature, formatTime, parseCards, parseCheck, parseResetOutcome, parseResetTicket, providerOrder, readTime, resetAccount, sameTarget, } from './view-model.js';
|
|
10
10
|
/**
|
|
11
11
|
* Magpie settings section: connection (with an advanced access mode), then
|
|
12
12
|
* per-provider groups (model visibility, DSH-side default effort, read-only
|
|
@@ -72,6 +72,11 @@ export function MagpieSettings({ api }) {
|
|
|
72
72
|
const [usageAlert, setUsageAlert] = useState('');
|
|
73
73
|
const [savingSettings, setSavingSettings] = useState(false);
|
|
74
74
|
const [latency, setLatency] = useState({ phase: 'idle' });
|
|
75
|
+
const [resetConfirm, setResetConfirm] = useState();
|
|
76
|
+
const [resetBusyKey, setResetBusyKey] = useState();
|
|
77
|
+
const [resetConsuming, setResetConsuming] = useState(false);
|
|
78
|
+
const [resetAcknowledged, setResetAcknowledged] = useState(false);
|
|
79
|
+
const [resetStatuses, setResetStatuses] = useState({});
|
|
75
80
|
const settingsRef = useRef(undefined);
|
|
76
81
|
const settingsVersion = useRef(0);
|
|
77
82
|
const settingsLock = useRef(false);
|
|
@@ -268,8 +273,7 @@ export function MagpieSettings({ api }) {
|
|
|
268
273
|
};
|
|
269
274
|
const groups = settings?.groups ?? [];
|
|
270
275
|
const cardList = cards?.cards ?? [];
|
|
271
|
-
const busy = refreshing !== undefined;
|
|
272
|
-
// Catalog first, then an all-provider usage read: the only bulk refresh left, for when no card exists yet.
|
|
276
|
+
const busy = refreshing !== undefined; // Catalog first, then an all-provider usage read: the only bulk refresh left, for when no card exists yet.
|
|
273
277
|
const fetchAll = async () => {
|
|
274
278
|
const result = await mutateSettings({ resync: true });
|
|
275
279
|
if (result?.view)
|
|
@@ -280,6 +284,85 @@ export function MagpieSettings({ api }) {
|
|
|
280
284
|
}
|
|
281
285
|
};
|
|
282
286
|
const groupNote = (provider) => noteScope === provider ? { notice, alert: usageAlert } : {};
|
|
287
|
+
const setResetStatus = (key, status) => setResetStatuses(current => {
|
|
288
|
+
const next = { ...current };
|
|
289
|
+
if (status === undefined)
|
|
290
|
+
delete next[key];
|
|
291
|
+
else
|
|
292
|
+
next[key] = status;
|
|
293
|
+
return next;
|
|
294
|
+
});
|
|
295
|
+
/**
|
|
296
|
+
* Prepare a reset: the host re-reads the card, revalidates the guard and hands
|
|
297
|
+
* back a one-shot ticket. Nothing is spent until the dialog is confirmed.
|
|
298
|
+
*/
|
|
299
|
+
const startReset = async (card) => {
|
|
300
|
+
const account = resetAccount(card);
|
|
301
|
+
if (!account || resetBusyKey !== undefined || resetConsuming)
|
|
302
|
+
return;
|
|
303
|
+
const key = cardKey(card);
|
|
304
|
+
setResetBusyKey(key);
|
|
305
|
+
setResetStatus(key, undefined);
|
|
306
|
+
try {
|
|
307
|
+
const response = await api({ method: 'POST', path: 'reset', body: { action: 'prepare', provider: 'codex', user: account } });
|
|
308
|
+
const ticket = isOk(response) ? parseResetTicket(response.body) : undefined;
|
|
309
|
+
if (!ticket) {
|
|
310
|
+
setResetStatus(key, { text: resetErrorText(response), tone: 'error' });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
setResetAcknowledged(false);
|
|
314
|
+
setResetConfirm({ card, ...ticket });
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
setResetStatus(key, { text: t('resetUseReadFailed'), tone: 'error' });
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
setResetBusyKey(undefined);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
/** Spend the prepared credit. Every answer is reported where the button was. */
|
|
324
|
+
const confirmReset = async () => {
|
|
325
|
+
if (!resetConfirm || !resetAcknowledged || resetConsuming)
|
|
326
|
+
return;
|
|
327
|
+
const key = cardKey(resetConfirm.card);
|
|
328
|
+
setResetConsuming(true);
|
|
329
|
+
try {
|
|
330
|
+
const response = await api({ method: 'POST', path: 'reset', body: { action: 'consume', ticket: resetConfirm.ticket } });
|
|
331
|
+
const outcome = parseResetOutcome(response.body);
|
|
332
|
+
if (!isOk(response) || !outcome) {
|
|
333
|
+
setResetStatus(key, { text: resetErrorText(response), tone: response.status === 502 ? 'warn' : 'error' });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (outcome.outcome === 'reset') {
|
|
337
|
+
if (outcome.cards)
|
|
338
|
+
setCards(parseCards({ status: 'ok', cards: outcome.cards }, 200));
|
|
339
|
+
setResetStatus(key, {
|
|
340
|
+
text: outcome.windows === undefined ? t('resetUseSuccess') : `${t('resetUseSuccess')} · ${t('resetUseResetCode', { windows: outcome.windows })}`,
|
|
341
|
+
tone: 'ok',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else if (outcome.outcome === 'uncertain')
|
|
345
|
+
setResetStatus(key, { text: t('resetUseUncertain'), tone: 'warn' });
|
|
346
|
+
else if (outcome.outcome === 'unconsumed')
|
|
347
|
+
setResetStatus(key, { text: t('resetUseUnconsumed', { reason: outcome.error ?? '' }), tone: 'error' });
|
|
348
|
+
else if (outcome.outcome === 'kept')
|
|
349
|
+
setResetStatus(key, { text: t('resetUseKept'), tone: 'ok' });
|
|
350
|
+
else if (outcome.outcome === 'no-credit')
|
|
351
|
+
setResetStatus(key, { text: t('resetUseNoCredit'), tone: 'error' });
|
|
352
|
+
else if (outcome.outcome === 'already')
|
|
353
|
+
setResetStatus(key, { text: t('resetUseAlready'), tone: 'error' });
|
|
354
|
+
else
|
|
355
|
+
setResetStatus(key, { text: t('resetUseUnknown', { code: outcome.code ?? '' }), tone: 'warn' });
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// The browser gave up before the host answered: the spend may have happened.
|
|
359
|
+
setResetStatus(key, { text: t('resetUseUncertain'), tone: 'warn' });
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
setResetConsuming(false);
|
|
363
|
+
setResetConfirm(undefined);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
283
366
|
return h('div', { className: 'dmg-settings' }, h('style', null, settingsCss), h('p', { className: 'dmg-intro' }, t('intro')), settingsError ? h('p', { className: 'dmg-error', role: 'alert' }, t('settingsReadFailed', { error: settingsError })) : null, settings ? h(Connection, { settings, saving: savingSettings, mutateSettings, api, latency, onRefreshLatency: (lineId) => { void readLatency(lineId); },
|
|
284
367
|
usage: h(UsageStatus, { cards, busy, ...(noteScope === undefined ? { notice, alert: usageAlert } : {}), onFetchAll: () => { void fetchAll(); }, saving: savingSettings }) })
|
|
285
368
|
: settingsError ? null : h('p', { role: 'status', className: 'dmg-hint' }, t('readingSettings')), settings ? h(SidebarUsagePreference, { settings, saving: savingSettings, mutateSettings }) : null, ...providerOrder(groups, cardList).map(provider => h(ProviderGroup, {
|
|
@@ -288,8 +371,15 @@ export function MagpieSettings({ api }) {
|
|
|
288
371
|
settings,
|
|
289
372
|
cards: cardList.filter(c => c.provider === provider),
|
|
290
373
|
busy, saving: savingSettings, mutateSettings, ...groupNote(provider),
|
|
374
|
+
resetBusyKey, resetStatuses,
|
|
375
|
+
onReset: (card) => { void startReset(card); },
|
|
291
376
|
onRefresh: (card) => { void refresh({ provider: card.provider, ...(card.user !== undefined ? { user: card.user } : {}) }); },
|
|
292
|
-
}))
|
|
377
|
+
})), h(ResetDialog, {
|
|
378
|
+
confirm: resetConfirm, acknowledged: resetAcknowledged, busy: resetConsuming,
|
|
379
|
+
onAcknowledge: setResetAcknowledged,
|
|
380
|
+
onCancel: () => { setResetConfirm(undefined); setResetAcknowledged(false); },
|
|
381
|
+
onConfirm: () => { void confirmReset(); },
|
|
382
|
+
}));
|
|
293
383
|
}
|
|
294
384
|
/** What to tell the user after a 2xx refresh: unchanged read times, or a still-empty cold start. */
|
|
295
385
|
function noticeAfter(target, before, next) {
|
|
@@ -328,6 +418,42 @@ function UsageStatus({ cards, busy, saving, notice, alert, onFetchAll }) {
|
|
|
328
418
|
return null;
|
|
329
419
|
return h('div', { className: 'dmg-usage', 'data-usage-state': state, 'aria-label': t('usageLabel') }, ...lines, canFetch ? h('div', { className: 'dmg-row' }, h('button', { type: 'button', className: 'dmg-button', disabled: busy || saving, onClick: onFetchAll }, t('fetchModelsAndUsage'))) : null);
|
|
330
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Why the host or the gateway said no, in the page's words. The host's codes are
|
|
423
|
+
* fixed strings; anything else falls back to its (already redacted) message.
|
|
424
|
+
*/
|
|
425
|
+
export function resetErrorText(response) {
|
|
426
|
+
const reason = response.body?.error;
|
|
427
|
+
const code = typeof reason === 'string' ? reason : '';
|
|
428
|
+
if (code === 'MAGPIE_RESET_BUSY')
|
|
429
|
+
return t('resetUseBusy');
|
|
430
|
+
if (code === 'MAGPIE_RESET_UNCERTAIN' || code.startsWith('MAGPIE_RESET_UNCERTAIN:'))
|
|
431
|
+
return t('resetUseUncertain');
|
|
432
|
+
if (code === 'MAGPIE_RESET_GUARD')
|
|
433
|
+
return t('resetUseGuard');
|
|
434
|
+
if (code === 'MAGPIE_RESET_UNAVAILABLE' || code === 'MAGPIE_RESET_CREDIT_GONE')
|
|
435
|
+
return t('resetUseUnavailable');
|
|
436
|
+
if (code === 'MAGPIE_RESET_REFRESH')
|
|
437
|
+
return t('resetUseRefresh');
|
|
438
|
+
if (code === 'MAGPIE_RESET_TICKET_INVALID')
|
|
439
|
+
return t('resetUseTicketInvalid');
|
|
440
|
+
if (code === 'MAGPIE_RESET_TICKET_EXPIRED')
|
|
441
|
+
return t('resetUseTicketExpired');
|
|
442
|
+
if (code.startsWith('MAGPIE_RESET_READ_FAILED'))
|
|
443
|
+
return t('resetUseReadFailed');
|
|
444
|
+
return failureText(response);
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* The confirmation of an irreversible spend. Rendered as an open `<dialog>` inside
|
|
448
|
+
* a backdrop: the shell has no modal service, and a plain element keeps the page
|
|
449
|
+
* testable without `showModal`. The confirm button stays inert until the box is ticked.
|
|
450
|
+
*/
|
|
451
|
+
function ResetDialog({ confirm, acknowledged, busy, onAcknowledge, onCancel, onConfirm }) {
|
|
452
|
+
if (!confirm)
|
|
453
|
+
return null;
|
|
454
|
+
const { card } = confirm;
|
|
455
|
+
return h('div', { className: 'dmg-modal-backdrop', 'data-reset-dialog': '' }, h('dialog', { open: true, className: 'dmg-modal', 'aria-labelledby': 'dmg-reset-title' }, h('h3', { id: 'dmg-reset-title' }, t('resetUseConfirmTitle')), h('p', { className: 'dmg-hint', 'data-reset-account': '' }, `${t('resetUseAccount')}:${card.user ?? ''}`), confirm.weeklyUsedPercent === undefined ? null : h('p', { className: 'dmg-hint', 'data-reset-weekly': '' }, `${t('resetUseWeekly')} ${Math.round(confirm.weeklyUsedPercent)}%`), confirm.creditExpiresAt === undefined ? null : h('p', { className: 'dmg-hint', 'data-reset-credit': '' }, t('resetCreditExpires', { date: formatTime(confirm.creditExpiresAt) })), h('p', { className: 'dmg-warn' }, t('resetUseWarning')), h('label', { className: 'dmg-check-row' }, h('input', { type: 'checkbox', checked: acknowledged, 'data-reset-acknowledge': '', onChange: (e) => onAcknowledge(e.target.checked) }), h('span', null, t('resetUseAcknowledge'))), h('div', { className: 'dmg-row' }, h('button', { type: 'button', className: 'dmg-button', onClick: onCancel }, t('cancel')), h('button', { type: 'button', className: 'dmg-button', 'data-reset-confirm': '', disabled: !acknowledged || busy, onClick: onConfirm }, busy ? t('resetUseConfirming') : t('resetUseConfirm')))));
|
|
456
|
+
}
|
|
331
457
|
async function postSettings(api, body) {
|
|
332
458
|
try {
|
|
333
459
|
const response = await api({ method: 'POST', path: 'settings', body });
|
|
@@ -595,7 +721,7 @@ function SyncStatus({ settings }) {
|
|
|
595
721
|
const registration = settings.routeRegistrationError;
|
|
596
722
|
return h('div', { className: 'dmg-meta', 'data-sync': '' }, settings.lastSyncAt ? h('p', null, t('modelsCount', { n: count }), h('time', { dateTime: settings.lastSyncAt }, formatTime(settings.lastSyncAt))) : null, settings.lastSyncError ? h('p', { className: 'dmg-error' }, t('syncFailed', { kind: settings.lastSyncErrorKind ? t('syncFailedKind', { kind: t(SYNC_KIND[settings.lastSyncErrorKind] ?? 'syncKindHttp') }) : '', error: settings.lastSyncError })) : null, registration ? h('p', { className: 'dmg-error', role: 'alert' }, t('routeRegistrationFailed', { code: registration.code, route: registration.route ? `, ${registration.route}` : '' })) : null, settings.knownOwnersReset ? h('p', { className: 'dmg-hint' }, t('ownersReset')) : null, settings.ownerLimitReached ? h('p', { className: 'dmg-hint' }, t('ownerLimitReached', { list: rejected.join(t('listSeparator')) })) : null);
|
|
597
723
|
}
|
|
598
|
-
function ProviderGroup({ provider, group, settings, cards, busy, saving, mutateSettings, onRefresh, notice, alert }) {
|
|
724
|
+
function ProviderGroup({ provider, group, settings, cards, busy, saving, mutateSettings, onRefresh, onReset, resetBusyKey, resetStatuses, notice, alert }) {
|
|
599
725
|
const [error, setError] = useState('');
|
|
600
726
|
const saved = settings?.visibleModels ?? {};
|
|
601
727
|
const following = !Object.hasOwn(saved, provider);
|
|
@@ -629,7 +755,12 @@ function ProviderGroup({ provider, group, settings, cards, busy, saving, mutateS
|
|
|
629
755
|
effort: settings?.defaultEfforts?.[model.id],
|
|
630
756
|
onVisible: (visible) => setVisible(model, visible),
|
|
631
757
|
onEffort: (effort) => { void post({ defaultEffort: { model: model.id, effort } }); },
|
|
632
|
-
}))) : null, cards.length ? h('div', { className: 'dmg-cards' }, ...cards.map(card => h(UsageCard, {
|
|
758
|
+
}))) : null, cards.length ? h('div', { className: 'dmg-cards' }, ...cards.map(card => h(UsageCard, {
|
|
759
|
+
key: cardKey(card), card, busy, disabled: saving,
|
|
760
|
+
resetBusy: resetBusyKey === cardKey(card),
|
|
761
|
+
...(resetStatuses?.[cardKey(card)] === undefined ? {} : { resetStatus: resetStatuses[cardKey(card)] }),
|
|
762
|
+
onRefresh: () => onRefresh(card), onReset: () => onReset(card),
|
|
763
|
+
}))) : null, notice === 'not-reread' ? h('p', { className: 'dmg-hint', role: 'status' }, t('notReread')) : null, alert ? h('p', { className: 'dmg-error', role: 'alert', 'data-usage-alert': '' }, alert) : null, error ? h('p', { className: 'dmg-error', role: 'alert' }, error) : null);
|
|
633
764
|
}
|
|
634
765
|
function ModelLine({ model, visible, effort, disabled, onVisible, onEffort }) {
|
|
635
766
|
const efforts = model.efforts ?? [];
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { type Card } from './view-model.js';
|
|
2
2
|
/** Bar colour: ≤70% normal (blue), above 70 and below 90 warn (orange), 90% and above high (red). */
|
|
3
3
|
export declare const usageLevel: (used: number) => "high" | "normal" | "warn";
|
|
4
|
-
export declare function UsageCard({ card, busy, onRefresh }: {
|
|
4
|
+
export declare function UsageCard({ card, busy, onRefresh, disabled, resetBusy, resetStatus, onReset }: {
|
|
5
5
|
card: Card;
|
|
6
6
|
busy: boolean;
|
|
7
7
|
onRefresh: () => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
resetBusy?: boolean;
|
|
10
|
+
resetStatus?: {
|
|
11
|
+
text: string;
|
|
12
|
+
tone: 'ok' | 'error' | 'warn';
|
|
13
|
+
};
|
|
14
|
+
onReset?: (card: Card) => void;
|
|
8
15
|
}): import("react").DetailedReactHTMLElement<{
|
|
9
16
|
className: string;
|
|
10
17
|
'data-card': string;
|
package/lib/client/UsageCard.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createElement as h } from 'react';
|
|
2
|
-
import { expiryHint, formatTime, readTime, windowReading } from './view-model.js';
|
|
2
|
+
import { expiryHint, formatTime, readTime, resetAccount, resetBlock, windowReading } from './view-model.js';
|
|
3
3
|
import { t } from './locales.js';
|
|
4
4
|
/**
|
|
5
|
-
* One
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* One usage card. Every gateway string is a React text child (never HTML),
|
|
6
|
+
* absent fields are omitted. The only controls are "刷新" and — on a card whose
|
|
7
|
+
* resets the host marked redeemable — "使用…" on the credit that expires first.
|
|
8
8
|
*/
|
|
9
9
|
const time = (raw) => h('time', { dateTime: raw }, formatTime(raw));
|
|
10
10
|
/** A reset credit's expiry: the localized instant, "expires …", in one machine-readable `<time>`. */
|
|
@@ -18,6 +18,8 @@ const inline = (nodes) => nodes.flatMap((node, index) => index === 0 ? [node] :
|
|
|
18
18
|
const windowLabel = (window) => window === 'fiveHour' ? t('resetWindowFiveHour') : window === 'weekly' ? t('resetWindowWeekly') : undefined;
|
|
19
19
|
/** Bar colour: ≤70% normal (blue), above 70 and below 90 warn (orange), 90% and above high (red). */
|
|
20
20
|
export const usageLevel = (used) => used >= 90 ? 'high' : used > 70 ? 'warn' : 'normal';
|
|
21
|
+
/** The reset area's disclosure arrow: a 12px glyph, so the unfolded rows line up with the title text. */
|
|
22
|
+
const ResetChevron = () => h('svg', { className: 'dmg-reset-chevron', 'aria-hidden': true, width: 12, height: 12, viewBox: '0 0 16 16' }, h('path', { d: 'm6 3 5 5-5 5', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
|
21
23
|
function WindowRow({ window }) {
|
|
22
24
|
const reading = windowReading(window);
|
|
23
25
|
const showReset = reading !== undefined && window.unlimited !== true && !present(window.display) && present(window.resetsAt);
|
|
@@ -38,28 +40,50 @@ function resetCount(resets) {
|
|
|
38
40
|
: null;
|
|
39
41
|
}
|
|
40
42
|
/** One reset credit: which kind it is, when it runs out and, for the soonest, how far off that is. */
|
|
41
|
-
function resetItem(item, soonest, key) {
|
|
43
|
+
function resetItem(item, soonest, key, action) {
|
|
42
44
|
const at = item.until;
|
|
43
45
|
const hint = present(at) && soonest !== undefined && Date.parse(at) === soonest ? expiryHint(at) : undefined;
|
|
44
|
-
return h('div', { className: 'dmg-reset-item', 'data-reset-item': '', key }, h('span', null, windowLabel(item.window) ?? t('resetCreditFull')), h('span',
|
|
46
|
+
return h('div', { className: 'dmg-reset-item', 'data-reset-item': '', key }, h('span', { className: 'dmg-reset-kind' }, h('span', null, windowLabel(item.window) ?? t('resetCreditFull')), action === undefined ? null : action), h('span', { className: 'dmg-reset-when' }, present(at) ? expiryTime(at) : t('resetCreditExpiryUnknown'), hint === undefined ? null : [' · ', hint]));
|
|
47
|
+
}
|
|
48
|
+
/** The "use" button of one credit: disabled state is explained on hover, never silently inert. */
|
|
49
|
+
function resetButton(card, busy, blocked, onReset) {
|
|
50
|
+
const reason = resetBlock(card);
|
|
51
|
+
const label = busy ? t('resetUseChecking') : t('resetUseButton');
|
|
52
|
+
const title = busy ? t('resetUseChecking') : reason === 'guard' ? t('resetUseGuard') : reason === 'refresh' ? t('resetUseRefresh') : reason === 'restart' ? t('resetUseRestart') : t('resetUseReady');
|
|
53
|
+
const inactive = busy || blocked || reason !== undefined;
|
|
54
|
+
return h('button', {
|
|
55
|
+
type: 'button', className: 'dmg-reset-use', 'data-reset-use': '', 'aria-disabled': inactive ? 'true' : 'false', title,
|
|
56
|
+
...(inactive ? {} : { 'data-ready': '' }),
|
|
57
|
+
onClick: () => { if (!inactive)
|
|
58
|
+
onReset?.(); },
|
|
59
|
+
}, label);
|
|
45
60
|
}
|
|
46
61
|
/**
|
|
47
62
|
* The card's reset credits, folded like the subscriptions plugin does: the count
|
|
48
|
-
* on the summary line, every credit and its expiry when opened
|
|
49
|
-
*
|
|
63
|
+
* on the summary line, every credit and its expiry when opened, and the use
|
|
64
|
+
* action on the credit that runs out first.
|
|
50
65
|
*/
|
|
51
|
-
function ResetLine({ resets }) {
|
|
66
|
+
function ResetLine({ card, resets, busy, blocked, onReset }) {
|
|
52
67
|
const each = resets.each ?? [];
|
|
53
68
|
const items = each.length ? each : present(resets.until) ? [{ until: resets.until }] : [];
|
|
54
69
|
const times = items.flatMap(item => { const at = present(item.until) ? Date.parse(item.until) : NaN; return Number.isNaN(at) ? [] : [at]; });
|
|
55
|
-
// The soonest credit carries the relative hint; it is the one worth not wasting.
|
|
70
|
+
// The soonest credit carries the relative hint and the use action; it is the one worth not wasting.
|
|
56
71
|
const soonest = times.length ? Math.min(...times) : undefined;
|
|
57
72
|
const count = resetCount(resets);
|
|
58
73
|
if (count === null && items.length === 0)
|
|
59
74
|
return null;
|
|
60
|
-
|
|
75
|
+
// The host marks the card, but the premise is also checked here: only a named Codex
|
|
76
|
+
// account can be spent for, and without that there is nothing to send.
|
|
77
|
+
const actionable = resets.redeemable === true && resetAccount(card) !== undefined && onReset !== undefined;
|
|
78
|
+
return h('details', { className: 'dmg-resets', 'data-field': 'resets' }, h('summary', null, h('span', { className: 'dmg-reset-chevron-slot', 'aria-hidden': true }, h(ResetChevron, null)), h('span', null, t('resetCreditsTitle')), count === null ? null : h('span', { className: 'dmg-resets-count' }, count)), items.length ? h('div', { className: 'dmg-reset-items' }, ...items.map((item, index) => {
|
|
79
|
+
const at = present(item.until) ? Date.parse(item.until) : NaN;
|
|
80
|
+
// A credit with no expiry never runs out, so every credit ties; the first one
|
|
81
|
+
// then stands in for "soonest" instead of leaving the card without an action.
|
|
82
|
+
const isSoonest = soonest === undefined ? index === 0 : !Number.isNaN(at) && at === soonest;
|
|
83
|
+
return resetItem(item, soonest, `reset-${index}`, actionable && isSoonest ? resetButton(card, busy, blocked, () => onReset(card)) : undefined);
|
|
84
|
+
})) : null);
|
|
61
85
|
}
|
|
62
|
-
export function UsageCard({ card, busy, onRefresh }) {
|
|
86
|
+
export function UsageCard({ card, busy, onRefresh, disabled, resetBusy, resetStatus, onReset }) {
|
|
63
87
|
const read = readTime(card);
|
|
64
88
|
const facts = [
|
|
65
89
|
present(card.user) && fact('user', t('usageUser'), card.user),
|
|
@@ -70,5 +94,5 @@ export function UsageCard({ card, busy, onRefresh }) {
|
|
|
70
94
|
present(read) && fact('readAt', card.asOf !== undefined ? t('usageReadAtStale') : t('usageReadAt'), time(read)),
|
|
71
95
|
].filter(Boolean);
|
|
72
96
|
const resets = card.resets;
|
|
73
|
-
return h('article', { className: 'dmg-card', 'data-card': '' }, h('div', { className: 'dmg-card-title' }, h('strong', null, present(card.name) ? card.name : card.provider), h('button', { type: 'button', className: 'dmg-button', disabled: busy, onClick: onRefresh }, t('refresh'))), facts.length ? h('div', { className: 'dmg-facts' }, ...facts) : null, ...(card.windows ?? []).map((window, index) => h(WindowRow, { key: `window-${index}`, window })), resets ? h(ResetLine, { resets }) : null, present(card.error) ? h('p', { className: 'dmg-error', 'data-field': 'error', role: 'alert' }, card.error) : null);
|
|
97
|
+
return h('article', { className: 'dmg-card', 'data-card': '' }, h('div', { className: 'dmg-card-title' }, h('strong', null, present(card.name) ? card.name : card.provider), h('button', { type: 'button', className: 'dmg-button', disabled: busy || disabled === true, onClick: onRefresh }, t('refresh'))), facts.length ? h('div', { className: 'dmg-facts' }, ...facts) : null, ...(card.windows ?? []).map((window, index) => h(WindowRow, { key: `window-${index}`, window })), resets ? h(ResetLine, { resets, card, busy: resetBusy === true, blocked: disabled === true, onReset }) : null, resetStatus ? h('p', { className: resetStatus.tone === 'error' ? 'dmg-error' : resetStatus.tone === 'warn' ? 'dmg-warn' : 'dmg-ok', role: resetStatus.tone === 'error' ? 'alert' : 'status', 'data-reset-status': resetStatus.tone }, resetStatus.text) : null, present(card.error) ? h('p', { className: 'dmg-error', 'data-field': 'error', role: 'alert' }, card.error) : null);
|
|
74
98
|
}
|
package/lib/client/api.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `/api/dsh-magpie-connector/`. Responses are bare JSON; errors are non-2xx with
|
|
4
4
|
* `{error: string}`. Injected so tests replace it without touching `fetch`.
|
|
5
5
|
*/
|
|
6
|
-
export type Resource = 'settings' | 'cards' | 'speed' | 'line-check' | 'latency';
|
|
6
|
+
export type Resource = 'settings' | 'cards' | 'speed' | 'line-check' | 'latency' | 'reset';
|
|
7
7
|
export interface ApiRequest {
|
|
8
8
|
method: 'GET' | 'POST';
|
|
9
9
|
path: Resource;
|
package/lib/client/api.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
const TIMEOUT_MS = 30_000;
|
|
2
|
+
/**
|
|
3
|
+
* Spending a reset outlives the generic budget: the host waits up to 45s for the
|
|
4
|
+
* gateway, which itself may spend 20s and then re-read the account for 15s. The
|
|
5
|
+
* browser must not give up first — a timeout here reads as "outcome unknown".
|
|
6
|
+
*/
|
|
7
|
+
const RESET_TIMEOUT_MS = 60_000;
|
|
2
8
|
export function createFetchApi(base = '/api/dsh-magpie-connector/') {
|
|
3
9
|
return async ({ method, path, query, body }) => {
|
|
4
10
|
const search = query ? `?${new URLSearchParams(query).toString()}` : '';
|
|
5
11
|
const controller = new AbortController();
|
|
6
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
12
|
+
const timer = setTimeout(() => controller.abort(), path === 'reset' ? RESET_TIMEOUT_MS : TIMEOUT_MS);
|
|
7
13
|
try {
|
|
8
14
|
const response = await fetch(`${base}${path}${search}`, {
|
|
9
15
|
method,
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare const en: {
|
|
|
37
37
|
refreshModels: string;
|
|
38
38
|
modelsRefreshed: string;
|
|
39
39
|
save: string;
|
|
40
|
+
cancel: string;
|
|
40
41
|
saving: string;
|
|
41
42
|
addLine: string;
|
|
42
43
|
addBackupLine: string;
|
|
@@ -147,6 +148,32 @@ export declare const en: {
|
|
|
147
148
|
resetExpired: string;
|
|
148
149
|
resetInHours: string;
|
|
149
150
|
resetInDays: string;
|
|
151
|
+
resetUseButton: string;
|
|
152
|
+
resetUseChecking: string;
|
|
153
|
+
resetUseConfirming: string;
|
|
154
|
+
resetUseGuard: string;
|
|
155
|
+
resetUseRefresh: string;
|
|
156
|
+
resetUseRestart: string;
|
|
157
|
+
resetUseReady: string;
|
|
158
|
+
resetUseConfirmTitle: string;
|
|
159
|
+
resetUseWarning: string;
|
|
160
|
+
resetUseAcknowledge: string;
|
|
161
|
+
resetUseConfirm: string;
|
|
162
|
+
resetUseSuccess: string;
|
|
163
|
+
resetUseKept: string;
|
|
164
|
+
resetUseNoCredit: string;
|
|
165
|
+
resetUseAlready: string;
|
|
166
|
+
resetUseUnknown: string;
|
|
167
|
+
resetUseUnconsumed: string;
|
|
168
|
+
resetUseUncertain: string;
|
|
169
|
+
resetUseBusy: string;
|
|
170
|
+
resetUseTicketInvalid: string;
|
|
171
|
+
resetUseTicketExpired: string;
|
|
172
|
+
resetUseUnavailable: string;
|
|
173
|
+
resetUseReadFailed: string;
|
|
174
|
+
resetUseAccount: string;
|
|
175
|
+
resetUseWeekly: string;
|
|
176
|
+
resetUseResetCode: string;
|
|
150
177
|
yes: string;
|
|
151
178
|
no: string;
|
|
152
179
|
unlimited: string;
|
package/lib/client/locales.js
CHANGED
|
@@ -37,6 +37,7 @@ export const en = {
|
|
|
37
37
|
refreshModels: 'Refresh model list',
|
|
38
38
|
modelsRefreshed: 'Model list refreshed',
|
|
39
39
|
save: 'Save',
|
|
40
|
+
cancel: 'Cancel',
|
|
40
41
|
saving: 'Saving…',
|
|
41
42
|
addLine: 'Add line',
|
|
42
43
|
addBackupLine: 'Add backup line',
|
|
@@ -147,6 +148,32 @@ export const en = {
|
|
|
147
148
|
resetExpired: 'expired',
|
|
148
149
|
resetInHours: 'in {n}h',
|
|
149
150
|
resetInDays: 'in {n}d',
|
|
151
|
+
resetUseButton: 'Use…',
|
|
152
|
+
resetUseChecking: 'Checking…',
|
|
153
|
+
resetUseConfirming: 'Spending…',
|
|
154
|
+
resetUseGuard: 'You can use this reset once weekly usage reaches 80%.',
|
|
155
|
+
resetUseRefresh: 'Refresh usage to confirm the current limits and reset availability.',
|
|
156
|
+
resetUseRestart: 'Reset details are missing. Refresh usage and try again.',
|
|
157
|
+
resetUseReady: 'Spend the reset that expires first. This needs confirmation.',
|
|
158
|
+
resetUseConfirmTitle: 'Confirm a full reset',
|
|
159
|
+
resetUseWarning: 'This spends one reset and restarts the weekly and 5-hour limits. It cannot be undone, and this confirmation expires in 60 seconds.',
|
|
160
|
+
resetUseAcknowledge: 'I understand that one reset will be spent.',
|
|
161
|
+
resetUseConfirm: 'Spend one reset',
|
|
162
|
+
resetUseSuccess: 'Reset confirmed. Usage refreshed.',
|
|
163
|
+
resetUseKept: 'Nothing needed resetting, so no reset was spent.',
|
|
164
|
+
resetUseNoCredit: 'This account has no reset left.',
|
|
165
|
+
resetUseAlready: 'That reset was already used.',
|
|
166
|
+
resetUseUnknown: 'The gateway reported “{code}”.',
|
|
167
|
+
resetUseUnconsumed: 'The gateway did not run the reset: {reason}',
|
|
168
|
+
resetUseUncertain: 'The outcome could not be confirmed. Check the account in Magpie before trying again — do not retry or restart to get around this.',
|
|
169
|
+
resetUseBusy: 'Another reset is already in progress. Wait for it to finish.',
|
|
170
|
+
resetUseTicketInvalid: 'That confirmation is no longer valid. Start again.',
|
|
171
|
+
resetUseTicketExpired: 'That confirmation expired. Start again.',
|
|
172
|
+
resetUseUnavailable: 'That reset is no longer available. Refresh and try again.',
|
|
173
|
+
resetUseReadFailed: 'Could not read fresh usage, so no reset was submitted.',
|
|
174
|
+
resetUseAccount: 'Account',
|
|
175
|
+
resetUseWeekly: 'Weekly usage',
|
|
176
|
+
resetUseResetCode: '{windows} windows restarted',
|
|
150
177
|
yes: 'Yes',
|
|
151
178
|
no: 'No',
|
|
152
179
|
unlimited: 'Unlimited',
|
|
@@ -193,6 +220,7 @@ export const zh = {
|
|
|
193
220
|
refreshModels: '刷新模型列表',
|
|
194
221
|
modelsRefreshed: '已刷新模型列表',
|
|
195
222
|
save: '保存',
|
|
223
|
+
cancel: '取消',
|
|
196
224
|
saving: '保存中…',
|
|
197
225
|
addLine: '添加线路',
|
|
198
226
|
addBackupLine: '添加备用线路',
|
|
@@ -303,6 +331,32 @@ export const zh = {
|
|
|
303
331
|
resetExpired: '已过期',
|
|
304
332
|
resetInHours: '{n} 小时后过期',
|
|
305
333
|
resetInDays: '{n} 天后过期',
|
|
334
|
+
resetUseButton: '使用…',
|
|
335
|
+
resetUseChecking: '正在检查…',
|
|
336
|
+
resetUseConfirming: '正在提交…',
|
|
337
|
+
resetUseGuard: '每周用量达到 80% 后才可使用此重置。',
|
|
338
|
+
resetUseRefresh: '请刷新用量,确认当前限额与重置可用性。',
|
|
339
|
+
resetUseRestart: '缺少重置详情。请刷新用量后重试。',
|
|
340
|
+
resetUseReady: '使用最早到期的这张重置,操作前需要确认。',
|
|
341
|
+
resetUseConfirmTitle: '确认完整重置',
|
|
342
|
+
resetUseWarning: '将消耗一次重置额度,重置每周与 5 小时限额。无法撤销,确认 60 秒后失效。',
|
|
343
|
+
resetUseAcknowledge: '我理解将消耗一次额度。',
|
|
344
|
+
resetUseConfirm: '消耗一次重置',
|
|
345
|
+
resetUseSuccess: '重置已确认,用量已刷新。',
|
|
346
|
+
resetUseKept: '没有需要重置的窗口,额度未消耗。',
|
|
347
|
+
resetUseNoCredit: '该账号已没有可用的重置额度。',
|
|
348
|
+
resetUseAlready: '这张重置已被使用。',
|
|
349
|
+
resetUseUnknown: '网关返回了「{code}」。',
|
|
350
|
+
resetUseUnconsumed: '网关没有执行重置:{reason}',
|
|
351
|
+
resetUseUncertain: '结果无法确认。请先在 Magpie 核对账号状态再试,不要依靠重试或重启绕过。',
|
|
352
|
+
resetUseBusy: '已有重置操作在进行,请等它完成。',
|
|
353
|
+
resetUseTicketInvalid: '确认已失效,请重新开始。',
|
|
354
|
+
resetUseTicketExpired: '确认已过期,请重新开始。',
|
|
355
|
+
resetUseUnavailable: '这张重置已不可用,请刷新后重试。',
|
|
356
|
+
resetUseReadFailed: '无法读取最新用量,未提交重置。',
|
|
357
|
+
resetUseAccount: '账号',
|
|
358
|
+
resetUseWeekly: '每周用量',
|
|
359
|
+
resetUseResetCode: '{windows} 个窗口已重置',
|
|
306
360
|
yes: '是',
|
|
307
361
|
no: '否',
|
|
308
362
|
unlimited: '无限',
|
package/lib/client/styles.js
CHANGED
|
@@ -45,12 +45,24 @@ export const settingsCss = `
|
|
|
45
45
|
.dmg-resets>summary{display:flex;align-items:center;gap:6px;cursor:pointer;list-style:none;color:var(--dsw-alias-label-secondary)}
|
|
46
46
|
.dmg-resets>summary::-webkit-details-marker{display:none}
|
|
47
47
|
.dmg-resets>summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px;border-radius:8px}
|
|
48
|
-
.dmg-
|
|
48
|
+
.dmg-reset-chevron-slot{display:inline-flex;flex:none;width:12px;height:12px;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);transition:transform 100ms ease}
|
|
49
|
+
.dmg-resets[open]>summary .dmg-reset-chevron-slot{transform:rotate(90deg)}
|
|
49
50
|
.dmg-resets-count{margin-left:auto;color:var(--dsw-alias-label-tertiary);white-space:nowrap}
|
|
50
51
|
.dmg-reset-items{display:flex;flex-direction:column;gap:2px;margin-top:6px;padding-left:18px}
|
|
51
|
-
.dmg-reset-item{display:flex;align-items:
|
|
52
|
-
.dmg-reset-
|
|
52
|
+
.dmg-reset-item{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:28px;color:var(--dsw-alias-label-tertiary)}
|
|
53
|
+
.dmg-reset-kind{display:inline-flex;align-items:center;gap:8px;flex:none;color:var(--dsw-alias-label-secondary)}
|
|
54
|
+
.dmg-reset-when{margin-left:auto;text-align:right}
|
|
55
|
+
.dmg-reset-use{height:22px;min-height:22px;padding:0 7px;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:1;cursor:pointer;white-space:nowrap}
|
|
56
|
+
.dmg-reset-use:hover[data-ready]{background:var(--dsw-alias-interactive-bg-hover)}
|
|
57
|
+
.dmg-reset-use[aria-disabled=true]{opacity:.4;cursor:not-allowed}
|
|
58
|
+
.dmg-reset-use:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
|
|
53
59
|
.dmg-reset-item time{color:var(--dsw-alias-label-tertiary)}
|
|
60
|
+
.dmg-warn{color:var(--dsw-alias-state-warn-primary);font-size:12px;overflow-wrap:anywhere}
|
|
61
|
+
.dmg-modal-backdrop{position:fixed;inset:0;z-index:30;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--dsw-alias-bg-mask-1,#0006)}
|
|
62
|
+
.dmg-modal{position:static;display:flex;flex-direction:column;gap:8px;max-width:420px;width:100%;padding:16px 18px;border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}
|
|
63
|
+
.dmg-modal h3{margin:0;font-size:14px;font-weight:500}
|
|
64
|
+
.dmg-check-row{display:flex;align-items:flex-start;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
65
|
+
.dmg-check-row input{margin:2px 0 0}
|
|
54
66
|
.dmg-models{display:flex;flex-direction:column}
|
|
55
67
|
.dmg-model{display:flex;align-items:center;gap:8px;padding:6px 0;border-top:.5px solid var(--dsw-alias-border-l2)}
|
|
56
68
|
.dmg-model label{display:flex;align-items:center;gap:6px;min-width:0;flex:1}
|