dsh-connect-minimaxcode 0.6.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -419,8 +419,46 @@ The OAuth token is valid for about an hour and is refreshed automatically by the
419
419
  - **The context window is declared as the gateway's real ceiling (1,000,000), not the 512,000 in the catalog.** Upstream's `limit.context` is the *default* window and the billing tier boundary (input above 512k costs more); the largest entry of `context_window_options` is the ceiling the gateway enforces. Measured on 2026-10-25: 946,206 tokens were served normally, 1,050,000 were refused (`invalid params, context window exceeds limit (2013)`). Declaring 512,000 meant every turn of a long conversation was reported as `pi-ai detected context overflow` — while the model had answered perfectly well and the reply was thrown away.
420
420
  - **The device code flow needs `account.minimax.cn` to be reachable**, and the browser that authorizes it has to be able to sign in to a MiniMax account. On some restricted networks the authorization page cannot be opened.
421
421
 
422
+ ## How to read the version number
423
+
424
+ The plugin follows semantic versioning, and **each level means the same thing** — so the version number says what to expect:
425
+
426
+ | Version | What it means |
427
+ | --- | --- |
428
+ | **patch** (x.y.**z**) | a defect is fixed. **No new configuration key, no interface change.** The existing surface and behaviour stay; one thing that was wrong is now right. |
429
+ | **minor** (x.**y**.0) | a **user-visible** capability is added, or the shape of an existing message changes. **No interface change**; the meaning of the existing configuration keys is unchanged. |
430
+ | **major** (**x**.0.0) | a breaking change: the meaning of an existing configuration key changes, the surface is rearranged enough to need re-learning, or the host interface it depends on has moved on. |
431
+
432
+ Every entry in the changelog opens with why it is that level, and with **what this release deliberately did not do** — the boundaries are worth knowing more than the features, so they come first.
422
433
  ## Changelog
423
434
 
435
+ ### 0.7.0 (2026-11-15)
436
+
437
+ > The version is **minor**: a new "Diagnostics" section and a switch for showing the balance in the composer, and a different shape for the message a failed generation shows. **No interface change**; the meaning of the existing configuration keys is unchanged.
438
+
439
+ #### Added
440
+
441
+ - **A "Diagnostics" section on the plugin page**: a read-only list of facts — the plugin version, the region, the upstream hosts, how the plugin is signed in, where the model catalog came from, which pieces of the host it found (eight of them), and **the upstream's own words about its most recent failure**. The one reason it exists is that **it can be copied into an issue as it stands**: there is no control in it that changes anything. The host's own version is **not** in it, because the host does not hand its version to a plugin — a line that had to be guessed would be worse than no line.
442
+ - **A "show the credit balance in the composer" switch**: on by default. Turning it off hides the balance in the pill and changes nothing else; it can be turned back on at any time.
443
+
444
+ #### Changed
445
+
446
+ - **A failed generation now says three things instead of one**: ① what to do (by the kind of failure); ② **that this request may already have been charged for** (when a video task was submitted and could not be read back); ③ **the upstream's own words**, collapsed, quoted rather than paraphrased.
447
+ - **Failures are classified** into `credentials` / `parameters` / `subscription` / `quota` / `rate-limited` / `upstream` / `local`, each with its own remedy. The classification reads the failure's *shape* (the HTTP status and the wording) rather than its code — a code is a number some other system chose, and the day it is renamed or withdrawn, a classifier keyed on it would quietly stop matching, while one keyed on what the failure *is* keeps working.
448
+
449
+ #### Deliberately not done
450
+
451
+ - **The session-title requests are not diverted to a lighter model.** The host sends one per new session, and it goes to the main model. Measured (2026-11-15): **our upstream catalog holds two models** (`MiniMax-M3` and `MiniMax-M3.1-Flash`, both with a 512K context, a 128K output and reasoning), there is **no lighter one**, and the catalog carries **no price field** — so there is no way to tell what diverting them would save. A change with no measured evidence behind it is not made.
452
+ - **No cooldown for the failures that spent nothing.** Only the ones where the upstream may have accepted the work, and may have charged for it, are held back; a bad parameter, a missing sign-in and a request refused locally cost nothing and may be retried at once.
453
+ - **No host version in the diagnostics.** The host does not offer its version to a plugin; that line could only be guessed.
454
+ - **No "drop the tools for this model" fallback.** Letting a model quietly lose every tool is harder to notice than an error.
455
+
456
+ #### Testing
457
+
458
+ - New: `tests/failure.spec.ts` (9), `tests/generation-cost.spec.ts` (7), `tests/failure-wording.spec.ts` (5), `tests/diagnostics.spec.ts` (6), `tests/control-layers.spec.ts` (6).
459
+ - **Mutation checks**: every fix in this release was reverted on its own to confirm the matching case turns red. All seven mutations of the classifier and all four of the cost distinction were caught. One mutation of the cost distinction **survived at first** — the case exercised the HTTP-failure branch while the mutation changed the "the answer was not JSON" one. Adding a case for that road caught it. It is exactly what "the test passed but was not proving what it claimed" looks like.
460
+ - The protocol findings moved into `docs/` (published with the package): H3 generation, the account endpoints, the credentials and the error taxonomy, each fact carrying the date it was measured.
461
+
424
462
  ### 0.6.9 (2026-11-13)
425
463
 
426
464
  - **The oldest supported DSH moved from `0.2.1-rc.2` down to `0.1.6-alpha.2`.** The old floor was a conservative value, not a technical one. This release searched the host packages' published history version by version and, for every host capability the plugin uses, found the release it first appeared in.
package/README.md CHANGED
@@ -430,8 +430,46 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
430
430
  - **上下文窗口按网关的真实上限声明(1,000,000),不是目录里的 512,000**。上游 `limit.context` 是**默认档位与计费分界**(输入超过 512k 的部分按更高倍率计费),`context_window_options` 里最大的一档才是网关执行的硬上限。实测(2026-10-25):946,206 tokens 正常受理,1,050,000 被拒(`invalid params, context window exceeds limit (2013)`)。此前按 512,000 声明,长会话一旦过线就**每一轮**都被判成「上下文溢出」,模型其实答得好好的,回复却被丢掉(`pi-ai detected context overflow`)。
431
431
  - **设备码流需要能访问 `account.minimax.cn`**,且授权时要在浏览器里能登录 MiniMax 账号。某些内网或受限网络下打不开授权页。
432
432
 
433
+ ## 版本号怎么读
434
+
435
+ 本插件的版本号遵循语义化版本,**而且每一级对应的事情是固定的** —— 读版本号就能预知能发生什么:
436
+
437
+ | 版本号 | 意味着什么 |
438
+ | --- | --- |
439
+ | **patch**(x.y.**z**) | 修一处缺陷。**无新增配置项、无接口变化**。已有的界面与行为不变,只是某处原来不对。 |
440
+ | **minor**(x.**y**.0) | 新增一项**用户可见**的能力,或改变已有提示的形状。**无接口变化**,既有配置键的含义不变。 |
441
+ | **major**(**x**.0.0) | 破坏性变化:既有配置键的含义改变、界面重排到需要重新适应,或依赖的宿主接口换代。 |
442
+
443
+ 更新记录里的每一版开头都写着这一版为什么是该级别,以及**这一版刻意没做什么** —— 边界比功能更值得知道,所以它写在了前面。
433
444
  ## 更新记录
434
445
 
