dsh-connect-minimaxcode 0.3.0 → 0.4.1
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 +33 -0
- package/lib/client.js +160 -42
- package/lib/index.d.ts +77 -0
- package/lib/index.js +151 -13
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -184,6 +184,30 @@ Content-Type: application/json
|
|
|
184
184
|
| `token_plan_credit.remaining_credits` | 已购积分池 | ❌ |
|
|
185
185
|
| `query_balance.available_amount` | 开放平台钱包(元) | ❌ |
|
|
186
186
|
|
|
187
|
+
### 积分会过期吗?会
|
|
188
|
+
|
|
189
|
+
积分不是一池子,而是**按笔发放的批次**,每批有自己的到期日。客户端每天发一批,每批在**发放日 + 30 个自然日**到期(按账户所在时区,当日 00:00 失效),于是形成一条逐日错开的 30 天阶梯:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
GET https://agent.minimax.cn/minimax-cloud/api/v1/credit/details?page_number=1&page_size=100
|
|
193
|
+
→ details[] { granted_amount, remaining_amount, consumed_amount, granted_at_ms, expire_at_ms, credit_type }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
实测本机 23 笔:09-20 发的在 10-20 到期、09-21 发的在 10-21 到期……另有一笔 08-25 发的在 2027-08-25 到期(一年期批次,`credit_type` 1=购买 / 2=签到)。**到期日一律读 `expire_at_ms`**,不写死天数——至少存在 30 天与 365 天两种制式。
|
|
197
|
+
|
|
198
|
+
插件据此在**插件页面**(设置 → 插件 → DSH Connect MiniMax Code)与输入框下方的胶囊里给出:
|
|
199
|
+
|
|
200
|
+
- **7 天内到期**的剩余积分,以及最近一笔的到期日
|
|
201
|
+
- **30 天内到期**的剩余积分
|
|
202
|
+
- 接下来几笔**到期批次**的清单(日期 + 金额)
|
|
203
|
+
- 余额条与本机消耗(token 数、等效金额)
|
|
204
|
+
|
|
205
|
+
只统计**仍有余额**的批次:已发放但用光的批次到期时并没有东西可损失,把它算进去会让每个账号都看起来"快过期了"。积分的消耗近似先进先出,所以**"7 天内到期"的金额常常是 0**(快到期的那些基本都已被用完),因此界面同时给出**最早到期时间**,那才是真正有意义的信号。
|
|
206
|
+
|
|
207
|
+
> 过期不会把批次里的 `remaining_amount` 清零(实测会留下 -0.31 这样的负值残留),而是把过期批次**排除在余额之外**;所以总额以余额接口为准。
|
|
208
|
+
>
|
|
209
|
+
> 台账的 `remaining_amount` 合计与余额接口的 `total_remaining_amount` 在**静止期逐位相等**;两次采样之间看到的差异,是账户在这段时间里被真实消耗掉的量。
|
|
210
|
+
|
|
187
211
|
### 消耗怎么算
|
|
188
212
|
|
|
189
213
|
两路数据各管一件事:
|
|
@@ -199,6 +223,8 @@ Content-Type: application/json
|
|
|
199
223
|
|
|
200
224
|
折算出的金额是**等效金额**,不是扣费:积分与金额之间 MiniMax 没有公开换算关系(套餐只公布了 1×/3×/12× 的相对倍数),所以插件不会把它说成「花了多少积分」。
|
|
201
225
|
|
|
226
|
+
> 官方价目表没有单列**缓存写入**(`cache_creation_input_tokens`)的单价,因此插件只统计它的 token 数、**不折算金额**,以免给出一个没有公开依据的数字。
|
|
227
|
+
|
|
202
228
|
### 关于续期
|
|
203
229
|
|
|
204
230
|
OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自己的令牌已过期时**才用 App 已存的 refresh token 续一次,并把轮换后的新令牌按客户端**原格式原子写回**同一个文件(`generation` 保持不变),因此桌面 App 的登录态不受影响。插件不会发起登录、不会登出、也不会调用任何付费/订阅类接口。
|
|
@@ -219,6 +245,13 @@ OAuth 令牌有效期约 1 小时,由客户端自动续期。插件**只在自
|
|
|
219
245
|
|
|
220
246
|
## 更新记录
|
|
221
247
|
|
|
248
|
+
### 0.4.0(2026-10-19)
|
|
249
|
+
|
|
250
|
+
- **新增「积分即将过期」**:积分是按笔发放、每批独立到期的(发放后约 29 天)。插件读取积分台账 `GET /minimax-cloud/api/v1/credit/details`,在**插件页面**与胶囊里显示 7 天内到期、30 天内到期、最近一笔到期日与到期批次清单。
|
|
251
|
+
- **只统计仍有余额的批次**,避免把「已用光但快到期」的批次算成损失。
|
|
252
|
+
- **插件页面卡片**:在「设置 → 插件 → DSH Connect MiniMax Code」新增一张卡片(余额、赠送/购买拆分、到期分布、到期批次、本机消耗与等效金额),列表里则显示一行摘要。
|
|
253
|
+
- 胶囊与卡片共用一个状态存储与同一个轮询器,多个界面不会重复请求账户接口。
|
|
254
|
+
|
|
222
255
|
### 0.3.0(2026-10-19)
|
|
223
256
|
|
|
224
257
|
- **新增积分余额显示**:对话框输入框下方的胶囊显示 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,46 @@ 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 a lot falls due thirty calendar days
|
|
316
|
+
* after the day it was granted, at midnight in the account's own timezone.
|
|
317
|
+
* Credits in a lot are lost when that moment arrives.
|
|
318
|
+
*/
|
|
319
|
+
interface CreditLot {
|
|
320
|
+
/** How much this lot was granted. */
|
|
321
|
+
granted: number;
|
|
322
|
+
/** How much of it is left. */
|
|
323
|
+
remaining: number;
|
|
324
|
+
/** How much of it has been used. */
|
|
325
|
+
consumed: number;
|
|
326
|
+
/** When the lot was granted, in milliseconds since the epoch. */
|
|
327
|
+
grantedAtMs: number;
|
|
328
|
+
/** When the lot expires, in milliseconds since the epoch. */
|
|
329
|
+
expireAtMs: number;
|
|
330
|
+
/** The upstream's kind of lot, when it says. */
|
|
331
|
+
creditType?: number;
|
|
332
|
+
}
|
|
333
|
+
/** What the lots say about credits that are about to be lost. */
|
|
334
|
+
interface ExpirySummary {
|
|
335
|
+
/** Credits in lots that expire within {@link EXPIRING_WINDOW_DAYS} days. */
|
|
336
|
+
expiring: number;
|
|
337
|
+
/** How many lots that is. */
|
|
338
|
+
expiringLots: number;
|
|
339
|
+
/** Credits in lots that expire within {@link EXPIRING_HORIZON_DAYS} days. */
|
|
340
|
+
expiringSoon: number;
|
|
341
|
+
/** When the next lot with credits left expires. */
|
|
342
|
+
nextExpiryAtMs?: number;
|
|
343
|
+
/** The next lots with credits left, soonest first. */
|
|
344
|
+
upcoming: {
|
|
345
|
+
amount: number;
|
|
346
|
+
expireAtMs: number;
|
|
347
|
+
}[];
|
|
348
|
+
/** Credits left across every lot. */
|
|
349
|
+
totalRemaining: number;
|
|
350
|
+
}
|
|
311
351
|
/**
|
|
312
352
|
* Keeps the current balance and the change between reads.
|
|
313
353
|
*
|
|
@@ -320,12 +360,32 @@ declare class MinimaxCredits {
|
|
|
320
360
|
get snapshot(): CreditSnapshot | undefined;
|
|
321
361
|
/** Why the last read failed, when one did. */
|
|
322
362
|
get error(): string | undefined;
|
|
363
|
+
/** The credit lots as last read. */
|
|
364
|
+
get lots(): readonly CreditLot[];
|
|
365
|
+
/** What the lots say about expiry, when the ledger could be read. */
|
|
366
|
+
get expiry(): ExpirySummary | undefined;
|
|
367
|
+
/** When the ledger was last read. */
|
|
368
|
+
get ledgerAtMs(): number | undefined;
|
|
369
|
+
/** Why the ledger could not be read, when it could not. */
|
|
370
|
+
get ledgerError(): string | undefined;
|
|
323
371
|
/**
|
|
324
372
|
* Read the balance and record how much was consumed since the last read.
|
|
325
373
|
*
|
|
374
|
+
* The credit ledger is read alongside it, but a ledger that cannot be read
|
|
375
|
+
* never hides the balance: the two come from different endpoints and only
|
|
376
|
+
* the balance is essential.
|
|
377
|
+
*
|
|
326
378
|
* @returns Whether the balance changed.
|
|
327
379
|
*/
|
|
328
380
|
refresh(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<boolean>;
|
|
381
|
+
/**
|
|
382
|
+
* Read the credit ledger and work out what is about to expire.
|
|
383
|
+
*
|
|
384
|
+
* A failure here is remembered but never thrown: the balance is what the
|
|
385
|
+
* user needs most, and a ledger that is temporarily unavailable must not
|
|
386
|
+
* take it down with it.
|
|
387
|
+
*/
|
|
388
|
+
refreshLedger(credential: OAuthCredential, region: MinimaxRegion, fetchImpl?: typeof fetch, now?: number): Promise<void>;
|
|
329
389
|
}
|
|
330
390
|
//#endregion
|
|
331
391
|
//#region src/usage.d.ts
|
|
@@ -434,6 +494,23 @@ interface CreditStatus {
|
|
|
434
494
|
updatedAtMs?: number;
|
|
435
495
|
/** Why the balance could not be read. */
|
|
436
496
|
error?: string;
|
|
497
|
+
/** Credits that expire within the next week. */
|
|
498
|
+
expiring?: number;
|
|
499
|
+
/** How many grants that is. */
|
|
500
|
+
expiringLots?: number;
|
|
501
|
+
/** Credits that expire within the next month. */
|
|
502
|
+
expiringSoon?: number;
|
|
503
|
+
/** When the next grant with credits left expires. */
|
|
504
|
+
nextExpiryAtMs?: number;
|
|
505
|
+
/** The next grants with credits left, soonest first. */
|
|
506
|
+
upcoming?: {
|
|
507
|
+
amount: number;
|
|
508
|
+
expireAtMs: number;
|
|
509
|
+
}[];
|
|
510
|
+
/** When the ledger was read, in milliseconds since the epoch. */
|
|
511
|
+
ledgerAtMs?: number;
|
|
512
|
+
/** Why the ledger could not be read. */
|
|
513
|
+
ledgerError?: string;
|
|
437
514
|
}
|
|
438
515
|
/** What the plugin's own requests have used. */
|
|
439
516
|
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,89 @@ 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, and the page parameter is `page_number` — `page`
|
|
998
|
+
* is accepted and then ignored, which would make every page return the same
|
|
999
|
+
* rows. The page size is capped upstream at 100.
|
|
1000
|
+
*
|
|
1001
|
+
* The default view already excludes expired grants, which is what this wants:
|
|
1002
|
+
* a grant that expired has nothing left to lose.
|
|
1003
|
+
*/
|
|
1004
|
+
async function fetchCreditLots(credential, region, fetchImpl = fetch) {
|
|
1005
|
+
const pageSize = 100;
|
|
1006
|
+
const lots = [];
|
|
1007
|
+
let page = 1;
|
|
1008
|
+
let total = Number.POSITIVE_INFINITY;
|
|
1009
|
+
while (lots.length < total && page <= 50) {
|
|
1010
|
+
const response = await fetchImpl(`${MINIMAX_ACCOUNT_HOSTS[region]}${CREDIT_DETAILS_PATH}?page_number=${page}&page_size=${pageSize}`, { headers: { authorization: `Bearer ${credential.token}` } });
|
|
1011
|
+
if (!response.ok) throw new Error(`credit ledger request failed: HTTP ${response.status}`);
|
|
1012
|
+
const document = await response.json();
|
|
1013
|
+
const batch = parseCreditLots(document);
|
|
1014
|
+
const before = lots.length;
|
|
1015
|
+
lots.push(...batch);
|
|
1016
|
+
const count = toNumber(document.total_count);
|
|
1017
|
+
if (count !== void 0) total = count;
|
|
1018
|
+
if (batch.length < pageSize) break;
|
|
1019
|
+
if (lots.length === before) break;
|
|
1020
|
+
page += 1;
|
|
1021
|
+
}
|
|
1022
|
+
return lots;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Summarize what is about to expire.
|
|
1026
|
+
*
|
|
1027
|
+
* Only lots that still hold credits count: a lot that was granted and used up
|
|
1028
|
+
* expires with nothing to lose, and counting it would make every account look
|
|
1029
|
+
* like it is about to lose credits.
|
|
1030
|
+
*/
|
|
1031
|
+
function summarizeExpiry(lots, now = Date.now()) {
|
|
1032
|
+
const alive = lots.filter((lot) => lot.remaining > 0 && lot.expireAtMs > now);
|
|
1033
|
+
const within = (days) => alive.filter((lot) => lot.expireAtMs - now <= days * 864e5);
|
|
1034
|
+
const week = within(7);
|
|
1035
|
+
const month = within(30);
|
|
1036
|
+
return {
|
|
1037
|
+
expiring: week.reduce((sum, lot) => sum + lot.remaining, 0),
|
|
1038
|
+
expiringLots: week.length,
|
|
1039
|
+
expiringSoon: month.reduce((sum, lot) => sum + lot.remaining, 0),
|
|
1040
|
+
nextExpiryAtMs: alive[0]?.expireAtMs,
|
|
1041
|
+
upcoming: alive.slice(0, 5).map((lot) => ({
|
|
1042
|
+
amount: lot.remaining,
|
|
1043
|
+
expireAtMs: lot.expireAtMs
|
|
1044
|
+
})),
|
|
1045
|
+
totalRemaining: lots.reduce((sum, lot) => sum + lot.remaining, 0)
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
964
1049
|
* Keeps the current balance and the change between reads.
|
|
965
1050
|
*
|
|
966
1051
|
* The first read has nothing to compare against, so it reports no movement
|
|
@@ -969,6 +1054,10 @@ async function fetchCreditSummary(credential, region, fetchImpl = fetch) {
|
|
|
969
1054
|
var MinimaxCredits = class {
|
|
970
1055
|
#snapshot;
|
|
971
1056
|
#error;
|
|
1057
|
+
#lots = [];
|
|
1058
|
+
#expiry;
|
|
1059
|
+
#ledgerAtMs;
|
|
1060
|
+
#ledgerError;
|
|
972
1061
|
/** The balance as last read. */
|
|
973
1062
|
get snapshot() {
|
|
974
1063
|
return this.#snapshot;
|
|
@@ -977,29 +1066,68 @@ var MinimaxCredits = class {
|
|
|
977
1066
|
get error() {
|
|
978
1067
|
return this.#error;
|
|
979
1068
|
}
|
|
1069
|
+
/** The credit lots as last read. */
|
|
1070
|
+
get lots() {
|
|
1071
|
+
return this.#lots;
|
|
1072
|
+
}
|
|
1073
|
+
/** What the lots say about expiry, when the ledger could be read. */
|
|
1074
|
+
get expiry() {
|
|
1075
|
+
return this.#expiry;
|
|
1076
|
+
}
|
|
1077
|
+
/** When the ledger was last read. */
|
|
1078
|
+
get ledgerAtMs() {
|
|
1079
|
+
return this.#ledgerAtMs;
|
|
1080
|
+
}
|
|
1081
|
+
/** Why the ledger could not be read, when it could not. */
|
|
1082
|
+
get ledgerError() {
|
|
1083
|
+
return this.#ledgerError;
|
|
1084
|
+
}
|
|
980
1085
|
/**
|
|
981
1086
|
* Read the balance and record how much was consumed since the last read.
|
|
982
1087
|
*
|
|
1088
|
+
* The credit ledger is read alongside it, but a ledger that cannot be read
|
|
1089
|
+
* never hides the balance: the two come from different endpoints and only
|
|
1090
|
+
* the balance is essential.
|
|
1091
|
+
*
|
|
983
1092
|
* @returns Whether the balance changed.
|
|
984
1093
|
*/
|
|
985
1094
|
async refresh(credential, region, fetchImpl = fetch, now = Date.now()) {
|
|
1095
|
+
let changed = false;
|
|
986
1096
|
try {
|
|
987
1097
|
const summary = await fetchCreditSummary(credential, region, fetchImpl);
|
|
988
|
-
if (summary === void 0)
|
|
989
|
-
|
|
990
|
-
|
|
1098
|
+
if (summary === void 0) this.#error = "membership response carried no credit summary";
|
|
1099
|
+
else {
|
|
1100
|
+
const previous = this.#snapshot;
|
|
1101
|
+
this.#snapshot = {
|
|
1102
|
+
summary,
|
|
1103
|
+
fetchedAtMs: now,
|
|
1104
|
+
consumed: previous === void 0 ? 0 : previous.summary.total - summary.total
|
|
1105
|
+
};
|
|
1106
|
+
this.#error = void 0;
|
|
1107
|
+
changed = previous === void 0 || previous.summary.total !== summary.total;
|
|
991
1108
|
}
|
|
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
1109
|
} catch (error) {
|
|
1001
1110
|
this.#error = error instanceof Error ? error.message : String(error);
|
|
1002
|
-
|
|
1111
|
+
}
|
|
1112
|
+
await this.refreshLedger(credential, region, fetchImpl, now);
|
|
1113
|
+
return changed;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Read the credit ledger and work out what is about to expire.
|
|
1117
|
+
*
|
|
1118
|
+
* A failure here is remembered but never thrown: the balance is what the
|
|
1119
|
+
* user needs most, and a ledger that is temporarily unavailable must not
|
|
1120
|
+
* take it down with it.
|
|
1121
|
+
*/
|
|
1122
|
+
async refreshLedger(credential, region, fetchImpl = fetch, now = Date.now()) {
|
|
1123
|
+
try {
|
|
1124
|
+
const lots = await fetchCreditLots(credential, region, fetchImpl);
|
|
1125
|
+
this.#lots = lots;
|
|
1126
|
+
this.#expiry = summarizeExpiry(lots, now);
|
|
1127
|
+
this.#ledgerAtMs = now;
|
|
1128
|
+
this.#ledgerError = void 0;
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
this.#ledgerError = error instanceof Error ? error.message : String(error);
|
|
1003
1131
|
}
|
|
1004
1132
|
}
|
|
1005
1133
|
};
|
|
@@ -1206,6 +1334,7 @@ function apply(ctx) {
|
|
|
1206
1334
|
/** The credit balance as the card and the browser half read it. */
|
|
1207
1335
|
const buildCreditStatus = () => {
|
|
1208
1336
|
const snapshot = credits.snapshot;
|
|
1337
|
+
const expiry = credits.expiry;
|
|
1209
1338
|
if (snapshot === void 0) return {
|
|
1210
1339
|
state: creditState,
|
|
1211
1340
|
...credits.error === void 0 ? {} : { error: credits.error }
|
|
@@ -1216,7 +1345,16 @@ function apply(ctx) {
|
|
|
1216
1345
|
purchased: snapshot.summary.purchased,
|
|
1217
1346
|
free: snapshot.summary.free,
|
|
1218
1347
|
consumed: snapshot.consumed,
|
|
1219
|
-
updatedAtMs: snapshot.fetchedAtMs
|
|
1348
|
+
updatedAtMs: snapshot.fetchedAtMs,
|
|
1349
|
+
...expiry === void 0 ? {} : {
|
|
1350
|
+
expiring: expiry.expiring,
|
|
1351
|
+
expiringLots: expiry.expiringLots,
|
|
1352
|
+
expiringSoon: expiry.expiringSoon,
|
|
1353
|
+
nextExpiryAtMs: expiry.nextExpiryAtMs,
|
|
1354
|
+
upcoming: expiry.upcoming
|
|
1355
|
+
},
|
|
1356
|
+
...credits.ledgerAtMs === void 0 ? {} : { ledgerAtMs: credits.ledgerAtMs },
|
|
1357
|
+
...credits.ledgerError === void 0 ? {} : { ledgerError: credits.ledgerError }
|
|
1220
1358
|
};
|
|
1221
1359
|
};
|
|
1222
1360
|
/** 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.1",
|
|
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
|
}
|