dsh-plugin-subscriptions 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/README.zh.md +19 -1
- package/lib/auth/rpc.d.ts +8 -0
- package/lib/auth/rpc.js +2 -0
- package/lib/client/SubscriptionsSection.d.ts +13 -0
- package/lib/client/SubscriptionsSection.js +111 -1
- package/lib/client/locales.d.ts +20 -0
- package/lib/client/locales.js +20 -0
- package/lib/client.js +228 -0
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +305 -25
- package/lib/providers/claude.d.ts +13 -1
- package/lib/providers/claude.js +89 -0
- package/lib/providers/codex.d.ts +13 -1
- package/lib/providers/codex.js +56 -0
- package/lib/providers/common.d.ts +20 -0
- package/lib/providers/grok.d.ts +51 -4
- package/lib/providers/grok.js +202 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,10 @@ Logged-in providers join the session model picker with their live model catalogs
|
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
17
|
+
Models that advertise reasoning levels get an **Effort** selector in the same menu — Codex models, and Grok 4.6 / 4.5 (levels and defaults come from each provider's live catalog, not a hardcoded list):
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
17
21
|
The `image_generate` tool renders its result inline in the conversation:
|
|
18
22
|
|
|
19
23
|

|
|
@@ -24,10 +28,12 @@ The `image_generate` tool renders its result inline in the conversation:
|
|
|
24
28
|
|----------|-------------------|--------|
|
|
25
29
|
| `codex` | ChatGPT Plus/Pro | live catalog from `chatgpt.com/backend-api/codex/models` |
|
|
26
30
|
| `claude` | Claude Pro/Max | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 |
|
|
27
|
-
| `grok` | X Premium (xAI) | live catalog from `api.x.ai/v1/models` (chat models only) |
|
|
31
|
+
| `grok` | X Premium (xAI) | live catalog from `api.x.ai/v1/models` (chat models only); reasoning efforts from the Grok CLI catalog (`cli-chat-proxy.grok.com/v1/models`) |
|
|
28
32
|
|
|
29
33
|
Only logged-in providers appear in the session model picker; the lists above refresh on login/logout. Vision-capable models declare `['text', 'image']` input modalities, and image content is translated to each provider's wire format.
|
|
30
34
|
|
|
35
|
+
Logged-in cards also show **subscription usage** — per rate-limit window (5-hour session, weekly, and per-model weekly where the plan has one) with the used percentage, a progress bar, and the reset time, plus a Refresh button. Codex usage comes from `chatgpt.com/backend-api/wham/usage` (also reports the plan), Claude usage from `api.anthropic.com/api/oauth/usage`, and Grok usage from the Grok Build CLI proxy's `cli-chat-proxy.grok.com/v1/billing` (the source of the CLI's `/usage` panel; reports the shared weekly pool and the subscription tier).
|
|
36
|
+
|
|
31
37
|
Also included, registered when the matching provider is enabled:
|
|
32
38
|
|
|
33
39
|
- **`x_search`** tool (Grok) — xAI's hosted X search, returning `{ answer, citations }`.
|
|
@@ -71,6 +77,18 @@ cp overlay.example.yml overlay.yml # then edit the name: to this checkout's ab
|
|
|
71
77
|
dsh --profile headless --patch <checkout>/overlay.yml "your task"
|
|
72
78
|
```
|
|
73
79
|
|
|
80
|
+
## Update
|
|
81
|
+
|
|
82
|
+
Installed from npm:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
dsh plugin --profile web update --latest dsh-plugin-subscriptions
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Installed from GitHub: re-run the same `add github:V1ki/dsh-plugin-subscriptions` command — it re-fetches the sources and rebuilds. A linked local checkout just needs `git pull && pnpm build` in the checkout.
|
|
89
|
+
|
|
90
|
+
Either way, restart `dsh web` afterwards so the new version loads.
|
|
91
|
+
|
|
74
92
|
## Use
|
|
75
93
|
|
|
76
94
|
1. `dsh web`, open the printed URL.
|
package/README.zh.md
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
17
|
+
声明了推理等级的模型会在同一菜单里多出**推理等级**选择 —— Codex 系列模型,以及 Grok 4.6 / 4.5(档位和默认值来自各 provider 的实时目录,不是硬编码列表):
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
17
21
|
`image_generate` 工具生成的图片直接内联显示在对话里:
|
|
18
22
|
|
|
19
23
|

|
|
@@ -24,10 +28,12 @@
|
|
|
24
28
|
|----------|------------------|------|
|
|
25
29
|
| `codex` | ChatGPT Plus/Pro | 从 `chatgpt.com/backend-api/codex/models` 实时获取 |
|
|
26
30
|
| `claude` | Claude Pro/Max | claude-opus-4-5、claude-sonnet-4-5、claude-haiku-4-5 |
|
|
27
|
-
| `grok` | X Premium (xAI) | 从 `api.x.ai/v1/models` 实时获取(仅对话模型) |
|
|
31
|
+
| `grok` | X Premium (xAI) | 从 `api.x.ai/v1/models` 实时获取(仅对话模型);推理等级来自 Grok CLI 目录(`cli-chat-proxy.grok.com/v1/models`) |
|
|
28
32
|
|
|
29
33
|
只有已登录的 provider 才会出现在会话模型选择器里;登录/退出后列表自动刷新。支持视觉的模型会声明 `['text', 'image']` 输入模态,图片内容会被翻译成各 provider 的 wire 格式。
|
|
30
34
|
|
|
35
|
+
已登录的卡片还会显示**订阅用量**——按限额窗口(5 小时会话窗、每周窗,以及计划包含的按模型每周窗)展示已用百分比、进度条和重置时间,并带刷新按钮。Codex 用量来自 `chatgpt.com/backend-api/wham/usage`(同时报告计划类型),Claude 用量来自 `api.anthropic.com/api/oauth/usage`,Grok 用量来自 Grok Build CLI 代理的 `cli-chat-proxy.grok.com/v1/billing`(即 CLI `/usage` 面板的数据源,报告共享每周额度和订阅档位)。
|
|
36
|
+
|
|
31
37
|
随 provider 启用自动注册的工具:
|
|
32
38
|
|
|
33
39
|
- **`x_search`**(Grok)—— xAI 托管的 X 搜索,返回 `{ answer, citations }`。
|
|
@@ -71,6 +77,18 @@ cp overlay.example.yml overlay.yml # 然后把 name: 改成本检出的 lib/in
|
|
|
71
77
|
dsh --profile headless --patch <检出目录>/overlay.yml "你的任务"
|
|
72
78
|
```
|
|
73
79
|
|
|
80
|
+
## 更新
|
|
81
|
+
|
|
82
|
+
npm 安装的:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
dsh plugin --profile web update --latest dsh-plugin-subscriptions
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
GitHub 安装的:重新执行一遍 `add github:V1ki/dsh-plugin-subscriptions` —— 会重新拉取源码并构建。link 的本地检出只需在检出目录里 `git pull && pnpm build`。
|
|
89
|
+
|
|
90
|
+
无论哪种方式,更新后都要重启 `dsh web` 才会加载新版本。
|
|
91
|
+
|
|
74
92
|
## 使用
|
|
75
93
|
|
|
76
94
|
1. `dsh web`,打开打印的 URL。
|
package/lib/auth/rpc.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { Context } from '@deepseek-ai/cordis';
|
|
8
8
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
9
9
|
import { type ProviderId } from './store.js';
|
|
10
|
+
import type { ProviderUsage } from '../providers/common.js';
|
|
10
11
|
/** The RPC channel this plugin registers on the host connection. */
|
|
11
12
|
export declare const SUBSCRIPTIONS_AUTH_CHANNEL = "/subscriptions-auth";
|
|
12
13
|
/** Decoded image bytes returned by the `image` endpoint. */
|
|
@@ -48,6 +49,13 @@ export interface AuthController {
|
|
|
48
49
|
cancel(provider: ProviderId): Promise<void>;
|
|
49
50
|
/** Delete the stored session. */
|
|
50
51
|
logout(provider: ProviderId): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Current subscription usage of one provider.
|
|
54
|
+
* @param signal - caller cancellation from the RPC transport.
|
|
55
|
+
* @returns `{ supported: false }` when the provider has no usage endpoint.
|
|
56
|
+
* @throws when logged out or the usage lookup fails.
|
|
57
|
+
*/
|
|
58
|
+
usage(provider: ProviderId, signal: AbortSignal): Promise<ProviderUsage>;
|
|
51
59
|
/**
|
|
52
60
|
* Read one image attachment's bytes for inline display.
|
|
53
61
|
* @param ref - the full durable reference (`readImage` verifies against it).
|
package/lib/auth/rpc.js
CHANGED
|
@@ -91,6 +91,8 @@ async function dispatch(controller, endpoint, payload, signal) {
|
|
|
91
91
|
case 'logout':
|
|
92
92
|
await controller.logout(readProvider(payload));
|
|
93
93
|
return ok({ ok: true });
|
|
94
|
+
case 'usage':
|
|
95
|
+
return ok(await controller.usage(readProvider(payload), signal));
|
|
94
96
|
case 'image':
|
|
95
97
|
return ok(await controller.readImage(readImageRef(payload), signal));
|
|
96
98
|
default:
|
|
@@ -10,6 +10,19 @@ export interface ProviderStatus {
|
|
|
10
10
|
account?: string;
|
|
11
11
|
detail?: string;
|
|
12
12
|
}
|
|
13
|
+
/** One rate-limit window as answered by the `usage` endpoint. */
|
|
14
|
+
export interface UsageWindow {
|
|
15
|
+
kind: 'session' | 'weekly' | 'other';
|
|
16
|
+
scope?: string;
|
|
17
|
+
usedPercent: number;
|
|
18
|
+
resetsAt?: number;
|
|
19
|
+
}
|
|
20
|
+
/** `usage` endpoint value: the node half owns this shape. */
|
|
21
|
+
export interface ProviderUsage {
|
|
22
|
+
supported: boolean;
|
|
23
|
+
windows?: UsageWindow[];
|
|
24
|
+
plan?: string;
|
|
25
|
+
}
|
|
13
26
|
/** Injected dependencies of {@link SubscriptionsSection} (slot `inject`). */
|
|
14
27
|
export interface SubscriptionsSectionInjected {
|
|
15
28
|
/** Generic logical-RPC caller over the Connection transport. */
|
|
@@ -89,6 +89,30 @@ const styles = {
|
|
|
89
89
|
color: 'var(--dsw-alias-label-primary)', font: 'inherit', fontSize: 12, lineHeight: '18px',
|
|
90
90
|
cursor: 'pointer',
|
|
91
91
|
},
|
|
92
|
+
usage: {
|
|
93
|
+
display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4,
|
|
94
|
+
borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8,
|
|
95
|
+
},
|
|
96
|
+
usageHeader: { display: 'flex', alignItems: 'center', gap: 8 },
|
|
97
|
+
usageTitle: { fontSize: 12, lineHeight: '18px', fontWeight: 500, color: 'var(--dsw-alias-label-secondary)' },
|
|
98
|
+
usagePlan: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
|
|
99
|
+
usageRefresh: {
|
|
100
|
+
boxSizing: 'border-box', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
101
|
+
height: 22, padding: '0 8px', borderRadius: 11, marginLeft: 'auto',
|
|
102
|
+
border: '1px solid var(--dsw-alias-border-l2)', background: 'transparent',
|
|
103
|
+
color: 'var(--dsw-alias-label-secondary)', font: 'inherit', fontSize: 12, lineHeight: '18px',
|
|
104
|
+
cursor: 'pointer',
|
|
105
|
+
},
|
|
106
|
+
usageRow: { display: 'flex', flexDirection: 'column', gap: 3 },
|
|
107
|
+
usageMeta: {
|
|
108
|
+
display: 'flex', justifyContent: 'space-between', gap: 8,
|
|
109
|
+
fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)',
|
|
110
|
+
},
|
|
111
|
+
usageTrack: {
|
|
112
|
+
height: 6, borderRadius: 3, overflow: 'hidden',
|
|
113
|
+
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
114
|
+
},
|
|
115
|
+
usageFill: { height: '100%', borderRadius: 3 },
|
|
92
116
|
manual: { marginTop: 4, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' },
|
|
93
117
|
manualRow: { display: 'flex', gap: 8, marginTop: 6 },
|
|
94
118
|
manualInput: {
|
|
@@ -133,6 +157,26 @@ function statusText(t, status) {
|
|
|
133
157
|
}
|
|
134
158
|
return t('notLoggedIn');
|
|
135
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Localized label of one usage window (kind, plus the model scope when named).
|
|
162
|
+
* @param t - section translate.
|
|
163
|
+
* @param window - the reported window.
|
|
164
|
+
* @returns e.g. "5-hour window" or "Weekly · Opus".
|
|
165
|
+
*/
|
|
166
|
+
function usageWindowLabel(t, window) {
|
|
167
|
+
const base = window.kind === 'session'
|
|
168
|
+
? t('usageSession')
|
|
169
|
+
: window.kind === 'weekly' ? t('usageWeekly') : t('usageWindow');
|
|
170
|
+
return window.scope !== undefined && window.scope !== '' ? `${base} · ${window.scope}` : base;
|
|
171
|
+
}
|
|
172
|
+
/** Bar fill color: success normally, warn from 80%, error from 95%. */
|
|
173
|
+
function usageBarColor(usedPercent) {
|
|
174
|
+
if (usedPercent >= 95)
|
|
175
|
+
return 'var(--dsw-alias-state-error-primary)';
|
|
176
|
+
if (usedPercent >= 80)
|
|
177
|
+
return 'var(--dsw-alias-state-warn-label)';
|
|
178
|
+
return 'var(--dsw-alias-state-success-primary)';
|
|
179
|
+
}
|
|
136
180
|
/**
|
|
137
181
|
* The Subscriptions settings page component.
|
|
138
182
|
* @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
|
|
@@ -146,8 +190,13 @@ export function SubscriptionsSection(props) {
|
|
|
146
190
|
const [manualDrafts, setManualDrafts] = useState({
|
|
147
191
|
codex: '', claude: '', grok: '',
|
|
148
192
|
});
|
|
193
|
+
const [usages, setUsages] = useState({});
|
|
194
|
+
const [usageErrors, setUsageErrors] = useState({});
|
|
195
|
+
const [usageLoading, setUsageLoading] = useState({});
|
|
149
196
|
const mountedRef = useRef(true);
|
|
150
197
|
const pollersRef = useRef(new Map());
|
|
198
|
+
/** Providers with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
|
|
199
|
+
const usageInflightRef = useRef(new Set());
|
|
151
200
|
const setProviderError = useCallback((provider, message) => {
|
|
152
201
|
if (!mountedRef.current)
|
|
153
202
|
return;
|
|
@@ -216,6 +265,58 @@ export function SubscriptionsSection(props) {
|
|
|
216
265
|
pollersRef.current.clear();
|
|
217
266
|
};
|
|
218
267
|
}, [refresh, startPolling]);
|
|
268
|
+
const loadUsage = useCallback(async (provider) => {
|
|
269
|
+
if (rpc === undefined || usageInflightRef.current.has(provider))
|
|
270
|
+
return;
|
|
271
|
+
usageInflightRef.current.add(provider);
|
|
272
|
+
setUsageLoading(prev => ({ ...prev, [provider]: true }));
|
|
273
|
+
try {
|
|
274
|
+
const usage = await callSubscriptionsAuth(rpc, 'usage', { provider });
|
|
275
|
+
if (!mountedRef.current)
|
|
276
|
+
return;
|
|
277
|
+
setUsages(prev => ({ ...prev, [provider]: usage }));
|
|
278
|
+
setUsageErrors((prev) => {
|
|
279
|
+
const next = { ...prev };
|
|
280
|
+
delete next[provider];
|
|
281
|
+
return next;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (mountedRef.current)
|
|
286
|
+
setUsageErrors(prev => ({ ...prev, [provider]: messageOf(error) }));
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
usageInflightRef.current.delete(provider);
|
|
290
|
+
if (mountedRef.current)
|
|
291
|
+
setUsageLoading(prev => ({ ...prev, [provider]: false }));
|
|
292
|
+
}
|
|
293
|
+
}, [rpc]);
|
|
294
|
+
// Fetch usage once a provider is logged in; drop the cached snapshot on
|
|
295
|
+
// logout so a re-login refetches. A failed lookup does not auto-retry — the
|
|
296
|
+
// per-card Refresh button is the retry path.
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
for (const { id } of PROVIDERS) {
|
|
299
|
+
const status = statuses[id];
|
|
300
|
+
if (status === undefined)
|
|
301
|
+
continue;
|
|
302
|
+
if (status.loggedIn) {
|
|
303
|
+
if (usages[id] === undefined && usageErrors[id] === undefined)
|
|
304
|
+
void loadUsage(id);
|
|
305
|
+
}
|
|
306
|
+
else if (usages[id] !== undefined || usageErrors[id] !== undefined) {
|
|
307
|
+
setUsages((prev) => {
|
|
308
|
+
const next = { ...prev };
|
|
309
|
+
delete next[id];
|
|
310
|
+
return next;
|
|
311
|
+
});
|
|
312
|
+
setUsageErrors((prev) => {
|
|
313
|
+
const next = { ...prev };
|
|
314
|
+
delete next[id];
|
|
315
|
+
return next;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}, [statuses, usages, usageErrors, loadUsage]);
|
|
219
320
|
const login = useCallback(async (provider) => {
|
|
220
321
|
if (rpc === undefined)
|
|
221
322
|
return;
|
|
@@ -285,6 +386,15 @@ export function SubscriptionsSection(props) {
|
|
|
285
386
|
return (_jsxs("div", { style: styles.section, children: [_jsx("p", { style: styles.intro, children: t('intro') }), PROVIDERS.map(({ id, name }) => {
|
|
286
387
|
const status = statuses[id];
|
|
287
388
|
const busy = status?.busy === true;
|
|
288
|
-
|
|
389
|
+
const usage = usages[id];
|
|
390
|
+
const usageError = usageErrors[id];
|
|
391
|
+
// Providers without a usage endpoint answer supported:false — no block.
|
|
392
|
+
const showUsage = status?.loggedIn === true && usage?.supported !== false
|
|
393
|
+
&& (usage !== undefined || usageError !== undefined || usageLoading[id] === true);
|
|
394
|
+
return (_jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.cardHeader, children: [_jsx("span", { style: { ...styles.dot, background: dotColor(status) } }), _jsx("span", { style: styles.name, children: name })] }), _jsx("p", { style: styles.statusLine, children: statusText(t, status) }), status?.detail !== undefined && status.detail !== '' && (_jsx("p", { style: styles.statusLine, children: status.detail })), errors[id] !== undefined && _jsx("p", { style: styles.errorLine, children: errors[id] }), _jsxs("div", { style: styles.actions, children: [!busy && status?.loggedIn !== true && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('login') })), busy && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(id); }, children: t('cancel') })), status?.loggedIn === true && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void logout(id, name); }, children: t('logout') }))] }), showUsage && (_jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), usage?.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: usage.plan }) })), _jsx("button", { type: "button", style: { ...styles.usageRefresh, ...usageLoading[id] === true ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: usageLoading[id] === true, onClick: () => { void loadUsage(id); }, children: t('usageRefresh') })] }), usage === undefined && usageError === undefined && (_jsx("p", { style: styles.statusLine, children: t('usageLoading') })), usageError !== undefined && (_jsx("p", { style: styles.errorLine, children: t('usageError', { message: usageError }) })), usage?.windows !== undefined && usage.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageEmpty') })), (usage?.windows ?? []).map((window, index) => {
|
|
395
|
+
const percent = Math.min(100, Math.max(0, window.usedPercent));
|
|
396
|
+
return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [`${String(Math.round(percent))}%`, window.resetsAt !== undefined
|
|
397
|
+
&& ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx("div", { style: styles.usageTrack, children: _jsx("div", { style: { ...styles.usageFill, width: `${String(percent)}%`, background: usageBarColor(percent) } }) })] }, index));
|
|
398
|
+
})] })), busy && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
|
|
289
399
|
})] }));
|
|
290
400
|
}
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -19,6 +19,16 @@ export declare const en: {
|
|
|
19
19
|
manualPlaceholder: string;
|
|
20
20
|
submit: string;
|
|
21
21
|
loginMissingUrl: string;
|
|
22
|
+
usageTitle: string;
|
|
23
|
+
usageRefresh: string;
|
|
24
|
+
usageLoading: string;
|
|
25
|
+
usageEmpty: string;
|
|
26
|
+
usageError: string;
|
|
27
|
+
usageSession: string;
|
|
28
|
+
usageWeekly: string;
|
|
29
|
+
usageWindow: string;
|
|
30
|
+
usageResets: string;
|
|
31
|
+
usagePlan: string;
|
|
22
32
|
generating: string;
|
|
23
33
|
image: string;
|
|
24
34
|
viewImage: string;
|
|
@@ -48,6 +58,16 @@ export declare const zh: {
|
|
|
48
58
|
manualPlaceholder: string;
|
|
49
59
|
submit: string;
|
|
50
60
|
loginMissingUrl: string;
|
|
61
|
+
usageTitle: string;
|
|
62
|
+
usageRefresh: string;
|
|
63
|
+
usageLoading: string;
|
|
64
|
+
usageEmpty: string;
|
|
65
|
+
usageError: string;
|
|
66
|
+
usageSession: string;
|
|
67
|
+
usageWeekly: string;
|
|
68
|
+
usageWindow: string;
|
|
69
|
+
usageResets: string;
|
|
70
|
+
usagePlan: string;
|
|
51
71
|
generating: string;
|
|
52
72
|
image: string;
|
|
53
73
|
viewImage: string;
|
package/lib/client/locales.js
CHANGED
|
@@ -19,6 +19,16 @@ export const en = {
|
|
|
19
19
|
manualPlaceholder: 'Paste the callback URL or code',
|
|
20
20
|
submit: 'Submit',
|
|
21
21
|
loginMissingUrl: 'login answered without an authorizeUrl',
|
|
22
|
+
usageTitle: 'Usage',
|
|
23
|
+
usageRefresh: 'Refresh',
|
|
24
|
+
usageLoading: 'Loading usage…',
|
|
25
|
+
usageEmpty: 'No usage windows reported.',
|
|
26
|
+
usageError: 'Usage lookup failed: {message}',
|
|
27
|
+
usageSession: '5-hour window',
|
|
28
|
+
usageWeekly: 'Weekly',
|
|
29
|
+
usageWindow: 'Window',
|
|
30
|
+
usageResets: 'resets {date}',
|
|
31
|
+
usagePlan: 'Plan: {plan}',
|
|
22
32
|
generating: 'Generating image…',
|
|
23
33
|
image: 'image',
|
|
24
34
|
viewImage: 'View image',
|
|
@@ -48,6 +58,16 @@ export const zh = {
|
|
|
48
58
|
manualPlaceholder: '粘贴回调 URL 或授权码',
|
|
49
59
|
submit: '提交',
|
|
50
60
|
loginMissingUrl: 'login 响应缺少 authorizeUrl',
|
|
61
|
+
usageTitle: '用量',
|
|
62
|
+
usageRefresh: '刷新',
|
|
63
|
+
usageLoading: '用量加载中…',
|
|
64
|
+
usageEmpty: '服务商未返回任何用量窗口。',
|
|
65
|
+
usageError: '用量查询失败:{message}',
|
|
66
|
+
usageSession: '5 小时窗口',
|
|
67
|
+
usageWeekly: '每周',
|
|
68
|
+
usageWindow: '窗口',
|
|
69
|
+
usageResets: '{date} 重置',
|
|
70
|
+
usagePlan: '计划:{plan}',
|
|
51
71
|
generating: '正在生成图片…',
|
|
52
72
|
image: '图片',
|
|
53
73
|
viewImage: '查看图片',
|
package/lib/client.js
CHANGED
|
@@ -54,6 +54,16 @@ const en = {
|
|
|
54
54
|
manualPlaceholder: "Paste the callback URL or code",
|
|
55
55
|
submit: "Submit",
|
|
56
56
|
loginMissingUrl: "login answered without an authorizeUrl",
|
|
57
|
+
usageTitle: "Usage",
|
|
58
|
+
usageRefresh: "Refresh",
|
|
59
|
+
usageLoading: "Loading usage…",
|
|
60
|
+
usageEmpty: "No usage windows reported.",
|
|
61
|
+
usageError: "Usage lookup failed: {message}",
|
|
62
|
+
usageSession: "5-hour window",
|
|
63
|
+
usageWeekly: "Weekly",
|
|
64
|
+
usageWindow: "Window",
|
|
65
|
+
usageResets: "resets {date}",
|
|
66
|
+
usagePlan: "Plan: {plan}",
|
|
57
67
|
generating: "Generating image…",
|
|
58
68
|
image: "image",
|
|
59
69
|
viewImage: "View image",
|
|
@@ -83,6 +93,16 @@ const zh = {
|
|
|
83
93
|
manualPlaceholder: "粘贴回调 URL 或授权码",
|
|
84
94
|
submit: "提交",
|
|
85
95
|
loginMissingUrl: "login 响应缺少 authorizeUrl",
|
|
96
|
+
usageTitle: "用量",
|
|
97
|
+
usageRefresh: "刷新",
|
|
98
|
+
usageLoading: "用量加载中…",
|
|
99
|
+
usageEmpty: "服务商未返回任何用量窗口。",
|
|
100
|
+
usageError: "用量查询失败:{message}",
|
|
101
|
+
usageSession: "5 小时窗口",
|
|
102
|
+
usageWeekly: "每周",
|
|
103
|
+
usageWindow: "窗口",
|
|
104
|
+
usageResets: "{date} 重置",
|
|
105
|
+
usagePlan: "计划:{plan}",
|
|
86
106
|
generating: "正在生成图片…",
|
|
87
107
|
image: "图片",
|
|
88
108
|
viewImage: "查看图片",
|
|
@@ -223,6 +243,71 @@ const styles$1 = {
|
|
|
223
243
|
lineHeight: "18px",
|
|
224
244
|
cursor: "pointer"
|
|
225
245
|
},
|
|
246
|
+
usage: {
|
|
247
|
+
display: "flex",
|
|
248
|
+
flexDirection: "column",
|
|
249
|
+
gap: 6,
|
|
250
|
+
marginTop: 4,
|
|
251
|
+
borderTop: "1px solid var(--dsw-alias-border-l2)",
|
|
252
|
+
paddingTop: 8
|
|
253
|
+
},
|
|
254
|
+
usageHeader: {
|
|
255
|
+
display: "flex",
|
|
256
|
+
alignItems: "center",
|
|
257
|
+
gap: 8
|
|
258
|
+
},
|
|
259
|
+
usageTitle: {
|
|
260
|
+
fontSize: 12,
|
|
261
|
+
lineHeight: "18px",
|
|
262
|
+
fontWeight: 500,
|
|
263
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
264
|
+
},
|
|
265
|
+
usagePlan: {
|
|
266
|
+
fontSize: 12,
|
|
267
|
+
lineHeight: "18px",
|
|
268
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
269
|
+
},
|
|
270
|
+
usageRefresh: {
|
|
271
|
+
boxSizing: "border-box",
|
|
272
|
+
display: "inline-flex",
|
|
273
|
+
alignItems: "center",
|
|
274
|
+
justifyContent: "center",
|
|
275
|
+
height: 22,
|
|
276
|
+
padding: "0 8px",
|
|
277
|
+
borderRadius: 11,
|
|
278
|
+
marginLeft: "auto",
|
|
279
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
280
|
+
background: "transparent",
|
|
281
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
282
|
+
font: "inherit",
|
|
283
|
+
fontSize: 12,
|
|
284
|
+
lineHeight: "18px",
|
|
285
|
+
cursor: "pointer"
|
|
286
|
+
},
|
|
287
|
+
usageRow: {
|
|
288
|
+
display: "flex",
|
|
289
|
+
flexDirection: "column",
|
|
290
|
+
gap: 3
|
|
291
|
+
},
|
|
292
|
+
usageMeta: {
|
|
293
|
+
display: "flex",
|
|
294
|
+
justifyContent: "space-between",
|
|
295
|
+
gap: 8,
|
|
296
|
+
fontSize: 12,
|
|
297
|
+
lineHeight: "18px",
|
|
298
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
299
|
+
},
|
|
300
|
+
usageTrack: {
|
|
301
|
+
height: 6,
|
|
302
|
+
borderRadius: 3,
|
|
303
|
+
overflow: "hidden",
|
|
304
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
305
|
+
border: "1px solid var(--dsw-alias-border-l2)"
|
|
306
|
+
},
|
|
307
|
+
usageFill: {
|
|
308
|
+
height: "100%",
|
|
309
|
+
borderRadius: 3
|
|
310
|
+
},
|
|
226
311
|
manual: {
|
|
227
312
|
marginTop: 4,
|
|
228
313
|
fontSize: 12,
|
|
@@ -275,6 +360,22 @@ function statusText(t, status) {
|
|
|
275
360
|
return t("notLoggedIn");
|
|
276
361
|
}
|
|
277
362
|
/**
|
|
363
|
+
* Localized label of one usage window (kind, plus the model scope when named).
|
|
364
|
+
* @param t - section translate.
|
|
365
|
+
* @param window - the reported window.
|
|
366
|
+
* @returns e.g. "5-hour window" or "Weekly · Opus".
|
|
367
|
+
*/
|
|
368
|
+
function usageWindowLabel(t, window$1) {
|
|
369
|
+
const base = window$1.kind === "session" ? t("usageSession") : window$1.kind === "weekly" ? t("usageWeekly") : t("usageWindow");
|
|
370
|
+
return window$1.scope !== void 0 && window$1.scope !== "" ? `${base} · ${window$1.scope}` : base;
|
|
371
|
+
}
|
|
372
|
+
/** Bar fill color: success normally, warn from 80%, error from 95%. */
|
|
373
|
+
function usageBarColor(usedPercent) {
|
|
374
|
+
if (usedPercent >= 95) return "var(--dsw-alias-state-error-primary)";
|
|
375
|
+
if (usedPercent >= 80) return "var(--dsw-alias-state-warn-label)";
|
|
376
|
+
return "var(--dsw-alias-state-success-primary)";
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
278
379
|
* The Subscriptions settings page component.
|
|
279
380
|
* @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
|
|
280
381
|
* @returns the section body, or a notice while the RPC face is absent.
|
|
@@ -289,8 +390,13 @@ function SubscriptionsSection(props) {
|
|
|
289
390
|
claude: "",
|
|
290
391
|
grok: ""
|
|
291
392
|
});
|
|
393
|
+
const [usages, setUsages] = (0, react.useState)({});
|
|
394
|
+
const [usageErrors, setUsageErrors] = (0, react.useState)({});
|
|
395
|
+
const [usageLoading, setUsageLoading] = (0, react.useState)({});
|
|
292
396
|
const mountedRef = (0, react.useRef)(true);
|
|
293
397
|
const pollersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
398
|
+
/** Providers with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
|
|
399
|
+
const usageInflightRef = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
294
400
|
const setProviderError = (0, react.useCallback)((provider, message) => {
|
|
295
401
|
if (!mountedRef.current) return;
|
|
296
402
|
setErrors((prev) => {
|
|
@@ -344,6 +450,63 @@ function SubscriptionsSection(props) {
|
|
|
344
450
|
pollersRef.current.clear();
|
|
345
451
|
};
|
|
346
452
|
}, [refresh, startPolling]);
|
|
453
|
+
const loadUsage = (0, react.useCallback)(async (provider) => {
|
|
454
|
+
if (rpc === void 0 || usageInflightRef.current.has(provider)) return;
|
|
455
|
+
usageInflightRef.current.add(provider);
|
|
456
|
+
setUsageLoading((prev) => ({
|
|
457
|
+
...prev,
|
|
458
|
+
[provider]: true
|
|
459
|
+
}));
|
|
460
|
+
try {
|
|
461
|
+
const usage = await callSubscriptionsAuth$1(rpc, "usage", { provider });
|
|
462
|
+
if (!mountedRef.current) return;
|
|
463
|
+
setUsages((prev) => ({
|
|
464
|
+
...prev,
|
|
465
|
+
[provider]: usage
|
|
466
|
+
}));
|
|
467
|
+
setUsageErrors((prev) => {
|
|
468
|
+
const next = { ...prev };
|
|
469
|
+
delete next[provider];
|
|
470
|
+
return next;
|
|
471
|
+
});
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (mountedRef.current) setUsageErrors((prev) => ({
|
|
474
|
+
...prev,
|
|
475
|
+
[provider]: messageOf(error)
|
|
476
|
+
}));
|
|
477
|
+
} finally {
|
|
478
|
+
usageInflightRef.current.delete(provider);
|
|
479
|
+
if (mountedRef.current) setUsageLoading((prev) => ({
|
|
480
|
+
...prev,
|
|
481
|
+
[provider]: false
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
}, [rpc]);
|
|
485
|
+
(0, react.useEffect)(() => {
|
|
486
|
+
for (const { id } of PROVIDERS) {
|
|
487
|
+
const status = statuses[id];
|
|
488
|
+
if (status === void 0) continue;
|
|
489
|
+
if (status.loggedIn) {
|
|
490
|
+
if (usages[id] === void 0 && usageErrors[id] === void 0) loadUsage(id);
|
|
491
|
+
} else if (usages[id] !== void 0 || usageErrors[id] !== void 0) {
|
|
492
|
+
setUsages((prev) => {
|
|
493
|
+
const next = { ...prev };
|
|
494
|
+
delete next[id];
|
|
495
|
+
return next;
|
|
496
|
+
});
|
|
497
|
+
setUsageErrors((prev) => {
|
|
498
|
+
const next = { ...prev };
|
|
499
|
+
delete next[id];
|
|
500
|
+
return next;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}, [
|
|
505
|
+
statuses,
|
|
506
|
+
usages,
|
|
507
|
+
usageErrors,
|
|
508
|
+
loadUsage
|
|
509
|
+
]);
|
|
347
510
|
const login = (0, react.useCallback)(async (provider) => {
|
|
348
511
|
if (rpc === void 0) return;
|
|
349
512
|
setProviderError(provider, void 0);
|
|
@@ -437,6 +600,9 @@ function SubscriptionsSection(props) {
|
|
|
437
600
|
}), PROVIDERS.map(({ id, name }) => {
|
|
438
601
|
const status = statuses[id];
|
|
439
602
|
const busy = status?.busy === true;
|
|
603
|
+
const usage = usages[id];
|
|
604
|
+
const usageError = usageErrors[id];
|
|
605
|
+
const showUsage = status?.loggedIn === true && usage?.supported !== false && (usage !== void 0 || usageError !== void 0 || usageLoading[id] === true);
|
|
440
606
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
441
607
|
style: styles$1.card,
|
|
442
608
|
children: [
|
|
@@ -491,6 +657,68 @@ function SubscriptionsSection(props) {
|
|
|
491
657
|
})
|
|
492
658
|
]
|
|
493
659
|
}),
|
|
660
|
+
showUsage && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
661
|
+
style: styles$1.usage,
|
|
662
|
+
children: [
|
|
663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
664
|
+
style: styles$1.usageHeader,
|
|
665
|
+
children: [
|
|
666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
667
|
+
style: styles$1.usageTitle,
|
|
668
|
+
children: t("usageTitle")
|
|
669
|
+
}),
|
|
670
|
+
usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
671
|
+
style: styles$1.usagePlan,
|
|
672
|
+
children: t("usagePlan", { plan: usage.plan })
|
|
673
|
+
}),
|
|
674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
675
|
+
type: "button",
|
|
676
|
+
style: {
|
|
677
|
+
...styles$1.usageRefresh,
|
|
678
|
+
...usageLoading[id] === true ? {
|
|
679
|
+
opacity: .5,
|
|
680
|
+
cursor: "default"
|
|
681
|
+
} : {}
|
|
682
|
+
},
|
|
683
|
+
disabled: usageLoading[id] === true,
|
|
684
|
+
onClick: () => {
|
|
685
|
+
loadUsage(id);
|
|
686
|
+
},
|
|
687
|
+
children: t("usageRefresh")
|
|
688
|
+
})
|
|
689
|
+
]
|
|
690
|
+
}),
|
|
691
|
+
usage === void 0 && usageError === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
692
|
+
style: styles$1.statusLine,
|
|
693
|
+
children: t("usageLoading")
|
|
694
|
+
}),
|
|
695
|
+
usageError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
696
|
+
style: styles$1.errorLine,
|
|
697
|
+
children: t("usageError", { message: usageError })
|
|
698
|
+
}),
|
|
699
|
+
usage?.windows !== void 0 && usage.windows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
700
|
+
style: styles$1.statusLine,
|
|
701
|
+
children: t("usageEmpty")
|
|
702
|
+
}),
|
|
703
|
+
(usage?.windows ?? []).map((window$1, index) => {
|
|
704
|
+
const percent = Math.min(100, Math.max(0, window$1.usedPercent));
|
|
705
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
706
|
+
style: styles$1.usageRow,
|
|
707
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
708
|
+
style: styles$1.usageMeta,
|
|
709
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: usageWindowLabel(t, window$1) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [`${String(Math.round(percent))}%`, window$1.resetsAt !== void 0 && ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
|
|
710
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
711
|
+
style: styles$1.usageTrack,
|
|
712
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
|
|
713
|
+
...styles$1.usageFill,
|
|
714
|
+
width: `${String(percent)}%`,
|
|
715
|
+
background: usageBarColor(percent)
|
|
716
|
+
} })
|
|
717
|
+
})]
|
|
718
|
+
}, index);
|
|
719
|
+
})
|
|
720
|
+
]
|
|
721
|
+
}),
|
|
494
722
|
busy && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
495
723
|
style: styles$1.manual,
|
|
496
724
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("manualSummary") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|