@windsland52/maa-log-tools 0.0.1 → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Windsland52
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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,16 +22,48 @@ 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
31
+ - `LogBundleFocus`
27
32
  - `@windsland52/maa-log-tools/cli`
28
33
  - CLI entry module
29
34
 
35
+ ## Focused Loading
36
+
37
+ `analyzeZipBuffer`, `analyzeZipFile`, `analyzeDirectory`, `extractZipContentFromNodeBuffer`, `extractZipContentFromNodeFile`, and `loadNodeLogDirectory` all accept an optional `focus` selector:
38
+
39
+ ```ts
40
+ {
41
+ keywords?: string[]
42
+ started_after?: string
43
+ started_before?: string
44
+ }
45
+ ```
46
+
47
+ When `focus` is provided, the helpers scan candidate primary and history log files and only merge files whose content matches the keywords and/or timestamp boundaries. If `focus` is omitted, the previous default loading behavior is preserved.
48
+
30
49
  ## CLI
31
50
 
32
51
  ```bash
33
- pnpm kernel:cli <path> [--pretty] [--no-events]
52
+ pnpm kernel:cli <path> [--pretty] [--no-events] [--preflight]
34
53
  ```
35
54
 
36
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
- export {};
2
+ import type { KernelOutput } from '@windsland52/maa-log-kernel/protocol';
3
+ import { type FrameworkSession, type FrameworkSessionExtraction, type FrameworkVersionSummary } from './frameworkVersion';
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 main = async () => {
41
- const { targetPath, pretty, noEvents } = parseArgs(process.argv.slice(2));
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,2 @@
1
+ import type { FrameworkLogSource } from './frameworkVersion';
2
+ export declare const loadFrameworkLogSources: (targetPath: string) => Promise<FrameworkLogSource[]>;
@@ -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,19 +1,23 @@
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
4
  type ParseOptions = ParseFileOptions;
4
5
  export interface AnalyzeZipBufferInput {
5
6
  zipData: Uint8Array;
6
7
  sourceRef?: string;
8
+ focus?: LogBundleFocus;
7
9
  parseOptions?: ParseOptions;
8
10
  parserVersion?: string;
9
11
  }
10
12
  export interface AnalyzeZipFileInput {
11
13
  zipFilePath: string;
14
+ focus?: LogBundleFocus;
12
15
  parseOptions?: ParseOptions;
13
16
  parserVersion?: string;
14
17
  }
15
18
  export interface AnalyzeDirectoryInput {
16
19
  directoryPath: string;
20
+ focus?: LogBundleFocus;
17
21
  parseOptions?: ParseOptions;
18
22
  parserVersion?: string;
19
23
  }
@@ -24,3 +28,5 @@ export declare const analyzeDirectory: (input: AnalyzeDirectoryInput) => Promise
24
28
  export { DEFAULT_CORE_PARSE_OPTIONS, } from '@windsland52/maa-log-runtime';
25
29
  export type { AnalyzeLogContentInput, ParseFileOptions } from '@windsland52/maa-log-runtime';
26
30
  export * from './nodeInput';
31
+ export * from './frameworkInput';
32
+ export * from './frameworkVersion';
package/dist/index.js CHANGED
@@ -8,7 +8,9 @@ export const analyzeLogContent = async (input) => {
8
8
  });
9
9
  };
