dsh-connect-minimaxcode 0.6.3 → 0.6.5

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.en.md CHANGED
@@ -384,6 +384,18 @@ The OAuth token is valid for about an hour and is refreshed automatically by the
384
384
 
385
385
  ## Changelog
386
386
 
387
+ ### 0.6.5 (2026-11-12)
388
+
389
+ - **Fixed "the limit windows never showed up"**: once a plan is held, the upstream changed the quota endpoint's **field names and its meaning** — from the share *used* (the string `"19%"`) to the share *remaining* (the number `90`). The plugin still read the old names, so the windows either could not be read at all or read as the opposite. Both shapes are accepted now, and what leaves the plugin is always the share *used*: 92% remaining is 8% used. The window length is still derived from the answer's own edges (measured: five hours and seven days) rather than assumed.
390
+ - **Added the plan to the card**: the membership answer now names the tier and the day it ends (`token_plan_tier`, `token_plan_expires_at`), and the plugin shows one more line in the pill detail and on the plugin page — "Go · until 2026-12-11". It comes from the same answer as the balance, so it costs no extra request.
391
+ - **An account without a plan sees exactly what it saw before**: the plan fields are simply absent there, so the plugin shows nothing about a plan; the windows are still absent, and the card still says "No M Plan subscription (credits only), so there are no usage windows". The new `tests/free-account.spec.ts` pins that whole path for a free account (balance, plan, windows), and `tests/plan.spec.ts` covers the new field shape, the old one and the free account.
392
+
393
+ ### 0.6.4 (2026-11-11)
394
+
395
+ - **Fixed "the self-check report never appeared"**: the previous version put the self-check in the client half (the card reports what the host offered), but **the server half was never landed** — the status document had no field to carry the report and no route to receive it, so the card reported into nowhere. It is complete now: the report is received by the plugin's own route and appears in the status document as `capability`.
396
+ - The episode also exposed a process problem: a few anchors in the patch script missed, and its output was covered by another script in the same command, so a partial application was read as a complete one. **Every patch is now checked individually**, and an end-to-end check was added: the report counts as working only once it has travelled from the card through the route and back into the status document.
397
+ - The route-count cases were brought up to date (the host now mounts four routes: status, action, capability, generation).
398
+
387
399
  ### 0.6.3 (2026-11-11)
388
400
 
389
401
  - **"Could not send it to the conversation" now names the missing piece**: delivering a picture needs several pieces of the host — the session identifier, the sessions service, the session binding, a way to send or to hold a draft, and the input box's attachment channel — and they arrive in different ways. There used to be one blanket failure message; each absence now has its own: the host named no session / offers no sessions service / this session is not reachable / it cannot send / it cannot hold drafts / the input box takes no attachments.
package/README.md CHANGED
@@ -395,6 +395,18 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
395
395
 
396
396
  ## 更新记录
397
397
 
398
+ ### 0.6.5(2026-11-12)
399
+
400
+ - **修复「限额窗口一直显示不出来」**:上游在有了套餐之后改了额度接口的**字段名和语义** —— 从「已用百分比」(字符串 `"19%"`)改成「**剩余**百分比」(数字 `90`)。插件仍按旧字段名读,于是窗口读不到,或者读成相反的意思。现在两种形态都认,且对外一律是「已用百分比」:读到剩余 92% 就是已用 8%。窗口长度仍从响应自身的起止时刻推导(实测 5 小时与 7 天),不写死。
401
+ - **新增套餐显示**:会员接口现在会报出档位与到期日(`token_plan_tier`、`token_plan_expires_at`),插件在胶囊详情与插件页多显示一行,例如「Go · 2026-12-11 到期」。这与余额读的是同一个响应,不额外发请求。
402
+ - **免费用户看到的完全不变**:没有套餐的账户,套餐字段根本不存在,插件也就不显示任何套餐信息;窗口依然读不到,卡片上那句「未订阅 M Plan(仅积分包),因此没有限额窗口」照旧。新增 `tests/free-account.spec.ts` 把免费账户的整条路径钉住(余额、套餐、窗口三样),另加 `tests/plan.spec.ts` 覆盖新字段形态、旧字段形态与免费账户三种情况。
403
+
404
+ ### 0.6.4(2026-11-11)
405
+
406
+ - **修复「自检报告从未出现」**:上一版把自检能力加在了客户端(卡片上报宿主提供了什么),但**服务端那一半没有落地** —— 状态文档里没有承载报告的字段,也没有接收报告的路由,于是卡片上报到哪里都无人接收。现已补齐:报告由插件自己的路由接收,并出现在状态文档的 `capability` 里。
407
+ - 这次同时暴露了一个流程问题:补丁脚本里有几处锚点未命中,而脚本的输出被同一条命令里的另一个脚本盖住了,于是「部分应用」被当成了「全部应用」。**现在每一处补丁的成败都单独核对**,并且加了端到端验证:报告从卡片出发、经路由回到状态文档,全程贯通才算通过。
408
+ - 顺带补上路由数量的用例(宿主侧现在有四条路由:状态、动作、自检、生成)。
409
+
398
410
  ### 0.6.3(2026-11-11)
