@windsland52/maa-log-tools 1.0.0 → 1.1.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/cli.d.ts +19 -1
- package/dist/cli.js +70 -4
- package/dist/frameworkInput.d.ts +2 -0
- package/dist/frameworkInput.js +124 -0
- package/dist/frameworkVersion.d.ts +36 -0
- package/dist/frameworkVersion.js +126 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Node tools package for Maa log analysis.
|
|
|
9
9
|
- `analyzeZipBuffer`
|
|
10
10
|
- `analyzeZipFile`
|
|
11
11
|
- `analyzeDirectory`
|
|
12
|
+
- Extract MaaFramework runtime sessions and version evidence from core Logger headers
|
|
12
13
|
- Provide CLI entry (`mla-log-tools`)
|
|
13
14
|
|
|
14
15
|
`analyzeLogContent` is delegated to `@windsland52/maa-log-runtime` with a local parser/statistics adapter.
|
|
@@ -21,6 +22,9 @@ The concrete adapter is provided by `@windsland52/maa-log-adapter`.
|
|
|
21
22
|
- `analyzeZipBuffer`
|
|
22
23
|
- `analyzeZipFile`
|
|
23
24
|
- `analyzeDirectory`
|
|
25
|
+
- `loadFrameworkLogSources`
|
|
26
|
+
- `extractFrameworkSessions`
|
|
27
|
+
- `resolveFrameworkSessionForTimestamp`
|
|
24
28
|
- `DEFAULT_CORE_PARSE_OPTIONS`
|
|
25
29
|
- `@windsland52/maa-log-tools/node-input`
|
|
26
30
|
- Node file/zip/folder extraction helpers
|
|
@@ -45,7 +49,21 @@ When `focus` is provided, the helpers scan candidate primary and history log fil
|
|
|
45
49
|
## CLI
|
|
46
50
|
|
|
47
51
|
```bash
|
|
48
|
-
pnpm kernel:cli <path> [--pretty] [--no-events]
|
|
52
|
+
pnpm kernel:cli <path> [--pretty] [--no-events] [--preflight]
|
|
49
53
|
```
|
|
50
54
|
|
|
51
55
|
`<path>` can be a log file, a zip file, or a log directory.
|
|
56
|
+
|
|
57
|
+
The `--preflight` option emits a compact `mla-preflight/v1` compatibility result. It exits
|
|
58
|
+
with 0 when Notify events produce at least one task lifecycle, 3 for an unsupported log format,
|
|
59
|
+
and 2 when the input contains no analyzable log content.
|
|
60
|
+
|
|
61
|
+
Preflight also emits `frameworkVersionSummary` and `frameworkSessions`. A session starts at an
|
|
62
|
+
exact `[Logger] MAA Process Start` header and gets its version only from `[Logger] Version ...`
|
|
63
|
+
evidence in that session. Every boundary and version occurrence retains its source reference and
|
|
64
|
+
line number. Multiple versions are preserved instead of collapsed, conflicts produce warnings,
|
|
65
|
+
and content before a process-start marker is explicitly marked as a partial file segment.
|
|
66
|
+
|
|
67
|
+
Use the session containing the relevant failure timestamp when selecting version-matched source.
|
|
68
|
+
`resolveFrameworkSessionForTimestamp` returns a session only when exactly one resolved,
|
|
69
|
+
process-start-bounded interval contains the timestamp; otherwise it returns `null`.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import type { KernelOutput } from '@windsland52/maa-log-kernel/protocol';
|
|
3
|
+
import { type FrameworkSession, type FrameworkSessionExtraction, type FrameworkVersionSummary } from './frameworkVersion.js';
|
|
4
|
+
export declare const MLA_PREFLIGHT_SCHEMA_VERSION = "mla-preflight/v1";
|
|
5
|
+
export type PreflightReason = 'notify_events_parsed' | 'empty_log' | 'no_notify_events' | 'no_task_lifecycle' | 'no_analyzable_content';
|
|
6
|
+
export interface PreflightOutput {
|
|
7
|
+
schemaVersion: typeof MLA_PREFLIGHT_SCHEMA_VERSION;
|
|
8
|
+
status: 'supported' | 'unsupported';
|
|
9
|
+
reason: PreflightReason;
|
|
10
|
+
parserVersion: string | null;
|
|
11
|
+
taskCount: number;
|
|
12
|
+
eventCount: number;
|
|
13
|
+
nodeStatisticCount: number;
|
|
14
|
+
recognitionStatisticCount: number;
|
|
15
|
+
frameworkVersionSummary: FrameworkVersionSummary;
|
|
16
|
+
frameworkSessions: FrameworkSession[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare const buildPreflightOutput: (output: KernelOutput | null, framework?: FrameworkSessionExtraction) => PreflightOutput;
|
|
20
|
+
export declare const main: () => Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
import { stat } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { loadFrameworkLogSources } from './frameworkInput.js';
|
|
6
|
+
import { extractFrameworkSessions, } from './frameworkVersion.js';
|
|
5
7
|
import { readNodeTextFileContent } from './nodeInput.js';
|
|
6
8
|
import { analyzeLogContent, analyzeDirectory, analyzeZipFile, } from './index.js';
|
|
7
9
|
const printUsage = () => {
|
|
8
|
-
console.error('Usage: mla-log-tools <path> [--pretty] [--no-events]');
|
|
10
|
+
console.error('Usage: mla-log-tools <path> [--pretty] [--no-events] [--preflight]');
|
|
9
11
|
console.error(' <path>: log file path, zip path, or log directory path');
|
|
10
12
|
};
|
|
11
13
|
const parseArgs = (argv) => {
|
|
12
14
|
let targetPath = null;
|
|
13
15
|
let pretty = false;
|
|
14
16
|
let noEvents = false;
|
|
17
|
+
let preflight = false;
|
|
15
18
|
for (const arg of argv) {
|
|
16
19
|
if (arg === '--pretty') {
|
|
17
20
|
pretty = true;
|
|
@@ -21,6 +24,10 @@ const parseArgs = (argv) => {
|
|
|
21
24
|
noEvents = true;
|
|
22
25
|
continue;
|
|
23
26
|
}
|
|
27
|
+
if (arg === '--preflight') {
|
|
28
|
+
preflight = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
24
31
|
if (arg === '--help' || arg === '-h') {
|
|
25
32
|
printUsage();
|
|
26
33
|
process.exit(0);
|
|
@@ -29,7 +36,7 @@ const parseArgs = (argv) => {
|
|
|
29
36
|
targetPath = arg;
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
|
-
return { targetPath, pretty, noEvents };
|
|
39
|
+
return { targetPath, pretty, noEvents, preflight };
|
|
33
40
|
};
|
|
34
41
|
const renderOutput = (output, pretty, noEvents) => {
|
|
35
42
|
const payload = noEvents
|
|
@@ -37,14 +44,60 @@ const renderOutput = (output, pretty, noEvents) => {
|
|
|
37
44
|
: output;
|
|
38
45
|
return JSON.stringify(payload, null, pretty ? 2 : 0);
|
|
39
46
|
};
|
|
40
|
-
const
|
|
41
|
-
|
|
47
|
+
export const MLA_PREFLIGHT_SCHEMA_VERSION = 'mla-preflight/v1';
|
|
48
|
+
const EMPTY_FRAMEWORK_EXTRACTION = {
|
|
49
|
+
sessions: [],
|
|
50
|
+
summary: { status: 'none', versions: [] },
|
|
51
|
+
warnings: [],
|
|
52
|
+
};
|
|
53
|
+
export const buildPreflightOutput = (output, framework = EMPTY_FRAMEWORK_EXTRACTION) => {
|
|
54
|
+
if (!output) {
|
|
55
|
+
return {
|
|
56
|
+
schemaVersion: MLA_PREFLIGHT_SCHEMA_VERSION,
|
|
57
|
+
status: 'unsupported',
|
|
58
|
+
reason: 'no_analyzable_content',
|
|
59
|
+
parserVersion: null,
|
|
60
|
+
taskCount: 0,
|
|
61
|
+
eventCount: 0,
|
|
62
|
+
nodeStatisticCount: 0,
|
|
63
|
+
recognitionStatisticCount: 0,
|
|
64
|
+
frameworkVersionSummary: framework.summary,
|
|
65
|
+
frameworkSessions: framework.sessions,
|
|
66
|
+
warnings: framework.warnings,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const reason = output.events.length > 0
|
|
70
|
+
? output.tasks.length > 0
|
|
71
|
+
? 'notify_events_parsed'
|
|
72
|
+
: 'no_task_lifecycle'
|
|
73
|
+
: output.warnings.includes('Empty log content.')
|
|
74
|
+
? 'empty_log'
|
|
75
|
+
: 'no_notify_events';
|
|
76
|
+
return {
|
|
77
|
+
schemaVersion: MLA_PREFLIGHT_SCHEMA_VERSION,
|
|
78
|
+
status: reason === 'notify_events_parsed' ? 'supported' : 'unsupported',
|
|
79
|
+
reason,
|
|
80
|
+
parserVersion: output.meta.parserVersion,
|
|
81
|
+
taskCount: output.tasks.length,
|
|
82
|
+
eventCount: output.events.length,
|
|
83
|
+
nodeStatisticCount: output.stats.nodes.length,
|
|
84
|
+
recognitionStatisticCount: output.stats.recognitionActions.length,
|
|
85
|
+
frameworkVersionSummary: framework.summary,
|
|
86
|
+
frameworkSessions: framework.sessions,
|
|
87
|
+
warnings: [...output.warnings, ...framework.warnings],
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
export const main = async () => {
|
|
91
|
+
const { targetPath, pretty, noEvents, preflight, } = parseArgs(process.argv.slice(2));
|
|
42
92
|
if (!targetPath) {
|
|
43
93
|
printUsage();
|
|
44
94
|
process.exit(1);
|
|
45
95
|
}
|
|
46
96
|
const resolvedPath = path.resolve(targetPath);
|
|
47
97
|
const targetStat = await stat(resolvedPath);
|
|
98
|
+
const framework = preflight
|
|
99
|
+
? extractFrameworkSessions(await loadFrameworkLogSources(resolvedPath))
|
|
100
|
+
: EMPTY_FRAMEWORK_EXTRACTION;
|
|
48
101
|
let result = null;
|
|
49
102
|
if (targetStat.isDirectory()) {
|
|
50
103
|
result = await analyzeDirectory({ directoryPath: resolvedPath });
|
|
@@ -57,9 +110,22 @@ const main = async () => {
|
|
|
57
110
|
result = await analyzeLogContent({ content });
|
|
58
111
|
}
|
|
59
112
|
if (!result) {
|
|
113
|
+
if (preflight) {
|
|
114
|
+
process.stdout.write(JSON.stringify(buildPreflightOutput(null, framework), null, pretty ? 2 : 0));
|
|
115
|
+
process.stdout.write('\n');
|
|
116
|
+
}
|
|
60
117
|
console.error('No analyzable log content found in the provided path.');
|
|
61
118
|
process.exit(2);
|
|
62
119
|
}
|
|
120
|
+
if (preflight) {
|
|
121
|
+
const output = buildPreflightOutput(result, framework);
|
|
122
|
+
process.stdout.write(JSON.stringify(output, null, pretty ? 2 : 0));
|
|
123
|
+
process.stdout.write('\n');
|
|
124
|
+
if (output.status === 'unsupported') {
|
|
125
|
+
process.exit(3);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
63
129
|
process.stdout.write(renderOutput(result, pretty, noEvents));
|
|
64
130
|
process.stdout.write('\n');
|
|
65
131
|
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { unzipSync } from 'fflate';
|
|
4
|
+
import { readNodeTextFileContent } from './nodeInput.js';
|
|
5
|
+
const MAIN_LOG_NAMES = ['maafw.log', 'maa.log'];
|
|
6
|
+
const BAK_LOG_NAMES = ['maafw.bak.log', 'maa.bak.log'];
|
|
7
|
+
const toPosixPath = (value) => value.replace(/\\/g, '/');
|
|
8
|
+
const decodeBytes = (bytes) => {
|
|
9
|
+
for (const encoding of ['utf-8', 'gbk', 'gb18030', 'gb2312']) {
|
|
10
|
+
try {
|
|
11
|
+
return new TextDecoder(encoding, { fatal: true }).decode(bytes);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
18
|
+
};
|
|
19
|
+
const findEntryPath = (paths, target) => {
|
|
20
|
+
const normalizedTarget = toPosixPath(target).toLowerCase();
|
|
21
|
+
return paths.find((candidate) => toPosixPath(candidate).toLowerCase() === normalizedTarget) ?? null;
|
|
22
|
+
};
|
|
23
|
+
const findZipBasePath = (paths) => {
|
|
24
|
+
for (const candidate of paths) {
|
|
25
|
+
const normalized = toPosixPath(candidate);
|
|
26
|
+
const lowerName = path.posix.basename(normalized).toLowerCase();
|
|
27
|
+
if (!MAIN_LOG_NAMES.includes(lowerName))
|
|
28
|
+
continue;
|
|
29
|
+
const parent = path.posix.dirname(normalized);
|
|
30
|
+
return parent === '.' ? '' : parent;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
34
|
+
const loadZipSources = async (zipPath) => {
|
|
35
|
+
const files = unzipSync(new Uint8Array(await readFile(zipPath)));
|
|
36
|
+
const paths = Object.keys(files);
|
|
37
|
+
const basePath = findZipBasePath(paths);
|
|
38
|
+
if (basePath == null)
|
|
39
|
+
return [];
|
|
40
|
+
const selected = [];
|
|
41
|
+
for (const name of [...BAK_LOG_NAMES, ...MAIN_LOG_NAMES]) {
|
|
42
|
+
const candidate = findEntryPath(paths, basePath ? `${basePath}/${name}` : name);
|
|
43
|
+
if (candidate && !selected.includes(candidate))
|
|
44
|
+
selected.push(candidate);
|
|
45
|
+
if (candidate && MAIN_LOG_NAMES.includes(name))
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return selected.flatMap((entryPath) => {
|
|
49
|
+
const bytes = files[entryPath];
|
|
50
|
+
if (!bytes)
|
|
51
|
+
return [];
|
|
52
|
+
const normalized = toPosixPath(entryPath);
|
|
53
|
+
return [{
|
|
54
|
+
path: normalized,
|
|
55
|
+
name: path.posix.basename(normalized),
|
|
56
|
+
content: decodeBytes(bytes),
|
|
57
|
+
reference: `zip:${toPosixPath(zipPath)}#${normalized}`,
|
|
58
|
+
}];
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
const pathExists = async (candidate) => {
|
|
62
|
+
try {
|
|
63
|
+
await stat(candidate);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const findDebugDirectory = async (root) => {
|
|
71
|
+
for (const name of MAIN_LOG_NAMES) {
|
|
72
|
+
if (await pathExists(path.join(root, name)))
|
|
73
|
+
return root;
|
|
74
|
+
}
|
|
75
|
+
const directDebug = path.join(root, 'debug');
|
|
76
|
+
for (const name of MAIN_LOG_NAMES) {
|
|
77
|
+
if (await pathExists(path.join(directDebug, name)))
|
|
78
|
+
return directDebug;
|
|
79
|
+
}
|
|
80
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
81
|
+
if (!entry.isDirectory())
|
|
82
|
+
continue;
|
|
83
|
+
const found = await findDebugDirectory(path.join(root, entry.name));
|
|
84
|
+
if (found)
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
const firstExisting = async (root, names) => {
|
|
90
|
+
for (const name of names) {
|
|
91
|
+
const candidate = path.join(root, name);
|
|
92
|
+
if (await pathExists(candidate))
|
|
93
|
+
return candidate;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
const loadDirectorySources = async (directoryPath) => {
|
|
98
|
+
const debugPath = await findDebugDirectory(directoryPath);
|
|
99
|
+
if (!debugPath)
|
|
100
|
+
return [];
|
|
101
|
+
const selected = [
|
|
102
|
+
await firstExisting(debugPath, BAK_LOG_NAMES),
|
|
103
|
+
await firstExisting(debugPath, MAIN_LOG_NAMES),
|
|
104
|
+
].filter((candidate) => candidate != null);
|
|
105
|
+
return Promise.all(selected.map(async (absolutePath) => ({
|
|
106
|
+
path: toPosixPath(path.relative(debugPath, absolutePath)),
|
|
107
|
+
name: path.basename(absolutePath),
|
|
108
|
+
content: await readNodeTextFileContent(absolutePath),
|
|
109
|
+
reference: `file:${toPosixPath(absolutePath)}`,
|
|
110
|
+
})));
|
|
111
|
+
};
|
|
112
|
+
export const loadFrameworkLogSources = async (targetPath) => {
|
|
113
|
+
const targetStat = await stat(targetPath);
|
|
114
|
+
if (targetStat.isDirectory())
|
|
115
|
+
return loadDirectorySources(targetPath);
|
|
116
|
+
if (targetPath.toLowerCase().endsWith('.zip'))
|
|
117
|
+
return loadZipSources(targetPath);
|
|
118
|
+
return [{
|
|
119
|
+
path: toPosixPath(targetPath),
|
|
120
|
+
name: path.basename(targetPath),
|
|
121
|
+
content: await readNodeTextFileContent(targetPath),
|
|
122
|
+
reference: `file:${toPosixPath(targetPath)}`,
|
|
123
|
+
}];
|
|
124
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface FrameworkLogSource {
|
|
2
|
+
path: string;
|
|
3
|
+
name: string;
|
|
4
|
+
content: string;
|
|
5
|
+
reference: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FrameworkLogPosition {
|
|
8
|
+
source: string;
|
|
9
|
+
path: string;
|
|
10
|
+
line: number;
|
|
11
|
+
timestamp: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface FrameworkVersionEvidence extends FrameworkLogPosition {
|
|
14
|
+
version: string;
|
|
15
|
+
}
|
|
16
|
+
export interface FrameworkSession {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
startKind: 'process_start' | 'partial_file';
|
|
19
|
+
status: 'resolved' | 'missing_version' | 'conflict';
|
|
20
|
+
version: string | null;
|
|
21
|
+
versions: string[];
|
|
22
|
+
start: FrameworkLogPosition;
|
|
23
|
+
end: FrameworkLogPosition;
|
|
24
|
+
versionEvidence: FrameworkVersionEvidence[];
|
|
25
|
+
}
|
|
26
|
+
export interface FrameworkVersionSummary {
|
|
27
|
+
status: 'none' | 'single' | 'multiple' | 'conflict';
|
|
28
|
+
versions: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface FrameworkSessionExtraction {
|
|
31
|
+
sessions: FrameworkSession[];
|
|
32
|
+
summary: FrameworkVersionSummary;
|
|
33
|
+
warnings: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare const extractFrameworkSessions: (sources: readonly FrameworkLogSource[]) => FrameworkSessionExtraction;
|
|
36
|
+
export declare const resolveFrameworkSessionForTimestamp: (extraction: FrameworkSessionExtraction, timestamp: string) => FrameworkSession | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const PROCESS_START_PATTERN = /\]\[Logger\]\s+MAA Process Start(?:\s|$)/;
|
|
2
|
+
const VERSION_PATTERN = /\]\[Logger\]\s+Version\s+(v\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)(?:\s|$)/;
|
|
3
|
+
const TIMESTAMP_PATTERN = /^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\]/;
|
|
4
|
+
const splitLines = (content) => {
|
|
5
|
+
const lines = content.split(/\r?\n/);
|
|
6
|
+
if (lines[lines.length - 1] === '')
|
|
7
|
+
lines.pop();
|
|
8
|
+
return lines;
|
|
9
|
+
};
|
|
10
|
+
const timestampOf = (line) => {
|
|
11
|
+
if (!line)
|
|
12
|
+
return null;
|
|
13
|
+
return line.match(TIMESTAMP_PATTERN)?.[1] ?? null;
|
|
14
|
+
};
|
|
15
|
+
const position = (source, lines, lineIndex) => ({
|
|
16
|
+
source: source.reference,
|
|
17
|
+
path: source.path,
|
|
18
|
+
line: lineIndex + 1,
|
|
19
|
+
timestamp: timestampOf(lines[lineIndex]),
|
|
20
|
+
});
|
|
21
|
+
const findTimestamp = (lines, startIndex, endIndex, direction) => {
|
|
22
|
+
for (let index = direction === 1 ? startIndex : endIndex; index >= startIndex && index <= endIndex; index += direction) {
|
|
23
|
+
const timestamp = timestampOf(lines[index]);
|
|
24
|
+
if (timestamp)
|
|
25
|
+
return timestamp;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
const buildSession = (source, lines, startIndex, endIndex, startKind, sessionIndex) => {
|
|
30
|
+
const versionEvidence = [];
|
|
31
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
32
|
+
const match = lines[index]?.match(VERSION_PATTERN);
|
|
33
|
+
const version = match?.[1];
|
|
34
|
+
if (!version)
|
|
35
|
+
continue;
|
|
36
|
+
versionEvidence.push({
|
|
37
|
+
...position(source, lines, index),
|
|
38
|
+
version,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const versions = [...new Set(versionEvidence.map((item) => item.version))];
|
|
42
|
+
const status = versions.length === 0
|
|
43
|
+
? 'missing_version'
|
|
44
|
+
: versions.length === 1
|
|
45
|
+
? 'resolved'
|
|
46
|
+
: 'conflict';
|
|
47
|
+
const start = position(source, lines, startIndex);
|
|
48
|
+
start.timestamp = findTimestamp(lines, startIndex, endIndex, 1);
|
|
49
|
+
const end = position(source, lines, endIndex);
|
|
50
|
+
end.timestamp = findTimestamp(lines, startIndex, endIndex, -1);
|
|
51
|
+
return {
|
|
52
|
+
sessionId: `framework-session-${sessionIndex}`,
|
|
53
|
+
startKind,
|
|
54
|
+
status,
|
|
55
|
+
version: status === 'resolved' ? (versions[0] ?? null) : null,
|
|
56
|
+
versions,
|
|
57
|
+
start,
|
|
58
|
+
end,
|
|
59
|
+
versionEvidence,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export const extractFrameworkSessions = (sources) => {
|
|
63
|
+
const sessions = [];
|
|
64
|
+
for (const source of sources) {
|
|
65
|
+
const lines = splitLines(source.content);
|
|
66
|
+
if (lines.length === 0)
|
|
67
|
+
continue;
|
|
68
|
+
const processStarts = [];
|
|
69
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
70
|
+
if (PROCESS_START_PATTERN.test(lines[index] ?? ''))
|
|
71
|
+
processStarts.push(index);
|
|
72
|
+
}
|
|
73
|
+
const firstProcessStart = processStarts[0];
|
|
74
|
+
const hasPartialPrefix = firstProcessStart != null
|
|
75
|
+
&& firstProcessStart > 0
|
|
76
|
+
&& lines.slice(0, firstProcessStart).some((line) => (VERSION_PATTERN.test(line) || line.includes('!!!OnEventNotify!!!')));
|
|
77
|
+
const boundaries = processStarts.length === 0 || hasPartialPrefix
|
|
78
|
+
? [0, ...processStarts]
|
|
79
|
+
: processStarts;
|
|
80
|
+
for (let index = 0; index < boundaries.length; index += 1) {
|
|
81
|
+
const startIndex = boundaries[index];
|
|
82
|
+
if (startIndex == null)
|
|
83
|
+
continue;
|
|
84
|
+
const nextStart = boundaries[index + 1];
|
|
85
|
+
const endIndex = nextStart == null ? lines.length - 1 : nextStart - 1;
|
|
86
|
+
const isProcessStart = processStarts.includes(startIndex);
|
|
87
|
+
sessions.push(buildSession(source, lines, startIndex, endIndex, isProcessStart ? 'process_start' : 'partial_file', sessions.length + 1));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const versions = [...new Set(sessions.flatMap((session) => session.versions))];
|
|
91
|
+
const hasConflict = sessions.some((session) => session.status === 'conflict');
|
|
92
|
+
const summary = {
|
|
93
|
+
status: hasConflict
|
|
94
|
+
? 'conflict'
|
|
95
|
+
: versions.length === 0
|
|
96
|
+
? 'none'
|
|
97
|
+
: versions.length === 1
|
|
98
|
+
? 'single'
|
|
99
|
+
: 'multiple',
|
|
100
|
+
versions,
|
|
101
|
+
};
|
|
102
|
+
const warnings = [];
|
|
103
|
+
if (summary.status === 'multiple') {
|
|
104
|
+
warnings.push(`Multiple MaaFramework versions found in selected logs: ${versions.join(', ')}.`);
|
|
105
|
+
}
|
|
106
|
+
if (summary.status === 'conflict') {
|
|
107
|
+
warnings.push('Conflicting MaaFramework version headers found within a runtime session.');
|
|
108
|
+
}
|
|
109
|
+
if (sessions.some((session) => session.startKind === 'partial_file')) {
|
|
110
|
+
warnings.push('Some core log content starts without a MAA Process Start marker; its session boundary is partial.');
|
|
111
|
+
}
|
|
112
|
+
if (sessions.some((session) => session.status === 'missing_version')) {
|
|
113
|
+
warnings.push('Some MaaFramework runtime sessions do not contain a Logger version header.');
|
|
114
|
+
}
|
|
115
|
+
return { sessions, summary, warnings };
|
|
116
|
+
};
|
|
117
|
+
export const resolveFrameworkSessionForTimestamp = (extraction, timestamp) => {
|
|
118
|
+
const candidates = extraction.sessions.filter((session) => {
|
|
119
|
+
if (session.status !== 'resolved' || session.startKind !== 'process_start')
|
|
120
|
+
return false;
|
|
121
|
+
const start = session.start.timestamp;
|
|
122
|
+
const end = session.end.timestamp;
|
|
123
|
+
return start != null && end != null && timestamp >= start && timestamp <= end;
|
|
124
|
+
});
|
|
125
|
+
return candidates.length === 1 ? (candidates[0] ?? null) : null;
|
|
126
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { KernelOutput } from '@windsland52/maa-log-kernel';
|
|
2
2
|
import type { AnalyzeLogContentInput, ParseFileOptions } from '@windsland52/maa-log-runtime';
|
|
3
|
-
import { type LogBundleFocus } from './nodeInput';
|
|
3
|
+
import { type LogBundleFocus } from './nodeInput.js';
|
|
4
4
|
type ParseOptions = ParseFileOptions;
|
|
5
5
|
export interface AnalyzeZipBufferInput {
|
|
6
6
|
zipData: Uint8Array;
|
|
@@ -27,4 +27,6 @@ export declare const analyzeZipFile: (input: AnalyzeZipFileInput) => Promise<Ker
|
|
|
27
27
|
export declare const analyzeDirectory: (input: AnalyzeDirectoryInput) => Promise<KernelOutput | null>;
|
|
28
28
|
export { DEFAULT_CORE_PARSE_OPTIONS, } from '@windsland52/maa-log-runtime';
|
|
29
29
|
export type { AnalyzeLogContentInput, ParseFileOptions } from '@windsland52/maa-log-runtime';
|
|
30
|
-
export * from './nodeInput';
|
|
30
|
+
export * from './nodeInput.js';
|
|
31
|
+
export * from './frameworkInput.js';
|
|
32
|
+
export * from './frameworkVersion.js';
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windsland52/maa-log-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -42,10 +42,10 @@
|
|
|
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-runtime": "1.0.
|
|
48
|
-
"@windsland52/maa-log-
|
|
45
|
+
"@windsland52/maa-log-adapter": "1.0.1",
|
|
46
|
+
"@windsland52/maa-log-parser": "1.0.1",
|
|
47
|
+
"@windsland52/maa-log-runtime": "1.0.1",
|
|
48
|
+
"@windsland52/maa-log-kernel": "1.0.1"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=24.0.0"
|