@windsland52/maafw-live 0.0.0-stage → 0.4.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 +21 -0
- package/README.md +293 -2
- package/bin/maafw-live.mjs +25 -0
- package/docs/daemon-protocol.md +121 -0
- package/docs/keyframe-retention-contract-v0.md +225 -0
- package/docs/session-unify-v0.md +115 -0
- package/lib/client/daemon.js +276 -0
- package/lib/commands/annotate.js +179 -0
- package/lib/commands/calibrate.js +61 -0
- package/lib/commands/color.js +73 -0
- package/lib/commands/crop.js +228 -0
- package/lib/commands/dispatch.js +121 -0
- package/lib/commands/env.js +409 -0
- package/lib/commands/index.js +70 -0
- package/lib/commands/input.js +257 -0
- package/lib/commands/kf.js +167 -0
- package/lib/commands/probe.js +70 -0
- package/lib/commands/reco.js +440 -0
- package/lib/commands/repl.js +1501 -0
- package/lib/commands/roi-check.js +273 -0
- package/lib/commands/roi-compare.js +388 -0
- package/lib/commands/runtime.js +718 -0
- package/lib/commands/skill.js +169 -0
- package/lib/commands/timing.js +247 -0
- package/lib/commands/version.js +66 -0
- package/lib/daemon/framed.mjs +3680 -0
- package/lib/daemon/reco_child.mjs +287 -0
- package/lib/exec.js +96 -0
- package/lib/flags.js +44 -0
- package/lib/image.js +90 -0
- package/lib/index.js +143 -0
- package/lib/interface/hotkey.js +142 -0
- package/lib/interface/index.js +12 -0
- package/lib/interface/jsonc.js +95 -0
- package/lib/interface/load.js +397 -0
- package/lib/interface/override.js +287 -0
- package/lib/interface/plan.js +279 -0
- package/lib/option-args.js +29 -0
- package/lib/pkg.js +54 -0
- package/lib/protocol.js +94 -0
- package/lib/runtime/actions.js +225 -0
- package/lib/runtime/keyframes.js +73 -0
- package/lib/runtime/log-watch.js +160 -0
- package/lib/runtime/node-source.js +100 -0
- package/lib/runtime/ocr-model.js +45 -0
- package/lib/runtime/pi-env.js +34 -0
- package/lib/runtime/pretask.js +217 -0
- package/lib/runtime/prune.js +28 -0
- package/lib/runtime/receipts.js +57 -0
- package/lib/runtime/reco-args.js +38 -0
- package/lib/runtime/reco-verdict.js +82 -0
- package/lib/runtime/roi-compare.js +198 -0
- package/lib/runtime/session-forward.js +79 -0
- package/lib/runtime/session-ipc.js +471 -0
- package/lib/runtime/session.js +135 -0
- package/lib/runtime/signal-guard.js +35 -0
- package/lib/types/client/daemon.d.ts +86 -0
- package/lib/types/commands/annotate.d.ts +3 -0
- package/lib/types/commands/calibrate.d.ts +7 -0
- package/lib/types/commands/color.d.ts +9 -0
- package/lib/types/commands/crop.d.ts +3 -0
- package/lib/types/commands/dispatch.d.ts +22 -0
- package/lib/types/commands/env.d.ts +21 -0
- package/lib/types/commands/index.d.ts +13 -0
- package/lib/types/commands/input.d.ts +17 -0
- package/lib/types/commands/kf.d.ts +10 -0
- package/lib/types/commands/probe.d.ts +15 -0
- package/lib/types/commands/reco.d.ts +27 -0
- package/lib/types/commands/repl.d.ts +10 -0
- package/lib/types/commands/roi-check.d.ts +3 -0
- package/lib/types/commands/roi-compare.d.ts +3 -0
- package/lib/types/commands/runtime.d.ts +66 -0
- package/lib/types/commands/skill.d.ts +17 -0
- package/lib/types/commands/timing.d.ts +64 -0
- package/lib/types/commands/version.d.ts +3 -0
- package/lib/types/exec.d.ts +31 -0
- package/lib/types/flags.d.ts +62 -0
- package/lib/types/image.d.ts +27 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/interface/hotkey.d.ts +36 -0
- package/lib/types/interface/index.d.ts +11 -0
- package/lib/types/interface/jsonc.d.ts +9 -0
- package/lib/types/interface/load.d.ts +190 -0
- package/lib/types/interface/override.d.ts +78 -0
- package/lib/types/interface/plan.d.ts +68 -0
- package/lib/types/option-args.d.ts +19 -0
- package/lib/types/pkg.d.ts +14 -0
- package/lib/types/protocol.d.ts +124 -0
- package/lib/types/runtime/actions.d.ts +248 -0
- package/lib/types/runtime/keyframes.d.ts +43 -0
- package/lib/types/runtime/log-watch.d.ts +28 -0
- package/lib/types/runtime/node-source.d.ts +25 -0
- package/lib/types/runtime/ocr-model.d.ts +8 -0
- package/lib/types/runtime/pi-env.d.ts +24 -0
- package/lib/types/runtime/pretask.d.ts +53 -0
- package/lib/types/runtime/prune.d.ts +4 -0
- package/lib/types/runtime/receipts.d.ts +7 -0
- package/lib/types/runtime/reco-args.d.ts +18 -0
- package/lib/types/runtime/reco-verdict.d.ts +50 -0
- package/lib/types/runtime/roi-compare.d.ts +117 -0
- package/lib/types/runtime/session-forward.d.ts +10 -0
- package/lib/types/runtime/session-ipc.d.ts +117 -0
- package/lib/types/runtime/session.d.ts +77 -0
- package/lib/types/runtime/signal-guard.d.ts +19 -0
- package/lib/types/update-check.d.ts +20 -0
- package/lib/types/usage.d.ts +3 -0
- package/lib/update-check.js +97 -0
- package/lib/usage.js +50 -0
- package/package.json +94 -4
- package/skills/README.md +51 -0
- package/skills/maafw-live/SKILL.md +293 -0
- package/skills/maafw-live/references/examples.md +238 -0
- package/skills/maafw-live/references/pitfalls.md +211 -0
- package/skills/maafw-live/references/workflows.md +164 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hotkey 占位符 → 当前控制器的虚拟按键码(PI v2.8.0+)。
|
|
3
|
+
*
|
|
4
|
+
* 协议只给规则、不给表:`{名称}` / `{名称}.primary` / `{名称}.modifier1` / `{名称}.modifier2`
|
|
5
|
+
* 要替换成"**当前控制器 type** 支持的虚拟按键码",且"映射规则应与 ClickKey、KeyDown 等动作所用
|
|
6
|
+
* 键码表一致"(MaaFW 5.14.2 `3.3-ProjectInterfaceV2协议`,pinned commit 8061d5bc)。
|
|
7
|
+
*
|
|
8
|
+
* 这里四张表的数值是各 OS **公开的键码常量**,不是谁发明的东西:
|
|
9
|
+
* - `Win32` → Windows Virtual-Key Codes(`VK_*`,见 winuser.h)
|
|
10
|
+
* - `Adb` → Android `KeyEvent`(`KEYCODE_*`)
|
|
11
|
+
* - `MacOS` → Carbon `Events.h`(`kVK_*`,ANSI 键盘物理位置)
|
|
12
|
+
* - `WlRoots`/`Linux` → Linux `input-event-codes.h`(`KEY_*`/evdev)
|
|
13
|
+
*
|
|
14
|
+
* 取值与 MXU 的实现交叉核对过(MXU 是 AGPL-3.0,本仓 MIT:**只核数值,不搬代码**)。
|
|
15
|
+
* 键名按 MXU 的持久化约定规范化(大写、`+` 连接、末段为主键),因为它就是协议里"人类可读
|
|
16
|
+
* 快捷键字符串"的实际来源之一。
|
|
17
|
+
*/
|
|
18
|
+
const WIN32 = {
|
|
19
|
+
BACKSPACE: 0x08, TAB: 0x09, ENTER: 0x0d, SHIFT: 0x10, CTRL: 0x11, ALT: 0x12,
|
|
20
|
+
META: 0x5b, COMMAND: 0x5b, CMD: 0x5b, PAUSE: 0x13, CAPSLOCK: 0x14, ESC: 0x1b, SPACE: 0x20,
|
|
21
|
+
PAGEUP: 0x21, PAGEDOWN: 0x22, END: 0x23, HOME: 0x24,
|
|
22
|
+
LEFT: 0x25, UP: 0x26, RIGHT: 0x27, DOWN: 0x28, INSERT: 0x2d, DELETE: 0x2e,
|
|
23
|
+
'0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33, '4': 0x34,
|
|
24
|
+
'5': 0x35, '6': 0x36, '7': 0x37, '8': 0x38, '9': 0x39,
|
|
25
|
+
A: 0x41, B: 0x42, C: 0x43, D: 0x44, E: 0x45, F: 0x46, G: 0x47, H: 0x48, I: 0x49,
|
|
26
|
+
J: 0x4a, K: 0x4b, L: 0x4c, M: 0x4d, N: 0x4e, O: 0x4f, P: 0x50, Q: 0x51, R: 0x52,
|
|
27
|
+
S: 0x53, T: 0x54, U: 0x55, V: 0x56, W: 0x57, X: 0x58, Y: 0x59, Z: 0x5a,
|
|
28
|
+
F1: 0x70, F2: 0x71, F3: 0x72, F4: 0x73, F5: 0x74, F6: 0x75,
|
|
29
|
+
F7: 0x76, F8: 0x77, F9: 0x78, F10: 0x79, F11: 0x7a, F12: 0x7b,
|
|
30
|
+
};
|
|
31
|
+
const MACOS = {
|
|
32
|
+
BACKSPACE: 0x33, TAB: 0x30, ENTER: 0x24, SHIFT: 0x38, CTRL: 0x3b, ALT: 0x3a,
|
|
33
|
+
META: 0x37, COMMAND: 0x37, CMD: 0x37, CAPSLOCK: 0x39, ESC: 0x35, SPACE: 0x31,
|
|
34
|
+
PAGEUP: 0x74, PAGEDOWN: 0x79, END: 0x77, HOME: 0x73,
|
|
35
|
+
LEFT: 0x7b, UP: 0x7e, RIGHT: 0x7c, DOWN: 0x7d, DELETE: 0x75,
|
|
36
|
+
'0': 0x1d, '1': 0x12, '2': 0x13, '3': 0x14, '4': 0x15,
|
|
37
|
+
'5': 0x17, '6': 0x16, '7': 0x1a, '8': 0x1c, '9': 0x19,
|
|
38
|
+
A: 0x00, B: 0x0b, C: 0x08, D: 0x02, E: 0x0e, F: 0x03, G: 0x05, H: 0x04, I: 0x22,
|
|
39
|
+
J: 0x26, K: 0x28, L: 0x25, M: 0x2e, N: 0x2d, O: 0x1f, P: 0x23, Q: 0x0c, R: 0x0f,
|
|
40
|
+
S: 0x01, T: 0x11, U: 0x20, V: 0x09, W: 0x0d, X: 0x07, Y: 0x10, Z: 0x06,
|
|
41
|
+
F1: 0x7a, F2: 0x78, F3: 0x63, F4: 0x76, F5: 0x60, F6: 0x61,
|
|
42
|
+
F7: 0x62, F8: 0x64, F9: 0x65, F10: 0x6d, F11: 0x67, F12: 0x6f,
|
|
43
|
+
};
|
|
44
|
+
const ADB = {
|
|
45
|
+
BACKSPACE: 67, TAB: 61, ENTER: 66, SHIFT: 59, CTRL: 113, ALT: 57,
|
|
46
|
+
META: 117, COMMAND: 117, CMD: 117, SPACE: 62, ESC: 111, DELETE: 112,
|
|
47
|
+
HOME: 3, END: 123, PAGEUP: 92, PAGEDOWN: 93,
|
|
48
|
+
LEFT: 21, RIGHT: 22, UP: 19, DOWN: 20,
|
|
49
|
+
'0': 7, '1': 8, '2': 9, '3': 10, '4': 11, '5': 12, '6': 13, '7': 14, '8': 15, '9': 16,
|
|
50
|
+
A: 29, B: 30, C: 31, D: 32, E: 33, F: 34, G: 35, H: 36, I: 37, J: 38, K: 39, L: 40,
|
|
51
|
+
M: 41, N: 42, O: 43, P: 44, Q: 45, R: 46, S: 47, T: 48, U: 49, V: 50, W: 51, X: 52,
|
|
52
|
+
Y: 53, Z: 54,
|
|
53
|
+
F1: 131, F2: 132, F3: 133, F4: 134, F5: 135, F6: 136, F7: 137, F8: 138, F9: 139,
|
|
54
|
+
F10: 140, F11: 141, F12: 142,
|
|
55
|
+
};
|
|
56
|
+
const WLROOTS = {
|
|
57
|
+
BACKSPACE: 14, TAB: 15, ENTER: 28, SHIFT: 42, CTRL: 29, ALT: 56,
|
|
58
|
+
META: 125, COMMAND: 125, CMD: 125, SPACE: 57, ESC: 1, DELETE: 111,
|
|
59
|
+
HOME: 102, END: 107, PAGEUP: 104, PAGEDOWN: 109,
|
|
60
|
+
LEFT: 105, RIGHT: 106, UP: 103, DOWN: 108,
|
|
61
|
+
'0': 11, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6, '6': 7, '7': 8, '8': 9, '9': 10,
|
|
62
|
+
A: 30, B: 48, C: 46, D: 32, E: 18, F: 33, G: 34, H: 35, I: 23, J: 36, K: 37, L: 38,
|
|
63
|
+
M: 50, N: 49, O: 24, P: 25, Q: 16, R: 19, S: 31, T: 20, U: 22, V: 47, W: 17, X: 45,
|
|
64
|
+
Y: 21, Z: 44,
|
|
65
|
+
F1: 59, F2: 60, F3: 61, F4: 62, F5: 63, F6: 64, F7: 65, F8: 66, F9: 67, F10: 68,
|
|
66
|
+
F11: 87, F12: 88,
|
|
67
|
+
};
|
|
68
|
+
/** 同义键名归一:用户手写的组合键五花八门,认得出就认,认不出会在下面报"未知按键"。 */
|
|
69
|
+
const ALIASES = {
|
|
70
|
+
CONTROL: 'CTRL', ESCAPE: 'ESC', RETURN: 'ENTER', DEL: 'DELETE', INS: 'INSERT',
|
|
71
|
+
PGUP: 'PAGEUP', PGDN: 'PAGEDOWN', WIN: 'META', WINDOWS: 'META', SUPER: 'META',
|
|
72
|
+
SPACEBAR: 'SPACE', CAPS: 'CAPSLOCK', OPTION: 'ALT',
|
|
73
|
+
};
|
|
74
|
+
/** 按控制器 **type** 取表(不是 name)。没有表的类型返回 null——调用方要告警,不能退回 Win32 猜。 */
|
|
75
|
+
export function hotkeyTableFor(controllerType, useWin32VkCode = false) {
|
|
76
|
+
switch (controllerType) {
|
|
77
|
+
case 'Win32':
|
|
78
|
+
return WIN32;
|
|
79
|
+
case 'Adb':
|
|
80
|
+
return ADB;
|
|
81
|
+
case 'MacOS':
|
|
82
|
+
return MACOS;
|
|
83
|
+
case 'WlRoots':
|
|
84
|
+
return WLROOTS;
|
|
85
|
+
/* Linux 控制器可声明 `linux.use_win32_vk_code`:true 时服务端按 Win32 VK 解释,false 用 evdev */
|
|
86
|
+
case 'Linux':
|
|
87
|
+
return useWin32VkCode ? WIN32 : WLROOTS;
|
|
88
|
+
default:
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function normalizeKey(name) {
|
|
93
|
+
const up = name.trim().toUpperCase();
|
|
94
|
+
return ALIASES[up] ?? up;
|
|
95
|
+
}
|
|
96
|
+
/** "Ctrl+Shift+A" → { primary: "A", modifiers: ["Ctrl","Shift"] };末段为主键。 */
|
|
97
|
+
export function splitHotkeyCombo(value) {
|
|
98
|
+
const parts = value.split('+').map((p) => p.trim()).filter(Boolean);
|
|
99
|
+
if (!parts.length)
|
|
100
|
+
return { primary: '', modifiers: [] };
|
|
101
|
+
return { primary: parts[parts.length - 1], modifiers: parts.slice(0, -1) };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 把一个 hotkey 字段解析成四种占位符的取值。
|
|
105
|
+
*
|
|
106
|
+
* 缺修饰键不是错(单键 "E" 本来就没有修饰键),补 0 = 无按键;**主键查不到是错**——
|
|
107
|
+
* 写个 0 进去等于"按了空键",任务会静默点不动。
|
|
108
|
+
*/
|
|
109
|
+
export function resolveHotkeyTokens(fieldName, raw, table) {
|
|
110
|
+
const { primary, modifiers } = splitHotkeyCombo(raw);
|
|
111
|
+
if (!primary)
|
|
112
|
+
return { tokens: {}, error: '取值为空' };
|
|
113
|
+
const code = table[normalizeKey(primary)];
|
|
114
|
+
if (code === undefined) {
|
|
115
|
+
return { tokens: {}, error: '主键 ' + primary + ' 不在该控制器的键码表里(' + raw + ')' };
|
|
116
|
+
}
|
|
117
|
+
const mod = (name) => {
|
|
118
|
+
if (!name)
|
|
119
|
+
return 0;
|
|
120
|
+
const c = table[normalizeKey(name)];
|
|
121
|
+
/* 修饰键查不到也按"无"处理会让组合键变成单键——那是静默错,直接报错 */
|
|
122
|
+
if (c === undefined)
|
|
123
|
+
throw new Error('修饰键 ' + name + ' 不在该控制器的键码表里(' + raw + ')');
|
|
124
|
+
return c;
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
const m1 = mod(modifiers[0]);
|
|
128
|
+
const m2 = mod(modifiers[1]);
|
|
129
|
+
return {
|
|
130
|
+
tokens: {
|
|
131
|
+
[fieldName]: code,
|
|
132
|
+
[fieldName + '.primary']: code,
|
|
133
|
+
[fieldName + '.modifier1']: m1,
|
|
134
|
+
[fieldName + '.modifier2']: m2,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
return { tokens: {}, error: String(e.message || e) };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=hotkey.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目声明(interface.json / ProjectInterface v2)的窄解析与连接规划。
|
|
3
|
+
*
|
|
4
|
+
* 这是"认识项目"的唯一一份实现:控制器声明 → daemon connect 参数、
|
|
5
|
+
* 资源声明 → resource 目录、task 声明 → run 入口解析、
|
|
6
|
+
* option/preset → pipeline_override 四级合并链。
|
|
7
|
+
*/
|
|
8
|
+
export { parseJsonc } from './jsonc.js';
|
|
9
|
+
export { loadInterface, findInterfaceFile, } from './load.js';
|
|
10
|
+
export { planController, resolveResourcePaths, } from './plan.js';
|
|
11
|
+
export { computePipelineOverride, appendOverride } from './override.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 容错 JSON:真实项目的 interface.json 带 `//` 注释(官方解析器基于 meojson),
|
|
3
|
+
* strict JSON.parse 会直接拒绝,因此先做最小脱敏——字符串外的注释与尾随逗号去掉。
|
|
4
|
+
*
|
|
5
|
+
* 取舍:只做这两件**最小**的事,不做单引号 / 无引号键 / 多行字符串那类扩展——
|
|
6
|
+
* 越是"看起来像 JSON"的输入,越容易被静默改写成作者没写的意思。
|
|
7
|
+
* 另:开头的 BOM(Windows 记事本等会写)先剥掉,它会让 JSON.parse 报 "Unexpected token"。
|
|
8
|
+
*/
|
|
9
|
+
export function parseJsonc(text) {
|
|
10
|
+
const noBom = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
|
|
11
|
+
return JSON.parse(stripTrailingCommas(stripComments(noBom)));
|
|
12
|
+
}
|
|
13
|
+
function stripComments(s) {
|
|
14
|
+
let out = '';
|
|
15
|
+
let i = 0;
|
|
16
|
+
let inStr = false;
|
|
17
|
+
while (i < s.length) {
|
|
18
|
+
const ch = s[i];
|
|
19
|
+
if (inStr) {
|
|
20
|
+
out += ch;
|
|
21
|
+
if (ch === '\\' && i + 1 < s.length) {
|
|
22
|
+
out += s[i + 1];
|
|
23
|
+
i += 2;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (ch === '"')
|
|
27
|
+
inStr = false;
|
|
28
|
+
i++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === '"') {
|
|
32
|
+
inStr = true;
|
|
33
|
+
out += ch;
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (ch === '/' && s[i + 1] === '/') {
|
|
38
|
+
while (i < s.length && s[i] !== '\n')
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (ch === '/' && s[i + 1] === '*') {
|
|
43
|
+
i += 2;
|
|
44
|
+
while (i < s.length && !(s[i] === '*' && s[i + 1] === '/'))
|
|
45
|
+
i++;
|
|
46
|
+
i += 2;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
out += ch;
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function stripTrailingCommas(s) {
|
|
55
|
+
let out = '';
|
|
56
|
+
let i = 0;
|
|
57
|
+
let inStr = false;
|
|
58
|
+
while (i < s.length) {
|
|
59
|
+
const ch = s[i];
|
|
60
|
+
if (inStr) {
|
|
61
|
+
out += ch;
|
|
62
|
+
if (ch === '\\' && i + 1 < s.length) {
|
|
63
|
+
out += s[i + 1];
|
|
64
|
+
i += 2;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (ch === '"')
|
|
68
|
+
inStr = false;
|
|
69
|
+
i++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (ch === '"') {
|
|
73
|
+
inStr = true;
|
|
74
|
+
out += ch;
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === ',') {
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < s.length && isSpace(s[j]))
|
|
81
|
+
j++;
|
|
82
|
+
if (s[j] === '}' || s[j] === ']') {
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
out += ch;
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function isSpace(c) {
|
|
93
|
+
return c === ' ' || c === '\t' || c === '\n' || c === '\r';
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=jsonc.js.map
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 载入项目 interface.json(ProjectInterface v2)。
|
|
3
|
+
*
|
|
4
|
+
* 解析面就是本工具真正消费的那部分:控制器(连接规划)、资源(路径解析)、任务入口
|
|
5
|
+
* (run --project)、option / preset / global_option(pipeline_override 四级链)、
|
|
6
|
+
* agent 与 pretask(子进程相关)、import(v2.2.0 合并规则)。
|
|
7
|
+
*
|
|
8
|
+
* 明确**不**解析、不消费的:task 级 pipeline_override(本工具只合成 option 链,任务级写死的
|
|
9
|
+
* 覆盖要由 Client 应用)、i18n(languages 与 `$` 键——当前不输出人类标签)、group / setting /
|
|
10
|
+
* telemetry、resource.hash(v2.6.0 的一致性校验,未实现)。
|
|
11
|
+
* 全过程不抛异常:问题进 problems[],由调用方决定怎么呈现。
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { parseJsonc } from './jsonc.js';
|
|
16
|
+
/** 找 interface.json:项目根优先,其次 assets/(部分项目把声明放资源目录下)。 */
|
|
17
|
+
const REL_CANDIDATES = ['interface.json', 'assets/interface.json'];
|
|
18
|
+
export function findInterfaceFile(dir) {
|
|
19
|
+
const base = resolve(dir);
|
|
20
|
+
for (const rel of REL_CANDIDATES) {
|
|
21
|
+
const p = join(base, rel);
|
|
22
|
+
if (existsSync(p))
|
|
23
|
+
return p;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function str(v) {
|
|
28
|
+
return typeof v === 'string' && v.trim() ? v : undefined;
|
|
29
|
+
}
|
|
30
|
+
function num(v) {
|
|
31
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
32
|
+
}
|
|
33
|
+
function strList(v) {
|
|
34
|
+
if (typeof v === 'string')
|
|
35
|
+
return [v];
|
|
36
|
+
if (Array.isArray(v))
|
|
37
|
+
return v.filter((x) => typeof x === 'string' && x.length > 0);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
function obj(v) {
|
|
41
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : null;
|
|
42
|
+
}
|
|
43
|
+
export function loadInterface(dir) {
|
|
44
|
+
const file = findInterfaceFile(dir);
|
|
45
|
+
const base = {
|
|
46
|
+
file,
|
|
47
|
+
dir: file ? dirname(file) : null,
|
|
48
|
+
version: null,
|
|
49
|
+
projectVersion: null,
|
|
50
|
+
name: null,
|
|
51
|
+
controllers: [],
|
|
52
|
+
resources: [],
|
|
53
|
+
tasks: [],
|
|
54
|
+
options: {},
|
|
55
|
+
presets: [],
|
|
56
|
+
globalOption: [],
|
|
57
|
+
pretask: [],
|
|
58
|
+
agents: [],
|
|
59
|
+
problems: [],
|
|
60
|
+
};
|
|
61
|
+
if (!file) {
|
|
62
|
+
return { ...base, problems: [{ message: '未找到 interface.json(找过 ' + REL_CANDIDATES.join(' / ') + ')' }] };
|
|
63
|
+
}
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = parseJsonc(readFileSync(file, 'utf8'));
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return { ...base, problems: [{ message: 'interface.json 解析失败:' + String(e.message) }] };
|
|
70
|
+
}
|
|
71
|
+
const o = obj(raw);
|
|
72
|
+
if (!o)
|
|
73
|
+
return { ...base, problems: [{ message: 'interface.json 顶层必须是对象' }] };
|
|
74
|
+
const problems = [];
|
|
75
|
+
const version = num(o.interface_version);
|
|
76
|
+
if (version === undefined)
|
|
77
|
+
problems.push({ message: '缺少 interface_version(ProjectInterface v2 应为 2)' });
|
|
78
|
+
else if (version !== 2)
|
|
79
|
+
problems.push({ message: 'interface_version=' + version + ';本工具按 v2 解析,可能有出入' });
|
|
80
|
+
const controllers = [];
|
|
81
|
+
for (const item of Array.isArray(o.controller) ? o.controller : []) {
|
|
82
|
+
const c = obj(item);
|
|
83
|
+
const name = c ? str(c.name) : undefined;
|
|
84
|
+
const type = c ? str(c.type) : undefined;
|
|
85
|
+
if (!c || !name || !type) {
|
|
86
|
+
problems.push({ message: 'controller 项缺少 name 或 type,已跳过' });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const display = {};
|
|
90
|
+
const ss = num(c.display_short_side);
|
|
91
|
+
if (ss !== undefined)
|
|
92
|
+
display.shortSide = ss;
|
|
93
|
+
const ls = num(c.display_long_side);
|
|
94
|
+
if (ls !== undefined)
|
|
95
|
+
display.longSide = ls;
|
|
96
|
+
if (c.display_raw === true)
|
|
97
|
+
display.raw = true;
|
|
98
|
+
if (Array.isArray(c.display_expand) && c.display_expand.length === 2) {
|
|
99
|
+
const a = num(c.display_expand[0]);
|
|
100
|
+
const b = num(c.display_expand[1]);
|
|
101
|
+
if (a !== undefined && b !== undefined)
|
|
102
|
+
display.expand = [a, b];
|
|
103
|
+
}
|
|
104
|
+
const w = obj(c.win32);
|
|
105
|
+
const g = obj(c.gamepad);
|
|
106
|
+
const mo = obj(c.macos);
|
|
107
|
+
const linuxDecl = obj(c.linux);
|
|
108
|
+
controllers.push({
|
|
109
|
+
name,
|
|
110
|
+
type,
|
|
111
|
+
raw: c,
|
|
112
|
+
display: Object.keys(display).length ? display : undefined,
|
|
113
|
+
macos: mo
|
|
114
|
+
? {
|
|
115
|
+
titleRegex: str(mo.title_regex),
|
|
116
|
+
screencap: str(mo.screencap),
|
|
117
|
+
input: str(mo.input),
|
|
118
|
+
}
|
|
119
|
+
: undefined,
|
|
120
|
+
win32: w
|
|
121
|
+
? {
|
|
122
|
+
classRegex: str(w.class_regex),
|
|
123
|
+
windowRegex: str(w.window_regex),
|
|
124
|
+
screencap: str(w.screencap),
|
|
125
|
+
mouse: str(w.mouse),
|
|
126
|
+
keyboard: str(w.keyboard),
|
|
127
|
+
}
|
|
128
|
+
: undefined,
|
|
129
|
+
gamepad: g
|
|
130
|
+
? {
|
|
131
|
+
classRegex: str(g.class_regex),
|
|
132
|
+
windowRegex: str(g.window_regex),
|
|
133
|
+
gamepadType: str(g.gamepad_type),
|
|
134
|
+
screencap: str(g.screencap),
|
|
135
|
+
}
|
|
136
|
+
: undefined,
|
|
137
|
+
attachResourcePath: strList(c.attach_resource_path),
|
|
138
|
+
option: strList(c.option),
|
|
139
|
+
...(linuxDecl
|
|
140
|
+
? {
|
|
141
|
+
linux: {
|
|
142
|
+
...(linuxDecl.use_win32_vk_code === true ? { useWin32VkCode: true } : {}),
|
|
143
|
+
...(str(linuxDecl.screencap) ? { screencap: str(linuxDecl.screencap) } : {}),
|
|
144
|
+
...(str(linuxDecl.input) ? { input: str(linuxDecl.input) } : {}),
|
|
145
|
+
...(str(linuxDecl.pipewire_source) ? { pipewireSource: str(linuxDecl.pipewire_source) } : {}),
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
...(c.permission_required === true ? { permissionRequired: true } : {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (!controllers.length)
|
|
153
|
+
problems.push({ message: 'controller[] 为空:项目没有声明任何控制器' });
|
|
154
|
+
const resources = [];
|
|
155
|
+
for (const item of Array.isArray(o.resource) ? o.resource : []) {
|
|
156
|
+
const r = obj(item);
|
|
157
|
+
const name = r ? str(r.name) : undefined;
|
|
158
|
+
const paths = r ? strList(r.path) : [];
|
|
159
|
+
if (!r || !name || !paths.length) {
|
|
160
|
+
problems.push({ message: 'resource 项缺少 name 或 path,已跳过' });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
resources.push({
|
|
164
|
+
name, paths, controllers: strList(r.controller), option: strList(r.option),
|
|
165
|
+
...(r.hash !== undefined ? { hash: str(r.hash) } : {}), raw: r,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const tasks = [];
|
|
169
|
+
const options = {};
|
|
170
|
+
const presets = [];
|
|
171
|
+
const globalOption = [];
|
|
172
|
+
const pretask = [];
|
|
173
|
+
const agents = [];
|
|
174
|
+
const readAgents = (list) => {
|
|
175
|
+
for (const item of Array.isArray(list) ? list : (list !== undefined ? [list] : [])) {
|
|
176
|
+
const a = obj(item);
|
|
177
|
+
const exec = a ? str(a.child_exec) : undefined;
|
|
178
|
+
if (!a || !exec)
|
|
179
|
+
continue;
|
|
180
|
+
agents.push({ exec, args: strList(a.child_args), ...(str(a.identifier) !== undefined ? { identifier: str(a.identifier) } : {}) });
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const readTasks = (list, src) => {
|
|
184
|
+
for (const item of Array.isArray(list) ? list : []) {
|
|
185
|
+
const t = obj(item);
|
|
186
|
+
const name = t ? str(t.name) : undefined;
|
|
187
|
+
if (!t || !name) {
|
|
188
|
+
problems.push({ message: 'task 项缺少 name,已跳过(' + src + ')' });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const po = obj(t.pipeline_override);
|
|
192
|
+
tasks.push({
|
|
193
|
+
name,
|
|
194
|
+
entry: str(t.entry),
|
|
195
|
+
controller: strList(t.controller),
|
|
196
|
+
resource: strList(t.resource),
|
|
197
|
+
option: strList(t.option),
|
|
198
|
+
...(po ? { pipelineOverride: po } : {}),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const readOptions = (dict, src) => {
|
|
203
|
+
const d = obj(dict);
|
|
204
|
+
if (!d) {
|
|
205
|
+
if (dict !== undefined)
|
|
206
|
+
problems.push({ message: 'option 应为对象(键=配置项名),已忽略(' + src + ')' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
for (const [key, val] of Object.entries(d)) {
|
|
210
|
+
const v = obj(val);
|
|
211
|
+
/* type 缺省按 select(上游 PI 文档与 Types.h:212 的默认);仍记 problem,
|
|
212
|
+
* 因为它大概率是漏写而非有意缺省——警告可见,行为对齐上游。 */
|
|
213
|
+
const type = v ? (str(v.type) ?? 'select') : undefined;
|
|
214
|
+
if (!v || !type) {
|
|
215
|
+
problems.push({ message: 'option ' + key + ' 缺少 type,已跳过(' + src + ')' });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const cases = [];
|
|
219
|
+
for (const cs of Array.isArray(v.cases) ? v.cases : []) {
|
|
220
|
+
const c2 = obj(cs);
|
|
221
|
+
const cname = c2 ? str(c2.name) : undefined;
|
|
222
|
+
if (!c2 || !cname)
|
|
223
|
+
continue;
|
|
224
|
+
const po = obj(c2.pipeline_override);
|
|
225
|
+
cases.push({
|
|
226
|
+
name: cname,
|
|
227
|
+
...(po ? { pipelineOverride: po } : {}),
|
|
228
|
+
option: strList(c2.option),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const tpl = obj(v.pipeline_override);
|
|
232
|
+
const inputs = [];
|
|
233
|
+
for (const it of Array.isArray(v.inputs) ? v.inputs : []) {
|
|
234
|
+
const f = obj(it);
|
|
235
|
+
const fname = f ? str(f.name) : undefined;
|
|
236
|
+
if (!f || !fname)
|
|
237
|
+
continue;
|
|
238
|
+
const pwd = f.password === true;
|
|
239
|
+
inputs.push({
|
|
240
|
+
name: fname,
|
|
241
|
+
...(str(f.default) !== undefined && !pwd ? { default: str(f.default) } : {}),
|
|
242
|
+
...(str(f.pipeline_type) !== undefined ? { pipelineType: str(f.pipeline_type) } : {}),
|
|
243
|
+
...(str(f.verify) !== undefined ? { verify: str(f.verify) } : {}),
|
|
244
|
+
...(pwd ? { password: true } : {}),
|
|
245
|
+
});
|
|
246
|
+
if (pwd && str(f.default) !== undefined) {
|
|
247
|
+
problems.push({ message: 'option ' + key + ' 的输入字段 ' + fname + ':password=true 时规范禁止同时设 default,default 已忽略' });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const hotkeys = [];
|
|
251
|
+
for (const it of Array.isArray(v.hotkeys) ? v.hotkeys : []) {
|
|
252
|
+
const f = obj(it);
|
|
253
|
+
const fname = f ? str(f.name) : undefined;
|
|
254
|
+
if (!f || !fname)
|
|
255
|
+
continue;
|
|
256
|
+
hotkeys.push({ name: fname, ...(str(f.default) !== undefined ? { default: str(f.default) } : {}) });
|
|
257
|
+
}
|
|
258
|
+
const minC = num(v.min_count);
|
|
259
|
+
const maxC = num(v.max_count);
|
|
260
|
+
if (type === 'checkbox') {
|
|
261
|
+
if (minC !== undefined && maxC !== undefined && minC > maxC) {
|
|
262
|
+
problems.push({ message: 'option ' + key + ':min_count=' + minC + ' > max_count=' + maxC + ',约束自相矛盾' });
|
|
263
|
+
}
|
|
264
|
+
if (minC !== undefined && cases.length && minC > cases.length) {
|
|
265
|
+
problems.push({ message: 'option ' + key + ':min_count=' + minC + ' 超过声明的 case 数 ' + cases.length + ',永远选不够' });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
options[key] = {
|
|
269
|
+
name: key,
|
|
270
|
+
type,
|
|
271
|
+
cases,
|
|
272
|
+
...(tpl ? { pipelineOverride: tpl } : {}),
|
|
273
|
+
...(inputs.length ? { inputs } : {}),
|
|
274
|
+
...(hotkeys.length ? { hotkeys } : {}),
|
|
275
|
+
...(v.default_case !== undefined ? { defaultCase: v.default_case } : {}),
|
|
276
|
+
controller: strList(v.controller),
|
|
277
|
+
resource: strList(v.resource),
|
|
278
|
+
...(type === 'checkbox' && minC !== undefined ? { minCount: minC } : {}),
|
|
279
|
+
...(type === 'checkbox' && maxC !== undefined ? { maxCount: maxC } : {}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const readPresets = (list) => {
|
|
284
|
+
for (const item of Array.isArray(list) ? list : []) {
|
|
285
|
+
const p = obj(item);
|
|
286
|
+
const name = p ? str(p.name) : undefined;
|
|
287
|
+
if (!p || !name)
|
|
288
|
+
continue;
|
|
289
|
+
const tlist = [];
|
|
290
|
+
for (const tp of Array.isArray(p.task) ? p.task : []) {
|
|
291
|
+
const t2 = obj(tp);
|
|
292
|
+
const tname = t2 ? str(t2.name) : undefined;
|
|
293
|
+
if (!t2 || !tname)
|
|
294
|
+
continue;
|
|
295
|
+
const pv = obj(t2.option);
|
|
296
|
+
tlist.push({ name: tname, enabled: t2.enabled !== false, ...(pv ? { option: pv } : {}) });
|
|
297
|
+
}
|
|
298
|
+
presets.push({ name, task: tlist });
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
readTasks(o.task, '主文件');
|
|
302
|
+
readOptions(o.option, '主文件');
|
|
303
|
+
readPresets(o.preset);
|
|
304
|
+
readAgents(o.agent);
|
|
305
|
+
for (const g of strList(o.global_option))
|
|
306
|
+
if (!globalOption.includes(g))
|
|
307
|
+
globalOption.push(g);
|
|
308
|
+
/* pretask:`exec` 必填;`args` / `option` / 过滤列表缺省为空。单条也可写成对象而不是数组。 */
|
|
309
|
+
const readPretasks = (list, src) => {
|
|
310
|
+
for (const pt of Array.isArray(list) ? list : (list !== undefined ? [list] : [])) {
|
|
311
|
+
const p2 = obj(pt);
|
|
312
|
+
const exec = p2 ? str(p2.exec) : undefined;
|
|
313
|
+
if (!p2)
|
|
314
|
+
continue;
|
|
315
|
+
if (!exec) {
|
|
316
|
+
problems.push({ message: 'pretask 缺少 exec,已跳过(' + src + ')' });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
pretask.push({
|
|
320
|
+
...(str(p2.name) !== undefined ? { name: str(p2.name) } : {}),
|
|
321
|
+
exec,
|
|
322
|
+
args: strList(p2.args),
|
|
323
|
+
option: strList(p2.option),
|
|
324
|
+
...(strList(p2.controller).length ? { controller: strList(p2.controller) } : {}),
|
|
325
|
+
...(strList(p2.resource).length ? { resource: strList(p2.resource) } : {}),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
readPretasks(o.pretask, '主文件');
|
|
330
|
+
/* import 合并(v2.2.0):只合 task/option/preset/global_option/pretask(及 group/setting,本工具不消费)。
|
|
331
|
+
* controller/resource 不可导入——子文件声明了也忽略并记 problems。循环导入用 visited 集合挡住。
|
|
332
|
+
*
|
|
333
|
+
* 上游口径(MaaFW 5.14.2 @ 8061d5b,逐条核过):
|
|
334
|
+
* - 被 import 的文件由 tools/interface_import.schema.json 约束(描述原文:"用于 interface.json 的
|
|
335
|
+
* import 字段引用的文件"),顶层只有 task/option/pretask/global_option/setting/preset,且
|
|
336
|
+
* `additionalProperties: false` → **`import` 不可嵌套**、`agent` 也不在其中;
|
|
337
|
+
* - 协议文档 3.3 的 import 节把 `group` 列进可导入字段(v2.4.0,合并表里也有 group 行),
|
|
338
|
+
* 而 5.14.2 的 import schema 顶层**没有 group** → 文档与 schema 不一致(上游漂移)。
|
|
339
|
+
* **后续:上游已就这条提 PR,结论是"改 schema"**(即 import 文件将来可以合法带 group)。
|
|
340
|
+
* 我们的行为不变——group 本工具不消费,因此既不合并也不报错;真要用到 UI 分组时再说。
|
|
341
|
+
* - `agent` 不在 import schema 里,但本工具**宽容读取**(下面的 readAgents):这是超出协议的扩展,
|
|
342
|
+
* 只影响"文件结构校验收紧的工具"与我们的行为差异,实现上进 candidates 前仍会如实报 problems。 */
|
|
343
|
+
const seen = new Set([file]);
|
|
344
|
+
const imports = strList(o.import);
|
|
345
|
+
for (const rel of imports) {
|
|
346
|
+
const childFile = join(dirname(file), rel);
|
|
347
|
+
if (seen.has(childFile)) {
|
|
348
|
+
problems.push({ message: 'import 循环引用,已跳过:' + rel });
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
seen.add(childFile);
|
|
352
|
+
let child = null;
|
|
353
|
+
try {
|
|
354
|
+
child = obj(parseJsonc(readFileSync(childFile, 'utf8')));
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
problems.push({ message: 'import 文件读取/解析失败:' + rel + '(' + String(e.message) + ')' });
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!child)
|
|
361
|
+
continue;
|
|
362
|
+
if (child.controller !== undefined || child.resource !== undefined) {
|
|
363
|
+
problems.push({ message: 'import 文件 ' + rel + ' 声明了 controller/resource:协议不可导入,已忽略' });
|
|
364
|
+
}
|
|
365
|
+
/* import 不可嵌套:被引用的文件由 interface_import.schema.json 约束(MaaFW 5.14.2,
|
|
366
|
+
* 顶层只有 task/option/pretask/global_option/setting/preset 且 additionalProperties: false,
|
|
367
|
+
* 文档 3.3 的 import 节也没有嵌套写法)。静默吞掉会让"二级文件"整份消失得无影无踪,
|
|
368
|
+
* 所以这里记一条 problem——它通常意味着拆分方式需要改成平铺。 */
|
|
369
|
+
if (child.import !== undefined) {
|
|
370
|
+
problems.push({ message: 'import 文件 ' + rel + ' 里还有 import:import 不可嵌套(该文件的 schema 顶层没有 import 字段),其引用的文件已忽略' });
|
|
371
|
+
}
|
|
372
|
+
readTasks(child.task, rel);
|
|
373
|
+
readOptions(child.option, rel);
|
|
374
|
+
readPresets(child.preset);
|
|
375
|
+
readAgents(child.agent);
|
|
376
|
+
for (const g of strList(child.global_option))
|
|
377
|
+
if (!globalOption.includes(g))
|
|
378
|
+
globalOption.push(g);
|
|
379
|
+
readPretasks(child.pretask, rel);
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
...base,
|
|
383
|
+
version: version ?? null,
|
|
384
|
+
projectVersion: str(o.version) ?? null,
|
|
385
|
+
name: str(o.name) ?? null,
|
|
386
|
+
controllers,
|
|
387
|
+
resources,
|
|
388
|
+
tasks,
|
|
389
|
+
options,
|
|
390
|
+
presets,
|
|
391
|
+
globalOption,
|
|
392
|
+
pretask,
|
|
393
|
+
agents,
|
|
394
|
+
problems,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
//# sourceMappingURL=load.js.map
|