dsh-subusage 0.10.8 → 0.10.10
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 +49 -27
- package/lib/index.js +27 -4
- 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
|
@@ -389,13 +389,16 @@ window.__ModuleLoader__.load({
|
|
|
389
389
|
}
|
|
390
390
|
return worst;
|
|
391
391
|
}
|
|
392
|
+
function remainingPercent(percent) {
|
|
393
|
+
return Math.round(Math.max(0, 100 - (Number(percent) || 0)) * 10) / 10;
|
|
394
|
+
}
|
|
392
395
|
/** 药丸文案:永远答「还能用多少」(余),限额/连坐直说。多窗带窗口短名。 */
|
|
393
396
|
function pillText(w, multi, t) {
|
|
394
397
|
// 短标签走 locale(中文「周 / 月 / 日」,英文 1w / 1m / 1d),不让人猜 W 是哪个窗口。
|
|
395
398
|
const short = multi ? `${t(KIND_SHORT[w.kind] ?? "srolling")} ` : "";
|
|
396
399
|
if (w.status === "rate-limited" && !w.cascade) return `${short}${t("pillLimited")}`;
|
|
397
400
|
if (w.status === "rate-limited") return `${short}${t("pillBlocked")}`;
|
|
398
|
-
return `${short}${t("pillRemaining").replace("{p}", String(
|
|
401
|
+
return `${short}${t("pillRemaining").replace("{p}", String(remainingPercent(w.percent)))}`;
|
|
399
402
|
}
|
|
400
403
|
/** extras 标签:plan / account / balance 三类,未知类型回退为余额。 */
|
|
401
404
|
function extraLabel(x, t) {
|
|
@@ -684,8 +687,9 @@ window.__ModuleLoader__.load({
|
|
|
684
687
|
if (disposed) return snapshot;
|
|
685
688
|
const value = { ...incoming };
|
|
686
689
|
if (request && request.version < metadataSequence) { delete value.settings; delete value.configured; delete value.updatedAt; }
|
|
690
|
+
// 过期请求的 settings 已在上一行丢掉,所以这里若还带着 settings,就是「当前代」看到的 Host 配置。
|
|
691
|
+
// 另一窗口改过配置时采用它(开关跟着变),但不再 epoch++ / invalidate 全员——那会让非活跃条目永久 loading。
|
|
687
692
|
if (value.settings) metadataSequence = request ? request.version : ++sequence;
|
|
688
|
-
if (!localChange && snapshot.settings && value.settings && snapshot.settings.revision !== value.settings.revision) { epoch++; invalidate(PROVIDER_ORDER); }
|
|
689
693
|
const entries = new Map(snapshot.entries.map((e) => [e.providerId, e]));
|
|
690
694
|
for (const e of value.entries || []) if (!request || providerSequence.get(e.providerId) === request.version) entries.set(e.providerId, e);
|
|
691
695
|
snapshot = { ...snapshot, ...value, entries: [...entries.values()] };
|
|
@@ -694,17 +698,22 @@ window.__ModuleLoader__.load({
|
|
|
694
698
|
const refresh = (ids, force = false) => {
|
|
695
699
|
if (disposed) return Promise.reject(new Error("Client 已卸载"));
|
|
696
700
|
const unique = [...new Set(ids)].filter((id) => PROVIDER_ORDER.includes(id) && providerEnabled(snapshot.settings, id) && providerRoutable(snapshot, id));
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
const startedEpoch = epoch, request = markRequest(
|
|
700
|
-
const task = Promise.resolve().then(() => call("refresh", { providerIds:
|
|
701
|
+
const start = (id) => {
|
|
702
|
+
if (inflight.has(id)) return inflight.get(id);
|
|
703
|
+
const startedEpoch = epoch, request = markRequest([id]);
|
|
704
|
+
const task = Promise.resolve().then(() => call("refresh", { providerIds: [id], force: !!force, commandCodeAccount: ccAccount })).then((value) => startedEpoch === epoch ? merge(value, request) : snapshot).catch((error) => {
|
|
701
705
|
// 传输失败不能擅自 retainPrevious:只有 Host 明确标 stale 才保留。
|
|
702
|
-
if (startedEpoch === epoch) merge({ entries:
|
|
706
|
+
if (startedEpoch === epoch) merge({ entries: [{ providerId: id, state: "error", error: error instanceof Error ? error.message : String(error), freshness: "unknown", coverage: "partial", windows: [], extras: [] }] }, request);
|
|
703
707
|
throw error;
|
|
704
|
-
}).finally(() => {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
+
}).finally(() => { if (inflight.get(id) === task) inflight.delete(id); });
|
|
709
|
+
inflight.set(id, task);
|
|
710
|
+
return task;
|
|
711
|
+
};
|
|
712
|
+
const tasks = unique.map(start);
|
|
713
|
+
// 调用方点名多家(刷新全部)即使过滤后只剩一家,也不把失败抬成整页错误。
|
|
714
|
+
const tolerant = ids.length > 1;
|
|
715
|
+
if (unique.length <= 1 && !tolerant) return (tasks[0] ?? Promise.resolve(snapshot)).then(() => snapshot);
|
|
716
|
+
return Promise.all(tasks.map((task) => task.catch(() => null))).then(() => snapshot);
|
|
708
717
|
};
|
|
709
718
|
const visible = () => { if (typeof document === "undefined" || document.visibilityState !== "hidden") void refresh([...active.keys()]).catch(() => {}); };
|
|
710
719
|
const tick = () => {
|
|
@@ -745,7 +754,7 @@ window.__ModuleLoader__.load({
|
|
|
745
754
|
// invalidate 只重写快照不通知:补一次 emit 让药丸立即进入加载态,
|
|
746
755
|
// 避免强制刷新返回前「标注已是新账户、数字还是旧账户」的瞬态错标。
|
|
747
756
|
emit();
|
|
748
|
-
return refresh(["commandcode"], true);
|
|
757
|
+
return refresh(["commandcode"], true).catch(() => snapshot);
|
|
749
758
|
};
|
|
750
759
|
return {
|
|
751
760
|
subscribe, getSnapshot: () => snapshot, refresh, activate,
|
|
@@ -755,10 +764,16 @@ window.__ModuleLoader__.load({
|
|
|
755
764
|
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); },
|
|
756
765
|
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); },
|
|
757
766
|
save: async (patch) => {
|
|
758
|
-
const value = await call("save", patch);
|
|
759
|
-
|
|
767
|
+
const value = await call("save", patch);
|
|
768
|
+
// 可见性开关只改 settings:不动 epoch、不重置在途请求序号——否则「刷新全部进行中拨开关」
|
|
769
|
+
// 会把所有在途用量结果整批丢弃(startedEpoch !== epoch 直接放弃 merge)。merge 内部会推进
|
|
770
|
+
// metadataSequence,让在途响应里的过期 settings 自动丢弃,本地新 revision 不被盖回。
|
|
771
|
+
if (patch.visibility) return merge({ ...value, entries: snapshot.entries }, null, true);
|
|
772
|
+
// 凭据变更改变用量语义:推进 epoch 丢弃在途。火山 AK/SK 是组内容共用的,必须整组失效。
|
|
773
|
+
epoch++;
|
|
760
774
|
const cookie = patch.cookieUpdate;
|
|
761
|
-
|
|
775
|
+
const affected = PROVIDER_META[patch.providerId]?.volc && patch.volc ? ARK_IDS : [patch.providerId];
|
|
776
|
+
invalidate(affected, !cookie || cookie.action === "keep"); return merge(value, null, true);
|
|
762
777
|
},
|
|
763
778
|
reader: (id) => {
|
|
764
779
|
if (!readers.has(id)) {
|
|
@@ -905,14 +920,23 @@ window.__ModuleLoader__.load({
|
|
|
905
920
|
);
|
|
906
921
|
}
|
|
907
922
|
|
|
923
|
+
function RemainingBar({ value, color, label }) {
|
|
924
|
+
const pct = Math.max(0, Math.min(100, Number(value) || 0));
|
|
925
|
+
return h("div", {
|
|
926
|
+
role: "progressbar",
|
|
927
|
+
"aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct, "aria-label": label,
|
|
928
|
+
style: { width: "100%", height: 10, marginTop: 8, borderRadius: 6, background: "rgba(148,163,184,.22)", overflow: "hidden" }
|
|
929
|
+
}, h("div", { style: { width: `${pct}%`, height: "100%", borderRadius: 6, background: color, boxShadow: pct ? "inset 0 1px 0 rgba(255,255,255,.2)" : "none" } }));
|
|
930
|
+
}
|
|
908
931
|
/**
|
|
909
|
-
* 单窗口行(药丸弹层与设置页共用):徽标 +
|
|
932
|
+
* 单窗口行(药丸弹层与设置页共用):徽标 + 余量% 主数字 + 进度条(满格=100% 剩余,随已用扣减)+ 明细行。
|
|
910
933
|
* 明细分两行:用量/适用范围一行,重置时间一行。挤在一行时「重置于 …(还有 …)」
|
|
911
934
|
* 会在窄弹层里被从中间折断,读起来像属性和数值分了家。
|
|
912
935
|
*/
|
|
913
936
|
function UsageWindowRow({ w, t, getLocale, uncertain = false }) {
|
|
914
937
|
const tier = uncertain && w.status !== "rate-limited" ? { icon: "?", badge: "badgeUnknown", color: "#b7791f", rank: 2 } : usageTier(w);
|
|
915
938
|
const blocked = w.status === "rate-limited";
|
|
939
|
+
const left = blocked ? 0 : remainingPercent(w.percent);
|
|
916
940
|
const usageLine = [
|
|
917
941
|
w.detail && typeof w.detail.used === "number" && typeof w.detail.limit === "number" && w.detail.limit > 0
|
|
918
942
|
? t("detailUsed").replace("{used}", fmtTokens(w.detail.used)).replace("{limit}", fmtTokens(w.detail.limit)).replace("{unit}", w.detail.unit || "")
|
|
@@ -943,12 +967,9 @@ window.__ModuleLoader__.load({
|
|
|
943
967
|
}, `${tier.icon} ${t(tier.badge)}`),
|
|
944
968
|
h("span", { style: { flex: 1 } }),
|
|
945
969
|
h("strong", { style: { fontSize: 13, color: numColor(w), whiteSpace: "nowrap" } },
|
|
946
|
-
blocked ? t(tier.num) : t("
|
|
970
|
+
blocked ? t(tier.num) : t("pillRemaining").replace("{p}", String(remainingPercent(w.percent))))
|
|
947
971
|
),
|
|
948
|
-
|
|
949
|
-
max: 100, value: Math.min(100, w.percent),
|
|
950
|
-
style: { width: "100%", height: 6, marginTop: 6, accentColor: tier.color }
|
|
951
|
-
}),
|
|
972
|
+
RemainingBar({ value: left, color: tier.color, label: blocked ? t(tier.num) : t("pillRemaining").replace("{p}", String(remainingPercent(w.percent))) }),
|
|
952
973
|
usageLine || resetParts
|
|
953
974
|
? h("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 4 } },
|
|
954
975
|
usageLine ? h("div", { style: { overflowWrap: "anywhere" } }, usageLine) : null,
|
|
@@ -987,7 +1008,7 @@ window.__ModuleLoader__.load({
|
|
|
987
1008
|
// unavailable/loading 保持现状;store 内部对相同账户去重,不产生多余请求。
|
|
988
1009
|
react.useEffect(() => {
|
|
989
1010
|
if (providerId !== "commandcode" || !ccState || !setCommandCodeAccount) return;
|
|
990
|
-
if (ccState.status === "ready") void setCommandCodeAccount(ccState.activeId);
|
|
1011
|
+
if (ccState.status === "ready") void setCommandCodeAccount(ccState.activeId).catch(() => {});
|
|
991
1012
|
}, [providerId, setCommandCodeAccount, ccState && ccState.status, ccState && ccState.activeId]);
|
|
992
1013
|
react.useEffect(() => {
|
|
993
1014
|
let alive = true;
|
|
@@ -1197,6 +1218,7 @@ window.__ModuleLoader__.load({
|
|
|
1197
1218
|
const [draft, setDraft] = react.useState(() => draftFor(result.settings, tab));
|
|
1198
1219
|
const [dirty, setDirty] = react.useState(false);
|
|
1199
1220
|
const [busy, setBusy] = react.useState(false);
|
|
1221
|
+
const [refreshing, setRefreshing] = react.useState(false);
|
|
1200
1222
|
const [notice, setNotice] = react.useState(null);
|
|
1201
1223
|
const [loadError, setLoadError] = react.useState(null);
|
|
1202
1224
|
const [login, setLogin] = react.useState({ jobId: null, state: "idle" });
|
|
@@ -1281,10 +1303,10 @@ window.__ModuleLoader__.load({
|
|
|
1281
1303
|
try { window.localStorage.setItem("dsh-subusage:last-provider", id); } catch {}
|
|
1282
1304
|
};
|
|
1283
1305
|
const refresh = async (all) => {
|
|
1284
|
-
|
|
1306
|
+
setRefreshing(true);
|
|
1285
1307
|
try { if (!result.settings) await usageStore.readAll(); else await usageStore.refresh(all ? PROVIDER_ORDER : usageId ? [usageId] : [], true); if (aliveRef.current) setLoadError(null); }
|
|
1286
1308
|
catch (error) { if (aliveRef.current) setLoadError(String(error.message || error)); }
|
|
1287
|
-
finally { if (aliveRef.current)
|
|
1309
|
+
finally { if (aliveRef.current) setRefreshing(false); }
|
|
1288
1310
|
};
|
|
1289
1311
|
const saveVisibility = async (visibility) => {
|
|
1290
1312
|
if (busy || !result.settings || !result.settings.revision) return;
|
|
@@ -1319,7 +1341,7 @@ window.__ModuleLoader__.load({
|
|
|
1319
1341
|
if (!providerEnabled(saved.settings, tab)) return;
|
|
1320
1342
|
// 保存已成功落盘;独立验证失败不改写成“保存失败”。
|
|
1321
1343
|
try {
|
|
1322
|
-
const verified = await usageStore.refresh([tab], true);
|
|
1344
|
+
const verified = await usageStore.refresh(PROVIDER_META[tab]?.volc ? ARK_IDS : [tab], true);
|
|
1323
1345
|
const current = verified.entries.find((e) => e.providerId === tab);
|
|
1324
1346
|
if (aliveRef.current) setLoadError(current && current.state === "ok" ? null : text("已保存;验证获取失败:", "Saved; validation fetch failed: ") + (current && current.error || statusText(current, t)));
|
|
1325
1347
|
} catch (error) { if (aliveRef.current) setLoadError(text("已保存;验证获取失败:", "Saved; validation fetch failed: ") + String(error.message || error)); }
|
|
@@ -1473,8 +1495,8 @@ window.__ModuleLoader__.load({
|
|
|
1473
1495
|
"aria-label": text(`${success}/${visibleIds.length} 家数据获取成功;${troubledIds.length ? `点击跳到 ${PROVIDER_META[troubledIds[0]].short}` : "没有待处理项"}`, `${success}/${visibleIds.length} providers fetched; ${troubledIds.length ? `click to jump to ${PROVIDER_META[troubledIds[0]].short}` : "nothing needs attention"}`),
|
|
1474
1496
|
style: { ...btnStyle, padding: "2px 8px", fontSize: 12, opacity: .85, cursor: troubledIds.length ? "pointer" : "default", color: troubledIds.length ? "#b7791f" : "inherit" }
|
|
1475
1497
|
}, text(`${success}/${visibleIds.length} 家数据获取成功`, `${success}/${visibleIds.length} providers fetched`)),
|
|
1476
|
-
h("button", { type: "button", disabled: busy || !usageId, style: primary, onClick: () => refresh(false) }, text("刷新当前", "Refresh current")),
|
|
1477
|
-
h("button", { type: "button", disabled: busy, style: btnStyle, onClick: () => refresh(true) }, text("刷新全部", "Refresh all"))
|
|
1498
|
+
h("button", { type: "button", disabled: busy || refreshing || !usageId, style: { ...primary, cursor: busy || refreshing || !usageId ? "default" : "pointer" }, onClick: () => refresh(false) }, refreshing ? t("refreshing") : text("刷新当前", "Refresh current")),
|
|
1499
|
+
h("button", { type: "button", disabled: busy || refreshing, style: { ...btnStyle, cursor: busy || refreshing ? "default" : "pointer" }, onClick: () => refresh(true) }, refreshing ? t("refreshing") : text("刷新全部", "Refresh all"))
|
|
1478
1500
|
),
|
|
1479
1501
|
loadError ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, loadError) : null,
|
|
1480
1502
|
!visibleIds.length ? h("p", { role: "status" }, text("没有可显示的提供商,请展开下方「提供商管理」开启、配置 API,或关闭默认隐藏。", "No visible providers. Expand Provider management below to enable a provider, configure API credentials, or turn off auto-hide.")) : null,
|
package/lib/index.js
CHANGED
|
@@ -387,6 +387,17 @@ function parseQuery(value) {
|
|
|
387
387
|
function failure(code, message, retryable = false, retryAfterMs) {
|
|
388
388
|
return new RemoteError(code, message, { retryable, retainPrevious: retryable, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}) });
|
|
389
389
|
}
|
|
390
|
+
// 与 fetchJson 的 AbortSignal.timeout 同语义的 promise 超时门;ms 非正数视为不限时。
|
|
391
|
+
// race 结束即清 timer:未触发的 setTimeout 否则会把测试进程挂到超时才退出;先 settle 的
|
|
392
|
+
// 原始 promise 之后才 reject 时,handler 已由 race 注册,不会变成 unhandledRejection。
|
|
393
|
+
function withTimeout(promise, ms, makeError) {
|
|
394
|
+
if (!Number.isFinite(ms) || ms <= 0) return promise;
|
|
395
|
+
let timer;
|
|
396
|
+
const work = Promise.resolve(promise);
|
|
397
|
+
work.catch(() => {});
|
|
398
|
+
const gate = new Promise((_, reject) => { timer = setTimeout(() => reject(makeError()), ms); });
|
|
399
|
+
return Promise.race([work, gate]).finally(() => { clearTimeout(timer); });
|
|
400
|
+
}
|
|
390
401
|
// 火山方舟把拒绝原因放在 ResponseMetadata.Error 业务信封里:401 是签名/凭据(服务端不知道你是谁),
|
|
391
402
|
// 403 是授权(身份已确认但不能做:IAM 权限不足、套餐未开通、欠费),两者对用户的行动项不同。
|
|
392
403
|
function volcFailure(action, error) {
|
|
@@ -400,7 +411,7 @@ function volcFailure(action, error) {
|
|
|
400
411
|
}
|
|
401
412
|
async function fetchJson(fetcher, url, headers, lifetimeSignal, init = {}) {
|
|
402
413
|
let response;
|
|
403
|
-
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.
|
|
414
|
+
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.10", ...headers }, signal: lifetimeSignal ? AbortSignal.any([AbortSignal.timeout(10000), lifetimeSignal]) : AbortSignal.timeout(10000), redirect: "manual" }); }
|
|
404
415
|
catch { throw failure("subusage/network", "网络请求失败或超时", true); }
|
|
405
416
|
if (response.status >= 300 && response.status < 400 || response.status === 401 || response.status === 403) throw failure("subusage/auth", "登录或凭据已失效");
|
|
406
417
|
if (!response.ok) {
|
|
@@ -876,6 +887,9 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
876
887
|
this.io = options.io ?? { readFileSync, mkdirSync, writeFileSync, renameSync, chmodSync };
|
|
877
888
|
this.path = options.configPath ?? CONFIG_PATH; this.fetcher = options.fetch ?? globalThis.fetch;
|
|
878
889
|
this.now = options.now ?? Date.now;
|
|
890
|
+
// identity 解析(凭据服务/登录文件读取)与 HTTP 同量级的超时,默认 10s(对齐 fetchJson 的
|
|
891
|
+
// AbortSignal.timeout);经 options 注入以便测试用极短值,不必真等 10 秒。
|
|
892
|
+
this.identityTimeoutMs = options.identityTimeoutMs ?? 10000;
|
|
879
893
|
this.credentials = options.resolveCredentials ?? (async id => await ctx.get("credentials")?.resolve(credentialRef(PROVIDERS[id].envName)));
|
|
880
894
|
this.environment = options.resolveEnvironment ?? (id => launchEnvironmentOf(ctx).get(PROVIDERS[id].envName));
|
|
881
895
|
// 火山方舟的 AK/SK 不是路由的推理 Key,按固定变量名各自解析,两端都命中才算配对。
|
|
@@ -1125,8 +1139,17 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1125
1139
|
async provider(id, settings, force, account = "") {
|
|
1126
1140
|
const startedGeneration = this.generations.get(id) ?? 0;
|
|
1127
1141
|
let identity;
|
|
1128
|
-
|
|
1129
|
-
|
|
1142
|
+
// identity 解析依赖凭据服务/登录文件读取,可能像 HTTP 一样永久挂起:套上与 fetchJson 同量级
|
|
1143
|
+
// 的超时。超时不等于凭据失效——不清 Cookie、不用 subusage/credentials(client 的
|
|
1144
|
+
// /auth|credential/ 判定会把它导去重新登录/凭据编辑入口),而是独立 errorCode 的可读条目。
|
|
1145
|
+
// 只让这一家及时返回,不拖死整批 Promise.all;挂起的原始 promise 由 race 持有,之后
|
|
1146
|
+
// settle 也被静默丢弃(含其 reject,handler 已注册,不会变成 unhandledRejection)。
|
|
1147
|
+
try { identity = await withTimeout(this.identity(id, settings, account), this.identityTimeoutMs, () => failure("subusage/identity-timeout", `凭据解析超时(${Math.round(this.identityTimeoutMs / 1000)} 秒无响应):凭据服务或登录文件读取未返回,请稍后重试`)); }
|
|
1148
|
+
catch (e) {
|
|
1149
|
+
const timedOut = e?.code === "subusage/identity-timeout";
|
|
1150
|
+
this.cache.delete(id);
|
|
1151
|
+
return { ...this.base(id, "none", settings, account), state: "error", errorCode: timedOut ? "subusage/identity-timeout" : "subusage/credentials", error: timedOut ? e.message : "无法解析凭据" };
|
|
1152
|
+
}
|
|
1130
1153
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
1131
1154
|
if (startedGeneration !== (this.generations.get(id) ?? 0)) return this.provider(id, this.load(), force, account);
|
|
1132
1155
|
if (!providerEnabled(settings, id)) {
|
|
@@ -1184,7 +1207,7 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1184
1207
|
const wanted = matched.length || !unknown.length ? matched : [...IDS];
|
|
1185
1208
|
const settings = this.load();
|
|
1186
1209
|
const force = request?.force === true;
|
|
1187
|
-
const entries = await Promise.all(wanted.map(id => this.provider(id, settings, force).catch(() => ({ providerId: id, label: PROVIDERS[id].label, state: "error", errorCode: "subusage/internal", error: "读取用量失败" }))));
|
|
1210
|
+
const entries = await withTimeout(Promise.all(wanted.map(id => this.provider(id, settings, force).catch(() => ({ providerId: id, label: PROVIDERS[id].label, state: "error", errorCode: "subusage/internal", error: "读取用量失败" })))), 30000, () => failure("subusage/timeout", "额度查询超过 30 秒仍未完成,请稍后重试或缩小 providers"));
|
|
1188
1211
|
// 认不出的名字要回一条**可读**条目而不是静默丢掉:调用方(尤其是模型)无从知道本插件用的是
|
|
1189
1212
|
// 路由 id,丢掉之后它只看到空列表,会当成「本机没配任何额度」——真正的原因只是名字没对上。
|
|
1190
1213
|
// 放在最前面,免得淹没在几十条数据后面。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-subusage",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.10",
|
|
4
4
|
"displayName": "订阅用量显示",
|
|
5
5
|
"description": "显示 DeepSeek 官方余额、火山方舟(arkcli 官方 CLI 路由:Agent / Coding Plan 个人版与企业版席位,设置页合并为一组)、Z.ai(中国与国际)、Kimi、MiMo、OpenCode Go、Command Code、SuperGrok、Codex(ChatGPT 订阅,默认关闭)、MiniMax(中国)、Synthetic、NanoGPT、SiliconFlow、OpenRouter、Novita、Hyperbolic、DeepInfra、Chutes、Ollama Cloud、Vercel AI Gateway、ZenMux、LiteLLM 的订阅余量与余额;共享刷新与凭据管理,默认关闭 17 家。",
|
|
6
6
|
"keywords": [
|