@duke-dsh-plugins/dsh-token-stats 1.5.2 → 1.5.4

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 CHANGED
@@ -65,7 +65,7 @@ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-token-sta
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 自动重试)。
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)。
69
69
 
70
70
  ## 工作原理
71
71
 
package/client.js CHANGED
@@ -33,6 +33,15 @@ window.__ModuleLoader__.load({
33
33
  .ts-head-title{font-size:16px;font-weight:500;line-height:24px;margin:0;flex:1}
34
34
  .ts-refresh{box-sizing:border-box;height:28px;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:transparent;color:var(--dsw-alias-label-secondary);padding:0 12px;font-size:12px;line-height:26px}
35
35
  .ts-refresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1)}
36
+ .ts-cred-block{display:flex;flex-direction:column;gap:8px}
37
+ .ts-cred-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
38
+ .ts-cred-label{min-width:64px;font-size:12px;color:var(--dsw-alias-label-secondary)}
39
+ .ts-cred-input{box-sizing:border-box;height:28px;flex:1;min-width:220px;font:inherit;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);padding:0 10px;font-size:12px}
40
+ .ts-cred-input:focus{outline:none;border-color:var(--dsw-alias-border-l1)}
41
+ .ts-cred-save{box-sizing:border-box;height:28px;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);padding:0 12px;font-size:12px}
42
+ .ts-cred-save:hover{border-color:var(--dsw-alias-border-l1)}
43
+ .ts-cred-save:disabled{opacity:.5;cursor:default}
44
+ .ts-cred-msg{font-size:12px;color:var(--dsw-alias-label-secondary)}
36
45
  .ts-status{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;min-height:18px}
37
46
  .ts-tabs{display:inline-flex;gap:4px;padding:3px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;align-self:flex-start}
38
47
  .ts-tab{box-sizing:border-box;height:26px;font:inherit;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-secondary);border-radius:7px;padding:0 14px;font-size:13px;line-height:26px}
