dsh-connect-minimaxcode 0.5.5 → 0.5.7

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
@@ -3,7 +3,7 @@
3
3
  把本机已登录的 **MiniMax Code 桌面 App(国内版)** 的模型接入 **DeepSeek Harness(DSH)**,装好即用,无需配置 API Key。
4
4
 
5
5
  ![license](https://img.shields.io/badge/license-MIT-blue.svg)
6
- [![version](https://img.shields.io/npm/v/dsh-connect-minimaxcode?color=cb3837)](https://www.npmjs.com/package/dsh-connect-minimaxcode)
6
+ [![version](https://img.shields.io/npm/v/dsh-connect-minimaxcode?color=cb3837)](https://www.npmjs.com/package/dsh-connect-minimaxcode)
7
7
  [![CI](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/actions/workflows/ci.yml/badge.svg)](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/actions/workflows/ci.yml)
8
8
 
9
9
  [English](README.en.md) | 简体中文
@@ -89,7 +89,7 @@ Web profile 把 `--profile desktop` 换成 `--profile web`。
89
89
 
90
90
  ```bash
91
91
  pnpm pack
92
- dsh plugin --profile desktop add ./dsh-connect-minimaxcode-0.5.5.tgz
92
+ dsh plugin --profile desktop add ./dsh-connect-minimaxcode-0.5.6.tgz
93
93
  ```
94
94
  > 桌面版的 profile 由 Electron 应用独占,若 `dsh` 命令不在 PATH 中,请使用 DSH 应用自带的载体 CLI(路径随安装位置而变,例如 `<DSH安装目录>\resources\runtime\cli\bin\dsh.cmd`)。
95
95
 
@@ -291,13 +291,17 @@ GET https://agent.minimax.cn/minimax-cloud/api/v1/credit/details?page_number=1&p
291
291
 
292
292
  ### 限额窗口(套餐的两个滚动窗口)
293
293
 
294
- 套餐用量按两个滚动窗口计量(当前套餐为 5 小时与 7 天),插件会显示各窗口的已用百分比与重置倒计时:
294
+ 套餐用量按两个滚动窗口计量(当前套餐为 5 小时与 7 天),插件会显示各窗口的已用百分比与重置倒计时。
295
+
296
+ 读取走**开放平台官方文档给出的接口**(M Plan 用量说明页「如何查看 M Plan 用量 → 方式三:使用 API 接口查询」):
295
297
 
296
298
  ```
297
- GET https://www.minimaxi.com/backend/account/token_plan/remains_percent?business=Agent&device_platform=desktop
298
- Authorization: Bearer <OAuth 令牌>
299
+ GET https://www.minimaxi.com/v1/token_plan/remains
300
+ Authorization: Bearer <API Key>
299
301
  ```
300
302
 
303
+ 该接口要的是**开放平台的 API 密钥**,不是 OAuth 令牌。密钥由账户自己持有,取法与桌面 App 相同 —— 用 OAuth 令牌请求 `GET /backend/account/token_plan_credit?business=Agent&device_platform=desktop`,响应里的 `api_key` 就是它。
304
+
301
305
  ```json
302
306
  "model_remains": [{
303
307
  "model_name": "general",
@@ -312,7 +316,7 @@ Authorization: Bearer <OAuth 令牌>
312
316
 
313
317
  - **窗口时长不写死**:从响应自身的起止字段推导(实测恰为 5 小时与 7 天),套餐条款变了显示会跟着变 —— 胶囊与详情行上的窗口名(`5h`、`7d`)也是这么来的,不是写死的字面量
314
318
  - **无限量的窗口不报百分比**:上游用 `current_*_status` 标出没有额度的窗口(桌面 App 读作 `isUnlimited: 3 === status`),这类窗口不参与显示,避免出现「0%」这种没人能据此行动的读数
315
- - **该接口在开放平台域**(`www.minimaxi.com`),只认 OAuth 令牌;JWT 打它返回 `1016 invalid api key`
319
+ - **没有订阅就没有窗口,且这不算错误**:上游对未订阅 M Plan 的账户明确回 `2062 no active token plan subscription`,插件会如实标为「无订阅」而不是「读取失败」—— 积分包本身不带滚动窗口
316
320
  - **接口异常时其余显示不受影响**:限额读不到,胶囊与卡片仍显示余额、签到与本机用量
317
321
  - 上游还发 `current_*_count` 计数字段(无计数制时为 `-1`),插件只读百分比与起止时刻
318
322
  ### 关于续期
@@ -340,6 +344,21 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
340
344
 
341
345
  ## 更新记录
342
346
 
347
+ ### 0.5.7(2026-11-10)
348
+
349
+ - **限额窗口改走官方文档接口**:原先读的是 `/backend/account/token_plan/remains_percent`,这条路径现在对任何账户都只回一个空的 `success`(连 `model_remains` 字段都没有),于是限额一栏对所有人都显示不出来,却看不出原因。改用开放平台官方文档(M Plan 用量说明页)给出的 `GET /v1/token_plan/remains`。新接口要求**开放平台 API 密钥**而非 OAuth 令牌,密钥取自 `token_plan_credit` 的响应并缓存复用。
350
+ - **「没有订阅」不再被报成「读取失败」**:上游对未订阅 M Plan 的账户明确回 `2062 no active token plan subscription` —— 积分包本身不带 5 小时 / 周滚动窗口。插件现在把这种情况标为「无订阅」,余额、签到与模型调用不受任何影响。
351
+ - 旧路径保留为**兜底**:账户若拿不到开放平台密钥,仍走旧路径,不会因此彻底没有限额显示。
352
+ - 顺带修正一处误伤可能:限额接口是拿开放平台密钥问的,它回 401 说明的是密钥不对,**不是本人凭据被吊销**;此前它参与「凭据是否被吊销」的判断,可能把人误登出。现在该判断只看账户域接口(会员信息与积分流水)的 401。
353
+ - 新增 `tests/quota-endpoint.spec.ts`(13 个用例)钉住:计划密钥的取法、官方接口用密钥而非 OAuth 令牌请求、`2062` 归为「无订阅」而非错误、无密钥时回落旧路径、密钥只取一次。全量 209 个用例通过。
354
+
355
+ ### 0.5.6(2026-11-09)
356
+
357
+ - **修复「模型请求全部失败,但积分/签到正常」**:这是两条凭证路径读的不是同一个文件。模型网关此前只认插件自登录与桌面 App 留下的**旧版 JWT**(`local-runtime.auth.json`),而新版 App 早已改用 OAuth 存储(`auth/prod/<region>/mcode-public/auth.json`,`mmoat_` 令牌)。旧 JWT 过期后网关一律回 `401 auth failed`,而积分走的是 OAuth 文件,于是出现「积分能看、模型不通」。现在网关依次尝试:插件自登录 → **桌面 App 的 OAuth 授权** → 旧版 JWT,三者取第一个可用的。
358
+ - **修复「令牌被上游吊销后永不重登」**:吊销不会改动本地记录的过期时间,而此前的续期判断**只看过期时间**,于是死令牌被一直使用,所有请求 401,界面却仍显示已登录、也没有重新登录入口。现在账户接口返回 `401` 会触发一次强制续期;续期被拒(`400`/`401`/`invalid_grant`,或凭据本身没有 refresh token)则删除该凭据、清空缓存状态,卡片改为提示重新登录。**续期的网络错误与 `5xx` 不算吊销**,不会因为上游一时抽风就把人登出。
359
+ - **修复状态卡把「可用」报成「已过期」**:卡片的登录态此前只读旧 JWT,因此网关明明能通也显示 `expired`。现在它描述的是**网关实际会用的那枚凭据**,并新增 `credential-revoked` / `credential-expired` 两种更贴切的提示文案(指向卡片上的登录按钮,而不是「去桌面 App 登录后重启 DSH」)。
360
+ - 新增 `tests/revoked-credential.spec.ts`(6 个用例)钉住:401 后凭据被删除且卡片转为未登录、被拒时给出「重新登录」提示、`503` 不会误删凭据、无桌面 App 时插件自登录显示为已登录、OAuth 授权优先于旧 JWT、状态文档不泄露任何令牌。全量 193 个用例通过。
361
+
343
362
  ### 0.5.5(2026-11-07)
344
363
 
345
364
  - **新增「限额窗口」显示**:读取开放平台的 `GET /backend/account/token_plan/remains_percent`(该接口来自桌面 App 每天真实调用的日志,同样未在官方文档中公开),展示套餐的**5 小时窗口**与**每周窗口**的已用百分比与重置倒计时。窗口时长从响应自身的起止字段推导,不写死 5 小时或 7 天——套餐条款变过不止一次,显示应跟随服务端所说。
package/lib/index.d.ts CHANGED
@@ -208,7 +208,12 @@ declare class MinimaxCatalog {
208
208
  declare const MINIMAX_OAUTH_CLIENT_ID = "mcode-public";
209
209
  /** Membership endpoint: carries the credit summary. */
210
210
  declare const MEMBERSHIP_PATH = "/matrix/api/v1/commerce/get_membership_info";
211
- /** Rate-limit window endpoint: the 5-hour and weekly usage the plan meters. */
211
+ /**
212
+ * Rate-limit window endpoint the plugin used first.
213
+ *
214
+ * Kept as a fallback for an account whose open platform hands out no plan
215
+ * key, since the documented path needs one.
216
+ */
212
217
  declare const REMAINS_PERCENT_PATH = "/backend/account/token_plan/remains_percent";
213
218
  /** The OAuth credential the account endpoints accept. */
214
219
  interface OAuthCredential {
@@ -371,7 +376,7 @@ interface RateLimitWindows {
371
376
  */
372
377
  declare function parseRateLimitWindows(document: unknown): RateLimitWindows | undefined;
373
378
  /**
374
- * Fetch the plan's two rate-limit windows.
379
+ * Fetch the plan's two rate-limit windows from the older open-platform path.
375
380
  *
376
381
  * The endpoint lives on the open-platform host, like the usage summary, and
377
382
  * answers the same OAuth credential the membership endpoint takes. The JWT the
@@ -380,6 +385,15 @@ declare function parseRateLimitWindows(document: unknown): RateLimitWindows | un
380
385
  * @returns The windows, or `undefined` when the response carries none.
381
386
  */
382
387
  declare function fetchRateLimitWindows(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch): Promise<RateLimitWindows | undefined>;
388
+ /** What a quota read found. */
389
+ interface RateLimitRead {
390
+ /** The windows, when the plan meters any. */
391
+ windows?: RateLimitWindows;
392
+ /** Why there are none, when the upstream said so plainly. */
393
+ absence?: 'no-subscription';
394
+ /** Why the read failed, when it did. */
395
+ error?: string;
396
+ }
383
397
  /** What the lots say about credits that are about to be lost. */
384
398
  interface ExpirySummary {
385
399
  /** Credits in lots that expire within {@link EXPIRING_WINDOW_DAYS} days. */
@@ -422,6 +436,8 @@ declare class MinimaxCredits {
422
436
  get rateLimit(): RateLimitWindows | undefined;
423
437
  /** Why the windows could not be read, when they could not. */
424
438
  get rateLimitError(): string | undefined;
439
+ /** Why the plan has no windows, when the upstream said so plainly. */
440
+ get rateLimitAbsence(): 'no-subscription' | undefined;
425
441
  /** When the windows were last read. */
426
442
  get rateLimitAtMs(): number | undefined;
427
443
  /**
@@ -451,6 +467,15 @@ declare class MinimaxCredits {
451
467
  * window that cannot be read leaves the last one on display.
452
468
  */
453
469
  refreshRateLimit(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<void>;
470
+ /**
471
+ * Read the windows from whichever endpoint can answer.
472
+ *
473
+ * The documented endpoint needs the open platform's plan key, which the
474
+ * same platform hands out; the key is remembered because it does not
475
+ * change from one read to the next. An account with no key at all still
476
+ * gets the older path's answer.
477
+ */
478
+ readWindows(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch): Promise<RateLimitRead>;
454
479
  }
455
480
  //#endregion
456
481
  //#region src/usage.d.ts
@@ -611,6 +636,8 @@ declare function pluginStatePath(env?: NodeJS.ProcessEnv): string;
611
636
  declare function readPluginCredential(env?: NodeJS.ProcessEnv): Promise<PluginCredential | undefined>;
612
637
  /** Write the plugin's own credential. */
613
638
  declare function writePluginCredential(credential: PluginCredential, env?: NodeJS.ProcessEnv): Promise<void>;
639
+ /** Remove the plugin's own credential, which is what signing out means. */
640
+ declare function deletePluginCredential(env?: NodeJS.ProcessEnv): Promise<void>;
614
641
  /** Read the plugin's own state. */
615
642
  declare function readPluginState(env?: NodeJS.ProcessEnv): Promise<PluginState>;
616
643
  /** Write the plugin's own state. */
@@ -908,6 +935,8 @@ interface RateLimitStatus {
908
935
  };
909
936
  /** When the windows were read, in milliseconds since the epoch. */
910
937
  updatedAtMs?: number;
938
+ /** Why the plan has no windows, when the upstream said so plainly. */
939
+ reason?: 'no-subscription';
911
940
  /** Why the windows could not be read. */
912
941
  error?: string;
913
942
  }
@@ -1037,7 +1066,6 @@ interface AdapterBundle {
1037
1066
  declare function createMinimaxCodeAdapter(options: AdapterOptions): AdapterBundle;
1038
1067
  //#endregion
1039
1068
  //#region src/index.d.ts
1040
- /** Loader entry name; also the id the bundle patch inserts. */
1041
1069
  declare const name = "dsh-connect-minimaxcode";
1042
1070
  /** Services this plugin needs before it can serve anything. */
1043
1071
  declare const inject: string[];
@@ -1052,4 +1080,4 @@ declare const inject: string[];
1052
1080
  */
1053
1081
  declare function apply(ctx: Context): void;
1054
1082
  //#endregion
1055
- export { type AuthState, type CatalogModel, type CatalogSource, type Credential, type CreditSummary, DEVICE_CODE_GRANT, DEVICE_CODE_PATH, type DeviceLoginState, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_ACTION_PATH, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, type MinimaxRegion, type ModelUsage, type OAuthCredential, type OAuthState, PLUGIN_CREDENTIAL_FILENAME, PLUGIN_STATE_FILENAME, type PluginCredential, type PluginState, REMAINS_PERCENT_PATH, type RateLimitWindows, SIGNIN_CLAIM_PATH, SIGNIN_CLAIM_RESULT, SIGNIN_CYCLE_DAYS, SIGNIN_DAY_STATUS, SIGNIN_PANEL_PATH, type SigninClaim, type SigninDay, type SigninPanel, type StatusDocument, type TokenClaims, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, claimSignin, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, dshHome, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, fetchRateLimitWindows, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parseRateLimitWindows, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };
1083
+ export { type AuthState, type CatalogModel, type CatalogSource, type Credential, type CreditSummary, DEVICE_CODE_GRANT, DEVICE_CODE_PATH, type DeviceLoginState, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_ACTION_PATH, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, type MinimaxRegion, type ModelUsage, type OAuthCredential, type OAuthState, PLUGIN_CREDENTIAL_FILENAME, PLUGIN_STATE_FILENAME, type PluginCredential, type PluginState, REMAINS_PERCENT_PATH, type RateLimitWindows, SIGNIN_CLAIM_PATH, SIGNIN_CLAIM_RESULT, SIGNIN_CYCLE_DAYS, SIGNIN_DAY_STATUS, SIGNIN_PANEL_PATH, type SigninClaim, type SigninDay, type SigninPanel, type StatusDocument, type TokenClaims, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, claimSignin, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, deletePluginCredential, dshHome, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, fetchRateLimitWindows, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parseRateLimitWindows, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };
package/lib/index.js CHANGED
@@ -693,7 +693,9 @@ const HINTS = {
693
693
  "app-missing": "未找到 MiniMax Code 的登录信息。请先安装并登录 MiniMax Code 桌面 App,然后重启 DSH。",
694
694
  "not-signed-in": "MiniMax Code 尚未登录。请在 MiniMax Code 桌面 App 中登录后重启 DSH。",
695
695
  unreadable: "登录信息文件无法读取。请确认 MiniMax Code 仍处于登录状态。",
696
- malformed: "登录令牌格式无法解析。请在 MiniMax Code 中重新登录后重启 DSH。"
696
+ malformed: "登录令牌格式无法解析。请在 MiniMax Code 中重新登录后重启 DSH。",
697
+ "credential-revoked": "登录凭据已被服务器撤销,模型请求无法通过。请点击下方登录按钮重新登录 MiniMax Code。",
698
+ "credential-expired": "登录凭据已过期。请点击下方登录按钮重新登录 MiniMax Code。"
697
699
  };
698
700
  /**
699
701
  * Build the card's status document.
@@ -721,7 +723,7 @@ function statusDocument(deps) {
721
723
  ...base,
722
724
  expiry: "expired",
723
725
  daysRemaining: 0,
724
- hint: "登录令牌已过期。请打开并登录 MiniMax Code 桌面 App 以获取新令牌,然后重启 DSH。"
726
+ hint: HINTS[auth.reason ?? ""] ?? "登录令牌已过期。请打开并登录 MiniMax Code 桌面 App 以获取新令牌,然后重启 DSH。"
725
727
  };
726
728
  if (auth.state === "signed-out") return {
727
729
  ...base,
@@ -791,7 +793,24 @@ const MINIMAX_OPEN_HOSTS = {
791
793
  const MEMBERSHIP_PATH = "/matrix/api/v1/commerce/get_membership_info";
792
794
  /** Credit ledger endpoint: per-grant amounts and expiry. */
793
795
  const CREDIT_DETAILS_PATH = "/minimax-cloud/api/v1/credit/details";
794
- /** Rate-limit window endpoint: the 5-hour and weekly usage the plan meters. */
796
+ /** Open-platform credit endpoint that carries the account's plan key. */
797
+ const TOKEN_PLAN_CREDIT_PATH = "/backend/account/token_plan_credit";
798
+ /**
799
+ * Rate-limit window endpoint, as the open platform documents it.
800
+ *
801
+ * The M Plan FAQ names `GET /v1/token_plan/remains` as the way to read the
802
+ * quota over HTTP. It asks for an API secret key rather than the desktop
803
+ * app's OAuth bearer, and it is the path the upstream still maintains: the
804
+ * older path this plugin started with now answers 200 with an empty body
805
+ * even for accounts that do have windows.
806
+ */
807
+ const TOKEN_PLAN_REMAINS_PATH = "/v1/token_plan/remains";
808
+ /**
809
+ * Rate-limit window endpoint the plugin used first.
810
+ *
811
+ * Kept as a fallback for an account whose open platform hands out no plan
812
+ * key, since the documented path needs one.
813
+ */
795
814
  const REMAINS_PERCENT_PATH = "/backend/account/token_plan/remains_percent";
796
815
  /** OAuth token endpoint. */
797
816
  const TOKEN_PATH$1 = "/oauth2/token";
@@ -1158,7 +1177,7 @@ function parsePercent(value) {
1158
1177
  return Number.isFinite(parsed) ? parsed : void 0;
1159
1178
  }
1160
1179
  /**
1161
- * Fetch the plan's two rate-limit windows.
1180
+ * Fetch the plan's two rate-limit windows from the older open-platform path.
1162
1181
  *
1163
1182
  * The endpoint lives on the open-platform host, like the usage summary, and
1164
1183
  * answers the same OAuth credential the membership endpoint takes. The JWT the
@@ -1175,6 +1194,61 @@ async function fetchRateLimitWindows(credential, region, fetchImpl = fetch) {
1175
1194
  return parseRateLimitWindows(await response.json());
1176
1195
  }
1177
1196
  /**
1197
+ * Read the open platform's plan key.
1198
+ *
1199
+ * The documented quota endpoint wants an API secret key, and the open
1200
+ * platform is where the account's key comes from: the desktop app asks the
1201
+ * same endpoint with its OAuth bearer and gets the key back in the answer,
1202
+ * so the key is a property of the account rather than something the person
1203
+ * has to configure.
1204
+ *
1205
+ * @returns The key, or `undefined` when the account has none.
1206
+ */
1207
+ async function fetchTokenPlanApiKey(credential, region, fetchImpl = fetch) {
1208
+ const response = await fetchImpl(`${MINIMAX_OPEN_HOSTS[region]}${TOKEN_PLAN_CREDIT_PATH}?business=Agent&device_platform=desktop`, { headers: {
1209
+ accept: "application/json",
1210
+ authorization: `Bearer ${credential.token}`
1211
+ } });
1212
+ if (!response.ok) throw new Error(`token plan credit request failed: HTTP ${response.status}`);
1213
+ const key = (await response.json()).api_key;
1214
+ return typeof key === "string" && key.length > 0 ? key : void 0;
1215
+ }
1216
+ /** The upstream's own code for "this account has no plan subscription". */
1217
+ const NO_SUBSCRIPTION_CODE = 2062;
1218
+ /** The upstream's own code for "the request carried no usable API key". */
1219
+ const MISSING_API_KEY_CODE = 1004;
1220
+ /** The upstream's status code inside a response body, when it carries one. */
1221
+ function readBaseRespCode(document) {
1222
+ const base = document.base_resp;
1223
+ if (typeof base !== "object" || base === null) return void 0;
1224
+ const code = base.status_code;
1225
+ return typeof code === "number" ? code : void 0;
1226
+ }
1227
+ /**
1228
+ * Read the windows from the documented quota endpoint.
1229
+ *
1230
+ * The upstream says plainly when there is nothing to report: `2062` means
1231
+ * the account has no plan subscription, and the quota does not exist for it
1232
+ * at all. That is a fact about the plan rather than a failure to read, so
1233
+ * it is reported as such and not as an error — the windows of a plan the
1234
+ * account does not hold are not missing data.
1235
+ */
1236
+ async function fetchTokenPlanWindows(apiKey, region, fetchImpl = fetch) {
1237
+ const response = await fetchImpl(`${MINIMAX_OPEN_HOSTS[region]}${TOKEN_PLAN_REMAINS_PATH}`, { headers: {
1238
+ accept: "application/json",
1239
+ authorization: `Bearer ${apiKey}`,
1240
+ "content-type": "application/json"
1241
+ } });
1242
+ if (!response.ok) throw new Error(`rate-limit window request failed: HTTP ${response.status}`);
1243
+ const document = await response.json();
1244
+ const code = readBaseRespCode(document);
1245
+ if (code === NO_SUBSCRIPTION_CODE) return { absence: "no-subscription" };
1246
+ if (code === MISSING_API_KEY_CODE) return { error: "the open platform did not accept the plan key" };
1247
+ const windows = parseRateLimitWindows(document);
1248
+ if (windows === void 0) return { error: "response carried no rate-limit windows" };
1249
+ return { windows };
1250
+ }
1251
+ /**
1178
1252
  * Summarize what is about to expire.
1179
1253
  *
1180
1254
  * Only lots that still hold credits count: a lot that was granted and used up
@@ -1213,7 +1287,9 @@ var MinimaxCredits = class {
1213
1287
  #ledgerError;
1214
1288
  #rateLimit;
1215
1289
  #rateLimitError;
1290
+ #rateLimitAbsence;
1216
1291
  #rateLimitAtMs;
1292
+ #apiKey;
1217
1293
  /** The balance as last read. */
1218
1294
  get snapshot() {
1219
1295
  return this.#snapshot;
@@ -1246,6 +1322,10 @@ var MinimaxCredits = class {
1246
1322
  get rateLimitError() {
1247
1323
  return this.#rateLimitError;
1248
1324
  }
1325
+ /** Why the plan has no windows, when the upstream said so plainly. */
1326
+ get rateLimitAbsence() {
1327
+ return this.#rateLimitAbsence;
1328
+ }
1249
1329
  /** When the windows were last read. */
1250
1330
  get rateLimitAtMs() {
1251
1331
  return this.#rateLimitAtMs;
@@ -1309,14 +1389,36 @@ var MinimaxCredits = class {
1309
1389
  */
1310
1390
  async refreshRateLimit(credential, region, fetchImpl = fetch, now = Date.now()) {
1311
1391
  try {
1312
- const windows = await fetchRateLimitWindows(credential, region, fetchImpl);
1313
- this.#rateLimit = windows;
1314
- this.#rateLimitError = windows === void 0 ? "response carried no rate-limit windows" : void 0;
1392
+ const read = await this.readWindows(credential, region, fetchImpl);
1393
+ if (read.error === void 0) this.#rateLimit = read.windows;
1394
+ this.#rateLimitAbsence = read.absence;
1395
+ this.#rateLimitError = read.error;
1315
1396
  this.#rateLimitAtMs = now;
1316
1397
  } catch (error) {
1317
1398
  this.#rateLimitError = error instanceof Error ? error.message : String(error);
1318
1399
  }
1319
1400
  }
1401
+ /**
1402
+ * Read the windows from whichever endpoint can answer.
1403
+ *
1404
+ * The documented endpoint needs the open platform's plan key, which the
1405
+ * same platform hands out; the key is remembered because it does not
1406
+ * change from one read to the next. An account with no key at all still
1407
+ * gets the older path's answer.
1408
+ */
1409
+ async readWindows(credential, region, fetchImpl = fetch) {
1410
+ const apiKey = this.#apiKey ?? await fetchTokenPlanApiKey(credential, region, fetchImpl);
1411
+ this.#apiKey = apiKey;
1412
+ if (apiKey !== void 0) try {
1413
+ return await fetchTokenPlanWindows(apiKey, region, fetchImpl);
1414
+ } catch (error) {
1415
+ const legacy = await fetchRateLimitWindows(credential, region, fetchImpl);
1416
+ if (legacy !== void 0) return { windows: legacy };
1417
+ throw error;
1418
+ }
1419
+ const legacy = await fetchRateLimitWindows(credential, region, fetchImpl);
1420
+ return legacy === void 0 ? { error: "response carried no rate-limit windows" } : { windows: legacy };
1421
+ }
1320
1422
  };
1321
1423
  //#endregion
1322
1424
  //#region src/credentials.ts
@@ -1405,6 +1507,11 @@ async function readPluginCredential(env = process.env) {
1405
1507
  async function writePluginCredential(credential, env = process.env) {
1406
1508
  await writePrivate(pluginCredentialPath(env), credential);
1407
1509
  }
1510
+ /** Remove the plugin's own credential, which is what signing out means. */
1511
+ async function deletePluginCredential(env = process.env) {
1512
+ const { rm } = await import("node:fs/promises");
1513
+ await rm(pluginCredentialPath(env), { force: true });
1514
+ }
1408
1515
  /** Read the plugin's own state. */
1409
1516
  async function readPluginState(env = process.env) {
1410
1517
  let text;
@@ -2012,6 +2119,25 @@ var UsageMeter = class {
2012
2119
  //#endregion
2013
2120
  //#region src/index.ts
2014
2121
  /** Loader entry name; also the id the bundle patch inserts. */
2122
+ /**
2123
+ * Whether a message says the server refused the credential itself.
2124
+ *
2125
+ * The account and catalog endpoints answer a revoked or unknown bearer with
2126
+ * `401`, and the only place that surfaces is the message the request layer
2127
+ * built. Matching the status code rather than the wording keeps this working
2128
+ * whatever language the message is written in.
2129
+ */
2130
+ const isUnauthorized = (message) => message !== void 0 && /\bHTTP 401\b/.test(message);
2131
+ /**
2132
+ * Whether a failed refresh means the credential can never be used again.
2133
+ *
2134
+ * The authorization server answers a revoked or already-spent refresh token
2135
+ * with `400`/`401`, and a credential that carries no refresh token can never
2136
+ * be renewed at all. Both are final. A `5xx` or a dropped connection is not:
2137
+ * throwing the credential away over a bad minute upstream would sign the
2138
+ * person out for nothing.
2139
+ */
2140
+ const isRefreshRefused = (message) => message.includes("没有可用于续期的 refresh token") || /\bHTTP (400|401)\b/.test(message);
2015
2141
  const name = "dsh-connect-minimaxcode";
2016
2142
  /** Services this plugin needs before it can serve anything. */
2017
2143
  const inject = ["llm"];
@@ -2072,14 +2198,50 @@ function apply(ctx) {
2072
2198
  let loginSource;
2073
2199
  /** Whether a device login is waiting to be authorized. */
2074
2200
  let loginPending = false;
2201
+ /** Whether the server refused the plugin's own credential. */
2202
+ let revokedByServer = false;
2203
+ /**
2204
+ * Give up on the plugin's own credential after the server refused it.
2205
+ *
2206
+ * A revoked token keeps its expiry date, so every later call would send the
2207
+ * same dead token and fail the same way. The only way back is a new sign-in,
2208
+ * so the credential is deleted, the cached views of the account it belonged
2209
+ * to are dropped, and the card is left asking for one.
2210
+ */
2211
+ const revokePluginCredential = async () => {
2212
+ await deletePluginCredential();
2213
+ if (pluginState.deviceLogin !== void 0) {
2214
+ pluginState = {
2215
+ ...pluginState,
2216
+ deviceLogin: void 0
2217
+ };
2218
+ await writePluginState(pluginState);
2219
+ }
2220
+ revokedByServer = true;
2221
+ modelToken = void 0;
2222
+ loginSource = void 0;
2223
+ loginPending = false;
2224
+ creditState = "signed-out";
2225
+ signinPanel = void 0;
2226
+ signinClaim = void 0;
2227
+ signinError = void 0;
2228
+ catalog.invalidate();
2229
+ lastCreditAtMs = Date.now();
2230
+ lastSigninAtMs = Date.now();
2231
+ lastCatalogAtMs = 0;
2232
+ };
2075
2233
  /**
2076
2234
  * The credential the model gateway is called with.
2077
2235
  *
2078
2236
  * The plugin's own sign-in comes first: it is the credential the person
2079
2237
  * chose, and on a machine where the desktop app was never installed it is
2080
- * the only one there is. The desktop app's token is the fallback, and the
2081
- * two are interchangeable as far as the gateway is concerned — it accepts
2082
- * either bearer.
2238
+ * the only one there is.
2239
+ *
2240
+ * The desktop app's OAuth grant is next, and it is not a mere fallback: it
2241
+ * is the credential a current install actually holds. Its `mmoat_` bearer is
2242
+ * what the gateway accepts, while the legacy JWT older installs left behind
2243
+ * is refused with `401 auth failed` — so the OAuth file has to be consulted
2244
+ * before that JWT, or a working machine looks broken.
2083
2245
  */
2084
2246
  const resolveGatewayCredential = async () => {
2085
2247
  const own = await readPluginCredential();
@@ -2089,11 +2251,18 @@ function apply(ctx) {
2089
2251
  await writePluginCredential(next);
2090
2252
  loginSource = "plugin";
2091
2253
  modelToken = next.accessToken;
2254
+ revokedByServer = false;
2092
2255
  return {
2093
2256
  token: next.accessToken,
2094
2257
  claims: { exp: Math.floor((next.expiresAtMs ?? Date.now()) / 1e3) }
2095
2258
  };
2096
- } catch {}
2259
+ } catch (error) {
2260
+ const message = error instanceof Error ? error.message : String(error);
2261
+ if (isRefreshRefused(message)) {
2262
+ await revokePluginCredential();
2263
+ return;
2264
+ }
2265
+ }
2097
2266
  else {
2098
2267
  loginSource = "plugin";
2099
2268
  modelToken = own.accessToken;
@@ -2103,6 +2272,15 @@ function apply(ctx) {
2103
2272
  };
2104
2273
  }
2105
2274
  }
2275
+ const oauth = await ensureOAuthCredential(region, process.env);
2276
+ if (oauth.state === "signed-in") {
2277
+ loginSource = "desktop-app";
2278
+ modelToken = oauth.credential.token;
2279
+ return {
2280
+ token: oauth.credential.token,
2281
+ claims: { exp: Math.floor((oauth.credential.expiresAtMs ?? Date.now()) / 1e3) }
2282
+ };
2283
+ }
2106
2284
  const state = await readCredential();
2107
2285
  if (state.state !== "signed-in") {
2108
2286
  loginSource = void 0;
@@ -2130,7 +2308,15 @@ function apply(ctx) {
2130
2308
  state: "signed-in",
2131
2309
  credential: toOAuthCredential(next)
2132
2310
  };
2133
- } catch {
2311
+ } catch (error) {
2312
+ const message = error instanceof Error ? error.message : String(error);
2313
+ if (isRefreshRefused(message)) {
2314
+ await revokePluginCredential();
2315
+ return {
2316
+ state: "signed-out",
2317
+ reason: "not-signed-in"
2318
+ };
2319
+ }
2134
2320
  return {
2135
2321
  state: "expired",
2136
2322
  credential: toOAuthCredential(own)
@@ -2185,6 +2371,11 @@ function apply(ctx) {
2185
2371
  }
2186
2372
  await credits.refresh(state.credential, region, fetch, now);
2187
2373
  creditState = credits.snapshot === void 0 ? "unavailable" : "ok";
2374
+ if ([credits.error, credits.ledgerError].find((message) => isUnauthorized(message)) !== void 0) {
2375
+ await revokePluginCredential();
2376
+ return;
2377
+ }
2378
+ if (credits.snapshot !== void 0) revokedByServer = false;
2188
2379
  };
2189
2380
  /**
2190
2381
  * Read the check-in cycle, and claim the day's grant when asked to.
@@ -2210,7 +2401,12 @@ function apply(ctx) {
2210
2401
  signinPanel = panel;
2211
2402
  signinError = void 0;
2212
2403
  } catch (error) {
2213
- signinError = error instanceof Error ? error.message : String(error);
2404
+ const message = error instanceof Error ? error.message : String(error);
2405
+ if (isUnauthorized(message)) {
2406
+ await revokePluginCredential();
2407
+ return;
2408
+ }
2409
+ signinError = message;
2214
2410
  return;
2215
2411
  }
2216
2412
  if (!pluginState.autoCheckin || panel === void 0) return;
@@ -2241,7 +2437,12 @@ function apply(ctx) {
2241
2437
  refreshCredits();
2242
2438
  return claim;
2243
2439
  } catch (error) {
2244
- signinError = error instanceof Error ? error.message : String(error);
2440
+ const message = error instanceof Error ? error.message : String(error);
2441
+ if (isUnauthorized(message)) {
2442
+ await revokePluginCredential();
2443
+ return;
2444
+ }
2445
+ signinError = message;
2245
2446
  return;
2246
2447
  }
2247
2448
  };
@@ -2277,6 +2478,7 @@ function apply(ctx) {
2277
2478
  const windows = credits.rateLimit;
2278
2479
  if (windows === void 0) return {
2279
2480
  state: "unavailable",
2481
+ ...credits.rateLimitAbsence === void 0 ? {} : { reason: credits.rateLimitAbsence },
2280
2482
  ...credits.rateLimitError === void 0 ? {} : { error: credits.rateLimitError }
2281
2483
  };
2282
2484
  return {
@@ -2363,21 +2565,47 @@ function apply(ctx) {
2363
2565
  }
2364
2566
  });
2365
2567
  ctx.llm.registerAdapter(["minimaxcode"], adapter);
2366
- /** Build the document the card renders, without ever exposing the token. */
2367
- const buildStatus = async () => {
2568
+ /**
2569
+ * The sign-in state the card reports.
2570
+ *
2571
+ * It describes the credential the gateway will actually be called with, not
2572
+ * a particular file on disk: the card exists to answer "will a request
2573
+ * work", and a token the server refused is not a sign-in however unexpired
2574
+ * it looks. Reading the legacy JWT alone is what made a machine whose model
2575
+ * gateway was working report a signed-out, expired card.
2576
+ */
2577
+ const buildAuthStatus = async () => {
2578
+ if (revokedByServer) return {
2579
+ state: "signed-out",
2580
+ reason: "credential-revoked"
2581
+ };
2582
+ const current = await readNow();
2583
+ if (current !== void 0) {
2584
+ const remaining = expiresInMs(current.claims);
2585
+ if (remaining <= 0) return {
2586
+ state: "expired",
2587
+ reason: "credential-expired"
2588
+ };
2589
+ return {
2590
+ state: "signed-in",
2591
+ daysRemaining: Math.ceil(remaining / 864e5),
2592
+ expiry: expiryLevel(current.claims)
2593
+ };
2594
+ }
2368
2595
  const state = await readCredential();
2369
- const auth = state.state === "signed-in" ? {
2370
- state: "signed-in",
2371
- daysRemaining: Math.ceil(expiresInMs(state.credential.claims) / 864e5),
2372
- expiry: expiryLevel(state.credential.claims)
2373
- } : state.state === "expired" ? { state: "expired" } : {
2596
+ if (state.state === "expired") return { state: "expired" };
2597
+ if (state.state === "signed-out") return {
2374
2598
  state: "signed-out",
2375
2599
  reason: state.reason
2376
2600
  };
2601
+ return { state: "signed-in" };
2602
+ };
2603
+ /** Build the document the card renders, without ever exposing the token. */
2604
+ const buildStatus = async () => {
2377
2605
  await refreshCredits();
2378
2606
  await refreshSignin();
2379
2607
  return statusDocument({
2380
- auth,
2608
+ auth: await buildAuthStatus(),
2381
2609
  region,
2382
2610
  catalogSource: catalog.source,
2383
2611
  catalogError: catalog.error,
@@ -2465,6 +2693,7 @@ function apply(ctx) {
2465
2693
  };
2466
2694
  await writePluginState(pluginState);
2467
2695
  loginPending = false;
2696
+ revokedByServer = false;
2468
2697
  loginSource = "plugin";
2469
2698
  modelToken = result.credential.accessToken;
2470
2699
  catalog.invalidate();
@@ -2582,4 +2811,4 @@ async function readRequestBody(req) {
2582
2811
  return text;
2583
2812
  }
2584
2813
  //#endregion
2585
- export { DEVICE_CODE_GRANT, DEVICE_CODE_PATH, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_ACTION_PATH, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, PLUGIN_CREDENTIAL_FILENAME, PLUGIN_STATE_FILENAME, REMAINS_PERCENT_PATH, SIGNIN_CLAIM_PATH, SIGNIN_CLAIM_RESULT, SIGNIN_CYCLE_DAYS, SIGNIN_DAY_STATUS, SIGNIN_PANEL_PATH, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, claimSignin, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, dshHome, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, fetchRateLimitWindows, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parseRateLimitWindows, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };
2814
+ export { DEVICE_CODE_GRANT, DEVICE_CODE_PATH, EMPTY_USAGE, FALLBACK_MODELS, MEMBERSHIP_PATH, MINIMAXCODE_PROVIDER_ID, MINIMAX_ACTION_PATH, MINIMAX_AUTH_FILENAME, MINIMAX_DATA_DIR, MINIMAX_GATEWAYS, MINIMAX_OAUTH_CLIENT_ID, MINIMAX_REGIONS, MINIMAX_STATUS_PATH, MINIMAX_STREAM_IDLE_TIMEOUT_MS, MODEL_PRICES, MinimaxCatalog, MinimaxCredits, PLUGIN_CREDENTIAL_FILENAME, PLUGIN_STATE_FILENAME, REMAINS_PERCENT_PATH, SIGNIN_CLAIM_PATH, SIGNIN_CLAIM_RESULT, SIGNIN_CYCLE_DAYS, SIGNIN_DAY_STATUS, SIGNIN_PANEL_PATH, UsageMeter, apply, authPath, bearerToken, catalogUrl, chatBaseUrl, claimSignin, createMinimaxCodeAdapter, dataDir, daysRemaining, decodeClaims, deletePluginCredential, dshHome, ensureOAuthCredential, estimateCny, expiresInMs, expiryLevel, fetchCreditSummary, fetchRateLimitWindows, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parseRateLimitWindows, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-minimaxcode",
3
3
  "displayName": "DSH Connect MiniMax Code",
4
4
  "description": "把本机已登录的 MiniMax Code 桌面 App 模型接入 DeepSeek Harness:零配置、Anthropic Messages 兼容、动态模型目录,并显示积分余额与到期 — bring MiniMax Code desktop app models into DSH with a dynamic model catalog and a credit balance with expiry details.",
5
- "version": "0.5.5",
5
+ "version": "0.5.7",
6
6
  "author": "JeffTsuiCUG",
7
7
  "contributors": [
8
8
  {