446
+ ### 0.7.0(2026-11-15)
447
+
448
+ > 版本号是 **minor**:新增「诊断信息」分栏与「在输入框显示余额」开关,并改变生成失败时的提示形状。**无接口变化**,既有配置键的含义不变。
449
+
450
+ #### 新增
451
+
452
+ - **插件页新增「诊断信息」分栏**:只读事实表 —— 插件版本、区域、上游地址、登录来源、模型目录来源、宿主提供了哪几块能力(八项)、以及**上游最近一次错误的原文**。这一栏存在的唯一理由是**能被整段复制进 issue**:里面没有任何会改设置的控件。宿主的版本号**不在其中**,因为宿主没有把它交给插件 —— 一个要靠猜的行比没有这一行更糟。
453
+ - **「在对话输入框显示积分余额」开关**:默认开。关掉即隐藏胶囊里的余额,积分与其它一切不受影响,随时可以再打开。
454
+
455
+ #### 改变
456
+
457
+ - **生成失败时的提示从一句话变成三件事**:① 该做什么(按失败性质分类);② **本次可能已被上游计费**(视频任务提交后读不回来时);③ **上游的原文**(折叠显示,原样引用不转述)。
458
+ - **失败按性质分类**:`credentials` / `parameters` / `subscription` / `quota` / `rate-limited` / `upstream` / `local` 七类,每类各有各的建议。分类**按失败的形状**(HTTP 状态与措辞)而不是按错误码 —— 错误码是别的系统取的号,它改名或撤销那天,按号分类的代码会安静地不再匹配,而按「失败是什么」分类不会。
459
+
460
+ #### 刻意没做
461
+
462
+ - **没有把会话标题请求改到轻量模型**。宿主每个新会话会发一次标题请求,走的是主模型。实测(2026-11-15):**我们的上游目录里只有两个模型**(`MiniMax-M3` 与 `MiniMax-M3.1-Flash`,同为 512K 上下文 / 128K 输出 / 推理模型),**没有更轻的模型**,而且目录里**没有价格字段** —— 无从判断改过去能省多少。没有实测证据支撑的改动不做。
463
+ - **没有给额度窗口之外的失败加冷却**。只给「上游可能已经受理、可能已计费」的那一类加;「参数错、没登录、本地就拒了」这类一分钱没花的失败**不加冷却**,改完立刻可以重试。
464
+ - **诊断栏里没有宿主版本号**。宿主没有向插件提供它的版本;那一行只能靠猜。
465
+ - **没有做「按模型摘除工具」的降级**。让模型静默失去全部工具能力,比报错更难被发现。
466
+
467
+ #### 测试
468
+
469
+ - 新增 `tests/failure.spec.ts`(9)、`tests/generation-cost.spec.ts`(7)、`tests/failure-wording.spec.ts`(5)、`tests/diagnostics.spec.ts`(6)、`tests/control-layers.spec.ts`(6)。
470
+ - **变异验证**:本轮的每一处修复都单独还原过,确认对应用例会转红。错误分类器的 7 个变异、扣费区分的 4 个变异,全部被抓到。其中「视频任务读回执失败是否算已扣费」这一条**最初变异存活了** —— 用例走的是 HTTP 失败分支,而变异改的是「响应体不是 JSON」那条路。补上那条路的用例之后,变异被抓住。这正是「测试通过了但证明的不是它声称的东西」的样子。
471
+ - 协议取证搬进 `docs/`(随包发布):H3 生成、账户接口、凭据、错误分类四篇,每条事实都带着测量日期。
472
+
435
473
  ### 0.6.9(2026-11-13)
436
474
 
437
475
  - **最低支持的 DSH 版本从 `0.2.1-rc.2` 降到 `0.1.6-alpha.2`**。原来的下限是保守取值,不是技术上的。这一版把宿主各包的发布历史逐版本查了一遍,对每个插件用到的宿主能力,核出它最早出现在宿主包的哪个版本里。
