dsh-grok-subscription 0.1.3 → 0.1.12
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 +71 -3
- package/lib/client.js +192 -13
- package/lib/index.js +914 -309
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
- 登录后从 `GET https://cli-chat-proxy.grok.com/v1/models-v2` 获取账号可用模型;实时目录不可用或为空时使用小型静态目录(包含 `grok-4.7`)。`grok-4.7` 优先来自实时 `models-v2` 返回。
|
|
11
11
|
- 未登录时仍注册 adapter,但模型列表为 **0**(fail closed)。
|
|
12
12
|
- 设置页显示脱敏账户、登录状态、模型与最近一次目录错误,并提供 CLI 登录、设备码登录、从 Grok CLI 拉取、退出登录。
|
|
13
|
-
-
|
|
13
|
+
- **实验性用量显示**:登录后从未文档化的 `GET /v1/billing?format=credits` 读取 `config.creditUsagePercent`(兼容顶层)与周期结束时间;失败时显示「不可用」且**绝不编造百分比**。不影响 Chat。
|
|
14
|
+
- Pull / 登录 / 退出 / 目录刷新后通过 `llm/adapters-updated` 通知 Web 客户端刷新 Chat / New Session 模型选择器(emit 延后,避免与 Pull RPC 重入)。
|
|
15
|
+
- 设置页 Pull **零出站网络、内存优先**:只读本地 auth.json,同步写入 `memoryAccessToken` 后立即返回;credentials.set / credentialRef 延后(带超时),不堵 Pull RPC;目录与用量由后台及客户端 fire-and-forget 的 `catalog/refresh` / `usage/refresh` 更新(不占用全局 busy);`status` / `currentToken` 优先内存。
|
|
14
16
|
|
|
15
17
|
## 安装
|
|
16
18
|
|
|
@@ -23,7 +25,7 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
|
|
|
23
25
|
# 或本地路径
|
|
24
26
|
# dsh plugin --profile web add /absolute/path/to/dsh-grok-subscription
|
|
25
27
|
# 或 npm(发布后)
|
|
26
|
-
# dsh plugin --profile web add dsh-grok-subscription@0.1.
|
|
28
|
+
# dsh plugin --profile web add dsh-grok-subscription@0.1.12
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
然后重启 `dsh web`,打开 **Settings → Grok 订阅**。也可以在设置页点击“CLI 登录”或“设备码登录”;设备码流程会在启动 DSH 的终端中显示提示。登录完成后点击“从 Grok CLI 拉取”可立即同步;Chat 模型列表会随之刷新。
|
|
@@ -44,6 +46,40 @@ npm test
|
|
|
44
46
|
npm run build
|
|
45
47
|
```
|
|
46
48
|
|
|
49
|
+
## v0.1.12
|
|
50
|
+
|
|
51
|
+
升级后请用 `dsh web` 新打印的带 token URL 打开,并硬刷新(Ctrl+Shift+R),避免旧 `/plugins` client 缓存。
|
|
52
|
+
|
|
53
|
+
修复 0.1.11 实机:Settings → Plugins 仍卡在「Reading plugins…」,Pull 等到客户端 45s 超时。根因是 `status` / `currentToken` 仍可能无超时地 `await credentials.resolve` 与裸 `import('@deepseek-ai/dsh-credentials')`,楔住连接桥后 Plugins 清单也跟着挂。现:`credentialRefOf` 用 timed `optionalImport`;resolve 硬超时(默认 1.5s);`status` 先内存 / auth.json,再可选 credentials;宿主 RPC 每端点 8s 硬超时;`llm/adapters-updated` 一律延后;`inject` 保持 `['llm','web']`;客户端缺 connection 软跳过,RPC 客户端超时降至 12s。本地 0.1.12 供复测,未发 npm。
|
|
54
|
+
|
|
55
|
+
## v0.1.10
|
|
56
|
+
|
|
57
|
+
修复 0.1.9 实机:Pull 仍可能 45s 超时,且 **Settings → Plugins** 卡在 `Reading plugins…`(宿主 Settings/RPC 通道被楔住)。根因是 `apply`/`boot` 路径会 `await` 无超时的动态 `import`(`pi-ai` / `dsh-llm-pi-ai` / schemastery),且 `inject` 含 sticky 的 `credentials`。现 `inject` 收窄为 `['llm','web']`;`apply` 同步注册 duck adapter 后立即返回;schemastery / `session.pull` / pi-ai 升级全部 `setImmediate` 延后;所有动态 import 硬超时(默认 2.5s),超时则保持 duck。保留 0.1.9 内存优先 Pull 与 0.1.8 客户端 fire-and-forget。
|
|
58
|
+
|
|
59
|
+
## v0.1.9
|
|
60
|
+
|
|
61
|
+
修复 0.1.8 实机:Pull RPC 本身卡到客户端 45s 超时。根因是零网络 Pull 仍 `await storeToken` → `credentialRefOf()`(动态 import `@deepseek-ai/dsh-credentials`)无超时,import/set 与 DSH credentials 死锁时会拖死整个 Pull。现会话服务持有 `memoryAccessToken`;Pull 成功路径同步写入内存后立即返回,credentials 持久化与 clear 全部 `scheduleDeferred`;`currentToken` / `status` 优先内存。客户端仍保持 0.1.8 的 fire-and-forget 刷新行为。
|
|
62
|
+
|
|
63
|
+
## v0.1.8
|
|
64
|
+
|
|
65
|
+
修复 0.1.7 实机:Pull 成功后客户端仍 await `usage/refresh` 并占用全局 busy,导致 Working… 卡住、按钮一直 disabled。现 Pull/登录成功后立即清 busy 并显示成功提示;`catalog/refresh` 与 `usage/refresh` 为 fire-and-forget(不置全局 busy)。仅「刷新用量」按钮使用本地 `usageBusy`。`fetchBillingUsage` 增加独立于 AbortSignal 的硬 `Promise.race` 超时(默认 8s);空 JSON `{}` 与非 JSON Content-Type 会写入更明确的不可用原因。
|
|
66
|
+
|
|
67
|
+
## v0.1.7
|
|
68
|
+
|
|
69
|
+
修复设置页 Pull 仍卡住 Working…(0.1.6 去掉 billing 后仍可能卡在 `credentials.set` 或 `loadCatalog`):Pull 关键路径**零出站网络**,`storeToken` 硬超时(默认 5s),立即返回缓存目录/用量;后台 kick `refreshCatalog` + `refreshUsage`;客户端在 Pull 成功后另行 `catalog/refresh` 与 `usage/refresh`(独立 busy 文案)。`notifyCatalogChange` 仍仅延后触发。
|
|
70
|
+
|
|
71
|
+
## v0.1.6
|
|
72
|
+
|
|
73
|
+
修复设置页 Pull 卡住 Working…、用量长期「不可用」:Pull 关键路径不再等待 billing;`status()` 只返回缓存用量;Pull 成功后由客户端另行 `usage/refresh`;`llm/adapters-updated` 延后发出;客户端 RPC 增加超时保护。解析逻辑仍沿用 0.1.5 的 config.* 兼容。
|
|
74
|
+
|
|
75
|
+
## v0.1.5
|
|
76
|
+
|
|
77
|
+
修复实验性用量解析:官方 `/v1/billing?format=credits` 实际把 `creditUsagePercent`、`currentPeriod`、`productUsage` 放在 `config` 下(非顶层)。0.1.4 因严格读顶层字段在 QA 中显示「不可用」。现同时接受 `config.*` 与顶层;不可用原因会附带顶层 key 名以便排查。**绝不编造百分比。**
|
|
78
|
+
|
|
79
|
+
## v0.1.4
|
|
80
|
+
|
|
81
|
+
实验性 SuperGrok 周用量显示:设置页新增「用量(实验性)」区块,只读请求 `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits`(与现有 OAuth + CLI fingerprint 请求头相同)。解析 `config.creditUsagePercent` / 顶层 `creditUsagePercent` 与 `config.currentPeriod.end`;RPC 提供 `usage` / `usage/refresh`,永不回传 token。失败 fail-closed 为「不可用」。**账单 API 未公开文档,可能随时变更。**
|
|
82
|
+
|
|
47
83
|
## v0.1.3
|
|
48
84
|
|
|
49
85
|
修复多轮 Chat 中助手正文不显示的问题:自定义 Responses 流在 `block-end` 时把已累计的 text/reasoning 写成空字符串,Harness `BlockAssembler` 会用空块覆盖流式增量,导致第 2 轮起界面只见思考(如 “Deep diving…”)不见正文。同时为 `grok-build` 强制 `include: reasoning.encrypted_content`(与 pi-ai 对 `xai` 的处理对齐),保证推理密文可回放。
|
|
@@ -66,11 +102,12 @@ v0.1.0 范围仍适用:仅文本/工具调用所需的 Responses 流、账号
|
|
|
66
102
|
- 这是社区插件,不是 xAI、Grok 或 DeepSeek 官方产品。
|
|
67
103
|
- 订阅账号用于非官方客户端可能处于供应商条款灰色地带;**只使用你自己的账号**,风险自担。
|
|
68
104
|
- 官方文档没有发布完整 HTTP wire protocol;代理协议、请求头、模型 ID 或权限规则可能随时改变。
|
|
105
|
+
- 用量面板依赖的 `/v1/billing?format=credits` **未公开文档**,属实验性功能;字段形状或可用性可能变化,失败时仅显示不可用。
|
|
69
106
|
- 设置页发起 CLI 登录时,浏览器/设备码提示由官方 CLI 处理;远程部署请优先用 `grok login --device-auth`。
|
|
70
107
|
|
|
71
108
|
## English
|
|
72
109
|
|
|
73
|
-
`dsh-grok-subscription` is a small community DSH plugin that reuses an official Grok Build CLI SuperGrok/X Premium session. It adds a separate `grok-build` route, calls the subscription Responses proxy, discovers models from `/v1/models-v2` (with a signed-in-only fallback including `grok-4.7`), and never exposes tokens to browser RPC. After Pull/login/logout the plugin emits `llm/adapters-updated` so the Chat model picker refreshes. Install locally with:
|
|
110
|
+
`dsh-grok-subscription` is a small community DSH plugin that reuses an official Grok Build CLI SuperGrok/X Premium session. It adds a separate `grok-build` route, calls the subscription Responses proxy, discovers models from `/v1/models-v2` (with a signed-in-only fallback including `grok-4.7`), and never exposes tokens to browser RPC. Settings also shows an **experimental** weekly usage panel from the undocumented `/v1/billing?format=credits` endpoint (fail-closed; never invents percentages; chat is unaffected). After Pull/login/logout the plugin emits `llm/adapters-updated` so the Chat model picker refreshes. Install locally with:
|
|
74
111
|
|
|
75
112
|
```bash
|
|
76
113
|
dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
|
|
@@ -78,6 +115,36 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
|
|
|
78
115
|
|
|
79
116
|
Reinstall after upgrades, restart `dsh web`, then open **Settings → Grok Subscription**.
|
|
80
117
|
|
|
118
|
+
### v0.1.12
|
|
119
|
+
|
|
120
|
+
After upgrading, open the fresh token URL from `dsh web` and hard-refresh (Ctrl+Shift+R) so the old immutable `/plugins` client is not reused.
|
|
121
|
+
|
|
122
|
+
修复 0.1.11 实机:Settings → Plugins 仍卡在「Reading plugins…」,Pull 等到客户端 45s 超时。根因是 `status` / `currentToken` 仍可能无超时地 `await credentials.resolve` 与裸 `import('@deepseek-ai/dsh-credentials')`,楔住连接桥后 Plugins 清单也跟着挂。现:`credentialRefOf` 用 timed `optionalImport`;resolve 硬超时(默认 1.5s);`status` 先内存 / auth.json,再可选 credentials;宿主 RPC 每端点 8s 硬超时;`llm/adapters-updated` 一律延后;`inject` 保持 `['llm','web']`;客户端缺 connection 软跳过,RPC 客户端超时降至 12s。本地 0.1.12 供复测,未发 npm。
|
|
123
|
+
|
|
124
|
+
## v0.1.10
|
|
125
|
+
|
|
126
|
+
Fixes 0.1.9 live hang: Pull could still hit the 45s client timeout, and **Settings → Plugins** stuck on `Reading plugins…` (host Settings/RPC channel wedged). Root cause: `apply`/`boot` awaited untimed dynamic `import`s (`pi-ai` / `dsh-llm-pi-ai` / schemastery), and `inject` listed sticky `credentials`. Now `inject` is `['llm','web']`; `apply` synchronously registers a duck adapter and returns; schemastery / `session.pull` / pi-ai upgrade are deferred via `setImmediate`; every dynamic import has a hard timeout (default 2.5s) and falls back to duck. Keeps 0.1.9 memory-first Pull and 0.1.8 client fire-and-forget.
|
|
127
|
+
|
|
128
|
+
### v0.1.9
|
|
129
|
+
|
|
130
|
+
Fixes 0.1.8 live hang where the Pull RPC itself hit the client 45s timeout: zero-network Pull still awaited `storeToken` → `credentialRefOf()` (dynamic import of `@deepseek-ai/dsh-credentials`) with no timeout, so a credentials import/set deadlock stalled Pull. Session now keeps `memoryAccessToken`; success Pull writes memory and returns immediately, deferring credential persist/clear; `currentToken` / `status` prefer memory. Keeps 0.1.8 client fire-and-forget refresh behavior.
|
|
131
|
+
|
|
132
|
+
### v0.1.8
|
|
133
|
+
|
|
134
|
+
Fixes 0.1.7 live hang: after Pull the client no longer awaits `usage/refresh` while holding global busy (Working… / all buttons disabled). Pull/login clear busy and show success immediately; catalog + usage refresh are fire-and-forget. Only the Usage Refresh button uses local `usageBusy`. `fetchBillingUsage` has a hard Promise.race timeout (default 8s) independent of AbortSignal; empty `{}` and non-JSON Content-Type get clearer unavailable reasons.
|
|
135
|
+
|
|
136
|
+
### v0.1.7
|
|
137
|
+
Fixes Settings Pull hang after 0.1.6: zero-network Pull path, `storeToken` hard timeout (default 5s), returns cached catalog/usage immediately; background kick of `refreshCatalog` + `refreshUsage`; client follows up with `catalog/refresh` and `usage/refresh`. Catalog notify stays deferred.
|
|
138
|
+
|
|
139
|
+
## v0.1.6
|
|
140
|
+
Fixes Settings Pull hang (Working…) and stale Unavailable usage: Pull no longer awaits billing; `status()` returns cached usage only; client calls `usage/refresh` after successful Pull; catalog notify is deferred; client RPC has a safety timeout. Keeps 0.1.5 config.* parsing.
|
|
141
|
+
|
|
142
|
+
### v0.1.5
|
|
143
|
+
Fixes experimental usage parsing: live `/v1/billing?format=credits` nests `creditUsagePercent`, `currentPeriod`, and `productUsage` under `config` (not top-level). 0.1.4 failed QA with Unavailable. Parser now accepts `config.*` and top-level; unavailable reasons include top-level key names. Never invents percentages.
|
|
144
|
+
|
|
145
|
+
### v0.1.4
|
|
146
|
+
Experimental SuperGrok weekly usage in Settings: read-only `GET /v1/billing?format=credits` with the same OAuth + CLI fingerprint headers; shows used % and period end; RPC `usage` / `usage/refresh` never return tokens; fail closed to Unavailable. The billing API is undocumented and may change.
|
|
147
|
+
|
|
81
148
|
### v0.1.3
|
|
82
149
|
Fixes multi-turn Chat where assistant body text vanished from turn 2 onward: the custom Responses SSE mapper ended text/reasoning blocks with empty strings, so Harness `BlockAssembler` replaced streamed deltas with blanks (UI could show "Deep diving…" then no body). Also forces `include: reasoning.encrypted_content` for `grok-build` so encrypted reasoning can replay on later turns. This is unofficial, may fall into a vendor-ToS gray area, and the undocumented wire protocol can change. Use your own account only.
|
|
83
150
|
|
|
@@ -90,6 +157,7 @@ Fixes multi-turn Chat where assistant body text vanished from turn 2 onward: the
|
|
|
90
157
|
- `PiAiAdapter` 对 `grok-build` 路由的流式映射(若宿主没有 `@deepseek-ai/dsh-llm-pi-ai` / `@earendil-works/pi-ai`,插件会降级到自带的 Responses SSE MVP adapter)
|
|
91
158
|
- 官方 CLI `grok login` / `grok login --device-auth` 从 Settings 按钮拉起后的终端交互
|
|
92
159
|
- 实时 `GET /v1/models-v2` 是否返回 `grok-4.7`(目录为空时使用静态回退)
|
|
160
|
+
- Settings「用量(实验性)」:登录后刷新是否显示已用 % 与周期结束时间;401/网络失败是否显示「不可用」且无假百分比;Chat 在用量失败时仍可正常对话
|
|
93
161
|
|
|
94
162
|
## 致谢 / Acknowledgments
|
|
95
163
|
|
package/lib/client.js
CHANGED
|
@@ -21,7 +21,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var client_exports = {};
|
|
22
22
|
__export(client_exports, {
|
|
23
23
|
GrokSubscriptionSection: () => GrokSubscriptionSection,
|
|
24
|
+
RPC_CALL_TIMEOUT_MS: () => RPC_CALL_TIMEOUT_MS,
|
|
24
25
|
apply: () => apply,
|
|
26
|
+
callRpc: () => callRpc,
|
|
25
27
|
inject: () => inject,
|
|
26
28
|
pickCopy: () => pickCopy
|
|
27
29
|
});
|
|
@@ -51,12 +53,26 @@ var zh = {
|
|
|
51
53
|
deviceHint: "\u8BBE\u5907\u7801\u63D0\u793A\u4F1A\u51FA\u73B0\u5728 DSH \u7EC8\u7AEF\u3002\u4E5F\u53EF\u624B\u52A8\u8FD0\u884C grok login --device-auth\uFF0C\u5B8C\u6210\u540E\u70B9\u201C\u4ECE Grok CLI \u62C9\u53D6\u201D\u3002",
|
|
52
54
|
pullHint: "\u5B89\u5168\u8BFB\u53D6 ~/.grok/auth.json\uFF08\u62D2\u7EDD\u7B26\u53F7\u94FE\u63A5\u4E0E\u7EC4/\u5176\u4ED6\u4EBA\u53EF\u8BFB\uFF09\uFF0C\u53EA\u628A\u77ED\u671F access token \u5199\u5165 DSH\u3002",
|
|
53
55
|
busy: "\u5904\u7406\u4E2D\u2026",
|
|
56
|
+
busyCatalog: "\u6B63\u5728\u5237\u65B0\u6A21\u578B\u76EE\u5F55\u2026",
|
|
57
|
+
busyUsage: "\u6B63\u5728\u5237\u65B0\u7528\u91CF\u2026",
|
|
54
58
|
error: "\u64CD\u4F5C\u5931\u8D25",
|
|
55
59
|
pullOk: "\u5DF2\u4ECE Grok CLI \u62C9\u53D6\u6210\u529F",
|
|
56
60
|
loginOk: "\u767B\u5F55\u5B8C\u6210\uFF0C\u4F1A\u8BDD\u5DF2\u540C\u6B65",
|
|
57
61
|
logoutOk: "\u5DF2\u9000\u51FA\u767B\u5F55",
|
|
58
62
|
cliMissing: "\u672A\u53D1\u73B0 grok CLI\u3002\u8BF7\u5148\u5B89\u88C5\u5B98\u65B9 CLI \u5E76\u8FD0\u884C grok login\uFF0C\u7136\u540E\u62C9\u53D6\u3002",
|
|
59
|
-
caveats: "\u793E\u533A\u63D2\u4EF6\uFF0C\u4E0D\u662F\u5B98\u65B9\u4EA7\u54C1\u3002\u8BA2\u9605\u7528\u4E8E\u975E\u5B98\u65B9\u5BA2\u6237\u7AEF\u53EF\u80FD\u5904\u4E8E\u4F9B\u5E94\u5546\u6761\u6B3E\u7070\u8272\u5730\u5E26\uFF1B\u53EA\u4F7F\u7528\u4F60\u81EA\u5DF1\u7684\u8D26\u53F7\u3002\u534F\u8BAE\u53EF\u80FD\u53D8\u5316\uFF0C\u56E0\u4E3A\u5B98\u65B9\u6587\u6863\u672A\u516C\u5F00\u5B8C\u6574 HTTP wire protocol\u3002"
|
|
63
|
+
caveats: "\u793E\u533A\u63D2\u4EF6\uFF0C\u4E0D\u662F\u5B98\u65B9\u4EA7\u54C1\u3002\u8BA2\u9605\u7528\u4E8E\u975E\u5B98\u65B9\u5BA2\u6237\u7AEF\u53EF\u80FD\u5904\u4E8E\u4F9B\u5E94\u5546\u6761\u6B3E\u7070\u8272\u5730\u5E26\uFF1B\u53EA\u4F7F\u7528\u4F60\u81EA\u5DF1\u7684\u8D26\u53F7\u3002\u534F\u8BAE\u53EF\u80FD\u53D8\u5316\uFF0C\u56E0\u4E3A\u5B98\u65B9\u6587\u6863\u672A\u516C\u5F00\u5B8C\u6574 HTTP wire protocol\u3002",
|
|
64
|
+
usageTitle: "\u7528\u91CF\uFF08\u5B9E\u9A8C\u6027\uFF09",
|
|
65
|
+
usageSubtitle: "\u6765\u81EA\u672A\u6587\u6863\u5316\u7684\u8BA2\u9605 billing API\uFF08/v1/billing?format=credits\uFF09\uFF0C\u4EC5\u4F9B\u53C2\u8003\uFF0C\u53EF\u80FD\u968F\u65F6\u5931\u6548\u3002",
|
|
66
|
+
usageUsed: "\u5DF2\u7528",
|
|
67
|
+
usageRemaining: "\u5269\u4F59\u7EA6",
|
|
68
|
+
usageReset: "\u5468\u671F\u7ED3\u675F / \u91CD\u7F6E",
|
|
69
|
+
usageUnavailable: "\u4E0D\u53EF\u7528",
|
|
70
|
+
usageRefresh: "\u5237\u65B0\u7528\u91CF",
|
|
71
|
+
usageOpenGrok: "\u5728 grok.com \u67E5\u770B\u7528\u91CF",
|
|
72
|
+
usageProduct: "\u4EA7\u54C1\u660E\u7EC6",
|
|
73
|
+
usageFetchedAt: "\u6700\u8FD1\u5237\u65B0",
|
|
74
|
+
usageRefreshOk: "\u7528\u91CF\u5DF2\u5237\u65B0",
|
|
75
|
+
catalogRefreshOk: "\u6A21\u578B\u76EE\u5F55\u5DF2\u5237\u65B0"
|
|
60
76
|
};
|
|
61
77
|
var en = {
|
|
62
78
|
nav: "Grok Subscription",
|
|
@@ -80,12 +96,26 @@ var en = {
|
|
|
80
96
|
deviceHint: "The device-code prompt appears in the DSH terminal. You can also run grok login --device-auth yourself, then Pull from Grok CLI.",
|
|
81
97
|
pullHint: "Reads ~/.grok/auth.json securely (refuses symlinks and group/other-readable files) and stores only the short-lived access token in DSH.",
|
|
82
98
|
busy: "Working\u2026",
|
|
99
|
+
busyCatalog: "Refreshing model catalog\u2026",
|
|
100
|
+
busyUsage: "Refreshing usage\u2026",
|
|
83
101
|
error: "Request failed",
|
|
84
102
|
pullOk: "Pulled from Grok CLI successfully",
|
|
85
103
|
loginOk: "Signed in and session synced",
|
|
86
104
|
logoutOk: "Signed out",
|
|
87
105
|
cliMissing: "The grok CLI was not found. Install it, run grok login, then pull.",
|
|
88
|
-
caveats: "Community plugin, not an official product. Using a subscription from an unofficial client may be a vendor-ToS gray area; use your own account only. The protocol can change because official docs do not publish the full HTTP wire protocol."
|
|
106
|
+
caveats: "Community plugin, not an official product. Using a subscription from an unofficial client may be a vendor-ToS gray area; use your own account only. The protocol can change because official docs do not publish the full HTTP wire protocol.",
|
|
107
|
+
usageTitle: "Usage (experimental)",
|
|
108
|
+
usageSubtitle: "From the undocumented subscription billing API (/v1/billing?format=credits). Informational only; the endpoint may change or disappear.",
|
|
109
|
+
usageUsed: "Used",
|
|
110
|
+
usageRemaining: "Remaining \u2248",
|
|
111
|
+
usageReset: "Period end / reset",
|
|
112
|
+
usageUnavailable: "Unavailable",
|
|
113
|
+
usageRefresh: "Refresh usage",
|
|
114
|
+
usageOpenGrok: "View usage on grok.com",
|
|
115
|
+
usageProduct: "Product breakdown",
|
|
116
|
+
usageFetchedAt: "Last refreshed",
|
|
117
|
+
usageRefreshOk: "Usage refreshed",
|
|
118
|
+
catalogRefreshOk: "Model catalog refreshed"
|
|
89
119
|
};
|
|
90
120
|
|
|
91
121
|
// src/rpc-contract.js
|
|
@@ -96,7 +126,9 @@ var RPC_ENDPOINTS = Object.freeze([
|
|
|
96
126
|
"login/device",
|
|
97
127
|
"pull",
|
|
98
128
|
"logout",
|
|
99
|
-
"catalog/refresh"
|
|
129
|
+
"catalog/refresh",
|
|
130
|
+
"usage",
|
|
131
|
+
"usage/refresh"
|
|
100
132
|
]);
|
|
101
133
|
function createRpcClient(transport) {
|
|
102
134
|
return Object.freeze({
|
|
@@ -115,6 +147,7 @@ function unwrap(response) {
|
|
|
115
147
|
|
|
116
148
|
// src/constants.js
|
|
117
149
|
var LOCALE_NS = "settings.grokSubscription";
|
|
150
|
+
var USAGE_PAGE_URL = "https://grok.com/?_s=usage";
|
|
118
151
|
var SESSION_AUTH_MODES = Object.freeze(["oidc", "external", "web_login", "grok"]);
|
|
119
152
|
var API_KEY_AUTH_MODES = Object.freeze(["api_key"]);
|
|
120
153
|
var STREAM_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -126,6 +159,7 @@ var FALLBACK_MODEL_IDS = Object.freeze(["grok-4.7", "grok-4.6", "grok-4.5"]);
|
|
|
126
159
|
// src/client.jsx
|
|
127
160
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
128
161
|
var inject = ["slots", "locale", "connection", "settingsScope"];
|
|
162
|
+
var RPC_CALL_TIMEOUT_MS = 12e3;
|
|
129
163
|
function pickCopy(locale) {
|
|
130
164
|
const language = typeof locale === "string" ? locale : locale?.language ?? locale?.lang;
|
|
131
165
|
const resolved = language ?? (typeof navigator !== "undefined" ? navigator.language : "zh-CN");
|
|
@@ -136,30 +170,146 @@ function sourceLabel(source, t) {
|
|
|
136
170
|
if (source === "fallback") return t("catalogSourceFallback");
|
|
137
171
|
return t("catalogSourceSignedOut");
|
|
138
172
|
}
|
|
173
|
+
function formatPercent(value) {
|
|
174
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
175
|
+
const rounded = Math.round(value * 10) / 10;
|
|
176
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
177
|
+
}
|
|
178
|
+
async function callRpc(rpc, endpoint, payload = {}) {
|
|
179
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
180
|
+
let timer;
|
|
181
|
+
try {
|
|
182
|
+
const call = rpc.call(CHANNEL, endpoint, payload, controller?.signal);
|
|
183
|
+
const timeout = new Promise((_, reject) => {
|
|
184
|
+
timer = setTimeout(() => {
|
|
185
|
+
try {
|
|
186
|
+
controller?.abort();
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
reject(new Error(`Request timed out after ${RPC_CALL_TIMEOUT_MS}ms`));
|
|
190
|
+
}, RPC_CALL_TIMEOUT_MS);
|
|
191
|
+
});
|
|
192
|
+
return unwrap(await Promise.race([call, timeout]));
|
|
193
|
+
} finally {
|
|
194
|
+
if (timer) clearTimeout(timer);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function UsagePanel({ usage, t, usageBusy, onRefresh, signedIn }) {
|
|
198
|
+
const ok = usage?.status === "ok";
|
|
199
|
+
const used = ok ? formatPercent(usage.usedPercent) : void 0;
|
|
200
|
+
const remaining = ok ? formatPercent(usage.remainingPercent) : void 0;
|
|
201
|
+
const resetLabel = usage?.periodEndLocal || usage?.periodEnd;
|
|
202
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "usageBlock", children: [
|
|
203
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("usageTitle") }),
|
|
204
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("usageSubtitle") }),
|
|
205
|
+
ok && used !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
206
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
|
|
207
|
+
t("usageUsed"),
|
|
208
|
+
": ",
|
|
209
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
210
|
+
used,
|
|
211
|
+
"%"
|
|
212
|
+
] }),
|
|
213
|
+
remaining !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
214
|
+
" \xB7 ",
|
|
215
|
+
t("usageRemaining"),
|
|
216
|
+
" ",
|
|
217
|
+
remaining,
|
|
218
|
+
"%"
|
|
219
|
+
] }) : null
|
|
220
|
+
] }),
|
|
221
|
+
resetLabel ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "muted", children: [
|
|
222
|
+
t("usageReset"),
|
|
223
|
+
": ",
|
|
224
|
+
resetLabel,
|
|
225
|
+
usage.periodEnd && usage.periodEndLocal ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
226
|
+
" (",
|
|
227
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: usage.periodEnd }),
|
|
228
|
+
")"
|
|
229
|
+
] }) : null
|
|
230
|
+
] }) : null,
|
|
231
|
+
Array.isArray(usage.productUsage) && usage.productUsage.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
232
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("usageProduct") }),
|
|
233
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: usage.productUsage.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
|
|
234
|
+
row.name ?? "\u2014",
|
|
235
|
+
typeof row.usedPercent === "number" ? ` \xB7 ${formatPercent(row.usedPercent)}%` : ""
|
|
236
|
+
] }, `${row.name ?? "row"}-${index}`)) })
|
|
237
|
+
] }) : null,
|
|
238
|
+
usage.fetchedAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "muted", children: [
|
|
239
|
+
t("usageFetchedAt"),
|
|
240
|
+
": ",
|
|
241
|
+
usage.fetchedAt
|
|
242
|
+
] }) : null
|
|
243
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
|
|
244
|
+
t("usageUnavailable"),
|
|
245
|
+
usage?.reason ? `: ${usage.reason}` : ""
|
|
246
|
+
] }),
|
|
247
|
+
usageBusy ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("busyUsage") }) : null,
|
|
248
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "row", children: [
|
|
249
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: Boolean(usageBusy) || !signedIn, onClick: onRefresh, children: t("usageRefresh") }),
|
|
250
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: USAGE_PAGE_URL, target: "_blank", rel: "noreferrer", children: t("usageOpenGrok") })
|
|
251
|
+
] })
|
|
252
|
+
] });
|
|
253
|
+
}
|
|
139
254
|
function GrokSubscriptionSection({ rpc, t }) {
|
|
140
255
|
const [busy, setBusy] = (0, import_react.useState)("");
|
|
256
|
+
const [usageBusy, setUsageBusy] = (0, import_react.useState)(false);
|
|
141
257
|
const [error, setError] = (0, import_react.useState)("");
|
|
142
258
|
const [notice, setNotice] = (0, import_react.useState)("");
|
|
143
259
|
const [status, setStatus] = (0, import_react.useState)(void 0);
|
|
260
|
+
const initialUsageKick = (0, import_react.useRef)(false);
|
|
261
|
+
const applyPartial = (value) => {
|
|
262
|
+
if (value?.account || value?.catalog || value?.usage) {
|
|
263
|
+
setStatus((current) => ({ ...current, ...value }));
|
|
264
|
+
}
|
|
265
|
+
};
|
|
144
266
|
const load = async () => {
|
|
145
|
-
const value =
|
|
267
|
+
const value = await callRpc(rpc, "status", {});
|
|
146
268
|
setStatus(value);
|
|
147
269
|
setError("");
|
|
270
|
+
return value;
|
|
148
271
|
};
|
|
149
272
|
(0, import_react.useEffect)(() => {
|
|
150
|
-
void load().
|
|
273
|
+
void load().then((value) => {
|
|
274
|
+
if (initialUsageKick.current) return;
|
|
275
|
+
if (value?.account?.signedIn !== true) return;
|
|
276
|
+
initialUsageKick.current = true;
|
|
277
|
+
void callRpc(rpc, "usage/refresh", {}).then((partial) => {
|
|
278
|
+
applyPartial(partial);
|
|
279
|
+
}).catch(() => {
|
|
280
|
+
});
|
|
281
|
+
}).catch((item) => {
|
|
151
282
|
setNotice("");
|
|
152
283
|
setError(item instanceof Error ? item.message : String(item));
|
|
153
284
|
});
|
|
154
285
|
}, [rpc]);
|
|
286
|
+
const kickFollowUpRefresh = () => {
|
|
287
|
+
void callRpc(rpc, "catalog/refresh", {}).then((partial) => {
|
|
288
|
+
applyPartial(partial);
|
|
289
|
+
}).catch(() => {
|
|
290
|
+
});
|
|
291
|
+
void callRpc(rpc, "usage/refresh", {}).then((partial) => {
|
|
292
|
+
applyPartial(partial);
|
|
293
|
+
}).catch(() => {
|
|
294
|
+
});
|
|
295
|
+
};
|
|
155
296
|
const run = async (endpoint) => {
|
|
156
|
-
|
|
297
|
+
const isUsageRefresh = endpoint === "usage/refresh";
|
|
298
|
+
if (isUsageRefresh) {
|
|
299
|
+
setUsageBusy(true);
|
|
300
|
+
} else {
|
|
301
|
+
setBusy(endpoint);
|
|
302
|
+
}
|
|
157
303
|
setError("");
|
|
158
304
|
setNotice("");
|
|
305
|
+
let value;
|
|
159
306
|
try {
|
|
160
|
-
|
|
161
|
-
if (value?.account || value?.catalog
|
|
162
|
-
|
|
307
|
+
value = await callRpc(rpc, endpoint, {});
|
|
308
|
+
if (value?.account || value?.catalog || value?.usage) {
|
|
309
|
+
applyPartial(value);
|
|
310
|
+
} else {
|
|
311
|
+
await load();
|
|
312
|
+
}
|
|
163
313
|
if (value?.ok === false && value.error) {
|
|
164
314
|
setNotice("");
|
|
165
315
|
setError(value.error);
|
|
@@ -168,7 +318,8 @@ function GrokSubscriptionSection({ rpc, t }) {
|
|
|
168
318
|
pull: "pullOk",
|
|
169
319
|
"login/cli": "loginOk",
|
|
170
320
|
"login/device": "loginOk",
|
|
171
|
-
logout: "logoutOk"
|
|
321
|
+
logout: "logoutOk",
|
|
322
|
+
"usage/refresh": "usageRefreshOk"
|
|
172
323
|
}[endpoint];
|
|
173
324
|
if (successKey) setNotice(t(successKey));
|
|
174
325
|
}
|
|
@@ -176,8 +327,11 @@ function GrokSubscriptionSection({ rpc, t }) {
|
|
|
176
327
|
setNotice("");
|
|
177
328
|
setError(item instanceof Error ? item.message : String(item));
|
|
178
329
|
} finally {
|
|
179
|
-
|
|
330
|
+
if (isUsageRefresh) setUsageBusy(false);
|
|
331
|
+
else setBusy("");
|
|
180
332
|
}
|
|
333
|
+
const shouldFollowUp = (endpoint === "pull" || endpoint === "login/cli" || endpoint === "login/device") && value?.ok !== false && !value?.error;
|
|
334
|
+
if (shouldFollowUp) kickFollowUpRefresh();
|
|
181
335
|
};
|
|
182
336
|
const account = status?.account;
|
|
183
337
|
const catalog = status?.catalog;
|
|
@@ -189,11 +343,13 @@ function GrokSubscriptionSection({ rpc, t }) {
|
|
|
189
343
|
.grokSubscription h2 { margin: 0 0 4px; font-size: 1.15rem; }
|
|
190
344
|
.grokSubscription p, .grokSubscription li { line-height: 1.5; }
|
|
191
345
|
.grokSubscription .muted { opacity: 0.78; font-size: 0.92rem; }
|
|
192
|
-
.grokSubscription .row { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
346
|
+
.grokSubscription .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
193
347
|
.grokSubscription button { cursor: pointer; }
|
|
194
348
|
.grokSubscription .error { color: #b42318; }
|
|
195
349
|
.grokSubscription .notice { color: #067647; }
|
|
196
350
|
.grokSubscription ul { margin: 0; padding-left: 1.2rem; }
|
|
351
|
+
.grokSubscription .usageBlock { display: grid; gap: 8px; padding: 10px 0; border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent); }
|
|
352
|
+
.grokSubscription a { color: inherit; }
|
|
197
353
|
` }),
|
|
198
354
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
199
355
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: t("title") }),
|
|
@@ -223,6 +379,16 @@ function GrokSubscriptionSection({ rpc, t }) {
|
|
|
223
379
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("deviceHint") }),
|
|
224
380
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("pullHint") }),
|
|
225
381
|
status?.cliAvailable === false ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("cliMissing") }) : null,
|
|
382
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
383
|
+
UsagePanel,
|
|
384
|
+
{
|
|
385
|
+
usage: status?.usage,
|
|
386
|
+
t,
|
|
387
|
+
usageBusy,
|
|
388
|
+
signedIn,
|
|
389
|
+
onRefresh: () => void run("usage/refresh")
|
|
390
|
+
}
|
|
391
|
+
),
|
|
226
392
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
227
393
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("models") }),
|
|
228
394
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: sourceLabel(catalog?.source, t) }),
|
|
@@ -242,7 +408,20 @@ function GrokSubscriptionSection({ rpc, t }) {
|
|
|
242
408
|
}
|
|
243
409
|
function apply(ctx) {
|
|
244
410
|
ctx.effect?.(() => ctx.locale?.register?.(LOCALE_NS, { zh, en }), "grok-subscription: copy");
|
|
245
|
-
|
|
411
|
+
let connection;
|
|
412
|
+
try {
|
|
413
|
+
connection = typeof ctx.get === "function" ? ctx.get("connection") : void 0;
|
|
414
|
+
} catch {
|
|
415
|
+
connection = void 0;
|
|
416
|
+
}
|
|
417
|
+
if (!connection) connection = ctx.connection;
|
|
418
|
+
if (!connection?.rpc) {
|
|
419
|
+
try {
|
|
420
|
+
ctx.logger?.warn?.("Grok subscription Settings section skipped: connection unavailable");
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
246
425
|
const rpc = createRpcClient(connection.rpc);
|
|
247
426
|
const bound = ctx.locale?.bind?.(LOCALE_NS);
|
|
248
427
|
const t = (key) => {
|