dsh-connect-minimaxcode 0.2.2 → 0.3.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.md +107 -39
- package/lib/client.js +199 -0
- package/lib/index.d.ts +279 -4
- package/lib/index.js +618 -27
- package/package.json +16 -5
package/README.md
CHANGED
|
@@ -3,21 +3,25 @@
|
|
|
3
3
|
把本机已登录的 **MiniMax Code 桌面 App(国内版)** 的模型接入 **DeepSeek Harness(DSH)**,装好即用,无需配置 API Key。
|
|
4
4
|
|
|
5
5
|

|
|
6
|
-

|
|
7
|
+
[](https://www.npmjs.com/package/dsh-connect-minimaxcode)
|
|
8
|
+
[](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/actions/workflows/ci.yml)
|
|
7
9
|
|
|
8
10
|
## 灵感来源
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
本插件的思路来自 **[corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)**(作者 Corrine Hu)。
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
它最早证明了这样一件事:**桌面 App 已经登录的模型,可以零配置地搬进 DSH**——不去碰账号密码,不引入新的鉴权体系,只是把本机已有的登录态接上一个 provider,并复用 DSH 自带的 provider 适配器。本插件把同一套做法用在了 MiniMax Code 上:
|
|
13
15
|
|
|
14
|
-
| | dsh-connect
|
|
16
|
+
| | dsh-workbuddy-connect | 本插件 |
|
|
15
17
|
|---|---|---|
|
|
16
18
|
| 数据来源 | WorkBuddy 桌面 App 的本地登录态 | MiniMax Code 桌面 App 的本地登录态 |
|
|
17
19
|
| 接入方式 | 注册 DSH provider,暴露已登录模型 | 同左 |
|
|
18
20
|
| 目录来源 | 上游实时模型列表 | 同左 |
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
同方向的另一实现是 **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)**,本插件在**插件结构与 provider 注册的组织方式**上参考了同一作者的 [dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae)(该项目的连接内核本身也参照了 corrinehu 的实现,见其 README 致谢)。
|
|
23
|
+
|
|
24
|
+
> 两者的价值可以这样区分:**corrinehu 是最早的开创者**,「复用本机登录态」的路线由它确立;**dingminhua 的实现更贴近 DSH 插件结构的既有约定**,本插件的目录布局与 `cordis.patch.yml` 写法沿用了后者。二者都已列入文末致谢。
|
|
21
25
|
|
|
22
26
|
---
|
|
23
27
|
|
|
@@ -37,9 +41,11 @@
|
|
|
37
41
|
|
|
38
42
|
- **零配置**:自动读取 MiniMax Code 桌面 App 的登录态,不在 DSH 中存储任何明文 Token
|
|
39
43
|
- **动态模型目录**:从上游接口实时拉取,模型增减无需更新插件
|
|
44
|
+
- **积分余额可见**:在对话框底部的胶囊里显示 MiniMax Code 的剩余积分,以及本机累计消耗
|
|
45
|
+
- **实时消耗统计**:每次请求的 token 用量取自上游响应本身,可按官方价目表折算等效金额
|
|
40
46
|
- **复用官方适配器**:上游为标准 Anthropic Messages 协议,直接复用 DSH 自带 provider,工具调用、推理块、上下文压缩均由 DSH 托管
|
|
41
|
-
-
|
|
42
|
-
-
|
|
47
|
+
- **只读为主**:除「续期已过期的桌面 App 登录态」外,不写入、不修改 MiniMax 的任何文件,也不执行任何登录动作
|
|
48
|
+
- **界面不打扰**:胶囊只在输入框下方的 dock 里占一行,不改动 DSH 的其它界面
|
|
43
49
|
|
|
44
50
|
## 前置条件
|
|
45
51
|
|
|
@@ -66,6 +72,8 @@ Web profile 把 `--profile desktop` 换成 `--profile web`。
|
|
|
66
72
|
|
|
67
73
|
安装后**无需重启**,插件会热加载;模型随后出现在模型选择器的 **MiniMax Code** 分组中。
|
|
68
74
|
|
|
75
|
+
> 界面胶囊(积分余额)属于浏览器侧插件,**首次安装或升级到 0.3.0 后需要重启一次 DSH** 才会出现在输入框下方。
|
|
76
|
+
|
|
69
77
|
### 若安装被 `allowBuilds` 拦截
|
|
70
78
|
|
|
71
79
|
本仓库把构建产物 `lib/` 一并提交,因此正常情况下安装不会触发构建。若你看到:
|
|
@@ -87,27 +95,36 @@ Web profile 把 `--profile desktop` 换成 `--profile web`。
|
|
|
87
95
|
2. **安装插件**(见上一步的命令),等待热加载完成。
|
|
88
96
|
|
|
89
97
|
3. **选择模型**
|
|
90
|
-
打开 DSH 的模型选择器,在 **MiniMax Code** 分组下选择模型。可用模型来自上游实时目录,例如 `
|
|
98
|
+
打开 DSH 的模型选择器,在 **MiniMax Code** 分组下选择模型。可用模型来自上游实时目录,例如 `M3`、`M3.1-Flash`。
|
|
91
99
|
|
|
92
100
|
4. **正常对话**
|
|
93
101
|
直接使用即可。工具调用由 DSH 本地工具执行,插件只负责提供模型通路。
|
|
94
102
|
|
|
95
103
|
5. **令牌过期时重新登录**
|
|
96
|
-
MiniMax Code
|
|
104
|
+
MiniMax Code 有**两份互不通用**的登录态,本插件各用其一:
|
|
105
|
+
|
|
106
|
+
| 凭证 | 位置 | 有效期 | 用途 |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| JWT | `~/.minimax/local-runtime.auth.json` | 约 15 天 | 调模型网关(`/mavis/api/v1/llm/v1/messages`) |
|
|
109
|
+
| OAuth(`mmoat_`) | `~/.minimax/auth/prod/<region>/mcode-public/auth.json` | 约 1 小时 | 读账户与积分(`/matrix/api/v1/commerce/get_membership_info`) |
|
|
110
|
+
|
|
111
|
+
两份凭证互不通用:拿 JWT 打账户接口一律 `401`,拿 OAuth 令牌打模型网关同样不通。**模型对话**用 JWT,**积分显示**用 OAuth。
|
|
97
112
|
|
|
98
113
|
| 现象 | 含义 | 处理方式 |
|
|
99
114
|
|---|---|---|
|
|
100
|
-
| 正常使用 |
|
|
101
|
-
|
|
|
102
|
-
|
|
|
115
|
+
| 正常使用 | 两份凭证都有效 | — |
|
|
116
|
+
| 模型报鉴权错误 | JWT 已失效 | 在 MiniMax Code 中**重新登录**,然后重启 DSH |
|
|
117
|
+
| 积分胶囊显示「登录态已过期」 | OAuth 令牌过期 | 插件会用 App 自己的 refresh token 续期并写回;若续期失败,请打开一次 MiniMax Code 桌面 App 让它自行续期 |
|
|
103
118
|
|
|
104
|
-
>
|
|
119
|
+
> OAuth 令牌过期**不会**影响模型对话:JWT 仍然有效,模型照常可用,只是积分胶囊暂时拿不到余额。
|
|
120
|
+
>
|
|
121
|
+
> 续期会**轮换** refresh token。插件因此把新的一对令牌按客户端**原格式原子写回**同一个文件,并保持 `generation` 不变——客户端靠它判断「这份凭据是否仍属于本次登录」,擅自抬高会让桌面 App 误判为异地登录。
|
|
105
122
|
|
|
106
123
|
### 关于响应速度
|
|
107
124
|
|
|
108
125
|
MiniMax 的这些模型**强制要求先推理再输出**,思考无法关闭。因此:
|
|
109
126
|
|
|
110
|
-
- 首个字符出现前会有一段等待(实测 M3.1-Flash
|
|
127
|
+
- 首个字符出现前会有一段等待(实测 M3.1-Flash 约 3 秒)
|
|
111
128
|
- 推理过程会以 `thinking` 块实时返回,可以在界面中看到模型"在想什么"
|
|
112
129
|
- 这是模型本身的行为,**不是插件造成的延迟**
|
|
113
130
|
|
|
@@ -118,6 +135,8 @@ MiniMax 的这些模型**强制要求先推理再输出**,思考无法关闭
|
|
|
118
135
|
1. 读取 `~/.minimax/local-runtime.auth.json`(MiniMax Code 写入的明文登录信息),**仅在内存中**解析其 JWT
|
|
119
136
|
2. 调用 `GET /mavis/api/v1/models` 获取实时模型目录(含上下文长度、多模态、工具调用等能力声明)
|
|
120
137
|
3. 注册 `minimaxcode` provider,指向 `POST /mavis/api/v1/llm/v1/messages`(标准 Anthropic Messages 协议)
|
|
138
|
+
4. 读取 `~/.minimax/auth/prod/<region>/mcode-public/auth.json` 的 OAuth 令牌,按需续期
|
|
139
|
+
5. 调用 `POST /matrix/api/v1/commerce/get_membership_info` 获取积分余额,通过插件自己的状态接口 `/plugins/dsh-connect-minimaxcode/status` 交给界面胶囊
|
|
121
140
|
|
|
122
141
|
### 与网关对接时的三个兼容处理
|
|
123
142
|
|
|
@@ -131,42 +150,89 @@ MiniMax 的这些模型**强制要求先推理再输出**,思考无法关闭
|
|
|
131
150
|
|
|
132
151
|
插件**不**包含任何硬编码的 Token、账号或接口密钥。
|
|
133
152
|
|
|
134
|
-
##
|
|
153
|
+
## 积分余额与用量
|
|
154
|
+
|
|
155
|
+
对话框输入框下方的胶囊会显示 MiniMax Code 的**剩余积分**;点开还能看到赠送/购买拆分、两次读取之间的消耗,以及本机累计的 token 与等效金额。
|
|
156
|
+
|
|
157
|
+
### 余额从哪里来
|
|
135
158
|
|
|
136
|
-
|
|
159
|
+
MiniMax Code 桌面 App 自己每天在调一个「会员信息」接口,响应里就带着客户端设置页显示的那三个数字:
|
|
137
160
|
|
|
138
|
-
|
|
161
|
+
```
|
|
162
|
+
POST https://agent.minimax.cn/matrix/api/v1/commerce/get_membership_info
|
|
163
|
+
Authorization: Bearer <桌面 App 的 mmoat_ OAuth 令牌>
|
|
164
|
+
Content-Type: application/json
|
|
139
165
|
|
|
166
|
+
{}
|
|
140
167
|
```
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
"op_credit_summary": {
|
|
171
|
+
"total_remaining_amount": "11459.454", // 总剩余积分
|
|
172
|
+
"purchased_remaining_amount": "0", // 其中购买的
|
|
173
|
+
"free_remaining_amount": "11459.454" // 其中赠送的
|
|
145
174
|
}
|
|
146
175
|
```
|
|
147
176
|
|
|
148
|
-
|
|
177
|
+
插件读的就是 `op_credit_summary.total_remaining_amount`。**注意别取错字段**:同一个响应里的 `total_remains_credit` 与 `opcredit_balance` 是旧账,在已迁移到积分池的账号上恒为 `0`,取它们会显示「0 积分」。
|
|
178
|
+
|
|
179
|
+
顺带一提,这里有三个**单位不同**的「钱」,别混:
|
|
180
|
+
|
|
181
|
+
| 数值 | 含义 | 本插件是否显示 |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `op_credit_summary.total_remaining_amount` | Code 积分余额 | ✅ 就是它 |
|
|
184
|
+
| `token_plan_credit.remaining_credits` | 已购积分池 | ❌ |
|
|
185
|
+
| `query_balance.available_amount` | 开放平台钱包(元) | ❌ |
|
|
186
|
+
|
|
187
|
+
### 消耗怎么算
|
|
149
188
|
|
|
150
|
-
|
|
151
|
-
|---|---|
|
|
152
|
-
| `GET /v1/api/user/info` + 模型网关令牌 | `401`,两类凭证互不通用 |
|
|
153
|
-
| `GET /v1/api/user/info` + 客户端 OAuth 令牌(`mmoat_`) | `200`,但响应只有昵称、头像、`vipInfo` 等账户资料,**不含任何额度字段** |
|
|
154
|
-
| 14 个候选额度路径(`user/credits`、`user/quota`、`workspace/list`、`credit/summary` 等) | 全部 `404` |
|
|
155
|
-
| `account.minimax.cn` 同一批路径 | 同样无额度字段 |
|
|
189
|
+
两路数据各管一件事:
|
|
156
190
|
|
|
157
|
-
|
|
191
|
+
- **真实积分消耗**:余额是个活值,两次读取的差值就是这段时间真正花掉的积分(客户端也会并发消耗,所以差值含其它客户端的开销)。
|
|
192
|
+
- **本机消耗**:每次请求的响应都自带 `usage`(`input_tokens` / `output_tokens` / `cache_creation_input_tokens` / `cache_read_input_tokens`),插件原样累计。折算金额时**只用这四个分量自己算**,不能用任何 `total_token` 汇总值——上游的两级汇总口径不同(差 1.94 倍,差额正是缓存读取部分)。单价取自 [MiniMax 官方价目表](https://platform.minimax.cn/docs/pricing/overview):
|
|
158
193
|
|
|
159
|
-
|
|
194
|
+
| 模型 | 输入 | 输出 | 缓存读取 |
|
|
195
|
+
|---|---|---|---|
|
|
196
|
+
| `MiniMax-M3.1-Flash` | 1.00 元/百万 tokens | 4.00 | 0.10 |
|
|
197
|
+
| `MiniMax-M3`(≤512k 输入) | 2.10 | 8.40 | 0.42 |
|
|
198
|
+
| `MiniMax-M3`(>512k 输入) | 4.20 | 16.80 | 0.84 |
|
|
199
|
+
|
|
200
|
+
折算出的金额是**等效金额**,不是扣费:积分与金额之间 MiniMax 没有公开换算关系(套餐只公布了 1×/3×/12× 的相对倍数),所以插件不会把它说成「花了多少积分」。
|
|
201
|
+
|
|
202
|
+
### 关于续期
|
|
203
|
+
|
|
204
|
+
OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自己的令牌已过期时**才用 App 已存的 refresh token 续一次,并把轮换后的新令牌按客户端**原格式原子写回**同一个文件(`generation` 保持不变),因此桌面 App 的登录态不受影响。插件不会发起登录、不会登出、也不会调用任何付费/订阅类接口。
|
|
205
|
+
|
|
206
|
+
> 这一节的结论与 0.2.x 的「无法显示额度」相反。当时的调查按 `credits` / `quota` / `summary` 这类英文语义猜路径,真实接口用的是 `commerce` / `membership` 这套计费域词汇,因此全部落空;后来是从桌面 App 自己的运行日志(`%APPDATA%\MiniMax\logs\api-*.log`)里读出它每天真实调用的 URL,才定位到这条路径。
|
|
160
207
|
|
|
161
208
|
## 已知限制
|
|
162
209
|
|
|
163
210
|
- **仅支持国内版(`agent.minimax.cn`)**。国际版网关是否可用**未经验证**,请勿据此推断其他区域可用。
|
|
164
211
|
- **必须开启推理**。MiniMax 的全部模型都声明需要推理,关不掉。首字延迟因此高于非推理模型。
|
|
165
|
-
- **未登录时模型列表是内置兜底**。此时选择器中仍会显示 `
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
-
|
|
169
|
-
-
|
|
212
|
+
- **未登录时模型列表是内置兜底**。此时选择器中仍会显示 `M3`、`M3.1-Flash` 两个内置条目,但它们无法真正调用;登录后才会切换为上游实时目录。判断是否可用,请以实际调用结果为准。
|
|
213
|
+
- **积分只覆盖 MiniMax Code 的赠送/购买积分**,不包含开放平台钱包余额与已购积分池,插件也不会把金额折算成积分。
|
|
214
|
+
- **余额是账户级数值**,两次读取的差值包含 MiniMax Code 其它客户端的并发消耗;本机单独的消耗看胶囊里的 token 数与等效金额。
|
|
215
|
+
- **等效金额是按公开价目表折算的估值**,不是实际扣费;官方未公开 token 与积分的换算关系。
|
|
216
|
+
- **OAuth 令牌过期时积分胶囊会暂时失效**,需打开一次 MiniMax Code 桌面 App 让它自行续期(或等待插件续期成功)。这不影响模型对话。
|
|
217
|
+
- **不提供设置界面卡片**。插件不在 DSH 设置页注册卡片;界面只增加输入框下方的一枚胶囊。
|
|
218
|
+
- **上游接口非公开**,MiniMax 客户端更新后可能失效。余额接口失效时胶囊会显示「积分不可用」,不会影响模型对话。
|
|
219
|
+
|
|
220
|
+
## 更新记录
|
|
221
|
+
|
|
222
|
+
### 0.3.0(2026-10-19)
|
|
223
|
+
|
|
224
|
+
- **新增积分余额显示**:对话框输入框下方的胶囊显示 MiniMax Code 的剩余积分(赠送/购买拆分),点开可看两次读取之间的消耗、本机累计 token 与按官方价目表折算的等效金额。
|
|
225
|
+
- **新增用量计量**:每次请求的 `usage` 取自上游响应本身,累计到本机并折算金额。
|
|
226
|
+
- **新增账户接口定位**:找到桌面 App 每天真实调用的 `POST /matrix/api/v1/commerce/get_membership_info`,并**推翻了 0.2.x 「无法显示额度」的结论**——原因见「积分余额与用量」末尾。
|
|
227
|
+
- **令牌策略调整**:模型网关用 JWT、账户接口用桌面 App 的 OAuth 令牌;后者过期时插件会用 App 已存的 refresh token 续期,并按客户端原格式写回,不影响桌面 App 登录态。
|
|
228
|
+
- **首次带浏览器侧插件**:新增 `lib/client.js`(classic script),只注册一枚 composer dock 胶囊。**升级后需重启一次 DSH**。
|
|
229
|
+
|
|
230
|
+
### 0.2.3(2026-10-19)
|
|
231
|
+
|
|
232
|
+
- **同步上游模型变动**:MiniMax 下架了 `M2.7`、`M2.7-highspeed` 两个旧模型;此前以匿名形式灰度的测试模型(代号 "space bunny" / 太空兔)正式发布为 **`M3.1-Flash`**。当前上游实时目录为 `M3`、`M3.1-Flash`。
|
|
233
|
+
- 内置兜底列表与文档随之更新。**已登录用户无需更新插件**即可看到模型变动(列表来自上游实时目录),本次发布只是让未登录兜底、测试与文档与上游保持一致。
|
|
234
|
+
|
|
235
|
+
历史版本见 [GitHub Releases](https://github.com/JeffTsuiCUG/dsh-connect-minimaxcode/releases) 与 [npm 版本列表](https://www.npmjs.com/package/dsh-connect-minimaxcode?activeTab=versions)。
|
|
170
236
|
|
|
171
237
|
## 卸载
|
|
172
238
|
|
|
@@ -185,6 +251,8 @@ pnpm build # 构建
|
|
|
185
251
|
|
|
186
252
|
构建依赖 `@earendil-works/pi-ai`,需与宿主中 `dsh-llm-pi-ai` 解析出的版本一致(当前为 `0.85.x`),否则类型不兼容。
|
|
187
253
|
|
|
254
|
+
界面半边(`src/client.ts`)由 `tsconfig.client.json` 单独编译成 **classic script**(`lib/client.js`):宿主以 `<script>` 方式加载客户端 bundle,若产出 ESM 会直接导致 Web 启动失败(`Cannot use import statement outside a module`),所以这里刻意不用打包器、也不自带 React——React 来自宿主的浏览器模块表。
|
|
255
|
+
|
|
188
256
|
**`lib/` 是纳入版本管理的构建产物**,不是生成物。这是刻意为之:git 源安装拿到的是仓库原样内容,若不含 `lib/`,pnpm 会判定该包"需要构建"并触发 `prepare`,随即被构建脚本白名单拦下。改动源码后请重新构建并提交:
|
|
189
257
|
|
|
190
258
|
```bash
|
|
@@ -201,7 +269,7 @@ pnpm build && git add lib
|
|
|
201
269
|
| **dsh-qoder-connect** | Qoder(国内版 + 国际版,PAT) | ✅ 每日 100 Credits | ✅ 侧栏额度小卡 + 明细 |
|
|
202
270
|
| **dsh-workbuddy-connect** | WorkBuddy(国内版 + 国际版) | — | ✅ 剩余积分 + 明细 |
|
|
203
271
|
|
|
204
|
-
>
|
|
272
|
+
> 各插件的签到规则、额度口径与限制以各自文档为准。下表为同一台机器上的实测读数,仅代表当时状态,非承诺。
|
|
205
273
|
|
|
206
274
|
### [dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae)
|
|
207
275
|
|
|
@@ -231,9 +299,9 @@ dsh plugin --profile web exec dsh-workbuddy-connect status
|
|
|
231
299
|
|
|
232
300
|
## 致谢
|
|
233
301
|
|
|
234
|
-
- **[dsh-connect
|
|
235
|
-
- [dsh-connect-
|
|
236
|
-
- [dsh-
|
|
302
|
+
- **[corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)** — **本插件的灵感来源**。「复用桌面 App 本机登录态、把已登录模型接进 DSH」的整体思路由该项目最早确立,本插件沿用同一做法;凭据只读复用的设计也参照于此。
|
|
303
|
+
- **[dingminhua/dsh-connect-workbuddy](https://github.com/dingminhua/dsh-connect-workbuddy)** — 同方向的另一实现,本插件的**插件结构与 provider 注册组织方式**参考了它(其连接内核亦参照 corrinehu 的实现,见该项目致谢)。
|
|
304
|
+
- [dingminhua/dsh-connect-trae](https://github.com/dingminhua/dsh-connect-trae) — 同作者的 DSH 插件,可作为 DSH 插件结构的参照。
|
|
237
305
|
|
|
238
306
|
> 上方「同类插件」中的项目均为独立开发者的作品,与本项目无隶属或背书关系,各自遵循其自身的许可与免责声明。
|
|
239
307
|
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The browser half: a credit pill in the composer's dock.
|
|
4
|
+
*
|
|
5
|
+
* The host loads a client bundle as a classic script and calls
|
|
6
|
+
* `window.__ModuleLoader__.load`, handing the factory a `require` that
|
|
7
|
+
* resolves against the browser's own module table. React therefore comes from
|
|
8
|
+
* the host — this file installs no React of its own, and ships no bundler
|
|
9
|
+
* output: it is a script, so it can be dropped into the page as-is.
|
|
10
|
+
*
|
|
11
|
+
* The pill shows the account's remaining credits and what this host's
|
|
12
|
+
* conversations have used, both read from the plugin's own status route. It
|
|
13
|
+
* degrades quietly: a failed read keeps the last value rather than blanking
|
|
14
|
+
* the dock.
|
|
15
|
+
*/
|
|
16
|
+
;
|
|
17
|
+
(function bootstrapMinimaxCodeClient() {
|
|
18
|
+
const BUNDLE_NAME = 'dsh-connect-minimaxcode';
|
|
19
|
+
const NAMESPACE = 'dsh-connect-minimaxcode';
|
|
20
|
+
const STATUS_PATH = '/plugins/dsh-connect-minimaxcode/status';
|
|
21
|
+
const POLL_INTERVAL_MS = 20000;
|
|
22
|
+
const zh = {
|
|
23
|
+
title: 'MiniMax Code 积分',
|
|
24
|
+
balance: '积分 {total}',
|
|
25
|
+
free: '赠送积分',
|
|
26
|
+
purchased: '购买积分',
|
|
27
|
+
consumed: '上次读取后消耗',
|
|
28
|
+
tokens: '本机累计 {count} tokens',
|
|
29
|
+
cost: '等效金额 ≈ ¥{amount}',
|
|
30
|
+
requests: '{count} 次请求',
|
|
31
|
+
updated: '更新于 {time}',
|
|
32
|
+
unavailable: '积分不可用',
|
|
33
|
+
expired: '登录态已过期',
|
|
34
|
+
signedOut: '未登录 MiniMax Code',
|
|
35
|
+
close: '收起',
|
|
36
|
+
};
|
|
37
|
+
const en = {
|
|
38
|
+
title: 'MiniMax Code credits',
|
|
39
|
+
balance: '{total} credits',
|
|
40
|
+
free: 'Free credits',
|
|
41
|
+
purchased: 'Purchased credits',
|
|
42
|
+
consumed: 'Spent since last read',
|
|
43
|
+
tokens: '{count} tokens used here',
|
|
44
|
+
cost: 'Equivalent ≈ ¥{amount}',
|
|
45
|
+
requests: '{count} requests',
|
|
46
|
+
updated: 'Updated {time}',
|
|
47
|
+
unavailable: 'Credits unavailable',
|
|
48
|
+
expired: 'Sign-in expired',
|
|
49
|
+
signedOut: 'MiniMax Code not signed in',
|
|
50
|
+
close: 'Hide',
|
|
51
|
+
};
|
|
52
|
+
const loader = globalThis.__ModuleLoader__;
|
|
53
|
+
if (loader === undefined)
|
|
54
|
+
return;
|
|
55
|
+
loader.load({
|
|
56
|
+
id: BUNDLE_NAME,
|
|
57
|
+
factory(require) {
|
|
58
|
+
const React = require('react');
|
|
59
|
+
const h = React.createElement;
|
|
60
|
+
/** Format a credit amount without pretending to more precision. */
|
|
61
|
+
function credits(value, digits) {
|
|
62
|
+
if (value === undefined || !Number.isFinite(value))
|
|
63
|
+
return '—';
|
|
64
|
+
return value.toLocaleString(undefined, { maximumFractionDigits: digits, minimumFractionDigits: 0 });
|
|
65
|
+
}
|
|
66
|
+
/** Format a token count compactly. */
|
|
67
|
+
function tokens(value) {
|
|
68
|
+
if (!Number.isFinite(value))
|
|
69
|
+
return '—';
|
|
70
|
+
if (value < 1000)
|
|
71
|
+
return String(value);
|
|
72
|
+
if (value < 1000000)
|
|
73
|
+
return `${(value / 1000).toFixed(1)}k`;
|
|
74
|
+
return `${(value / 1000000).toFixed(2)}M`;
|
|
75
|
+
}
|
|
76
|
+
/** Format a small amount of money. */
|
|
77
|
+
function money(value) {
|
|
78
|
+
if (value === undefined || !Number.isFinite(value))
|
|
79
|
+
return '—';
|
|
80
|
+
if (value === 0)
|
|
81
|
+
return '0';
|
|
82
|
+
if (value < 0.01)
|
|
83
|
+
return value.toFixed(4);
|
|
84
|
+
return value.toFixed(2);
|
|
85
|
+
}
|
|
86
|
+
/** The pill, plus the detail panel it opens. */
|
|
87
|
+
function CreditPill(props) {
|
|
88
|
+
const [status, setStatus] = React.useState(undefined);
|
|
89
|
+
const [open, setOpen] = React.useState(false);
|
|
90
|
+
// `t` comes from the host's locale service when the slot declares the
|
|
91
|
+
// namespace; the built-in copy is the fallback for a host that does not.
|
|
92
|
+
const t = props.t ?? ((key, params) => {
|
|
93
|
+
const template = zh[key] ?? key;
|
|
94
|
+
return template.replace(/\{(\w+)\}/g, (_match, name) => String(params?.[name] ?? ''));
|
|
95
|
+
});
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
let cancelled = false;
|
|
98
|
+
const load = async () => {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(STATUS_PATH, { headers: { accept: 'application/json' } });
|
|
101
|
+
if (!response.ok)
|
|
102
|
+
return;
|
|
103
|
+
const document = await response.json();
|
|
104
|
+
if (!cancelled)
|
|
105
|
+
setStatus(document);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Keep the last value: a failed poll is not worth a blank pill.
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
void load();
|
|
112
|
+
const timer = setInterval(() => { void load(); }, POLL_INTERVAL_MS);
|
|
113
|
+
return () => {
|
|
114
|
+
cancelled = true;
|
|
115
|
+
clearInterval(timer);
|
|
116
|
+
};
|
|
117
|
+
}, []);
|
|
118
|
+
const credits_ = status?.credits;
|
|
119
|
+
const usage = status?.usage;
|
|
120
|
+
const available = credits_?.state === 'ok' && credits_.total !== undefined;
|
|
121
|
+
const label = available
|
|
122
|
+
? t('balance', { total: credits(credits_.total, 2) })
|
|
123
|
+
: t(credits_?.state === 'expired' ? 'expired' : credits_?.state === 'signed-out' ? 'signedOut' : 'unavailable');
|
|
124
|
+
const detail = available
|
|
125
|
+
? h('div', { style: detailStyle, 'data-minimax-credits-detail': true }, h('div', { style: detailRowStyle }, h('span', null, t('free')), h('span', null, credits(credits_.free, 3))), h('div', { style: detailRowStyle }, h('span', null, t('purchased')), h('span', null, credits(credits_.purchased, 3))), credits_.consumed !== undefined && credits_.consumed > 0
|
|
126
|
+
? h('div', { style: detailRowStyle }, h('span', null, t('consumed')), h('span', null, credits(credits_.consumed, 3)))
|
|
127
|
+
: null, usage !== undefined
|
|
128
|
+
? h('div', { style: detailRowStyle }, h('span', null, t('tokens', { count: tokens(usage.inputTokens + usage.outputTokens + usage.cacheReadTokens) })), h('span', null, t('cost', { amount: money(usage.estimatedCny) })))
|
|
129
|
+
: null, usage !== undefined && usage.requests > 0
|
|
130
|
+
? h('div', { style: detailRowStyle }, h('span', null, t('requests', { count: usage.requests })), h('span', null, usage.lastModel ?? ''))
|
|
131
|
+
: null, credits_.updatedAtMs !== undefined
|
|
132
|
+
? h('div', { style: detailRowStyle }, h('span', null, t('updated', { time: new Date(credits_.updatedAtMs).toLocaleTimeString() })), h('span', null, ''))
|
|
133
|
+
: null)
|
|
134
|
+
: null;
|
|
135
|
+
return h('div', { style: wrapStyle, 'data-minimax-credits': true }, h('button', {
|
|
136
|
+
type: 'button',
|
|
137
|
+
style: buttonStyle,
|
|
138
|
+
title: t('title'),
|
|
139
|
+
onClick: () => setOpen(!open),
|
|
140
|
+
}, label), open ? detail : null);
|
|
141
|
+
}
|
|
142
|
+
const wrapStyle = {
|
|
143
|
+
position: 'relative',
|
|
144
|
+
display: 'inline-flex',
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
};
|
|
147
|
+
const buttonStyle = {
|
|
148
|
+
appearance: 'none',
|
|
149
|
+
background: 'transparent',
|
|
150
|
+
border: '1px solid var(--dsh-composer-dock-inset, currentColor)',
|
|
151
|
+
borderRadius: '999px',
|
|
152
|
+
color: 'inherit',
|
|
153
|
+
cursor: 'pointer',
|
|
154
|
+
font: 'inherit',
|
|
155
|
+
fontSize: '12px',
|
|
156
|
+
lineHeight: 1.4,
|
|
157
|
+
opacity: 0.8,
|
|
158
|
+
padding: '1px 8px',
|
|
159
|
+
};
|
|
160
|
+
const detailStyle = {
|
|
161
|
+
position: 'absolute',
|
|
162
|
+
bottom: 'calc(100% + 6px)',
|
|
163
|
+
left: 0,
|
|
164
|
+
zIndex: 10,
|
|
165
|
+
minWidth: '220px',
|
|
166
|
+
borderRadius: '8px',
|
|
167
|
+
background: 'var(--dsh-panel-background, Canvas)',
|
|
168
|
+
color: 'var(--dsh-panel-foreground, CanvasText)',
|
|
169
|
+
border: '1px solid var(--dsh-panel-border, currentColor)',
|
|
170
|
+
boxShadow: '0 6px 20px rgba(0,0,0,0.18)',
|
|
171
|
+
padding: '8px 10px',
|
|
172
|
+
fontSize: '12px',
|
|
173
|
+
lineHeight: 1.6,
|
|
174
|
+
textAlign: 'left',
|
|
175
|
+
};
|
|
176
|
+
const detailRowStyle = {
|
|
177
|
+
display: 'flex',
|
|
178
|
+
justifyContent: 'space-between',
|
|
179
|
+
gap: '16px',
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
inject: ['slots', 'locale'],
|
|
183
|
+
apply(ctx) {
|
|
184
|
+
const context = ctx;
|
|
185
|
+
// Visible text goes through the host's locale service.
|
|
186
|
+
context.effect(() => context.locale.register(NAMESPACE, { zh, en }), `${BUNDLE_NAME}: locale`);
|
|
187
|
+
// The composer dock is the strip under the input box, which is where
|
|
188
|
+
// the host itself renders its activity and usage pills.
|
|
189
|
+
context.slots.inject('conversation.composer.dock', () => context.slots.register({
|
|
190
|
+
name: 'conversation.composer.dock',
|
|
191
|
+
id: 'minimax-credits',
|
|
192
|
+
order: 2,
|
|
193
|
+
locale: NAMESPACE,
|
|
194
|
+
}, CreditPill));
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
})();
|