dsh-subusage 0.10.0 → 0.10.1
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 +1 -1
- package/lib/client.js +12 -5
- package/lib/index.js +20 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> **English.** dsh-subusage shows subscription quota and balance for the AI providers you already use, as a pill next to the model selector in DeepSeek Harness. Provider switches and credentials live in one settings page, and other plugins or agents can read the same snapshot through a read-only quota API.
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-subusage)
|
|
10
10
|
[](#license--security--许可与安全)
|
|
11
11
|
[](#compatibility--兼容性)
|
|
12
12
|
|
package/lib/client.js
CHANGED
|
@@ -282,9 +282,15 @@ window.__ModuleLoader__.load({
|
|
|
282
282
|
// 顺序只影响展示:DeepSeek 放在末尾以免打乱既有条目位置;药丸匹配靠 provider id,与顺序无关。
|
|
283
283
|
const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "openai-codex", "xai-oauth", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt", "ark-coding-plan-cn", "ark-agent-plan-cn", "ark-coding-plan-byteplus", "siliconflow", "deepseek", "openrouter", "novita", "hyperbolic", "deepinfra", "chutes", "ollama-cloud", "vercel-ai-gateway", "zenmux", "litellm", "arkcli-agent-plan", "arkcli-coding-plan", "arkcli-agent-plan-team", "arkcli-coding-plan-team"];
|
|
284
284
|
function providerEnabled(settings, id) { return settings?.visibility?.providers?.[id] ?? (PROVIDER_META[id]?.defaultEnabled !== false); }
|
|
285
|
-
|
|
285
|
+
// provider id 就是路由 id,而路由由提供方插件/CLI 按「你实际订阅了什么」写入:例如 arkcli helper
|
|
286
|
+
// 只会为真正持有的套餐写路由。所以本机没有 Coding Plan 路由时,就不该默认把 Coding Plan 显示出来
|
|
287
|
+
// ——这比「常量里默认开启」更贴近事实。undefined = 拿不到路由表,按「未知」放行,不谎报未安装。
|
|
288
|
+
function providerRoutable(result, id) { return result?.configured?.[id] !== false; }
|
|
289
|
+
function providerVisible(settings, id, entry, result) {
|
|
286
290
|
if (!providerEnabled(settings, id)) return false;
|
|
291
|
+
// 关掉「没有检测到 API 的默认隐藏」就是要看全部,不再按路由或凭据收起。
|
|
287
292
|
if (settings?.visibility?.hideWithoutApi === false) return true;
|
|
293
|
+
if (!providerRoutable(result, id)) return false;
|
|
288
294
|
return entry?.apiDetected !== false && !["no-key", "no-cookie"].includes(entry?.state);
|
|
289
295
|
}
|
|
290
296
|
/** token 数紧凑格式:1,234,567,890 → 1.23B */
|
|
@@ -642,7 +648,7 @@ window.__ModuleLoader__.load({
|
|
|
642
648
|
};
|
|
643
649
|
const refresh = (ids, force = false) => {
|
|
644
650
|
if (disposed) return Promise.reject(new Error("Client 已卸载"));
|
|
645
|
-
const unique = [...new Set(ids)].filter((id) => PROVIDER_ORDER.includes(id) && providerEnabled(snapshot.settings, id));
|
|
651
|
+
const unique = [...new Set(ids)].filter((id) => PROVIDER_ORDER.includes(id) && providerEnabled(snapshot.settings, id) && providerRoutable(snapshot, id));
|
|
646
652
|
const missing = unique.filter((id) => !inflight.has(id));
|
|
647
653
|
if (missing.length) {
|
|
648
654
|
const startedEpoch = epoch, request = markRequest(missing);
|
|
@@ -711,7 +717,7 @@ window.__ModuleLoader__.load({
|
|
|
711
717
|
},
|
|
712
718
|
reader: (id) => {
|
|
713
719
|
if (!readers.has(id)) {
|
|
714
|
-
const current = () => { const e = snapshot.entries.find((item) => item.providerId === id); return { ...(e || { providerId: id, state: "loading", windows: [] }), visible: providerVisible(snapshot.settings, id, e) }; };
|
|
720
|
+
const current = () => { const e = snapshot.entries.find((item) => item.providerId === id); return { ...(e || { providerId: id, state: "loading", windows: [] }), visible: providerVisible(snapshot.settings, id, e, snapshot) }; };
|
|
715
721
|
const fn = async (force = false) => { await refresh([id], force); return current(); };
|
|
716
722
|
fn.subscribe = (cb) => subscribe(() => cb(current()));
|
|
717
723
|
fn.activate = () => activate(id);
|
|
@@ -1245,7 +1251,7 @@ window.__ModuleLoader__.load({
|
|
|
1245
1251
|
// 待处理的排前面;组内保持登记顺序,所以顺序不会自己抖动。
|
|
1246
1252
|
const visibleIds = PROVIDER_ORDER
|
|
1247
1253
|
.map((id, index) => ({ id, index, rank: usageAttentionRank(entries.find(e => e.providerId === id), t) }))
|
|
1248
|
-
.filter(item => providerVisible(result.settings, item.id, entries.find(e => e.providerId === item.id)))
|
|
1254
|
+
.filter(item => providerVisible(result.settings, item.id, entries.find(e => e.providerId === item.id), result))
|
|
1249
1255
|
.sort((a, b) => b.rank - a.rank || a.index - b.index)
|
|
1250
1256
|
.map(item => item.id);
|
|
1251
1257
|
const success = entries.filter((e) => visibleIds.includes(e.providerId) && e.state === "ok" && e.freshness !== "stale").length;
|
|
@@ -1344,7 +1350,8 @@ window.__ModuleLoader__.load({
|
|
|
1344
1350
|
!editorOpen && notice ? h("p", { role: "status", style: { color: notice.ok ? "inherit" : "#dc2626" } }, notice.text) : null,
|
|
1345
1351
|
h("button", { type: "button", role: "switch", "aria-checked": hideWithoutApi, disabled: busy || dirty || !result.settings?.revision, "aria-label": text("没有检测到API的默认隐藏", "Hide providers without detected API credentials"), style: hideWithoutApi ? primary : btnStyle, onClick: () => saveVisibility({ hideWithoutApi: !hideWithoutApi }) }, `${hideWithoutApi ? "✓ " : "○ "}${text("没有检测到API的默认隐藏", "Hide providers without detected API credentials")}`),
|
|
1346
1352
|
h("p", { style: { fontSize: 12, opacity: .75 } }, text("检测依据为可用 Key 或 MiMo Cookie 是否存在;认证失败和网络错误不会当成未配置 API。隐藏的提供商仍可在这里配置。", "Detection checks for a Key or MiMo Cookie, not request success. Auth/network errors are not treated as missing API credentials. Hidden providers remain configurable here.")),
|
|
1347
|
-
|
|
1353
|
+
// 启用的排到最前面,省得每次往下翻;组内保持 PROVIDER_ORDER 的既有顺序(sort 稳定)。
|
|
1354
|
+
h("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 240px), 1fr))", gap: 8 } }, [...PROVIDER_ORDER].sort((a, b) => Number(!providerEnabled(result.settings, a)) - Number(!providerEnabled(result.settings, b))).map(id => {
|
|
1348
1355
|
const enabled = providerEnabled(result.settings, id), e = entries.find(item => item.providerId === id), editing = tab === id && editorOpen;
|
|
1349
1356
|
return h("div", { key: id, style: { gridColumn: editing ? "1 / -1" : undefined, display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center", border: "1px solid rgba(128,128,128,.25)", borderRadius: 8, padding: 8 } },
|
|
1350
1357
|
h("span", { style: { flex: 1, minWidth: 80 } }, PROVIDER_META[id].short),
|
package/lib/index.js
CHANGED
|
@@ -227,7 +227,9 @@ function parseSettings(value) {
|
|
|
227
227
|
}
|
|
228
228
|
return { expectedRevision: value.expectedRevision, visibility };
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
// 这里只校验形状:未知 provider(新版 Client + 还没重启的旧 Host)交给 persist 给可读错误,
|
|
231
|
+
// 否则用户只会看到网关的 "boundary validation" 通用报错,无从判断该重启还是该改配置。
|
|
232
|
+
if (!value || typeof value !== "object" || typeof value.providerId !== "string" || !value.providerId) throw invalid("Invalid provider patch");
|
|
231
233
|
const out = { providerId: value.providerId };
|
|
232
234
|
if (value.expectedRevision !== undefined) { if (typeof value.expectedRevision !== "string") throw invalid("Invalid revision"); out.expectedRevision = value.expectedRevision; }
|
|
233
235
|
if (value.keyMode !== undefined) { if (!["inherit", "manual"].includes(value.keyMode)) throw invalid("Invalid key mode"); out.keyMode = value.keyMode; }
|
|
@@ -273,12 +275,16 @@ function parseSettings(value) {
|
|
|
273
275
|
function parseQuery(value) {
|
|
274
276
|
// request 在 wire 层可选(read 描述符声明 acceptsUndefined):缺参/undefined/null
|
|
275
277
|
// = 全量默认账户查询,兼容旧 client bundle 的无参 read。
|
|
276
|
-
if (value === undefined || value === null) return { providerIds: [...IDS], force: false, commandCodeAccount: "" };
|
|
277
|
-
|
|
278
|
+
if (value === undefined || value === null) return { providerIds: [...IDS], unknown: [], force: false, commandCodeAccount: "" };
|
|
279
|
+
// 未知 id **过滤而不是整体拒绝**:provider 清单随版本演进,而 Host 需要重启、Client 刷新即生效,
|
|
280
|
+
// 这个版本窗口期里客户端必然带着本 Host 还不认识的 id 过来。整体拒绝会让用户一片空白;
|
|
281
|
+
// 过滤掉未知项并单独回一条可读原因(见 refresh),既保住其它提供商,也说清了为什么。
|
|
282
|
+
if (!value || !Array.isArray(value.providerIds) || typeof value.force !== "boolean") throw invalid("Invalid refresh request");
|
|
278
283
|
// commandCodeAccount 是可选的显示账户选择:空串/缺省 = 默认账户(顶层 Key);
|
|
279
284
|
// 额外账户 id 即其凭据引用名(apiKeyEnv),由 identity 按引用解析。
|
|
280
285
|
if (value.commandCodeAccount !== undefined && typeof value.commandCodeAccount !== "string") throw invalid("Invalid account selection");
|
|
281
|
-
|
|
286
|
+
const requested = [...new Set(value.providerIds)].filter(id => typeof id === "string");
|
|
287
|
+
return { providerIds: requested.filter(id => IDS.includes(id)), unknown: requested.filter(id => !IDS.includes(id)), force: value.force, commandCodeAccount: typeof value.commandCodeAccount === "string" ? value.commandCodeAccount.trim() : "" };
|
|
282
288
|
}
|
|
283
289
|
function failure(code, message, retryable = false, retryAfterMs) {
|
|
284
290
|
return new RemoteError(code, message, { retryable, retainPrevious: retryable, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}) });
|
|
@@ -296,7 +302,7 @@ function volcFailure(action, error) {
|
|
|
296
302
|
}
|
|
297
303
|
async function fetchJson(fetcher, url, headers, lifetimeSignal, init = {}) {
|
|
298
304
|
let response;
|
|
299
|
-
try { response = await fetcher(url, { method: init.method ?? "GET", ...(init.body !== undefined ? { body: init.body } : {}), headers: { accept: "application/json", "user-agent": "dsh-subusage/0.10.
|
|
305
|
+
try { response = await fetcher(url, { method: init.method ?? "GET", ...(init.body !== undefined ? { body: init.body } : {}), headers: { accept: "application/json", "user-agent": "dsh-subusage/0.10.1", ...headers }, signal: lifetimeSignal ? AbortSignal.any([AbortSignal.timeout(10000), lifetimeSignal]) : AbortSignal.timeout(10000), redirect: "manual" }); }
|
|
300
306
|
catch { throw failure("subusage/network", "网络请求失败或超时", true); }
|
|
301
307
|
if (response.status >= 300 && response.status < 400 || response.status === 401 || response.status === 403) throw failure("subusage/auth", "登录或凭据已失效");
|
|
302
308
|
if (!response.ok) {
|
|
@@ -744,7 +750,7 @@ export function normalizeLitellm(keyBody, budgetBody) {
|
|
|
744
750
|
}
|
|
745
751
|
function detectConfigured(ctx) {
|
|
746
752
|
try { const ids = new Set(ctx.llm.listProviders().map(p => p.id)); return Object.fromEntries(IDS.map(id => [id, ids.has(id)])); }
|
|
747
|
-
catch { return
|
|
753
|
+
catch { return undefined; }
|
|
748
754
|
}
|
|
749
755
|
const codec = (type, parse) => ({ mode: "strict", typeSymbol: `dsh-subusage#${type}`, schema: { parse }, create: () => ({ parse }) });
|
|
750
756
|
const resultCodec = codec("SubUsageResult", value => { if (!value || !Array.isArray(value.entries) || !value.settings || !value.configured || typeof value.updatedAt !== "string") throw invalid("Invalid result"); return value; });
|
|
@@ -1078,9 +1084,12 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1078
1084
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
1079
1085
|
const query = parseQuery(request); const settings = this.load();
|
|
1080
1086
|
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: "读取用量失败" }))));
|
|
1087
|
+
// 客户端请求了本 Host 不认识的 provider(装了新版但还没重启 DSH):回一条可读原因,
|
|
1088
|
+
// 否则它只会显示成「无数据」,用户无从判断是该重启还是该配凭据。
|
|
1089
|
+
const unknownEntries = (query.unknown ?? []).map(id => ({ providerId: id, label: id, state: "error", errorCode: "subusage/unknown-provider", error: "本机运行中的 Host 版本较旧,不认识该提供商;重启 DSH 后生效", apiDetected: null, coverage: "partial", freshness: "unknown", keySource: "none", retryable: false, retainPrevious: false, windows: [], extras: [] }));
|
|
1081
1090
|
const latest = this.load();
|
|
1082
|
-
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: "读取期间配置已改变,请刷新" })));
|
|
1083
|
-
return this.result(latest, entries);
|
|
1091
|
+
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: "读取期间配置已改变,请刷新" })), ...unknownEntries]);
|
|
1092
|
+
return this.result(latest, [...entries, ...unknownEntries]);
|
|
1084
1093
|
}
|
|
1085
1094
|
startMimoLogin(request) { return this.login.start(parseLoginStart(request)); } getMimoLoginStatus() { return this.login.status(); }
|
|
1086
1095
|
cancelMimoLogin(request) { return this.login.cancel(parseLoginCancel(request)); }
|
|
@@ -1105,6 +1114,9 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1105
1114
|
persist(settings, cookieCap) {
|
|
1106
1115
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
1107
1116
|
const patch = parseSettings(settings); const stored = this.load();
|
|
1117
|
+
// 未知 provider:这是「客户端已更新、Host 还没重启」的版本窗口期,给出可读原因而不是静默写入垃圾配置。
|
|
1118
|
+
// 只改可见性的 patch 不带 providerId,不能因此被拒。
|
|
1119
|
+
if (patch.providerId !== undefined && !IDS.includes(patch.providerId)) throw failure("subusage/unknown-provider", "本机运行中的 Host 版本较旧,不认识该提供商;重启 DSH 后生效");
|
|
1108
1120
|
if (patch.expectedRevision !== undefined && patch.expectedRevision !== hash(stored)) throw failure("subusage/revision-conflict", "设置已被其他窗口修改,请重新读取");
|
|
1109
1121
|
const id = patch.providerId;
|
|
1110
1122
|
// 火山方舟两个 provider 共用一组 AK/SK:改凭据要让所有用它的条目失效,不能只失效当前标签页。
|