dsh-subusage 0.10.4 → 0.10.5
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 +10 -4
- package/lib/index.js +129 -17
- 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
|
|
|
@@ -246,9 +246,14 @@ ark-coding-plan-byteplus ARK_CODING_PLAN_BYTEPLUS_API_KEY
|
|
|
246
246
|
|
|
247
247
|
| 参数 | 类型 | 说明 |
|
|
248
248
|
|---|---|---|
|
|
249
|
-
| `providers` | `string[]`(可选) |
|
|
249
|
+
| `providers` | `string[]`(可选) | 限定范围。可传本插件的 provider id(`deepseek`、`zai-coding-cn`、`kimi-coding`…),也可直接传厂商名(`zai`、`kimi`、`mimo`、`minimax`、`commandcode`、`grok`、`codex`、`ark`、`deepseek`…);大小写与 `-` `.` `_` 都无关。省略即读取全部 |
|
|
250
250
|
| `refresh` | `boolean`(可选) | `true` 绕过最多 60 秒的缓存,仅在确需最新数字时使用 |
|
|
251
251
|
|
|
252
|
+
调用约定(**结果永远不为空**):
|
|
253
|
+
|
|
254
|
+
- 模型手里的名字往往不是这里的 provider id(它更可能看到 DSH 的路由名,例如 `zai`),所以匹配是**宽松**的:一个名字命中多条路由时**全部返回**(`zai` → 中国版 + 国际版,`ark` → 7 条),每条各自带着真实 `state`(没启用的如实标 `disabled`),由调用方自己判断要看哪条。
|
|
255
|
+
- 一个名字都认不出时,返回里会**先**给一条说明(写明可用写法),**再附上全部 30 条数据**——数据先给出去,判断交给调用方,而不是回一个空结果。
|
|
256
|
+
|
|
252
257
|
**Host 服务**(Cordis `Service`,key 为 `subUsage`):
|
|
253
258
|
|
|
254
259
|
```js
|
|
@@ -256,14 +261,15 @@ const service = ctx.get("subUsage");
|
|
|
256
261
|
const view = await service.quota({ providerIds: ["deepseek"], force: false });
|
|
257
262
|
// view = {
|
|
258
263
|
// updatedAt,
|
|
259
|
-
// providers: [{ providerId, label, state, windows, extras, coverage
|
|
264
|
+
// providers: [{ providerId, label, state, windows, extras, coverage?, freshness?, lastSuccessAt?, error? }]
|
|
260
265
|
// }
|
|
261
266
|
```
|
|
262
267
|
|
|
263
268
|
- `state`:`ok` | `no-key` | `no-cookie` | `error` | `disabled`
|
|
264
269
|
- `windows[].percent` 是**已用**百分比(0–100);`extras` 放余额与套餐名等补充项
|
|
265
270
|
- 视图**刻意精简**:不返回设置,也不返回 `keySource` / `apiDetected` / 继承变量名等内部状态,第三方消费者拿不到任何与 Key 相关的字段
|
|
266
|
-
- `
|
|
271
|
+
- 视图里**不出现值为 `undefined` 的字段**:字段缺席表示「这家没有报告」,而不是「报告了空」——带 `undefined` 的键会被调用方的 schema 校验判成型别错误
|
|
272
|
+
- 单家读取失败只影响它自己的条目,其余照常返回
|
|
267
273
|
- 与设置页共用同一份缓存(TTL 60 秒),频繁调用不会反复请求各家接口
|
|
268
274
|
|
|
269
275
|
## Troubleshooting / 常见问题
|
package/lib/index.js
CHANGED
|
@@ -85,6 +85,65 @@ const IDS = Object.keys(PROVIDERS);
|
|
|
85
85
|
// 用一个组级开关控制整组。这些 id 仍各自存在——药丸要靠 provider id 匹配路由,不能合并。
|
|
86
86
|
const ARK_IDS = IDS.filter(id => PROVIDERS[id].credentialKind === "volc");
|
|
87
87
|
const ARK_DEFAULT_ON = ARK_IDS.some(id => PROVIDERS[id].defaultEnabled !== false);
|
|
88
|
+
/**
|
|
89
|
+
* 调用方(尤其是模型)手里的「提供商名」通常**不是**本插件的 provider id。模型能看到的往往是
|
|
90
|
+
* DSH 的 llm 路由名或厂商品牌名(`zai`、`grok`、`codex`、`ark`),而这里的 id 是路由 id
|
|
91
|
+
* (`zai-coding-cn`、`xai-oauth`、`openai-codex`…)。只做精确匹配时,`["zai"]` 会被整批过滤掉,
|
|
92
|
+
* 工具返回空列表——看上去像「本机什么都没配」,真正的原因却只是名字没对上。
|
|
93
|
+
* 所以先按别名收敛;一个别名指向多条路由时(`zai` 同时是中国版与国际版)不猜,交给调用方看结果。
|
|
94
|
+
*/
|
|
95
|
+
const PROVIDER_ALIASES = {
|
|
96
|
+
zai: ["zai-coding-cn", "zai-coding"], "z.ai": ["zai-coding-cn", "zai-coding"], "z-ai": ["zai-coding-cn", "zai-coding"],
|
|
97
|
+
zhipu: ["zai-coding-cn", "zai-coding"], glm: ["zai-coding-cn", "zai-coding"], "智谱": ["zai-coding-cn", "zai-coding"],
|
|
98
|
+
kimi: ["kimi-coding"], moonshot: ["kimi-coding"], "月之暗面": ["kimi-coding"],
|
|
99
|
+
mimo: ["xiaomi-token-plan-cn"], xiaomi: ["xiaomi-token-plan-cn"], "小米": ["xiaomi-token-plan-cn"],
|
|
100
|
+
minimax: ["minimax-cn", "minimax"], "海螺": ["minimax-cn", "minimax"],
|
|
101
|
+
commandcode: ["commandcode"], "command-code": ["commandcode"], "command code": ["commandcode"],
|
|
102
|
+
supergrok: ["xai-oauth"], grok: ["xai-oauth"], xai: ["xai-oauth"],
|
|
103
|
+
codex: ["openai-codex"], chatgpt: ["openai-codex"], gpt: ["openai-codex"], openai: ["openai-codex"],
|
|
104
|
+
ark: ARK_IDS, volcengine: ARK_IDS, volc: ARK_IDS, "火山": ARK_IDS, "方舟": ARK_IDS, "火山方舟": ARK_IDS, doubao: ARK_IDS, "豆包": ARK_IDS
|
|
105
|
+
};
|
|
106
|
+
/** 查表用的键:大小写与分隔符都无关,`Z.AI` / `zai_coding_cn` / `zai-coding-cn` 视为同一个词。 */
|
|
107
|
+
function normalizeProviderKey(value) { return String(value ?? "").trim().toLowerCase().replace(/[\s._-]+/g, ""); }
|
|
108
|
+
/**
|
|
109
|
+
* 归一化索引:id 与别名都进同一张表,因此「精确 id」只是这张表的一个特例。
|
|
110
|
+
* 一个键可以指向多条路由(`zai` → 中国版 + 国际版,`ark` → 7 条)。
|
|
111
|
+
*/
|
|
112
|
+
const PROVIDER_LOOKUP = (() => {
|
|
113
|
+
const map = new Map();
|
|
114
|
+
const add = (key, id) => {
|
|
115
|
+
const k = normalizeProviderKey(key);
|
|
116
|
+
if (!k) return;
|
|
117
|
+
const list = map.get(k) ?? [];
|
|
118
|
+
if (!list.includes(id)) list.push(id);
|
|
119
|
+
map.set(k, list);
|
|
120
|
+
};
|
|
121
|
+
for (const id of IDS) add(id, id);
|
|
122
|
+
for (const [alias, ids] of Object.entries(PROVIDER_ALIASES)) for (const id of ids) if (IDS.includes(id)) add(alias, id);
|
|
123
|
+
return map;
|
|
124
|
+
})();
|
|
125
|
+
/**
|
|
126
|
+
* 把调用方给的任意写法收敛成 provider id,**纯名字解析、不查任何外部状态**。
|
|
127
|
+
* 两条硬约定:
|
|
128
|
+
* 1. 认不出的原串**原样返回**而不是丢掉——调用方要能拿到「这个名字不认识,可用的是……」才能
|
|
129
|
+
* 自我纠正;静默过滤只会让它以为本机没有额度。
|
|
130
|
+
* 2. 一个名字命中多条路由时**全部返回**,各自带着真实状态(没启用的会如实标 disabled)。
|
|
131
|
+
* 例如 `zai` 同时是中国版与国际版、`ark` 是 7 条路由:宁可多给几条数据让调用方自己判断,
|
|
132
|
+
* 也不替它猜一条,更不能返回空。
|
|
133
|
+
* @param requested 调用方给的原始名字列表。
|
|
134
|
+
*/
|
|
135
|
+
function resolveProviderIds(requested) {
|
|
136
|
+
const hit = new Set(), unknown = [];
|
|
137
|
+
for (const raw of requested) {
|
|
138
|
+
const name = String(raw ?? "").trim();
|
|
139
|
+
if (!name) continue;
|
|
140
|
+
const ids = PROVIDER_LOOKUP.get(normalizeProviderKey(name));
|
|
141
|
+
if (!ids) { if (!unknown.includes(name)) unknown.push(name); continue; }
|
|
142
|
+
for (const id of ids) hit.add(id);
|
|
143
|
+
}
|
|
144
|
+
// 按登记顺序返回:命中多条时(例如 `ark`)读起来和设置页一致,而不是跟着调用方的输入顺序抖动。
|
|
145
|
+
return { ids: IDS.filter(id => hit.has(id)), unknown };
|
|
146
|
+
}
|
|
88
147
|
// 火山方舟额度接口的凭据是 IAM Access Key:AK 与 SK 必须同源配对,跨来源拼接只会得到 401。
|
|
89
148
|
// 变量名沿用火山官方 SDK 习惯;与方舟推理 Key(ARK_*_API_KEY)无关。
|
|
90
149
|
const VOLC_ACCESS_KEY_ENV = "VOLC_ACCESSKEY";
|
|
@@ -312,7 +371,7 @@ function volcFailure(action, error) {
|
|
|
312
371
|
}
|
|
313
372
|
async function fetchJson(fetcher, url, headers, lifetimeSignal, init = {}) {
|
|
314
373
|
let response;
|
|
315
|
-
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.
|
|
374
|
+
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.5", ...headers }, signal: lifetimeSignal ? AbortSignal.any([AbortSignal.timeout(10000), lifetimeSignal]) : AbortSignal.timeout(10000), redirect: "manual" }); }
|
|
316
375
|
catch { throw failure("subusage/network", "网络请求失败或超时", true); }
|
|
317
376
|
if (response.status >= 300 && response.status < 400 || response.status === 401 || response.status === 403) throw failure("subusage/auth", "登录或凭据已失效");
|
|
318
377
|
if (!response.ok) {
|
|
@@ -1072,23 +1131,37 @@ export class SubUsageService extends TypertRemoteService {
|
|
|
1072
1131
|
// 不需要知道本机配了哪些凭据,更不该拿到任何与 Key 相关的字段。
|
|
1073
1132
|
async quota(request = {}) {
|
|
1074
1133
|
if (this.disposed) throw failure("subusage/disposed", "服务已卸载");
|
|
1075
|
-
const
|
|
1134
|
+
const asked = Array.isArray(request?.providerIds) ? request.providerIds.filter(id => typeof id === "string" && id.trim()) : [];
|
|
1135
|
+
const { ids: matched, unknown } = asked.length ? resolveProviderIds(asked) : { ids: [...IDS], unknown: [] };
|
|
1136
|
+
// 一个名字都没认出来,说明调用方手里的是**另一套命名**(最常见的就是 DSH 的 llm 路由名)。
|
|
1137
|
+
// 与其只回一句「不认识」,不如把全量数据一并给出:数据先给出去,怎么判断交给调用方。
|
|
1138
|
+
// 混合输入(认出一部分 + 认不出一部分)则只补上认出的那些,不额外放大请求量。
|
|
1139
|
+
const wanted = matched.length || !unknown.length ? matched : [...IDS];
|
|
1076
1140
|
const settings = this.load();
|
|
1077
1141
|
const force = request?.force === true;
|
|
1078
1142
|
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: "读取用量失败" }))));
|
|
1143
|
+
// 认不出的名字要回一条**可读**条目而不是静默丢掉:调用方(尤其是模型)无从知道本插件用的是
|
|
1144
|
+
// 路由 id,丢掉之后它只看到空列表,会当成「本机没配任何额度」——真正的原因只是名字没对上。
|
|
1145
|
+
// 放在最前面,免得淹没在几十条数据后面。
|
|
1146
|
+
const unknownEntries = unknown.map(name => ({
|
|
1147
|
+
providerId: name, label: name, state: "error", windows: [], extras: [], coverage: "partial", freshness: "unknown",
|
|
1148
|
+
error: `不认识提供商名「${name}」${wanted.length === IDS.length && matched.length === 0 ? ",下面是全部提供商的额度" : ""}。可以传本插件的 provider id(例如 deepseek、zai-coding-cn、kimi-coding、xiaomi-token-plan-cn),或直接传厂商名(zai、kimi、mimo、minimax、commandcode、grok、codex、ark、deepseek …)。省略 providers 也会查询全部 ${IDS.length} 条。`
|
|
1149
|
+
}));
|
|
1079
1150
|
return {
|
|
1080
1151
|
updatedAt: new Date(this.now()).toISOString(),
|
|
1081
|
-
providers: entries.map(entry => ({
|
|
1152
|
+
providers: [...unknownEntries, ...entries.map(entry => ({
|
|
1082
1153
|
providerId: entry.providerId,
|
|
1083
1154
|
label: entry.label,
|
|
1084
1155
|
state: entry.state,
|
|
1085
1156
|
windows: Array.isArray(entry.windows) ? entry.windows : [],
|
|
1086
1157
|
extras: Array.isArray(entry.extras) ? entry.extras : [],
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1158
|
+
// 只输出真正有值的字段:视图里带 `undefined` 的键会被调用方的 schema 校验判成型别错误
|
|
1159
|
+
// (DSH 就会),而且「字段缺席」本来就该表示「这家没报告」,不是「报告了空」。
|
|
1160
|
+
...(typeof entry.coverage === "string" ? { coverage: entry.coverage } : {}),
|
|
1161
|
+
...(typeof entry.freshness === "string" ? { freshness: entry.freshness } : {}),
|
|
1162
|
+
...(typeof entry.lastSuccessAt === "string" ? { lastSuccessAt: entry.lastSuccessAt } : {}),
|
|
1090
1163
|
...(entry.error ? { error: entry.error } : {})
|
|
1091
|
-
}))
|
|
1164
|
+
}))]
|
|
1092
1165
|
};
|
|
1093
1166
|
}
|
|
1094
1167
|
async read(request) { return this.refresh(request && request.providerIds ? request : { providerIds: IDS, force: false }); }
|
|
@@ -1169,11 +1242,15 @@ function registerRemotes(ctx) {
|
|
|
1169
1242
|
ctx.inject(["typert"], scope => { scope.effect(() => scope.typert.register({ package: subUsageRemote.package, face: "host", schemas: [], model: { services: [], events: [], objects: [] }, invocations: subUsageRemote.descriptors })); });
|
|
1170
1243
|
}
|
|
1171
1244
|
function apply(ctx) { registerRemotes(ctx); ctx.plugin(SubUsageService, {}); registerQuotaTool(ctx); ctx.logger.info("dsh-subusage: usage service ready"); }
|
|
1172
|
-
/**
|
|
1245
|
+
/**
|
|
1246
|
+
* 工具结果的人读文本:一行一个 provider,先给窗口再给余额,异常与未配置直说。
|
|
1247
|
+
* 这段文本才是模型真正读到的东西,所以**任何情况下都不返回空**:读不到数据时也要给出
|
|
1248
|
+
* 「怎么才能读到」的下一步,否则模型只会回一句「没有额度」,而原因其实是个可以修的动作项。
|
|
1249
|
+
*/
|
|
1173
1250
|
function formatQuotaText(value) {
|
|
1174
1251
|
const providers = Array.isArray(value?.providers) ? value.providers : [];
|
|
1175
|
-
if (!providers.length) return
|
|
1176
|
-
|
|
1252
|
+
if (!providers.length) return `没有读到任何提供商。请不带 providers 参数再调用一次以查询全部 ${IDS.length} 条;若仍为空,在设置页确认「检测」开关与凭据。`;
|
|
1253
|
+
const lines = providers.map(entry => {
|
|
1177
1254
|
const head = `${entry.label || entry.providerId} [${entry.state}]`;
|
|
1178
1255
|
if (entry.state === "no-key" || entry.state === "no-cookie") return `${head}:未配置凭据`;
|
|
1179
1256
|
if (entry.state === "disabled") return `${head}:检测已关闭`;
|
|
@@ -1185,7 +1262,14 @@ function formatQuotaText(value) {
|
|
|
1185
1262
|
for (const extra of entry.extras || []) parts.push(`${extra.kind}: ${extra.value}`);
|
|
1186
1263
|
if (entry.freshness === "stale") parts.push("缓存数据");
|
|
1187
1264
|
return `${head}:${parts.length ? parts.join(";") : "暂无额度数据"}`;
|
|
1188
|
-
})
|
|
1265
|
+
});
|
|
1266
|
+
// 一条额度都没读到时,末尾补一句汇总原因:模型据此才能告诉用户下一步做什么。
|
|
1267
|
+
if (!providers.some(entry => entry.state === "ok")) {
|
|
1268
|
+
const missing = providers.filter(entry => entry.state === "no-key" || entry.state === "no-cookie").length;
|
|
1269
|
+
const off = providers.filter(entry => entry.state === "disabled").length;
|
|
1270
|
+
lines.push(`(这 ${providers.length} 条都没读到额度:未配置凭据 ${missing} 条、检测已关闭 ${off} 条;其余为读取失败。凭据与开关都在 dsh-subusage 设置页,或参考 docs/credentials.md。)`);
|
|
1271
|
+
}
|
|
1272
|
+
return lines.join("\n");
|
|
1189
1273
|
}
|
|
1190
1274
|
/**
|
|
1191
1275
|
* 注册 subusage_quota 工具:模型可以直接问「额度还剩多少」,不必自己逐个厂商请求。
|
|
@@ -1195,9 +1279,9 @@ function registerQuotaTool(ctx) {
|
|
|
1195
1279
|
ctx.inject(["tools"], toolCtx => {
|
|
1196
1280
|
toolCtx.tools.register(defineTool({
|
|
1197
1281
|
name: "subusage_quota",
|
|
1198
|
-
description: "Read the remaining quota and balance of the AI subscriptions configured on this machine (DeepSeek, Z.ai, Kimi, MiMo, OpenCode Go, Command Code, Codex, SuperGrok, MiniMax, Volcengine Ark, OpenRouter, SiliconFlow, ...). Read-only: it never returns credentials and never changes settings. Results may come from a cache up to 60 seconds old; pass refresh=true only when the user explicitly needs up-to-date numbers.",
|
|
1282
|
+
description: "Read the remaining quota and balance of the AI subscriptions configured on this machine (DeepSeek, Z.ai, Kimi, MiMo, OpenCode Go, Command Code, Codex, SuperGrok, MiniMax, Volcengine Ark, OpenRouter, SiliconFlow, ...). Call it with no arguments to read every provider — that is the usual call. `providers` is an optional filter that accepts both this plugin's provider ids and plain vendor names (\"zai\", \"kimi\", \"mimo\", \"minimax\", \"commandcode\", \"grok\", \"codex\", \"ark\", \"deepseek\"); one name can match several routes and every match is returned with its own state, and a name that matches nothing still returns the full data plus an explanation instead of an empty result. Read-only: it never returns credentials and never changes settings. Each entry has state (ok | no-key | no-cookie | error | disabled), windows (percent is the USED percentage; absent resetsAt means the provider does not report one) and extras (plan name, balance). Results may come from a cache up to 60 seconds old; pass refresh=true only when the user explicitly needs up-to-date numbers.",
|
|
1199
1283
|
parameters: {
|
|
1200
|
-
providers: { type: "array", items: { type: "string" }, description: "Optional provider
|
|
1284
|
+
providers: { type: "array", items: { type: "string" }, description: "Optional filter. Either a provider id this tool reports back (\"deepseek\", \"zai-coding-cn\", \"kimi-coding\") or a plain vendor name (\"zai\", \"kimi\", \"mimo\", \"minimax\", \"commandcode\", \"grok\", \"codex\", \"ark\"). Case and separators do not matter; an unmatched name is reported with the full data rather than dropped." },
|
|
1201
1285
|
refresh: { type: "boolean", description: "Force a fresh fetch instead of the up-to-60s cache. Use only when the user asks for the latest numbers." }
|
|
1202
1286
|
},
|
|
1203
1287
|
output: {
|
|
@@ -1214,11 +1298,39 @@ function registerQuotaTool(ctx) {
|
|
|
1214
1298
|
type: "object",
|
|
1215
1299
|
additionalProperties: false,
|
|
1216
1300
|
properties: {
|
|
1217
|
-
providerId: { type: "string", required: true },
|
|
1301
|
+
providerId: { type: "string", required: true, description: "Provider id (the route id), or the unmatched name that was passed in." },
|
|
1218
1302
|
label: { type: "string", required: true },
|
|
1219
1303
|
state: { type: "string", required: true, description: "ok | no-key | no-cookie | error | disabled" },
|
|
1220
|
-
windows: {
|
|
1221
|
-
|
|
1304
|
+
windows: {
|
|
1305
|
+
type: "array", required: true, description: "Quota windows; percent is the USED percentage (0-100).",
|
|
1306
|
+
items: {
|
|
1307
|
+
// 这里刻意用 additionalProperties: true:窗口形态随厂商变化(有的给绝对量、有的给额度线标注),
|
|
1308
|
+
// 收紧成白名单只会让新增字段在运行时被判违规,而这里声明的字段仍照旧受型别约束。
|
|
1309
|
+
type: "object", additionalProperties: true,
|
|
1310
|
+
properties: {
|
|
1311
|
+
kind: { type: "string", required: true, description: "Window length: 5h | week | month | day | 7d | rolling | sub | period." },
|
|
1312
|
+
percent: { type: "number", required: true, description: "USED percentage 0-100; remaining = 100 - percent." },
|
|
1313
|
+
status: { type: "string", required: true, description: "ok | rate-limited" },
|
|
1314
|
+
resetsAt: { type: "string", description: "ISO reset time; absent when the provider does not report one." },
|
|
1315
|
+
groupLabel: { type: "string", description: "Credit line, when the provider has more than one (e.g. Volcengine AFP)." },
|
|
1316
|
+
cascade: { type: "boolean", description: "true when this window is only blocked by a wider exhausted window." },
|
|
1317
|
+
detail: {
|
|
1318
|
+
type: "object", additionalProperties: true, description: "Absolute amounts and scope notes, when the provider reports them.",
|
|
1319
|
+
properties: {
|
|
1320
|
+
used: { type: "number" }, limit: { type: "number" }, remaining: { type: "number" }, unit: { type: "string" },
|
|
1321
|
+
limitSource: { type: "string" }, note: { type: "string" }, blockedBy: { type: "array", items: { type: "string" } }
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
},
|
|
1327
|
+
extras: {
|
|
1328
|
+
type: "array", required: true, description: "Non-window facts: plan name, balance, seat id, ... (kind/value).",
|
|
1329
|
+
items: { type: "object", additionalProperties: false, properties: {
|
|
1330
|
+
kind: { type: "string", required: true, description: "plan | balance | spend | account | monthly-balance | ..." },
|
|
1331
|
+
value: { type: "string", required: true }
|
|
1332
|
+
} }
|
|
1333
|
+
},
|
|
1222
1334
|
coverage: { type: "string", description: "complete | partial" },
|
|
1223
1335
|
freshness: { type: "string", description: "fresh | stale" },
|
|
1224
1336
|
lastSuccessAt: { type: "string" },
|
|
@@ -1233,7 +1345,7 @@ function registerQuotaTool(ctx) {
|
|
|
1233
1345
|
execute: async (args, exec) => {
|
|
1234
1346
|
exec?.signal?.throwIfAborted?.();
|
|
1235
1347
|
const service = toolCtx.get("subUsage");
|
|
1236
|
-
if (!service || typeof service.quota !== "function") throw new Error("dsh-subusage
|
|
1348
|
+
if (!service || typeof service.quota !== "function") throw new Error("dsh-subusage 服务尚未就绪:Host 端可能还在启动或刚更新完还没重启。请稍后重试;持续失败就重启 DSH。");
|
|
1237
1349
|
return await service.quota({ providerIds: args?.providers, force: args?.refresh === true });
|
|
1238
1350
|
}
|
|
1239
1351
|
}));
|