dankgrinder 5.21.0 → 5.23.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/lib/commands/inventory.js +29 -5
- package/lib/grinder.js +88 -55
- package/package.json +1 -1
|
@@ -194,7 +194,7 @@ async function enrichItems(items) {
|
|
|
194
194
|
/**
|
|
195
195
|
* Check inventory for all pages and return full item list.
|
|
196
196
|
*/
|
|
197
|
-
async function runInventory({ channel, waitForDankMemer, client, accountId, redis }) {
|
|
197
|
+
async function runInventory({ channel, waitForDankMemer, client, accountId, redis, onPageProgress }) {
|
|
198
198
|
LOG.cmd(`${c.white}${c.bold}pls inv${c.reset}`);
|
|
199
199
|
|
|
200
200
|
await channel.send('pls inv');
|
|
@@ -202,14 +202,21 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
202
202
|
|
|
203
203
|
if (!response) {
|
|
204
204
|
LOG.warn('[inv] No response');
|
|
205
|
-
return { result: 'no response', items: [] };
|
|
205
|
+
return { result: 'no response', items: [], complete: false, pagesVisited: 0, pagesTotal: 0 };
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
if (isHoldTight(response)) {
|
|
209
209
|
const reason = getHoldTightReason(response);
|
|
210
210
|
LOG.warn(`[inv] Hold Tight${reason ? ` (reason: ${reason})` : ''}`);
|
|
211
211
|
await sleep(30000);
|
|
212
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
result: `hold tight (${reason || 'unknown'})`,
|
|
214
|
+
items: [],
|
|
215
|
+
holdTightReason: reason,
|
|
216
|
+
complete: false,
|
|
217
|
+
pagesVisited: 0,
|
|
218
|
+
pagesTotal: 0,
|
|
219
|
+
};
|
|
213
220
|
}
|
|
214
221
|
|
|
215
222
|
if (isCV2(response)) await ensureCV2(response);
|
|
@@ -218,6 +225,9 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
218
225
|
const allItems = [];
|
|
219
226
|
let { page, total } = parsePageInfo(response);
|
|
220
227
|
LOG.info(`[inv] Page ${page}/${total}`);
|
|
228
|
+
if (typeof onPageProgress === 'function') {
|
|
229
|
+
try { onPageProgress({ page, total }); } catch {}
|
|
230
|
+
}
|
|
221
231
|
const visitedPages = new Set([page]);
|
|
222
232
|
|
|
223
233
|
allItems.push(...parseInventoryPage(response));
|
|
@@ -316,6 +326,9 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
316
326
|
visitedPages.add(page);
|
|
317
327
|
pageChanged = true;
|
|
318
328
|
LOG.info(`[inv] Page ${page}/${total}`);
|
|
329
|
+
if (typeof onPageProgress === 'function') {
|
|
330
|
+
try { onPageProgress({ page, total }); } catch {}
|
|
331
|
+
}
|
|
319
332
|
break;
|
|
320
333
|
}
|
|
321
334
|
// Clear CV2 cache again for next retry
|
|
@@ -340,13 +353,19 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
340
353
|
}
|
|
341
354
|
|
|
342
355
|
const items = Object.values(itemMap);
|
|
343
|
-
|
|
356
|
+
const pagesVisited = visitedPages.size;
|
|
357
|
+
const pagesTotal = Math.max(total || 1, 1);
|
|
358
|
+
const complete = page >= total || pagesVisited >= pagesTotal;
|
|
359
|
+
LOG.success(`[inv] Found ${items.length} unique items across ${pagesVisited}/${pagesTotal} pages`);
|
|
360
|
+
if (!complete) {
|
|
361
|
+
LOG.warn(`[inv] Incomplete pagination detected: stopped at page ${page}/${pagesTotal}`);
|
|
362
|
+
}
|
|
344
363
|
|
|
345
364
|
const { totalValue, totalMarket } = await enrichItems(items);
|
|
346
365
|
LOG.info(`[inv] Net value: ${c.bold}${c.green}⏣ ${totalValue.toLocaleString()}${c.reset} Market: ${c.bold}⏣ ${totalMarket.toLocaleString()}${c.reset}`);
|
|
347
366
|
|
|
348
367
|
// Store in Redis — NO EXPIRY (permanent until next update)
|
|
349
|
-
if (redis && accountId) {
|
|
368
|
+
if (complete && redis && accountId) {
|
|
350
369
|
try {
|
|
351
370
|
const payload = {
|
|
352
371
|
items,
|
|
@@ -360,6 +379,8 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
360
379
|
} catch (e) {
|
|
361
380
|
LOG.error(`[inv] Redis store failed: ${e.message}`);
|
|
362
381
|
}
|
|
382
|
+
} else if (!complete) {
|
|
383
|
+
LOG.warn('[inv] Skipping Redis store for incomplete inventory scan');
|
|
363
384
|
}
|
|
364
385
|
|
|
365
386
|
return {
|
|
@@ -368,6 +389,9 @@ async function runInventory({ channel, waitForDankMemer, client, accountId, redi
|
|
|
368
389
|
totalValue,
|
|
369
390
|
totalMarket,
|
|
370
391
|
coins: 0,
|
|
392
|
+
complete,
|
|
393
|
+
pagesVisited,
|
|
394
|
+
pagesTotal,
|
|
371
395
|
};
|
|
372
396
|
}
|
|
373
397
|
|
package/lib/grinder.js
CHANGED
|
@@ -1085,39 +1085,78 @@ class AccountWorker {
|
|
|
1085
1085
|
|
|
1086
1086
|
// ── Check Balance ───────────────────────────────────────────
|
|
1087
1087
|
async checkInventory(options = {}) {
|
|
1088
|
-
const {
|
|
1089
|
-
|
|
1090
|
-
|
|
1088
|
+
const {
|
|
1089
|
+
force = false,
|
|
1090
|
+
startupProgress = null,
|
|
1091
|
+
requireComplete = false,
|
|
1092
|
+
maxAttempts = 1,
|
|
1093
|
+
} = options;
|
|
1094
|
+
if (this._invRunning) return { ok: false, skipped: 'busy' };
|
|
1095
|
+
if (!force && this._lastInvCheck && Date.now() - this._lastInvCheck < 300_000) return { ok: false, skipped: 'recent' };
|
|
1091
1096
|
this._invRunning = true;
|
|
1092
1097
|
this._lastInvCheck = Date.now();
|
|
1093
1098
|
this.busy = true;
|
|
1094
1099
|
try {
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1100
|
+
const tries = Math.max(1, Number.isFinite(maxAttempts) ? maxAttempts : 1);
|
|
1101
|
+
let lastErr = null;
|
|
1102
|
+
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
1103
|
+
if (startupProgress && Number.isInteger(startupProgress.current) && Number.isInteger(startupProgress.total)) {
|
|
1104
|
+
this.log('info', `Checking inventory... (${startupProgress.current}/${startupProgress.total}) [try ${attempt}/${tries}]`);
|
|
1105
|
+
} else {
|
|
1106
|
+
this.log('info', `Checking inventory...${tries > 1 ? ` [try ${attempt}/${tries}]` : ''}`);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
try {
|
|
1110
|
+
const result = await commands.runInventory({
|
|
1111
|
+
channel: this.channel,
|
|
1112
|
+
waitForDankMemer: (timeout) => this.waitForDankMemer(timeout),
|
|
1113
|
+
client: this.client,
|
|
1114
|
+
accountId: this.account.id,
|
|
1115
|
+
redis,
|
|
1116
|
+
onPageProgress: ({ page, total }) => {
|
|
1117
|
+
this.log('info', `Inventory pages: ${page}/${total}`);
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
if (requireComplete && !result.complete) {
|
|
1122
|
+
throw new Error(`incomplete pages (${result.pagesVisited || 0}/${result.pagesTotal || 0})`);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
this.log('success', `Inventory: ${result.items?.length || 0} items, ⏣ ${(result.totalValue || 0).toLocaleString()} net`);
|
|
1126
|
+
try {
|
|
1127
|
+
await fetch(`${API_URL}/api/grinder/inventory`, {
|
|
1128
|
+
method: 'POST',
|
|
1129
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
1130
|
+
body: JSON.stringify({
|
|
1131
|
+
account_id: this.account.id,
|
|
1132
|
+
items: result.items || [],
|
|
1133
|
+
totalValue: result.totalValue || 0,
|
|
1134
|
+
}),
|
|
1135
|
+
});
|
|
1136
|
+
} catch {}
|
|
1137
|
+
|
|
1138
|
+
return {
|
|
1139
|
+
ok: true,
|
|
1140
|
+
complete: !!result.complete,
|
|
1141
|
+
pagesVisited: result.pagesVisited || 0,
|
|
1142
|
+
pagesTotal: result.pagesTotal || 0,
|
|
1143
|
+
attempts: attempt,
|
|
1144
|
+
result,
|
|
1145
|
+
};
|
|
1146
|
+
} catch (e) {
|
|
1147
|
+
lastErr = e;
|
|
1148
|
+
if (attempt < tries) {
|
|
1149
|
+
this.log('warn', `Inventory attempt ${attempt}/${tries} failed (${e.message}). Retrying...`);
|
|
1150
|
+
await sleep(1500 + Math.floor(Math.random() * 1500));
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
throw lastErr || new Error('inventory check failed');
|
|
1119
1157
|
} catch (e) {
|
|
1120
1158
|
this.log('error', `Inventory check failed: ${e.message}`);
|
|
1159
|
+
return { ok: false, error: e.message };
|
|
1121
1160
|
} finally {
|
|
1122
1161
|
this._invRunning = false;
|
|
1123
1162
|
this.busy = false;
|
|
@@ -2418,36 +2457,30 @@ async function start(apiKey, apiUrl) {
|
|
|
2418
2457
|
log('info', `${c.dim}Checking inventory for all ${workers.length} accounts...${c.reset}`);
|
|
2419
2458
|
let invDone = 0;
|
|
2420
2459
|
let invFailed = 0;
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
workers.length
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
const w = workers[i];
|
|
2436
|
-
const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
|
|
2437
|
-
log('info', `${c.dim}[inv-startup] ${i + 1}/${workers.length} ${label} ${c.dim}(runner ${slot})${c.reset}`);
|
|
2438
|
-
try {
|
|
2439
|
-
await w.checkInventory({ force: true, startupProgress: { current: i + 1, total: workers.length } });
|
|
2440
|
-
invDone++;
|
|
2441
|
-
} catch {
|
|
2442
|
-
invFailed++;
|
|
2443
|
-
}
|
|
2444
|
-
const invComplete = invDone + invFailed;
|
|
2445
|
-
log('info', `${c.dim}[inv-startup-progress] ${invComplete}/${workers.length} complete (${invDone} ok, ${invFailed} failed)${c.reset}`);
|
|
2446
|
-
await new Promise(r => setTimeout(r, 200 + Math.floor(Math.random() * 400)));
|
|
2460
|
+
await Promise.all(workers.map(async (w, i) => {
|
|
2461
|
+
const label = w?.username || w?.account?.label || w?.account?.id || `account-${i + 1}`;
|
|
2462
|
+
log('info', `${c.dim}[inv-startup] ${i + 1}/${workers.length} ${label}${c.reset}`);
|
|
2463
|
+
try {
|
|
2464
|
+
const invRes = await w.checkInventory({
|
|
2465
|
+
force: true,
|
|
2466
|
+
startupProgress: { current: i + 1, total: workers.length },
|
|
2467
|
+
requireComplete: true,
|
|
2468
|
+
maxAttempts: 3,
|
|
2469
|
+
});
|
|
2470
|
+
if (invRes?.ok) invDone++;
|
|
2471
|
+
else invFailed++;
|
|
2472
|
+
} catch {
|
|
2473
|
+
invFailed++;
|
|
2447
2474
|
}
|
|
2448
|
-
|
|
2475
|
+
const invComplete = invDone + invFailed;
|
|
2476
|
+
log('info', `${c.dim}[inv-startup-progress] ${invComplete}/${workers.length} complete (${invDone} ok, ${invFailed} failed)${c.reset}`);
|
|
2477
|
+
}));
|
|
2478
|
+
|
|
2479
|
+
if (invFailed > 0) {
|
|
2480
|
+
log('error', `${c.red}Inventory phase incomplete: ${invDone}/${workers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2449
2483
|
|
|
2450
|
-
await Promise.all(Array.from({ length: invConcurrency }, (_, idx) => runInventoryWorker(idx + 1)));
|
|
2451
2484
|
const invSummaryColor = invFailed > 0 ? c.yellow : c.green;
|
|
2452
2485
|
log('success', `${c.dim}Inventory phase complete: ${invSummaryColor}${invDone}/${workers.length}${c.reset}${c.dim} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}. Starting grind loops...${c.reset}`);
|
|
2453
2486
|
|