@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.
Files changed (4) hide show
  1. package/README.md +5 -3
  2. package/client.js +65 -16
  3. package/index.js +278 -158
  4. 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 显示 `套餐 X% | 本月 Y%`(Token Plan 已用百分比,悬停查看重置时间;需在凭据里配 platform.xiaomimimo.com 的登录 Cookie,见下);
64
+ - 小米 MiMo 默认显示 `今日 X · 7d Y`(**本地用量统计,零凭据、永不过期**);若在「套餐余额」里填了 dashboard 登录 Cookie,则升级为官方 `剩余 X%` + 周重置倒计时(见下);
65
65
  - 切换模型时自动切换 provider;每 60s 自动刷新,**点击读数立即强制刷新**;
66
66
  - 未配置对应 API Key 或拉取失败时显示 `⚠`(悬停查看原因);当前 provider 不在支持列表时不占位。
67
67
 
68
- > **MiMo(小米)配置**:MiMo 的用量走 dashboard admin API(非公开 endpoint),纯 API Key(Bearer)实测会被 401 拒绝,可靠凭证是浏览器登录 Cookie。打开 platform.xiaomimimo.com → 订阅管理,F12 → Network 任一 `/api/v1/tokenPlan/*` 请求 → 复制完整 `Cookie` 请求头值,粘到 DSH 凭据的 `XIAOMI_MIMO_COOKIE`(或把 API Key 配到 `XIAOMI_MIMO_API_KEY`,插件会先 Bearer 后 Cookie 自动重试)。按 route 派生命名的凭据(设置页自动生成,如 `XIAOMI_TOKEN_PLAN_CN_API_KEY`)同样识别;composer 读数按 route id 匹配(`xiaomi-mimo` / `xiaomimimo` / `xiaomi-token-plan-cn` / 名字含 xiaomi 或 mimo 均可)。**v1.5.4 起也可以直接在「设置 → Token 统计 → 套餐余额 → 凭据设置」里粘贴保存**,无需手改凭据库(mimo 写入 `XIAOMI_MIMO_COOKIE`,不影响 LLM 路由的 API Key)。
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 写 XIAOMI_MIMO_API_KEY(v1.6.0 起推荐:Bearer /v1/user/balance
687
- // 与路由 LLM API Key 同源,稳定不易过期;Cookie 仍可作为兜底探测)。
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: "XIAOMI_MIMO_API_KEY",
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:PAYG 显示 CNY 余额,Token Plan 显示 套餐% + 本月%。
728
+ // MiMo:官方读数显示「剩余 X%」(balanceText),本地用量显示
729
+ // 「今日 X · 7d Y」——行内位置窄,只取前半段,完整内容在悬停面板。
727
730
  const parts = [labelEl];
728
731
  if (typeof d.balanceText === "string" && d.balanceText) {
729
- parts.push(React.createElement("span", { key: "bal", style: strong }, d.balanceText));
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, "账户余额" + (d.currency ? " (" + d.currency + ")" : "")),
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" }, "账户余额" + (d.currency ? " · " + d.currency : "")),
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
- if (configured.length === 0 && needsCred.length === 0) return null;
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
- ? (isAuthFailed
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 — 多端点探测 (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 兜底)。
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
- // 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 失败时的兜底)
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 多端点常量 — 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 兜底。 */
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
- 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,套餐 % + 重置时间)。 */
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
- 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`;
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/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 */) {
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 balance 响应 JSON 解析失败" };
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 balance 响应不是对象" };
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 API Key 已失效,请到 platform.xiaomimimo.com 重新生成",
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 = (json.data && typeof json.data === "object") ? json.data : json;
612
+ const data = json.data && typeof json.data === "object" ? json.data : json;
596
613
 
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);
614
+ const raw = Number(data.percent);
615
+ if (!isFinite(raw)) {
603
616
  return {
604
- ok: true,
617
+ ok: false,
605
618
  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
- },
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
- // 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
- };
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: false,
636
+ ok: true,
639
637
  provider: "mimo",
640
- kind: "parse",
641
- message: "MiMo balance 响应缺少 token_balance/token_limit 或 balance 字段",
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
- 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;
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
- async _httpGet(url, key, authStyle) {
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
- return this._curlGet(url, key, authStyle);
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
- * 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。
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 _fetchMimoQuota(apiKey, cookie) {
1333
- if (!apiKey && !cookie) {
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: "unconfigured",
1338
- message: "未配置 MiMo API Key 或 Cookie(在 DSH 模型设置里配置对应 provider)",
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
- // 探测链 — 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
- }
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
- 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;
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 lastError || { ok: false, provider: "mimo", kind: "other", message: "MiMo 所有端点探测均失败" };
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.6.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).",
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
  }