399
411
 
400
412
  - **「无法发送到对话」现在会说明缺哪一步**:把图片送进对话需要宿主的好几块能力(会话标识、会话服务、会话绑定、发送或草稿通道、输入框的附件通道),它们分别以不同方式交到插件手上。原先只有一句笼统的失败提示,现在每种缺失都有各自的话:宿主没给会话标识 / 没有会话服务 / 当前会话不可达 / 会话不支持发送 / 不支持草稿 / 输入框不接受附件。
package/lib/client.js CHANGED
@@ -77,6 +77,8 @@
77
77
  usageRanking: '用量排名前 {percent}%',
78
78
  usageBestDay: '最活跃的一天 {date}',
79
79
  planNoSubscription: '未订阅 M Plan(仅积分包),因此没有限额窗口',
80
+ planGeneric: 'M Plan 订阅',
81
+ planExpires: '{date} 到期',
80
82
  generateTitle: '生成',
81
83
  generateImage: '图片',
82
84
  generateVideo: '视频',
@@ -155,6 +157,8 @@
155
157
  usageRanking: 'Top {percent}% by usage',
156
158
  usageBestDay: 'Busiest day {date}',
157
159
  planNoSubscription: 'No M Plan subscription (credits only), so there are no usage windows',
160
+ planGeneric: 'M Plan subscription',
161
+ planExpires: 'until {date}',
158
162
  generateTitle: 'Generate',
159
163
  generateImage: 'Image',
160
164
  generateVideo: 'Video',
@@ -383,6 +387,23 @@
383
387
  return null;
384
388
  return h('div', { style: mutedStyle, 'data-minimax-plan-notice': true }, t('planNoSubscription'));
385
389
  }
390
+ /**
391
+ * The plan the account holds, when it holds one.
392
+ *
393
+ * An account without a plan has nothing to say here, and the card shows
394
+ * it nothing: the rolling windows it does not have are already
395
+ * explained by the notice beside them. What this adds is for the
396
+ * accounts that do hold one — the tier's name and the day it ends.
397
+ */
398
+ function planLine(t, plan) {
399
+ if (plan === undefined || !plan.active)
400
+ return null;
401
+ const tier = plan.tier ?? t('planGeneric');
402
+ const expiry = plan.expiresAtMs === undefined
403
+ ? null
404
+ : t('planExpires', { date: day(plan.expiresAtMs) });
405
+ return h('div', { style: rowStyle, 'data-minimax-plan': true }, h('span', null, tier), h('span', null, expiry ?? ''));
406
+ }
386
407
  /** The pill itself; the gate decides whether it is drawn at all. */
387
408
  function CreditPill(props) {
388
409
  const [status] = useStatus();
@@ -415,7 +436,7 @@
415
436
  const detail = available
416
437
  ? h('div', { style: detailStyle, 'data-minimax-credits-detail': true }, row(t('free'), credits(credits_.free, 3)), row(t('purchased'), credits(credits_.purchased, 3)), credits_.consumed !== undefined && credits_.consumed > 0
417
438
  ? row(t('consumed'), credits(credits_.consumed, 3))
418
- : null, expiryBlock(t, credits_), rateLimitBlock(t, status?.rateLimit), planNotice(t, status?.rateLimit), usageProfileBlock(t, status?.usageProfile), usage !== undefined
439
+ : null, expiryBlock(t, credits_), rateLimitBlock(t, status?.rateLimit), planNotice(t, status?.rateLimit), planLine(t, status?.plan), usageProfileBlock(t, status?.usageProfile), usage !== undefined
419
440
  ? h('div', { style: rowStyle }, h('span', null, t('tokens', { count: tokens(usage.inputTokens + usage.outputTokens + usage.cacheReadTokens) })), h('span', null, t('cost', { amount: money(usage.estimatedCny) })))
420
441
  : null, credits_.updatedAtMs !== undefined
421
442
  ? row(t('updated', { time: new Date(credits_.updatedAtMs).toLocaleTimeString() }), '')
@@ -913,7 +934,7 @@
913
934
  // usable even when the balance cannot be read.
914
935
  h(GenerationPanel, props));
915
936
  }
