@duke-dsh-plugins/dsh-token-stats 1.6.0 → 1.7.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/README.md +5 -3
- package/client.js +65 -16
- package/index.js +278 -158
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -61,11 +61,13 @@ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-token-sta
|
|
|
61
61
|
4. **会话输入框工具行**(model 选择器左侧)内联显示当前 provider 的套餐余额:
|
|
62
62
|
- DeepSeek / OpenRouter 显示余额(如 `¥43.97` / `$12.50`);
|
|
63
63
|
- MiniMax / Kimi / 智谱 显示 `5h X% | 7d Y%`(5 小时 / 7 天窗口已用百分比,悬停查看重置时间);
|
|
64
|
-
- 小米 MiMo
|
|
64
|
+
- 小米 MiMo 默认显示 `今日 X · 7d Y`(**本地用量统计,零凭据、永不过期**);若在「套餐余额」里填了 dashboard 登录 Cookie,则升级为官方 `剩余 X%` + 周重置倒计时(见下);
|
|
65
65
|
- 切换模型时自动切换 provider;每 60s 自动刷新,**点击读数立即强制刷新**;
|
|
66
66
|
- 未配置对应 API Key 或拉取失败时显示 `⚠`(悬停查看原因);当前 provider 不在支持列表时不占位。
|
|
67
67
|
|
|
68
|
-
> **MiMo
|
|
68
|
+
> **MiMo(小米)配置**:**默认零配置**——读数来自本插件自己的会话日志聚合(今日 / 近 7 天消耗 token 数),不发任何 MiMo 请求、不会过期。缺点是拿不到套餐百分比(官方没开放任何 API Key 配额接口:`token-plan-*` 与 `api.` 主机只有 OpenAI 兼容的 `/v1` 推理 API,实测所有 Bearer 配额路径均 404;key 还与区域强绑定,CN key 在 SGP/AMS 主机一律 401)。
|
|
69
|
+
>
|
|
70
|
+
> 想要**官方剩余用量百分比**,唯一路径是 dashboard 账号会话:打开 platform.xiaomimimo.com 登录后,F12 → Network 任一 `/api/v1/*` 请求 → 复制完整 `Cookie` 请求头(含 `api-platform_serviceToken` 与 `userId`)→ 粘到「设置 → Token 统计 → 套餐余额 → **MiMo 官方剩余用量(可选)**」保存(写入 `XIAOMI_MIMO_COOKIE`)。插件内置最小 cookie jar:服务端若在响应里 `Set-Cookie` 续发会话会被自动吸收并写回凭据库(跨轮询、跨重启生效);**Cookie 彻底失效时自动静默退回本地用量,不会报错**。composer 读数按 route id 匹配(`xiaomi-mimo` / `xiaomimimo` / `xiaomi-token-plan-cn` / 名字含 xiaomi 或 mimo 均可)。
|
|
69
71
|
|
|
70
72
|
## 工作原理
|
|
71
73
|
|
|
@@ -88,7 +90,7 @@ TokenStatsService.getQuota() ← ctx.remote.tokenStats.getQuota(provider, forc
|
|
|
88
90
|
- 数据按**本地日历天** × **模型**(`provider::model`)聚合;`total = input + output + cacheRead + cacheWrite`。
|
|
89
91
|
- 历史回填只统计插件启动前发生的调用,实时监听只统计启动后的,两者通过每个会话的事件序号水位线合并,不会重复。
|
|
90
92
|
- 模型/模型来自 `assistant/message` 的 `message.source`(kind = 'model'),无需自行解析请求头。
|
|
91
|
-
- 套餐余额:Host 半经 `credentials` 服务解析用户在模型设置里已配置的 API Key(引用命名遵循 `<ROUTE>_API_KEY` 约定),GET provider 官方端点(宿主全局 fetch 优先,无 fetch 时回退 `subprocess` curl);30s 缓存 + 失败指数退避(5s→30min
|
|
93
|
+
- 套餐余额:Host 半经 `credentials` 服务解析用户在模型设置里已配置的 API Key(引用命名遵循 `<ROUTE>_API_KEY` 约定),GET provider 官方端点(宿主全局 fetch 优先,无 fetch 时回退 `subprocess` curl);30s 缓存 + 失败指数退避(5s→30min),不落盘。**小米 MiMo 是例外**:它没有 API Key 配额接口,默认用本地会话日志聚合出用量,填了 dashboard Cookie 才升级为官方剩余用量 %。
|
|
92
94
|
|
|
93
95
|
## 目录结构
|
|
94
96
|
|
package/client.js
CHANGED
|
@@ -683,15 +683,17 @@ window.__ModuleLoader__.load({
|
|
|
683
683
|
return null;
|
|
684
684
|
}
|
|
685
685
|
// 凭据设置行写入的 ref — 与 host QUOTA_PROVIDERS.apiKeyRefs 首选项对齐。
|
|
686
|
-
// mimo 写
|
|
687
|
-
//
|
|
686
|
+
// mimo 写 XIAOMI_MIMO_COOKIE(v1.7.0 起):MiMo 没有开放任何 API Key
|
|
687
|
+
// 配额接口(token-plan-* 主机只有 /v1 推理 API,实测见 scripts/probe-mimo.mjs),
|
|
688
|
+
// 只有 dashboard 账号会话 Cookie 能拿到官方「剩余用量 %」。不填也不影响
|
|
689
|
+
// —— host 会退回本地用量统计。
|
|
688
690
|
const QUOTA_CRED_REFS = {
|
|
689
691
|
minimax: "MINIMAX_CN_API_KEY",
|
|
690
692
|
deepseek: "DEEPSEEK_API_KEY",
|
|
691
693
|
kimi: "KIMI_CODING_API_KEY",
|
|
692
694
|
openrouter: "OPENROUTER_API_KEY",
|
|
693
695
|
zhipu: "ZAI_CODING_CN_API_KEY",
|
|
694
|
-
mimo: "
|
|
696
|
+
mimo: "XIAOMI_MIMO_COOKIE",
|
|
695
697
|
};
|
|
696
698
|
const QUOTA_PROVIDER_ORDER = ["minimax", "deepseek", "kimi", "openrouter", "zhipu", "mimo"];
|
|
697
699
|
|
|
@@ -723,10 +725,12 @@ window.__ModuleLoader__.load({
|
|
|
723
725
|
return [labelEl, React.createElement("span", { key: "b", style: strong }, txt)];
|
|
724
726
|
}
|
|
725
727
|
if (provider === "mimo") {
|
|
726
|
-
// MiMo
|
|
728
|
+
// MiMo:官方读数显示「剩余 X%」(balanceText),本地用量显示
|
|
729
|
+
// 「今日 X · 7d Y」——行内位置窄,只取前半段,完整内容在悬停面板。
|
|
727
730
|
const parts = [labelEl];
|
|
728
731
|
if (typeof d.balanceText === "string" && d.balanceText) {
|
|
729
|
-
|
|
732
|
+
const compact = d.balanceText.split(" · ")[0];
|
|
733
|
+
parts.push(React.createElement("span", { key: "bal", style: strong }, compact));
|
|
730
734
|
return parts;
|
|
731
735
|
}
|
|
732
736
|
if (typeof d.fiveHrPct === "number") {
|
|
@@ -757,20 +761,24 @@ window.__ModuleLoader__.load({
|
|
|
757
761
|
if (props.ok && props.display) {
|
|
758
762
|
const d = props.display;
|
|
759
763
|
if (typeof d.balanceText === "string" && d.balanceText) {
|
|
764
|
+
// mimo 的 balanceText 可能是「本地用量」也可能是官方「剩余 X%」。
|
|
765
|
+
const cap =
|
|
766
|
+
props.provider === "mimo"
|
|
767
|
+
? (typeof d.weeklyPct === "number" ? "官方剩余用量" : "本地用量(近 7 天 / 今日)")
|
|
768
|
+
: "账户余额" + (d.currency ? " (" + d.currency + ")" : "");
|
|
760
769
|
rows.push(
|
|
761
770
|
React.createElement(
|
|
762
771
|
"div", { key: "bal", className: "ts-quota-pop-row" },
|
|
763
|
-
React.createElement("span", null,
|
|
772
|
+
React.createElement("span", null, cap),
|
|
764
773
|
React.createElement("b", null, d.balanceText),
|
|
765
774
|
),
|
|
766
775
|
);
|
|
767
776
|
}
|
|
777
|
+
// mimo 官方读数只有一个 Token Plan 窗口(/api/v1/user/usage 的
|
|
778
|
+
// percent 是「剩余」,host 已换算成已用 %);本地用量模式没有窗口。
|
|
768
779
|
const windows =
|
|
769
780
|
props.provider === "mimo"
|
|
770
|
-
? [
|
|
771
|
-
["套餐已用", d.fiveHrPct, d.fiveHrResetsIn],
|
|
772
|
-
["本月总额度", d.weeklyPct, d.weeklyResetsIn],
|
|
773
|
-
]
|
|
781
|
+
? [["Token Plan", d.weeklyPct, d.weeklyResetsIn]]
|
|
774
782
|
: [
|
|
775
783
|
["5h 窗口", d.fiveHrPct, d.fiveHrResetsIn],
|
|
776
784
|
["7d 窗口", d.weeklyPct, d.weeklyResetsIn],
|
|
@@ -970,12 +978,17 @@ window.__ModuleLoader__.load({
|
|
|
970
978
|
}
|
|
971
979
|
const d = v.display || {};
|
|
972
980
|
if (typeof d.balanceText === "string" && d.balanceText) {
|
|
981
|
+
// mimo 复用 balanceText 承载两种读数:本地用量(无窗口百分比)或
|
|
982
|
+
// 官方「剩余 X%」(有 weeklyPct),所以标题不能写死「账户余额」。
|
|
983
|
+
const head = p === "mimo"
|
|
984
|
+
? (typeof d.weeklyPct === "number" ? "Token Plan" : "本地用量")
|
|
985
|
+
: "账户余额" + (d.currency ? " · " + d.currency : "");
|
|
973
986
|
return React.createElement(
|
|
974
987
|
"div", { className: "ts-card ts-quota-card" },
|
|
975
988
|
React.createElement(
|
|
976
989
|
"div", { className: "ts-quota-card-head" },
|
|
977
990
|
React.createElement("span", { className: "ts-quota-card-name" }, label),
|
|
978
|
-
React.createElement("span", { className: "ts-quota-card-status" },
|
|
991
|
+
React.createElement("span", { className: "ts-quota-card-status" }, head),
|
|
979
992
|
),
|
|
980
993
|
React.createElement("div", { className: "ts-quota-card-value" }, d.balanceText),
|
|
981
994
|
);
|
|
@@ -1069,7 +1082,14 @@ window.__ModuleLoader__.load({
|
|
|
1069
1082
|
const needsCred = names.filter(
|
|
1070
1083
|
(p) => quotas[p] && quotas[p].ok === false && (quotas[p].kind === "unconfigured" || quotas[p].kind === "auth_failed"),
|
|
1071
1084
|
);
|
|
1072
|
-
|
|
1085
|
+
// mimo 零凭据可用(本地用量),所以不会落进 needsCred;这里在它还处于
|
|
1086
|
+
// 本地模式时补一个**可选**的 Cookie 入口——粘了就能升级成官方剩余用量 %。
|
|
1087
|
+
const mimo = quotas.mimo;
|
|
1088
|
+
const mimoLocalMode =
|
|
1089
|
+
!!(mimo && mimo.ok === true) &&
|
|
1090
|
+
typeof mimo.display === "object" &&
|
|
1091
|
+
typeof mimo.display.weeklyPct !== "number";
|
|
1092
|
+
if (configured.length === 0 && needsCred.length === 0 && !mimoLocalMode) return null;
|
|
1073
1093
|
return React.createElement(
|
|
1074
1094
|
"div", { className: "ts-block" },
|
|
1075
1095
|
React.createElement(
|
|
@@ -1100,7 +1120,6 @@ window.__ModuleLoader__.load({
|
|
|
1100
1120
|
"div", { className: "ts-cred-block" },
|
|
1101
1121
|
React.createElement("div", { className: "ts-block-title" }, "凭据设置(粘贴后保存,自动重新拉取)"),
|
|
1102
1122
|
needsCred.map((p) => {
|
|
1103
|
-
const isAuthFailed = quotas[p] && quotas[p].kind === "auth_failed";
|
|
1104
1123
|
return React.createElement(
|
|
1105
1124
|
"div", { key: p, className: "ts-cred-row" },
|
|
1106
1125
|
React.createElement("span", { className: "ts-cred-label" }, quotaProviderLabel(p)),
|
|
@@ -1108,9 +1127,7 @@ window.__ModuleLoader__.load({
|
|
|
1108
1127
|
type: "password",
|
|
1109
1128
|
className: "ts-cred-input",
|
|
1110
1129
|
placeholder: p === "mimo"
|
|
1111
|
-
?
|
|
1112
|
-
? "凭据失效:粘贴新的 API Key(推荐)或 Cookie"
|
|
1113
|
-
: "推荐粘贴 API Key (tp-...),与 LLM 路由同源不易过期;Cookie 也能用")
|
|
1130
|
+
? "粘贴 platform.xiaomimimo.com 的 Cookie 请求头(含 api-platform_serviceToken 与 userId)"
|
|
1114
1131
|
: "粘贴 API Key",
|
|
1115
1132
|
value: credDrafts[p] || "",
|
|
1116
1133
|
onChange: (e) => setCredDrafts((d) => ({ ...d, [p]: e.target.value })),
|
|
@@ -1131,6 +1148,38 @@ window.__ModuleLoader__.load({
|
|
|
1131
1148
|
}),
|
|
1132
1149
|
)
|
|
1133
1150
|
: null,
|
|
1151
|
+
// mimo 专用:可选的 dashboard Cookie —— 粘了把本地用量升级成官方
|
|
1152
|
+
// 「剩余用量 % + 周重置」;不粘也能正常显示本地用量统计。
|
|
1153
|
+
mimoLocalMode
|
|
1154
|
+
? React.createElement(
|
|
1155
|
+
"div", { className: "ts-cred-block" },
|
|
1156
|
+
React.createElement("div", { className: "ts-block-title" }, "MiMo 官方剩余用量(可选)"),
|
|
1157
|
+
React.createElement(
|
|
1158
|
+
"div", { className: "ts-cred-row" },
|
|
1159
|
+
React.createElement("span", { className: "ts-cred-label" }, "Cookie"),
|
|
1160
|
+
React.createElement("input", {
|
|
1161
|
+
type: "password",
|
|
1162
|
+
className: "ts-cred-input",
|
|
1163
|
+
placeholder:
|
|
1164
|
+
"MiMo 未开放 API Key 配额接口;登录 platform.xiaomimimo.com 后从开发者工具复制完整 Cookie 请求头(含 api-platform_serviceToken 与 userId),可显示官方剩余 %(会过期,失效后自动退回本地用量)",
|
|
1165
|
+
value: credDrafts.mimo || "",
|
|
1166
|
+
onChange: (e) => setCredDrafts((d) => ({ ...d, mimo: e.target.value })),
|
|
1167
|
+
onKeyDown: (e) => {
|
|
1168
|
+
if (e.key === "Enter") saveCred("mimo", credDrafts.mimo || "");
|
|
1169
|
+
},
|
|
1170
|
+
}),
|
|
1171
|
+
React.createElement(
|
|
1172
|
+
"button", {
|
|
1173
|
+
className: "ts-cred-save",
|
|
1174
|
+
disabled: !!credSaving.mimo || !(credDrafts.mimo && credDrafts.mimo.trim()),
|
|
1175
|
+
onClick: () => saveCred("mimo", credDrafts.mimo || ""),
|
|
1176
|
+
},
|
|
1177
|
+
credSaving.mimo ? "保存中…" : "保存",
|
|
1178
|
+
),
|
|
1179
|
+
credMsgs.mimo ? React.createElement("span", { className: "ts-cred-msg" }, credMsgs.mimo) : null,
|
|
1180
|
+
),
|
|
1181
|
+
)
|
|
1182
|
+
: null,
|
|
1134
1183
|
);
|
|
1135
1184
|
}
|
|
1136
1185
|
|
package/index.js
CHANGED
|
@@ -142,40 +142,61 @@ 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.7.0 起重写)。
|
|
146
|
+
//
|
|
147
|
+
// 实测(2026-09,scripts/probe-mimo.mjs / probe-mimo-nocookie.mjs):
|
|
148
|
+
// · token-plan-{cn,sgp,ams} 与 api. 主机**只有 OpenAI 兼容的 /v1 推理 API**,
|
|
149
|
+
// `/v1/user/balance`、`/api/v1/tokenPlan/usage`、`/api/v1/user/usage` 全部
|
|
150
|
+
// 404 —— MiMo 没有给 API Key 开任何配额接口。
|
|
151
|
+
// · key 与区域强绑定(CN key 在 sgp/ams/api 一律 401 invalid_key),所以也不
|
|
152
|
+
// 能"换主机重试"。
|
|
153
|
+
// · 唯一存在的配额接口是 platform dashboard 的 `/api/v1/user/usage`
|
|
154
|
+
// (`{percent, resetDate}`,percent = **剩余**百分比;也是 MiMo 桌面端
|
|
155
|
+
// IPC `mimo:getUserUsage` 的同一条链路),但它只认小米账号会话 Cookie。
|
|
156
|
+
// 拿 tp- key 冒充 serviceToken / X-Service-Token / ?apiKey= 全部 401。
|
|
157
|
+
// → v1.6.0 用户看到的 401 正是"把 API Key 打了只认 Cookie 的 dashboard",
|
|
158
|
+
// 与 key 是否有效无关。
|
|
159
|
+
//
|
|
160
|
+
// 因此不再使用 API Key 读配额(`apiKeyRefs` 已移除):默认走**本地用量**
|
|
161
|
+
// (本插件已有的会话日志聚合,零凭据、零过期),配置了 Cookie 时自动升级为
|
|
162
|
+
// 官方「剩余用量 % + 周重置」,Cookie 失效则静默退回本地读数。
|
|
155
163
|
mimo: {
|
|
156
|
-
//
|
|
157
|
-
apiKeyRefs: ["XIAOMI_MIMO_API_KEY", "XIAOMI_TOKEN_PLAN_CN_API_KEY", "MIMO_API_KEY"],
|
|
158
|
-
// Cookie 鉴权 ref 候选 (浏览器 dashboard session, Bearer 失败时的兜底)
|
|
164
|
+
// dashboard 会话 Cookie(唯一能拿到官方套餐 % 的凭据)
|
|
159
165
|
cookieRefs: ["XIAOMI_MIMO_COOKIE", "XIAOMI_TOKEN_PLAN_CN_COOKIE", "MIMO_COOKIE"],
|
|
160
166
|
parse: parseMimoResponse,
|
|
161
167
|
},
|
|
162
168
|
};
|
|
163
169
|
|
|
164
|
-
/** Xiaomi MiMo
|
|
165
|
-
* 协议来源:
|
|
166
|
-
*
|
|
167
|
-
*
|
|
170
|
+
/** Xiaomi MiMo 常量 —— 唯一可用的配额接口在 dashboard,且只认账号会话 Cookie。
|
|
171
|
+
* 协议来源:MiMo 桌面端 IPC `mimo:getUserUsage`(逆向记录见
|
|
172
|
+
* https://github.com/ethanweave/mimo-composer-token-status/blob/main/docs/INVESTIGATION.md)、
|
|
173
|
+
* CodexBar docs/mimo.md(MIT)、quotas crate mimo.rs(Unlicense)。*/
|
|
168
174
|
const MIMO_PLATFORM = "https://platform.xiaomimimo.com";
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
/**
|
|
175
|
+
/** 官方桌面端同款:GET {accountBase}/user/usage → { percent(剩余), resetDate }。*/
|
|
176
|
+
const MIMO_USER_USAGE_URL = `${MIMO_PLATFORM}/api/v1/user/usage`;
|
|
177
|
+
/** 旧版套餐明细端点(保留兜底:部分账号只有它给数;已用比例 + 重置时间)。*/
|
|
172
178
|
const MIMO_USAGE_URL = `${MIMO_PLATFORM}/api/v1/tokenPlan/usage`;
|
|
173
179
|
const MIMO_DETAIL_URL = `${MIMO_PLATFORM}/api/v1/tokenPlan/detail`;
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
/**
|
|
177
|
-
const
|
|
178
|
-
|
|
180
|
+
/** dashboard 必需 Cookie 名(CodexBar docs/mimo.md):缺失就给明确指引。 */
|
|
181
|
+
const MIMO_COOKIE_REQUIRED = ["api-platform_serviceToken", "userId"];
|
|
182
|
+
/** dashboard admin API 需要的额外请求头(quotas crate mimo.rs 用 accept-language: en)。*/
|
|
183
|
+
const MIMO_PLATFORM_HEADERS = { "accept-language": "en" };
|
|
184
|
+
|
|
185
|
+
/** `"a=1; b=2"` → Map{name: value}(容忍空格、容忍值里有 `=`)。 */
|
|
186
|
+
function cookieStringToMap(s) {
|
|
187
|
+
const out = new Map();
|
|
188
|
+
for (const part of String(s || "").split(";")) {
|
|
189
|
+
const i = part.indexOf("=");
|
|
190
|
+
if (i <= 0) continue;
|
|
191
|
+
out.set(part.slice(0, i).trim(), part.slice(i + 1).trim());
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Map → `"a=1; b=2"`。 */
|
|
197
|
+
function cookieMapToString(m) {
|
|
198
|
+
return [...m].map(([k, v]) => k + "=" + v).join("; ");
|
|
199
|
+
}
|
|
179
200
|
|
|
180
201
|
function quotaBackoffMs(streak) {
|
|
181
202
|
if (streak <= 0) return 0;
|
|
@@ -556,33 +577,29 @@ function parseMimoResponse(body, detailBody) {
|
|
|
556
577
|
};
|
|
557
578
|
}
|
|
558
579
|
|
|
559
|
-
// ----- xiaomi mimo /v1/user/
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
// balance 可能是字符串或数字(来自 Rust 端 serde 默认行为)。
|
|
567
|
-
// 数据本身已含 plan 字段,无需再拉 detail。
|
|
568
|
-
|
|
569
|
-
function parseMimoBalanceResponse(body /*, detailBody — unused, balance 端点不含 detail */) {
|
|
580
|
+
// ----- xiaomi mimo /api/v1/user/usage parser (v1.7.0,官方桌面端同款链路) -----
|
|
581
|
+
// { "percent": 78, "resetDate": "2026-09-16" }
|
|
582
|
+
// **percent 是「剩余」百分比**(账户菜单「剩余用量 78%」),与上面
|
|
583
|
+
// tokenPlan/usage 的 items[].percent(**已用**比例)语义相反,别混用。
|
|
584
|
+
// wire 里 fiveHrPct/weeklyPct 一律是「已用 %」,所以取 100 - percent。
|
|
585
|
+
|
|
586
|
+
function parseMimoUserUsage(body) {
|
|
570
587
|
let json;
|
|
571
588
|
try {
|
|
572
589
|
json = typeof body === "string" ? JSON.parse(body) : body;
|
|
573
590
|
} catch {
|
|
574
|
-
return { ok: false, provider: "mimo", kind: "parse", message: "MiMo
|
|
591
|
+
return { ok: false, provider: "mimo", kind: "parse", message: "MiMo 响应 JSON 解析失败" };
|
|
575
592
|
}
|
|
576
593
|
if (!json || typeof json !== "object") {
|
|
577
|
-
return { ok: false, provider: "mimo", kind: "parse", message: "MiMo
|
|
594
|
+
return { ok: false, provider: "mimo", kind: "parse", message: "MiMo 响应不是对象" };
|
|
578
595
|
}
|
|
579
596
|
if (typeof json.code === "number" && json.code !== 0) {
|
|
580
|
-
if (json.code >= 40100 && json.code < 40200) {
|
|
597
|
+
if (json.code === 401 || (json.code >= 40100 && json.code < 40200)) {
|
|
581
598
|
return {
|
|
582
599
|
ok: false,
|
|
583
600
|
provider: "mimo",
|
|
584
601
|
kind: "auth_failed",
|
|
585
|
-
message: "MiMo
|
|
602
|
+
message: "MiMo Cookie 已失效,请重新登录 platform.xiaomimimo.com 并复制 Cookie 请求头",
|
|
586
603
|
};
|
|
587
604
|
}
|
|
588
605
|
return {
|
|
@@ -592,63 +609,52 @@ function parseMimoBalanceResponse(body /*, detailBody — unused, balance 端点
|
|
|
592
609
|
message: "MiMo 业务错误 code=" + json.code + " · " + (json.message || ""),
|
|
593
610
|
};
|
|
594
611
|
}
|
|
595
|
-
const data =
|
|
612
|
+
const data = json.data && typeof json.data === "object" ? json.data : json;
|
|
596
613
|
|
|
597
|
-
|
|
598
|
-
|
|
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);
|
|
614
|
+
const raw = Number(data.percent);
|
|
615
|
+
if (!isFinite(raw)) {
|
|
603
616
|
return {
|
|
604
|
-
ok:
|
|
617
|
+
ok: false,
|
|
605
618
|
provider: "mimo",
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
weeklyPct: null,
|
|
609
|
-
fiveHrResetsIn: null,
|
|
610
|
-
weeklyResetsIn: null,
|
|
611
|
-
balanceText: null,
|
|
612
|
-
balanceUsd: null,
|
|
613
|
-
currency: null,
|
|
614
|
-
},
|
|
619
|
+
kind: "parse",
|
|
620
|
+
message: "MiMo 响应缺少 percent 字段(keys: " + Object.keys(data).join(",") + ")",
|
|
615
621
|
};
|
|
616
622
|
}
|
|
623
|
+
const remaining = Math.max(0, Math.min(100, Math.round(raw)));
|
|
624
|
+
const used = 100 - remaining;
|
|
617
625
|
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
};
|
|
626
|
+
// resetDate 形如 "2026-09-16"(本地日),按当天 23:59:59 解释为到期时刻。
|
|
627
|
+
let resetsAt = null;
|
|
628
|
+
if (typeof data.resetDate === "string" && data.resetDate) {
|
|
629
|
+
const iso = data.resetDate.length <= 10 ? data.resetDate + "T23:59:59" : data.resetDate;
|
|
630
|
+
const t = Date.parse(iso);
|
|
631
|
+
if (!isNaN(t)) resetsAt = t;
|
|
635
632
|
}
|
|
633
|
+
const resetsIn = resetsAt ? formatResetsIn(resetsAt) : null;
|
|
636
634
|
|
|
637
635
|
return {
|
|
638
|
-
ok:
|
|
636
|
+
ok: true,
|
|
639
637
|
provider: "mimo",
|
|
640
|
-
|
|
641
|
-
|
|
638
|
+
display: {
|
|
639
|
+
// 剩余用量是单一窗口(账户菜单标「1 周」),映射到 7d 窗口位
|
|
640
|
+
weeklyPct: used,
|
|
641
|
+
weeklyResetsIn: resetsIn,
|
|
642
|
+
fiveHrPct: null,
|
|
643
|
+
fiveHrResetsIn: null,
|
|
644
|
+
balanceText: "剩余 " + remaining + "%",
|
|
645
|
+
balanceUsd: null,
|
|
646
|
+
currency: null,
|
|
647
|
+
},
|
|
642
648
|
};
|
|
643
649
|
}
|
|
644
650
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
return
|
|
651
|
+
/** 12345678 → "12.3M"(读数行/卡片用的紧凑 token 计数)。 */
|
|
652
|
+
function formatTokenCount(n) {
|
|
653
|
+
if (!isFinite(n) || n <= 0) return "0";
|
|
654
|
+
if (n >= 1e9) return (n / 1e9).toFixed(n >= 1e10 ? 0 : 1) + "B";
|
|
655
|
+
if (n >= 1e6) return (n / 1e6).toFixed(n >= 1e7 ? 0 : 1) + "M";
|
|
656
|
+
if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "K";
|
|
657
|
+
return String(Math.round(n));
|
|
652
658
|
}
|
|
653
659
|
|
|
654
660
|
function formatResetsIn(resetsAtMs) {
|
|
@@ -1207,7 +1213,7 @@ export class TokenStatsService extends TypertRemoteService {
|
|
|
1207
1213
|
}
|
|
1208
1214
|
|
|
1209
1215
|
/** curl fallback for hosts without a global fetch (same shape as musage). */
|
|
1210
|
-
async _curlGet(url, key, authStyle) {
|
|
1216
|
+
async _curlGet(url, key, authStyle, extraHeaders) {
|
|
1211
1217
|
const subprocess = this.ctx.get("subprocess");
|
|
1212
1218
|
if (!subprocess || typeof subprocess.spawn !== "function") {
|
|
1213
1219
|
return { ok: false, kind: "network", message: "subprocess service 不可用, 且宿主无全局 fetch" };
|
|
@@ -1219,15 +1225,19 @@ export class TokenStatsService extends TypertRemoteService {
|
|
|
1219
1225
|
: authStyle === "cookie"
|
|
1220
1226
|
? "Cookie: " + key
|
|
1221
1227
|
: "Authorization: Bearer " + key;
|
|
1228
|
+
const argv = [
|
|
1229
|
+
c, "-sS",
|
|
1230
|
+
"--max-time", String(Math.floor(QUOTA_REQUEST_TIMEOUT_MS / 1000)),
|
|
1231
|
+
"-w", "\n%{http_code}",
|
|
1232
|
+
"-H", authHeader,
|
|
1233
|
+
"-H", "Accept: application/json",
|
|
1234
|
+
];
|
|
1235
|
+
for (const [name, value] of Object.entries(extraHeaders || {})) {
|
|
1236
|
+
argv.push("-H", name + ": " + value);
|
|
1237
|
+
}
|
|
1238
|
+
argv.push(url);
|
|
1222
1239
|
const handle = subprocess.spawn({
|
|
1223
|
-
argv
|
|
1224
|
-
c, "-sS",
|
|
1225
|
-
"--max-time", String(Math.floor(QUOTA_REQUEST_TIMEOUT_MS / 1000)),
|
|
1226
|
-
"-w", "\n%{http_code}",
|
|
1227
|
-
"-H", authHeader,
|
|
1228
|
-
"-H", "Accept: application/json",
|
|
1229
|
-
url,
|
|
1230
|
-
],
|
|
1240
|
+
argv,
|
|
1231
1241
|
cwd: "/",
|
|
1232
1242
|
stdio: {
|
|
1233
1243
|
stdin: "ignore",
|
|
@@ -1261,11 +1271,13 @@ export class TokenStatsService extends TypertRemoteService {
|
|
|
1261
1271
|
return { ok: true, body };
|
|
1262
1272
|
}
|
|
1263
1273
|
|
|
1264
|
-
/** GET one provider endpoint with its API key; fetch first, curl fallback.
|
|
1265
|
-
|
|
1274
|
+
/** GET one provider endpoint with its API key; fetch first, curl fallback.
|
|
1275
|
+
* `extraHeaders` carries provider-specific headers (e.g. MiMo dashboard 的
|
|
1276
|
+
* accept-language: en),fetch 与 curl 两条路径都要带上。 */
|
|
1277
|
+
async _httpGet(url, key, authStyle, extraHeaders) {
|
|
1266
1278
|
if (typeof fetch === "function") {
|
|
1267
1279
|
try {
|
|
1268
|
-
const headers = { accept: "application/json" };
|
|
1280
|
+
const headers = { accept: "application/json", ...(extraHeaders || {}) };
|
|
1269
1281
|
// authStyle: undefined → Bearer; "raw" → 原样 Authorization(智谱);
|
|
1270
1282
|
// "cookie" → 整串 Cookie 头(小米 MiMo dashboard admin API)。
|
|
1271
1283
|
if (authStyle === "cookie") headers.cookie = key;
|
|
@@ -1276,20 +1288,25 @@ export class TokenStatsService extends TypertRemoteService {
|
|
|
1276
1288
|
signal: AbortSignal.timeout(QUOTA_REQUEST_TIMEOUT_MS),
|
|
1277
1289
|
});
|
|
1278
1290
|
const body = await res.text();
|
|
1291
|
+
// 带回 Set-Cookie:mimo 的 cookie jar 靠它接住服务端续发的会话
|
|
1292
|
+
// (浏览器自动存,我们此前每次重发死 cookie,等于放弃了续期)。
|
|
1293
|
+
const setCookie = typeof res.headers.getSetCookie === "function" ? res.headers.getSetCookie() : [];
|
|
1279
1294
|
if (res.status !== 200) {
|
|
1280
1295
|
return {
|
|
1281
1296
|
ok: false,
|
|
1282
1297
|
kind: classifyQuotaHttpStatus(res.status),
|
|
1283
1298
|
httpStatus: res.status,
|
|
1299
|
+
setCookie,
|
|
1284
1300
|
message: "HTTP " + res.status + " · " + body.slice(0, 200),
|
|
1285
1301
|
};
|
|
1286
1302
|
}
|
|
1287
|
-
return { ok: true, body };
|
|
1303
|
+
return { ok: true, body, setCookie };
|
|
1288
1304
|
} catch (e) {
|
|
1289
1305
|
return { ok: false, kind: "network", message: "fetch 失败: " + String((e && e.message) || e) };
|
|
1290
1306
|
}
|
|
1291
1307
|
}
|
|
1292
|
-
|
|
1308
|
+
// curl 回退路径不带 Set-Cookie(无法维持会话续期,仅影响 mimo 的 cookie 升级)。
|
|
1309
|
+
return this._curlGet(url, key, authStyle, extraHeaders);
|
|
1293
1310
|
}
|
|
1294
1311
|
|
|
1295
1312
|
async _fetchProviderQuota(provider) {
|
|
@@ -1297,8 +1314,8 @@ export class TokenStatsService extends TypertRemoteService {
|
|
|
1297
1314
|
if (!cfg) {
|
|
1298
1315
|
return { ok: false, provider, kind: "other", message: "未知 provider: " + provider };
|
|
1299
1316
|
}
|
|
1300
|
-
const { apiKey, apiKeyRef, cookie } = await this._quotaApiKey(provider);
|
|
1301
|
-
if (provider === "mimo") return this._fetchMimoQuota(apiKey, cookie);
|
|
1317
|
+
const { apiKey, apiKeyRef, cookie, cookieRef } = await this._quotaApiKey(provider);
|
|
1318
|
+
if (provider === "mimo") return this._fetchMimoQuota(apiKey, apiKeyRef, cookie, cookieRef);
|
|
1302
1319
|
if (!apiKey) {
|
|
1303
1320
|
return {
|
|
1304
1321
|
ok: false,
|
|
@@ -1315,73 +1332,176 @@ export class TokenStatsService extends TypertRemoteService {
|
|
|
1315
1332
|
}
|
|
1316
1333
|
|
|
1317
1334
|
/**
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1335
|
+
* MiMo 本地用量(零凭据、零过期):从会话日志聚合里筛出 MiMo 模型,
|
|
1336
|
+
* 算「今日 / 近 7 天(含今天)」消耗 token。provider 判定与 client 的
|
|
1337
|
+
* PROVIDER_ALIASES 同一套包含回退(route id 用户自由命名)。
|
|
1338
|
+
*/
|
|
1339
|
+
_mimoLocalUsage() {
|
|
1340
|
+
const isMimo = (meta, key) => {
|
|
1341
|
+
const p = String((meta && meta.provider) || "").toLowerCase();
|
|
1342
|
+
if (p) return p.includes("mimo") || p.includes("xiaomi");
|
|
1343
|
+
const n = String((meta && meta.name) || key || "").toLowerCase();
|
|
1344
|
+
return n.includes("mimo");
|
|
1345
|
+
};
|
|
1346
|
+
const now = Date.now();
|
|
1347
|
+
const todayKey = this._dayKeyOf(now);
|
|
1348
|
+
const weekCut = this._dayKeyOf(now - 6 * DAY_MS);
|
|
1349
|
+
let today = 0;
|
|
1350
|
+
let week = 0;
|
|
1351
|
+
for (const [dk, day] of this._byDay) {
|
|
1352
|
+
if (dk > todayKey) continue;
|
|
1353
|
+
let dayTotal = 0;
|
|
1354
|
+
for (const [key, rec] of day) {
|
|
1355
|
+
const meta = this._modelMeta.get(key);
|
|
1356
|
+
if (!isMimo(meta, key)) continue;
|
|
1357
|
+
dayTotal += rec.total || 0;
|
|
1358
|
+
}
|
|
1359
|
+
if (dk === todayKey) today = dayTotal;
|
|
1360
|
+
if (dk >= weekCut) week += dayTotal;
|
|
1361
|
+
}
|
|
1362
|
+
return { today, week };
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* 最小 cookie jar:把响应 `Set-Cookie` 里属于 MiMo 的条目吸收进当前 cookie 串,
|
|
1367
|
+
* 并写回 DSH 凭据库,使服务端续发的会话能跨轮询、跨重启生效。
|
|
1368
|
+
*
|
|
1369
|
+
* 只吸收两类名字:jar 里已存在的(续期/轮换同一条 cookie),以及 MiMo 已知的那几个
|
|
1370
|
+
* (防止服务端塞进无关 cookie 污染用户粘贴的内容)。没有变化则不写盘。
|
|
1371
|
+
*
|
|
1372
|
+
* @returns {Promise<string|null>} 合并后的新 cookie 串;无变化返回 null。
|
|
1373
|
+
*/
|
|
1374
|
+
async _absorbMimoCookie(cookieRef, current, setCookieHeaders) {
|
|
1375
|
+
if (!Array.isArray(setCookieHeaders) || setCookieHeaders.length === 0) return null;
|
|
1376
|
+
const jar = cookieStringToMap(current);
|
|
1377
|
+
const known = new Set([...MIMO_COOKIE_REQUIRED, "api-platform_ph", "api-platform_slh"]);
|
|
1378
|
+
let changed = false;
|
|
1379
|
+
for (const raw of setCookieHeaders) {
|
|
1380
|
+
// 只取第一段 `name=value`,后面的 Path/HttpOnly/Expires 是属性不是 cookie
|
|
1381
|
+
const pair = String(raw).split(";")[0];
|
|
1382
|
+
const i = pair.indexOf("=");
|
|
1383
|
+
if (i <= 0) continue;
|
|
1384
|
+
const name = pair.slice(0, i).trim();
|
|
1385
|
+
if (!name) continue;
|
|
1386
|
+
if (!jar.has(name) && !known.has(name)) continue;
|
|
1387
|
+
const value = pair.slice(i + 1).trim();
|
|
1388
|
+
// 浏览器语义:空值或 Max-Age=0 表示**删除**该 cookie,不是把它置空
|
|
1389
|
+
// (置空会发出 `api-platform_serviceToken=`,等于带着坏 cookie 去请求)。
|
|
1390
|
+
if (value === "" || /;\s*max-age\s*=\s*0/i.test(raw)) {
|
|
1391
|
+
if (jar.delete(name)) changed = true;
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
if (jar.get(name) === value) continue;
|
|
1395
|
+
jar.set(name, value);
|
|
1396
|
+
changed = true;
|
|
1397
|
+
}
|
|
1398
|
+
if (!changed) return null;
|
|
1399
|
+
const next = cookieMapToString(jar);
|
|
1400
|
+
const credentials = this.ctx.get("credentials");
|
|
1401
|
+
// 服务端把会话 cookie 全清掉了 = 会话没了:删掉凭据比留个空串更贴近浏览器,
|
|
1402
|
+
// 下一轮会自然退回本地用量,而不是带着空 cookie 反复 401。
|
|
1403
|
+
if (!next) {
|
|
1404
|
+
try {
|
|
1405
|
+
if (cookieRef && credentials && typeof credentials.unset === "function") {
|
|
1406
|
+
await credentials.unset(cookieRef);
|
|
1407
|
+
}
|
|
1408
|
+
} catch {
|
|
1409
|
+
/* 写回失败不影响本次读数 */
|
|
1410
|
+
}
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
// 写回官方凭据库(宿主侧 credentials.set);失败只影响跨重启续期,本进程内
|
|
1414
|
+
// 仍用 next 继续请求,所以吞掉异常。
|
|
1415
|
+
try {
|
|
1416
|
+
if (cookieRef && credentials && typeof credentials.set === "function") {
|
|
1417
|
+
await credentials.set(cookieRef, next);
|
|
1418
|
+
}
|
|
1419
|
+
} catch {
|
|
1420
|
+
/* 写回失败不影响本次读数 */
|
|
1421
|
+
}
|
|
1422
|
+
return next;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/** MiMo 读数(v1.7.0):官方 Cookie 优先,失败静默退回本地用量。 */
|
|
1426
|
+
async _fetchMimoQuota(_apiKey, _apiKeyRef, cookie, cookieRef) {
|
|
1427
|
+
if (cookie) {
|
|
1428
|
+
const official = await this._fetchMimoOfficial(cookie, cookieRef);
|
|
1429
|
+
if (official.ok) return official;
|
|
1430
|
+
// Cookie 失效(auth_failed)→ 不打扰用户,静默退回本地用量。
|
|
1431
|
+
// 其余(套餐过期 / 解析失败 / 限流 / 5xx)都是真实状态,如实上报。
|
|
1432
|
+
if (official.kind !== "auth_failed") return official;
|
|
1433
|
+
}
|
|
1434
|
+
const local = this._mimoLocalUsage();
|
|
1435
|
+
return {
|
|
1436
|
+
ok: true,
|
|
1437
|
+
provider: "mimo",
|
|
1438
|
+
display: {
|
|
1439
|
+
fiveHrPct: null,
|
|
1440
|
+
weeklyPct: null,
|
|
1441
|
+
fiveHrResetsIn: null,
|
|
1442
|
+
weeklyResetsIn: null,
|
|
1443
|
+
balanceText: "今日 " + formatTokenCount(local.today) + " · 7d " + formatTokenCount(local.week),
|
|
1444
|
+
balanceUsd: null,
|
|
1445
|
+
currency: null,
|
|
1446
|
+
},
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* 官方 dashboard 配额(仅 Cookie):主用桌面端同款 `/api/v1/user/usage`
|
|
1452
|
+
* (percent=剩余 + resetDate),失败再退到旧版 `/api/v1/tokenPlan/usage`
|
|
1453
|
+
* + `/detail`(已用比例)。两条都不通 = Cookie 失效。
|
|
1331
1454
|
*/
|
|
1332
|
-
async
|
|
1333
|
-
|
|
1455
|
+
async _fetchMimoOfficial(cookie, cookieRef) {
|
|
1456
|
+
const missing = MIMO_COOKIE_REQUIRED.filter((n) => cookie.indexOf(n) < 0);
|
|
1457
|
+
if (missing.length === MIMO_COOKIE_REQUIRED.length) {
|
|
1334
1458
|
return {
|
|
1335
1459
|
ok: false,
|
|
1336
1460
|
provider: "mimo",
|
|
1337
|
-
kind: "
|
|
1338
|
-
message:
|
|
1461
|
+
kind: "auth_failed",
|
|
1462
|
+
message:
|
|
1463
|
+
"MiMo Cookie 缺少 " + MIMO_COOKIE_REQUIRED.join(" / ") +
|
|
1464
|
+
":登录 platform.xiaomimimo.com 后从开发者工具复制完整 Cookie 请求头",
|
|
1339
1465
|
};
|
|
1340
1466
|
}
|
|
1341
1467
|
|
|
1342
|
-
//
|
|
1343
|
-
//
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
}
|
|
1468
|
+
// 每个响应后立刻吸收 Set-Cookie:服务端续发的会话要马上用于后续请求
|
|
1469
|
+
// (browser jar 的等价行为),否则同一轮里还在用旧 token。
|
|
1470
|
+
const jar = async (res) => {
|
|
1471
|
+
const next = await this._absorbMimoCookie(cookieRef, cookie, res && res.setCookie);
|
|
1472
|
+
if (next) cookie = next;
|
|
1473
|
+
return res;
|
|
1474
|
+
};
|
|
1355
1475
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
const parsed =
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
lastError = { ...parsed, provider: "mimo" };
|
|
1377
|
-
continue;
|
|
1378
|
-
}
|
|
1379
|
-
// 其他失败(parse / server_error):直接返回,不必继续探测。
|
|
1380
|
-
return parsed;
|
|
1381
|
-
}
|
|
1382
|
-
return parsed;
|
|
1476
|
+
const user = await jar(await this._httpGet(MIMO_USER_USAGE_URL, cookie, "cookie", MIMO_PLATFORM_HEADERS));
|
|
1477
|
+
if (user.ok) {
|
|
1478
|
+
const parsed = parseMimoUserUsage(user.body);
|
|
1479
|
+
if (parsed.ok) return parsed;
|
|
1480
|
+
// 业务码 401 = 会话已死,不用再试旧端点。
|
|
1481
|
+
if (parsed.kind === "auth_failed") return parsed;
|
|
1482
|
+
} else if (user.httpStatus === 401 || user.httpStatus === 403) {
|
|
1483
|
+
return { ...user, provider: "mimo" };
|
|
1484
|
+
} else if (user.kind === "rate_limited" || (user.httpStatus >= 500 && user.httpStatus < 600)) {
|
|
1485
|
+
return { ...user, provider: "mimo" };
|
|
1486
|
+
}
|
|
1487
|
+
// 404(该账号没有这个端点)或其它 → 退到旧版 tokenPlan/usage + detail。
|
|
1488
|
+
// detail 必须先拉(expired 标记 + 重置时间要在 usage 解析前拿到)。
|
|
1489
|
+
const detail = await jar(await this._httpGet(MIMO_DETAIL_URL, cookie, "cookie", MIMO_PLATFORM_HEADERS));
|
|
1490
|
+
const usage = await jar(await this._httpGet(MIMO_USAGE_URL, cookie, "cookie", MIMO_PLATFORM_HEADERS));
|
|
1491
|
+
if (usage.ok) {
|
|
1492
|
+
const parsed = parseMimoResponse(usage.body, detail.ok ? detail.body : null);
|
|
1493
|
+
// plan_expired / schema_unknown / parse 都是"拿到了但内容不对",如实上报;
|
|
1494
|
+
// 只有 auth_failed 才继续降级。
|
|
1495
|
+
if (parsed.ok || parsed.kind !== "auth_failed") return parsed;
|
|
1383
1496
|
}
|
|
1384
|
-
return
|
|
1497
|
+
return {
|
|
1498
|
+
ok: false,
|
|
1499
|
+
provider: "mimo",
|
|
1500
|
+
kind: "auth_failed",
|
|
1501
|
+
message:
|
|
1502
|
+
"MiMo Cookie 已失效(" + MIMO_USER_USAGE_URL.replace("https://", "") +
|
|
1503
|
+
" 返回 " + (user.httpStatus || "异常") + "):重新登录 platform.xiaomimimo.com 后复制 Cookie",
|
|
1504
|
+
};
|
|
1385
1505
|
}
|
|
1386
1506
|
|
|
1387
1507
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duke-dsh-plugins/dsh-token-stats",
|
|
3
|
-
"version": "1.
|
|
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).",
|
|
3
|
+
"version": "1.7.0",
|
|
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/mimo; mimo falls back to local usage since MiMo exposes no API-key quota API).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/MoonlitDropOfBlood/dsh-token-stats.git"
|
|
@@ -52,6 +52,9 @@
|
|
|
52
52
|
"smoke:robust": "node scripts/smoke-robust-host.mjs && node scripts/smoke-robust-client.mjs",
|
|
53
53
|
"smoke:typert": "node scripts/smoke-typert.mjs",
|
|
54
54
|
"smoke:compat": "node scripts/smoke-compat.mjs",
|
|
55
|
+
"probe:mimo": "node scripts/probe-mimo.mjs",
|
|
56
|
+
"probe:mimo:nocookie": "node scripts/probe-mimo-nocookie.mjs",
|
|
57
|
+
"probe:mimo:session": "node scripts/probe-mimo-session.mjs",
|
|
55
58
|
"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"
|
|
56
59
|
}
|
|
57
60
|
}
|