@@ -646,8 +655,29 @@ window.__ModuleLoader__.load({
646
655
  // Xiaomi MiMo:route id 由用户添加 provider 时命名,覆盖常见拼法。
647
656
  "xiaomi-mimo": "mimo",
648
657
  "xiaomimimo": "mimo",
658
+ "xiaomi-token-plan-cn": "mimo",
649
659
  "mimo": "mimo",
650
660
  };
661
+ /** route id → 内部 key:精确查表未命中时对 mimo 做包含回退(route id 用户自由命名,如 xiaomi-token-plan-cn)。 */
662
+ function providerKeyForRoute(route) {
663
+ if (!route) return null;
664
+ const exact = PROVIDER_ALIASES[route];
665
+ if (exact !== undefined) return exact;
666
+ const lower = String(route).toLowerCase();
667
+ if (lower.includes("mimo") || lower.includes("xiaomi")) return "mimo";
668
+ return null;
669
+ }
670
+ // 凭据设置行写入的 ref — 与 host QUOTA_PROVIDERS.refs 首选项对齐。
671
+ // mimo 写 XIAOMI_MIMO_COOKIE(README 文档路径),不触碰 LLM 路由的
672
+ // API Key ref(host probe 顺序 Cookie 优先,v1.5.4)。
673
+ const QUOTA_CRED_REFS = {
674
+ minimax: "MINIMAX_CN_API_KEY",
675
+ deepseek: "DEEPSEEK_API_KEY",
676
+ kimi: "KIMI_CODING_API_KEY",
677
+ openrouter: "OPENROUTER_API_KEY",
678
+ zhipu: "ZAI_CODING_CN_API_KEY",
679
+ mimo: "XIAOMI_MIMO_COOKIE",
680
+ };
651
681
  const QUOTA_PROVIDER_ORDER = ["minimax", "deepseek", "kimi", "openrouter", "zhipu", "mimo"];
652
682
 
653
683
  function quotaProviderLabel(p) {
@@ -785,7 +815,7 @@ window.__ModuleLoader__.load({
785
815
  try {
786
816
  const snap = directory.store.getSnapshot();
787
817
  const route = snap && snap.current && snap.current.provider;
788
- setProvider(route ? PROVIDER_ALIASES[route] || null : null);
818
+ setProvider(providerKeyForRoute(route));
789
819
  } catch (e) {
790
820
  setProvider(null);
791
821
  }
@@ -900,7 +930,8 @@ window.__ModuleLoader__.load({
900
930
  }
901
931
 
902
932
  // ---- Settings 页余额区块 ----------------------------------------------
903
- // 展示所有已配置 API Key 的 provider 余额卡片; 全部未配置时整块不渲染.
933
+ // 卡片 = 已配置的 provider;未配置或凭据失效(auth_failed)的 provider
934
+ // 在「凭据设置」行里粘贴凭据(remote.credentials.set → force 重拉)。
904
935
 
905
936
  function QuotaCard(props) {
906
937
  const p = props.provider;
@@ -968,6 +999,9 @@ window.__ModuleLoader__.load({
968
999
 
969
1000
  function QuotaSection() {
970
1001
  const [quotas, setQuotas] = React.useState(null);
1002
+ const [credDrafts, setCredDrafts] = React.useState({});
1003
+ const [credMsgs, setCredMsgs] = React.useState({});
1004
+ const [credSaving, setCredSaving] = React.useState({});
971
1005
  const load = React.useCallback(async (force) => {
972
1006
  try {
973
1007
  const payload = unwrapRemote(await remote.getAllQuotas(force === true));
@@ -982,16 +1016,46 @@ window.__ModuleLoader__.load({
982
1016
  return () => clearInterval(t);
983
1017
  }, [load]);
984
1018
 
1019
+ const saveCred = React.useCallback(async (p, value) => {
1020
+ const ref = QUOTA_CRED_REFS[p];
1021
+ const trimmed = typeof value === "string" ? value.trim() : "";
1022
+ if (!ref || !trimmed) return;
1023
+ setCredSaving((s) => ({ ...s, [p]: true }));
1024
+ try {
1025
+ // 官方 credentials 命名空间(settings-models 同款用法);不写进
1026
+ // inject 硬依赖,保存时才惰性取,取不到就提示错误。
1027
+ const ns = (ctx.remote && ctx.remote.credentials) || ctx.get("remote.credentials");
1028
+ if (!ns || typeof ns.set !== "function") throw new Error("credentials Remote 不可用");
1029
+ const r = await ns.set(ref, trimmed);
1030
+ if (r && r.ok) {
1031
+ setCredDrafts((d) => ({ ...d, [p]: "" }));
1032
+ setCredMsgs((m) => ({ ...m, [p]: "已保存,正在拉取…" }));
1033
+ await load(true);
1034
+ setCredMsgs((m) => ({ ...m, [p]: "已保存 ✓" }));
1035
+ } else {
1036
+ const msg = r && r.error && r.error.message ? r.error.message : "保存失败";
1037
+ setCredMsgs((m) => ({ ...m, [p]: msg }));
1038
+ }
1039
+ } catch (e) {
1040
+ setCredMsgs((m) => ({ ...m, [p]: e instanceof Error ? e.message : String(e) }));
1041
+ } finally {
1042
+ setCredSaving((s) => ({ ...s, [p]: false }));
1043
+ }
1044
+ }, [load]);
1045
+
985
1046
  if (!quotas) return null;
986
1047
  const names = QUOTA_PROVIDER_ORDER.filter((p) => quotas[p]);
987
- // 只展示已配置 API Key 的 provider; 全部未配置 → 整块不渲染.
1048
+ // 卡片 = 非 unconfigured;设置行 = unconfigured 或 auth_failed。
988
1049
  const configured = names.filter((p) => !(quotas[p] && quotas[p].ok === false && quotas[p].kind === "unconfigured"));
989
- if (configured.length === 0) return null;
1050
+ const needsCred = names.filter(
1051
+ (p) => quotas[p] && quotas[p].ok === false && (quotas[p].kind === "unconfigured" || quotas[p].kind === "auth_failed"),
1052
+ );
1053
+ if (configured.length === 0 && needsCred.length === 0) return null;
990
1054
  return React.createElement(
991
1055
  "div", { className: "ts-block" },
992
1056
  React.createElement(
993
1057
  "div", { className: "ts-block-title" },
994
- "套餐余额 · 已配置 provider",
1058
+ "套餐余额",
995
1059
  " ",
996
1060
  React.createElement(
997
1061
  "a",
@@ -1006,10 +1070,46 @@ window.__ModuleLoader__.load({
1006
1070
  "(刷新)",
1007
1071
  ),
1008
1072
  ),
1009
- React.createElement(
1010
- "div", { className: "ts-quota-list" },
1011
- configured.map((p) => React.createElement(QuotaCard, { key: p, provider: p, value: quotas[p] })),
1012
- ),
1073
+ configured.length > 0
1074
+ ? React.createElement(
1075
+ "div", { className: "ts-quota-list" },
1076
+ configured.map((p) => React.createElement(QuotaCard, { key: p, provider: p, value: quotas[p] })),
1077
+ )
1078
+ : null,
1079
+ needsCred.length > 0
1080
+ ? React.createElement(
1081
+ "div", { className: "ts-cred-block" },
1082
+ React.createElement("div", { className: "ts-block-title" }, "凭据设置(粘贴后保存,自动重新拉取)"),
1083
+ needsCred.map((p) => {
1084
+ const isAuthFailed = quotas[p] && quotas[p].kind === "auth_failed";
1085
+ return React.createElement(
1086
+ "div", { key: p, className: "ts-cred-row" },
1087
+ React.createElement("span", { className: "ts-cred-label" }, quotaProviderLabel(p)),
1088
+ React.createElement("input", {
1089
+ type: "password",
1090
+ className: "ts-cred-input",
1091
+ placeholder: p === "mimo"
1092
+ ? (isAuthFailed ? "凭据已失效:重新粘贴 Cookie 或 API Key" : "粘贴登录 Cookie 或 API Key")
1093
+ : "粘贴 API Key",
1094
+ value: credDrafts[p] || "",
1095
+ onChange: (e) => setCredDrafts((d) => ({ ...d, [p]: e.target.value })),
1096
+ onKeyDown: (e) => {
1097
+ if (e.key === "Enter") saveCred(p, credDrafts[p] || "");
1098
+ },
1099
+ }),
1100
+ React.createElement(
1101
+ "button", {
1102
+ className: "ts-cred-save",
1103
+ disabled: !!credSaving[p] || !(credDrafts[p] && credDrafts[p].trim()),
1104
+ onClick: () => saveCred(p, credDrafts[p] || ""),
1105
+ },
1106
+ credSaving[p] ? "保存中…" : "保存",
1107
+ ),
1108
+ credMsgs[p] ? React.createElement("span", { className: "ts-cred-msg" }, credMsgs[p]) : null,
1109
+ );
1110
+ }),
1111
+ )
1112
+ : null,
1013
1113
  );
1014
1114
  }
1015
1115
 
package/index.js CHANGED
@@ -146,10 +146,17 @@ const QUOTA_PROVIDERS = {
146
146
  // Bearer 实测 401(session 守护),可靠路径是登录 Cookie;单个凭证值先
147
147
  // Bearer 后 Cookie 自动重试(对齐 Musage xiaomi.rs 的 BearerThenCookie)。
148
148
  // 在 DSH 凭据里把浏览器 DevTools 复制的完整 Cookie 或 API Key 配到
149
- // XIAOMI_MIMO_API_KEY / XIAOMI_MIMO_COOKIE(或 MIMO_*)任一 ref。
149
+ // XIAOMI_MIMO_API_KEY / XIAOMI_MIMO_COOKIE(或 MIMO_*)任一 ref;route 派生
150
+ // 名(设置页按 route 生成的 XIAOMI_TOKEN_PLAN_CN_API_KEY/_COOKIE,与
151
+ // 其他 provider 的 MINIMAX_CN_* 同规则)同样识别。
152
+ // Cookie 类 ref 排最前:README 的可靠路径是 Cookie,且 Token 统计页「凭据
153
+ // 设置」写入 XIAOMI_MIMO_COOKIE——已存在但失效的 API Key(如 route 派生名
154
+ // 下的 tp- key 被 dashboard 401 拒)不能挡住用户新粘贴 Cookie(v1.5.4)。
150
155
  mimo: {
151
- refs: ["XIAOMI_MIMO_API_KEY", "XIAOMI_MIMO_COOKIE", "MIMO_API_KEY", "MIMO_COOKIE"],
156
+ refs: ["XIAOMI_MIMO_COOKIE", "XIAOMI_TOKEN_PLAN_CN_COOKIE", "XIAOMI_MIMO_API_KEY", "XIAOMI_TOKEN_PLAN_CN_API_KEY", "MIMO_COOKIE", "MIMO_API_KEY"],
152
157
  urls: {
158
+ XIAOMI_TOKEN_PLAN_CN_API_KEY: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
159
+ XIAOMI_TOKEN_PLAN_CN_COOKIE: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
153
160
  XIAOMI_MIMO_API_KEY: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
154
161
  XIAOMI_MIMO_COOKIE: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
155
162
  MIMO_API_KEY: "https://platform.xiaomimimo.com/api/v1/tokenPlan/usage",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duke-dsh-plugins/dsh-token-stats",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
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",