916
- return h('div', { style: pageStyle, 'data-minimax-page': true }, h('div', { style: cardStyle, 'data-minimax-credits-card': true }, h('div', { style: cardTitleStyle }, t('title')), h('div', { style: bigNumberStyle }, t('balance', { total: credits(credits_.total, 2) })), row(t('free'), credits(credits_.free, 3)), row(t('purchased'), credits(credits_.purchased, 3)), expiryBlock(t, credits_), rateLimitBlock(t, status?.rateLimit), planNotice(t, status?.rateLimit), signinBlock(t, signin, refresh), h('div', { style: sectionTitleStyle }, t('lots')), (credits_.upcoming ?? []).length === 0
937
+ return h('div', { style: pageStyle, 'data-minimax-page': true }, h('div', { style: cardStyle, 'data-minimax-credits-card': true }, h('div', { style: cardTitleStyle }, t('title')), h('div', { style: bigNumberStyle }, t('balance', { total: credits(credits_.total, 2) })), row(t('free'), credits(credits_.free, 3)), row(t('purchased'), credits(credits_.purchased, 3)), expiryBlock(t, credits_), rateLimitBlock(t, status?.rateLimit), planNotice(t, status?.rateLimit), planLine(t, status?.plan), signinBlock(t, signin, refresh), h('div', { style: sectionTitleStyle }, t('lots')), (credits_.upcoming ?? []).length === 0
917
938
  ? h('div', { style: mutedStyle }, t('noLots'))
918
939
  : h('ul', { style: listStyle }, ...(credits_.upcoming ?? []).map((lot, index) => h('li', { key: `${lot.expireAtMs}-${index}`, style: rowStyle }, h('span', null, day(lot.expireAtMs)), h('span', null, credits(lot.amount, 2))))), usage !== undefined
919
940
  ? h('div', { style: usageBoxStyle }, row(t('tokens', { count: tokens(usage.inputTokens + usage.outputTokens + usage.cacheReadTokens) }), ''), row(t('cost', { amount: money(usage.estimatedCny) }), ''), usage.requests > 0 ? row(t('requests', { count: usage.requests }), usage.lastModel ?? '') : null)
package/lib/index.d.ts CHANGED
@@ -320,6 +320,32 @@ interface CreditSummary {
320
320
  /** Remaining free (granted) credits. */
321
321
  free: number;
322
322
  }
323
+ /**
324
+ * The plan the account subscribes to.
325
+ *
326
+ * An account without one still reads a balance — it simply has no rolling
327
+ * windows, which is what the limit readout used to report as missing. With a
328
+ * plan the upstream names the tier and the day it ends, and both are worth
329
+ * showing: the tier says what the account has, the day says how long it has.
330
+ */
331
+ interface PlanSummary {
332
+ /** The tier's name, as the upstream spells it ("Go"). */
333
+ tier?: string;
334
+ /** When the plan ends, in milliseconds since the epoch. */
335
+ expiresAtMs?: number;
336
+ /** Whether the upstream reports an active plan at all. */
337
+ active: boolean;
338
+ }
339
+ /**
340
+ * Read the plan out of a membership response.
341
+ *
342
+ * The upstream only fills these in for an account that subscribes: without a
343
+ * plan `has_token_plan` is false and the tier is absent. `subscription_type`
344
+ * says so too — "mplan" once a plan is held, "none" before.
345
+ *
346
+ * @returns The plan, or `undefined` when the response carries no plan fields.
347
+ */
348
+ declare function parsePlanSummary(document: unknown): PlanSummary | undefined;
323
349
  /**
324
350
  * Read the credit summary out of a membership response.
325
351
  *
@@ -327,6 +353,14 @@ interface CreditSummary {
327
353
  * balance fields, so the same fallback chain is used here.
328
354
  */
329
355
  declare function parseCreditSummary(document: unknown): CreditSummary | undefined;
356
+ /**
357
+ * Fetch the membership document.
358
+ *
359
+ * One answer carries several things: the credit balance, the plan the
360
+ * account subscribes to, and when that plan ends. They are read together so
361
+ * that the account is asked once.
362
+ */
363
+ declare function fetchMembershipDocument(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch): Promise<Record<string, unknown> | undefined>;
330
364
  /**
331
365
  * Fetch the current credit balance.
332
366
  *
@@ -387,19 +421,24 @@ interface RateLimitWindows {
387
421
  weekly: UsageWindow;
388
422
  }
389
423
  /**
390
- * Read the two rate-limit windows out of a remains-percent response.
424
+ * Read the two rate-limit windows out of a remains response.
391
425
  *
392
426
  * The upstream answers with one entry per metered group; the general one is
393
427
  * the only one the plan meters today, and the first entry is used when it is
394
- * absent. The percents arrive as strings ("19%"), and the counts arrive as -1
395
- * when the plan does not meter by count, so only the percents and the window
396
- * edges are read. The window length is derived from the entry's own endpoints
397
- * rather than assumed to be five hours or seven days — the plan's terms have
428
+ * absent. The window length is derived from the entry's own endpoints rather
429
+ * than assumed to be five hours or seven days — the plan's terms have
398
430
  * changed before, and the display should follow whatever the server says.
399
431
  *
400
432
  * A window the server marks as unlimited (`current_*_status` of 3) carries no
401
433
  * allowance to be a percentage of, so it produces no window at all: reporting
402
434
  * "0%" of an unlimited quota would be a number nobody can act on.
435
+ *
436
+ * **Which share the upstream reports changed.** It used to answer with the
437
+ * share already used, as a string ("19%"); with a plan subscribed it answers
438
+ * with the share still *remaining*, as a plain number (90). Both are
439
+ * accepted, and the two are opposites, so what leaves this function is
440
+ * always the share used — otherwise the same window would read 90% used on
441
+ * a plan that has barely spent anything.
403
442
  */
404
443
  declare function parseRateLimitWindows(document: unknown): RateLimitWindows | undefined;
405
444
  /**
@@ -493,6 +532,8 @@ declare class MinimaxCredits {
493
532
  get rateLimit(): RateLimitWindows | undefined;
494
533
  /** Why the windows could not be read, when they could not. */
495
534
  get rateLimitError(): string | undefined;
535
+ /** The plan the account subscribes to, as the last read found it. */
536
+ get plan(): PlanSummary | undefined;
496
537
  /** Why the plan has no windows, when the upstream said so plainly. */
497
538
  get rateLimitAbsence(): 'no-subscription' | undefined;
498
539
  /** When the windows were last read. */
@@ -985,6 +1026,15 @@ interface UsageStatus {
985
1026
  /** How many requests were measured. */
986
1027
  requests: number;
987
1028
  }
1029
+ /** The plan the account subscribes to, as the card reads it. */
1030
+ interface PlanStatus {
1031
+ /** Whether the account holds a plan. */
1032
+ active: boolean;
1033
+ /** The tier's name, as the upstream spells it ("Go"). */
1034
+ tier?: string;
1035
+ /** When the plan ends, in milliseconds since the epoch. */
1036
+ expiresAtMs?: number;
1037
+ }
988
1038
  /** The plan's two rolling usage windows, as the card reads them. */
989
1039
  interface RateLimitStatus {
990
1040
  /** Whether the windows could be read. */
@@ -1085,8 +1135,17 @@ interface StatusDocument {
1085
1135
  login?: LoginStatus;
1086
1136
  /** The account's usage profile, when it could be read. */
1087
1137
  usageProfile?: UsageProfileStatus;
1138
+ /** The plan the account subscribes to, when there is one. */
1139
+ plan?: PlanStatus;
1088
1140
  /** The choices the generation endpoints offer. */
1089
1141
  generation?: GenerationOptions;
1142
+ /** What the host offered the card the last time it looked. */
1143
+ capability?: {
1144
+ /** When the card reported it. */
1145
+ at: number;
1146
+ /** One entry per piece the card looked for. */
1147
+ findings: Record<string, string>;
1148
+ };
1090
1149
  }
1091
1150
  /** Inputs the document is derived from. */
1092
1151
  interface StatusDeps {
@@ -1117,8 +1176,17 @@ interface StatusDeps {
1117
1176
  login?: LoginStatus;
1118
1177
  /** The account's usage profile, when the account API could be reached. */
1119
1178
  usageProfile?: UsageProfileStatus;
1179
+ /** The plan the account subscribes to, when there is one. */
1180
+ plan?: PlanStatus;
1120
1181
  /** The choices the generation endpoints offer. */
1121
1182
  generation?: GenerationOptions;
1183
+ /** What the host offered the card the last time it looked. */
1184
+ capability?: {
1185
+ /** When the card reported it. */
1186
+ at: number;
1187
+ /** One entry per piece the card looked for. */
1188
+ findings: Record<string, string>;
1189
+ };
1122
1190
  }
1123
1191
  /**
1124
1192
  * Build the card's status document.
@@ -1201,4 +1269,4 @@ declare const inject: string[];
1201
1269
  */
1202
1270
  declare function apply(ctx: Context): void;
1203
1271
  //#endregion
1204
- 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 };
1272
+ 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 PlanSummary, 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, fetchMembershipDocument, fetchRateLimitWindows, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parsePlanSummary, parseRateLimitWindows, parseSigninClaim, parseSigninPanel, pluginCredentialPath, pluginStatePath, pollDeviceLogin, priceFor, readCredential, readOAuthCredential, readPluginCredential, readPluginState, refreshOAuthCredential, refreshPluginCredential, startDeviceLogin, statusDocument, timezoneId, writeOAuthCredential, writePluginCredential, writePluginState };
package/lib/index.js CHANGED
@@ -746,6 +746,16 @@ const MINIMAX_ACTION_PATH = "/plugins/dsh-connect-minimaxcode/action";
746
746
  * takes a minute and a half and is polled.
747
747
  */
748
748
  const MINIMAX_GENERATE_PATH = "/plugins/dsh-connect-minimaxcode/generate";
749
+ /**
750
+ * Route the card reports what the host offers it on.
751
+ *
752
+ * Delivering a generated picture into a conversation needs several pieces of
753
+ * the host — the session, its face, the input box's attachment channel — and
754
+ * they are handed over in different ways. Reporting what is actually there
755
+ * turns "it did not work" into "this piece is missing", which is the only
756
+ * useful answer when something like that goes wrong.
757
+ */
758
+ const MINIMAX_CAPABILITY_PATH = "/plugins/dsh-connect-minimaxcode/capability";
749
759
  const HINTS = {
750
760
  "app-missing": "未找到 MiniMax Code 的登录信息。请先安装并登录 MiniMax Code 桌面 App,然后重启 DSH。",
751
761
  "not-signed-in": "MiniMax Code 尚未登录。请在 MiniMax Code 桌面 App 中登录后重启 DSH。",
@@ -776,6 +786,8 @@ function statusDocument(deps) {
776
786
  ...deps.signin === void 0 ? {} : { signin: deps.signin },
777
787
  ...deps.login === void 0 ? {} : { login: deps.login },
778
788
  ...deps.usageProfile === void 0 ? {} : { usageProfile: deps.usageProfile },
789
+ ...deps.plan === void 0 ? {} : { plan: deps.plan },
790
+ ...deps.capability === void 0 ? {} : { capability: deps.capability },
779
791
  ...deps.generation === void 0 ? {} : { generation: deps.generation }
780
792
  };
781
793
  if (auth.state === "expired") return {
@@ -1363,6 +1375,29 @@ async function ensureOAuthCredential(region, env = process.env, now = Date.now()
1363
1375
  }
1364
1376
  }
1365
1377
  /**
1378
+ * Read the plan out of a membership response.
1379
+ *
1380
+ * The upstream only fills these in for an account that subscribes: without a
1381
+ * plan `has_token_plan` is false and the tier is absent. `subscription_type`
1382
+ * says so too — "mplan" once a plan is held, "none" before.
1383
+ *
1384
+ * @returns The plan, or `undefined` when the response carries no plan fields.
1385
+ */
1386
+ function parsePlanSummary(document) {
1387
+ if (typeof document !== "object" || document === null) return void 0;
1388
+ const record = document;
1389
+ const hasTokenPlan = record.has_token_plan;
1390
+ const tier = typeof record.token_plan_tier === "string" && record.token_plan_tier.length > 0 ? record.token_plan_tier : void 0;
1391
+ const expires = toNumber$1(record.token_plan_expires_at);
1392
+ const expiresAtMs = expires === void 0 || expires <= 0 ? void 0 : expires;
1393
+ if (hasTokenPlan !== true && tier === void 0 && expiresAtMs === void 0) return void 0;
1394
+ return {
1395
+ tier,
1396
+ expiresAtMs,
1397
+ active: hasTokenPlan === true || tier !== void 0
1398
+ };
1399
+ }
1400
+ /**
1366
1401
  * Read the credit summary out of a membership response.
1367
1402
  *
1368
1403
  * The app's settings page prefers the summary and falls back to the flat
@@ -1400,11 +1435,13 @@ function toNumber$1(value) {
1400
1435
  }
1401
1436
  }
1402
1437
  /**
1403
- * Fetch the current credit balance.
1438
+ * Fetch the membership document.
1404
1439
  *
1405
- * @returns The balance, or `undefined` when the account has no credit summary.
1440
+ * One answer carries several things: the credit balance, the plan the
1441
+ * account subscribes to, and when that plan ends. They are read together so
1442
+ * that the account is asked once.
1406
1443
  */
1407
- async function fetchCreditSummary(credential, region, fetchImpl = fetch) {
1444
+ async function fetchMembershipDocument(credential, region, fetchImpl = fetch) {
1408
1445
  const response = await fetchImpl(`${MINIMAX_ACCOUNT_HOSTS[region]}${MEMBERSHIP_PATH}`, {
1409
1446
  method: "POST",
1410
1447
  headers: {
@@ -1414,7 +1451,17 @@ async function fetchCreditSummary(credential, region, fetchImpl = fetch) {
1414
1451
  body: "{}"
1415
1452
  });
1416
1453
  if (!response.ok) throw new Error(`membership request failed: HTTP ${response.status}`);
1417
- return parseCreditSummary(await response.json());
1454
+ const document = await response.json();
1455
+ return typeof document === "object" && document !== null ? document : void 0;
1456
+ }
1457
+ /**
1458
+ * Fetch the current credit balance.
1459
+ *
1460
+ * @returns The balance, or `undefined` when the account has no credit summary.
1461
+ */
1462
+ async function fetchCreditSummary(credential, region, fetchImpl = fetch) {
1463
+ const document = await fetchMembershipDocument(credential, region, fetchImpl);
1464
+ return document === void 0 ? void 0 : parseCreditSummary(document);
1418
1465
  }
1419
1466
  /**
1420
1467
  * Read the credit lots out of a ledger response.
@@ -1476,19 +1523,24 @@ async function fetchCreditLots(credential, region, fetchImpl = fetch) {
1476
1523
  return lots;
1477
1524
  }
1478
1525
  /**
1479
- * Read the two rate-limit windows out of a remains-percent response.
1526
+ * Read the two rate-limit windows out of a remains response.
1480
1527
  *
1481
1528
  * The upstream answers with one entry per metered group; the general one is
1482
1529
  * the only one the plan meters today, and the first entry is used when it is
1483
- * absent. The percents arrive as strings ("19%"), and the counts arrive as -1
1484
- * when the plan does not meter by count, so only the percents and the window
1485
- * edges are read. The window length is derived from the entry's own endpoints
1486
- * rather than assumed to be five hours or seven days — the plan's terms have
1530
+ * absent. The window length is derived from the entry's own endpoints rather
1531
+ * than assumed to be five hours or seven days — the plan's terms have
1487
1532
  * changed before, and the display should follow whatever the server says.
1488
1533
  *
1489
1534
  * A window the server marks as unlimited (`current_*_status` of 3) carries no
1490
1535
  * allowance to be a percentage of, so it produces no window at all: reporting
1491
1536
  * "0%" of an unlimited quota would be a number nobody can act on.
1537
+ *
1538
+ * **Which share the upstream reports changed.** It used to answer with the
1539
+ * share already used, as a string ("19%"); with a plan subscribed it answers
1540
+ * with the share still *remaining*, as a plain number (90). Both are
1541
+ * accepted, and the two are opposites, so what leaves this function is
1542
+ * always the share used — otherwise the same window would read 90% used on
1543
+ * a plan that has barely spent anything.
1492
1544
  */
1493
1545
  function parseRateLimitWindows(document) {
1494
1546
  if (typeof document !== "object" || document === null) return void 0;
@@ -1497,8 +1549,8 @@ function parseRateLimitWindows(document) {
1497
1549
  const entry = remains.find((item) => typeof item === "object" && item !== null && item.model_name === "general") ?? remains[0];
1498
1550
  if (typeof entry !== "object" || entry === null) return void 0;
1499
1551
  const record = entry;
1500
- const interval = readWindow(record.current_interval_used_percent, record.start_time, record.end_time, record.current_interval_status);
1501
- const weekly = readWindow(record.current_weekly_used_percent, record.weekly_start_time, record.weekly_end_time, record.current_weekly_status);
1552
+ const interval = readWindow(record.current_interval_used_percent, record.current_interval_remaining_percent, record.start_time, record.end_time, record.current_interval_status);
1553
+ const weekly = readWindow(record.current_weekly_used_percent, record.current_weekly_remaining_percent, record.weekly_start_time, record.weekly_end_time, record.current_weekly_status);
1502
1554
  if (interval === void 0 || weekly === void 0) return void 0;
1503
1555
  return {
1504
1556
  interval,
@@ -1511,10 +1563,25 @@ function parseRateLimitWindows(document) {
1511
1563
  * The desktop app reads it the same way: `isUnlimited: 3 === status`.
1512
1564
  */
1513
1565
  const UNLIMITED_STATUS = 3;
1566
+ /**
1567
+ * The share of a window's allowance already used, whichever way it is reported.
1568
+ *
1569
+ * The endpoint named the used share `current_*_used_percent` and then
1570
+ * renamed it to `current_*_remaining_percent` while inverting its meaning.
1571
+ * The used share is what a person needs, so it is what this returns; the
1572
+ * remaining share is subtracted from a hundred to get there.
1573
+ */
1574
+ function readUsedPercent(used, remaining) {
1575
+ const usedPercent = parsePercent(used);
1576
+ if (usedPercent !== void 0) return usedPercent;
1577
+ const remainingPercent = parsePercent(remaining);
1578
+ if (remainingPercent === void 0) return void 0;
1579
+ return 100 - remainingPercent;
1580
+ }
1514
1581
  /** Assemble one window from its percent and edge fields; all must be present. */
1515
- function readWindow(percent, startAtMs, resetAtMs, status) {
1582
+ function readWindow(percent, remainingPercent, startAtMs, resetAtMs, status) {
1516
1583
  if (status === UNLIMITED_STATUS) return void 0;
1517
- const usedPercent = parsePercent(percent);
1584
+ const usedPercent = readUsedPercent(percent, remainingPercent);
1518
1585
  const start = toNumber$1(startAtMs);
1519
1586
  const reset = toNumber$1(resetAtMs);
1520
1587
  if (usedPercent === void 0 || start === void 0 || reset === void 0) return void 0;
@@ -1707,6 +1774,7 @@ var MinimaxCredits = class {
1707
1774
  #ledgerError;
1708
1775
  #rateLimit;
1709
1776
  #rateLimitError;
1777
+ #plan;
1710
1778
  #rateLimitAbsence;
1711
1779
  #rateLimitAtMs;
1712
1780
  #usageProfile;
@@ -1744,6 +1812,10 @@ var MinimaxCredits = class {
1744
1812
  get rateLimitError() {
1745
1813
  return this.#rateLimitError;
1746
1814
  }
1815
+ /** The plan the account subscribes to, as the last read found it. */
1816
+ get plan() {
1817
+ return this.#plan;
1818
+ }
1747
1819
  /** Why the plan has no windows, when the upstream said so plainly. */
1748
1820
  get rateLimitAbsence() {
1749
1821
  return this.#rateLimitAbsence;
@@ -1774,7 +1846,9 @@ var MinimaxCredits = class {
1774
1846
  async refresh(credential, region, fetchImpl = fetch, now = Date.now()) {
1775
1847
  let changed = false;
1776
1848
  try {
1777
- const summary = await fetchCreditSummary(credential, region, fetchImpl);
1849
+ const document = await fetchMembershipDocument(credential, region, fetchImpl);
1850
+ if (document !== void 0) this.#plan = parsePlanSummary(document);
1851
+ const summary = document === void 0 ? void 0 : parseCreditSummary(document);
1778
1852
  if (summary === void 0) this.#error = "membership response carried no credit summary";
1779
1853
  else {
1780
1854
  const previous = this.#snapshot;
@@ -2646,6 +2720,8 @@ function apply(ctx) {
2646
2720
  let loginPending = false;
2647
2721
  /** Whether the server refused the plugin's own credential. */
2648
2722
  let revokedByServer = false;
2723
+ /** What the card last reported about what the host offered it. */
2724
+ let capability;
2649
2725
  /**
2650
2726
  * Give up on the plugin's own credential after the server refused it.
2651
2727
  *
@@ -2943,6 +3019,20 @@ function apply(ctx) {
2943
3019
  updatedAt: profile.lastUpdateTime
2944
3020
  };
2945
3021
  };
3022
+ /**
3023
+ * The plan the account holds, as the card reads it.
3024
+ *
3025
+ * It comes from the same membership answer as the balance, so it is free.
3026
+ */
3027
+ const buildPlanStatus = () => {
3028
+ const held = credits.plan;
3029
+ if (held === void 0) return void 0;
3030
+ return {
3031
+ active: held.active,
3032
+ tier: held.tier,
3033
+ expiresAtMs: held.expiresAtMs
3034
+ };
3035
+ };
2946
3036
  /** The plan's rolling usage windows as the card reads them. */
2947
3037
  const buildRateLimitStatus = () => {
2948
3038
  if (creditState === "signed-out" || creditState === "expired") return { state: creditState };
@@ -3092,6 +3182,8 @@ function apply(ctx) {
3092
3182
  signin: buildSigninStatus(),
3093
3183
  login: buildLoginStatus(),
3094
3184
  usageProfile: buildUsageProfileStatus(),
3185
+ plan: buildPlanStatus(),
3186
+ capability,
3095
3187
  generation: {
3096
3188
  imageModels: [...IMAGE_MODELS],
3097
3189
  imageAspectRatios: [...IMAGE_ASPECT_RATIOS],
@@ -3407,6 +3499,36 @@ function apply(ctx) {
3407
3499
  }
3408
3500
  }
3409
3501
  }));
3502
+ webCtx.effect(() => webCtx.webServer.register({
3503
+ kind: "exact",
3504
+ path: MINIMAX_CAPABILITY_PATH,
3505
+ handler: async (req, res) => {
3506
+ if (!isLoopback(req.headers.host)) {
3507
+ res.writeHead(403, { "Content-Type": "application/json" });
3508
+ res.end(JSON.stringify({ error: "loopback only" }));
3509
+ return;
3510
+ }
3511
+ try {
3512
+ const raw = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("report") ?? "";
3513
+ let findings = {};
3514
+ try {
3515
+ const parsed = JSON.parse(raw);
3516
+ if (typeof parsed === "object" && parsed !== null) for (const [key, value] of Object.entries(parsed)) findings[key] = String(value);
3517
+ } catch {
3518
+ findings = { report: "unreadable" };
3519
+ }
3520
+ capability = {
3521
+ at: Date.now(),
3522
+ findings
3523
+ };
3524
+ res.writeHead(204);
3525
+ res.end();
3526
+ } catch (error) {
3527
+ res.writeHead(500, { "Content-Type": "application/json" });
3528
+ res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
3529
+ }
3530
+ }
3531
+ }));
3410
3532
  webCtx.effect(() => webCtx.webServer.register({
3411
3533
  kind: "exact",
3412
3534
  path: MINIMAX_GENERATE_PATH,
@@ -3470,4 +3592,4 @@ async function readRequestBody(req) {
3470
3592
  return text;
3471
3593
  }
3472
3594
  //#endregion
3473
- 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 };
3595
+ 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, fetchMembershipDocument, fetchRateLimitWindows, fetchSigninPanel, inject, localDay, makePkce, name, parseAuthDocument, parseCatalog, parseCreditSummary, parseOAuthDocument, parsePlanSummary, 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.6.3",
5
+ "version": "0.6.5",
6
6
  "author": "JeffTsuiCUG",
7
7
  "contributors": [
8
8
  {