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 +38 -0
- package/README.md +38 -0
- package/docs/ACCOUNT-ENDPOINTS.md +118 -0
- package/docs/CREDENTIALS.md +65 -0
- package/docs/ERROR-TAXONOMY.md +72 -0
- package/docs/H3-GENERATION.md +109 -0
- package/docs/README.md +38 -0
- package/lib/client.js +179 -17
- package/lib/index.d.ts +40 -0
- package/lib/index.js +246 -12
- package/package.json +2 -1
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
|
|
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 ===
|
|
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
|
|
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
|
|
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
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|