@windsland52/maa-log-tools 1.2.1 → 1.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 +13 -1
- package/dist/archiveLimits.d.ts +59 -0
- package/dist/archiveLimits.js +522 -0
- package/dist/boundedFileReader.d.ts +25 -0
- package/dist/boundedFileReader.js +96 -0
- package/dist/cli.js +6 -2
- package/dist/frameworkInput.d.ts +5 -1
- package/dist/frameworkInput.js +33 -54
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/nodeInput.d.ts +27 -1
- package/dist/nodeInput.js +248 -57
- package/dist/runtimeInspection.d.ts +12 -18
- package/dist/runtimeInspection.js +17 -9
- package/package.json +7 -7
package/dist/nodeInput.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lstat, opendir, realpath } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { addArchiveDirectoryEntry, addSelectedEntry, ArchiveLimitError, assertArchiveInputsWithinLimits, createStoredFileMetadata, EMPTY_ARCHIVE_DIRECTORY_BUDGET, EMPTY_EXTRACTION_BUDGET, extractZipEntriesWithinLimits, resolveArchiveLimits, } from './archiveLimits.js';
|
|
4
|
+
import { getFileIdentity, InputFileError, readBoundedRegularFile, sameFileIdentity, } from './boundedFileReader.js';
|
|
5
|
+
export { ArchiveFormatError, ArchiveLimitError, DEFAULT_ARCHIVE_LIMITS, resolveArchiveLimits, } from './archiveLimits.js';
|
|
6
|
+
export { InputFileError } from './boundedFileReader.js';
|
|
4
7
|
const MAIN_LOG_NAMES = ['maa.log', 'maafw.log'];
|
|
5
8
|
const BAK_LOG_NAMES = ['maa.bak.log', 'maafw.bak.log'];
|
|
6
9
|
const SEARCH_TEXT_EXTENSIONS = ['.log', '.txt', '.jsonl'];
|
|
@@ -214,10 +217,92 @@ const sortLogPaths = (paths) => {
|
|
|
214
217
|
return left.localeCompare(right);
|
|
215
218
|
});
|
|
216
219
|
};
|
|
217
|
-
const
|
|
220
|
+
const pathKey = (value) => {
|
|
221
|
+
const resolved = path.resolve(value);
|
|
222
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
223
|
+
};
|
|
224
|
+
const isPathInside = (rootPath, candidatePath) => {
|
|
225
|
+
const relativePath = path.relative(rootPath, candidatePath);
|
|
226
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
227
|
+
};
|
|
228
|
+
const assertPathInsideContext = async (context, fullPath) => {
|
|
229
|
+
const absolutePath = path.resolve(fullPath);
|
|
230
|
+
if (!isPathInside(context.rootPath, absolutePath)) {
|
|
231
|
+
throw new InputFileError('path-escape', fullPath, `Input path escapes the selected root: ${fullPath}`);
|
|
232
|
+
}
|
|
233
|
+
const physicalPath = await realpath(absolutePath);
|
|
234
|
+
if (!isPathInside(context.rootRealPath, physicalPath)) {
|
|
235
|
+
throw new InputFileError('path-escape', fullPath, `Input path resolves outside the selected root: ${fullPath}`);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
export const createNodeInputBudgetContext = async (rootPath, limits, requireDirectoryRoot = true) => {
|
|
239
|
+
const absoluteRoot = path.resolve(rootPath);
|
|
240
|
+
const rootStats = await lstat(absoluteRoot);
|
|
241
|
+
if (rootStats.isSymbolicLink()) {
|
|
242
|
+
throw new InputFileError('symlink', absoluteRoot, `Symbolic-link roots are not allowed: ${absoluteRoot}`);
|
|
243
|
+
}
|
|
244
|
+
if (requireDirectoryRoot && !rootStats.isDirectory()) {
|
|
245
|
+
throw new InputFileError('not-directory', absoluteRoot, `Expected a directory root: ${absoluteRoot}`);
|
|
246
|
+
}
|
|
247
|
+
const physicalRoot = await realpath(absoluteRoot);
|
|
248
|
+
return {
|
|
249
|
+
limits,
|
|
250
|
+
rootPath: absoluteRoot,
|
|
251
|
+
rootRealPath: physicalRoot,
|
|
252
|
+
directory: EMPTY_ARCHIVE_DIRECTORY_BUDGET,
|
|
253
|
+
extraction: EMPTY_EXTRACTION_BUDGET,
|
|
254
|
+
chargedPaths: new Set(),
|
|
255
|
+
discoveredIdentities: new Map(),
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
const chargePath = (context, fullPath) => {
|
|
259
|
+
const key = pathKey(fullPath);
|
|
260
|
+
if (context.chargedPaths.has(key))
|
|
261
|
+
return;
|
|
262
|
+
const relativePath = toPosixPath(path.relative(context.rootPath, path.resolve(fullPath)));
|
|
263
|
+
context.directory = addArchiveDirectoryEntry(context.directory, {
|
|
264
|
+
name: relativePath,
|
|
265
|
+
size: 0,
|
|
266
|
+
originalSize: 0,
|
|
267
|
+
compression: 0,
|
|
268
|
+
}, context.limits);
|
|
269
|
+
context.chargedPaths.add(key);
|
|
270
|
+
};
|
|
271
|
+
const recordDiscoveredIdentity = (context, fullPath, stats) => {
|
|
272
|
+
const key = pathKey(fullPath);
|
|
273
|
+
const identity = getFileIdentity(stats);
|
|
274
|
+
const previous = context.discoveredIdentities.get(key);
|
|
275
|
+
if (previous && !sameFileIdentity(previous, identity)) {
|
|
276
|
+
throw new InputFileError('identity-changed', fullPath, `Input identity changed during directory analysis: ${fullPath}`);
|
|
277
|
+
}
|
|
278
|
+
context.discoveredIdentities.set(key, identity);
|
|
279
|
+
};
|
|
280
|
+
const inspectDirectoryEntry = async (context, fullPath) => {
|
|
281
|
+
const stats = await lstat(fullPath);
|
|
282
|
+
chargePath(context, fullPath);
|
|
283
|
+
if (stats.isSymbolicLink()) {
|
|
284
|
+
throw new InputFileError('symlink', fullPath, `Symbolic-link entries are not allowed: ${fullPath}`);
|
|
285
|
+
}
|
|
286
|
+
await assertPathInsideContext(context, fullPath);
|
|
287
|
+
recordDiscoveredIdentity(context, fullPath, stats);
|
|
288
|
+
return stats;
|
|
289
|
+
};
|
|
290
|
+
const readNodeTextFileWithinBudget = async (filePath, context) => {
|
|
291
|
+
chargePath(context, filePath);
|
|
292
|
+
await assertPathInsideContext(context, filePath);
|
|
293
|
+
const remainingBytes = context.limits.maxExtractedBytes - context.extraction.extractedBytes;
|
|
294
|
+
const maxBytes = Math.min(context.limits.maxFileBytes, remainingBytes);
|
|
295
|
+
const limitCode = context.limits.maxFileBytes <= remainingBytes ? 'file-size' : 'extracted-size';
|
|
296
|
+
const bytes = await readBoundedRegularFile(filePath, maxBytes, (actualBytes) => new ArchiveLimitError(limitCode, limitCode === 'extracted-size'
|
|
297
|
+
? context.extraction.extractedBytes + actualBytes
|
|
298
|
+
: actualBytes, limitCode === 'extracted-size' ? context.limits.maxExtractedBytes : context.limits.maxFileBytes), { expectedIdentity: context.discoveredIdentities.get(pathKey(filePath)) });
|
|
299
|
+
context.extraction = addSelectedEntry(context.extraction, createStoredFileMetadata(toPosixPath(filePath), bytes.byteLength), context.limits, false);
|
|
300
|
+
return decodeNodeBytes(bytes);
|
|
301
|
+
};
|
|
302
|
+
const collectFocusedFileContents = async (logPaths, focus, context) => {
|
|
218
303
|
const chunks = [];
|
|
219
304
|
for (const logPath of sortLogPaths(logPaths)) {
|
|
220
|
-
const content = await
|
|
305
|
+
const content = await readNodeTextFileWithinBudget(logPath, context);
|
|
221
306
|
if (!contentMatchesFocus(content, focus))
|
|
222
307
|
continue;
|
|
223
308
|
chunks.push({
|
|
@@ -282,14 +367,26 @@ const buildDefaultZipContent = (entries, paths, basePath, sourceRef) => {
|
|
|
282
367
|
}
|
|
283
368
|
return joinMergedWithSources(chunks);
|
|
284
369
|
};
|
|
285
|
-
export const readNodeTextFileContent = async (filePath) => {
|
|
286
|
-
const
|
|
287
|
-
|
|
370
|
+
export const readNodeTextFileContent = async (filePath, options = {}) => {
|
|
371
|
+
const limits = resolveArchiveLimits(options.archiveLimits);
|
|
372
|
+
const context = options.budgetContext ?? await createNodeInputBudgetContext(path.dirname(path.resolve(filePath)), limits);
|
|
373
|
+
return readNodeTextFileWithinBudget(filePath, context);
|
|
374
|
+
};
|
|
375
|
+
export const readNodeTextFilesContent = async (filePaths, options = {}) => {
|
|
376
|
+
const limits = resolveArchiveLimits(options.archiveLimits);
|
|
377
|
+
const commonRoot = filePaths.length > 0
|
|
378
|
+
? path.dirname(path.resolve(filePaths[0]))
|
|
379
|
+
: process.cwd();
|
|
380
|
+
const context = options.budgetContext ?? await createNodeInputBudgetContext(commonRoot, limits);
|
|
381
|
+
const contents = [];
|
|
382
|
+
for (const filePath of filePaths) {
|
|
383
|
+
contents.push(await readNodeTextFileWithinBudget(filePath, context));
|
|
384
|
+
}
|
|
385
|
+
return contents;
|
|
288
386
|
};
|
|
289
387
|
export const extractZipContentFromNodeBuffer = (zipData, sourceRef = 'memory.zip', options = {}) => {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
});
|
|
388
|
+
const limits = resolveArchiveLimits(options.archiveLimits);
|
|
389
|
+
const { files } = extractZipEntriesWithinLimits(zipData, isNeededZipEntry, limits);
|
|
293
390
|
const paths = Object.keys(files);
|
|
294
391
|
const basePath = findBaseDirectory(paths);
|
|
295
392
|
if (basePath == null)
|
|
@@ -343,71 +440,163 @@ export const extractZipContentFromNodeBuffer = (zipData, sourceRef = 'memory.zip
|
|
|
343
440
|
return { content: merged.content, sourceSegments: merged.segments, errorImages, visionImages, waitFreezesImages, textFiles };
|
|
344
441
|
};
|
|
345
442
|
export const extractZipContentFromNodeFile = async (zipFilePath, options = {}) => {
|
|
346
|
-
const
|
|
347
|
-
|
|
443
|
+
const limits = resolveArchiveLimits(options.archiveLimits);
|
|
444
|
+
const bytes = await readNodeArchiveFileBytes(zipFilePath, limits);
|
|
445
|
+
return extractZipContentFromNodeBuffer(bytes, zipFilePath, {
|
|
446
|
+
...options,
|
|
447
|
+
archiveLimits: limits,
|
|
448
|
+
});
|
|
348
449
|
};
|
|
349
|
-
const
|
|
450
|
+
export const readNodeArchiveFileBytes = async (zipFilePath, limits) => {
|
|
451
|
+
assertArchiveInputsWithinLimits([{ size: 0 }], limits);
|
|
452
|
+
const context = await createNodeInputBudgetContext(path.dirname(path.resolve(zipFilePath)), limits);
|
|
453
|
+
chargePath(context, zipFilePath);
|
|
454
|
+
await assertPathInsideContext(context, zipFilePath);
|
|
455
|
+
const bytes = await readBoundedRegularFile(zipFilePath, limits.maxCompressedBytes, (actualBytes) => new ArchiveLimitError('compressed-size', actualBytes, limits.maxCompressedBytes));
|
|
456
|
+
assertArchiveInputsWithinLimits([{ size: bytes.byteLength }], limits);
|
|
457
|
+
return bytes;
|
|
458
|
+
};
|
|
459
|
+
const assertDirectoryIdentity = (directoryPath, expected, stats) => {
|
|
460
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
461
|
+
throw new InputFileError('not-directory', directoryPath, `Expected a stable directory: ${directoryPath}`);
|
|
462
|
+
}
|
|
463
|
+
if (!sameFileIdentity(expected, getFileIdentity(stats))) {
|
|
464
|
+
throw new InputFileError('identity-changed', directoryPath, `Directory identity changed during traversal: ${directoryPath}`);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
const inspectDirectory = async (context, directoryPath) => {
|
|
468
|
+
if (pathKey(directoryPath) !== pathKey(context.rootPath))
|
|
469
|
+
chargePath(context, directoryPath);
|
|
470
|
+
const stats = await lstat(directoryPath);
|
|
471
|
+
if (stats.isSymbolicLink()) {
|
|
472
|
+
throw new InputFileError('symlink', directoryPath, `Symbolic-link directories are not allowed: ${directoryPath}`);
|
|
473
|
+
}
|
|
474
|
+
if (!stats.isDirectory()) {
|
|
475
|
+
throw new InputFileError('not-directory', directoryPath, `Expected a directory: ${directoryPath}`);
|
|
476
|
+
}
|
|
477
|
+
await assertPathInsideContext(context, directoryPath);
|
|
478
|
+
recordDiscoveredIdentity(context, directoryPath, stats);
|
|
479
|
+
return stats;
|
|
480
|
+
};
|
|
481
|
+
const withSafeDirectory = async (context, directoryPath, consume) => {
|
|
482
|
+
const beforeOpen = await inspectDirectory(context, directoryPath);
|
|
483
|
+
const expectedIdentity = getFileIdentity(beforeOpen);
|
|
484
|
+
const directory = await opendir(directoryPath);
|
|
485
|
+
const afterOpen = await lstat(directoryPath);
|
|
486
|
+
assertDirectoryIdentity(directoryPath, expectedIdentity, afterOpen);
|
|
350
487
|
try {
|
|
351
|
-
await
|
|
488
|
+
return await consume(directory);
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
await directory.close().catch(() => undefined);
|
|
492
|
+
const afterRead = await lstat(directoryPath);
|
|
493
|
+
assertDirectoryIdentity(directoryPath, expectedIdentity, afterRead);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const tryInspectDirectory = async (context, directoryPath) => {
|
|
497
|
+
try {
|
|
498
|
+
await inspectDirectory(context, directoryPath);
|
|
352
499
|
return true;
|
|
353
500
|
}
|
|
354
|
-
catch {
|
|
355
|
-
|
|
501
|
+
catch (error) {
|
|
502
|
+
if (error.code === 'ENOENT')
|
|
503
|
+
return false;
|
|
504
|
+
throw error;
|
|
356
505
|
}
|
|
357
506
|
};
|
|
358
|
-
const
|
|
507
|
+
export const hasNodeMainLogInDirectory = async (context, directoryPath) => {
|
|
508
|
+
if (!await tryInspectDirectory(context, directoryPath))
|
|
509
|
+
return false;
|
|
359
510
|
for (const name of MAIN_LOG_NAMES) {
|
|
360
|
-
|
|
361
|
-
|
|
511
|
+
const candidatePath = path.join(directoryPath, name);
|
|
512
|
+
try {
|
|
513
|
+
const stats = await inspectDirectoryEntry(context, candidatePath);
|
|
514
|
+
if (stats.isFile())
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (error.code === 'ENOENT')
|
|
519
|
+
continue;
|
|
520
|
+
throw error;
|
|
362
521
|
}
|
|
363
522
|
}
|
|
364
523
|
return false;
|
|
365
524
|
};
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
525
|
+
export const findExistingRegularNodeFile = async (context, directoryPath, names) => {
|
|
526
|
+
for (const name of names) {
|
|
527
|
+
const candidatePath = path.join(directoryPath, name);
|
|
528
|
+
try {
|
|
529
|
+
const stats = await inspectDirectoryEntry(context, candidatePath);
|
|
530
|
+
if (stats.isFile())
|
|
531
|
+
return candidatePath;
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
if (error.code === 'ENOENT')
|
|
535
|
+
continue;
|
|
536
|
+
throw error;
|
|
374
537
|
}
|
|
375
|
-
const nested = await findDebugDirectoryRecursively(subDirPath);
|
|
376
|
-
if (nested)
|
|
377
|
-
return nested;
|
|
378
538
|
}
|
|
379
539
|
return null;
|
|
380
540
|
};
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
541
|
+
const findDebugDirectoryRecursively = async (rootPath, context) => {
|
|
542
|
+
const pending = [rootPath];
|
|
543
|
+
while (pending.length > 0) {
|
|
544
|
+
const currentPath = pending.pop();
|
|
545
|
+
if (!currentPath)
|
|
546
|
+
break;
|
|
547
|
+
const found = await withSafeDirectory(context, currentPath, async (directory) => {
|
|
548
|
+
for await (const entry of directory) {
|
|
549
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
550
|
+
const stats = await inspectDirectoryEntry(context, fullPath);
|
|
551
|
+
if (!stats.isDirectory())
|
|
552
|
+
continue;
|
|
553
|
+
if (await hasNodeMainLogInDirectory(context, fullPath))
|
|
554
|
+
return fullPath;
|
|
555
|
+
pending.push(fullPath);
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
});
|
|
559
|
+
if (found)
|
|
560
|
+
return found;
|
|
384
561
|
}
|
|
562
|
+
return null;
|
|
563
|
+
};
|
|
564
|
+
export const resolveNodeDebugDirectory = async (inputPath, context) => {
|
|
565
|
+
if (await hasNodeMainLogInDirectory(context, inputPath))
|
|
566
|
+
return inputPath;
|
|
385
567
|
const directDebugPath = path.join(inputPath, 'debug');
|
|
386
|
-
if (await
|
|
568
|
+
if (await hasNodeMainLogInDirectory(context, directDebugPath))
|
|
387
569
|
return directDebugPath;
|
|
388
|
-
|
|
389
|
-
return findDebugDirectoryRecursively(inputPath);
|
|
570
|
+
return findDebugDirectoryRecursively(inputPath, context);
|
|
390
571
|
};
|
|
391
|
-
const collectFilesRecursively = async (rootPath) => {
|
|
572
|
+
const collectFilesRecursively = async (rootPath, context) => {
|
|
392
573
|
const collected = [];
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
574
|
+
const pending = [rootPath];
|
|
575
|
+
while (pending.length > 0) {
|
|
576
|
+
const currentPath = pending.pop();
|
|
577
|
+
if (!currentPath)
|
|
578
|
+
break;
|
|
579
|
+
await withSafeDirectory(context, currentPath, async (directory) => {
|
|
580
|
+
for await (const entry of directory) {
|
|
581
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
582
|
+
const stats = await inspectDirectoryEntry(context, fullPath);
|
|
583
|
+
if (stats.isDirectory()) {
|
|
584
|
+
pending.push(fullPath);
|
|
585
|
+
}
|
|
586
|
+
else if (stats.isFile()) {
|
|
587
|
+
collected.push(fullPath);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
402
591
|
}
|
|
403
592
|
return collected;
|
|
404
593
|
};
|
|
405
594
|
const pickPrimaryLogPath = async (debugPath, allFiles, candidates) => {
|
|
406
595
|
for (const name of candidates) {
|
|
407
596
|
const directPath = path.join(debugPath, name);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
597
|
+
const directMatch = allFiles.find((filePath) => pathKey(filePath) === pathKey(directPath));
|
|
598
|
+
if (directMatch)
|
|
599
|
+
return directMatch;
|
|
411
600
|
}
|
|
412
601
|
const normalizedCandidates = new Set(candidates.map((name) => name.toLowerCase()));
|
|
413
602
|
for (const filePath of allFiles) {
|
|
@@ -418,20 +607,20 @@ const pickPrimaryLogPath = async (debugPath, allFiles, candidates) => {
|
|
|
418
607
|
}
|
|
419
608
|
return null;
|
|
420
609
|
};
|
|
421
|
-
const buildDefaultDirectoryContent = async (debugPath, allFiles) => {
|
|
610
|
+
const buildDefaultDirectoryContent = async (debugPath, allFiles, context) => {
|
|
422
611
|
const bakLogPath = await pickPrimaryLogPath(debugPath, allFiles, BAK_LOG_NAMES);
|
|
423
612
|
const mainLogPath = await pickPrimaryLogPath(debugPath, allFiles, MAIN_LOG_NAMES);
|
|
424
613
|
const chunks = [];
|
|
425
614
|
if (bakLogPath) {
|
|
426
615
|
chunks.push({
|
|
427
|
-
content: await
|
|
616
|
+
content: await readNodeTextFileWithinBudget(bakLogPath, context),
|
|
428
617
|
source: toFileReference(bakLogPath),
|
|
429
618
|
path: toPosixPath(path.relative(debugPath, bakLogPath)),
|
|
430
619
|
});
|
|
431
620
|
}
|
|
432
621
|
if (mainLogPath) {
|
|
433
622
|
chunks.push({
|
|
434
|
-
content: await
|
|
623
|
+
content: await readNodeTextFileWithinBudget(mainLogPath, context),
|
|
435
624
|
source: toFileReference(mainLogPath),
|
|
436
625
|
path: toPosixPath(path.relative(debugPath, mainLogPath)),
|
|
437
626
|
});
|
|
@@ -439,13 +628,15 @@ const buildDefaultDirectoryContent = async (debugPath, allFiles) => {
|
|
|
439
628
|
return joinMergedWithSources(chunks);
|
|
440
629
|
};
|
|
441
630
|
export const loadNodeLogDirectory = async (inputDirectoryPath, options = {}) => {
|
|
442
|
-
const
|
|
631
|
+
const limits = resolveArchiveLimits(options.archiveLimits);
|
|
632
|
+
const context = await createNodeInputBudgetContext(inputDirectoryPath, limits);
|
|
633
|
+
const debugPath = await resolveNodeDebugDirectory(inputDirectoryPath, context);
|
|
443
634
|
if (!debugPath)
|
|
444
635
|
return null;
|
|
445
|
-
const allFiles = await collectFilesRecursively(debugPath);
|
|
636
|
+
const allFiles = await collectFilesRecursively(debugPath, context);
|
|
446
637
|
const merged = options.focus
|
|
447
|
-
? await collectFocusedFileContents(allFiles.filter((filePath) => isCoreLogName(path.basename(filePath))), options.focus)
|
|
448
|
-
: await buildDefaultDirectoryContent(debugPath, allFiles);
|
|
638
|
+
? await collectFocusedFileContents(allFiles.filter((filePath) => isCoreLogName(path.basename(filePath))), options.focus, context)
|
|
639
|
+
: await buildDefaultDirectoryContent(debugPath, allFiles, context);
|
|
449
640
|
if (!merged.content)
|
|
450
641
|
return null;
|
|
451
642
|
const errorImages = new Map();
|
|
@@ -479,7 +670,7 @@ export const loadNodeLogDirectory = async (inputDirectoryPath, options = {}) =>
|
|
|
479
670
|
textFiles.push({
|
|
480
671
|
path: relativePath,
|
|
481
672
|
name: fileName,
|
|
482
|
-
content: await
|
|
673
|
+
content: await readNodeTextFileWithinBudget(absolutePath, context),
|
|
483
674
|
reference: toFileReference(absolutePath),
|
|
484
675
|
});
|
|
485
676
|
}
|
|
@@ -61,6 +61,15 @@ export interface RecognitionOccurrenceSample {
|
|
|
61
61
|
end: RuntimeEvidencePosition;
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
+
export interface RepeatedNodeOccurrenceSample {
|
|
65
|
+
pattern: string[];
|
|
66
|
+
firstSeenAt: string;
|
|
67
|
+
lastSeenAt: string;
|
|
68
|
+
repeatCount: number;
|
|
69
|
+
durationMs: number;
|
|
70
|
+
termination: 'left_pattern' | 'task_ended' | 'still_repeating_at_log_end';
|
|
71
|
+
evidence: RuntimeEvidencePosition;
|
|
72
|
+
}
|
|
64
73
|
export interface RecognitionActivitySignal extends RuntimeScope {
|
|
65
74
|
signalId: string;
|
|
66
75
|
kind: 'recognition_activity';
|
|
@@ -116,24 +125,9 @@ export interface RepeatedNodeSequenceSignal extends RuntimeScope {
|
|
|
116
125
|
stillRepeatingAtLogEnd: number;
|
|
117
126
|
};
|
|
118
127
|
representatives: {
|
|
119
|
-
first:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
repeatCount: number;
|
|
123
|
-
evidence: RuntimeEvidencePosition;
|
|
124
|
-
};
|
|
125
|
-
longest: {
|
|
126
|
-
firstSeenAt: string;
|
|
127
|
-
lastSeenAt: string;
|
|
128
|
-
repeatCount: number;
|
|
129
|
-
evidence: RuntimeEvidencePosition;
|
|
130
|
-
};
|
|
131
|
-
last: {
|
|
132
|
-
firstSeenAt: string;
|
|
133
|
-
lastSeenAt: string;
|
|
134
|
-
repeatCount: number;
|
|
135
|
-
evidence: RuntimeEvidencePosition;
|
|
136
|
-
};
|
|
128
|
+
first: RepeatedNodeOccurrenceSample;
|
|
129
|
+
longest: RepeatedNodeOccurrenceSample;
|
|
130
|
+
last: RepeatedNodeOccurrenceSample;
|
|
137
131
|
};
|
|
138
132
|
detector: {
|
|
139
133
|
name: 'repeated-completed-node-sequence';
|
|
@@ -395,21 +395,30 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
|
|
|
395
395
|
const last = completed[lastIndex];
|
|
396
396
|
if (!first || !last)
|
|
397
397
|
continue;
|
|
398
|
-
const reachesEnd = lastIndex === completed.length - 1;
|
|
399
398
|
const rawPattern = completed.slice(repeated.start, repeated.start + repeated.length).map(node => node.name);
|
|
400
399
|
const pattern = repeated.length === 1 ? rawPattern : canonicalCycle(rawPattern);
|
|
401
400
|
const kind = repeated.length === 1 ? 'repeated_node' : 'repeated_node_cycle';
|
|
402
401
|
const key = JSON.stringify([kind, pattern]);
|
|
403
402
|
const group = repetitionGroups.get(key) ?? [];
|
|
403
|
+
const timelineLastIndex = timeline.findIndex(item => item.nodeInfo === last);
|
|
404
|
+
const trailing = timelineLastIndex < 0 ? [] : timeline.slice(timelineLastIndex + 1);
|
|
405
|
+
const reachesCompletedEnd = lastIndex === completed.length - 1;
|
|
406
|
+
const continuesAtLogEnd = reachesCompletedEnd
|
|
407
|
+
&& task.status === 'running'
|
|
408
|
+
&& trailing.every((item, offset) => (item.nodeInfo.status === 'running'
|
|
409
|
+
&& item.nodeInfo.name === rawPattern[offset % rawPattern.length]));
|
|
410
|
+
const taskEndedAtPattern = reachesCompletedEnd
|
|
411
|
+
&& timelineLastIndex === timeline.length - 1
|
|
412
|
+
&& task.status !== 'running';
|
|
404
413
|
group.push({
|
|
405
414
|
pattern,
|
|
406
415
|
repeatCount: repeated.count,
|
|
407
416
|
durationMs: elapsed(first.ts, last.end_ts ?? last.ts) ?? 0,
|
|
408
417
|
firstSeenAt: first.ts,
|
|
409
418
|
lastSeenAt: last.end_ts ?? last.ts,
|
|
410
|
-
termination:
|
|
411
|
-
?
|
|
412
|
-
: 'left_pattern',
|
|
419
|
+
termination: continuesAtLogEnd
|
|
420
|
+
? 'still_repeating_at_log_end'
|
|
421
|
+
: taskEndedAtPattern ? 'task_ended' : 'left_pattern',
|
|
413
422
|
evidence: evidenceAt(evidenceIndex, first.ts),
|
|
414
423
|
});
|
|
415
424
|
repetitionGroups.set(key, group);
|
|
@@ -429,8 +438,6 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
|
|
|
429
438
|
const totalRepeatCount = group.reduce((sum, item) => sum + item.repeatCount, 0);
|
|
430
439
|
const maximumRepeatCount = Math.max(...group.map(item => item.repeatCount));
|
|
431
440
|
const repetitionReasons = [];
|
|
432
|
-
if (terminations.leftPattern > 0)
|
|
433
|
-
repetitionReasons.push('incomplete_repetition');
|
|
434
441
|
if (terminations.stillRepeatingAtLogEnd > 0)
|
|
435
442
|
repetitionReasons.push('still_repeating_at_log_end');
|
|
436
443
|
if (maximumRepeatCount >= 10 || totalRepeatCount >= 20)
|
|
@@ -483,8 +490,9 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
|
|
|
483
490
|
const visionImages = imageSets.flatMap(set => set.vision);
|
|
484
491
|
const ownFailures = failures.filter(failure => directFailureIds.includes(failure.failureId));
|
|
485
492
|
const ownSignals = signals.filter(signal => signalIds.includes(signal.signalId));
|
|
486
|
-
const
|
|
487
|
-
.filter((signal) => (signal.kind === 'recognition_activity'))
|
|
493
|
+
const recognitionSignals = ownSignals
|
|
494
|
+
.filter((signal) => (signal.kind === 'recognition_activity'));
|
|
495
|
+
const recognitionActivity = [...recognitionSignals]
|
|
488
496
|
.sort((left, right) => (right.unsuccessfulAttempts.maximum - left.unsuccessfulAttempts.maximum
|
|
489
497
|
|| right.occurrenceCount - left.occurrenceCount))
|
|
490
498
|
.slice(0, 5)
|
|
@@ -519,7 +527,7 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
|
|
|
519
527
|
const statuses = new Set(attempts.map(attempt => attempt.status));
|
|
520
528
|
return statuses.has('failed') && statuses.has('success');
|
|
521
529
|
}).length,
|
|
522
|
-
recognitionActivityGroups:
|
|
530
|
+
recognitionActivityGroups: recognitionSignals.length,
|
|
523
531
|
maximumRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map(attempts => attempts.length)),
|
|
524
532
|
maximumUnsuccessfulRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map(attempts => attempts.filter(attempt => attempt.status === 'failed').length)),
|
|
525
533
|
actionAttempts: timeline.filter(item => item.nodeInfo.action_details != null).length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windsland52/maa-log-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"fflate": "^0.8.2",
|
|
45
|
-
"@windsland52/maa-log-adapter": "1.0
|
|
46
|
-
"@windsland52/maa-log-
|
|
47
|
-
"@windsland52/maa-log-
|
|
48
|
-
"@windsland52/maa-log-runtime": "1.0
|
|
45
|
+
"@windsland52/maa-log-adapter": "1.1.0",
|
|
46
|
+
"@windsland52/maa-log-parser": "1.1.0",
|
|
47
|
+
"@windsland52/maa-log-kernel": "1.0.2",
|
|
48
|
+
"@windsland52/maa-log-runtime": "1.1.0"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
|
-
"node": ">=
|
|
51
|
+
"node": ">=20.18.0"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public"
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"typecheck": "tsc -p ./tsconfig.json",
|
|
67
|
-
"build": "
|
|
67
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p ./tsconfig.build.json && node ../../scripts/fix-esm-imports.mjs ./dist",
|
|
68
68
|
"clean": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\""
|
|
69
69
|
}
|
|
70
70
|
}
|