@@ -0,0 +1,118 @@
1
+ # The account endpoints
2
+
3
+ The upstream surfaces this plugin reads about the account, as measured on
4
+ **2026-11-12** (and re-measured on **2026-11-15** against a subscribed
5
+ account) in the **China** region.
6
+
7
+ ## The membership answer
8
+
9
+ ```
10
+ POST https://agent.minimax.cn/matrix/api/v1/commerce/get_membership_info
11
+ Authorization: Bearer <the desktop app's OAuth token>
12
+ Content-Type: application/json
13
+ Body: {}
14
+ ```
15
+
16
+ The answer is the one place several facts arrive together:
17
+
18
+ | Field | Meaning |
19
+ | --- | --- |
20
+ | `op_credit_summary.total_remaining_amount` | the balance, free plus purchased |
21
+ | `op_credit_summary.purchased_remaining_amount` | the purchased part |
22
+ | `op_credit_summary.free_remaining_amount` | the granted part |
23
+ | `has_token_plan` | whether the account holds a plan |
24
+ | `token_plan_tier` | the plan's name, e.g. `Go` |
25
+ | `token_plan_expires_at` | when the plan ends, in milliseconds |
26
+ | `subscription_type` | `mplan` once a plan is held, `none` before |
27
+ | `opcredit_balance`, `total_remains_credit` | the older, flat view |
28
+
29
+ **The flat fields are a legacy view.** On an account that has migrated to the
30
+ credit pool they read zero even though credits remain, so the plugin uses
31
+ them only when the summary is absent altogether.
32
+
33
+ **A plan's tier is the upstream's own value.** `token_plan_tier` said `Go`
34
+ for the account measured here; another plan would say its own name. Only an
35
+ answer without that field falls back to a generic label.
36
+
37
+ ## The plan's limit windows
38
+
39
+ ```
40
+ GET https://api.minimax.cn/v1/token_plan/remains
41
+ Authorization: Bearer <the open-platform key>
42
+ ```
43
+
44
+ **The field names and their meaning changed.** Before a plan was held the
45
+ answer carried `current_interval_used_percent` and
46
+ `current_weekly_used_percent` as strings (`"19%"`). With a plan it carries
47
+ `current_interval_remaining_percent` and `current_weekly_remaining_percent`
48
+ as plain numbers (`90`) — the *remaining* share, under a name that says so.
49
+
50
+ Both shapes are accepted, and what leaves the plugin is always the share
51
+ **used**: 92% remaining is 8% used. The window length is taken from the
52
+ answer's own edges rather than from the plan's terms, because the terms have
53
+ changed before: the measured answer is a five-hour window and a 168-hour one.
54
+
55
+ A window the upstream marks as unlimited (`current_*_status` of 3) carries no
56
+ allowance to be a percentage of, so it produces no window at all.
57
+
58
+ **An account without a plan gets `2062 no active token plan subscription`**
59
+ rather than an answer. That is the difference between "no windows" and "the
60
+ read failed", and the plugin tells them apart.
61
+
62
+ ## The credit ledger
63
+
64
+ ```
65
+ GET https://agent.minimax.cn/minimax-cloud/api/v1/credit/details?page_number=1&page_size=20
66
+ ```
67
+
68
+ The answer's `details[]` carry `expire_at_ms` and the remaining amount. The
69
+ upstream's lots expire **day by day, spread over thirty-odd days**, so the
70
+ plugin's "expiring soon" window is a display choice, not something the
71
+ upstream states.
72
+
73
+ The credit-package summary lives at:
74
+
75
+ ```
76
+ GET https://www.minimaxi.com/backend/account/token_plan_credit?business=Agent&device_platform=desktop
77
+ ```
78
+
79
+ whose answer carries `total_credits`, `used_credits`, `remaining_credits`,
80
+ `credit_packages_details[]` and the `balance_breakdown` buckets.
81
+
82
+ ## The usage profile
83
+
84
+ ```
85
+ GET https://www.minimaxi.com/backend/account/token_plan/usage_summary?token_type=5&heatmap_days=365&business=Agent&device_platform=desktop
86
+ ```
87
+
88
+ The answer carries `total_days`, `total_token_consumed`, `usage_ranking_percent`,
89
+ `most_active_day`, `active_days`, `current_consecutive_days`, and per-day
90
+ `date_model_usage[]` with a per-model breakdown (including
91
+ `cache_hit_percent`).
92
+
93
+ ## What the catalog does not carry
94
+
95
+ **No promotion-shaped fields.** A search of the whole catalog answer for
96
+ `modelPromotions`, `promotions`, `activities`, `discount` and `free_trial`
97
+ found nothing. The plugin therefore draws no activity badge: a badge would
98
+ have to be invented, and an invented badge is worse than no badge.
99
+
100
+ **No price fields.** The model catalog carries the models and their limits,
101
+ but nothing about what a request costs, so the plugin cannot say what a
102
+ title request or a long turn spent.
103
+
104
+ ## The model catalog
105
+
106
+ ```
107
+ GET https://agent.minimax.cn/mavis/api/v1/models
108
+ Authorization: Bearer <the desktop app's OAuth token>
109
+ ```
110
+
111
+ The answer carries `providers[].config.models`, each with `name`,
112
+ `attachment`, `reasoning`, `limit.context`, `limit.output` and `modalities`.
113
+
114
+ At the time of measurement the catalog carried **two** models,
115
+ `MiniMax-M3` and `MiniMax-M3.1-Flash`, both with a 512,000-token context and
116
+ a 128,000-token output limit. There was **no lighter model** in it — which
117
+ is why the plugin does not divert the host's session-title requests
118
+ elsewhere; see the changelog for that entry.
@@ -0,0 +1,65 @@
1
+ # The credentials, and where they come from
2
+
3
+ The plugin can be signed in two ways, and they arrive by different paths.
4
+ Which one is in use is what the card reports as its sign-in source.
5
+
6
+ ## The desktop app's login (the usual one)
7
+
8
+ The MiniMax Code desktop app leaves its login state in a file. The plugin
9
+ reads it and never writes it.
10
+
11
+ The file's location is probed per platform, and the card lists every path it
12
+ tried and why each one did not work — five distinct reasons, so that the
13
+ commonest misdiagnosis ("the app is not signed in") is not mistaken for
14
+ "the file is somewhere else".
15
+
16
+ The desktop app's token is what the **model gateway** takes:
17
+
18
+ ```
19
+ POST https://agent.minimax.cn/mavis/api/v1/llm/v1/messages
20
+ Authorization: Bearer <the desktop app's OAuth token>
21
+ ```
22
+
23
+ It is also what the **account endpoints** take — membership, the credit
24
+ ledger, the usage profile, and the credit-package summary that hands out the
25
+ open-platform key.
26
+
27
+ ## The plugin's own login (the fallback)
28
+
29
+ With no desktop app on the machine, the plugin starts an OAuth device flow
30
+ from the card and keeps its own token under the Harness home. The person
31
+ confirms it in a browser.
32
+
33
+ That token serves the model gateway and the account endpoints too. The
34
+ **generation** endpoints do not take it — they take the open-platform key,
35
+ which the credit-package summary hands out.
36
+
37
+ ## The open-platform key
38
+
39
+ ```
40
+ GET https://www.minimaxi.com/backend/account/token_plan_credit?business=Agent&device_platform=desktop
41
+ ```
42
+
43
+ The answer carries `api_key`, a `sk-cp-…` value. The plugin keeps it for as
44
+ long as it runs and never writes it to disk.
45
+
46
+ Passing the OAuth token to the generation endpoints instead is refused with
47
+ `1004 login fail: Please carry the API secret key`.
48
+
49
+ ## The two paths that failed before
50
+
51
+ Both of these were real, and both are why the plugin reads the account
52
+ endpoints at all:
53
+
54
+ - **The model gateway read only the legacy JWT.** It now falls back to the
55
+ desktop app's OAuth credential, and a re-login takes effect without
56
+ restarting the host.
57
+ - **The credit endpoint was answered with an empty body.** The plugin now
58
+ reads the documented endpoints, and the answer's own summary.
59
+
60
+ ## What the plugin never does
61
+
62
+ - It never writes to the desktop app's files.
63
+ - It never prints a token. The plugin's own state file holds the OAuth
64
+ credential; the open-platform key is held in memory only.
65
+ - It does not start a login of its own unless the card is pressed.
@@ -0,0 +1,72 @@
1
+ # How a failure is classified
2
+
3
+ The plugin turns an upstream refusal into three things a person can act on:
4
+ **what kind of failure it is**, **whether it may already have been charged
5
+ for**, and **the upstream's own words**.
6
+
7
+ ## Why the classification is by shape
8
+
9
+ The upstream's failures arrive in several shapes: an HTTP status, a business
10
+ code, a sentence, sometimes all three. The obvious thing to key on is the
11
+ code — `2013` means a bad parameter, `2062` means no plan.
12
+
13
+ **The plugin does not.** It keys on the *shape* of the failure: the HTTP
14
+ status and the wording.
15
+
16
+ The reason is that a code is a number **some other system chose**. It may be
17
+ renamed, or withdrawn, or reused for something else, and a classifier that
18
+ keyed on it would one day quietly stop matching — the plugin would report
19
+ "something went wrong" for a failure it used to recognise, with no sign
20
+ that anything had changed. A classifier that keyed on the *kind* of failure
21
+ keeps working across renames, and only has to be extended when a genuinely
22
+ new kind appears.
23
+
24
+ This is the same rule the plugin follows elsewhere: a fact that travels with
25
+ the **data** may be relied on; a fact that travels with another system's
26
+ **code** may not.
27
+
28
+ ## The kinds
29
+
30
+ | Kind | Recognised by | The remedy |
31
+ | --- | --- | --- |
32
+ | `credentials` | HTTP 401 / 403, or wording about a refused key or a failed sign-in | sign in again |
33
+ | `parameters` | HTTP 400 / 422, or wording about an invalid or unsupported value | change the request |
34
+ | `subscription` | wording about no active plan or subscription | check the plan |
35
+ | `quota` | wording about an insufficient balance or a spent quota | wait, or check the windows |
36
+ | `rate-limited` | HTTP 429, or wording about too many requests | wait |
37
+ | `upstream` | anything else | show the upstream's words and ask for a report |
38
+ | `local` | the request never left the plugin | fix it and try again |
39
+
40
+ **A failure that is not recognised is not guessed at.** It becomes
41
+ `upstream`, whose remedy is to show the upstream's own words — which is what
42
+ a report needs, and better than a confident wrong answer.
43
+
44
+ **The wording is checked before the status**, except for the two statuses
45
+ that name a class outright (401/403 and 429). The reason is that a bad
46
+ request can arrive for a reason that is not a bad request: the "no active
47
+ subscription" refusal arrives as HTTP 400, and telling the reader to change
48
+ the parameters would send them looking in the wrong place.
49
+
50
+ ## What it cost
51
+
52
+ A failure that happened **before** the upstream accepted the work spent
53
+ nothing: a bad parameter, a refused credential, a request the plugin
54
+ refused to send. A retry after fixing something is free, and the plugin says
55
+ so.
56
+
57
+ A failure that happened **after** the upstream accepted the work — a video
58
+ task that was submitted and then could not be read back — may already have
59
+ been charged for. The plugin says so too, and holds off for a while.
60
+
61
+ ## What the reader sees
62
+
63
+ Three lines, in this order:
64
+
65
+ 1. **the remedy**, in the host's language;
66
+ 2. **a warning when the work may have been charged for**;
67
+ 3. **the upstream's own words**, collapsed — they are read when a report is
68
+ being written, not when the failure happens.
69
+
70
+ The upstream's words are shown **as they arrived**. A paraphrase would be a
71
+ guess about what the upstream meant, and the guess would be the thing a
72
+ reader would quote back.
@@ -0,0 +1,109 @@
1
+ # H3 image and video generation
2
+
3
+ The upstream endpoints this plugin calls to make a picture or a video with
4
+ H3, as measured on **2026-11-12** against the account this plugin was
5
+ developed with, in the **China** region.
6
+
7
+ Everything here was observed, not read from documentation — the endpoints are
8
+ not documented publicly. Where a measurement could be repeated, it was.
9
+
10
+ ## Credentials
11
+
12
+ The generation endpoints do **not** take the OAuth bearer the model gateway
13
+ takes. They take the **open-platform key**, which the account's own credit
14
+ endpoint hands out:
15
+
16
+ ```
17
+ GET https://www.minimaxi.com/backend/account/token_plan_credit?business=Agent&device_platform=desktop
18
+ Authorization: Bearer <the desktop app's OAuth token>
19
+ ```
20
+
21
+ The answer carries `api_key` — a `sk-cp-…` value — and the plugin keeps it
22
+ for as long as it runs. Passing the OAuth token instead is refused with
23
+ `1004 login fail: Please carry the API secret key`, which arrives with
24
+ HTTP 200 and a code in the body.
25
+
26
+ ## Images
27
+
28
+ ```
29
+ POST https://api.minimax.cn/v1/images/generations
30
+ Authorization: Bearer <the open-platform key>
31
+ Content-Type: application/json
32
+ ```
33
+
34
+ The request carries `model`, `prompt`, `response_format`, and optionally
35
+ `n`, `aspect_ratio` and `area`.
36
+
37
+ | Model | Note |
38
+ | --- | --- |
39
+ | `MiniMax-H3-Image` | the default |
40
+ | `MiniMax-H3-Image-Highspeed` | the faster one |
41
+
42
+ The answer carries `data[]` with either a `url` (valid for **24 hours**) or
43
+ `b64_json`. It also carries `usage.generated_images`, `usage.total_tokens`
44
+ and a `request_id`.
45
+
46
+ The plugin downloads the bytes while the link is still fresh, because a
47
+ plugin that kept only the link would leave the picture behind.
48
+
49
+ ## Videos
50
+
51
+ ```
52
+ POST https://api.minimax.cn/v2/video_generation
53
+ ```
54
+
55
+ The request carries `model` and a `content[]` array whose entries are
56
+ `{type: 'text'}`, `{type: 'image_url', image_url: {url}, role: 'first_frame' | 'last_frame'}`.
57
+ `duration` is 4–15, `resolution` is `768P` or `2K`, and `ratio` runs from
58
+ `21:9` to `9:16`.
59
+
60
+ The answer carries a `task_id`; the state is read from:
61
+
62
+ ```
63
+ GET https://api.minimax.cn/v2/query/video_generation/{task_id}
64
+ ```
65
+
66
+ whose answer carries `task.status` (`running`, `succeeded`, and the failure
67
+ spellings), `task.content.url`, `task.resolution`, `task.duration`,
68
+ `task.ratio` and `task.usage.total_seconds`.
69
+
70
+ The v1 path answers "该模型请使用 /v2/video_generation 接口" — the model moved
71
+ to v2, and the old path says so.
72
+
73
+ ## What a refusal looks like
74
+
75
+ **A refusal can arrive with HTTP 200.** The body carries `base_resp` with a
76
+ `status_code` that is not zero. Reading only the HTTP status would call
77
+ these a success.
78
+
79
+ | Code | Meaning, as observed |
80
+ | --- | --- |
81
+ | `1004` | the credential was refused (`login fail`) |
82
+ | `2013` | a parameter was refused; the message names the field |
83
+ | `2062` | `no active token plan subscription` — the account has no plan |
84
+
85
+ The plugin classifies these by shape, not by number: see
86
+ [ERROR-TAXONOMY.md](ERROR-TAXONOMY.md).
87
+
88
+ ## What was charged
89
+
90
+ The plugin distinguishes two failures, because the difference decides whether
91
+ a retry is sensible:
92
+
93
+ - a failure **before** the upstream accepted the work (a bad parameter, a
94
+ refused credential, a request the plugin refused to send) spent nothing
95
+ and may be retried at once;
96
+ - a failure **after** the upstream accepted it — a video task that was
97
+ submitted and then could not be read back — may already have been charged
98
+ for, and a retry is another task and another charge.
99
+
100
+ The plugin says which of the two happened, in the same place it shows the
101
+ upstream's own words.
102
+
103
+ ## Not measured
104
+
105
+ - Whether the 24-hour link can be refreshed rather than re-generated.
106
+ - What a partially completed video task costs.
107
+ - The international region's endpoints on this protocol: the same paths are
108
+ used against `api.minimaxi.com`, but nothing here was measured against
109
+ that region.
package/docs/README.md ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The upstream protocols this plugin speaks, as they were measured.
3
+ *
4
+ * Each file records one upstream surface: the endpoints, the shape of what
5
+ * comes back, and — most importantly — what was actually observed, with the
6
+ * date it was observed. The upstream is not under this project's control and
7
+ * may change, so a reader needs to know how old a measurement is before
8
+ * trusting it.
9
+ *
10
+ * Nothing here is read at runtime. These are notes for whoever works on the
11
+ * plugin next, and for whoever reads a bug report about a field they do not
12
+ * recognise.
13
+ */
14
+
15
+ # Upstream protocols
16
+
17
+ | Document | What it covers |
18
+ | --- | --- |
19
+ | [H3-GENERATION.md](H3-GENERATION.md) | The H3 image and video endpoints: requests, answers, the accounting, and what a refusal looks like. |
20
+ | [ACCOUNT-ENDPOINTS.md](ACCOUNT-ENDPOINTS.md) | The account endpoints: the membership answer, the credit ledger, the plan's limit windows, and the usage profile. |
21
+ | [CREDENTIALS.md](CREDENTIALS.md) | Where the login state comes from, and the two paths it can arrive by. |
22
+ | [ERROR-TAXONOMY.md](ERROR-TAXONOMY.md) | How a failure is classified, and why the classification is by shape rather than by code. |
23
+
24
+ ## How a measurement is recorded
25
+
26
+ Every fact here carries the date it was measured and the endpoint it came
27
+ from. Two reasons:
28
+
29
+ - the upstream answers differently over time — the plan's window fields were
30
+ renamed and their meaning inverted between one release and the next, and a
31
+ note without a date would have said the wrong thing for months;
32
+ - a reader who wants to reproduce a measurement needs to know which build and
33
+ which account it came from, because an account without a plan gets a
34
+ different answer than one with it.
35
+
36
+ A measurement that has not been repeated in a long time is treated as a
37
+ hypothesis, not as a fact. The ones below were measured against the account
38
+ this plugin was developed with, in the region named in each document.
package/lib/client.js CHANGED
@@ -83,6 +83,26 @@
83
83
  usageRanking: '用量排名前 {percent}%',
