helixlife-v5-cli 1.2.0 → 1.2.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/lib/.cli.runtime.generated.json +7 -0
- package/lib/automation-overlay.init.js +208 -78
- package/lib/cli.js +76 -45
- package/lib/cli.runtime.config.json +1 -3
- package/lib/visual.js +248 -0
- package/package.json +1 -1
- package/lib/automation-overlay.browser.js +0 -76
- package/lib/automation-overlay.js +0 -238
|
@@ -1,78 +1,208 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* helixlife-v5-cli 页面内视觉反馈层。
|
|
4
|
+
* 挂载 window.__helix_visual__:节点高亮框 + 动作标签(pending/success/danger/info)+ 右上角 HUD。
|
|
5
|
+
* 可作为 Playwright initScript 注入,也可由 CLI eval 懒加载。
|
|
6
|
+
*/
|
|
7
|
+
(() => {
|
|
8
|
+
if (window.__helix_visual__) return;
|
|
9
|
+
|
|
10
|
+
const VERSION = '1.0.0';
|
|
11
|
+
const VISUAL_DEFAULTS = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
durationMs: 420,
|
|
14
|
+
};
|
|
15
|
+
const VISUAL_STYLE_ID = '__helix_visual_style__';
|
|
16
|
+
const VISUAL_LAYER_ID = '__helix_visual_layer__';
|
|
17
|
+
const VISUAL_HUD_ID = '__helix_visual_hud__';
|
|
18
|
+
const VISUAL_TONE_MAP = {
|
|
19
|
+
pending: { border: '#faad14', fill: 'rgba(250, 173, 20, 0.16)', pill: '#ad6800', text: '#fffbe6' },
|
|
20
|
+
success: { border: '#52c41a', fill: 'rgba(82, 196, 26, 0.14)', pill: '#237804', text: '#f6ffed' },
|
|
21
|
+
danger: { border: '#ff4d4f', fill: 'rgba(255, 77, 79, 0.14)', pill: '#a8071a', text: '#fff1f0' },
|
|
22
|
+
info: { border: '#1677ff', fill: 'rgba(22, 119, 255, 0.14)', pill: '#0958d9', text: '#f0f5ff' },
|
|
23
|
+
};
|
|
24
|
+
const visualState = {
|
|
25
|
+
config: { ...VISUAL_DEFAULTS },
|
|
26
|
+
hudTimer: null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function clamp(n, min, max) {
|
|
30
|
+
return Math.max(min, Math.min(max, n));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeDuration(ms, fallback = VISUAL_DEFAULTS.durationMs) {
|
|
34
|
+
const n = Number(ms);
|
|
35
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
36
|
+
return clamp(Math.round(n), 120, 4000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeVisualOptions(options = {}) {
|
|
40
|
+
const base = { ...visualState.config };
|
|
41
|
+
if (!options || typeof options !== 'object') return base;
|
|
42
|
+
if (typeof options.enabled === 'boolean') base.enabled = options.enabled;
|
|
43
|
+
if (options.durationMs != null) base.durationMs = normalizeDuration(options.durationMs, base.durationMs);
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureVisualRoot() {
|
|
48
|
+
let style = document.getElementById(VISUAL_STYLE_ID);
|
|
49
|
+
if (!style) {
|
|
50
|
+
style = document.createElement('style');
|
|
51
|
+
style.id = VISUAL_STYLE_ID;
|
|
52
|
+
style.textContent = `
|
|
53
|
+
#${VISUAL_LAYER_ID}{
|
|
54
|
+
position:fixed;
|
|
55
|
+
inset:0;
|
|
56
|
+
pointer-events:none;
|
|
57
|
+
z-index:2147483646;
|
|
58
|
+
overflow:visible;
|
|
59
|
+
}
|
|
60
|
+
.__helix_visual_box{
|
|
61
|
+
position:fixed;
|
|
62
|
+
box-sizing:border-box;
|
|
63
|
+
border-radius:8px;
|
|
64
|
+
animation:__helix_visual_pulse .55s ease-out 1;
|
|
65
|
+
}
|
|
66
|
+
.__helix_visual_badge{
|
|
67
|
+
position:absolute;
|
|
68
|
+
left:0;
|
|
69
|
+
top:-28px;
|
|
70
|
+
max-width:280px;
|
|
71
|
+
padding:4px 10px;
|
|
72
|
+
border-radius:999px;
|
|
73
|
+
font:600 12px/1.2 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
74
|
+
letter-spacing:.01em;
|
|
75
|
+
white-space:nowrap;
|
|
76
|
+
text-overflow:ellipsis;
|
|
77
|
+
overflow:hidden;
|
|
78
|
+
box-shadow:0 8px 24px rgba(0,0,0,.18);
|
|
79
|
+
}
|
|
80
|
+
#${VISUAL_HUD_ID}{
|
|
81
|
+
position:fixed;
|
|
82
|
+
top:16px;
|
|
83
|
+
right:16px;
|
|
84
|
+
max-width:360px;
|
|
85
|
+
padding:10px 14px;
|
|
86
|
+
border-radius:12px;
|
|
87
|
+
box-shadow:0 12px 32px rgba(0,0,0,.22);
|
|
88
|
+
font:600 13px/1.35 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
89
|
+
white-space:pre-wrap;
|
|
90
|
+
}
|
|
91
|
+
@keyframes __helix_visual_pulse{
|
|
92
|
+
0%{transform:scale(.985);opacity:.2}
|
|
93
|
+
35%{transform:scale(1.003);opacity:1}
|
|
94
|
+
100%{transform:scale(1);opacity:1}
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
(document.head || document.documentElement).appendChild(style);
|
|
98
|
+
}
|
|
99
|
+
let layer = document.getElementById(VISUAL_LAYER_ID);
|
|
100
|
+
if (!layer) {
|
|
101
|
+
layer = document.createElement('div');
|
|
102
|
+
layer.id = VISUAL_LAYER_ID;
|
|
103
|
+
(document.body || document.documentElement).appendChild(layer);
|
|
104
|
+
}
|
|
105
|
+
return layer;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toneSpec(tone) {
|
|
109
|
+
return VISUAL_TONE_MAP[tone] || VISUAL_TONE_MAP.info;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function removeLater(el, durationMs) {
|
|
113
|
+
if (!el) return;
|
|
114
|
+
const ms = normalizeDuration(durationMs);
|
|
115
|
+
window.setTimeout(() => {
|
|
116
|
+
if (el && el.parentNode) el.remove();
|
|
117
|
+
}, ms);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function flashElement(el, options = {}) {
|
|
121
|
+
const { tone = 'info', label = '', durationMs, inset = 0 } = options;
|
|
122
|
+
if (!el || !visualState.config.enabled) return false;
|
|
123
|
+
const rect = el.getBoundingClientRect();
|
|
124
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
|
|
125
|
+
const layer = ensureVisualRoot();
|
|
126
|
+
const spec = toneSpec(tone);
|
|
127
|
+
const box = document.createElement('div');
|
|
128
|
+
box.className = '__helix_visual_box';
|
|
129
|
+
box.style.left = Math.max(4, rect.left - inset) + 'px';
|
|
130
|
+
box.style.top = Math.max(4, rect.top - inset) + 'px';
|
|
131
|
+
box.style.width = Math.max(18, rect.width + inset * 2) + 'px';
|
|
132
|
+
box.style.height = Math.max(18, rect.height + inset * 2) + 'px';
|
|
133
|
+
box.style.border = `2px solid ${spec.border}`;
|
|
134
|
+
box.style.background = spec.fill;
|
|
135
|
+
box.style.boxShadow = `0 0 0 1px ${spec.border}33, 0 12px 28px ${spec.border}22`;
|
|
136
|
+
if (label) {
|
|
137
|
+
const badge = document.createElement('div');
|
|
138
|
+
badge.className = '__helix_visual_badge';
|
|
139
|
+
badge.textContent = label;
|
|
140
|
+
badge.style.background = spec.pill;
|
|
141
|
+
badge.style.color = spec.text;
|
|
142
|
+
badge.style.top = (rect.top < 38 ? rect.height + 8 : -28) + 'px';
|
|
143
|
+
box.appendChild(badge);
|
|
144
|
+
}
|
|
145
|
+
layer.appendChild(box);
|
|
146
|
+
removeLater(box, durationMs);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function showHud(options = {}) {
|
|
151
|
+
const { action = '', target = '', status = 'pending', detail = '', durationMs } = options;
|
|
152
|
+
if (!visualState.config.enabled) return false;
|
|
153
|
+
const layer = ensureVisualRoot();
|
|
154
|
+
let hud = document.getElementById(VISUAL_HUD_ID);
|
|
155
|
+
if (!hud) {
|
|
156
|
+
hud = document.createElement('div');
|
|
157
|
+
hud.id = VISUAL_HUD_ID;
|
|
158
|
+
layer.appendChild(hud);
|
|
159
|
+
}
|
|
160
|
+
const spec = toneSpec(status);
|
|
161
|
+
const lines = [];
|
|
162
|
+
if (action) lines.push(action);
|
|
163
|
+
if (target) lines.push(target);
|
|
164
|
+
if (detail) lines.push(detail);
|
|
165
|
+
hud.textContent = lines.join('\n');
|
|
166
|
+
hud.style.border = `1px solid ${spec.border}`;
|
|
167
|
+
hud.style.background = spec.fill.replace(/0\.\d+\)$/, '0.92)');
|
|
168
|
+
hud.style.color = spec.pill;
|
|
169
|
+
if (visualState.hudTimer) clearTimeout(visualState.hudTimer);
|
|
170
|
+
visualState.hudTimer = window.setTimeout(() => {
|
|
171
|
+
if (hud && hud.parentNode) hud.remove();
|
|
172
|
+
visualState.hudTimer = null;
|
|
173
|
+
}, normalizeDuration(durationMs, Math.max(900, visualState.config.durationMs * 2)));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function cleanupVisualArtifacts() {
|
|
178
|
+
const layer = document.getElementById(VISUAL_LAYER_ID);
|
|
179
|
+
if (!layer) return;
|
|
180
|
+
Array.from(layer.querySelectorAll('.__helix_visual_box')).forEach((el) => el.remove());
|
|
181
|
+
const hud = document.getElementById(VISUAL_HUD_ID);
|
|
182
|
+
if (hud) hud.remove();
|
|
183
|
+
if (visualState.hudTimer) {
|
|
184
|
+
clearTimeout(visualState.hudTimer);
|
|
185
|
+
visualState.hudTimer = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function setVisualOptions(options = {}) {
|
|
190
|
+
if (!options || typeof options !== 'object') {
|
|
191
|
+
visualState.config = { ...VISUAL_DEFAULTS };
|
|
192
|
+
cleanupVisualArtifacts();
|
|
193
|
+
return { ok: true, data: { ...visualState.config } };
|
|
194
|
+
}
|
|
195
|
+
visualState.config = normalizeVisualOptions(options);
|
|
196
|
+
if (!visualState.config.enabled) cleanupVisualArtifacts();
|
|
197
|
+
return { ok: true, data: { ...visualState.config } };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
window.__helix_visual__ = {
|
|
201
|
+
version: () => VERSION,
|
|
202
|
+
setVisualOptions,
|
|
203
|
+
flash: flashElement,
|
|
204
|
+
showHud,
|
|
205
|
+
cleanup: cleanupVisualArtifacts,
|
|
206
|
+
__meta: { version: VERSION, loadedAt: new Date().toISOString() },
|
|
207
|
+
};
|
|
208
|
+
})();
|
package/lib/cli.js
CHANGED
|
@@ -5,12 +5,12 @@ const { spawnSync } = require('node:child_process');
|
|
|
5
5
|
|
|
6
6
|
const PACKAGE_JSON = require(path.join(__dirname, '..', 'package.json'));
|
|
7
7
|
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} = require('./
|
|
8
|
+
parseVisualFlags,
|
|
9
|
+
injectVisualConfig,
|
|
10
|
+
shouldWrapWithVisual,
|
|
11
|
+
runWithVisualFeedback,
|
|
12
|
+
DEFAULT_VISUAL_ENABLED,
|
|
13
|
+
} = require('./visual');
|
|
14
14
|
|
|
15
15
|
/** 默认快照/下载等工作区目录名(相对 cwd),替代引擎默认的 .playwright-cli */
|
|
16
16
|
const DEFAULT_OUTPUT_DIR_NAME = 'helixlife-v5-cli';
|
|
@@ -41,40 +41,36 @@ function buildChildEnv() {
|
|
|
41
41
|
/**
|
|
42
42
|
* 将参数原样交给底层浏览器自动化引擎,返回子进程退出码。
|
|
43
43
|
* 使用 node 直接执行入口脚本,避免依赖 npx、Shell 与 PATH,在 Windows / macOS 上一致。
|
|
44
|
+
* @param {string[]} args
|
|
45
|
+
* @param {{ capture?: boolean }} [options]
|
|
46
|
+
* @returns {number | { code: number, stdout: string, stderr: string }}
|
|
44
47
|
*/
|
|
45
48
|
function runPlaywrightCliPassthrough(args, options = {}) {
|
|
46
|
-
const
|
|
47
|
-
stdio: options.quiet ? ['ignore', 'ignore', 'ignore'] : 'inherit',
|
|
49
|
+
const spawnOpts = {
|
|
48
50
|
env: buildChildEnv(),
|
|
49
51
|
windowsHide: true,
|
|
50
|
-
}
|
|
52
|
+
};
|
|
53
|
+
if (options.capture) {
|
|
54
|
+
spawnOpts.encoding = 'utf8';
|
|
55
|
+
spawnOpts.stdio = ['ignore', 'pipe', 'pipe'];
|
|
56
|
+
} else {
|
|
57
|
+
spawnOpts.stdio = 'inherit';
|
|
58
|
+
}
|
|
59
|
+
const result = spawnSync(process.execPath, [playwrightCliEntry(), ...args], spawnOpts);
|
|
51
60
|
if (result.error) {
|
|
52
61
|
throw result.error;
|
|
53
62
|
}
|
|
54
63
|
if (result.signal) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function updateAutomationOverlay(tokens, phase) {
|
|
62
|
-
if (!isOverlayEnabled() || !shouldUpdateOverlay(tokens)) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const payload = describeOverlayPayload(tokens, phase);
|
|
66
|
-
const code = buildOverlayRunCode(payload);
|
|
67
|
-
runPlaywrightCliPassthrough(['run-code', code], { quiet: true });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function runPlaywrightCliWithOverlay(tokens) {
|
|
71
|
-
const args = injectOpenConfig(tokens);
|
|
72
|
-
if (isOverlayEnabled() && shouldUpdateOverlay(args)) {
|
|
73
|
-
updateAutomationOverlay(args, 'before');
|
|
64
|
+
if (!options.capture) {
|
|
65
|
+
console.error(`[${PACKAGE_JSON.name}] 子进程被终止: ${result.signal}`);
|
|
66
|
+
}
|
|
67
|
+
return options.capture
|
|
68
|
+
? { code: 1, stdout: result.stdout || '', stderr: result.stderr || '' }
|
|
69
|
+
: 1;
|
|
74
70
|
}
|
|
75
|
-
const code =
|
|
76
|
-
if (
|
|
77
|
-
|
|
71
|
+
const code = result.status === null || result.status === undefined ? 1 : result.status;
|
|
72
|
+
if (options.capture) {
|
|
73
|
+
return { code, stdout: result.stdout || '', stderr: result.stderr || '' };
|
|
78
74
|
}
|
|
79
75
|
return code;
|
|
80
76
|
}
|
|
@@ -85,11 +81,21 @@ function parseHelixFlags(tokens, startIndex) {
|
|
|
85
81
|
cdpUrl: process.env.HELIX_CDP_URL || 'http://127.0.0.1:9222',
|
|
86
82
|
profile:
|
|
87
83
|
process.env.HELIX_PROFILE_DIR || path.join(process.cwd(), '.helixlife-profile'),
|
|
84
|
+
visual: { enabled: DEFAULT_VISUAL_ENABLED, durationMs: null },
|
|
88
85
|
};
|
|
89
86
|
for (let i = startIndex; i < tokens.length; i += 1) {
|
|
90
87
|
const t = tokens[i];
|
|
91
88
|
if (t === '--headless') {
|
|
92
89
|
options.headless = true;
|
|
90
|
+
} else if (t === '--visual') {
|
|
91
|
+
options.visual.enabled = true;
|
|
92
|
+
} else if (t === '--no-visual') {
|
|
93
|
+
options.visual.enabled = false;
|
|
94
|
+
} else if (t === '--visual-ms') {
|
|
95
|
+
options.visual.durationMs = tokens[i + 1];
|
|
96
|
+
i += 1;
|
|
97
|
+
} else if (t.startsWith('--visual-ms=')) {
|
|
98
|
+
options.visual.durationMs = t.slice('--visual-ms='.length);
|
|
93
99
|
} else if (t === '--profile') {
|
|
94
100
|
const next = tokens[i + 1];
|
|
95
101
|
if (next === undefined) {
|
|
@@ -113,6 +119,14 @@ function parseHelixFlags(tokens, startIndex) {
|
|
|
113
119
|
return null;
|
|
114
120
|
}
|
|
115
121
|
}
|
|
122
|
+
if (options.visual.durationMs != null) {
|
|
123
|
+
const n = Number(options.visual.durationMs);
|
|
124
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
125
|
+
console.error(`[${PACKAGE_JSON.name}] --visual-ms 需要正数,收到 ${JSON.stringify(options.visual.durationMs)}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
options.visual.durationMs = Math.round(n);
|
|
129
|
+
}
|
|
116
130
|
return options;
|
|
117
131
|
}
|
|
118
132
|
|
|
@@ -133,6 +147,11 @@ function printHelp() {
|
|
|
133
147
|
` ${n} helix doctor [--cdp-url <url>] [--profile <dir>] 先 list,再执行 helix home`
|
|
134
148
|
);
|
|
135
149
|
console.log('');
|
|
150
|
+
console.log('页面内视觉反馈(默认开启,作用于 click/fill/hover 等元素命令):');
|
|
151
|
+
console.log(` ${n} click e5 操作前黄框 + 标签,成功后绿框`);
|
|
152
|
+
console.log(` ${n} click e5 --no-visual 静默执行,无页面高亮`);
|
|
153
|
+
console.log(` ${n} fill e3 "文本" --visual-ms 700 自定义高亮时长(毫秒)`);
|
|
154
|
+
console.log('');
|
|
136
155
|
console.log('本地开发可使用:');
|
|
137
156
|
console.log(' node helix-cli.js <同上参数>');
|
|
138
157
|
console.log('');
|
|
@@ -140,12 +159,9 @@ function printHelp() {
|
|
|
140
159
|
'环境变量:HELIX_BASE_URL、HELIX_CDP_URL、HELIX_PROFILE_DIR(默认 profile 目录)、'
|
|
141
160
|
);
|
|
142
161
|
console.log(
|
|
143
|
-
` HELIX_OUTPUT_DIR / PLAYWRIGHT_MCP_OUTPUT_DIR(默认输出目录 <cwd>/${DEFAULT_OUTPUT_DIR_NAME}
|
|
144
|
-
);
|
|
145
|
-
console.log(
|
|
146
|
-
' HELIX_AUTOMATION_OVERLAY(设为 0/false/off 可关闭右上角机器操作浮层,默认开启)'
|
|
162
|
+
` HELIX_OUTPUT_DIR / PLAYWRIGHT_MCP_OUTPUT_DIR(默认输出目录 <cwd>/${DEFAULT_OUTPUT_DIR_NAME})、`
|
|
147
163
|
);
|
|
148
|
-
console.log('
|
|
164
|
+
console.log(' HELIX_VISUAL=0(全局关闭视觉反馈,可用 --visual 单次开启)');
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
function getBaseUrl() {
|
|
@@ -154,20 +170,19 @@ function getBaseUrl() {
|
|
|
154
170
|
|
|
155
171
|
function helixHome(options) {
|
|
156
172
|
const baseUrl = getBaseUrl();
|
|
157
|
-
|
|
158
|
-
const openArgs = ['open', baseUrl];
|
|
173
|
+
const openArgs = injectVisualConfig(['open', baseUrl], options.visual);
|
|
159
174
|
if (options.profile) {
|
|
160
175
|
openArgs.push(`--profile=${options.profile}`);
|
|
161
176
|
}
|
|
162
177
|
if (!options.headless) {
|
|
163
178
|
openArgs.push('--headed');
|
|
164
179
|
}
|
|
165
|
-
const code =
|
|
180
|
+
const code = runPlaywrightCliPassthrough(openArgs);
|
|
166
181
|
if (code !== 0) {
|
|
167
182
|
return code;
|
|
168
183
|
}
|
|
169
184
|
console.error(`[${PACKAGE_JSON.name}] opened: ${baseUrl}`);
|
|
170
|
-
return
|
|
185
|
+
return runPlaywrightCliPassthrough(['eval', 'document.title']);
|
|
171
186
|
}
|
|
172
187
|
|
|
173
188
|
function helixDoctor(options) {
|
|
@@ -184,6 +199,25 @@ function helixDoctor(options) {
|
|
|
184
199
|
return helixHome(options);
|
|
185
200
|
}
|
|
186
201
|
|
|
202
|
+
async function runPassthroughWithVisual(tokens) {
|
|
203
|
+
let visual;
|
|
204
|
+
let stripped;
|
|
205
|
+
try {
|
|
206
|
+
({ tokens: stripped, visual } = parseVisualFlags(tokens));
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.error(`[${PACKAGE_JSON.name}] ${e.message}`);
|
|
209
|
+
return 3;
|
|
210
|
+
}
|
|
211
|
+
const withConfig = injectVisualConfig(stripped, visual);
|
|
212
|
+
if (shouldWrapWithVisual(stripped, visual)) {
|
|
213
|
+
return runWithVisualFeedback(stripped, visual, (args, opts) => {
|
|
214
|
+
const result = runPlaywrightCliPassthrough(args, opts);
|
|
215
|
+
return opts && opts.capture ? result.code : result;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return runPlaywrightCliPassthrough(withConfig);
|
|
219
|
+
}
|
|
220
|
+
|
|
187
221
|
/**
|
|
188
222
|
* @param {string[]} argv process.argv
|
|
189
223
|
* @returns {Promise<number>} exit code
|
|
@@ -196,7 +230,6 @@ async function main(argv = process.argv) {
|
|
|
196
230
|
return 0;
|
|
197
231
|
}
|
|
198
232
|
|
|
199
|
-
// 勿透传到底层引擎:否则 --version 会变成 @playwright/cli 的版本(如 0.1.x),易与本体版本混淆。
|
|
200
233
|
if (tokens[0] === '--version' || tokens[0] === '-v') {
|
|
201
234
|
console.log(PACKAGE_JSON.version);
|
|
202
235
|
return 0;
|
|
@@ -230,19 +263,17 @@ async function main(argv = process.argv) {
|
|
|
230
263
|
return 2;
|
|
231
264
|
}
|
|
232
265
|
|
|
233
|
-
return
|
|
266
|
+
return runPassthroughWithVisual(tokens);
|
|
234
267
|
}
|
|
235
268
|
|
|
236
269
|
main.printHelp = printHelp;
|
|
237
270
|
main.runPlaywrightCliPassthrough = runPlaywrightCliPassthrough;
|
|
238
271
|
main.playwrightCliEntry = playwrightCliEntry;
|
|
239
272
|
|
|
240
|
-
// 注意:`require.main === module` 入口守卫统一放在 helix-cli.js;
|
|
241
|
-
// 本文件仅导出,不在此处重复启动。
|
|
242
|
-
|
|
243
273
|
module.exports = {
|
|
244
274
|
main,
|
|
245
275
|
resolveOutputDir,
|
|
246
276
|
buildChildEnv,
|
|
247
277
|
DEFAULT_OUTPUT_DIR_NAME,
|
|
278
|
+
runPassthroughWithVisual,
|
|
248
279
|
};
|
package/lib/visual.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const INIT_SCRIPT_PATH = path.join(__dirname, 'automation-overlay.init.js');
|
|
7
|
+
const GENERATED_CONFIG_PATH = path.join(__dirname, '.cli.runtime.generated.json');
|
|
8
|
+
|
|
9
|
+
/** 默认开启页面内视觉反馈;HELIX_VISUAL=0 可全局关闭 */
|
|
10
|
+
const DEFAULT_VISUAL_ENABLED = process.env.HELIX_VISUAL !== '0';
|
|
11
|
+
|
|
12
|
+
const VISUAL_COMMANDS = {
|
|
13
|
+
click: { pending: '正在点击', success: '已点击', pendingTone: 'pending', successTone: 'success' },
|
|
14
|
+
dblclick: { pending: '正在双击', success: '已双击', pendingTone: 'pending', successTone: 'success' },
|
|
15
|
+
fill: { pending: '正在填写', success: '已填写', pendingTone: 'pending', successTone: 'success' },
|
|
16
|
+
hover: { pending: '正在悬停', success: '已悬停', pendingTone: 'info', successTone: 'info' },
|
|
17
|
+
check: { pending: '正在勾选', success: '已勾选', pendingTone: 'pending', successTone: 'success' },
|
|
18
|
+
uncheck: { pending: '正在取消勾选', success: '已取消勾选', pendingTone: 'pending', successTone: 'info' },
|
|
19
|
+
select: { pending: '正在选择', success: '已选择', pendingTone: 'pending', successTone: 'success' },
|
|
20
|
+
drag: { pending: '正在拖拽', success: '已拖拽', pendingTone: 'pending', successTone: 'success' },
|
|
21
|
+
drop: { pending: '正在放置', success: '已放置', pendingTone: 'pending', successTone: 'success' },
|
|
22
|
+
upload: { hudOnly: true, pending: '正在上传', success: '已上传', pendingTone: 'pending', successTone: 'success' },
|
|
23
|
+
press: { hudOnly: true, pending: '按键', success: '按键完成', pendingTone: 'info', successTone: 'success' },
|
|
24
|
+
type: { hudOnly: true, pending: '正在输入', success: '输入完成', pendingTone: 'pending', successTone: 'success' },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const SESSION_START_COMMANDS = new Set(['open', 'attach']);
|
|
28
|
+
|
|
29
|
+
function readInitScriptSource() {
|
|
30
|
+
return fs.readFileSync(INIT_SCRIPT_PATH, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveGeneratedConfigPath() {
|
|
34
|
+
const payload = {
|
|
35
|
+
browser: {
|
|
36
|
+
initScript: [INIT_SCRIPT_PATH.replace(/\\/g, '/')],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
fs.writeFileSync(GENERATED_CONFIG_PATH, JSON.stringify(payload, null, 2), 'utf8');
|
|
40
|
+
return GENERATED_CONFIG_PATH;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 从 argv 中提取并剥离 --visual / --no-visual / --visual-ms。
|
|
45
|
+
* @returns {{ tokens: string[], visual: { enabled: boolean, durationMs: number | null } }}
|
|
46
|
+
*/
|
|
47
|
+
function parseVisualFlags(tokens) {
|
|
48
|
+
const visual = {
|
|
49
|
+
enabled: DEFAULT_VISUAL_ENABLED,
|
|
50
|
+
durationMs: null,
|
|
51
|
+
};
|
|
52
|
+
const out = [];
|
|
53
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
54
|
+
const t = tokens[i];
|
|
55
|
+
if (t === '--visual') {
|
|
56
|
+
visual.enabled = true;
|
|
57
|
+
} else if (t === '--no-visual') {
|
|
58
|
+
visual.enabled = false;
|
|
59
|
+
} else if (t === '--visual-ms') {
|
|
60
|
+
visual.durationMs = tokens[i + 1];
|
|
61
|
+
i += 1;
|
|
62
|
+
} else if (t.startsWith('--visual-ms=')) {
|
|
63
|
+
visual.durationMs = t.slice('--visual-ms='.length);
|
|
64
|
+
} else {
|
|
65
|
+
out.push(t);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (visual.durationMs != null) {
|
|
69
|
+
const n = Number(visual.durationMs);
|
|
70
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
71
|
+
throw Object.assign(new Error(`--visual-ms 需要正数,收到 ${JSON.stringify(visual.durationMs)}`), { code: 'E_BAD_ARG' });
|
|
72
|
+
}
|
|
73
|
+
visual.durationMs = Math.round(n);
|
|
74
|
+
}
|
|
75
|
+
return { tokens: out, visual };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hasConfigFlag(tokens) {
|
|
79
|
+
return tokens.some((t) => t === '--config' || t.startsWith('--config='));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* open / attach 时自动注入 initScript 配置,使新页面预装视觉层。
|
|
84
|
+
*/
|
|
85
|
+
function injectVisualConfig(tokens, visual) {
|
|
86
|
+
if (!visual.enabled) return tokens;
|
|
87
|
+
const cmd = resolveCommandName(tokens);
|
|
88
|
+
if (!SESSION_START_COMMANDS.has(cmd) || hasConfigFlag(tokens)) {
|
|
89
|
+
return tokens;
|
|
90
|
+
}
|
|
91
|
+
const configPath = resolveGeneratedConfigPath();
|
|
92
|
+
return [...tokens, `--config=${configPath}`];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveCommandName(tokens) {
|
|
96
|
+
let i = 0;
|
|
97
|
+
if (tokens[i] === '--raw') i += 1;
|
|
98
|
+
if (tokens[i] === '--json') i += 1;
|
|
99
|
+
if (tokens[i] && tokens[i].startsWith('-s=')) i += 1;
|
|
100
|
+
return tokens[i] || '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isElementRef(token) {
|
|
104
|
+
if (!token || token.startsWith('-')) return false;
|
|
105
|
+
if (/^e\d+$/.test(token)) return true;
|
|
106
|
+
if (token.startsWith('"') || token.startsWith("'")) return true;
|
|
107
|
+
if (token.startsWith('#') || token.startsWith('.')) return true;
|
|
108
|
+
if (token.startsWith('getBy')) return true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 从命令参数中提取主要元素 target(用于高亮)。
|
|
114
|
+
*/
|
|
115
|
+
function extractElementTarget(tokens) {
|
|
116
|
+
const cmd = resolveCommandName(tokens);
|
|
117
|
+
if (!VISUAL_COMMANDS[cmd]) return null;
|
|
118
|
+
const def = VISUAL_COMMANDS[cmd];
|
|
119
|
+
if (def.hudOnly) return null;
|
|
120
|
+
|
|
121
|
+
let i = 0;
|
|
122
|
+
if (tokens[i] === '--raw') i += 1;
|
|
123
|
+
if (tokens[i] === '--json') i += 1;
|
|
124
|
+
if (tokens[i] && tokens[i].startsWith('-s=')) i += 1;
|
|
125
|
+
i += 1; // skip command name
|
|
126
|
+
|
|
127
|
+
const positional = [];
|
|
128
|
+
for (; i < tokens.length; i += 1) {
|
|
129
|
+
const t = tokens[i];
|
|
130
|
+
if (t.startsWith('--')) {
|
|
131
|
+
if (t.includes('=')) continue;
|
|
132
|
+
const next = tokens[i + 1];
|
|
133
|
+
if (next !== undefined && !next.startsWith('-')) i += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
positional.push(t);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (cmd === 'drag' && positional.length >= 1) return positional[0];
|
|
140
|
+
if (cmd === 'fill' && positional.length >= 1) return positional[0];
|
|
141
|
+
if (cmd === 'select' && positional.length >= 1) return positional[0];
|
|
142
|
+
if (cmd === 'drop' && positional.length >= 1) return positional[0];
|
|
143
|
+
if (positional.length >= 1 && isElementRef(positional[0])) return positional[0];
|
|
144
|
+
if (positional.length >= 2 && isElementRef(positional[positional.length - 1])) {
|
|
145
|
+
return positional[positional.length - 1];
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildFlashEval(target, options) {
|
|
151
|
+
const optsLit = JSON.stringify(options);
|
|
152
|
+
return [
|
|
153
|
+
'eval',
|
|
154
|
+
`el => { const v = window.__helix_visual__; if (!v) return false; return v.flash(el, ${optsLit}); }`,
|
|
155
|
+
target,
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildHudEval(options) {
|
|
160
|
+
const optsLit = JSON.stringify(options);
|
|
161
|
+
return ['eval', `() => { const v = window.__helix_visual__; if (!v) return false; return v.showHud(${optsLit}); }`];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildSetOptionsEval(visual) {
|
|
165
|
+
const payload = JSON.stringify({
|
|
166
|
+
enabled: visual.enabled,
|
|
167
|
+
durationMs: visual.durationMs,
|
|
168
|
+
});
|
|
169
|
+
return ['eval', `() => { const v = window.__helix_visual__; if (!v) return null; return v.setVisualOptions(${payload}); }`];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildInstallEval() {
|
|
173
|
+
const src = readInitScriptSource();
|
|
174
|
+
return ['eval', src];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function buildBridgeCheckEval() {
|
|
178
|
+
return ['eval', '--raw', 'Boolean(window.__helix_visual__)'];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function shouldWrapWithVisual(tokens, visual) {
|
|
182
|
+
if (!visual.enabled) return false;
|
|
183
|
+
const cmd = resolveCommandName(tokens);
|
|
184
|
+
return Boolean(VISUAL_COMMANDS[cmd]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 在元素交互命令前后注入视觉反馈。
|
|
189
|
+
* @param {string[]} tokens 已剥离 visual flags 的参数
|
|
190
|
+
* @param {{ enabled: boolean, durationMs: number | null }} visual
|
|
191
|
+
* @param {(args: string[], opts?: object) => number} runPassthrough
|
|
192
|
+
*/
|
|
193
|
+
async function runWithVisualFeedback(tokens, visual, runPassthrough) {
|
|
194
|
+
const cmd = resolveCommandName(tokens);
|
|
195
|
+
const def = VISUAL_COMMANDS[cmd];
|
|
196
|
+
const target = extractElementTarget(tokens);
|
|
197
|
+
|
|
198
|
+
const runCapture = (args) => runPassthrough(args, { capture: true });
|
|
199
|
+
|
|
200
|
+
const check = runCapture(buildBridgeCheckEval());
|
|
201
|
+
if (String(check.stdout || '').trim() !== 'true') {
|
|
202
|
+
runCapture(buildInstallEval());
|
|
203
|
+
}
|
|
204
|
+
runCapture(buildSetOptionsEval(visual));
|
|
205
|
+
|
|
206
|
+
const hudBase = { action: cmd, target: target || '', status: 'pending' };
|
|
207
|
+
runCapture(buildHudEval({ ...hudBase, detail: def.pending }));
|
|
208
|
+
|
|
209
|
+
if (target) {
|
|
210
|
+
runCapture(buildFlashEval(target, {
|
|
211
|
+
tone: def.pendingTone || 'pending',
|
|
212
|
+
label: def.pending,
|
|
213
|
+
durationMs: visual.durationMs,
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const code = runPassthrough(tokens);
|
|
218
|
+
|
|
219
|
+
const successTone = code === 0 ? (def.successTone || 'success') : 'danger';
|
|
220
|
+
const successLabel = code === 0 ? def.success : '操作失败';
|
|
221
|
+
runCapture(buildSetOptionsEval(visual));
|
|
222
|
+
if (target) {
|
|
223
|
+
runCapture(buildFlashEval(target, {
|
|
224
|
+
tone: successTone,
|
|
225
|
+
label: successLabel,
|
|
226
|
+
durationMs: visual.durationMs,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
runCapture(buildHudEval({
|
|
230
|
+
action: cmd,
|
|
231
|
+
target: target || '',
|
|
232
|
+
status: code === 0 ? 'success' : 'danger',
|
|
233
|
+
detail: successLabel,
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
return code;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
INIT_SCRIPT_PATH,
|
|
241
|
+
VISUAL_COMMANDS,
|
|
242
|
+
DEFAULT_VISUAL_ENABLED,
|
|
243
|
+
parseVisualFlags,
|
|
244
|
+
injectVisualConfig,
|
|
245
|
+
shouldWrapWithVisual,
|
|
246
|
+
runWithVisualFeedback,
|
|
247
|
+
resolveGeneratedConfigPath,
|
|
248
|
+
};
|
package/package.json
CHANGED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
const ROOT_ID = '__helix_cli_automation_overlay';
|
|
3
|
-
|
|
4
|
-
function ensureOverlay() {
|
|
5
|
-
if (document.getElementById(ROOT_ID)) {
|
|
6
|
-
return window.__helixCliOverlay;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const root = document.createElement('div');
|
|
10
|
-
root.id = ROOT_ID;
|
|
11
|
-
root.setAttribute('data-helix-cli', 'automation-overlay');
|
|
12
|
-
root.setAttribute('aria-hidden', 'true');
|
|
13
|
-
root.style.cssText = [
|
|
14
|
-
'position:fixed',
|
|
15
|
-
'top:16px',
|
|
16
|
-
'right:16px',
|
|
17
|
-
'z-index:2147483647',
|
|
18
|
-
'pointer-events:none',
|
|
19
|
-
'box-sizing:border-box',
|
|
20
|
-
'max-width:280px',
|
|
21
|
-
'padding:10px 14px',
|
|
22
|
-
'border-radius:12px',
|
|
23
|
-
'background:#f5b942',
|
|
24
|
-
'color:#3d2b00',
|
|
25
|
-
'font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif',
|
|
26
|
-
'box-shadow:0 4px 14px rgba(0,0,0,.18)',
|
|
27
|
-
'letter-spacing:.01em',
|
|
28
|
-
].join(';');
|
|
29
|
-
|
|
30
|
-
const title = document.createElement('div');
|
|
31
|
-
title.dataset.line = 'title';
|
|
32
|
-
title.style.cssText = 'font-weight:600;margin-bottom:4px;word-break:break-all;';
|
|
33
|
-
|
|
34
|
-
const status = document.createElement('div');
|
|
35
|
-
status.dataset.line = 'status';
|
|
36
|
-
status.style.cssText = 'opacity:.92;';
|
|
37
|
-
|
|
38
|
-
const detail = document.createElement('div');
|
|
39
|
-
detail.dataset.line = 'detail';
|
|
40
|
-
detail.style.cssText = 'opacity:.78;margin-top:2px;font-size:12px;';
|
|
41
|
-
|
|
42
|
-
root.append(title, status, detail);
|
|
43
|
-
(document.body || document.documentElement).appendChild(root);
|
|
44
|
-
|
|
45
|
-
const api = {
|
|
46
|
-
update(payload = {}) {
|
|
47
|
-
const el = document.getElementById(ROOT_ID);
|
|
48
|
-
if (!el) return;
|
|
49
|
-
const t = el.querySelector('[data-line="title"]');
|
|
50
|
-
const s = el.querySelector('[data-line="status"]');
|
|
51
|
-
const d = el.querySelector('[data-line="detail"]');
|
|
52
|
-
if (t) t.textContent = payload.title || 'helixlife-v5-cli';
|
|
53
|
-
if (s) s.textContent = payload.status || '机器自动化';
|
|
54
|
-
if (d) d.textContent = payload.detail || 'CLI 操作中';
|
|
55
|
-
el.style.display = payload.hidden ? 'none' : 'block';
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
window.__helixCliOverlay = api;
|
|
60
|
-
return api;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function boot() {
|
|
64
|
-
ensureOverlay().update({
|
|
65
|
-
title: 'helixlife-v5-cli',
|
|
66
|
-
status: '机器自动化',
|
|
67
|
-
detail: '会话已就绪',
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (document.readyState === 'loading') {
|
|
72
|
-
document.addEventListener('DOMContentLoaded', boot, { once: true });
|
|
73
|
-
} else {
|
|
74
|
-
boot();
|
|
75
|
-
}
|
|
76
|
-
})();
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const path = require("node:path");
|
|
5
|
-
|
|
6
|
-
const OVERLAY_SKIP_COMMANDS = new Set([
|
|
7
|
-
"run-code",
|
|
8
|
-
"list",
|
|
9
|
-
"close",
|
|
10
|
-
"close-all",
|
|
11
|
-
"kill-all",
|
|
12
|
-
"detach",
|
|
13
|
-
"delete-data",
|
|
14
|
-
"config-print",
|
|
15
|
-
"cookie-list",
|
|
16
|
-
"cookie-get",
|
|
17
|
-
"cookie-set",
|
|
18
|
-
"cookie-delete",
|
|
19
|
-
"cookie-clear",
|
|
20
|
-
"localstorage-list",
|
|
21
|
-
"localstorage-get",
|
|
22
|
-
"localstorage-set",
|
|
23
|
-
"localstorage-delete",
|
|
24
|
-
"localstorage-clear",
|
|
25
|
-
"sessionstorage-list",
|
|
26
|
-
"sessionstorage-get",
|
|
27
|
-
"sessionstorage-set",
|
|
28
|
-
"sessionstorage-delete",
|
|
29
|
-
"sessionstorage-clear",
|
|
30
|
-
"route-list",
|
|
31
|
-
"tab-list",
|
|
32
|
-
"console",
|
|
33
|
-
"network",
|
|
34
|
-
"state-save",
|
|
35
|
-
"state-load",
|
|
36
|
-
"tracing-start",
|
|
37
|
-
"tracing-stop",
|
|
38
|
-
"video-start",
|
|
39
|
-
"video-stop",
|
|
40
|
-
"video-chapter",
|
|
41
|
-
"highlight",
|
|
42
|
-
"generate-locator",
|
|
43
|
-
"show",
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
const OVERLAY_SESSION_OPEN_COMMANDS = new Set(["open", "attach"]);
|
|
47
|
-
|
|
48
|
-
let cachedBrowserScript = null;
|
|
49
|
-
|
|
50
|
-
function isOverlayEnabled() {
|
|
51
|
-
const flag = process.env.HELIX_AUTOMATION_OVERLAY;
|
|
52
|
-
if (flag === "0" || flag === "false" || flag === "off") {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function resolveCliConfigPath() {
|
|
59
|
-
if (process.env.HELIX_CLI_CONFIG) {
|
|
60
|
-
return path.resolve(process.env.HELIX_CLI_CONFIG);
|
|
61
|
-
}
|
|
62
|
-
const initScriptPath = path.join(__dirname, "automation-overlay.init.js");
|
|
63
|
-
const runtimeConfigPath = path.join(__dirname, "cli.runtime.config.json");
|
|
64
|
-
const config = {
|
|
65
|
-
browser: {
|
|
66
|
-
initScript: [initScriptPath],
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
fs.writeFileSync(
|
|
70
|
-
runtimeConfigPath,
|
|
71
|
-
`${JSON.stringify(config, null, 2)}\n`,
|
|
72
|
-
"utf8",
|
|
73
|
-
);
|
|
74
|
-
return runtimeConfigPath;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function getBrowserOverlayScript() {
|
|
78
|
-
if (!cachedBrowserScript) {
|
|
79
|
-
cachedBrowserScript = fs.readFileSync(
|
|
80
|
-
path.join(__dirname, "automation-overlay.browser.js"),
|
|
81
|
-
"utf8",
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
return cachedBrowserScript;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function stripLeadingFlags(tokens) {
|
|
88
|
-
let i = 0;
|
|
89
|
-
while (i < tokens.length) {
|
|
90
|
-
const t = tokens[i];
|
|
91
|
-
if (t === "--raw" || t === "--json") {
|
|
92
|
-
i += 1;
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (t.startsWith("-s=")) {
|
|
96
|
-
i += 1;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
return tokens.slice(i);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function truncate(text, max = 48) {
|
|
105
|
-
const s = String(text || "")
|
|
106
|
-
.replace(/\s+/g, " ")
|
|
107
|
-
.trim();
|
|
108
|
-
if (s.length <= max) return s;
|
|
109
|
-
return `${s.slice(0, max - 1)}…`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function describeOverlayPayload(tokens, phase) {
|
|
113
|
-
const core = stripLeadingFlags(tokens);
|
|
114
|
-
const command = core[0];
|
|
115
|
-
if (!command) {
|
|
116
|
-
return {
|
|
117
|
-
title: "helixlife-v5-cli",
|
|
118
|
-
status: phase === "after" ? "已完成" : "执行中...",
|
|
119
|
-
detail: "CLI自动化操作中",
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const args = core.slice(1).filter((t) => !t.startsWith("--"));
|
|
124
|
-
let title = command;
|
|
125
|
-
let detail = args.length ? truncate(args.join(" ")) : "CLI自动化操作中";
|
|
126
|
-
|
|
127
|
-
switch (command) {
|
|
128
|
-
case "click":
|
|
129
|
-
case "dblclick":
|
|
130
|
-
case "hover":
|
|
131
|
-
case "check":
|
|
132
|
-
case "uncheck":
|
|
133
|
-
title = command;
|
|
134
|
-
detail = truncate(args[0] || "元素", 40);
|
|
135
|
-
break;
|
|
136
|
-
case "fill":
|
|
137
|
-
title = "fill";
|
|
138
|
-
detail = truncate(args[0] || "输入框", 40);
|
|
139
|
-
break;
|
|
140
|
-
case "type":
|
|
141
|
-
title = "type";
|
|
142
|
-
detail = truncate(args[0] || "", 32);
|
|
143
|
-
break;
|
|
144
|
-
case "goto":
|
|
145
|
-
case "open":
|
|
146
|
-
title = command;
|
|
147
|
-
detail = truncate(args[0] || "页面", 56);
|
|
148
|
-
break;
|
|
149
|
-
case "snapshot":
|
|
150
|
-
title = "snapshot";
|
|
151
|
-
detail = "页面验收";
|
|
152
|
-
break;
|
|
153
|
-
case "run-code":
|
|
154
|
-
title = "run-code";
|
|
155
|
-
detail = truncate(args[0] || "脚本", 40);
|
|
156
|
-
break;
|
|
157
|
-
case "press":
|
|
158
|
-
title = "press";
|
|
159
|
-
detail = truncate(args[0] || "按键", 24);
|
|
160
|
-
break;
|
|
161
|
-
case "select":
|
|
162
|
-
title = "select";
|
|
163
|
-
detail = truncate(args.join(" "), 40);
|
|
164
|
-
break;
|
|
165
|
-
case "upload":
|
|
166
|
-
title = "upload";
|
|
167
|
-
detail = truncate(args[0] || "文件", 40);
|
|
168
|
-
break;
|
|
169
|
-
case "eval":
|
|
170
|
-
title = "eval";
|
|
171
|
-
detail = "读取页面状态";
|
|
172
|
-
break;
|
|
173
|
-
default:
|
|
174
|
-
title = command;
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
title,
|
|
180
|
-
status: phase === "after" ? "已完成" : "执行中...",
|
|
181
|
-
detail: phase === "after" ? "CLI自动化操作中" : detail || "CLI自动化操作中",
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function shouldUpdateOverlay(tokens) {
|
|
186
|
-
const core = stripLeadingFlags(tokens);
|
|
187
|
-
const command = core[0];
|
|
188
|
-
if (!command) return false;
|
|
189
|
-
if (OVERLAY_SKIP_COMMANDS.has(command)) return false;
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function shouldInjectOpenConfig(tokens) {
|
|
194
|
-
const core = stripLeadingFlags(tokens);
|
|
195
|
-
const command = core[0];
|
|
196
|
-
if (!command || !OVERLAY_SESSION_OPEN_COMMANDS.has(command)) return false;
|
|
197
|
-
return !tokens.some((t) => t === "--config" || t.startsWith("--config="));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function injectOpenConfig(tokens) {
|
|
201
|
-
if (!isOverlayEnabled() || !shouldInjectOpenConfig(tokens)) {
|
|
202
|
-
return tokens;
|
|
203
|
-
}
|
|
204
|
-
const configPath = resolveCliConfigPath();
|
|
205
|
-
const next = [...tokens];
|
|
206
|
-
const core = stripLeadingFlags(tokens);
|
|
207
|
-
const insertAt = tokens.indexOf(core[0]);
|
|
208
|
-
next.splice(insertAt + 1, 0, `--config=${configPath}`);
|
|
209
|
-
return next;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function buildOverlayRunCode(payload) {
|
|
213
|
-
const browserScript = getBrowserOverlayScript();
|
|
214
|
-
const payloadJson = JSON.stringify(payload);
|
|
215
|
-
return [
|
|
216
|
-
"async page => {",
|
|
217
|
-
` const browserScript = ${JSON.stringify(browserScript)};`,
|
|
218
|
-
` const payload = ${payloadJson};`,
|
|
219
|
-
" await page.evaluate(({ script, data }) => {",
|
|
220
|
-
" // eslint-disable-next-line no-eval",
|
|
221
|
-
" eval(script);",
|
|
222
|
-
" if (window.__helixCliOverlay) window.__helixCliOverlay.update(data);",
|
|
223
|
-
" }, { script: browserScript, data: payload });",
|
|
224
|
-
"}",
|
|
225
|
-
].join("\n");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
module.exports = {
|
|
229
|
-
OVERLAY_SKIP_COMMANDS,
|
|
230
|
-
isOverlayEnabled,
|
|
231
|
-
resolveCliConfigPath,
|
|
232
|
-
stripLeadingFlags,
|
|
233
|
-
describeOverlayPayload,
|
|
234
|
-
shouldUpdateOverlay,
|
|
235
|
-
shouldInjectOpenConfig,
|
|
236
|
-
injectOpenConfig,
|
|
237
|
-
buildOverlayRunCode,
|
|
238
|
-
};
|