maxpool 1.24.28 → 1.24.30
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/package.json +1 -1
- package/src/prober.js +27 -3
- package/src/zai-reset-cards.js +23 -13
package/package.json
CHANGED
package/src/prober.js
CHANGED
|
@@ -211,20 +211,44 @@ export class Prober {
|
|
|
211
211
|
} catch { /* best-effort; never let a probe throw */ }
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
/** Execute a z.ai card redeem decided by the policy. Logs before/after.
|
|
214
|
+
/** Execute a z.ai card redeem decided by the policy. Logs before/after.
|
|
215
|
+
* A FAILED redeem backs off exponentially per card (15m, 30m, 1h, cap 6h):
|
|
216
|
+
* 2026-10-22/23, a z.ai API change made every redeem fail "unavailable" and the
|
|
217
|
+
* 2-minute poll retried the same card 519 times over 16h — the exact
|
|
218
|
+
* rapid-retry shape z.ai's usage-risk notice flagged before. A success resets
|
|
219
|
+
* the backoff; the idempotent requestId keeps any overlapping attempt single-spend. */
|
|
215
220
|
async _redeemZaiCard(account, act) {
|
|
216
221
|
try {
|
|
222
|
+
const key = `${account.name}:${act.card.recordId}`; // name, not index: index shifts on account removal
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
const until = this._cardRetryUntil?.get(key);
|
|
225
|
+
if (until && until > now) return; // still backing off
|
|
217
226
|
const q = account.quota || {};
|
|
218
227
|
this.log(`[reset-card] ${account.name}: redeeming ${act.resetType} card ${act.card.recordId} (${act.reason}; ses=${q.providerSes ?? '?'} wk=${q.providerWk ?? '?'})`);
|
|
219
|
-
|
|
228
|
+
let r = await this._withTimeout(redeemResetCard(account.credential, act.card.recordId, act.resetType, { grantType: act.card.grantType, accountKey: account.name }));
|
|
229
|
+
// grantType-less listing + "unavailable" is the exact 2026-10-22 outage shape; GLOBAL
|
|
230
|
+
// is the only grantType z.ai has ever issued and a wrong one answers 400 no-spend
|
|
231
|
+
// (measured, src/zai-reset-cards.js header) — so one fallback retry is safe.
|
|
232
|
+
if (!r?.ok && !act.card.grantType && /unavailable/i.test(r?.error || '')) {
|
|
233
|
+
r = await this._withTimeout(redeemResetCard(account.credential, act.card.recordId, act.resetType, { grantType: 'GLOBAL', accountKey: account.name }));
|
|
234
|
+
}
|
|
220
235
|
if (r?.ok) {
|
|
236
|
+
if (!this._cardRetryUntil) this._cardRetryUntil = new Map();
|
|
237
|
+
this._cardRetryUntil.set(key, 0); // success clears any backoff
|
|
238
|
+
this._cardTries?.set(key, 0); // ...and the failure count (red-team 2026-10-23)
|
|
221
239
|
this.log(`[reset-card] ${account.name}: card ${act.card.recordId} redeemed`);
|
|
222
240
|
if (this.am.applyResetCards) {
|
|
223
241
|
const fresh = await this._withTimeout(listResetCards(account.credential));
|
|
224
242
|
if (fresh) this.am.applyResetCards(account.index, fresh);
|
|
225
243
|
}
|
|
226
244
|
} else {
|
|
227
|
-
this.
|
|
245
|
+
const tries = (this._cardTries?.get(key) || 0) + 1;
|
|
246
|
+
if (!this._cardTries) this._cardTries = new Map();
|
|
247
|
+
this._cardTries.set(key, tries);
|
|
248
|
+
const waitMs = Math.min(6 * 3600_000, 900_000 * 2 ** (tries - 1));
|
|
249
|
+
if (!this._cardRetryUntil) this._cardRetryUntil = new Map();
|
|
250
|
+
this._cardRetryUntil.set(key, now + waitMs);
|
|
251
|
+
this.log(`[reset-card] ${account.name}: redeem FAILED card ${act.card.recordId} (try ${tries}, next attempt in ${Math.round(waitMs / 60000)}min): ${r?.error}`);
|
|
228
252
|
}
|
|
229
253
|
} catch (e) { this.log(`[reset-card] ${account.name}: redeem error ${e.message}`); }
|
|
230
254
|
}
|
package/src/zai-reset-cards.js
CHANGED
|
@@ -44,7 +44,7 @@ export function parseCardExpiry(s) {
|
|
|
44
44
|
return Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]) - 8 * 3600_000;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Normalize a raw card array -> [{recordId, expiresAt, expired}], available only. */
|
|
47
|
+
/** Normalize a raw card array -> [{recordId, grantType, expiresAt, expired}], available only. */
|
|
48
48
|
function normalizeCards(arr, now = Date.now()) {
|
|
49
49
|
const out = [];
|
|
50
50
|
for (const c of (Array.isArray(arr) ? arr : [])) {
|
|
@@ -52,7 +52,7 @@ function normalizeCards(arr, now = Date.now()) {
|
|
|
52
52
|
const recordId = Number(c.recordId);
|
|
53
53
|
if (!Number.isFinite(recordId)) continue;
|
|
54
54
|
const expiresAt = parseCardExpiry(c.expireTime);
|
|
55
|
-
out.push({ recordId, expiresAt, expired: expiresAt != null && expiresAt <= now });
|
|
55
|
+
out.push({ recordId, grantType: typeof c.grantType === 'string' ? c.grantType : undefined, expiresAt, expired: expiresAt != null && expiresAt <= now });
|
|
56
56
|
}
|
|
57
57
|
// Soonest-expiring first — that's the one to spend first.
|
|
58
58
|
out.sort((a, b) => (a.expiresAt ?? Infinity) - (b.expiresAt ?? Infinity));
|
|
@@ -61,10 +61,10 @@ function normalizeCards(arr, now = Date.now()) {
|
|
|
61
61
|
|
|
62
62
|
/** List this account's available reset cards. Zero-spend read.
|
|
63
63
|
* Returns { fiveHour: [], weekly: [] } | { error }. */
|
|
64
|
-
export async function listResetCards(token, { signal } = {}) {
|
|
64
|
+
export async function listResetCards(token, { signal, fetchImpl = fetch } = {}) {
|
|
65
65
|
if (!token) return { error: 'no token' };
|
|
66
66
|
try {
|
|
67
|
-
const res = await
|
|
67
|
+
const res = await fetchImpl(LIST_URL, {
|
|
68
68
|
headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' },
|
|
69
69
|
signal,
|
|
70
70
|
});
|
|
@@ -85,27 +85,37 @@ export async function listResetCards(token, { signal } = {}) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/** Deterministic requestId for a card: retries of the SAME redeem are idempotent
|
|
88
|
-
* server-side, so a network blip mid-redeem can never double-spend.
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
* server-side, so a network blip mid-redeem can never double-spend. The account
|
|
89
|
+
* key scopes it too — recordIds collide across z.ai accounts (card 9 on max+1 AND
|
|
90
|
+
* glm4, 2026-10-23); z.ai idempotency measured per-account, but scoping keeps the
|
|
91
|
+
* two accounts' keys distinct even if that assumption ever changes (red-team).
|
|
92
|
+
* accountKey never changes semantics for a given account, so retries stay stable. */
|
|
93
|
+
function requestIdFor(recordId, accountKey = '') {
|
|
94
|
+
const h = createHash('sha256').update(`maxpool-zai-card-${accountKey}-${recordId}`).digest('hex').slice(0, 24);
|
|
91
95
|
return `maxpool-${h}`;
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
/** Redeem one card. NEVER retries on the wire here (the idempotency key makes a
|
|
95
99
|
* caller-level retry safe). Returns { ok:true, recordId } | { ok:false, error, code }. */
|
|
96
|
-
export async function redeemResetCard(token, recordId, resetType, { signal } = {}) {
|
|
100
|
+
export async function redeemResetCard(token, recordId, resetType, { grantType, accountKey = '', signal, fetchImpl = fetch } = {}) {
|
|
97
101
|
if (resetType !== 'WEEK' && resetType !== 'FIVE_HOUR') {
|
|
98
102
|
return { ok: false, error: `invalid resetType ${resetType}` };
|
|
99
103
|
}
|
|
100
104
|
if (!Number.isFinite(recordId)) return { ok: false, error: 'invalid recordId' };
|
|
101
105
|
try {
|
|
102
|
-
|
|
106
|
+
// grantType is REQUIRED (2026-10-23): z.ai started rejecting card redeems without
|
|
107
|
+
// it ("The specified reset attempt is unavailable") — their web UI sends
|
|
108
|
+
// {targetType, resetType, recordId, requestId, grantType:'GLOBAL'}. A bogus
|
|
109
|
+
// grantType answers "Unsupported coupon grant type" without spending anything.
|
|
110
|
+
const body = {
|
|
111
|
+
recordId, targetType: 'PERSONAL', resetType,
|
|
112
|
+
requestId: requestIdFor(recordId, accountKey),
|
|
113
|
+
};
|
|
114
|
+
if (grantType) body.grantType = grantType;
|
|
115
|
+
const res = await fetchImpl(USE_URL, {
|
|
103
116
|
method: 'POST',
|
|
104
117
|
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
105
|
-
body: JSON.stringify(
|
|
106
|
-
recordId, targetType: 'PERSONAL', resetType,
|
|
107
|
-
requestId: requestIdFor(recordId),
|
|
108
|
-
}),
|
|
118
|
+
body: JSON.stringify(body),
|
|
109
119
|
signal,
|
|
110
120
|
});
|
|
111
121
|
const data = await res.json().catch(() => null);
|