dsh-subusage 0.8.1 → 0.8.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/README.md +8 -5
- package/lib/client.js +49 -13
- package/lib/index.js +70 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 安装与快速开始
|
|
8
8
|
|
|
9
|
-
当前仓库版本 **0.8.
|
|
9
|
+
当前仓库版本 **0.8.3**,可从 GitHub Release 获取。npm 自动发布采用 Trusted Publishing(GitHub OIDC);可用版本见 [npm](https://www.npmjs.com/package/dsh-subusage)。安装时使用目标 DSH 部署提供的插件管理器或 CLI,并启用本 bundle:
|
|
10
10
|
|
|
11
11
|
| 来源 | 安装标识或下载地址 |
|
|
12
12
|
|---|---|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
### 药丸
|
|
31
31
|
|
|
32
|
-
选中已开启且未被默认隐藏的模型商时显示,例如 `✓ 余 87%`、`⚠ 7d 余 12%`、`✕ 7d 已达限额`。多窗口取最差一窗,点击看完整详情。Command Code 药丸弹层底部可切换提供方插件的服务账户(自动轮换 / 默认账户 /
|
|
32
|
+
选中已开启且未被默认隐藏的模型商时显示,例如 `✓ 余 87%`、`⚠ 7d 余 12%`、`✕ 7d 已达限额`。多窗口取最差一窗,点击看完整详情。Command Code 药丸弹层底部可切换提供方插件的服务账户(自动轮换 / 默认账户 / 各额外账户,与其设置页同一开关,即时生效);切换后药丸与弹层上方的用量同步改为所选账户的额度(自动轮换时仍显示默认账户/顶层 Key 的用量,弹层有标注说明)。关闭默认隐藏后,缺凭据时显示配置指引;可保留的临时网络错误会明确标注缓存,而非冒充当前成功。
|
|
33
33
|
|
|
34
34
|
### 设置
|
|
35
35
|
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
| Z.ai 国际 | `ZAI_CODING_API_KEY` | `zai-coding`,默认关闭,独立国际 Coding Plan Key |
|
|
52
52
|
| Synthetic | `SYNTHETIC_API_KEY` | `synthetic`,默认关闭,模型订阅请求额度 |
|
|
53
53
|
| NanoGPT | `NANOGPT_API_KEY` | `nanogpt`,默认关闭;也可手动填写 Usage only 管理令牌 |
|
|
54
|
-
| Kimi | `KIMI_CODING_API_KEY` | 需要 Kimi Coding Key,不是 Moonshot 开平台 Key |
|
|
54
|
+
| Kimi | `KIMI_CODING_API_KEY` | 需要 Kimi Coding Key,不是 Moonshot 开平台 Key;窗口集合按套餐体系下发(老套餐 5 小时 + 7 天,新套餐 Go / Plus 为 5 小时 + 月度) |
|
|
55
55
|
| MiMo | 不使用 API Key | 通过控制台 Cookie 会话读取 |
|
|
56
56
|
| OpenCode Go | `OPENCODE_API_KEY` | OpenCode Go Key |
|
|
57
57
|
| MiniMax 国际 | `MINIMAX_API_KEY` | `minimax` 路由;国际站订阅 Key |
|
|
@@ -100,7 +100,7 @@ JSON 中有域名的条目按 `platform.xiaomimimo.com` 的 Cookie 域规则过
|
|
|
100
100
|
| Z.ai 国际 | 5 小时、每周(按响应) | Token / Credits 配额,区域凭据独立 |
|
|
101
101
|
| Synthetic | 滚动订阅池 | 已用/总请求次数、重置时间 |
|
|
102
102
|
| NanoGPT | 每日、每周、试用周期(按响应) | 输入 Token 数、重置时间;不包含图像额度 |
|
|
103
|
-
| Kimi | 5 小时、7
|
|
103
|
+
| Kimi | 5 小时、7 天、月度(按账户下发) | 已用百分比、套餐档 |
|
|
104
104
|
| MiMo | 本周期额度池 | 用量明细、重置时间、套餐与余额 |
|
|
105
105
|
| OpenCode Go | 滚动、每周、每月 | 已用百分比、重置时间 |
|
|
106
106
|
| MiniMax 国际 / 中国 | 短周期(通常 5 小时)、每周 | 通用/编程池已用百分比、可信计数明细、重置时间 |
|
|
@@ -108,6 +108,8 @@ JSON 中有域名的条目按 `platform.xiaomimimo.com` 的 Cookie 域规则过
|
|
|
108
108
|
|
|
109
109
|
非公开控制台接口可能变更;缺失或非法百分比不会当作零用量。部分数据、未知额度和暂时失败有明确状态,不能据此保证推理接口一定可用。
|
|
110
110
|
|
|
111
|
+
Kimi 的窗口集合随套餐体系变化:老套餐(节奏命名,如 Allegro)返回 5 小时与 7 天,新套餐(Go / Plus 命名)返回 5 小时与月度总额,因此同一插件在不同账号上显示的窗口数可以不同;月度池里的 Code 份额不是独立预算,不单独成窗。插件按接口实际下发的窗口解析,不要求固定集合。
|
|
112
|
+
|
|
111
113
|
|
|
112
114
|
## 常见问题
|
|
113
115
|
|
|
@@ -116,6 +118,7 @@ JSON 中有域名的条目按 `platform.xiaomimimo.com` 的 Cookie 域规则过
|
|
|
116
118
|
| 设置页没有提供商标签 | 展开提供商管理,确认开关和凭据;无凭据的条目默认隐藏,可临时关闭自动隐藏查看指引 |
|
|
117
119
|
| 已配置凭据却没有药丸 | 确认当前模型的提供商 ID 与支持表一致、检测已开启;自定义 ID 可在设置页查看,但不会自动映射 |
|
|
118
120
|
| 显示认证失效 | 检查是否用了对应产品/区域的订阅 Key;MiMo 重新登录或导入 Cookie;旧额度不会当作有效数据保留 |
|
|
121
|
+
| Kimi 缺少 7 天窗口,或提示结构错误 | 窗口集合按套餐体系下发:老套餐(节奏命名,如 Allegro)为 5 小时 + 7 天,新套餐(Go / Plus 命名)为 5 小时 + 月度总额。0.8.3 起按实际下发的窗口解析,不再要求 7 天窗口;升级后仍报「Invalid Kimi usage response」即为未识别的字段结构,插件不会猜测额度,可回报该响应 |
|
|
119
122
|
| 显示缓存、部分数据或额度未知 | 查看更新时间和错误说明;缓存来自之前的成功读取,部分数据不保证有可用额度,余额也不等于订阅余量 |
|
|
120
123
|
| 刷新后数字暂未变化 | 成功结果按提供商缓存 60 秒;手动刷新可跳过普通 TTL,但仍遵守限流退避和 Retry-After |
|
|
121
124
|
| MiMo 自动登录无法启动 | Host 所在机器需安装 Google Chrome 并有桌面环境;远程或无桌面部署使用手动导入 |
|
|
@@ -124,7 +127,7 @@ JSON 中有域名的条目按 `platform.xiaomimimo.com` 的 Cookie 域规则过
|
|
|
124
127
|
| 保存成功但验证失败 | 凭据已保存,检查网络、区域或账号后重试;保存与在线验证分别反馈 |
|
|
125
128
|
| 保存提示配置已改变 | 另一窗口或实例更新了设置;重新读取配置后再编辑,避免旧表单覆盖新值 |
|
|
126
129
|
| 升级后仍是旧界面或 RPC 不匹配 | 确认运行中的 Host 和 Client 都已重载;仅刷新页面不能保证 Host 升级 |
|
|
127
|
-
| Command Code 额度与实际账户不同 |
|
|
130
|
+
| Command Code 额度与实际账户不同 | 可在药丸弹层切换提供方插件的服务账户(activeAccount),切换后药丸与弹层用量同步显示所选账户的额度;「自动轮换」时仍显示默认账户(顶层 Key)的用量,不跟随轮换中的实际服务账户,提供方自定义 apiBase 也不跟随 |
|
|
128
131
|
|
|
129
132
|
## 提供商 ID 与默认开关
|
|
130
133
|
|
package/lib/client.js
CHANGED
|
@@ -106,13 +106,15 @@ window.__ModuleLoader__.load({
|
|
|
106
106
|
pillNeedCommandcode: "需配置 —— 可在 设置 → Command Code 登录、运行 cmd login,或导出 COMMANDCODE_API_KEY 后重试。",
|
|
107
107
|
accountLabel: "账户",
|
|
108
108
|
commandcodeManaged: "凭据由 Command Code 提供方插件(设置 → Command Code)管理;本页只读、不保存凭据。用量由本插件直接拉取其计费接口,取用同一凭据来源(凭据服务 COMMANDCODE_API_KEY → 启动环境 → ~/.commandcode/auth.json)。",
|
|
109
|
-
commandcodeHelp: "并行读取 /alpha/billing/credits 和 /alpha/billing/subscriptions(默认 API 地址 https://api.commandcode.ai
|
|
109
|
+
commandcodeHelp: "并行读取 /alpha/billing/credits 和 /alpha/billing/subscriptions(默认 API 地址 https://api.commandcode.ai)。月总额按已知套餐快照推算,未知套餐只显示月剩余;月池耗尽不代表已购/赠送池不可用。显示药丸弹层中选择的账户的额度(未选择时为默认账户/顶层 Key),可能与实际服务账户不同;插件里自定义 apiBase 不会被跟随。",
|
|
110
110
|
ccAccountsTitle: "Command Code 账户",
|
|
111
111
|
ccAccountAuto: "自动轮换",
|
|
112
112
|
ccAccountDefault: "默认账户",
|
|
113
113
|
ccAccountActive: "使用中",
|
|
114
|
-
ccAccountsHint: "切换对 Command Code
|
|
114
|
+
ccAccountsHint: "切换对 Command Code 提供方插件全局生效(与其设置页同一开关);上方用量同步切换为所选账户的额度。",
|
|
115
115
|
ccAccountsUnavailable: "账户列表不可用(需要 Command Code 提供方插件在运行)",
|
|
116
|
+
ccUsageAccount: "上方用量:{account}",
|
|
117
|
+
ccUsageAutoNote: "自动轮换时上方用量仍是默认账户(顶层 Key),不跟随实际服务账户",
|
|
116
118
|
ccSwitching: "切换中…",
|
|
117
119
|
ccSwitchFailed: "切换失败"
|
|
118
120
|
};
|
|
@@ -204,13 +206,15 @@ window.__ModuleLoader__.load({
|
|
|
204
206
|
pillNeedCommandcode: "Setup required — sign in under Settings → Command Code, run cmd login, or export COMMANDCODE_API_KEY and retry.",
|
|
205
207
|
accountLabel: "Account",
|
|
206
208
|
commandcodeManaged: "Credentials are managed by the Command Code provider plugin (Settings → Command Code); this page is read-only and stores nothing. Usage is fetched directly by this plugin from its billing endpoint, using the same credential sources (credential service COMMANDCODE_API_KEY → launch environment → ~/.commandcode/auth.json).",
|
|
207
|
-
commandcodeHelp: "Parallel requests to /alpha/billing/credits and /alpha/billing/subscriptions (default API base https://api.commandcode.ai). Monthly totals use a known-plan snapshot; unknown plans show remaining credits only. Monthly-pool depletion does not imply purchased/free credits are unusable.
|
|
209
|
+
commandcodeHelp: "Parallel requests to /alpha/billing/credits and /alpha/billing/subscriptions (default API base https://api.commandcode.ai). Monthly totals use a known-plan snapshot; unknown plans show remaining credits only. Monthly-pool depletion does not imply purchased/free credits are unusable. Shows the quota of the account selected in the pill popover (the default/top-level key account when none is selected), which may differ from the serving account; a custom apiBase in the provider plugin is not followed.",
|
|
208
210
|
ccAccountsTitle: "Command Code accounts",
|
|
209
211
|
ccAccountAuto: "Automatic (rotation order)",
|
|
210
212
|
ccAccountDefault: "Default account",
|
|
211
213
|
ccAccountActive: "in use",
|
|
212
|
-
ccAccountsHint: "Switching applies globally to the Command Code provider plugin (same switch as its settings page); the
|
|
214
|
+
ccAccountsHint: "Switching applies globally to the Command Code provider plugin (same switch as its settings page); the usage above switches to the selected account's quota.",
|
|
213
215
|
ccAccountsUnavailable: "Account list unavailable (requires the Command Code provider plugin to be running)",
|
|
216
|
+
ccUsageAccount: "Usage above: {account}",
|
|
217
|
+
ccUsageAutoNote: "with automatic rotation the usage above stays on the default (top-level key) account, not the serving one",
|
|
214
218
|
ccSwitching: "Switching…",
|
|
215
219
|
ccSwitchFailed: "Switch failed"
|
|
216
220
|
};
|
|
@@ -402,7 +406,7 @@ window.__ModuleLoader__.load({
|
|
|
402
406
|
namespace: "subUsage",
|
|
403
407
|
method: "read",
|
|
404
408
|
invocation: { kind: "direct" },
|
|
405
|
-
parameters: [],
|
|
409
|
+
parameters: [{ name: "request", wire: "request", source: "json", acceptsUndefined: true, codec: queryCodec }],
|
|
406
410
|
result: resultCodec
|
|
407
411
|
},
|
|
408
412
|
{
|
|
@@ -527,6 +531,9 @@ window.__ModuleLoader__.load({
|
|
|
527
531
|
let snapshot = { updatedAt: null, settings: null, configured: {}, entries: [] };
|
|
528
532
|
let disposed = false, initial = null, timer = null, epoch = 0, sequence = 0, metadataSequence = 0;
|
|
529
533
|
let loginGeneration = 0, loginJobId = null, loginPending = false;
|
|
534
|
+
// Command Code 显示账户:弹层账户区选中的账户("" = 默认账户/顶层 Key)。
|
|
535
|
+
// 所有 commandcode 读取(readAll/refresh/定时刷新)都携带它,切换后用量同步跟随。
|
|
536
|
+
let ccAccount = "";
|
|
530
537
|
const mergedLoginJobs = new Set();
|
|
531
538
|
const providerSequence = new Map();
|
|
532
539
|
const markRequest = (ids) => { const version = ++sequence; for (const id of ids) providerSequence.set(id, version); return { version, ids }; };
|
|
@@ -555,7 +562,7 @@ window.__ModuleLoader__.load({
|
|
|
555
562
|
const missing = unique.filter((id) => !inflight.has(id));
|
|
556
563
|
if (missing.length) {
|
|
557
564
|
const startedEpoch = epoch, request = markRequest(missing);
|
|
558
|
-
const task = Promise.resolve().then(() => call("refresh", { providerIds: missing, force: !!force })).then((value) => startedEpoch === epoch ? merge(value, request) : snapshot).catch((error) => {
|
|
565
|
+
const task = Promise.resolve().then(() => call("refresh", { providerIds: missing, force: !!force, commandCodeAccount: ccAccount })).then((value) => startedEpoch === epoch ? merge(value, request) : snapshot).catch((error) => {
|
|
559
566
|
// 传输失败不能擅自 retainPrevious:只有 Host 明确标 stale 才保留。
|
|
560
567
|
if (startedEpoch === epoch) merge({ entries: missing.map((providerId) => ({ providerId, state: "error", error: error instanceof Error ? error.message : String(error), freshness: "unknown", coverage: "partial", windows: [], extras: [] })) }, request);
|
|
561
568
|
throw error;
|
|
@@ -594,12 +601,24 @@ window.__ModuleLoader__.load({
|
|
|
594
601
|
}
|
|
595
602
|
return value;
|
|
596
603
|
};
|
|
604
|
+
const setCommandCodeAccount = (id) => {
|
|
605
|
+
const next = typeof id === "string" ? id.trim() : "";
|
|
606
|
+
if (disposed || ccAccount === next) return Promise.resolve(snapshot);
|
|
607
|
+
ccAccount = next;
|
|
608
|
+
// 账户变了:旧账户的在途/缓存数据立即失效,强制按新账户重拉。
|
|
609
|
+
invalidate(["commandcode"]);
|
|
610
|
+
// invalidate 只重写快照不通知:补一次 emit 让药丸立即进入加载态,
|
|
611
|
+
// 避免强制刷新返回前「标注已是新账户、数字还是旧账户」的瞬态错标。
|
|
612
|
+
emit();
|
|
613
|
+
return refresh(["commandcode"], true);
|
|
614
|
+
};
|
|
597
615
|
return {
|
|
598
616
|
subscribe, getSnapshot: () => snapshot, refresh, activate,
|
|
617
|
+
getCommandCodeAccount: () => ccAccount, setCommandCodeAccount,
|
|
599
618
|
startMimoLogin: async (request) => { const generation = ++loginGeneration, startedEpoch = epoch; loginJobId = null; return acceptLogin(await call("startMimoLogin", request), generation, startedEpoch); },
|
|
600
619
|
getMimoLoginStatus: async () => { const generation = loginGeneration, startedEpoch = epoch, jobId = loginPending ? loginJobId : null; return acceptLogin(await call("getMimoLoginStatus"), generation, startedEpoch, jobId); },
|
|
601
620
|
cancelMimoLogin: async (request) => { if (loginJobId && request.jobId !== loginJobId) return null; const generation = ++loginGeneration, startedEpoch = epoch; return acceptLogin(await call("cancelMimoLogin", request), generation, startedEpoch, request.jobId); },
|
|
602
|
-
readAll: () => { if (!initial) { const startedEpoch = epoch, request = markRequest(PROVIDER_ORDER); initial = Promise.resolve().then(() => call("read")).then((value) => startedEpoch === epoch ? merge(value, request) : snapshot).catch((error) => { initial = null; throw error; }); } return initial.then(() => snapshot); },
|
|
621
|
+
readAll: () => { if (!initial) { const startedEpoch = epoch, request = markRequest(PROVIDER_ORDER); initial = Promise.resolve().then(() => call("read", { providerIds: PROVIDER_ORDER, force: false, commandCodeAccount: ccAccount })).then((value) => startedEpoch === epoch ? merge(value, request) : snapshot).catch((error) => { initial = null; throw error; }); } return initial.then(() => snapshot); },
|
|
603
622
|
save: async (patch) => {
|
|
604
623
|
const value = await call("save", patch); epoch++;
|
|
605
624
|
if (patch.visibility) { const entries = snapshot.entries; markRequest(PROVIDER_ORDER); return merge({ ...value, entries }, null, true); }
|
|
@@ -628,8 +647,10 @@ window.__ModuleLoader__.load({
|
|
|
628
647
|
|
|
629
648
|
// ── Command Code 账户切换(对齐提供方插件的账户显示)────────────────────
|
|
630
649
|
// 账户列表与 id 规则对齐 @mars-sea/dsh-commandcode-provider 的 slots():
|
|
631
|
-
// 默认账户 id 固定 "default";额外账户 id 取其 apiKeyEnv
|
|
632
|
-
//
|
|
650
|
+
// 默认账户 id 固定 "default";额外账户 id 取其 apiKeyEnv(凭据引用名)。
|
|
651
|
+
// 与 slots() 的差异:仅配置 literal apiKey(无凭据引用)的槽位不进列表——
|
|
652
|
+
// 其 Key 存在提供方配置内,本插件无法按引用解析,列出只会选中即报 no-key。
|
|
653
|
+
// enrollment 进行中的账户不进列表(与其一致)。
|
|
633
654
|
const COMMANDCODE_SETTINGS_NS = "llm-commandcode";
|
|
634
655
|
function commandCodeAccountList(value) {
|
|
635
656
|
const list = [{ id: "default", label: "default" }];
|
|
@@ -779,7 +800,7 @@ window.__ModuleLoader__.load({
|
|
|
779
800
|
);
|
|
780
801
|
}
|
|
781
802
|
|
|
782
|
-
function UsagePill({ providerId, label, readEntry, t, getLocale, cc }) {
|
|
803
|
+
function UsagePill({ providerId, label, readEntry, t, getLocale, cc, setCommandCodeAccount }) {
|
|
783
804
|
const [snapshot, setSnapshot] = react.useState(null);
|
|
784
805
|
const [failed, setFailed] = react.useState(null);
|
|
785
806
|
const [failState, setFailState] = react.useState(void 0);
|
|
@@ -790,6 +811,14 @@ window.__ModuleLoader__.load({
|
|
|
790
811
|
// Command Code 账户区:cc 由装配层注入(提供方插件未运行时为 null,区块整体隐藏)。
|
|
791
812
|
const ccState = react.useSyncExternalStore(cc ? cc.subscribe : () => () => {}, cc ? cc.getSnapshot : () => null);
|
|
792
813
|
react.useEffect(() => { if (open && cc) void cc.load(); }, [open, cc]);
|
|
814
|
+
// 挂载即读一次账户配置:提供方插件固定了账户时,药丸本体不必等弹层打开就跟随。
|
|
815
|
+
react.useEffect(() => { if (providerId === "commandcode" && cc) void cc.load(); }, [providerId, cc]);
|
|
816
|
+
// 显示账户跟随账户区的当前固定项(含切换成功后的回读):ready 才同步,
|
|
817
|
+
// unavailable/loading 保持现状;store 内部对相同账户去重,不产生多余请求。
|
|
818
|
+
react.useEffect(() => {
|
|
819
|
+
if (providerId !== "commandcode" || !ccState || !setCommandCodeAccount) return;
|
|
820
|
+
if (ccState.status === "ready") void setCommandCodeAccount(ccState.activeId);
|
|
821
|
+
}, [providerId, setCommandCodeAccount, ccState && ccState.status, ccState && ccState.activeId]);
|
|
793
822
|
react.useEffect(() => {
|
|
794
823
|
let alive = true;
|
|
795
824
|
let busy = false;
|
|
@@ -916,6 +945,12 @@ window.__ModuleLoader__.load({
|
|
|
916
945
|
cacheAge(rawEntry, t) ? h("p", { style: { color: "#b7791f" } }, cacheAge(rawEntry, t)) : null,
|
|
917
946
|
// 到期提醒常驻弹层:即使用量正常,也让用户看见 Cookie 还剩多久。
|
|
918
947
|
expiry ? h("p", { role: "status", style: { margin: "6px 0 0", color: expiring ? expiry.color : "inherit" } }, `${expiry.label} · ${t("mimoCookieExpiresAt").replace("{time}", new Date(expiry.expiresAt).toLocaleString(getLocale()))}`) : null,
|
|
948
|
+
// 用量账户标注:账户区 ready 时说明上方数字属于哪个账户;自动轮换时明确不跟随实际服务账户。
|
|
949
|
+
providerId === "commandcode" && ccState && ccState.status === "ready"
|
|
950
|
+
? h("p", { style: { margin: "6px 0 0", fontSize: 11, opacity: 0.75 } },
|
|
951
|
+
t("ccUsageAccount").replace("{account}", ccState.activeId === "" || ccState.activeId === "default" ? t("ccAccountDefault") : (ccState.accounts.find((a) => a.id === ccState.activeId)?.label ?? ccState.activeId)),
|
|
952
|
+
ccState.activeId === "" ? h("span", { style: { opacity: 0.85 } }, ` · ${t("ccUsageAutoNote")}`) : null)
|
|
953
|
+
: null,
|
|
919
954
|
windows.map((w) => h(UsageWindowRow, { key: w.kind, w, t, getLocale, uncertain: degraded })),
|
|
920
955
|
entry && entry.extras && entry.extras.length > 0
|
|
921
956
|
? h("div", { style: { marginTop: 10, opacity: 0.85 } },
|
|
@@ -1263,13 +1298,13 @@ window.__ModuleLoader__.load({
|
|
|
1263
1298
|
ctx.inject(["modelDirectories"], (scope) => {
|
|
1264
1299
|
try {
|
|
1265
1300
|
scope.slots.inject("conversation.input.right", () => scope.slots.register({ name: "conversation.input.right", id: "subusage-usage", order: 1001,
|
|
1266
|
-
inject: (sessionId) => ({ store: scope.modelDirectories.directoryFor(sessionId).store, readEntry, t, getLocale, cc }) }, SubusagePillEntry));
|
|
1301
|
+
inject: (sessionId) => ({ store: scope.modelDirectories.directoryFor(sessionId).store, readEntry, t, getLocale, cc, setCommandCodeAccount: usageStore.setCommandCodeAccount }) }, SubusagePillEntry));
|
|
1267
1302
|
} catch (error) { console.error("[dsh-subusage] 药丸注册失败:", error); }
|
|
1268
1303
|
});
|
|
1269
1304
|
}
|
|
1270
1305
|
|
|
1271
1306
|
/** 槽包装:先看当前模型的 provider,再分发到对应药丸。 */
|
|
1272
|
-
function SubusagePillEntry({ store, readEntry, t, getLocale, cc }) {
|
|
1307
|
+
function SubusagePillEntry({ store, readEntry, t, getLocale, cc, setCommandCodeAccount }) {
|
|
1273
1308
|
const state = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1274
1309
|
const provider = state.current && state.current.provider;
|
|
1275
1310
|
const info = PROVIDER_ORDER.includes(provider) ? PROVIDER_META[provider] : null;
|
|
@@ -1286,7 +1321,8 @@ window.__ModuleLoader__.load({
|
|
|
1286
1321
|
readEntry: reader,
|
|
1287
1322
|
t,
|
|
1288
1323
|
getLocale,
|
|
1289
|
-
cc
|
|
1324
|
+
cc,
|
|
1325
|
+
setCommandCodeAccount
|
|
1290
1326
|
});
|
|
1291
1327
|
}
|
|
1292
1328
|
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-subusage Host:秘密仅保存在 Host,远端只暴露 provider 补丁和公开设置。
|
|
2
2
|
import { RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
3
|
-
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
3
|
+
import { credentialRef, isCredentialRefName } from "@deepseek-ai/dsh-credentials";
|
|
4
4
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
5
5
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
@@ -133,15 +133,21 @@ function parseSettings(value) {
|
|
|
133
133
|
return out;
|
|
134
134
|
}
|
|
135
135
|
function parseQuery(value) {
|
|
136
|
+
// request 在 wire 层可选(read 描述符声明 acceptsUndefined):缺参/undefined/null
|
|
137
|
+
// = 全量默认账户查询,兼容旧 client bundle 的无参 read。
|
|
138
|
+
if (value === undefined || value === null) return { providerIds: [...IDS], force: false, commandCodeAccount: "" };
|
|
136
139
|
if (!value || !Array.isArray(value.providerIds) || value.providerIds.some(id => !IDS.includes(id)) || typeof value.force !== "boolean") throw invalid("Invalid refresh request");
|
|
137
|
-
|
|
140
|
+
// commandCodeAccount 是可选的显示账户选择:空串/缺省 = 默认账户(顶层 Key);
|
|
141
|
+
// 额外账户 id 即其凭据引用名(apiKeyEnv),由 identity 按引用解析。
|
|
142
|
+
if (value.commandCodeAccount !== undefined && typeof value.commandCodeAccount !== "string") throw invalid("Invalid account selection");
|
|
143
|
+
return { providerIds: [...new Set(value.providerIds)], force: value.force, commandCodeAccount: typeof value.commandCodeAccount === "string" ? value.commandCodeAccount.trim() : "" };
|
|
138
144
|
}
|
|
139
145
|
function failure(code, message, retryable = false, retryAfterMs) {
|
|
140
146
|
return new RemoteError(code, message, { retryable, retainPrevious: retryable, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}) });
|
|
141
147
|
}
|
|
142
148
|
async function fetchJson(fetcher, url, headers, lifetimeSignal) {
|
|
143
149
|
let response;
|
|
144
|
-
try { response = await fetcher(url, { headers: { accept: "application/json", "user-agent": "dsh-subusage/0.8.
|
|
150
|
+
try { response = await fetcher(url, { headers: { accept: "application/json", "user-agent": "dsh-subusage/0.8.3", ...headers }, signal: lifetimeSignal ? AbortSignal.any([AbortSignal.timeout(10000), lifetimeSignal]) : AbortSignal.timeout(10000), redirect: "manual" }); }
|
|
145
151
|
catch { throw failure("subusage/network", "网络请求失败或超时", true); }
|
|
146
152
|
if (response.status >= 300 && response.status < 400 || response.status === 401 || response.status === 403) throw failure("subusage/auth", "登录或凭据已失效");
|
|
147
153
|
if (!response.ok) {
|
|
@@ -179,10 +185,32 @@ function cascadeRateLimited(windows) {
|
|
|
179
185
|
return windows;
|
|
180
186
|
}
|
|
181
187
|
export function normalizeKimi(body) {
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
// 上游 /coding/v1/usages 有两代形态:新版 usages 比例池(limit_5h / limit_7d /
|
|
189
|
+
// limit_month_total),旧版顶层 usage + limits[](绝对 limit / used / remaining)。
|
|
190
|
+
// 只有 300 分钟窗口在所有形态都下发,因此没有哪个窗口名可以当必需字段:现行账户
|
|
191
|
+
// 下发 5h + 月池而省略 limit_7d,旧账户则完全没有 usages。不推断未知字段。
|
|
192
|
+
const pools = [body?.usages, body?.usage, body?.limits].filter(v => v && typeof v === "object" && !Array.isArray(v));
|
|
193
|
+
const windows = [];
|
|
194
|
+
// limit_month_code 是 month_total 的 Code 份额而非独立预算,不作为独立窗口。
|
|
195
|
+
for (const [field, kind] of [["limit_5h", "5h"], ["limit_7d", "7d"], ["limit_month_total", "month"]]) {
|
|
196
|
+
const row = pools.map(pool => pool[field]).find(v => v !== undefined);
|
|
197
|
+
if (row === undefined) continue;
|
|
198
|
+
if (!numeric(row.used_ratio)) throw failure("subusage/response", "Invalid Kimi used_ratio");
|
|
199
|
+
windows.push(windowRow(kind, row.used_ratio * 100, row.reset_time));
|
|
200
|
+
}
|
|
201
|
+
// 无比例池时退回绝对值:limits[] 的 300 分钟项是 5 小时窗口,顶层 usage 是周额度。
|
|
202
|
+
if (!windows.length) {
|
|
203
|
+
const absolute = (row, kind) => {
|
|
204
|
+
const limit = Number(row?.limit), remaining = Number(row?.remaining);
|
|
205
|
+
if (!Number.isFinite(limit) || limit <= 0 || !Number.isFinite(remaining)) return;
|
|
206
|
+
windows.push(windowRow(kind, (limit - remaining) / limit * 100, row.resetTime));
|
|
207
|
+
};
|
|
208
|
+
const burst = (Array.isArray(body?.limits) ? body.limits : []).find(row => row?.window?.duration === 300 && row.window.timeUnit === "TIME_UNIT_MINUTE");
|
|
209
|
+
if (burst) absolute(burst.detail, "5h");
|
|
210
|
+
if (body?.usage && typeof body.usage === "object") absolute(body.usage, "week");
|
|
211
|
+
}
|
|
212
|
+
if (!windows.length) throw failure("subusage/response", "Invalid Kimi usage response");
|
|
213
|
+
return cascadeRateLimited(windows);
|
|
186
214
|
}
|
|
187
215
|
export function normalizeZai(body) {
|
|
188
216
|
const data = body?.data;
|
|
@@ -341,7 +369,7 @@ const loginCodec = codec("SubUsageLoginState", value => {
|
|
|
341
369
|
export const subUsageRemote = {
|
|
342
370
|
package: name,
|
|
343
371
|
descriptors: [
|
|
344
|
-
{ id: `${name}#subUsage/read`, service: "subUsage", namespace: "subUsage", method: "read", invocation: { kind: "direct" }, parameters: [], result: resultCodec },
|
|
372
|
+
{ id: `${name}#subUsage/read`, service: "subUsage", namespace: "subUsage", method: "read", invocation: { kind: "direct" }, parameters: [{ name: "request", wire: "request", source: "json", acceptsUndefined: true, codec: codec("SubUsageQuery", parseQuery) }], result: resultCodec },
|
|
345
373
|
...[["refresh", "request", "SubUsageQuery", parseQuery], ["save", "settings", "SubUsageSettings", parseSettings]].map(([method, param, type, parse]) => ({ id: `${name}#subUsage/${method}`, service: "subUsage", namespace: "subUsage", method, invocation: { kind: "direct" }, parameters: [{ name: param, wire: param, source: "json", codec: codec(type, parse) }], result: resultCodec })),
|
|
346
374
|
{ id: `${name}#subUsage/getMimoLoginStatus`, service: "subUsage", namespace: "subUsage", method: "getMimoLoginStatus", invocation: { kind: "direct" }, parameters: [], result: loginCodec },
|
|
347
375
|
...[["startMimoLogin", "MimoLoginStart", parseLoginStart], ["cancelMimoLogin", "MimoLoginCancel", parseLoginCancel]].map(([method, type, parse]) => ({ id: `${name}#subUsage/${method}`, service: "subUsage", namespace: "subUsage", method, invocation: { kind: "direct" }, parameters: [{ name: "request", wire: "request", source: "json", codec: codec(type, parse) }], result: loginCodec }))
|
|
@@ -357,6 +385,10 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
357
385
|
this.now = options.now ?? Date.now;
|
|
358
386
|
this.credentials = options.resolveCredentials ?? (async id => await ctx.get("credentials")?.resolve(credentialRef(PROVIDERS[id].envName)));
|
|
359
387
|
this.environment = options.resolveEnvironment ?? (id => launchEnvironmentOf(ctx).get(PROVIDERS[id].envName));
|
|
388
|
+
// 额外账户按凭据引用名(apiKeyEnv)解析,与提供方插件 slots()/resolveRef 同一通道;
|
|
389
|
+
// 默认账户仍走上面的 envName 链(凭据服务 → 启动环境 → ~/.commandcode/auth.json)。
|
|
390
|
+
this.accountCredentials = options.resolveAccountCredentials ?? (async ref => await ctx.get("credentials")?.resolve(credentialRef(ref)));
|
|
391
|
+
this.accountEnvironment = options.resolveAccountEnvironment ?? (ref => launchEnvironmentOf(ctx).get(ref));
|
|
360
392
|
this.authFile = options.resolveAuthFile ?? (async () => commandCodeAuthFileKey(this.io));
|
|
361
393
|
this.cache = new Map(); this.inflight = new Map(); this.generations = new Map(); this.fingerprints = new Map(); this.disposed = false; this.controller = new AbortController();
|
|
362
394
|
this.login = new MimoLogin({
|
|
@@ -379,9 +411,19 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
379
411
|
catch (e) { if (e?.code === "ENOENT") return defaultSettings(); throw failure("subusage/config", "无法读取订阅设置"); }
|
|
380
412
|
}
|
|
381
413
|
result(settings, entries) { return { updatedAt: new Date(this.now()).toISOString(), settings: publicSettings(settings), configured: detectConfigured(this.ctx), entries }; }
|
|
382
|
-
async identity(id, s) {
|
|
414
|
+
async identity(id, s, account) {
|
|
383
415
|
if (id === "xiaomi-token-plan-cn") return { key: s.xiaomi.cookie.trim(), source: s.xiaomi.cookie.trim() ? "cookie" : "none" };
|
|
384
416
|
if (!PROVIDERS[id].managedByPlugin && s.keyModes[id] === "manual") return { key: s.keys[id]?.trim(), source: s.keys[id]?.trim() ? "manual" : "none" };
|
|
417
|
+
if (id === "commandcode" && account && account !== "default") {
|
|
418
|
+
// 额外账户:id 即凭据引用名(apiKeyEnv)。引用名不合法(不可能来自提供方
|
|
419
|
+
// 插件的 slots,但 client 输入不可信)按未配置处理,不让 credentialRef 抛错。
|
|
420
|
+
if (!isCredentialRefName(account)) return { key: undefined, source: "none" };
|
|
421
|
+
const hit = await this.accountCredentials(account);
|
|
422
|
+
if (typeof hit?.value === "string" && hit.value.trim()) return { key: hit.value.trim(), source: "credentials" };
|
|
423
|
+
const env = await this.accountEnvironment(account);
|
|
424
|
+
if (typeof env?.value === "string" && env.value.trim()) return { key: env.value.trim(), source: "env" };
|
|
425
|
+
return { key: undefined, source: "none" };
|
|
426
|
+
}
|
|
385
427
|
const hit = await this.credentials(id);
|
|
386
428
|
if (typeof hit?.value === "string" && hit.value.trim()) return { key: hit.value.trim(), source: "credentials" };
|
|
387
429
|
const env = await this.environment(id);
|
|
@@ -432,23 +474,27 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
432
474
|
if (usageError) data.usageError = usageError;
|
|
433
475
|
return data;
|
|
434
476
|
}
|
|
435
|
-
base(id, source, settings) {
|
|
477
|
+
base(id, source, settings, account = "") {
|
|
436
478
|
// MiMo 会话 Cookie 自签发起 24 小时有效;到期时间随每条 entry 下发,供药丸与设置页倒计时。
|
|
437
479
|
const expiresAt = id === "xiaomi-token-plan-cn" ? isoStamp(settings?.xiaomi?.expiresAt) : null;
|
|
438
|
-
|
|
480
|
+
// commandcode 记录本条用量所属账户(default 或额外账户引用名),供弹层标注与测试核对。
|
|
481
|
+
const shown = id === "commandcode" ? { account: account !== "" ? account : "default" } : {};
|
|
482
|
+
return { providerId: id, label: PROVIDERS[id].label, envName: PROVIDERS[id].envName, keySource: source, apiDetected: source === "none" ? null : true, coverage: "partial", freshness: "unknown", lastSuccessAt: null, lastAttemptAt: new Date(this.now()).toISOString(), retryable: false, retainPrevious: false, ...shown, ...(expiresAt ? { cookieExpiresAt: expiresAt } : {}) };
|
|
439
483
|
}
|
|
440
|
-
async provider(id, settings, force) {
|
|
484
|
+
async provider(id, settings, force, account = "") {
|
|
441
485
|
const startedGeneration = this.generations.get(id) ?? 0;
|
|
442
486
|
let identity;
|
|
443
|
-
try { identity = await this.identity(id, settings); }
|
|
444
|
-
catch { this.cache.delete(id); return { ...this.base(id, "none", settings), state: "error", errorCode: "subusage/credentials", error: "无法解析凭据" }; }
|
|
487
|
+
try { identity = await this.identity(id, settings, account); }
|
|
488
|
+
catch { this.cache.delete(id); return { ...this.base(id, "none", settings, account), state: "error", errorCode: "subusage/credentials", error: "无法解析凭据" }; }
|
|
445
489
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
446
|
-
if (startedGeneration !== (this.generations.get(id) ?? 0)) return this.provider(id, this.load(), force);
|
|
490
|
+
if (startedGeneration !== (this.generations.get(id) ?? 0)) return this.provider(id, this.load(), force, account);
|
|
447
491
|
if (!settings.visibility.providers[id]) {
|
|
448
492
|
this.cache.delete(id); this.inflight.delete(id);
|
|
449
|
-
return { ...this.base(id, identity.source, settings), state: "disabled", apiDetected: !!identity.key, windows: [], extras: [] };
|
|
493
|
+
return { ...this.base(id, identity.source, settings, account), state: "disabled", apiDetected: !!identity.key, windows: [], extras: [] };
|
|
450
494
|
}
|
|
451
|
-
|
|
495
|
+
// fingerprint 计入账户选择:切换账户后同 id 缓存/inflight 不串账(不同账户 key 必不同);
|
|
496
|
+
// 空串与 "default" 同为默认账户,归一后共享缓存。
|
|
497
|
+
const fingerprint = hash([identity.key ?? "", identity.source, settings.keyModes[id], id === "zai-coding-cn" ? settings.zai : null, id === "commandcode" ? (account && account !== "default" ? account : "default") : null]);
|
|
452
498
|
if (this.fingerprints.has(id) && this.fingerprints.get(id) !== fingerprint) this.generations.set(id, (this.generations.get(id) ?? 0) + 1);
|
|
453
499
|
this.fingerprints.set(id, fingerprint);
|
|
454
500
|
const generation = this.generations.get(id) ?? 0;
|
|
@@ -461,8 +507,8 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
461
507
|
const resetDue = previous?.entry.windows?.some(w => Number.isFinite(Date.parse(w.resetsAt)) && Date.parse(w.resetsAt) > previous.at && Date.parse(w.resetsAt) <= now);
|
|
462
508
|
if (previous && (now < previous.retryAt || !resetDue && !force && now - previous.at < TTL)) return previous.entry;
|
|
463
509
|
const promise = (async () => {
|
|
464
|
-
const base = this.base(id, identity.source, settings); let entry; let retryAt = 0; let failures = previous?.failures ?? 0;
|
|
465
|
-
if (!identity.key) entry = { ...base, apiDetected: false, state: id === "xiaomi-token-plan-cn" ? "no-cookie" : "no-key", error: id === "xiaomi-token-plan-cn" ? "需登录或粘贴 Cookie" : id === "commandcode" ? "未检测到 Key:可在 设置 → Command Code 登录、运行 cmd login,或使用 COMMANDCODE_API_KEY(凭据服务/启动环境)" : "未配置 API Key", errorCode: "subusage/no-key" };
|
|
510
|
+
const base = this.base(id, identity.source, settings, account); let entry; let retryAt = 0; let failures = previous?.failures ?? 0;
|
|
511
|
+
if (!identity.key) entry = { ...base, apiDetected: false, state: id === "xiaomi-token-plan-cn" ? "no-cookie" : "no-key", error: id === "xiaomi-token-plan-cn" ? "需登录或粘贴 Cookie" : id === "commandcode" ? (account && account !== "default" ? `未检测到账户 ${account} 的 Key:确认提供方插件已保存该账户的凭据(凭据服务或启动环境中无引用 ${account})` : "未检测到 Key:可在 设置 → Command Code 登录、运行 cmd login,或使用 COMMANDCODE_API_KEY(凭据服务/启动环境)") : "未配置 API Key", errorCode: "subusage/no-key" };
|
|
466
512
|
else try {
|
|
467
513
|
const data = await this.fetchProvider(id, identity, settings);
|
|
468
514
|
const usageError = data.usageError; delete data.usageError;
|
|
@@ -478,19 +524,19 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
478
524
|
if (temporary) retryAt = this.now() + Math.max(e.details?.retryAfterMs ?? 0, Math.min(300000, 1000 * 2 ** Math.min(failures, 8)));
|
|
479
525
|
}
|
|
480
526
|
if (!this.disposed && generation === (this.generations.get(id) ?? 0)) this.cache.set(id, { fingerprint, entry, at: this.now(), retryAt, failures });
|
|
481
|
-
if (this.disposed || generation !== (this.generations.get(id) ?? 0)) return { ...this.base(id, "none", settings), state: "error", errorCode: "subusage/config-changed", error: "读取期间配置已改变,请刷新" };
|
|
527
|
+
if (this.disposed || generation !== (this.generations.get(id) ?? 0)) return { ...this.base(id, "none", settings, account), state: "error", errorCode: "subusage/config-changed", error: "读取期间配置已改变,请刷新" };
|
|
482
528
|
return entry;
|
|
483
529
|
})();
|
|
484
530
|
this.inflight.set(id, { fingerprint, generation, promise });
|
|
485
531
|
try { return await promise; } finally { if (this.inflight.get(id)?.promise === promise) this.inflight.delete(id); }
|
|
486
532
|
}
|
|
487
|
-
async read() { return this.refresh({ providerIds: IDS, force: false }); }
|
|
533
|
+
async read(request) { return this.refresh(request && request.providerIds ? request : { providerIds: IDS, force: false }); }
|
|
488
534
|
async refresh(request) {
|
|
489
535
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
490
536
|
const query = parseQuery(request); const settings = this.load();
|
|
491
|
-
const entries = await Promise.all(query.providerIds.map(id => this.provider(id, settings, query.force).catch(() => ({ ...this.base(id, "none", settings), state: "error", errorCode: "subusage/internal", error: "读取用量失败" }))));
|
|
537
|
+
const entries = await Promise.all(query.providerIds.map(id => this.provider(id, settings, query.force, query.commandCodeAccount).catch(() => ({ ...this.base(id, "none", settings, query.commandCodeAccount), state: "error", errorCode: "subusage/internal", error: "读取用量失败" }))));
|
|
492
538
|
const latest = this.load();
|
|
493
|
-
if (hash(latest) !== hash(settings)) return this.result(latest, query.providerIds.map(id => ({ ...this.base(id, "none", latest), state: "error", errorCode: "subusage/config-changed", error: "读取期间配置已改变,请刷新" })));
|
|
539
|
+
if (hash(latest) !== hash(settings)) return this.result(latest, query.providerIds.map(id => ({ ...this.base(id, "none", latest, query.commandCodeAccount), state: "error", errorCode: "subusage/config-changed", error: "读取期间配置已改变,请刷新" })));
|
|
494
540
|
return this.result(latest, entries);
|
|
495
541
|
}
|
|
496
542
|
startMimoLogin(request) { return this.login.start(parseLoginStart(request)); }
|
|
@@ -506,7 +552,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
506
552
|
const entry = { ...this.base(id, "cookie", stored), ...data, state: "ok", coverage: data.coverage ?? "partial", freshness: "fresh", lastSuccessAt: new Date(this.now()).toISOString(), ...(usageError ? { errorCode: usageError.code, error: "额度暂时无法读取", retryable: usageError.details?.retryable === true } : {}) };
|
|
507
553
|
// 响应内容也不得回显本次 Cookie(服务端字段属于不可信输入)。
|
|
508
554
|
const safeEntry = redactMimo(entry, cookie);
|
|
509
|
-
const fingerprint = hash([cookie, "cookie", stored.keyModes[id], null]);
|
|
555
|
+
const fingerprint = hash([cookie, "cookie", stored.keyModes[id], null, null]);
|
|
510
556
|
this.fingerprints.set(id, fingerprint); this.cache.set(id, { fingerprint, entry: safeEntry, at: this.now(), retryAt: usageError ? this.now() + 15000 : 0, failures: 0 });
|
|
511
557
|
this.loginResultMaterial = this.mimoMaterial(stored);
|
|
512
558
|
return this.result(stored, [safeEntry]);
|