84
84
  usageBestDay: '最活跃的一天 {date}',
85
85
  planNoSubscription: '未订阅 M Plan(仅积分包),因此没有限额窗口',
86
+ displayTitle: '显示',
87
+ showPill: '在对话输入框显示积分余额',
88
+ diagnosticsTitle: '诊断信息',
89
+ diagnosticsHint: '这一栏是只读的,可整段复制进 issue,它不会改变任何设置。',
90
+ diagnosticsPluginVersion: '插件版本',
91
+ diagnosticsRegion: '区域',
92
+ diagnosticsHosts: '上游地址',
93
+ diagnosticsAuthSource: '登录来源',
94
+ diagnosticsCatalogSource: '模型目录来源',
95
+ diagnosticsCapability: '宿主能力',
96
+ diagnosticsLastError: '最近一次上游错误',
97
+ failureCredentials: '登录凭据被拒绝。请在插件页重新登录 MiniMax Code。',
98
+ failureParameters: '请求的参数被上游拒绝。请调整提示词或选项后重试。',
99
+ failureSubscription: '账户没有可用的订阅套餐,因此调用被拒绝。请确认 M Plan 订阅是否生效。',
100
+ failureQuota: '账户的额度已用尽。请等待额度恢复,或查看套餐的限额窗口。',
101
+ failureRateLimited: '上游正在限流。请稍后重试。',
102
+ failureLocal: '请求没有发出,因此没有产生任何消耗。',
103
+ failureUpstream: '上游拒绝了这次请求。请把下面的原文一并反馈,以便定位。',
104
+ failureCharged: '本次请求可能已被上游计费,请勿立即重试。',
105
+ failureUpstreamDetail: '上游原文',
86
106
  planGeneric: 'M Plan 订阅',
87
107
  planExpires: '{date} 到期',
88
108
  generateTitle: '生成',
@@ -169,6 +189,26 @@
169
189
  usageRanking: 'Top {percent}% by usage',
170
190
  usageBestDay: 'Busiest day {date}',
171
191
  planNoSubscription: 'No M Plan subscription (credits only), so there are no usage windows',
192
+ displayTitle: 'Display',
193
+ showPill: 'Show the credit balance in the composer',
194
+ diagnosticsTitle: 'Diagnostics',
195
+ diagnosticsHint: 'This section is read-only. Copy it into an issue as it stands; it changes nothing.',
196
+ diagnosticsPluginVersion: 'Plugin version',
197
+ diagnosticsRegion: 'Region',
198
+ diagnosticsHosts: 'Upstream hosts',
199
+ diagnosticsAuthSource: 'Signed in via',
200
+ diagnosticsCatalogSource: 'Catalog source',
201
+ diagnosticsCapability: 'Host capabilities',
202
+ diagnosticsLastError: 'Last upstream error',
203
+ failureCredentials: 'The credential was refused. Sign in to MiniMax Code again on the plugin page.',
204
+ failureParameters: 'The upstream rejected the request parameters. Adjust the prompt or the options and try again.',
205
+ failureSubscription: 'The account holds no plan, so the call was refused. Check whether the M Plan subscription is active.',
206
+ failureQuota: "The allowance is spent. Wait for it to recover, or check the plan's limit windows.",
207
+ failureRateLimited: 'The upstream is throttling. Try again a little later.',
208
+ failureLocal: 'The request never left the plugin, so nothing was spent.',
209
+ failureUpstream: 'The upstream refused the request. Please report the original words below so it can be traced.',
210
+ failureCharged: 'This request may already have been charged for upstream; do not retry it at once.',
211
+ failureUpstreamDetail: 'Upstream words',
172
212
  planGeneric: 'M Plan subscription',
173
213
  planExpires: 'until {date}',
174
214
  generateTitle: 'Generate',
@@ -406,6 +446,97 @@
406
446
  return null;
407
447
  return h('div', { style: mutedStyle, 'data-minimax-plan-notice': true }, t('planNoSubscription'));
408
448
  }