10
10
  export const analyzeZipBuffer = async (input) => {
11
- const extracted = extractZipContentFromNodeBuffer(input.zipData, input.sourceRef);
11
+ const extracted = extractZipContentFromNodeBuffer(input.zipData, input.sourceRef, {
12
+ focus: input.focus,
13
+ });
12
14
  if (!extracted)
13
15
  return null;
14
16
  return analyzeLogContent({
@@ -21,7 +23,9 @@ export const analyzeZipBuffer = async (input) => {
21
23
  });
22
24
  };
23
25
  export const analyzeZipFile = async (input) => {
24
- const extracted = await extractZipContentFromNodeFile(input.zipFilePath);
26
+ const extracted = await extractZipContentFromNodeFile(input.zipFilePath, {
27
+ focus: input.focus,
28
+ });
25
29
  if (!extracted)
26
30
  return null;
27
31
  return analyzeLogContent({
@@ -34,7 +38,9 @@ export const analyzeZipFile = async (input) => {
34
38
  });
35
39
  };
36
40
  export const analyzeDirectory = async (input) => {
37
- const extracted = await loadNodeLogDirectory(input.directoryPath);
41
+ const extracted = await loadNodeLogDirectory(input.directoryPath, {
42
+ focus: input.focus,
43
+ });
38
44
  if (!extracted)
39
45
  return null;
40
46
  return analyzeLogContent({
@@ -48,3 +54,5 @@ export const analyzeDirectory = async (input) => {
48
54
  };
49
55
  export { DEFAULT_CORE_PARSE_OPTIONS, } from '@windsland52/maa-log-runtime';
50
56
  export * from './nodeInput.js';
57
+ export * from './frameworkInput.js';
58
+ export * from './frameworkVersion.js';
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -3,7 +3,7 @@ import { sortNodesByGlobalExecutionOrder } from './taskExecutionOrder.js';
3
3
  const isNodeActionFailed = (node) => {
4
4
  if (node.action_details && node.action_details.success === false)
5
5
  return true;
6
- return (node.node_flow || []).some((item) => item.type === 'action' && item.status === 'failed');
6
+ return (node.node_flow || []).some((item) => (item.type === 'action' || item.type === 'action_node') && item.status === 'failed');
7
7
  };
8
8
  export const buildNodeExecutionTimeline = (nodes, options = {}) => {
9
9
  const originalIndexByNode = new Map();
@@ -11,7 +11,18 @@ export interface NodeExtractedLogContent {
11
11
  waitFreezesImages: Map<string, string>;
12
12
  textFiles: KernelTextFile[];
13
13
  }
14
+ export interface LogBundleFocus {
15
+ keywords?: string[];
16
+ started_after?: string;
17
+ started_before?: string;
18
+ }
19
+ export interface ExtractZipContentOptions {
20
+ focus?: LogBundleFocus;
21
+ }
22
+ export interface LoadNodeLogDirectoryOptions {
23
+ focus?: LogBundleFocus;
24
+ }
14
25
  export declare const readNodeTextFileContent: (filePath: string) => Promise<string>;
15
- export declare const extractZipContentFromNodeBuffer: (zipData: Uint8Array, sourceRef?: string) => NodeExtractedLogContent | null;
16
- export declare const extractZipContentFromNodeFile: (zipFilePath: string) => Promise<NodeExtractedLogContent | null>;
17
- export declare const loadNodeLogDirectory: (inputDirectoryPath: string) => Promise<NodeExtractedLogContent | null>;
26
+ export declare const extractZipContentFromNodeBuffer: (zipData: Uint8Array, sourceRef?: string, options?: ExtractZipContentOptions) => NodeExtractedLogContent | null;
27
+ export declare const extractZipContentFromNodeFile: (zipFilePath: string, options?: ExtractZipContentOptions) => Promise<NodeExtractedLogContent | null>;
28
+ export declare const loadNodeLogDirectory: (inputDirectoryPath: string, options?: LoadNodeLogDirectoryOptions) => Promise<NodeExtractedLogContent | null>;
package/dist/nodeInput.js CHANGED
@@ -4,16 +4,24 @@ import { unzipSync } from 'fflate';
4
4
  const MAIN_LOG_NAMES = ['maa.log', 'maafw.log'];
5
5
  const BAK_LOG_NAMES = ['maa.bak.log', 'maafw.bak.log'];
6
6
  const SEARCH_TEXT_EXTENSIONS = ['.log', '.txt', '.jsonl'];
7
- const PRIMARY_LOG_NAME_SET = new Set([
8
- ...MAIN_LOG_NAMES,
9
- ...BAK_LOG_NAMES,
10
- ].map((name) => name.toLowerCase()));
7
+ const MAIN_LOG_NAME_SET = new Set(MAIN_LOG_NAMES.map((name) => name.toLowerCase()));
8
+ const HISTORY_LOG_NAME_PATTERNS = [
9
+ /^maa\.bak(?:\..+)?\.log$/i,
10
+ /^maafw\.bak(?:\..+)?\.log$/i,
11
+ ];
11
12
  const toPosixPath = (value) => value.replace(/\\/g, '/');
12
13
  const normalizeLowerPath = (value) => toPosixPath(value).toLowerCase();
13
14
  const isSearchTextFile = (normalizedPath) => {
14
15
  const lower = normalizedPath.toLowerCase();
15
16
  return SEARCH_TEXT_EXTENSIONS.some((ext) => lower.endsWith(ext));
16
17
  };
18
+ const isHistoryLogName = (fileName) => {
19
+ return HISTORY_LOG_NAME_PATTERNS.some((pattern) => pattern.test(fileName));
20
+ };
21
+ const isCoreLogName = (fileName) => {
22
+ const lower = fileName.toLowerCase();
23
+ return MAIN_LOG_NAME_SET.has(lower) || isHistoryLogName(lower);
24
+ };
17
25
  const decodeNodeBytes = (bytes) => {
18
26
  const encodings = ['utf-8', 'gbk', 'gb18030', 'gb2312'];
19
27
  for (const encoding of encodings) {
@@ -84,13 +92,11 @@ const isNeededZipEntry = (entryPath) => {
84
92
  const name = lower.slice(lower.lastIndexOf('/') + 1);
85
93
  if (isSearchTextFile(lower))
86
94
  return true;
87
- if (MAIN_LOG_NAMES.includes(name))
95
+ if (isCoreLogName(name))
88
96
  return true;
89
- if (BAK_LOG_NAMES.includes(name))
97
+ if ((lower.includes('/on_error/') || lower.startsWith('on_error/')) && lower.endsWith('.png'))
90
98
  return true;
91
- if (lower.includes('/on_error/') && lower.endsWith('.png'))
92
- return true;
93
- if (lower.includes('/vision/') && lower.endsWith('.jpg'))
99
+ if ((lower.includes('/vision/') || lower.startsWith('vision/')) && lower.endsWith('.jpg'))
94
100
  return true;
95
101
  return false;
96
102
  };
@@ -100,11 +106,132 @@ const toZipReference = (sourceRef, entryPath) => {
100
106
  const toFileReference = (absolutePath) => {
101
107
  return `file:${toPosixPath(absolutePath)}`;
102
108
  };
109
+ const isRelativeImagePath = (relativePath, directory, extension) => {
110
+ const normalized = relativePath.toLowerCase();
111
+ return normalized === `${directory}${extension}`
112
+ || normalized.startsWith(`${directory}/`)
113
+ || normalized.includes(`/${directory}/`);
114
+ };
115
+ const normalizeTimestampBoundary = (value) => {
116
+ if (!value)
117
+ return null;
118
+ const trimmed = value.trim();
119
+ if (trimmed.length === 0)
120
+ return null;
121
+ return trimmed.includes('.') ? trimmed : `${trimmed}.000`;
122
+ };
123
+ const extractTimestamps = (content) => {
124
+ const matches = content.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\]/g) ?? [];
125
+ return matches
126
+ .map((item) => item.slice(1, -1))
127
+ .map((item) => normalizeTimestampBoundary(item) ?? item);
128
+ };
129
+ const contentMatchesFocus = (content, focus) => {
130
+ const keywords = (focus.keywords ?? []).filter((keyword) => keyword.trim().length > 0);
131
+ if (keywords.length > 0 && !keywords.some((keyword) => content.includes(keyword))) {
132
+ return false;
133
+ }
134
+ const startedAfter = normalizeTimestampBoundary(focus.started_after);
135
+ const startedBefore = normalizeTimestampBoundary(focus.started_before);
136
+ if (!startedAfter && !startedBefore) {
137
+ return true;
138
+ }
139
+ return extractTimestamps(content).some((timestamp) => {
140
+ if (startedAfter && timestamp < startedAfter) {
141
+ return false;
142
+ }
143
+ if (startedBefore && timestamp > startedBefore) {
144
+ return false;
145
+ }
146
+ return true;
147
+ });
148
+ };
149
+ const joinMergedContent = (chunks) => {
150
+ return chunks.reduce((result, chunk) => {
151
+ if (chunk.length === 0)
152
+ return result;
153
+ if (result.length === 0)
154
+ return chunk;
155
+ return result.endsWith('\n') ? `${result}${chunk}` : `${result}\n${chunk}`;
156
+ }, '');
157
+ };
158
+ const rankLogPath = (filePath) => {
159
+ const baseName = path.basename(filePath).toLowerCase();
160
+ if (baseName === 'maafw.bak.log' || baseName.startsWith('maafw.bak.')) {
161
+ return 0;
162
+ }
163
+ if (baseName === 'maa.bak.log' || baseName.startsWith('maa.bak.')) {
164
+ return 1;
165
+ }
166
+ if (baseName === 'maafw.log') {
167
+ return 2;
168
+ }
169
+ if (baseName === 'maa.log') {
170
+ return 3;
171
+ }
172
+ return 10;
173
+ };
174
+ const sortLogPaths = (paths) => {
175
+ return [...paths].sort((left, right) => {
176
+ const rankDiff = rankLogPath(left) - rankLogPath(right);
177
+ if (rankDiff !== 0)
178
+ return rankDiff;
179
+ return left.localeCompare(right);
180
+ });
181
+ };
182
+ const collectFocusedFileContents = async (logPaths, focus) => {
183
+ const chunks = [];
184
+ for (const logPath of sortLogPaths(logPaths)) {
185
+ const content = await readNodeTextFileContent(logPath);
186
+ if (!contentMatchesFocus(content, focus))
187
+ continue;
188
+ chunks.push(content);
189
+ }
190
+ return joinMergedContent(chunks);
191
+ };
192
+ const collectFocusedZipContents = (entries, paths, basePath, focus) => {
193
+ const normalizedBasePath = normalizeLowerPath(basePath);
194
+ const candidatePaths = sortLogPaths(paths.filter((entryPath) => {
195
+ const normalizedPath = toPosixPath(entryPath);
196
+ const lastSlash = normalizedPath.lastIndexOf('/');
197
+ const parentPath = lastSlash === -1 ? '' : normalizedPath.slice(0, lastSlash);
198
+ if (normalizeLowerPath(parentPath) !== normalizedBasePath) {
199
+ return false;
200
+ }
201
+ const fileName = normalizedPath.slice(lastSlash + 1);
202
+ return isCoreLogName(fileName);
203
+ }));
204
+ const chunks = [];
205
+ for (const entryPath of candidatePaths) {
206
+ const bytes = entries[entryPath];
207
+ if (!bytes)
208
+ continue;
209
+ const content = decodeNodeBytes(bytes);
210
+ if (!contentMatchesFocus(content, focus))
211
+ continue;
212
+ chunks.push(content);
213
+ }
214
+ return joinMergedContent(chunks);
215
+ };
216
+ const buildDefaultZipContent = (entries, paths, basePath) => {
217
+ const bakLogName = BAK_LOG_NAMES.find((name) => findZipEntry(entries, paths, joinPath(basePath, name)));
218
+ const mainLogName = MAIN_LOG_NAMES.find((name) => findZipEntry(entries, paths, joinPath(basePath, name)));
219
+ const bakData = bakLogName ? findZipEntry(entries, paths, joinPath(basePath, bakLogName)) : null;
220
+ const mainData = mainLogName ? findZipEntry(entries, paths, joinPath(basePath, mainLogName)) : null;
221
+ const chunks = [];
222
+ if (bakData) {
223
+ chunks.push(decodeNodeBytes(bakData));
224
+ }
225
+ if (mainData) {
226
+ chunks.push(decodeNodeBytes(mainData));
227
+ }
228
+ return joinMergedContent(chunks);
229
+ };
103
230
  export const readNodeTextFileContent = async (filePath) => {
104
231
  const bytes = await readFile(filePath);
105
232
  return decodeNodeBytes(new Uint8Array(bytes));
106
233
  };
107
- export const extractZipContentFromNodeBuffer = (zipData, sourceRef = 'memory.zip') => {
234
+ export const extractZipContentFromNodeBuffer = (zipData, sourceRef = 'memory.zip', options = {}) => {
108
235
  const files = unzipSync(zipData, {
109
236
  filter: (entry) => isNeededZipEntry(entry.name),
110
237
  });
@@ -112,19 +239,9 @@ export const extractZipContentFromNodeBuffer = (zipData, sourceRef = 'memory.zip
112
239
  const basePath = findBaseDirectory(paths);
113
240
  if (basePath == null)
114
241
  return null;
115
- const bakLogName = BAK_LOG_NAMES.find((name) => findZipEntry(files, paths, joinPath(basePath, name)));
116
- const mainLogName = MAIN_LOG_NAMES.find((name) => findZipEntry(files, paths, joinPath(basePath, name)));
117
- const bakData = bakLogName ? findZipEntry(files, paths, joinPath(basePath, bakLogName)) : null;
118
- const mainData = mainLogName ? findZipEntry(files, paths, joinPath(basePath, mainLogName)) : null;
119
- let content = '';
120
- if (bakData) {
121
- content += decodeNodeBytes(bakData);
122
- }
123
- if (mainData) {
124
- if (content && !content.endsWith('\n'))
125
- content += '\n';
126
- content += decodeNodeBytes(mainData);
127
- }
242
+ const content = options.focus
243
+ ? collectFocusedZipContents(files, paths, basePath, options.focus)
244
+ : buildDefaultZipContent(files, paths, basePath);
128
245
  if (!content)
129
246
  return null;
130
247
  const errorImages = new Map();
@@ -153,24 +270,26 @@ export const extractZipContentFromNodeBuffer = (zipData, sourceRef = 'memory.zip
153
270
  waitFreezesImages.set(waitKey, toZipReference(sourceRef, normalizedPath));
154
271
  }
155
272
  }
156
- if (isSearchTextFile(normalizedPath)) {
157
- const fileData = files[currentPath];
158
- if (!fileData)
159
- continue;
160
- textFiles.push({
161
- path: normalizedPath,
162
- name: fileName,
163
- content: decodeNodeBytes(fileData),
164
- reference: toZipReference(sourceRef, normalizedPath),
165
- });
166
- }
273
+ if (!isSearchTextFile(normalizedPath))
274
+ continue;
275
+ if (isCoreLogName(fileName))
276
+ continue;
277
+ const fileData = files[currentPath];
278
+ if (!fileData)
279
+ continue;
280
+ textFiles.push({
281
+ path: normalizedPath,
282
+ name: fileName,
283
+ content: decodeNodeBytes(fileData),
284
+ reference: toZipReference(sourceRef, normalizedPath),
285
+ });
167
286
  }
168
287
  textFiles.sort((a, b) => a.path.localeCompare(b.path));
169
288
  return { content, errorImages, visionImages, waitFreezesImages, textFiles };
170
289
  };
171
- export const extractZipContentFromNodeFile = async (zipFilePath) => {
290
+ export const extractZipContentFromNodeFile = async (zipFilePath, options = {}) => {
172
291
  const bytes = await readFile(zipFilePath);
173
- return extractZipContentFromNodeBuffer(new Uint8Array(bytes), zipFilePath);
292
+ return extractZipContentFromNodeBuffer(new Uint8Array(bytes), zipFilePath, options);
174
293
  };
175
294
  const pathExists = async (targetPath) => {
176
295
  try {
@@ -244,22 +363,26 @@ const pickPrimaryLogPath = async (debugPath, allFiles, candidates) => {
244
363
  }
245
364
  return null;
246
365
  };
247
- export const loadNodeLogDirectory = async (inputDirectoryPath) => {
248
- const debugPath = await resolveDebugDirectory(inputDirectoryPath);
249
- if (!debugPath)
250
- return null;
251
- const allFiles = await collectFilesRecursively(debugPath);
366
+ const buildDefaultDirectoryContent = async (debugPath, allFiles) => {
252
367
  const bakLogPath = await pickPrimaryLogPath(debugPath, allFiles, BAK_LOG_NAMES);
253
368
  const mainLogPath = await pickPrimaryLogPath(debugPath, allFiles, MAIN_LOG_NAMES);
254
- let content = '';
369
+ const chunks = [];
255
370
  if (bakLogPath) {
256
- content += await readNodeTextFileContent(bakLogPath);
371
+ chunks.push(await readNodeTextFileContent(bakLogPath));
257
372
  }
258
373
  if (mainLogPath) {
259
- if (content && !content.endsWith('\n'))
260
- content += '\n';
261
- content += await readNodeTextFileContent(mainLogPath);
374
+ chunks.push(await readNodeTextFileContent(mainLogPath));
262
375
  }
376
+ return joinMergedContent(chunks);
377
+ };
378
+ export const loadNodeLogDirectory = async (inputDirectoryPath, options = {}) => {
379
+ const debugPath = await resolveDebugDirectory(inputDirectoryPath);
380
+ if (!debugPath)
381
+ return null;
382
+ const allFiles = await collectFilesRecursively(debugPath);
383
+ const content = options.focus
384
+ ? await collectFocusedFileContents(allFiles.filter((filePath) => isCoreLogName(path.basename(filePath))), options.focus)
385
+ : await buildDefaultDirectoryContent(debugPath, allFiles);
263
386
  if (!content)
264
387
  return null;
265
388
  const errorImages = new Map();
@@ -270,14 +393,13 @@ export const loadNodeLogDirectory = async (inputDirectoryPath) => {
270
393
  const relativePath = toPosixPath(path.relative(debugPath, absolutePath));
271
394
  const lowerRelativePath = relativePath.toLowerCase();
272
395
  const fileName = path.basename(absolutePath);
273
- const lowerFileName = fileName.toLowerCase();
274
- if (lowerRelativePath.includes('/on_error/') && lowerRelativePath.endsWith('.png')) {
396
+ if (isRelativeImagePath(lowerRelativePath, 'on_error', '.png')) {
275
397
  const key = parseErrorImageKey(fileName);
276
398
  if (key) {
277
399
  errorImages.set(key, toFileReference(absolutePath));
278
400
  }
279
401
  }
280
- if (lowerRelativePath.includes('/vision/') && lowerRelativePath.endsWith('.jpg')) {
402
+ if (isRelativeImagePath(lowerRelativePath, 'vision', '.jpg')) {
281
403
  const visionKey = parseVisionImageKey(fileName);
282
404
  if (visionKey) {
283
405
  visionImages.set(visionKey, toFileReference(absolutePath));
@@ -289,7 +411,7 @@ export const loadNodeLogDirectory = async (inputDirectoryPath) => {
289
411
  }
290
412
  if (!isSearchTextFile(relativePath))
291
413
  continue;
292
- if (PRIMARY_LOG_NAME_SET.has(lowerFileName))
414
+ if (isCoreLogName(fileName))
293
415
  continue;
294
416
  textFiles.push({
295
417
  path: relativePath,
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,13 +1,8 @@
1
1
  {
2
2
  "name": "@windsland52/maa-log-tools",
3
- "version": "0.0.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "private": false,
6
- "scripts": {
7
- "typecheck": "tsc -p ./tsconfig.json",
8
- "build": "pnpm run clean && tsc -p ./tsconfig.build.json && node ../../scripts/fix-esm-imports.mjs ./dist",
9
- "clean": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\""
10
- },
11
6
  "bin": {
12
7
  "mla-log-tools": "./dist/cli.js"
13
8
  },
@@ -46,11 +41,11 @@
46
41
  }
47
42
  },
48
43
  "dependencies": {
49
- "@windsland52/maa-log-adapter": "workspace:*",
50
- "@windsland52/maa-log-kernel": "workspace:*",
51
- "@windsland52/maa-log-parser": "workspace:*",
52
- "@windsland52/maa-log-runtime": "workspace:*",
53
- "fflate": "^0.8.2"
44
+ "fflate": "^0.8.2",
45
+ "@windsland52/maa-log-kernel": "1.0.0",
46
+ "@windsland52/maa-log-parser": "1.0.0",
47
+ "@windsland52/maa-log-runtime": "1.0.0",
48
+ "@windsland52/maa-log-adapter": "1.0.0"
54
49
  },
55
50
  "engines": {
56
51
  "node": ">=24.0.0"
@@ -61,5 +56,15 @@
61
56
  "files": [
62
57
  "dist",
63
58
  "README.md"
64
- ]
65
- }
59
+ ],
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "https://github.com/MaaXYZ/MaaLogAnalyzer",
63
+ "directory": "packages/maa-log-tools"
64
+ },
65
+ "scripts": {
66
+ "typecheck": "tsc -p ./tsconfig.json",
67
+ "build": "pnpm run clean && tsc -p ./tsconfig.build.json && node ../../scripts/fix-esm-imports.mjs ./dist",
68
+ "clean": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\""
69
+ }
70
+ }