@windsland52/maa-log-tools 1.2.2 → 1.3.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 +19 -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.js +125 -0
- 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
|
}
|
|
@@ -64,6 +64,39 @@ const imagesFor = (node) => {
|
|
|
64
64
|
vision: attempts.map(item => item.vision_image).filter((item) => Boolean(item)),
|
|
65
65
|
};
|
|
66
66
|
};
|
|
67
|
+
const ownedRecognitionItems = (items) => ((items ?? []).flatMap(item => {
|
|
68
|
+
if (item.type === 'task' || item.type === 'pipeline_node')
|
|
69
|
+
return [];
|
|
70
|
+
return [
|
|
71
|
+
...(item.type === 'recognition' || item.type === 'recognition_node' ? [item] : []),
|
|
72
|
+
...ownedRecognitionItems(item.children),
|
|
73
|
+
];
|
|
74
|
+
}));
|
|
75
|
+
const imagesForFlowItem = (item) => {
|
|
76
|
+
const attempts = ownedRecognitionItems(item.children);
|
|
77
|
+
return {
|
|
78
|
+
error: [item.error_image, ...attempts.map(attempt => attempt.error_image)]
|
|
79
|
+
.filter((image) => Boolean(image)),
|
|
80
|
+
vision: [item.vision_image, ...attempts.map(attempt => attempt.vision_image)]
|
|
81
|
+
.filter((image) => Boolean(image)),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
const hasFailedOwnedAction = (items) => ((items ?? []).some(item => {
|
|
85
|
+
if (item.type === 'task' || item.type === 'pipeline_node')
|
|
86
|
+
return false;
|
|
87
|
+
if ((item.type === 'action' || item.type === 'action_node')
|
|
88
|
+
&& (item.status === 'failed' || item.action_details?.success === false))
|
|
89
|
+
return true;
|
|
90
|
+
return hasFailedOwnedAction(item.children);
|
|
91
|
+
}));
|
|
92
|
+
const hasFailedNestedTask = (items) => ((items ?? []).some(item => (item.type === 'task' ? item.status === 'failed' : hasFailedNestedTask(item.children))));
|
|
93
|
+
const nestedPipelineFailureKind = (item) => {
|
|
94
|
+
if (item.status !== 'failed')
|
|
95
|
+
return null;
|
|
96
|
+
if (item.action_details?.success === false || hasFailedOwnedAction(item.children))
|
|
97
|
+
return 'action_failed';
|
|
98
|
+
return hasFailedNestedTask(item.children) ? null : 'next_list_timeout';
|
|
99
|
+
};
|
|
67
100
|
const scopeFor = (task, sessionId, executionId) => ({
|
|
68
101
|
sessionId,
|
|
69
102
|
executionId,
|
|
@@ -215,7 +248,99 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
|
|
|
215
248
|
const recognitionOccurrences = [];
|
|
216
249
|
const evidenceIndex = buildEvidenceIndex(task);
|
|
217
250
|
const timeline = buildNodeExecutionTimeline(task.nodes, { rootTaskId: task.task_id });
|
|
251
|
+
const seenNestedTasks = new Set();
|
|
252
|
+
const seenNestedPipelines = new Set();
|
|
253
|
+
const inspectNestedTask = (taskItem) => {
|
|
254
|
+
if (seenNestedTasks.has(taskItem.id))
|
|
255
|
+
return;
|
|
256
|
+
seenNestedTasks.add(taskItem.id);
|
|
257
|
+
const taskId = taskItem.task_details?.task_id ?? taskItem.task_id;
|
|
258
|
+
if (taskId == null) {
|
|
259
|
+
inspectNestedTasks(taskItem.children);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const nestedScope = {
|
|
263
|
+
sessionId,
|
|
264
|
+
executionId,
|
|
265
|
+
taskId,
|
|
266
|
+
taskName: taskItem.task_details?.entry ?? taskItem.name,
|
|
267
|
+
};
|
|
268
|
+
const nestedDirectFailureIds = [];
|
|
269
|
+
const inspectOwnedItems = (items) => {
|
|
270
|
+
for (const child of items ?? []) {
|
|
271
|
+
if (child.type === 'task') {
|
|
272
|
+
inspectNestedTask(child);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (child.type !== 'pipeline_node') {
|
|
276
|
+
inspectOwnedItems(child.children);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (seenNestedPipelines.has(child.id))
|
|
280
|
+
continue;
|
|
281
|
+
seenNestedPipelines.add(child.id);
|
|
282
|
+
inspectOwnedItems(child.children);
|
|
283
|
+
const failureKind = nestedPipelineFailureKind(child);
|
|
284
|
+
let nodeFailureId = null;
|
|
285
|
+
if (failureKind && child.node_id != null) {
|
|
286
|
+
nodeFailureId = `failure-${failures.length + 1}`;
|
|
287
|
+
const images = imagesForFlowItem(child);
|
|
288
|
+
failures.push({
|
|
289
|
+
...nestedScope,
|
|
290
|
+
failureId: nodeFailureId,
|
|
291
|
+
kind: failureKind,
|
|
292
|
+
nodeId: child.node_id,
|
|
293
|
+
nodeName: child.name,
|
|
294
|
+
startedAt: child.ts,
|
|
295
|
+
endedAt: child.end_ts ?? null,
|
|
296
|
+
errorImages: [...new Set(images.error)],
|
|
297
|
+
visionImages: [...new Set(images.vision)],
|
|
298
|
+
evidence: evidenceAt(evidenceIndex, child.end_ts ?? child.ts),
|
|
299
|
+
});
|
|
300
|
+
nestedDirectFailureIds.push(nodeFailureId);
|
|
301
|
+
}
|
|
302
|
+
if (child.status !== 'success') {
|
|
303
|
+
const outcomeId = `outcome-${outcomes.length + 1}`;
|
|
304
|
+
outcomes.push({
|
|
305
|
+
...nestedScope,
|
|
306
|
+
outcomeId,
|
|
307
|
+
kind: 'pipeline_node',
|
|
308
|
+
status: child.status,
|
|
309
|
+
nodeId: child.node_id ?? null,
|
|
310
|
+
nodeName: child.name,
|
|
311
|
+
directFailureIds: nodeFailureId ? [nodeFailureId] : [],
|
|
312
|
+
evidence: evidenceAt(evidenceIndex, child.end_ts ?? child.ts),
|
|
313
|
+
});
|
|
314
|
+
outcomeIds.push(outcomeId);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
inspectOwnedItems(taskItem.children);
|
|
319
|
+
if (taskItem.status !== 'success') {
|
|
320
|
+
const outcomeId = `outcome-${outcomes.length + 1}`;
|
|
321
|
+
outcomes.push({
|
|
322
|
+
...nestedScope,
|
|
323
|
+
outcomeId,
|
|
324
|
+
kind: 'task',
|
|
325
|
+
status: taskItem.status,
|
|
326
|
+
nodeId: null,
|
|
327
|
+
nodeName: null,
|
|
328
|
+
directFailureIds: nestedDirectFailureIds,
|
|
329
|
+
evidence: evidenceAt(evidenceIndex, taskItem.end_ts ?? taskItem.ts),
|
|
330
|
+
});
|
|
331
|
+
outcomeIds.push(outcomeId);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const inspectNestedTasks = (items) => {
|
|
335
|
+
for (const item of items ?? []) {
|
|
336
|
+
if (item.type === 'task')
|
|
337
|
+
inspectNestedTask(item);
|
|
338
|
+
else
|
|
339
|
+
inspectNestedTasks(item.children);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
218
342
|
for (const item of timeline) {
|
|
343
|
+
inspectNestedTasks(item.nodeInfo.node_flow);
|
|
219
344
|
const failureKind = item.navStatus === 'action-failed'
|
|
220
345
|
? 'action_failed'
|
|
221
346
|
: item.navStatus === 'timeout' && item.nodeInfo.next_list.length > 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windsland52/maa-log-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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-
|
|
46
|
-
"@windsland52/maa-log-
|
|
47
|
-
"@windsland52/maa-log-
|
|
48
|
-
"@windsland52/maa-log-
|
|
45
|
+
"@windsland52/maa-log-parser": "1.1.0",
|
|
46
|
+
"@windsland52/maa-log-kernel": "1.0.2",
|
|
47
|
+
"@windsland52/maa-log-adapter": "1.1.0",
|
|
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
|
}
|