449
+ /**
450
+ * The sentence that says what to do about a failed generation call.
451
+ *
452
+ * The kind is the server's verdict on what the failure *is*, and each
453
+ * kind has one remedy; the upstream's own words are kept beside it
454
+ * rather than in place of it, because the words are the evidence and
455
+ * the remedy is the help.
456
+ */
457
+ function generationRemedy(t, kind) {
458
+ switch (kind) {
459
+ case 'credentials': return t('failureCredentials');
460
+ case 'parameters': return t('failureParameters');
461
+ case 'subscription': return t('failureSubscription');
462
+ case 'quota': return t('failureQuota');
463
+ case 'rate-limited': return t('failureRateLimited');
464
+ case 'local': return t('failureLocal');
465
+ default: return t('failureUpstream');
466
+ }
467
+ }
468
+ /**
469
+ * Turn a failed call into what the person is shown.
470
+ *
471
+ * Three things travel together: the remedy, whether the upstream had
472
+ * already accepted the work, and the upstream's own words. The last
473
+ * one is shown collapsed rather than inline — it can be long, and it is
474
+ * read when a report is being written, not when the failure happens.
475
+ */
476
+ function generationNote(t, failure) {
477
+ const upstream = failure.upstream?.message ?? failure.error;
478
+ return {
479
+ text: generationRemedy(t, failure.kind),
480
+ upstream,
481
+ charged: failure.charged === true,
482
+ };
483
+ }
484
+ /**
485
+ * The diagnostics section: a read-only list of facts about this
486
+ * installation.
487
+ *
488
+ * Its purpose is to be copied. Every line is a fact, nothing here
489
+ * changes a setting, and the whole thing can be pasted into a report
490
+ * as it stands — which is the whole point, because the facts a report
491
+ * needs are exactly the ones nobody can be expected to remember.
492
+ */
493
+ function diagnosticsBlock(t, diagnostics) {
494
+ if (diagnostics === undefined)
495
+ return null;
496
+ const lines = [];
497
+ const add = (label, value) => {
498
+ if (value !== undefined && value.length > 0)
499
+ lines.push(`${label}: ${value}`);
500
+ };
501
+ add(t('diagnosticsPluginVersion'), diagnostics.pluginVersion);
502
+ add(t('diagnosticsRegion'), diagnostics.region);
503
+ add(t('diagnosticsHosts'), diagnostics.hosts?.join(' '));
504
+ add(t('diagnosticsAuthSource'), diagnostics.authSource);
505
+ add(t('diagnosticsCatalogSource'), diagnostics.catalogSource);
506
+ add(t('diagnosticsCapability'), diagnostics.capability === undefined
507
+ ? undefined
508
+ : Object.entries(diagnostics.capability).map(([key, value]) => `${key}=${value}`).join(' '));
509
+ add(t('diagnosticsLastError'), diagnostics.lastUpstreamError);
510
+ if (lines.length === 0)
511
+ return null;
512
+ return h('div', { style: boxStyle, 'data-minimax-diagnostics': true }, h('div', { style: boxTitleStyle }, t('diagnosticsTitle')), h('pre', { style: upstreamTextStyle, 'data-minimax-diagnostics-text': true }, lines.join('\n')), h('div', { style: mutedStyle }, t('diagnosticsHint')));
513
+ }
514
+ /**
515
+ * A note the card shows.
516
+ *
517
+ * Most notes are one sentence — the delivery refusals name what is
518
+ * missing — while a failed call carries its evidence with it. Both are
519
+ * drawn by the same helper, so the two kinds sit in one place.
520
+ */
521
+ function noteBlock(t, note) {
522
+ if (typeof note === 'string')
523
+ return h('span', { style: mutedStyle }, note);
524
+ return generationNoteBlock(t, note);
525
+ }
526
+ /**
527
+ * The note as it is drawn: the remedy, then the upstream's words.
528
+ *
529
+ * The charged case says so in its own line: a person who was told
530
+ * only "try again" would spend again, and the upstream may already
531
+ * have taken the first one.
532
+ */
533
+ function generationNoteBlock(t, note) {
534
+ return h('div', { style: mutedStyle, 'data-minimax-generation-note': true }, h('div', null, note.text), note.charged === true
535
+ ? h('div', { style: warnStyle, 'data-minimax-generation-charged': true }, t('failureCharged'))
536
+ : null, note.upstream === undefined
537
+ ? null
538
+ : h('details', { style: mutedStyle, 'data-minimax-generation-upstream': true }, h('summary', null, t('failureUpstreamDetail')), h('pre', { style: upstreamTextStyle }, note.upstream)));
539
+ }
409
540
  /**
410
541
  * What the page card's plan box says when it has no windows to show.
411
542
  *
@@ -546,6 +677,10 @@
546
677
  const provider = (projection?.next ?? projection?.lastUsed)?.provider;
547
678
  if (provider !== undefined && provider !== MINIMAX_PROVIDER_ID)
548
679
  return null;
680
+ // The balance may be hidden: a display choice, applied at once.
681
+ const [status] = useStatus();
682
+ if (status?.showPill === false)
683
+ return null;
549
684
  return h(CreditPill, props);
550
685
  }
551
686
  /**
@@ -610,7 +745,7 @@
610
745
  }, []);
611
746
  const [prompt, setPrompt] = React.useState('');
612
747
  const [busy, setBusy] = React.useState(false);
613
- const [note, setNote] = React.useState('');
748
+ const [note, setNote] = React.useState(undefined);
614
749
  /**
615
750
  * The session's own send path.
616
751
  *
@@ -686,7 +821,7 @@
686
821
  if (text === '')
687
822
  return;
688
823
  setBusy(true);
689
- setNote('');
824
+ setNote(undefined);
690
825
  try {
691
826
  const response = await fetch(GENERATE_PATH, {
692
827
  method: 'POST',
@@ -695,7 +830,7 @@
695
830
  });
696
831
  const body = await response.json();
697
832
  if (body.ok !== true || body.dataUrl === undefined) {
698
- setNote(body.error ?? t('generateFailed'));
833
+ setNote(generationNote(t, body));
699
834
  return;
700
835
  }
701
836
  const part = imagePart(body.dataUrl, body.mediaType ?? 'image/jpeg');
@@ -737,7 +872,7 @@
737
872
  setOpen(false);
738
873
  // The picture's own arrival is the feedback: it is in the
739
874
  // conversation now, so the composer needs no banner.
740
- setNote('');
875
+ setNote(undefined);
741
876
  return;
742
877
  }
743
878
  const drafts = draftFace();
@@ -760,7 +895,7 @@
760
895
  addAttachments(created.map(draft => draft.id));
761
896
  setPrompt('');
762
897
  setOpen(false);
763
- setNote('');
898
+ setNote(undefined);
764
899
  return;
765
900
  }
766
901
  setNote(t('generateSendFailed'));
@@ -777,7 +912,7 @@
777
912
  type: 'button',
778
913
  style: buttonStyle,
779
914
  title: t('generateToChatHint'),
780
- onClick: () => { setOpen(true); setNote(''); },
915
+ onClick: () => { setOpen(true); setNote(undefined); },
781
916
  }, t('generateToChat'));
782
917
  }
783
918
  return h('div', { style: dockStyle, 'data-minimax-generate-dock': true }, h('input', {
@@ -795,8 +930,8 @@
795
930
  type: 'button',
796
931
  style: buttonStyle,
797
932
  disabled: busy,
798
- onClick: () => { setOpen(false); setPrompt(''); setNote(''); },
799
- }, '✕'), note === '' ? null : h('span', { style: mutedStyle }, note));
933
+ onClick: () => { setOpen(false); setPrompt(''); setNote(undefined); },
934
+ }, '✕'), note === undefined ? null : noteBlock(t, note));
800
935
  }
801
936
  /** Ask the host to do something, and let the card read the result. */
