dsh-connect-minimaxcode 0.3.0 → 0.4.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 +31 -0
- package/lib/client.js +160 -42
- package/lib/index.d.ts +76 -0
- package/lib/index.js +146 -13
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -184,6 +184,28 @@ Content-Type: application/json
|
|
|
184
184
|
| `token_plan_credit.remaining_credits` | 已购积分池 | ❌ |
|
|
185
185
|
| `query_balance.available_amount` | 开放平台钱包(元) | ❌ |
|
|
186
186
|
|
|
187
|
+
### 积分会过期吗?会
|
|
188
|
+
|
|
189
|
+
积分不是一池子,而是**按笔发放的批次**,每批有自己的到期日。客户端每天发一批,每批大约在**发放后 29 天**到期,于是形成一条逐日错开的 30 天阶梯:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
GET https://agent.minimax.cn/minimax-cloud/api/v1/credit/details?page=1&page_size=100
|
|
193
|
+
→ details[] { granted_amount, remaining_amount, consumed_amount, granted_at_ms, expire_at_ms }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
实测本机 23 笔:09-20 发的在 10-19 到期,09-21 发的在 10-20 到期……另有一笔 08-25 发的在 **2027-08-25** 到期(一年期批次)。
|
|
197
|
+
|
|
198
|
+
插件据此在**插件页面**(设置 → 插件 → DSH Connect MiniMax Code)与输入框下方的胶囊里给出:
|
|
199
|
+
|
|
200
|
+
- **7 天内到期**的剩余积分,以及最近一笔的到期日
|
|
201
|
+
- **30 天内到期**的剩余积分
|
|
202
|
+
- 接下来几笔**到期批次**的清单(日期 + 金额)
|
|
203
|
+
- 余额条与本机消耗(token 数、等效金额)
|
|
204
|
+
|
|
205
|
+
只统计**仍有余额**的批次:已发放但用光的批次到期时并没有东西可损失,把它算进去会让每个账号都看起来"快过期了"。
|
|
206
|
+
|
|
207
|
+
> 台账的 `remaining_amount` 合计与余额接口的 `total_remaining_amount` 会**略有出入**(实测差 0.006,浮点累加所致;两次采样之间余额本身也会变)。插件以**余额接口**显示总额,台账只用来回答"什么时候过期"。
|
|
208
|
+
|
|
187
209
|
### 消耗怎么算
|
|
188
210
|
|
|
189
211
|
两路数据各管一件事:
|
|
@@ -199,6 +221,8 @@ Content-Type: application/json
|
|
|
199
221
|
|
|
200
222
|
折算出的金额是**等效金额**,不是扣费:积分与金额之间 MiniMax 没有公开换算关系(套餐只公布了 1×/3×/12× 的相对倍数),所以插件不会把它说成「花了多少积分」。
|
|
201
223
|
|
|
224
|
+
> 官方价目表没有单列**缓存写入**(`cache_creation_input_tokens`)的单价,因此插件只统计它的 token 数、**不折算金额**,以免给出一个没有公开依据的数字。
|
|
225
|
+
|
|
202
226
|
### 关于续期
|
|
203
227
|
|
|
204
228
|
OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自己的令牌已过期时**才用 App 已存的 refresh token 续一次,并把轮换后的新令牌按客户端**原格式原子写回**同一个文件(`generation` 保持不变),因此桌面 App 的登录态不受影响。插件不会发起登录、不会登出、也不会调用任何付费/订阅类接口。
|
|
@@ -219,6 +243,13 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
|
|
|
219
243
|
|
|
220
244
|
## 更新记录
|
|
221
245
|
|
|
246
|
+
### 0.4.0(2026-10-19)
|
|
247
|
+
|
|
248
|
+
- **新增「积分即将过期」**:积分是按笔发放、每批独立到期的(发放后约 29 天)。插件读取积分台账 `GET /minimax-cloud/api/v1/credit/details`,在**插件页面**与胶囊里显示 7 天内到期、30 天内到期、最近一笔到期日与到期批次清单。
|
|
249
|
+
- **只统计仍有余额的批次**,避免把「已用光但快到期」的批次算成损失。
|
|
250
|
+
- **插件页面卡片**:在「设置 → 插件 → DSH Connect MiniMax Code」新增一张卡片(余额、赠送/购买拆分、到期分布、到期批次、本机消耗与等效金额),列表里则显示一行摘要。
|
|
251
|
+
- 胶囊与卡片共用一个状态存储与同一个轮询器,多个界面不会重复请求账户接口。
|
|
252
|
+
|
|
222
253
|
### 0.3.0(2026-10-19)
|
|
223
254
|
|
|
224
255
|
- **新增积分余额显示**:对话框输入框下方的胶囊显示 MiniMax Code 的剩余积分(赠送/购买拆分),点开可看两次读取之间的消耗、本机累计 token 与按官方价目表折算的等效金额。
|
package/lib/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* The browser half:
|
|
3
|
+
* The browser half: the credit balance, in the composer and on the page.
|
|
4
4
|
*
|
|
5
5
|
* The host loads a client bundle as a classic script and calls
|
|
6
6
|
* `window.__ModuleLoader__.load`, handing the factory a `require` that
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* the host — this file installs no React of its own, and ships no bundler
|
|
9
9
|
* output: it is a script, so it can be dropped into the page as-is.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Two surfaces read the same data: a pill in the composer's dock, and a card
|
|
12
|
+
* on the plugin's own page. Both read it through one store with one ticker,
|
|
13
|
+
* so the account API is asked once per interval no matter how many surfaces
|
|
14
|
+
* are on screen.
|
|
15
15
|
*/
|
|
16
16
|
;
|
|
17
17
|
(function bootstrapMinimaxCodeClient() {
|
|
@@ -32,7 +32,13 @@
|
|
|
32
32
|
unavailable: '积分不可用',
|
|
33
33
|
expired: '登录态已过期',
|
|
34
34
|
signedOut: '未登录 MiniMax Code',
|
|
35
|
-
|
|
35
|
+
expiring: '即将过期',
|
|
36
|
+
expiringIn: '{amount} 积分 · {date} 到期',
|
|
37
|
+
expiringNone: '7 天内没有积分到期',
|
|
38
|
+
expiringSoon: '30 天内到期 {amount} 积分',
|
|
39
|
+
nextExpiry: '最近一笔到期',
|
|
40
|
+
lots: '到期批次',
|
|
41
|
+
noLots: '没有待到期的积分批次',
|
|
36
42
|
};
|
|
37
43
|
const en = {
|
|
38
44
|
title: 'MiniMax Code credits',
|
|
@@ -47,7 +53,13 @@
|
|
|
47
53
|
unavailable: 'Credits unavailable',
|
|
48
54
|
expired: 'Sign-in expired',
|
|
49
55
|
signedOut: 'MiniMax Code not signed in',
|
|
50
|
-
|
|
56
|
+
expiring: 'Expiring soon',
|
|
57
|
+
expiringIn: '{amount} credits · expires {date}',
|
|
58
|
+
expiringNone: 'No credits expire within 7 days',
|
|
59
|
+
expiringSoon: '{amount} credits expire within 30 days',
|
|
60
|
+
nextExpiry: 'Next expiry',
|
|
61
|
+
lots: 'Expiring grants',
|
|
62
|
+
noLots: 'No grant is waiting to expire',
|
|
51
63
|
};
|
|
52
64
|
const loader = globalThis.__ModuleLoader__;
|
|
53
65
|
if (loader === undefined)
|
|
@@ -83,38 +95,81 @@
|
|
|
83
95
|
return value.toFixed(4);
|
|
84
96
|
return value.toFixed(2);
|
|
85
97
|
}
|
|
86
|
-
/**
|
|
87
|
-
function
|
|
98
|
+
/** Format a date as a short day. */
|
|
99
|
+
function day(value) {
|
|
100
|
+
if (value === undefined)
|
|
101
|
+
return '—';
|
|
102
|
+
const date = new Date(value);
|
|
103
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The shared status store.
|
|
107
|
+
*
|
|
108
|
+
* One ticker serves every surface, so a second surface costs no extra
|
|
109
|
+
* request. The store starts when the first surface subscribes and stops
|
|
110
|
+
* when the last one leaves.
|
|
111
|
+
*/
|
|
112
|
+
const listeners = new Set();
|
|
113
|
+
let snapshot;
|
|
114
|
+
let timer;
|
|
115
|
+
async function load() {
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(STATUS_PATH, { headers: { accept: 'application/json' } });
|
|
118
|
+
if (!response.ok)
|
|
119
|
+
return;
|
|
120
|
+
const document = await response.json();
|
|
121
|
+
snapshot = document;
|
|
122
|
+
for (const listener of listeners)
|
|
123
|
+
listener(snapshot);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Keep the last value: a failed poll is not worth a blank pill.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function subscribe(listener) {
|
|
130
|
+
listeners.add(listener);
|
|
131
|
+
if (listeners.size === 1) {
|
|
132
|
+
void load();
|
|
133
|
+
timer = setInterval(() => { void load(); }, POLL_INTERVAL_MS);
|
|
134
|
+
}
|
|
135
|
+
return () => {
|
|
136
|
+
listeners.delete(listener);
|
|
137
|
+
if (listeners.size === 0 && timer !== undefined) {
|
|
138
|
+
clearInterval(timer);
|
|
139
|
+
timer = undefined;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Read the shared status into a component's state. */
|
|
144
|
+
function useStatus() {
|
|
88
145
|
const [status, setStatus] = React.useState(undefined);
|
|
146
|
+
React.useEffect(() => subscribe(setStatus), []);
|
|
147
|
+
return status;
|
|
148
|
+
}
|
|
149
|
+
/** A row of label and value. */
|
|
150
|
+
function row(label, value) {
|
|
151
|
+
return h('div', { style: rowStyle }, h('span', null, label), h('span', null, value));
|
|
152
|
+
}
|
|
153
|
+
/** The expiry block, shared by the pill's detail and the page card. */
|
|
154
|
+
function expiryBlock(t, credits_) {
|
|
155
|
+
if (credits_ === undefined)
|
|
156
|
+
return null;
|
|
157
|
+
const expiring = credits_.expiring ?? 0;
|
|
158
|
+
const next = credits_.nextExpiryAtMs;
|
|
159
|
+
return h('div', { style: expiryBoxStyle, 'data-minimax-credits-expiry': true }, h('div', { style: rowStyle }, h('span', null, t('expiring')), h('span', { style: expiring > 0 ? warnStyle : undefined }, expiring > 0
|
|
160
|
+
? t('expiringIn', { amount: credits(expiring, 2), date: day(next) })
|
|
161
|
+
: t('expiringNone'))), credits_.expiringSoon !== undefined && credits_.expiringSoon > 0
|
|
162
|
+
? row(t('expiringSoon', { amount: credits(credits_.expiringSoon, 2) }), '')
|
|
163
|
+
: null, next !== undefined ? row(t('nextExpiry'), day(next)) : null);
|
|
164
|
+
}
|
|
165
|
+
/** The pill in the composer's dock. */
|
|
166
|
+
function CreditPill(props) {
|
|
167
|
+
const status = useStatus();
|
|
89
168
|
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
169
|
const t = props.t ?? ((key, params) => {
|
|
93
170
|
const template = zh[key] ?? key;
|
|
94
171
|
return template.replace(/\{(\w+)\}/g, (_match, name) => String(params?.[name] ?? ''));
|
|
95
172
|
});
|
|
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
173
|
const credits_ = status?.credits;
|
|
119
174
|
const usage = status?.usage;
|
|
120
175
|
const available = credits_?.state === 'ok' && credits_.total !== undefined;
|
|
@@ -122,14 +177,12 @@
|
|
|
122
177
|
? t('balance', { total: credits(credits_.total, 2) })
|
|
123
178
|
: t(credits_?.state === 'expired' ? 'expired' : credits_?.state === 'signed-out' ? 'signedOut' : 'unavailable');
|
|
124
179
|
const detail = available
|
|
125
|
-
? h('div', { style: detailStyle, 'data-minimax-credits-detail': true },
|
|
126
|
-
?
|
|
127
|
-
: null, usage !== undefined
|
|
128
|
-
? h('div', { style:
|
|
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 ?? ''))
|
|
180
|
+
? h('div', { style: detailStyle, 'data-minimax-credits-detail': true }, row(t('free'), credits(credits_.free, 3)), row(t('purchased'), credits(credits_.purchased, 3)), credits_.consumed !== undefined && credits_.consumed > 0
|
|
181
|
+
? row(t('consumed'), credits(credits_.consumed, 3))
|
|
182
|
+
: null, expiryBlock(t, credits_), usage !== undefined
|
|
183
|
+
? h('div', { style: rowStyle }, h('span', null, t('tokens', { count: tokens(usage.inputTokens + usage.outputTokens + usage.cacheReadTokens) })), h('span', null, t('cost', { amount: money(usage.estimatedCny) })))
|
|
131
184
|
: null, credits_.updatedAtMs !== undefined
|
|
132
|
-
?
|
|
185
|
+
? row(t('updated', { time: new Date(credits_.updatedAtMs).toLocaleTimeString() }), '')
|
|
133
186
|
: null)
|
|
134
187
|
: null;
|
|
135
188
|
return h('div', { style: wrapStyle, 'data-minimax-credits': true }, h('button', {
|
|
@@ -139,6 +192,39 @@
|
|
|
139
192
|
onClick: () => setOpen(!open),
|
|
140
193
|
}, label), open ? detail : null);
|
|
141
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The card on the plugin's own page.
|
|
197
|
+
*
|
|
198
|
+
* The summary view is the one line the plugin list shows; the page view
|
|
199
|
+
* is the full card, where the expiry detail belongs.
|
|
200
|
+
*/
|
|
201
|
+
function PluginPage(props) {
|
|
202
|
+
const status = useStatus();
|
|
203
|
+
const t = props.t ?? ((key, params) => {
|
|
204
|
+
const template = zh[key] ?? key;
|
|
205
|
+
return template.replace(/\{(\w+)\}/g, (_match, name) => String(params?.[name] ?? ''));
|
|
206
|
+
});
|
|
207
|
+
const credits_ = status?.credits;
|
|
208
|
+
const usage = status?.usage;
|
|
209
|
+
const available = credits_?.state === 'ok' && credits_.total !== undefined;
|
|
210
|
+
if (props.view === 'summary') {
|
|
211
|
+
if (!available)
|
|
212
|
+
return h('span', null, t('unavailable'));
|
|
213
|
+
return h('span', { 'data-minimax-credits-summary': true }, t('balance', { total: credits(credits_.total, 2) }), credits_.expiring !== undefined && credits_.expiring > 0
|
|
214
|
+
? ` · ${t('expiringIn', { amount: credits(credits_.expiring, 2), date: day(credits_.nextExpiryAtMs) })}`
|
|
215
|
+
: '');
|
|
216
|
+
}
|
|
217
|
+
if (!available) {
|
|
218
|
+
return h('div', { style: cardStyle, 'data-minimax-credits-card': true }, h('div', { style: cardTitleStyle }, t('title')), h('div', { style: rowStyle }, h('span', null, t('unavailable')), h('span', null, credits_?.state === 'expired' ? t('expired') : '')));
|
|
219
|
+
}
|
|
220
|
+
return h('div', { style: cardStyle, 'data-minimax-credits-card': true }, h('div', { style: cardTitleStyle }, t('title')), h('div', { style: bigNumberStyle }, t('balance', { total: credits(credits_.total, 2) })), row(t('free'), credits(credits_.free, 3)), row(t('purchased'), credits(credits_.purchased, 3)), expiryBlock(t, credits_), h('div', { style: sectionTitleStyle }, t('lots')), (credits_.upcoming ?? []).length === 0
|
|
221
|
+
? h('div', { style: mutedStyle }, t('noLots'))
|
|
222
|
+
: h('ul', { style: listStyle }, ...(credits_.upcoming ?? []).map((lot, index) => h('li', { key: `${lot.expireAtMs}-${index}`, style: rowStyle }, h('span', null, day(lot.expireAtMs)), h('span', null, credits(lot.amount, 2))))), usage !== undefined
|
|
223
|
+
? h('div', { style: usageBoxStyle }, row(t('tokens', { count: tokens(usage.inputTokens + usage.outputTokens + usage.cacheReadTokens) }), ''), row(t('cost', { amount: money(usage.estimatedCny) }), ''), usage.requests > 0 ? row(t('requests', { count: usage.requests }), usage.lastModel ?? '') : null)
|
|
224
|
+
: null, credits_.updatedAtMs !== undefined
|
|
225
|
+
? h('div', { style: mutedStyle }, t('updated', { time: new Date(credits_.updatedAtMs).toLocaleTimeString() }))
|
|
226
|
+
: null);
|
|
227
|
+
}
|
|
142
228
|
const wrapStyle = {
|
|
143
229
|
position: 'relative',
|
|
144
230
|
display: 'inline-flex',
|
|
@@ -162,7 +248,7 @@
|
|
|
162
248
|
bottom: 'calc(100% + 6px)',
|
|
163
249
|
left: 0,
|
|
164
250
|
zIndex: 10,
|
|
165
|
-
minWidth: '
|
|
251
|
+
minWidth: '240px',
|
|
166
252
|
borderRadius: '8px',
|
|
167
253
|
background: 'var(--dsh-panel-background, Canvas)',
|
|
168
254
|
color: 'var(--dsh-panel-foreground, CanvasText)',
|
|
@@ -173,11 +259,36 @@
|
|
|
173
259
|
lineHeight: 1.6,
|
|
174
260
|
textAlign: 'left',
|
|
175
261
|
};
|
|
176
|
-
const
|
|
262
|
+
const rowStyle = {
|
|
177
263
|
display: 'flex',
|
|
178
264
|
justifyContent: 'space-between',
|
|
179
265
|
gap: '16px',
|
|
180
266
|
};
|
|
267
|
+
const expiryBoxStyle = {
|
|
268
|
+
marginTop: '6px',
|
|
269
|
+
paddingTop: '6px',
|
|
270
|
+
borderTop: '1px solid var(--dsh-panel-border, currentColor)',
|
|
271
|
+
opacity: 0.95,
|
|
272
|
+
};
|
|
273
|
+
const warnStyle = { color: 'var(--dsh-warning, #b45309)' };
|
|
274
|
+
const cardStyle = {
|
|
275
|
+
display: 'flex',
|
|
276
|
+
flexDirection: 'column',
|
|
277
|
+
gap: '6px',
|
|
278
|
+
fontSize: '13px',
|
|
279
|
+
lineHeight: 1.6,
|
|
280
|
+
};
|
|
281
|
+
const cardTitleStyle = { fontWeight: 600 };
|
|
282
|
+
const bigNumberStyle = { fontSize: '20px', fontWeight: 600, marginBottom: '2px' };
|
|
283
|
+
const sectionTitleStyle = { marginTop: '8px', fontWeight: 600 };
|
|
284
|
+
const mutedStyle = { opacity: 0.7, fontSize: '12px' };
|
|
285
|
+
const listStyle = { listStyle: 'none', margin: 0, padding: 0 };
|
|
286
|
+
const usageBoxStyle = {
|
|
287
|
+
marginTop: '8px',
|
|
288
|
+
paddingTop: '6px',
|
|
289
|
+
borderTop: '1px solid var(--dsh-panel-border, currentColor)',
|
|
290
|
+
opacity: 0.9,
|
|
291
|
+
};
|
|
181
292
|
return {
|
|
182
293
|
inject: ['slots', 'locale'],
|
|
183
294
|
apply(ctx) {
|
|
@@ -192,6 +303,13 @@
|
|
|
192
303
|
order: 2,
|
|
193
304
|
locale: NAMESPACE,
|
|
194
305
|
}, CreditPill));
|
|
306
|
+
// The plugin's own page, keyed by the package name, where the
|
|
307
|
+
// balance and the expiry schedule are spelled out.
|
|
308
|
+
context.slots.inject('plugins.bundle.config', () => context.slots.register({
|
|
309
|
+
name: 'plugins.bundle.config',
|
|
310
|
+
key: BUNDLE_NAME,
|
|
311
|
+
locale: NAMESPACE,
|
|
312
|
+
}, PluginPage));
|
|
195
313
|
},
|
|
196
314
|
};
|
|
197
315
|
},
|
package/lib/index.d.ts
CHANGED
|
@@ -308,6 +308,45 @@ interface CreditSnapshot {
|
|
|
308
308
|
*/
|
|
309
309
|
consumed: number;
|
|
310
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* One grant of credits.
|
|
313
|
+
*
|
|
314
|
+
* Credits are granted in lots, and each lot carries its own expiry: the
|
|
315
|
+
* desktop app grants credits daily, and every lot falls due about a month
|
|
316
|
+
* later on a day of its own. Credits in a lot are lost when that day arrives.
|
|
317
|
+
*/
|
|
318
|
+
interface CreditLot {
|
|
319
|
+
/** How much this lot was granted. */
|
|
320
|
+
granted: number;
|
|
321
|
+
/** How much of it is left. */
|
|
322
|
+
remaining: number;
|
|
323
|
+
/** How much of it has been used. */
|
|
324
|
+
consumed: number;
|
|
325
|
+
/** When the lot was granted, in milliseconds since the epoch. */
|
|
326
|
+
grantedAtMs: number;
|
|
327
|
+
/** When the lot expires, in milliseconds since the epoch. */
|
|
328
|
+
expireAtMs: number;
|
|
329
|
+
/** The upstream's kind of lot, when it says. */
|
|
330
|
+
creditType?: number;
|
|
331
|
+
}
|
|
332
|
+
/** What the lots say about credits that are about to be lost. */
|
|
333
|
+
interface ExpirySummary {
|
|
334
|
+
/** Credits in lots that expire within {@link EXPIRING_WINDOW_DAYS} days. */
|
|
335
|
+
expiring: number;
|
|
336
|
+
/** How many lots that is. */
|
|
337
|
+
expiringLots: number;
|
|
338
|
+
/** Credits in lots that expire within {@link EXPIRING_HORIZON_DAYS} days. */
|
|
339
|
+
expiringSoon: number;
|
|
340
|
+
/** When the next lot with credits left expires. */
|
|
341
|
+
nextExpiryAtMs?: number;
|
|
342
|
+
/** The next lots with credits left, soonest first. */
|
|
343
|
+
upcoming: {
|
|
344
|
+
amount: number;
|
|
345
|
+
expireAtMs: number;
|
|
346
|
+
}[];
|
|
347
|
+
/** Credits left across every lot. */
|
|
348
|
+
totalRemaining: number;
|
|
349
|
+
}
|
|
311
350
|
/**
|
|
312
351
|
* Keeps the current balance and the change between reads.
|
|
313
352
|
*
|
|
@@ -320,12 +359,32 @@ declare class MinimaxCredits {
|
|
|
320
359
|
get snapshot(): CreditSnapshot | undefined;
|
|
321
360
|
/** Why the last read failed, when one did. */
|
|
322
361
|
get error(): string | undefined;
|
|
362
|
+
/** The credit lots as last read. */
|
|
363
|
+
get lots(): readonly CreditLot[];
|
|
364
|
+
/** What the lots say about expiry, when the ledger could be read. */
|
|
365
|
+
get expiry(): ExpirySummary | undefined;
|
|
366
|
+
/** When the ledger was last read. */
|
|
367
|
+
get ledgerAtMs(): number | undefined;
|
|
368
|
+
/** Why the ledger could not be read, when it could not. */
|
|
369
|
+
get ledgerError(): string | undefined;
|
|
323
370
|
/**
|
|
324
371
|
* Read the balance and record how much was consumed since the last read.
|
|
325
372
|
*
|
|
373
|
+
* The credit ledger is read alongside it, but a ledger that cannot be read
|
|
374
|
+
* never hides the balance: the two come from different endpoints and only
|
|
375
|
+
* the balance is essential.
|
|
376
|
+
*
|
|
326
377
|
* @returns Whether the balance changed.
|
|
327
378
|
*/
|
|
328
379
|
refresh(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<boolean>;
|
|
380
|
+
/**
|
|
381
|
+
* Read the credit ledger and work out what is about to expire.
|
|
382
|
+
*
|
|
383
|
+
* A failure here is remembered but never thrown: the balance is what the
|
|
384
|
+
* user needs most, and a ledger that is temporarily unavailable must not
|
|
385
|
+
* take it down with it.
|
|
386
|
+
*/
|
|
387
|
+
refreshLedger(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<void>;
|
|
329
388
|
}
|
|
330
389
|
//#endregion
|
|
331
390
|
//#region src/usage.d.ts
|
|
@@ -434,6 +493,23 @@ interface CreditStatus {
|
|
|
434
493
|
updatedAtMs?: number;
|
|
435
494
|
/** Why the balance could not be read. */
|
|
436
495
|
error?: string;
|
|
496
|
+
/** Credits that expire within the next week. */
|
|
497
|
+
expiring?: number;
|
|
498
|
+
/** How many grants that is. */
|
|
499
|
+
expiringLots?: number;
|
|
500
|
+
/** Credits that expire within the next month. */
|
|
501
|
+
expiringSoon?: number;
|
|
502
|
+
/** When the next grant with credits left expires. */
|
|
503
|
+
nextExpiryAtMs?: number;
|
|
504
|
+
/** The next grants with credits left, soonest first. */
|
|
505
|
+
upcoming?: {
|
|
506
|
+
amount: number;
|
|
507
|
+
expireAtMs: number;
|
|
508
|
+
}[];
|
|
509
|
+
/** When the ledger was read, in milliseconds since the epoch. */
|
|
510
|
+
ledgerAtMs?: number;
|
|
511
|
+
/** Why the ledger could not be read. */
|
|
512
|
+
ledgerError?: string;
|
|
437
513
|
}
|
|
438
514
|
/** What the plugin's own requests have used. */
|
|
439
515
|
interface UsageStatus {
|
package/lib/index.js
CHANGED
|
@@ -716,6 +716,8 @@ const MINIMAX_ACCOUNT_HOSTS = {
|
|
|
716
716
|
};
|
|
717
717
|
/** Membership endpoint: carries the credit summary. */
|
|
718
718
|
const MEMBERSHIP_PATH = "/matrix/api/v1/commerce/get_membership_info";
|
|
719
|
+
/** Credit ledger endpoint: per-grant amounts and expiry. */
|
|
720
|
+
const CREDIT_DETAILS_PATH = "/minimax-cloud/api/v1/credit/details";
|
|
719
721
|
/** OAuth token endpoint. */
|
|
720
722
|
const TOKEN_PATH = "/oauth2/token";
|
|
721
723
|
/** Absolute path of the app's credential file for a region. */
|
|
@@ -961,6 +963,84 @@ async function fetchCreditSummary(credential, region, fetchImpl = fetch) {
|
|
|
961
963
|
return parseCreditSummary(await response.json());
|
|
962
964
|
}
|
|
963
965
|
/**
|
|
966
|
+
* Read the credit lots out of a ledger response.
|
|
967
|
+
*
|
|
968
|
+
* Lots that carry no expiry are ignored: a lot without an expiry date would
|
|
969
|
+
* otherwise be treated as one that never expires.
|
|
970
|
+
*/
|
|
971
|
+
function parseCreditLots(document) {
|
|
972
|
+
if (typeof document !== "object" || document === null) return [];
|
|
973
|
+
const details = document.details;
|
|
974
|
+
if (!Array.isArray(details)) return [];
|
|
975
|
+
const lots = [];
|
|
976
|
+
for (const entry of details) {
|
|
977
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
978
|
+
const record = entry;
|
|
979
|
+
const expireAtMs = toNumber(record.expire_at_ms);
|
|
980
|
+
if (expireAtMs === void 0) continue;
|
|
981
|
+
const granted = toNumber(record.granted_amount) ?? 0;
|
|
982
|
+
const remaining = toNumber(record.remaining_amount) ?? 0;
|
|
983
|
+
lots.push({
|
|
984
|
+
granted,
|
|
985
|
+
remaining,
|
|
986
|
+
consumed: toNumber(record.consumed_amount) ?? Math.max(0, granted - remaining),
|
|
987
|
+
grantedAtMs: toNumber(record.granted_at_ms) ?? 0,
|
|
988
|
+
expireAtMs,
|
|
989
|
+
creditType: toNumber(record.credit_type)
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return lots.sort((a, b) => a.expireAtMs - b.expireAtMs);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Fetch the credit ledger.
|
|
996
|
+
*
|
|
997
|
+
* The ledger is paginated, so every page is read. The number of pages comes
|
|
998
|
+
* from the account's own `total_count` rather than from a guess, and a lot
|
|
999
|
+
* count that stops growing ends the walk.
|
|
1000
|
+
*/
|
|
1001
|
+
async function fetchCreditLots(credential, region, fetchImpl = fetch) {
|
|
1002
|
+
const pageSize = 100;
|
|
1003
|
+
const lots = [];
|
|
1004
|
+
let page = 1;
|
|
1005
|
+
let total = Number.POSITIVE_INFINITY;
|
|
1006
|
+
while (lots.length < total && page <= 50) {
|
|
1007
|
+
const response = await fetchImpl(`${MINIMAX_ACCOUNT_HOSTS[region]}${CREDIT_DETAILS_PATH}?page=${page}&page_size=${pageSize}`, { headers: { authorization: `Bearer ${credential.token}` } });
|
|
1008
|
+
if (!response.ok) throw new Error(`credit ledger request failed: HTTP ${response.status}`);
|
|
1009
|
+
const document = await response.json();
|
|
1010
|
+
const batch = parseCreditLots(document);
|
|
1011
|
+
lots.push(...batch);
|
|
1012
|
+
const count = toNumber(document.total_count);
|
|
1013
|
+
if (count !== void 0) total = count;
|
|
1014
|
+
if (batch.length < pageSize) break;
|
|
1015
|
+
page += 1;
|
|
1016
|
+
}
|
|
1017
|
+
return lots;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Summarize what is about to expire.
|
|
1021
|
+
*
|
|
1022
|
+
* Only lots that still hold credits count: a lot that was granted and used up
|
|
1023
|
+
* expires with nothing to lose, and counting it would make every account look
|
|
1024
|
+
* like it is about to lose credits.
|
|
1025
|
+
*/
|
|
1026
|
+
function summarizeExpiry(lots, now = Date.now()) {
|
|
1027
|
+
const alive = lots.filter((lot) => lot.remaining > 0 && lot.expireAtMs > now);
|
|
1028
|
+
const within = (days) => alive.filter((lot) => lot.expireAtMs - now <= days * 864e5);
|
|
1029
|
+
const week = within(7);
|
|
1030
|
+
const month = within(30);
|
|
1031
|
+
return {
|
|
1032
|
+
expiring: week.reduce((sum, lot) => sum + lot.remaining, 0),
|
|
1033
|
+
expiringLots: week.length,
|
|
1034
|
+
expiringSoon: month.reduce((sum, lot) => sum + lot.remaining, 0),
|
|
1035
|
+
nextExpiryAtMs: alive[0]?.expireAtMs,
|
|
1036
|
+
upcoming: alive.slice(0, 5).map((lot) => ({
|
|
1037
|
+
amount: lot.remaining,
|
|
1038
|
+
expireAtMs: lot.expireAtMs
|
|
1039
|
+
})),
|
|
1040
|
+
totalRemaining: lots.reduce((sum, lot) => sum + lot.remaining, 0)
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
964
1044
|
* Keeps the current balance and the change between reads.
|
|
965
1045
|
*
|
|
966
1046
|
* The first read has nothing to compare against, so it reports no movement
|
|
@@ -969,6 +1049,10 @@ async function fetchCreditSummary(credential, region, fetchImpl = fetch) {
|
|
|
969
1049
|
var MinimaxCredits = class {
|
|
970
1050
|
#snapshot;
|
|
971
1051
|
#error;
|
|
1052
|
+
#lots = [];
|
|
1053
|
+
#expiry;
|
|
1054
|
+
#ledgerAtMs;
|
|
1055
|
+
#ledgerError;
|
|
972
1056
|
/** The balance as last read. */
|
|
973
1057
|
get snapshot() {
|
|
974
1058
|
return this.#snapshot;
|
|
@@ -977,29 +1061,68 @@ var MinimaxCredits = class {
|
|
|
977
1061
|
get error() {
|
|
978
1062
|
return this.#error;
|
|
979
1063
|
}
|
|
1064
|
+
/** The credit lots as last read. */
|
|
1065
|
+
get lots() {
|
|
1066
|
+
return this.#lots;
|
|
1067
|
+
}
|
|
1068
|
+
/** What the lots say about expiry, when the ledger could be read. */
|
|
1069
|
+
get expiry() {
|
|
1070
|
+
return this.#expiry;
|
|
1071
|
+
}
|
|
1072
|
+
/** When the ledger was last read. */
|
|
1073
|
+
get ledgerAtMs() {
|
|
1074
|
+
return this.#ledgerAtMs;
|
|
1075
|
+
}
|
|
1076
|
+
/** Why the ledger could not be read, when it could not. */
|
|
1077
|
+
get ledgerError() {
|
|
1078
|
+
return this.#ledgerError;
|
|
1079
|
+
}
|
|
980
1080
|
/**
|
|
981
1081
|
* Read the balance and record how much was consumed since the last read.
|
|
982
1082
|
*
|
|
1083
|
+
* The credit ledger is read alongside it, but a ledger that cannot be read
|
|
1084
|
+
* never hides the balance: the two come from different endpoints and only
|
|
1085
|
+
* the balance is essential.
|
|
1086
|
+
*
|
|
983
1087
|
* @returns Whether the balance changed.
|
|
984
1088
|
*/
|
|
985
1089
|
async refresh(credential, region, fetchImpl = fetch, now = Date.now()) {
|
|
1090
|
+
let changed = false;
|
|
986
1091
|
try {
|
|
987
1092
|
const summary = await fetchCreditSummary(credential, region, fetchImpl);
|
|
988
|
-
if (summary === void 0)
|
|
989
|
-
|
|
990
|
-
|
|
1093
|
+
if (summary === void 0) this.#error = "membership response carried no credit summary";
|
|
1094
|
+
else {
|
|
1095
|
+
const previous = this.#snapshot;
|
|
1096
|
+
this.#snapshot = {
|
|
1097
|
+
summary,
|
|
1098
|
+
fetchedAtMs: now,
|
|
1099
|
+
consumed: previous === void 0 ? 0 : previous.summary.total - summary.total
|
|
1100
|
+
};
|
|
1101
|
+
this.#error = void 0;
|
|
1102
|
+
changed = previous === void 0 || previous.summary.total !== summary.total;
|
|
991
1103
|
}
|
|
992
|
-
const previous = this.#snapshot;
|
|
993
|
-
this.#snapshot = {
|
|
994
|
-
summary,
|
|
995
|
-
fetchedAtMs: now,
|
|
996
|
-
consumed: previous === void 0 ? 0 : previous.summary.total - summary.total
|
|
997
|
-
};
|
|
998
|
-
this.#error = void 0;
|
|
999
|
-
return previous === void 0 || previous.summary.total !== summary.total;
|
|
1000
1104
|
} catch (error) {
|
|
1001
1105
|
this.#error = error instanceof Error ? error.message : String(error);
|
|
1002
|
-
|
|
1106
|
+
}
|
|
1107
|
+
await this.refreshLedger(credential, region, fetchImpl, now);
|
|
1108
|
+
return changed;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Read the credit ledger and work out what is about to expire.
|
|
1112
|
+
*
|
|
1113
|
+
* A failure here is remembered but never thrown: the balance is what the
|
|
1114
|
+
* user needs most, and a ledger that is temporarily unavailable must not
|
|
1115
|
+
* take it down with it.
|
|
1116
|
+
*/
|
|
1117
|
+
async refreshLedger(credential, region, fetchImpl = fetch, now = Date.now()) {
|
|
1118
|
+
try {
|
|
1119
|
+
const lots = await fetchCreditLots(credential, region, fetchImpl);
|
|
1120
|
+
this.#lots = lots;
|
|
1121
|
+
this.#expiry = summarizeExpiry(lots, now);
|
|
1122
|
+
this.#ledgerAtMs = now;
|
|
1123
|
+
this.#ledgerError = void 0;
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
this.#ledgerError = error instanceof Error ? error.message : String(error);
|
|
1003
1126
|
}
|
|
1004
1127
|
}
|
|
1005
1128
|
};
|
|
@@ -1206,6 +1329,7 @@ function apply(ctx) {
|
|
|
1206
1329
|
/** The credit balance as the card and the browser half read it. */
|
|
1207
1330
|
const buildCreditStatus = () => {
|
|
1208
1331
|
const snapshot = credits.snapshot;
|
|
1332
|
+
const expiry = credits.expiry;
|
|
1209
1333
|
if (snapshot === void 0) return {
|
|
1210
1334
|
state: creditState,
|
|
1211
1335
|
...credits.error === void 0 ? {} : { error: credits.error }
|
|
@@ -1216,7 +1340,16 @@ function apply(ctx) {
|
|
|
1216
1340
|
purchased: snapshot.summary.purchased,
|
|
1217
1341
|
free: snapshot.summary.free,
|
|
1218
1342
|
consumed: snapshot.consumed,
|
|
1219
|
-
updatedAtMs: snapshot.fetchedAtMs
|
|
1343
|
+
updatedAtMs: snapshot.fetchedAtMs,
|
|
1344
|
+
...expiry === void 0 ? {} : {
|
|
1345
|
+
expiring: expiry.expiring,
|
|
1346
|
+
expiringLots: expiry.expiringLots,
|
|
1347
|
+
expiringSoon: expiry.expiringSoon,
|
|
1348
|
+
nextExpiryAtMs: expiry.nextExpiryAtMs,
|
|
1349
|
+
upcoming: expiry.upcoming
|
|
1350
|
+
},
|
|
1351
|
+
...credits.ledgerAtMs === void 0 ? {} : { ledgerAtMs: credits.ledgerAtMs },
|
|
1352
|
+
...credits.ledgerError === void 0 ? {} : { ledgerError: credits.ledgerError }
|
|
1220
1353
|
};
|
|
1221
1354
|
};
|
|
1222
1355
|
/** What this host's own requests have used. */
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-connect-minimaxcode",
|
|
3
3
|
"displayName": "DSH Connect MiniMax Code",
|
|
4
|
-
"description": "把本机已登录的 MiniMax Code 桌面 App 的模型接入 DeepSeek Harness:复用其登录态,提供 Anthropic Messages
|
|
5
|
-
"version": "0.
|
|
4
|
+
"description": "把本机已登录的 MiniMax Code 桌面 App 的模型接入 DeepSeek Harness:复用其登录态,提供 Anthropic Messages 兼容接入、动态模型目录与积分余额/到期提示。",
|
|
5
|
+
"version": "0.4.0",
|
|
6
6
|
"author": "JeffTsuiCUG",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"private": false,
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"inject": [
|
|
85
85
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
86
86
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
87
88
|
"@deepseek-ai/dsh-client-locale"
|
|
88
89
|
]
|
|
89
90
|
}
|