dsh-subusage 0.8.1 → 0.8.2
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 +3 -3
- package/lib/client.js +49 -13
- package/lib/index.js +44 -20
- 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.2**,可从 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
|
|
|
@@ -124,7 +124,7 @@ JSON 中有域名的条目按 `platform.xiaomimimo.com` 的 Cookie 域规则过
|
|
|
124
124
|
| 保存成功但验证失败 | 凭据已保存,检查网络、区域或账号后重试;保存与在线验证分别反馈 |
|
|
125
125
|
| 保存提示配置已改变 | 另一窗口或实例更新了设置;重新读取配置后再编辑,避免旧表单覆盖新值 |
|
|
126
126
|
| 升级后仍是旧界面或 RPC 不匹配 | 确认运行中的 Host 和 Client 都已重载;仅刷新页面不能保证 Host 升级 |
|
|
127
|
-
| Command Code 额度与实际账户不同 |
|
|
127
|
+
| Command Code 额度与实际账户不同 | 可在药丸弹层切换提供方插件的服务账户(activeAccount),切换后药丸与弹层用量同步显示所选账户的额度;「自动轮换」时仍显示默认账户(顶层 Key)的用量,不跟随轮换中的实际服务账户,提供方自定义 apiBase 也不跟随 |
|
|
128
128
|
|
|
129
129
|
## 提供商 ID 与默认开关
|
|
130
130
|
|
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.2", ...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) {
|
|
@@ -341,7 +347,7 @@ const loginCodec = codec("SubUsageLoginState", value => {
|
|
|
341
347
|
export const subUsageRemote = {
|
|
342
348
|
package: name,
|
|
343
349
|
descriptors: [
|
|
344
|
-
{ id: `${name}#subUsage/read`, service: "subUsage", namespace: "subUsage", method: "read", invocation: { kind: "direct" }, parameters: [], result: resultCodec },
|
|
350
|
+
{ 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
351
|
...[["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
352
|
{ id: `${name}#subUsage/getMimoLoginStatus`, service: "subUsage", namespace: "subUsage", method: "getMimoLoginStatus", invocation: { kind: "direct" }, parameters: [], result: loginCodec },
|
|
347
353
|
...[["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 +363,10 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
357
363
|
this.now = options.now ?? Date.now;
|
|
358
364
|
this.credentials = options.resolveCredentials ?? (async id => await ctx.get("credentials")?.resolve(credentialRef(PROVIDERS[id].envName)));
|
|
359
365
|
this.environment = options.resolveEnvironment ?? (id => launchEnvironmentOf(ctx).get(PROVIDERS[id].envName));
|
|
366
|
+
// 额外账户按凭据引用名(apiKeyEnv)解析,与提供方插件 slots()/resolveRef 同一通道;
|
|
367
|
+
// 默认账户仍走上面的 envName 链(凭据服务 → 启动环境 → ~/.commandcode/auth.json)。
|
|
368
|
+
this.accountCredentials = options.resolveAccountCredentials ?? (async ref => await ctx.get("credentials")?.resolve(credentialRef(ref)));
|
|
369
|
+
this.accountEnvironment = options.resolveAccountEnvironment ?? (ref => launchEnvironmentOf(ctx).get(ref));
|
|
360
370
|
this.authFile = options.resolveAuthFile ?? (async () => commandCodeAuthFileKey(this.io));
|
|
361
371
|
this.cache = new Map(); this.inflight = new Map(); this.generations = new Map(); this.fingerprints = new Map(); this.disposed = false; this.controller = new AbortController();
|
|
362
372
|
this.login = new MimoLogin({
|
|
@@ -379,9 +389,19 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
379
389
|
catch (e) { if (e?.code === "ENOENT") return defaultSettings(); throw failure("subusage/config", "无法读取订阅设置"); }
|
|
380
390
|
}
|
|
381
391
|
result(settings, entries) { return { updatedAt: new Date(this.now()).toISOString(), settings: publicSettings(settings), configured: detectConfigured(this.ctx), entries }; }
|
|
382
|
-
async identity(id, s) {
|
|
392
|
+
async identity(id, s, account) {
|
|
383
393
|
if (id === "xiaomi-token-plan-cn") return { key: s.xiaomi.cookie.trim(), source: s.xiaomi.cookie.trim() ? "cookie" : "none" };
|
|
384
394
|
if (!PROVIDERS[id].managedByPlugin && s.keyModes[id] === "manual") return { key: s.keys[id]?.trim(), source: s.keys[id]?.trim() ? "manual" : "none" };
|
|
395
|
+
if (id === "commandcode" && account && account !== "default") {
|
|
396
|
+
// 额外账户:id 即凭据引用名(apiKeyEnv)。引用名不合法(不可能来自提供方
|
|
397
|
+
// 插件的 slots,但 client 输入不可信)按未配置处理,不让 credentialRef 抛错。
|
|
398
|
+
if (!isCredentialRefName(account)) return { key: undefined, source: "none" };
|
|
399
|
+
const hit = await this.accountCredentials(account);
|
|
400
|
+
if (typeof hit?.value === "string" && hit.value.trim()) return { key: hit.value.trim(), source: "credentials" };
|
|
401
|
+
const env = await this.accountEnvironment(account);
|
|
402
|
+
if (typeof env?.value === "string" && env.value.trim()) return { key: env.value.trim(), source: "env" };
|
|
403
|
+
return { key: undefined, source: "none" };
|
|
404
|
+
}
|
|
385
405
|
const hit = await this.credentials(id);
|
|
386
406
|
if (typeof hit?.value === "string" && hit.value.trim()) return { key: hit.value.trim(), source: "credentials" };
|
|
387
407
|
const env = await this.environment(id);
|
|
@@ -432,23 +452,27 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
432
452
|
if (usageError) data.usageError = usageError;
|
|
433
453
|
return data;
|
|
434
454
|
}
|
|
435
|
-
base(id, source, settings) {
|
|
455
|
+
base(id, source, settings, account = "") {
|
|
436
456
|
// MiMo 会话 Cookie 自签发起 24 小时有效;到期时间随每条 entry 下发,供药丸与设置页倒计时。
|
|
437
457
|
const expiresAt = id === "xiaomi-token-plan-cn" ? isoStamp(settings?.xiaomi?.expiresAt) : null;
|
|
438
|
-
|
|
458
|
+
// commandcode 记录本条用量所属账户(default 或额外账户引用名),供弹层标注与测试核对。
|
|
459
|
+
const shown = id === "commandcode" ? { account: account !== "" ? account : "default" } : {};
|
|
460
|
+
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
461
|
}
|
|
440
|
-
async provider(id, settings, force) {
|
|
462
|
+
async provider(id, settings, force, account = "") {
|
|
441
463
|
const startedGeneration = this.generations.get(id) ?? 0;
|
|
442
464
|
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: "无法解析凭据" }; }
|
|
465
|
+
try { identity = await this.identity(id, settings, account); }
|
|
466
|
+
catch { this.cache.delete(id); return { ...this.base(id, "none", settings, account), state: "error", errorCode: "subusage/credentials", error: "无法解析凭据" }; }
|
|
445
467
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
446
|
-
if (startedGeneration !== (this.generations.get(id) ?? 0)) return this.provider(id, this.load(), force);
|
|
468
|
+
if (startedGeneration !== (this.generations.get(id) ?? 0)) return this.provider(id, this.load(), force, account);
|
|
447
469
|
if (!settings.visibility.providers[id]) {
|
|
448
470
|
this.cache.delete(id); this.inflight.delete(id);
|
|
449
|
-
return { ...this.base(id, identity.source, settings), state: "disabled", apiDetected: !!identity.key, windows: [], extras: [] };
|
|
471
|
+
return { ...this.base(id, identity.source, settings, account), state: "disabled", apiDetected: !!identity.key, windows: [], extras: [] };
|
|
450
472
|
}
|
|
451
|
-
|
|
473
|
+
// fingerprint 计入账户选择:切换账户后同 id 缓存/inflight 不串账(不同账户 key 必不同);
|
|
474
|
+
// 空串与 "default" 同为默认账户,归一后共享缓存。
|
|
475
|
+
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
476
|
if (this.fingerprints.has(id) && this.fingerprints.get(id) !== fingerprint) this.generations.set(id, (this.generations.get(id) ?? 0) + 1);
|
|
453
477
|
this.fingerprints.set(id, fingerprint);
|
|
454
478
|
const generation = this.generations.get(id) ?? 0;
|
|
@@ -461,8 +485,8 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
461
485
|
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
486
|
if (previous && (now < previous.retryAt || !resetDue && !force && now - previous.at < TTL)) return previous.entry;
|
|
463
487
|
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" };
|
|
488
|
+
const base = this.base(id, identity.source, settings, account); let entry; let retryAt = 0; let failures = previous?.failures ?? 0;
|
|
489
|
+
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
490
|
else try {
|
|
467
491
|
const data = await this.fetchProvider(id, identity, settings);
|
|
468
492
|
const usageError = data.usageError; delete data.usageError;
|
|
@@ -478,19 +502,19 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
478
502
|
if (temporary) retryAt = this.now() + Math.max(e.details?.retryAfterMs ?? 0, Math.min(300000, 1000 * 2 ** Math.min(failures, 8)));
|
|
479
503
|
}
|
|
480
504
|
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: "读取期间配置已改变,请刷新" };
|
|
505
|
+
if (this.disposed || generation !== (this.generations.get(id) ?? 0)) return { ...this.base(id, "none", settings, account), state: "error", errorCode: "subusage/config-changed", error: "读取期间配置已改变,请刷新" };
|
|
482
506
|
return entry;
|
|
483
507
|
})();
|
|
484
508
|
this.inflight.set(id, { fingerprint, generation, promise });
|
|
485
509
|
try { return await promise; } finally { if (this.inflight.get(id)?.promise === promise) this.inflight.delete(id); }
|
|
486
510
|
}
|
|
487
|
-
async read() { return this.refresh({ providerIds: IDS, force: false }); }
|
|
511
|
+
async read(request) { return this.refresh(request && request.providerIds ? request : { providerIds: IDS, force: false }); }
|
|
488
512
|
async refresh(request) {
|
|
489
513
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
490
514
|
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: "读取用量失败" }))));
|
|
515
|
+
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
516
|
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: "读取期间配置已改变,请刷新" })));
|
|
517
|
+
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
518
|
return this.result(latest, entries);
|
|
495
519
|
}
|
|
496
520
|
startMimoLogin(request) { return this.login.start(parseLoginStart(request)); }
|
|
@@ -506,7 +530,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
506
530
|
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
531
|
// 响应内容也不得回显本次 Cookie(服务端字段属于不可信输入)。
|
|
508
532
|
const safeEntry = redactMimo(entry, cookie);
|
|
509
|
-
const fingerprint = hash([cookie, "cookie", stored.keyModes[id], null]);
|
|
533
|
+
const fingerprint = hash([cookie, "cookie", stored.keyModes[id], null, null]);
|
|
510
534
|
this.fingerprints.set(id, fingerprint); this.cache.set(id, { fingerprint, entry: safeEntry, at: this.now(), retryAt: usageError ? this.now() + 15000 : 0, failures: 0 });
|
|
511
535
|
this.loginResultMaterial = this.mimoMaterial(stored);
|
|
512
536
|
return this.result(stored, [safeEntry]);
|