802
937
  async function act(action, body) {
@@ -914,14 +1049,14 @@
914
1049
  const [resolution, setResolution] = React.useState('2K');
915
1050
  const [ratio, setRatio] = React.useState('16:9');
916
1051
  const [busy, setBusy] = React.useState(false);
917
- const [note, setNote] = React.useState('');
1052
+ const [note, setNote] = React.useState(undefined);
918
1053
  const [picture, setPicture] = React.useState(undefined);
919
1054
  const [video, setVideo] = React.useState(undefined);
920
1055
  const imageModels = options?.imageModels ?? ['MiniMax-H3-Image'];
921
1056
  /** Ask for a picture; the answer carries the bytes inline. */
922
1057
  async function makeImage() {
923
1058
  setBusy(true);
924
- setNote('');
1059
+ setNote(undefined);
925
1060
  setVideo(undefined);
926
1061
  try {
927
1062
  const response = await fetch(GENERATE_PATH, {
@@ -937,7 +1072,7 @@
937
1072
  });
938
1073
  const body = await response.json();
939
1074
  if (body.ok !== true || body.dataUrl === undefined) {
940
- setNote(body.error ?? t('generateFailed'));
1075
+ setNote(generationNote(t, body));
941
1076
  return;
942
1077
  }
943
1078
  setPicture({ dataUrl: body.dataUrl, size: body.size, saved: body.attachment !== undefined });
@@ -952,7 +1087,7 @@
952
1087
  /** Start a video task, then wait for it the way the upstream does. */
953
1088
  async function makeVideo() {
954
1089
  setBusy(true);
955
- setNote('');
1090
+ setNote(undefined);
956
1091
  setPicture(undefined);
957
1092
  try {
958
1093
  const response = await fetch(GENERATE_PATH, {
@@ -968,7 +1103,7 @@
968
1103
  });
969
1104
  const body = await response.json();
970
1105
  if (body.ok !== true || body.taskId === undefined) {
971
- setNote(body.error ?? t('generateFailed'));
1106
+ setNote(generationNote(t, body));
972
1107
  return;
973
1108
  }
974
1109
  // The task takes a minute and a half; the upstream is asked every
@@ -979,11 +1114,11 @@
979
1114
  const task = await polled.json();
980
1115
  if (task.url !== undefined) {
981
1116
  setVideo({ url: task.url, status: task.status });
982
- setNote('');
1117
+ setNote(undefined);
983
1118
  return;
984
1119
  }
985
1120
  if (task.status !== undefined && /fail|cancel/i.test(task.status)) {
986
- setNote(task.error ?? t('generateFailed'));
1121
+ setNote(generationNote(t, task));
987
1122
  return;
988
1123
  }
989
1124
  }
@@ -1022,7 +1157,7 @@
1022
1157
  style: actionButtonStyle,
1023
1158
  disabled: busy || prompt.trim() === '',
1024
1159
  onClick: () => { void (mode === 'image' ? makeImage() : makeVideo()); },
1025
- }, busy ? (mode === 'video' ? t('generateVideoRunning') : t('generateRunning')) : t('generateRun'))), note === '' ? null : h('div', { style: mutedStyle }, note), picture === undefined
1160
+ }, busy ? (mode === 'video' ? t('generateVideoRunning') : t('generateRunning')) : t('generateRun'))), note === undefined ? null : noteBlock(t, note), picture === undefined
1026
1161
  ? null
1027
1162
  : h('div', { style: generationResultStyle, 'data-minimax-generation-image': true }, h('img', { src: picture.dataUrl, alt: prompt, style: generatedImageStyle }), h('div', { style: rowStyle }, h('span', null, picture.size === undefined ? t('generateImageDone') : `${t('generateImageDone')} · ${picture.size}`), picture.saved === true ? h('span', null, t('generateSaved')) : null, h('a', { href: picture.dataUrl, download: 'minimax-h3-image.jpg', style: linkStyle }, t('generateDownload')))), video === undefined
1028
1163
  ? null
@@ -1092,7 +1227,20 @@
1092
1227
  ? h('div', { style: { ...boxStyle, ...span2Style }, 'data-minimax-usage-profile': true }, h('div', { style: boxTitleStyle }, t('accountUsage')), h('div', { style: profileGridStyle }, ...profileLines.map((line, index) => h('div', { key: `profile-${index}` }, line))))
1093
1228
  : null, login !== undefined && login.state !== 'signed-in'
1094
1229
  ? h('div', { style: span2Style }, loginBlock(t, login, refresh))
1095
- : null)),
1230
+ : null,
1231
+ // The read-only facts, last in the card: a report is made from
1232
+ // them, and they are of no use to the person using the plugin.
1233
+ diagnosticsBlock(t, status?.diagnostics),
1234
+ // A display choice: it takes effect at once, with no draft to
1235
+ // save. The other things on this page are facts to read; this
1236
+ // is the one control, and it changes only what is drawn.
1237
+ h('div', { style: { ...boxStyle, ...span2Style }, 'data-minimax-pill-toggle': true }, h('div', { style: boxTitleStyle }, t('displayTitle')), h('label', { style: { display: 'inline-flex', alignItems: 'center', gap: '6px', fontSize: '12px' } }, h('input', {
1238
+ type: 'checkbox',
1239
+ checked: status?.showPill !== false,
1240
+ onChange: (event) => {
1241
+ void act('pillVisible', { showPill: event.target.checked });
1242
+ },
1243
+ }), h('span', null, t('showPill')))))),
1096
1244
  // The generation panel does not need the balance: it spends the
1097
1245
  // account's generation allowance, not its credits, so it stays
1098
1246
  // usable even when the balance cannot be read.
@@ -1232,6 +1380,20 @@
1232
1380
  };
1233
1381
  const planExpiryStyle = { fontSize: '13px', fontWeight: 400, lineHeight: '1.6' };
1234
1382
  const mutedStyle = { opacity: 0.7, fontSize: '12px' };
