@duke-dsh-plugins/dsh-token-stats 1.5.4 → 1.6.0
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/client.js +31 -10
- package/index.js +242 -54
- package/package.json +7 -5
package/client.js
CHANGED
|
@@ -18,8 +18,16 @@
|
|
|
18
18
|
* (`ctx.remote.tokenStats.getStats()/getQuota()`), published by the Host half
|
|
19
19
|
* in `index.js`.
|
|
20
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* This package's client-module id. It is both the `__ModuleLoader__.load` id and
|
|
23
|
+
* the HMR style owner (`data-plugin`) — client-modules keys style ownership off the
|
|
24
|
+
* registered id, so the two must stay the same string. See the `claimStyles` note
|
|
25
|
+
* where the stylesheet is created.
|
|
26
|
+
*/
|
|
27
|
+
const CLIENT_BUNDLE_ID = "@duke-dsh-plugins/dsh-token-stats";
|
|
28
|
+
|
|
21
29
|
window.__ModuleLoader__.load({
|
|
22
|
-
id:
|
|
30
|
+
id: CLIENT_BUNDLE_ID,
|
|
23
31
|
factory: (require) => {
|
|
24
32
|
var module = { exports: {} };
|
|
25
33
|
var exports = module.exports;
|
|
@@ -260,6 +268,13 @@ window.__ModuleLoader__.load({
|
|
|
260
268
|
try {
|
|
261
269
|
styleTag = document.createElement("style");
|
|
262
270
|
styleTag.textContent = CSS;
|
|
271
|
+
// dsh 0.2.0 client-modules runs `claimStyles(ownerId)` during
|
|
272
|
+
// materialization, which claims every *untagged* <style> in the document
|
|
273
|
+
// for the materializing package. `apply()` is async and appends this tag
|
|
274
|
+
// after that sweep, so without an explicit owner it would stay untagged
|
|
275
|
+
// and a later package could adopt it — and `removeOwnedStyles(thatId)`
|
|
276
|
+
// would then strip our stylesheet on its HMR invalidation. Claim it now.
|
|
277
|
+
styleTag.setAttribute("data-plugin", CLIENT_BUNDLE_ID);
|
|
263
278
|
document.head.appendChild(styleTag);
|
|
264
279
|
ctx.effect(() => () => styleTag && styleTag.remove());
|
|
265
280
|
} catch (e) {
|
|
@@ -667,16 +682,16 @@ window.__ModuleLoader__.load({
|
|
|
667
682
|
if (lower.includes("mimo") || lower.includes("xiaomi")) return "mimo";
|
|
668
683
|
return null;
|
|
669
684
|
}
|
|
670
|
-
// 凭据设置行写入的 ref — 与 host QUOTA_PROVIDERS.
|
|
671
|
-
// mimo 写
|
|
672
|
-
// API Key
|
|
685
|
+
// 凭据设置行写入的 ref — 与 host QUOTA_PROVIDERS.apiKeyRefs 首选项对齐。
|
|
686
|
+
// mimo 写 XIAOMI_MIMO_API_KEY(v1.6.0 起推荐:Bearer /v1/user/balance
|
|
687
|
+
// 与路由 LLM API Key 同源,稳定不易过期;Cookie 仍可作为兜底探测)。
|
|
673
688
|
const QUOTA_CRED_REFS = {
|
|
674
689
|
minimax: "MINIMAX_CN_API_KEY",
|
|
675
690
|
deepseek: "DEEPSEEK_API_KEY",
|
|
676
691
|
kimi: "KIMI_CODING_API_KEY",
|
|
677
692
|
openrouter: "OPENROUTER_API_KEY",
|
|
678
693
|
zhipu: "ZAI_CODING_CN_API_KEY",
|
|
679
|
-
mimo: "
|
|
694
|
+
mimo: "XIAOMI_MIMO_API_KEY",
|
|
680
695
|
};
|
|
681
696
|
const QUOTA_PROVIDER_ORDER = ["minimax", "deepseek", "kimi", "openrouter", "zhipu", "mimo"];
|
|
682
697
|
|
|
@@ -708,8 +723,12 @@ window.__ModuleLoader__.load({
|
|
|
708
723
|
return [labelEl, React.createElement("span", { key: "b", style: strong }, txt)];
|
|
709
724
|
}
|
|
710
725
|
if (provider === "mimo") {
|
|
711
|
-
// MiMo Token Plan
|
|
726
|
+
// MiMo:PAYG 显示 CNY 余额,Token Plan 显示 套餐% + 本月%。
|
|
712
727
|
const parts = [labelEl];
|
|
728
|
+
if (typeof d.balanceText === "string" && d.balanceText) {
|
|
729
|
+
parts.push(React.createElement("span", { key: "bal", style: strong }, d.balanceText));
|
|
730
|
+
return parts;
|
|
731
|
+
}
|
|
713
732
|
if (typeof d.fiveHrPct === "number") {
|
|
714
733
|
parts.push(React.createElement("span", { key: "plan", style: strong }, "套餐 " + d.fiveHrPct + "%"));
|
|
715
734
|
}
|
|
@@ -1022,8 +1041,8 @@ window.__ModuleLoader__.load({
|
|
|
1022
1041
|
if (!ref || !trimmed) return;
|
|
1023
1042
|
setCredSaving((s) => ({ ...s, [p]: true }));
|
|
1024
1043
|
try {
|
|
1025
|
-
// 官方 credentials 命名空间(settings-models
|
|
1026
|
-
// inject
|
|
1044
|
+
// 官方 credentials 命名空间(settings-models 同款用法;依赖已在
|
|
1045
|
+
// exports.inject 声明——cordis 强制 inject 才能取服务属性,v1.5.5)。
|
|
1027
1046
|
const ns = (ctx.remote && ctx.remote.credentials) || ctx.get("remote.credentials");
|
|
1028
1047
|
if (!ns || typeof ns.set !== "function") throw new Error("credentials Remote 不可用");
|
|
1029
1048
|
const r = await ns.set(ref, trimmed);
|
|
@@ -1089,7 +1108,9 @@ window.__ModuleLoader__.load({
|
|
|
1089
1108
|
type: "password",
|
|
1090
1109
|
className: "ts-cred-input",
|
|
1091
1110
|
placeholder: p === "mimo"
|
|
1092
|
-
? (isAuthFailed
|
|
1111
|
+
? (isAuthFailed
|
|
1112
|
+
? "凭据失效:粘贴新的 API Key(推荐)或 Cookie"
|
|
1113
|
+
: "推荐粘贴 API Key (tp-...),与 LLM 路由同源不易过期;Cookie 也能用")
|
|
1093
1114
|
: "粘贴 API Key",
|
|
1094
1115
|
value: credDrafts[p] || "",
|
|
1095
1116
|
onChange: (e) => setCredDrafts((d) => ({ ...d, [p]: e.target.value })),
|
|
@@ -1320,7 +1341,7 @@ window.__ModuleLoader__.load({
|
|
|
1320
1341
|
}
|
|
1321
1342
|
|
|
1322
1343
|
exports.apply = apply;
|
|
1323
|
-
exports.inject = ["slots", "remote"];
|
|
1344
|
+
exports.inject = ["slots", "remote", "remote.credentials"];
|
|
1324
1345
|
return module.exports;
|
|
1325
1346
|
},
|
|
1326
1347
|
});
|
package/index.js
CHANGED
|
@@ -142,33 +142,40 @@ const QUOTA_PROVIDERS = {
|
|
|
142
142
|
// 智谱特殊: Authorization 不加 "Bearer " 前缀 (来自 Musage zhipu.rs 注释)
|
|
143
143
|
authStyle: "raw",
|
|
144
144
|
},
|
|
145
|
-
// Xiaomi MiMo Token Plan —
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
145
|
+
// Xiaomi MiMo Token Plan — 多端点探测 (v1.6.0 起):
|
|
146
|
+
// 可靠路径是用户给 LLM 路由配的 API Key(tp-..., Bearer 鉴权)走
|
|
147
|
+
// /v1/user/balance 或 /api/v1/tokenPlan/usage (Bearer 端点)。
|
|
148
|
+
// dashboard Cookie 路径(platform.xiaomimimo.com)作为兜底——Cookie 容易
|
|
149
|
+
// 过期,但仍有用户在用。Endpoint 链见 _fetchMimoQuota。
|
|
150
|
+
// 凭据 ref 命名沿用设置页派生规则:
|
|
151
|
+
// - apiKey 走 Bearer: XIAOMI_MIMO_API_KEY / XIAOMI_TOKEN_PLAN_CN_API_KEY / MIMO_API_KEY
|
|
152
|
+
// - cookie 走 Cookie: XIAOMI_MIMO_COOKIE / XIAOMI_TOKEN_PLAN_CN_COOKIE / MIMO_COOKIE
|
|
153
|
+
// 凭据设置 UI 默认写入 XIAOMI_MIMO_API_KEY (v1.6.0 起推荐路径);
|
|
154
|
+
// 已有 Cookie 的旧用户不受影响(探测链自动用 Cookie 兜底)。
|
|
155
155
|
mimo: {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
XIAOMI_MIMO_API_KEY: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
|
|
161
|
-
XIAOMI_MIMO_COOKIE: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
|
|
162
|
-
MIMO_API_KEY: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
|
|
163
|
-
MIMO_COOKIE: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
|
|
164
|
-
},
|
|
156
|
+
// Bearer 鉴权 ref 候选 (DSH 模型设置里配的 LLM 路由 API Key / tp-...)
|
|
157
|
+
apiKeyRefs: ["XIAOMI_MIMO_API_KEY", "XIAOMI_TOKEN_PLAN_CN_API_KEY", "MIMO_API_KEY"],
|
|
158
|
+
// Cookie 鉴权 ref 候选 (浏览器 dashboard session, Bearer 失败时的兜底)
|
|
159
|
+
cookieRefs: ["XIAOMI_MIMO_COOKIE", "XIAOMI_TOKEN_PLAN_CN_COOKIE", "MIMO_COOKIE"],
|
|
165
160
|
parse: parseMimoResponse,
|
|
166
161
|
},
|
|
167
162
|
};
|
|
168
163
|
|
|
169
|
-
/** Xiaomi MiMo
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
/** Xiaomi MiMo 多端点常量 — usage 走 Bearer/Cookie 双探, balance 走 Bearer (PAYG + Token Plan SGP)。
|
|
165
|
+
* 协议来源:https://docs.rs/quotas/latest/src/quotas/providers/mimo.rs.html (clankercode/quotas, MIT)
|
|
166
|
+
* 与 https://github.com/steipete/CodexBar/blob/main/docs/mimo.md (CodexBar, MIT)。
|
|
167
|
+
* Bearer 鉴权用 LLM 路由 API Key (tp-...) 即可,Cookie 是 dashboard session 兜底。 */
|
|
168
|
+
const MIMO_PLATFORM = "https://platform.xiaomimimo.com";
|
|
169
|
+
const MIMO_PAYG_BASE = "https://api.xiaomimimo.com";
|
|
170
|
+
const MIMO_TOKEN_PLAN_SGP = "https://token-plan-sgp.xiaomimimo.com";
|
|
171
|
+
/** /api/v1/tokenPlan/usage + /detail(Bearer 或 Cookie,套餐 % + 重置时间)。 */
|
|
172
|
+
const MIMO_USAGE_URL = `${MIMO_PLATFORM}/api/v1/tokenPlan/usage`;
|
|
173
|
+
const MIMO_DETAIL_URL = `${MIMO_PLATFORM}/api/v1/tokenPlan/detail`;
|
|
174
|
+
const MIMO_TOKEN_PLAN_SGP_USAGE_URL = `${MIMO_TOKEN_PLAN_SGP}/api/v1/tokenPlan/usage`;
|
|
175
|
+
const MIMO_TOKEN_PLAN_SGP_DETAIL_URL = `${MIMO_TOKEN_PLAN_SGP}/api/v1/tokenPlan/detail`;
|
|
176
|
+
/** /v1/user/balance(仅 Bearer:Token Plan token_balance/token_limit 或 PAYG CNY balance)。 */
|
|
177
|
+
const MIMO_PAYG_BALANCE_URL = `${MIMO_PAYG_BASE}/v1/user/balance`;
|
|
178
|
+
const MIMO_TOKEN_PLAN_SGP_BALANCE_URL = `${MIMO_TOKEN_PLAN_SGP}/v1/user/balance`;
|
|
172
179
|
|
|
173
180
|
function quotaBackoffMs(streak) {
|
|
174
181
|
if (streak <= 0) return 0;
|
|
@@ -466,7 +473,7 @@ function parseMimoResponse(body, detailBody) {
|
|
|
466
473
|
ok: false,
|
|
467
474
|
provider: "mimo",
|
|
468
475
|
kind: "auth_failed",
|
|
469
|
-
message: "MiMo 凭据已失效(Cookie
|
|
476
|
+
message: "MiMo 凭据已失效(API Key 或 Cookie 过期),请到 platform.xiaomimimo.com 续费或重新生成",
|
|
470
477
|
};
|
|
471
478
|
}
|
|
472
479
|
return {
|
|
@@ -549,6 +556,101 @@ function parseMimoResponse(body, detailBody) {
|
|
|
549
556
|
};
|
|
550
557
|
}
|
|
551
558
|
|
|
559
|
+
// ----- xiaomi mimo /v1/user/balance parser (Bearer-only, 优先路径) -----
|
|
560
|
+
// PAYG 账号:
|
|
561
|
+
// { data: { balance: "12.50", charge_balance: "10.00",
|
|
562
|
+
// granted_balance: "2.50", plan: "PAYG" } }
|
|
563
|
+
// Token Plan 账号:
|
|
564
|
+
// { data: { token_balance: 800000, token_limit: 1000000,
|
|
565
|
+
// plan_name: "Pro" } }
|
|
566
|
+
// balance 可能是字符串或数字(来自 Rust 端 serde 默认行为)。
|
|
567
|
+
// 数据本身已含 plan 字段,无需再拉 detail。
|
|
568
|
+
|
|
569
|
+
function parseMimoBalanceResponse(body /*, detailBody — unused, balance 端点不含 detail */) {
|
|
570
|
+
let json;
|
|
571
|
+
try {
|
|
572
|
+
json = typeof body === "string" ? JSON.parse(body) : body;
|
|
573
|
+
} catch {
|
|
574
|
+
return { ok: false, provider: "mimo", kind: "parse", message: "MiMo balance 响应 JSON 解析失败" };
|
|
575
|
+
}
|
|
576
|
+
if (!json || typeof json !== "object") {
|
|
577
|
+
return { ok: false, provider: "mimo", kind: "parse", message: "MiMo balance 响应不是对象" };
|
|
578
|
+
}
|
|
579
|
+
if (typeof json.code === "number" && json.code !== 0) {
|
|
580
|
+
if (json.code >= 40100 && json.code < 40200) {
|
|
581
|
+
return {
|
|
582
|
+
ok: false,
|
|
583
|
+
provider: "mimo",
|
|
584
|
+
kind: "auth_failed",
|
|
585
|
+
message: "MiMo API Key 已失效,请到 platform.xiaomimimo.com 重新生成",
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
ok: false,
|
|
590
|
+
provider: "mimo",
|
|
591
|
+
kind: "server_error",
|
|
592
|
+
message: "MiMo 业务错误 code=" + json.code + " · " + (json.message || ""),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
const data = (json.data && typeof json.data === "object") ? json.data : json;
|
|
596
|
+
|
|
597
|
+
// Token Plan: token_balance + token_limit 存在 → 套餐已用 %
|
|
598
|
+
const tokBal = parseMimoNumberField(data.token_balance);
|
|
599
|
+
const tokLim = parseMimoNumberField(data.token_limit);
|
|
600
|
+
if (tokLim !== null && tokLim > 0 && tokBal !== null) {
|
|
601
|
+
const used = Math.max(0, tokLim - tokBal);
|
|
602
|
+
const pct = Math.round((used / tokLim) * 100);
|
|
603
|
+
return {
|
|
604
|
+
ok: true,
|
|
605
|
+
provider: "mimo",
|
|
606
|
+
display: {
|
|
607
|
+
fiveHrPct: Math.max(0, Math.min(100, pct)),
|
|
608
|
+
weeklyPct: null,
|
|
609
|
+
fiveHrResetsIn: null,
|
|
610
|
+
weeklyResetsIn: null,
|
|
611
|
+
balanceText: null,
|
|
612
|
+
balanceUsd: null,
|
|
613
|
+
currency: null,
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// PAYG: balance 字段(CNY)→ 余额文本
|
|
619
|
+
const bal = parseMimoNumberField(data.balance);
|
|
620
|
+
if (bal !== null && bal > 0) {
|
|
621
|
+
return {
|
|
622
|
+
ok: true,
|
|
623
|
+
provider: "mimo",
|
|
624
|
+
currency: "CNY",
|
|
625
|
+
display: {
|
|
626
|
+
balanceText: formatBalance(bal, "CNY"),
|
|
627
|
+
balanceUsd: null,
|
|
628
|
+
fiveHrPct: null,
|
|
629
|
+
weeklyPct: null,
|
|
630
|
+
fiveHrResetsIn: null,
|
|
631
|
+
weeklyResetsIn: null,
|
|
632
|
+
currency: "CNY",
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
ok: false,
|
|
639
|
+
provider: "mimo",
|
|
640
|
+
kind: "parse",
|
|
641
|
+
message: "MiMo balance 响应缺少 token_balance/token_limit 或 balance 字段",
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function parseMimoNumberField(v) {
|
|
646
|
+
if (typeof v === "number" && isFinite(v)) return v;
|
|
647
|
+
if (typeof v === "string") {
|
|
648
|
+
const n = parseFloat(v);
|
|
649
|
+
return isFinite(n) ? n : null;
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
|
|
552
654
|
function formatResetsIn(resetsAtMs) {
|
|
553
655
|
if (typeof resetsAtMs !== "number" || !resetsAtMs) return "";
|
|
554
656
|
const ms = resetsAtMs - Date.now();
|
|
@@ -1035,30 +1137,63 @@ export class TokenStatsService extends TypertRemoteService {
|
|
|
1035
1137
|
* each provider's shipped default `apiKeyEnv`; without the credentials
|
|
1036
1138
|
* service (headless deployment) the process environment is consulted.
|
|
1037
1139
|
*/
|
|
1140
|
+
/**
|
|
1141
|
+
* Resolve credentials for one provider through the DSH credentials seam.
|
|
1142
|
+
* Ref candidates follow the settings page's `<ROUTE>_API_KEY` derivation and
|
|
1143
|
+
* each provider's shipped default `apiKeyEnv`; without the credentials
|
|
1144
|
+
* service (headless deployment) the process environment is consulted.
|
|
1145
|
+
*
|
|
1146
|
+
* v1.6.0 起,mimo 同时持有 API Key (Bearer) + Cookie (dashboard session) 两类
|
|
1147
|
+
* 凭据;其他 provider 只用 API Key。返回值总是
|
|
1148
|
+
* { apiKey, apiKeyRef, cookie, cookieRef },
|
|
1149
|
+
* 未配置那一路为 null。`_quotaActiveRef[provider + ":api" | ":cookie"]` 按
|
|
1150
|
+
* provider+kind 缓存上次命中的 ref(保持粘性,但不互锁)。
|
|
1151
|
+
*/
|
|
1038
1152
|
async _quotaApiKey(provider) {
|
|
1039
1153
|
const cfg = QUOTA_PROVIDERS[provider];
|
|
1040
|
-
if (!cfg) return {
|
|
1154
|
+
if (!cfg) return { apiKey: null, apiKeyRef: null, cookie: null, cookieRef: null };
|
|
1041
1155
|
const credentials = this.ctx.get("credentials");
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1156
|
+
// 兼容新旧两种配置形态:新的是 {apiKeyRefs, cookieRefs},旧的是单数组 refs。
|
|
1157
|
+
const apiKeyRefList = cfg.apiKeyRefs || cfg.refs || [];
|
|
1158
|
+
const cookieRefList = cfg.cookieRefs || [];
|
|
1159
|
+
|
|
1160
|
+
const resolveOne = async (ref) => {
|
|
1046
1161
|
try {
|
|
1047
1162
|
if (credentials && typeof credentials.resolve === "function") {
|
|
1048
1163
|
const hit = await credentials.resolve(ref);
|
|
1049
|
-
if (hit && hit.value)
|
|
1050
|
-
this._quotaActiveRef[provider] = ref;
|
|
1051
|
-
return { ref, key: hit.value };
|
|
1052
|
-
}
|
|
1164
|
+
if (hit && hit.value) return hit.value;
|
|
1053
1165
|
} else if (typeof process !== "undefined" && process.env && process.env[ref]) {
|
|
1054
|
-
|
|
1055
|
-
return { ref, key: process.env[ref] };
|
|
1166
|
+
return process.env[ref];
|
|
1056
1167
|
}
|
|
1057
1168
|
} catch (e) {
|
|
1058
1169
|
/* try the next ref */
|
|
1059
1170
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1171
|
+
return null;
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
const resolveList = async (refList, activeKey) => {
|
|
1175
|
+
const active = this._quotaActiveRef[activeKey];
|
|
1176
|
+
const candidates = active
|
|
1177
|
+
? [active, ...refList.filter((r) => r !== active)]
|
|
1178
|
+
: refList;
|
|
1179
|
+
for (const ref of candidates) {
|
|
1180
|
+
const v = await resolveOne(ref);
|
|
1181
|
+
if (v) {
|
|
1182
|
+
this._quotaActiveRef[activeKey] = ref;
|
|
1183
|
+
return { ref, value: v };
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return { ref: null, value: null };
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
const api = await resolveList(apiKeyRefList, provider + ":api");
|
|
1190
|
+
const ck = await resolveList(cookieRefList, provider + ":cookie");
|
|
1191
|
+
return {
|
|
1192
|
+
apiKey: api.value,
|
|
1193
|
+
apiKeyRef: api.ref,
|
|
1194
|
+
cookie: ck.value,
|
|
1195
|
+
cookieRef: ck.ref,
|
|
1196
|
+
};
|
|
1062
1197
|
}
|
|
1063
1198
|
|
|
1064
1199
|
async _resolveCurl() {
|
|
@@ -1162,8 +1297,9 @@ export class TokenStatsService extends TypertRemoteService {
|
|
|
1162
1297
|
if (!cfg) {
|
|
1163
1298
|
return { ok: false, provider, kind: "other", message: "未知 provider: " + provider };
|
|
1164
1299
|
}
|
|
1165
|
-
const {
|
|
1166
|
-
if (
|
|
1300
|
+
const { apiKey, apiKeyRef, cookie } = await this._quotaApiKey(provider);
|
|
1301
|
+
if (provider === "mimo") return this._fetchMimoQuota(apiKey, cookie);
|
|
1302
|
+
if (!apiKey) {
|
|
1167
1303
|
return {
|
|
1168
1304
|
ok: false,
|
|
1169
1305
|
provider,
|
|
@@ -1171,29 +1307,81 @@ export class TokenStatsService extends TypertRemoteService {
|
|
|
1171
1307
|
message: "未配置 " + provider + " API Key (在 DSH 模型设置里配置对应 provider)",
|
|
1172
1308
|
};
|
|
1173
1309
|
}
|
|
1174
|
-
|
|
1175
|
-
const url = cfg.urls[
|
|
1176
|
-
const raw = await this._httpGet(url,
|
|
1310
|
+
const refList = cfg.apiKeyRefs || cfg.refs || [];
|
|
1311
|
+
const url = cfg.urls[apiKeyRef] || cfg.urls[refList[0]];
|
|
1312
|
+
const raw = await this._httpGet(url, apiKey, cfg.authStyle);
|
|
1177
1313
|
if (!raw.ok) return { ...raw, provider };
|
|
1178
1314
|
return cfg.parse(raw.body);
|
|
1179
1315
|
}
|
|
1180
1316
|
|
|
1181
1317
|
/**
|
|
1182
|
-
* Xiaomi MiMo
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1318
|
+
* Xiaomi MiMo 多端点探测链 (v1.6.0 起,对齐 quotas crate mimo.rs + CodexBar):
|
|
1319
|
+
* Bearer (API Key, 稳定优先):
|
|
1320
|
+
* 1. token-plan-sgp/v1/user/balance (Token Plan token_balance/token_limit)
|
|
1321
|
+
* 2. api.xiaomimimo.com/v1/user/balance (PAYG CNY balance)
|
|
1322
|
+
* 3. token-plan-sgp/api/v1/tokenPlan/usage (Token Plan 套餐 % + 月度 %)
|
|
1323
|
+
* 4. platform/api/v1/tokenPlan/usage (Bearer 平台端点)
|
|
1324
|
+
* Cookie (dashboard session, 兜底):
|
|
1325
|
+
* 5. platform/api/v1/tokenPlan/usage (经典 dashboard 路径)
|
|
1326
|
+
* 6. token-plan-sgp/api/v1/tokenPlan/usage (SGP dashboard 路径)
|
|
1327
|
+
* 每个端点 HTTP 200 后用对应 parser 解析;业务 401xx → auth_failed, 进入下一
|
|
1328
|
+
* 个端点;parse 失败(非凭据)→ 直接返回。usage 端点成功后再 best-effort 拉
|
|
1329
|
+
* detail(同鉴权方式)补 reset 时间。balance 端点本身包含 plan 字段,不再
|
|
1330
|
+
* 拉 detail。
|
|
1186
1331
|
*/
|
|
1187
|
-
async _fetchMimoQuota(
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1332
|
+
async _fetchMimoQuota(apiKey, cookie) {
|
|
1333
|
+
if (!apiKey && !cookie) {
|
|
1334
|
+
return {
|
|
1335
|
+
ok: false,
|
|
1336
|
+
provider: "mimo",
|
|
1337
|
+
kind: "unconfigured",
|
|
1338
|
+
message: "未配置 MiMo API Key 或 Cookie(在 DSH 模型设置里配置对应 provider)",
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// 探测链 — Bearer 在前, Cookie 在后。`detailUrl === null` 表示该端点
|
|
1343
|
+
// 不需要 detail(balance 端点本身已含 plan 信息)。
|
|
1344
|
+
const chain = [];
|
|
1345
|
+
if (apiKey) {
|
|
1346
|
+
chain.push({ auth: "bearer", url: MIMO_TOKEN_PLAN_SGP_BALANCE_URL, parser: parseMimoBalanceResponse, detailUrl: null, tag: "balance-sgp" });
|
|
1347
|
+
chain.push({ auth: "bearer", url: MIMO_PAYG_BALANCE_URL, parser: parseMimoBalanceResponse, detailUrl: null, tag: "balance-payg" });
|
|
1348
|
+
chain.push({ auth: "bearer", url: MIMO_TOKEN_PLAN_SGP_USAGE_URL, parser: parseMimoResponse, detailUrl: MIMO_TOKEN_PLAN_SGP_DETAIL_URL, tag: "usage-sgp" });
|
|
1349
|
+
chain.push({ auth: "bearer", url: MIMO_USAGE_URL, parser: parseMimoResponse, detailUrl: MIMO_DETAIL_URL, tag: "usage-platform" });
|
|
1350
|
+
}
|
|
1351
|
+
if (cookie) {
|
|
1352
|
+
chain.push({ auth: "cookie", url: MIMO_USAGE_URL, parser: parseMimoResponse, detailUrl: MIMO_DETAIL_URL, tag: "usage-platform" });
|
|
1353
|
+
chain.push({ auth: "cookie", url: MIMO_TOKEN_PLAN_SGP_USAGE_URL, parser: parseMimoResponse, detailUrl: MIMO_TOKEN_PLAN_SGP_DETAIL_URL, tag: "usage-sgp" });
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
let lastError = null;
|
|
1357
|
+
for (const step of chain) {
|
|
1358
|
+
const cred = step.auth === "cookie" ? cookie : apiKey;
|
|
1359
|
+
const raw = await this._httpGet(step.url, cred, step.auth === "cookie" ? "cookie" : undefined);
|
|
1360
|
+
if (!raw.ok) {
|
|
1361
|
+
lastError = { ...raw, provider: "mimo" };
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
// usage 端点: 先拉 detail (expired 标记 + reset 时间). detail 失败不影响
|
|
1365
|
+
// usage 解析, 留空 detail 让 parser 走"无 detail"分支. balance 端点
|
|
1366
|
+
// (detailUrl === null) 本身已含 plan, 不再请求 detail.
|
|
1367
|
+
let detailBody = null;
|
|
1368
|
+
if (step.detailUrl) {
|
|
1369
|
+
const d = await this._httpGet(step.detailUrl, cred, step.auth === "cookie" ? "cookie" : undefined);
|
|
1370
|
+
if (d.ok) detailBody = d.body;
|
|
1371
|
+
}
|
|
1372
|
+
const parsed = step.parser(raw.body, detailBody);
|
|
1373
|
+
if (!parsed.ok) {
|
|
1374
|
+
if (parsed.kind === "auth_failed") {
|
|
1375
|
+
// 凭据失效 → 进入下一端点.
|
|
1376
|
+
lastError = { ...parsed, provider: "mimo" };
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
// 其他失败(parse / server_error):直接返回,不必继续探测。
|
|
1380
|
+
return parsed;
|
|
1381
|
+
}
|
|
1382
|
+
return parsed;
|
|
1193
1383
|
}
|
|
1194
|
-
|
|
1195
|
-
const detail = await this._httpGet(MIMO_DETAIL_URL, key, cookie ? "cookie" : undefined);
|
|
1196
|
-
return parseMimoResponse(raw.body, detail && detail.ok ? detail.body : null);
|
|
1384
|
+
return lastError || { ok: false, provider: "mimo", kind: "other", message: "MiMo 所有端点探测均失败" };
|
|
1197
1385
|
}
|
|
1198
1386
|
|
|
1199
1387
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duke-dsh-plugins/dsh-token-stats",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Token usage statistics for DeepSeek Harness (7/30-day per-model charts + yearly heatmap) plus an inline coding-plan balance/quota readout in the composer (minimax/deepseek/kimi/openrouter/zhipu).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"engines": {
|
|
30
|
-
"dsh": ">=0.1.4-rc.2 <0.
|
|
30
|
+
"dsh": ">=0.1.4-rc.2 <0.3.0"
|
|
31
31
|
},
|
|
32
32
|
"dsh": {
|
|
33
33
|
"bundle": {
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
42
|
-
"@deepseek-ai/dsh": ">=0.1.4-rc.2 <0.
|
|
43
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7"
|
|
42
|
+
"@deepseek-ai/dsh": ">=0.1.4-rc.2 <0.3.0",
|
|
43
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7 || ^0.2.0-rc.1"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"zod": "^4.4.3"
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
"smoke": "node scripts/smoke-store.mjs",
|
|
51
51
|
"smoke:quota": "node scripts/smoke-quota.mjs",
|
|
52
52
|
"smoke:robust": "node scripts/smoke-robust-host.mjs && node scripts/smoke-robust-client.mjs",
|
|
53
|
-
"smoke:typert": "node scripts/smoke-typert.mjs"
|
|
53
|
+
"smoke:typert": "node scripts/smoke-typert.mjs",
|
|
54
|
+
"smoke:compat": "node scripts/smoke-compat.mjs",
|
|
55
|
+
"e2e": "npm run check && npm run smoke && npm run smoke:quota && npm run smoke:robust && npm run smoke:typert && npm run smoke:compat && node scripts/repro-client-mount.mjs && node scripts/repro-strict-dispatch.mjs && node scripts/repro-profile-boot.mjs"
|
|
54
56
|
}
|
|
55
57
|
}
|