1383
+ /**
1384
+ * The upstream's own words, as they arrive.
1385
+ *
1386
+ * Kept monospaced and wrapped, because it is quoted evidence: a report
1387
+ * should carry it exactly as the upstream wrote it.
1388
+ */
1389
+ const upstreamTextStyle = {
1390
+ margin: '4px 0 0',
1391
+ whiteSpace: 'pre-wrap',
1392
+ wordBreak: 'break-word',
1393
+ fontFamily: 'var(--dsh-mono, ui-monospace, monospace)',
1394
+ fontSize: '11px',
1395
+ opacity: 0.9,
1396
+ };
1235
1397
  const actionButtonStyle = {
1236
1398
  appearance: 'none',
1237
1399
  background: 'transparent',
package/lib/index.d.ts CHANGED
@@ -719,6 +719,15 @@ interface PluginState {
719
719
  schemaVersion: number;
720
720
  /** Whether the plugin claims the daily grant on its own. */
721
721
  autoCheckin: boolean;
722
+ /**
723
+ * Whether the balance shows in the composer.
724
+ *
725
+ * A display choice rather than a setting about the account: it changes
726
+ * nothing upstream and nothing about what the plugin does, only whether
727
+ * the balance is drawn beside the composer. It therefore takes effect at
728
+ * once, with no draft to save — see the note on the settings below.
729
+ */
730
+ showPill?: boolean;
722
731
  /** The day the last claim happened, in the account's time zone. */
723
732
  lastClaimDay?: string;
724
733
  /** When the last claim happened, in milliseconds since the epoch. */
@@ -1026,6 +1035,29 @@ interface UsageStatus {
1026
1035
  /** How many requests were measured. */
1027
1036
  requests: number;
1028
1037
  }
1038
+ /**
1039
+ * The facts a report needs, gathered in one place.
1040
+ *
1041
+ * The point of this section is that it can be copied as a whole into a
1042
+ * report: every line is a fact about this installation, and nothing here
1043
+ * changes any setting — there is no control on it, by design.
1044
+ */
1045
+ interface DiagnosticsStatus {
1046
+ /** This plugin's own version. */
1047
+ pluginVersion?: string;
1048
+ /** The region this plugin talks to. */
1049
+ region?: string;
1050
+ /** The upstream hosts this plugin uses. */
1051
+ hosts?: string[];
1052
+ /** How the plugin is signed in. */
1053
+ authSource?: string;
1054
+ /** Where the model catalog came from. */
1055
+ catalogSource?: string;
1056
+ /** The most recent upstream failure, in its own words. */
1057
+ lastUpstreamError?: string;
1058
+ /** What the host offered the card, one entry per piece. */
1059
+ capability?: Record<string, string>;
1060
+ }
1029
1061
  /** The plan the account subscribes to, as the card reads it. */
1030
1062
  interface PlanStatus {
1031
1063
  /** Whether the account holds a plan. */
@@ -1137,6 +1169,10 @@ interface StatusDocument {
1137
1169
  usageProfile?: UsageProfileStatus;
1138
1170
  /** The plan the account subscribes to, when there is one. */
1139
1171
  plan?: PlanStatus;
1172
+ /** The facts a report needs, gathered in one place. */
1173
+ diagnostics?: DiagnosticsStatus;
1174
+ /** Whether the balance is drawn beside the composer. */
1175
+ showPill?: boolean;
1140
1176
  /** The choices the generation endpoints offer. */
1141
1177
  generation?: GenerationOptions;
1142
1178
  /** What the host offered the card the last time it looked. */
@@ -1178,6 +1214,10 @@ interface StatusDeps {
1178
1214
  usageProfile?: UsageProfileStatus;
1179
1215
  /** The plan the account subscribes to, when there is one. */
1180
1216
  plan?: PlanStatus;
1217
+ /** The facts a report needs, gathered in one place. */
1218
+ diagnostics?: DiagnosticsStatus;
1219
+ /** Whether the balance is drawn beside the composer. */
1220
+ showPill?: boolean;
1181
1221
  /** The choices the generation endpoints offer. */
1182
1222
  generation?: GenerationOptions;
1183
1223
  /** What the host offered the card the last time it looked. */
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
5
  import { dirname, join } from "node:path";
@@ -733,6 +734,68 @@ function imageInputHooks(ctx) {
733
734
  };
734
735
  }
735
736
  //#endregion
737
+ //#region src/failure.ts
738
+ /**
739
+ * Match a shape of the upstream's message.
740
+ *
741
+ * The patterns name *facts* rather than identifiers: "no active subscription"
742
+ * is a fact about the account, "2062" is a number another system chose.
743
+ */
744
+ const SHAPES = [
745
+ {
746
+ kind: "subscription",
747
+ pattern: /no active (token plan|subscription)|not subscribed|no subscription/i
748
+ },
749
+ {
750
+ kind: "credentials",
751
+ pattern: /login fail|log ?in fail|unauthorized|invalid api[ _-]?key|api secret key|invalid credential|authentication (failed|error)/i
752
+ },
753
+ {
754
+ kind: "quota",
755
+ pattern: /insufficient|quota (exceed|exhaust)|balance (is )?(not enough|insufficient)|not enough (credit|balance)|credit (exceed|exhaust)|超出|余额不足/i
756
+ },
757
+ {
758
+ kind: "rate-limited",
759
+ pattern: /rate ?limit|too many requests|请求过于频繁|频率限制/i
760
+ },
761
+ {
762
+ kind: "parameters",
763
+ pattern: /invalid (parameter|argument|request|value)|parameter .* (invalid|must be|not allowed)|unsupported (parameter|value|model)|must be (a |an )?(string|number|array|object|boolean)|参数(错误|无效)|不合法/i
764
+ }
765
+ ];
766
+ /**
767
+ * Classify a failure by what the upstream said and how it said it.
768
+ *
769
+ * @param message - The upstream's message, or a description of what went
770
+ * wrong locally.
771
+ * @param status - The HTTP status the answer arrived with, when there was one.
772
+ * @returns The kind of failure. A failure this plugin does not recognise is
773
+ * `upstream` rather than a guess: the remedy for an unknown failure is to
774
+ * show the upstream's words, not to pretend to know better.
775
+ */
776
+ function classifyFailure(message, status) {
777
+ if (status === 401 || status === 403) return "credentials";
778
+ if (status === 429) return "rate-limited";
779
+ for (const shape of SHAPES) if (shape.pattern.test(message)) return shape.kind;
780
+ if (status === 400 || status === 422) return "parameters";
781
+ return "upstream";
782
+ }
783
+ /**
784
+ * How long to hold off before trying the same thing again.
785
+ *
786
+ * A failure that spent nothing has nothing to cool down: the caller fixed
787
+ * something and may try again at once. A failure that happened after the
788
+ * upstream accepted the work may already have been charged for, so a retry
789
+ * right away is another charge — that one waits.
790
+ *
791
+ * @returns The wait in milliseconds, or zero when no wait is warranted.
792
+ */
793
+ function cooldownFor(failure) {
794
+ return failure.charged ? COOLDOWN_MS : 0;
795
+ }
796
+ /** The wait after a failure that may already have been charged for. */
797
+ const COOLDOWN_MS = 3e4;
798
+ //#endregion
736
799
  //#region src/status-route.ts
737
800
  /** Route this plugin mounts, matching its settings card. */
738
801
  const MINIMAX_STATUS_PATH = "/plugins/dsh-connect-minimaxcode/status";
@@ -787,6 +850,8 @@ function statusDocument(deps) {
787
850
  ...deps.login === void 0 ? {} : { login: deps.login },
788
851
  ...deps.usageProfile === void 0 ? {} : { usageProfile: deps.usageProfile },
789
852
  ...deps.plan === void 0 ? {} : { plan: deps.plan },
853
+ ...deps.diagnostics === void 0 ? {} : { diagnostics: deps.diagnostics },
854
+ ...deps.showPill === void 0 ? {} : { showPill: deps.showPill },
790
855
  ...deps.capability === void 0 ? {} : { capability: deps.capability },
791
856
  ...deps.generation === void 0 ? {} : { generation: deps.generation }
792
857
  };
@@ -888,10 +953,27 @@ const VIDEO_DURATIONS = [
888
953
  var GenerationError = class extends Error {
889
954
  /** The upstream's own status code, when it gave one. */
890
955
  code;
891
- constructor(message, code) {
956
+ /**
957
+ * What kind of failure this is, and what it cost.
958
+ *
959
+ * The verdict is what a caller acts on: it says whether the remedy is to
960
+ * change something, to sign in again, or to wait, and whether the work may
961
+ * already have been charged for.
962
+ */
963
+ verdict;
964
+ constructor(message, code, verdict) {
892
965
  super(message);
893
966
  this.name = "GenerationError";
894
967
  this.code = code;
968
+ this.verdict = {
969
+ kind: verdict?.kind ?? classifyFailure(message, code),
970
+ charged: verdict?.charged ?? false,
971
+ upstream: verdict?.upstream ?? (code === void 0 ? void 0 : {
972
+ status: code,
973
+ code,
974
+ message
975
+ })
976
+ };
895
977
  }
896
978
  };
897
979
  /**
@@ -981,12 +1063,38 @@ async function callGeneration(apiKey, url, body, fetchImpl) {
981
1063
  try {
982
1064
  document = JSON.parse(text);
983
1065
  } catch {
984
- if (!response.ok) throw new GenerationError(`generation request failed: HTTP ${response.status}`, response.status);
985
- throw new GenerationError("generation response was not JSON");
1066
+ if (!response.ok) throw new GenerationError(`generation request failed: HTTP ${response.status}`, response.status, {
1067
+ charged: false,
1068
+ upstream: {
1069
+ status: response.status,
1070
+ message: `generation request failed: HTTP ${response.status}`
1071
+ }
1072
+ });
1073
+ throw new GenerationError("generation response was not JSON", void 0, { charged: false });
1074
+ }
1075
+ if (!response.ok) {
1076
+ const message = readErrorMessage(document) ?? `generation request failed: HTTP ${response.status}`;
1077
+ const code = readErrorCode(document);
1078
+ throw new GenerationError(message, code ?? response.status, {
1079
+ kind: classifyFailure(message, response.status),
1080
+ charged: false,
1081
+ upstream: {
1082
+ status: response.status,
1083
+ code,
1084
+ message
1085
+ }
1086
+ });
986
1087
  }
987
- if (!response.ok) throw new GenerationError(readErrorMessage(document) ?? `generation request failed: HTTP ${response.status}`, readErrorCode(document) ?? response.status);
988
1088
  const refusal = readBodyRefusal(document);
989
- if (refusal !== void 0) throw new GenerationError(refusal.message, refusal.code);
1089
+ if (refusal !== void 0) throw new GenerationError(refusal.message, refusal.code, {
1090
+ kind: classifyFailure(refusal.message, response.status),
1091
+ charged: false,
1092
+ upstream: {
1093
+ status: response.status,
1094
+ code: refusal.code,
1095
+ message: refusal.message
1096
+ }
1097
+ });
990
1098
  return document ?? {};
991
1099
  }
992
1100
  /**
@@ -1092,11 +1200,37 @@ async function queryVideoTask(options) {
1092
1200
  try {
1093
1201
  document = JSON.parse(text) ?? {};
1094
1202
  } catch {
1095
- throw new GenerationError(`video task query failed: HTTP ${response.status}`, response.status);
1203
+ throw new GenerationError(`video task query failed: HTTP ${response.status}`, response.status, {
1204
+ charged: true,
1205
+ upstream: {
1206
+ status: response.status,
1207
+ message: `video task query failed: HTTP ${response.status}`
1208
+ }
1209
+ });
1210
+ }
1211
+ if (!response.ok) {
1212
+ const message = readErrorMessage(document) ?? `video task query failed: HTTP ${response.status}`;
1213
+ const code = readErrorCode(document);
1214
+ throw new GenerationError(message, code ?? response.status, {
1215
+ kind: classifyFailure(message, response.status),
1216
+ charged: true,
1217
+ upstream: {
1218
+ status: response.status,
1219
+ code,
1220
+ message
1221
+ }
1222
+ });
1096
1223
  }
1097
- if (!response.ok) throw new GenerationError(readErrorMessage(document) ?? `video task query failed: HTTP ${response.status}`, readErrorCode(document) ?? response.status);
1098
1224
  const refusal = readBodyRefusal(document);
1099
- if (refusal !== void 0) throw new GenerationError(refusal.message, refusal.code);
1225
+ if (refusal !== void 0) throw new GenerationError(refusal.message, refusal.code, {
1226
+ kind: classifyFailure(refusal.message, response.status),
1227
+ charged: true,
1228
+ upstream: {
1229
+ status: response.status,
1230
+ code: refusal.code,
1231
+ message: refusal.message
1232
+ }
1233
+ });
1100
1234
  const task = document.task ?? document;
1101
1235
  const content = task.content;
1102
1236
  const usage = task.usage;
@@ -2642,6 +2776,16 @@ var UsageMeter = class {
2642
2776
  };
2643
2777
  //#endregion
2644
2778
  //#region src/index.ts
2779
+ /**
2780
+ * MiniMax Code models for DeepSeek Harness.
2781
+ *
2782
+ * Registers one provider, `minimaxcode`, that serves the models the signed-in
2783
+ * MiniMax Code desktop app has access to. Streaming, tool calls, and
2784
+ * permissions stay Harness-owned; this plugin only supplies the route and the
2785
+ * catalog.
2786
+ *
2787
+ * @module dsh-connect-minimaxcode
2788
+ */
2645
2789
  /** Loader entry name; also the id the bundle patch inserts. */
2646
2790
  /**
2647
2791
  * Whether a message says the server refused the credential itself.
@@ -2727,6 +2871,13 @@ function apply(ctx) {
2727
2871
  /** What the card last reported about what the host offered it. */
2728
2872
  let capability;
2729
2873
  /**
2874
+ * The most recent upstream failure, in its own words.
2875
+ *
2876
+ * It is kept verbatim and unedited: a report that carries it does not
2877
+ * depend on the reader describing it correctly.
2878
+ */
2879
+ let lastUpstreamError;
2880
+ /**
2730
2881
  * Give up on the plugin's own credential after the server refused it.
2731
2882
  *
2732
2883
  * A revoked token keeps its expiry date, so every later call would send the
@@ -3024,6 +3175,54 @@ function apply(ctx) {
3024
3175
  };
3025
3176
  };
3026
3177
  /**
3178
+ * This plugin's own version, read from the package it was installed as.
3179
+ *
3180
+ * It is read at runtime rather than written into the bundle, so the number
3181
+ * in a report is the number of the package that is actually installed — a
3182
+ * number baked in at build time would drift from it on the first release
3183
+ * that forgot to rebuild.
3184
+ *
3185
+ * A package root that cannot be read leaves the field out rather than
3186
+ * guessing: a wrong version in a report is worse than no version.
3187
+ */
3188
+ function readPluginVersion() {
3189
+ try {
3190
+ const manifest = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
3191
+ return typeof manifest.version === "string" ? manifest.version : void 0;
3192
+ } catch {
3193
+ return;
3194
+ }
3195
+ }
3196
+ /** This plugin's own version, read once when it loads. */
3197
+ const PLUGIN_VERSION = readPluginVersion();
3198
+ /**
3199
+ * The facts a report needs, gathered in one place.
3200
+ *
3201
+ * Every line is a fact about this installation and nothing here changes
3202
+ * anything: the section exists so that a person can copy it into a report
3203
+ * without having to work out what to include.
3204
+ *
3205
+ * The host's own version is not in here, because the host does not offer
3206
+ * it to a plugin — a line that had to be guessed would be worse than no
3207
+ * line at all.
3208
+ */
3209
+ const buildDiagnostics = async () => {
3210
+ const auth = await buildAuthStatus();
3211
+ return {
3212
+ pluginVersion: PLUGIN_VERSION,
3213
+ region,
3214
+ hosts: [.../* @__PURE__ */ new Set([
3215
+ MINIMAX_GATEWAYS[region],
3216
+ MINIMAX_ACCOUNT_HOSTS[region],
3217
+ MINIMAX_API_HOSTS[region]
3218
+ ])],
3219
+ authSource: auth.state === "signed-in" ? buildLoginStatus().source : auth.state,
3220
+ catalogSource: catalog.source,
3221
+ lastUpstreamError,
3222
+ ...capability === void 0 ? {} : { capability: capability.findings }
3223
+ };
3224
+ };
3225
+ /**
3027
3226
  * The plan the account holds, as the card reads it.
3028
3227
  *
3029
3228
  * It comes from the same membership answer as the balance, so it is free.
@@ -3188,6 +3387,8 @@ function apply(ctx) {
3188
3387
  usageProfile: buildUsageProfileStatus(),
3189
3388
  plan: buildPlanStatus(),
3190
3389
  capability,
3390
+ diagnostics: await buildDiagnostics(),
3391
+ showPill: pluginState.showPill ?? true,
3191
3392
  generation: {
3192
3393
  imageModels: [...IMAGE_MODELS],
3193
3394
  imageAspectRatios: [...IMAGE_ASPECT_RATIOS],
@@ -3299,6 +3500,22 @@ function apply(ctx) {
3299
3500
  state: result.state
3300
3501
  };
3301
3502
  }
3503
+ case "pillVisible": {
3504
+ const showPill = body.showPill;
3505
+ if (typeof showPill !== "boolean") return {
3506
+ ok: false,
3507
+ error: "showPill 必须是布尔值"
3508
+ };
3509
+ pluginState = {
3510
+ ...pluginState,
3511
+ showPill
3512
+ };
3513
+ await writePluginState(pluginState);
3514
+ return {
3515
+ ok: true,
3516
+ showPill
3517
+ };
3518
+ }
3302
3519
  case "settings": {
3303
3520
  const autoCheckin = body.autoCheckin;
3304
3521
  if (typeof autoCheckin !== "boolean") return {
@@ -3334,9 +3551,15 @@ function apply(ctx) {
3334
3551
  const resolvePlanApiKey = async () => {
3335
3552
  if (planApiKey !== void 0) return planApiKey;
3336
3553
  const account = await resolveAccountCredential();
3337
- if (account.state !== "signed-in") throw new GenerationError("尚未登录 MiniMax Code");
3554
+ if (account.state !== "signed-in") throw new GenerationError("尚未登录 MiniMax Code", void 0, {
3555
+ kind: "credentials",
3556
+ charged: false
3557
+ });
3338
3558
  const key = await fetchPlanApiKey(account.credential, region);
3339
- if (key === void 0) throw new GenerationError("账户没有开放平台密钥,无法调用生成接口");
3559
+ if (key === void 0) throw new GenerationError("账户没有开放平台密钥,无法调用生成接口", void 0, {
3560
+ kind: "credentials",
3561
+ charged: false
3562
+ });
3340
3563
  planApiKey = key;
3341
3564
  return key;
3342
3565
  };
@@ -3383,7 +3606,10 @@ function apply(ctx) {
3383
3606
  const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
3384
3607
  if (prompt.length === 0) return {
3385
3608
  ok: false,
3386
- error: "请填写提示词"
3609
+ error: "请填写提示词",
3610
+ kind: "local",
3611
+ charged: false,
3612
+ cooldownMs: 0
3387
3613
  };
3388
3614
  const apiKey = await resolvePlanApiKey();
3389
3615
  if (kind === "video") return {
@@ -3575,11 +3801,19 @@ function apply(ctx) {
3575
3801
  res.end(JSON.stringify(await runGeneration(body)));
3576
3802
  } catch (error) {
3577
3803
  const status = error instanceof GenerationError ? 400 : 500;
3804
+ if (error instanceof GenerationError) lastUpstreamError = error.verdict.upstream?.message ?? error.message;
3578
3805
  res.writeHead(status, { "Content-Type": "application/json" });
3806
+ const verdict = error instanceof GenerationError ? error.verdict : void 0;
3579
3807
  res.end(JSON.stringify({
3580
3808
  ok: false,
3581
3809
  error: error instanceof Error ? error.message : String(error),
3582
- ...error instanceof GenerationError && error.code !== void 0 ? { code: error.code } : {}
3810
+ ...error instanceof GenerationError && error.code !== void 0 ? { code: error.code } : {},
3811
+ ...verdict === void 0 ? {} : {
3812
+ kind: verdict.kind,
3813
+ charged: verdict.charged,
3814
+ cooldownMs: cooldownFor(verdict)
3815
+ },
3816
+ ...verdict?.upstream === void 0 ? {} : { upstream: verdict.upstream }
3583
3817
  }));
3584
3818
  }
3585
3819
  }
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.9",
5
+ "version": "0.7.0",
6
6
  "author": "JeffTsuiCUG",
7
7
  "contributors": [
8
8
  {
@@ -58,6 +58,7 @@
58
58
  "files": [
59
59
  "lib",
60
60
  "locale",
61
+ "docs",
61
62
  "icon.svg",
62
63
  "cordis.patch.yml",
63
64
  "README.md",