helixlife-v5-cli 1.1.8 → 1.2.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.
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
})();
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Playwright browser init script: inject automation status overlay on every document.
|
|
2
|
+
// Source is duplicated from automation-overlay.browser.js (keep in sync when editing).
|
|
3
|
+
(() => {
|
|
4
|
+
const ROOT_ID = '__helix_cli_automation_overlay';
|
|
5
|
+
|
|
6
|
+
function ensureOverlay() {
|
|
7
|
+
if (document.getElementById(ROOT_ID)) {
|
|
8
|
+
return window.__helixCliOverlay;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const root = document.createElement('div');
|
|
12
|
+
root.id = ROOT_ID;
|
|
13
|
+
root.setAttribute('data-helix-cli', 'automation-overlay');
|
|
14
|
+
root.setAttribute('aria-hidden', 'true');
|
|
15
|
+
root.style.cssText = [
|
|
16
|
+
'position:fixed',
|
|
17
|
+
'top:16px',
|
|
18
|
+
'right:16px',
|
|
19
|
+
'z-index:2147483647',
|
|
20
|
+
'pointer-events:none',
|
|
21
|
+
'box-sizing:border-box',
|
|
22
|
+
'max-width:280px',
|
|
23
|
+
'padding:10px 14px',
|
|
24
|
+
'border-radius:12px',
|
|
25
|
+
'background:#f5b942',
|
|
26
|
+
'color:#3d2b00',
|
|
27
|
+
'font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif',
|
|
28
|
+
'box-shadow:0 4px 14px rgba(0,0,0,.18)',
|
|
29
|
+
'letter-spacing:.01em',
|
|
30
|
+
].join(';');
|
|
31
|
+
|
|
32
|
+
const title = document.createElement('div');
|
|
33
|
+
title.dataset.line = 'title';
|
|
34
|
+
title.style.cssText = 'font-weight:600;margin-bottom:4px;word-break:break-all;';
|
|
35
|
+
|
|
36
|
+
const status = document.createElement('div');
|
|
37
|
+
status.dataset.line = 'status';
|
|
38
|
+
status.style.cssText = 'opacity:.92;';
|
|
39
|
+
|
|
40
|
+
const detail = document.createElement('div');
|
|
41
|
+
detail.dataset.line = 'detail';
|
|
42
|
+
detail.style.cssText = 'opacity:.78;margin-top:2px;font-size:12px;';
|
|
43
|
+
|
|
44
|
+
root.append(title, status, detail);
|
|
45
|
+
(document.body || document.documentElement).appendChild(root);
|
|
46
|
+
|
|
47
|
+
const api = {
|
|
48
|
+
update(payload = {}) {
|
|
49
|
+
const el = document.getElementById(ROOT_ID);
|
|
50
|
+
if (!el) return;
|
|
51
|
+
const t = el.querySelector('[data-line="title"]');
|
|
52
|
+
const s = el.querySelector('[data-line="status"]');
|
|
53
|
+
const d = el.querySelector('[data-line="detail"]');
|
|
54
|
+
if (t) t.textContent = payload.title || 'helixlife-v5-cli';
|
|
55
|
+
if (s) s.textContent = payload.status || '机器自动化';
|
|
56
|
+
if (d) d.textContent = payload.detail || 'CLI 操作中';
|
|
57
|
+
el.style.display = payload.hidden ? 'none' : 'block';
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
window.__helixCliOverlay = api;
|
|
62
|
+
return api;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function boot() {
|
|
66
|
+
ensureOverlay().update({
|
|
67
|
+
title: 'helixlife-v5-cli',
|
|
68
|
+
status: '机器自动化',
|
|
69
|
+
detail: '会话已就绪',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (document.readyState === 'loading') {
|
|
74
|
+
document.addEventListener('DOMContentLoaded', boot, { once: true });
|
|
75
|
+
} else {
|
|
76
|
+
boot();
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
};
|
package/lib/cli.js
CHANGED
|
@@ -4,6 +4,13 @@ const path = require('node:path');
|
|
|
4
4
|
const { spawnSync } = require('node:child_process');
|
|
5
5
|
|
|
6
6
|
const PACKAGE_JSON = require(path.join(__dirname, '..', 'package.json'));
|
|
7
|
+
const {
|
|
8
|
+
isOverlayEnabled,
|
|
9
|
+
injectOpenConfig,
|
|
10
|
+
shouldUpdateOverlay,
|
|
11
|
+
describeOverlayPayload,
|
|
12
|
+
buildOverlayRunCode,
|
|
13
|
+
} = require('./automation-overlay');
|
|
7
14
|
|
|
8
15
|
/** 默认快照/下载等工作区目录名(相对 cwd),替代引擎默认的 .playwright-cli */
|
|
9
16
|
const DEFAULT_OUTPUT_DIR_NAME = 'helixlife-v5-cli';
|
|
@@ -35,9 +42,9 @@ function buildChildEnv() {
|
|
|
35
42
|
* 将参数原样交给底层浏览器自动化引擎,返回子进程退出码。
|
|
36
43
|
* 使用 node 直接执行入口脚本,避免依赖 npx、Shell 与 PATH,在 Windows / macOS 上一致。
|
|
37
44
|
*/
|
|
38
|
-
function runPlaywrightCliPassthrough(args) {
|
|
45
|
+
function runPlaywrightCliPassthrough(args, options = {}) {
|
|
39
46
|
const result = spawnSync(process.execPath, [playwrightCliEntry(), ...args], {
|
|
40
|
-
stdio: 'inherit',
|
|
47
|
+
stdio: options.quiet ? ['ignore', 'ignore', 'ignore'] : 'inherit',
|
|
41
48
|
env: buildChildEnv(),
|
|
42
49
|
windowsHide: true,
|
|
43
50
|
});
|
|
@@ -51,6 +58,27 @@ function runPlaywrightCliPassthrough(args) {
|
|
|
51
58
|
return result.status === null || result.status === undefined ? 1 : result.status;
|
|
52
59
|
}
|
|
53
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');
|
|
74
|
+
}
|
|
75
|
+
const code = runPlaywrightCliPassthrough(args);
|
|
76
|
+
if (code === 0 && isOverlayEnabled() && shouldUpdateOverlay(args)) {
|
|
77
|
+
updateAutomationOverlay(args, 'after');
|
|
78
|
+
}
|
|
79
|
+
return code;
|
|
80
|
+
}
|
|
81
|
+
|
|
54
82
|
function parseHelixFlags(tokens, startIndex) {
|
|
55
83
|
const options = {
|
|
56
84
|
headless: false,
|
|
@@ -114,6 +142,10 @@ function printHelp() {
|
|
|
114
142
|
console.log(
|
|
115
143
|
` HELIX_OUTPUT_DIR / PLAYWRIGHT_MCP_OUTPUT_DIR(默认输出目录 <cwd>/${DEFAULT_OUTPUT_DIR_NAME})`
|
|
116
144
|
);
|
|
145
|
+
console.log(
|
|
146
|
+
' HELIX_AUTOMATION_OVERLAY(设为 0/false/off 可关闭右上角机器操作浮层,默认开启)'
|
|
147
|
+
);
|
|
148
|
+
console.log(' HELIX_CLI_CONFIG(自定义 Playwright CLI 配置文件路径)');
|
|
117
149
|
}
|
|
118
150
|
|
|
119
151
|
function getBaseUrl() {
|
|
@@ -130,12 +162,12 @@ function helixHome(options) {
|
|
|
130
162
|
if (!options.headless) {
|
|
131
163
|
openArgs.push('--headed');
|
|
132
164
|
}
|
|
133
|
-
const code =
|
|
165
|
+
const code = runPlaywrightCliWithOverlay(openArgs);
|
|
134
166
|
if (code !== 0) {
|
|
135
167
|
return code;
|
|
136
168
|
}
|
|
137
169
|
console.error(`[${PACKAGE_JSON.name}] opened: ${baseUrl}`);
|
|
138
|
-
return
|
|
170
|
+
return runPlaywrightCliWithOverlay(['eval', 'document.title']);
|
|
139
171
|
}
|
|
140
172
|
|
|
141
173
|
function helixDoctor(options) {
|
|
@@ -198,7 +230,7 @@ async function main(argv = process.argv) {
|
|
|
198
230
|
return 2;
|
|
199
231
|
}
|
|
200
232
|
|
|
201
|
-
return
|
|
233
|
+
return runPlaywrightCliWithOverlay(tokens);
|
|
202
234
|
}
|
|
203
235
|
|
|
204
236
|
main.printHelp = printHelp;
|
package/package.json
CHANGED
|
@@ -155,6 +155,11 @@
|
|
|
155
155
|
| 对话 · 新建会话 | S-09 |
|
|
156
156
|
| 工作站 · 新建任务弹窗 | S-10 |
|
|
157
157
|
| 工作台 · 进入子工作台 | S-11 |
|
|
158
|
+
| 工作台 · 综述全流程(拟定主题→精修) | WB-1 |
|
|
159
|
+
| 工作台 · SCI 论著全流程 | WB-2 |
|
|
160
|
+
| 工作台 · 国自然基金全流程 | WB-3 |
|
|
161
|
+
| 工作台 · 临床申请书全流程 | WB-4 |
|
|
162
|
+
| 工作台 · Meta(未上线) | WB-0 |
|
|
158
163
|
| 应用 · 标准操作(搜索 / 级联分类 / 滚动加载 / 收藏 / 进入详情 / 多块复制 / mindmap) | S-12(细则 AP-1~AP-13 + 规约 A~H) |
|
|
159
164
|
| 应用 · 取消收藏 · 二次确认弹窗 | AP-8(规约 E) |
|
|
160
165
|
| 应用 · 多块输出 · 单块复制 vs 全部复制 | AP-10 / AP-11(规约 F) |
|
|
@@ -338,7 +343,154 @@ div.side-menu--item[to='/edu/courses']
|
|
|
338
343
|
|
|
339
344
|
#### AI 工作台 `/workbench`
|
|
340
345
|
|
|
341
|
-
|
|
346
|
+
> **核验日期**:2026-06-02(有头会话 `helixlife-v5-cli`,已逐卡进入并完成各工具主流程点击验收)。下文 **WB-xx** 为可复用步骤编号;**禁止**依赖快照 `e*` 作为长期定位器。
|
|
347
|
+
|
|
348
|
+
##### 门户与子工具索引(WB-0)
|
|
349
|
+
|
|
350
|
+
| 首页卡片文案(`getByText(..., { exact: true })`) | 路由 slug | 首屏 `pathname` | 面包屑第二级文案 | 状态 |
|
|
351
|
+
| --- | --- | --- | --- | --- |
|
|
352
|
+
| 综述工作台 | `jova` | `/workbench/jova/topic` | 综述工作台 | 可用 |
|
|
353
|
+
| SCI论著工作台 | `sci` | `/workbench/sci/topic` | SCI论著工作台 | 可用 |
|
|
354
|
+
| 基金工作台 | `nsfc-v2` | `/workbench/nsfc-v2/topic` | 基金工作台 | 可用 |
|
|
355
|
+
| 临床工作台 | `clinical` | `/workbench/clinical/topic` | 临床申请书工作台 | 可用 |
|
|
356
|
+
| Meta工作台 | — | (点击后仍停留 `/workbench`) | — | **未上线**;副文案「Meta工作台正在开发中,敬请期待...」,**勿**当作可进入子路由 |
|
|
357
|
+
|
|
358
|
+
**进入门户**:S-07 → `div.side-menu--item[to='/workbench']`,或已在站内时面包屑 `getByRole('navigation', { name: '面包屑导航' }).getByText('AI工作台', { exact: true })`。
|
|
359
|
+
|
|
360
|
+
**进入某一子工具(S-11)**:在 `/workbench` 主内容区 `click` 对应卡片标题(精确匹配上表「首页卡片文案」)。验收:`location.pathname` 为 `/workbench/{slug}/topic`(Meta 除外)。
|
|
361
|
+
|
|
362
|
+
**各子工具 URL 形态**(`{slug}` 见上表;`{step}` 为下节各工具步骤路径段):
|
|
363
|
+
|
|
364
|
+
```text
|
|
365
|
+
/workbench/{slug}/topic
|
|
366
|
+
/workbench/{slug}/outline
|
|
367
|
+
/workbench/{slug}/proofread # 仅 jova、nsfc-v2、clinical
|
|
368
|
+
/workbench/{slug}/writing
|
|
369
|
+
/workbench/nsfc-v2/hypothesis_confirm # 基金专用
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
##### 子工具页通用壳(所有已上线 WB)
|
|
373
|
+
|
|
374
|
+
| 区域 | 稳定定位 | 说明 |
|
|
375
|
+
| --- | --- | --- |
|
|
376
|
+
| 面包屑 · 回门户 | `getByRole('navigation', { name: '面包屑导航' }).getByText('AI工作台', { exact: true })` | 回到 `/workbench`,**优先**于侧栏再点一次「AI工作台」 |
|
|
377
|
+
| 面包屑 · 回当前工具首页 | `getByRole('navigation', { name: '面包屑导航' }).getByText('<面包屑第二级文案>', { exact: true })` | 一般回到该工具 `topic` 步 |
|
|
378
|
+
| 演示教程 | `getByText('演示教程', { exact: true })` | 打开 `dialog "演示教程"`(内嵌飞书 wiki `iframe`);关闭:`getByRole('dialog', { name: '演示教程' }).getByRole('button', { name: 'Close' })`。教程打开时**勿**点击被 iframe 遮挡的页面按钮 |
|
|
379
|
+
| 步骤条 | `getByText('<步骤名>', { exact: true })` | 仅展示进度;**不要**用步骤条 `goto` 跳步 |
|
|
380
|
+
| 底栏免责声明 | 正文含 `内容由AI生成,仅供学习参考` | 无交互 |
|
|
381
|
+
|
|
382
|
+
**页面内 Modal(Ant Design)**:全文生成确认、完成校对确认等须 `getByRole('dialog', { name: '…' })` + `getByRole('button', { name: '…' })`;**禁止** `dialog-accept` / `dialog-dismiss`(仅适用于原生 `alert`/`confirm`)。
|
|
383
|
+
|
|
384
|
+
**生成等待**:出现「…生成中,请耐心等待 …%」或 Loading 时**禁止**连点主按钮;轮询 `eval` 直至底栏主按钮出现且 loading 文案消失,再 `snapshot` 一次验收。
|
|
385
|
+
|
|
386
|
+
**下载类按钮**(`下载提纲` / `下载文档`):会触发浏览器下载;向用户汇报须遵守本文 skill「下载行为 · 强制规则」两行绝对路径格式。
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
##### WB-1:综述工作台(`jova`)
|
|
391
|
+
|
|
392
|
+
**步骤条(4 步)**:拟定主题 → 确认提纲 → 初稿校对 → 初稿精修。
|
|
393
|
+
|
|
394
|
+
| 步骤 | `pathname` | 主要表单项 / 区块 | 底栏主按钮(`getByRole('button', …)`) |
|
|
395
|
+
| --- | --- | --- | --- |
|
|
396
|
+
| 拟定主题 | `/workbench/jova/topic` | **生成模式**(三选一,`click` 文案):`短篇`(6000-10000字)、`中篇`(10000-15000字)、`长篇`(>15000字);**示例主题**列表 + `getByText('换一组', { exact: true })`;**综述主题** `getByRole('textbox', { name: '请输入综述主题概要内容' })`(上限 2000 字,界面显示 `0 / 2000`) | `继续编辑本地文档`;`生成提纲` |
|
|
397
|
+
| 确认提纲 | `/workbench/jova/outline` | 左侧「论文提纲」树;节点行内为**无文案图标按钮**(增删改,需时用 `generate-locator` 固化,勿记 `e*`) | `返回主题`;`下载提纲`;`生成全文` |
|
|
398
|
+
| 初稿校对 | `/workbench/jova/proofread` | 左目录 + 右正文;段落带「请校对本段内容」提示 | `返回提纲`;`下载文档`;`完成校对` |
|
|
399
|
+
| 初稿精修 | `/workbench/jova/writing` | 左目录 + 中央 `textbox` 富文本;顶栏 **修订模式**(含接受/拒绝修订) | `返回提纲`;`修订模式` 切换钮(文案含 `接受修订` / `拒绝修订`);`新建论文`;`下载文档` |
|
|
400
|
+
|
|
401
|
+
**WB-1 关键动作**
|
|
402
|
+
|
|
403
|
+
1. **示例主题填入**:`click` → `getByText('<示例标题>', { exact: true })` → 验收 textbox 已填入同标题。
|
|
404
|
+
2. **生成提纲**:填主题后 `click` → `getByRole('button', { name: '生成提纲' })` → 验收 `/workbench/jova/outline`;等待「提纲内容生成中…」结束。
|
|
405
|
+
3. **生成全文(含弹窗)**:`click` → `getByRole('button', { name: '生成全文' })` → `dialog "是否已完成提纲修改,继续生成全文"` 内:`getByText('中文'|'English', { exact: true })`;综述另须 `getByText('是'|'否', { exact: true })`(「需要中文文献」);`click` → `getByRole('dialog', { name: '是否已完成提纲修改,继续生成全文' }).getByRole('button', { name: '确 认' })`(注意「确」「认」间有空格)。验收:`/workbench/jova/proofread`。
|
|
406
|
+
4. **完成校对 → 精修**:`click` → `getByRole('button', { name: '完成校对' })` → 二次 `dialog` 文案「是否确认完成所有段落内容校对,进入全文编辑模式」→ `getByRole('button', { name: '确认' })`(无空格)。验收:`/workbench/jova/writing`。
|
|
407
|
+
|
|
408
|
+
**取消全文生成弹窗**:同 dialog 内 `getByRole('button', { name: '取 消' })` 或 `Close`。
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
##### WB-2:SCI 论著工作台(`sci`)
|
|
413
|
+
|
|
414
|
+
**步骤条(3 步,无「初稿校对」步)**:拟定主题 → 确认提纲 → 初稿精修。
|
|
415
|
+
|
|
416
|
+
| 步骤 | `pathname` | 主要表单项 / 区块 | 底栏主按钮 |
|
|
417
|
+
| --- | --- | --- | --- |
|
|
418
|
+
| 拟定主题 | `/workbench/sci/topic` | **写作类型**:`SCI论文写作练习` / `SCI论文写作实操`;**文章模板**:`基础` / `临床` / `生信`(卡片,`locator('main').getByText('基础', { exact: true }).first()` 等);**基础主题** `getByRole('textbox', { name: '请输入基础主题' })`(5000);**方法和结果(选填)** + `从本地文件中提取方法和结果` / `从推荐文献中提取方法和结果` | `继续编辑本地文档`;`生成提纲` |
|
|
419
|
+
| 确认提纲 | `/workbench/sci/outline` | 提纲树(结构同综述) | `返回主题`;`生成全文`(**无** `下载提纲` 于 2026-06-02 实测底栏,若 UI 恢复则回写) |
|
|
420
|
+
| 初稿精修 | `/workbench/sci/writing` | 生成全文后**直接进入** writing(跳过 proofread 步骤条) | `返回提纲`;`新建论文`;`下载文档` |
|
|
421
|
+
|
|
422
|
+
**WB-2 关键动作**
|
|
423
|
+
|
|
424
|
+
1. 选定写作类型 + 模板后填 `请输入基础主题` → `生成提纲` → 验收 `/workbench/sci/outline`。
|
|
425
|
+
2. `生成全文` → dialog「是否已完成提纲修改,继续生成全文」:仅需选 `中文` 或 `English`(**无**「需要中文文献」项)→ `确 认` → 验收 `/workbench/sci/writing`(长耗时,等待 loading 结束)。
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
##### WB-3:国自然基金工作台(`nsfc-v2`)
|
|
430
|
+
|
|
431
|
+
**步骤条(5 步)**:Step1 生成科学假说 → Step2 确认科学假说 → Step3 生成申请书提纲 → Step4 校对申请书全文 → Step5 精修申请书全文。
|
|
432
|
+
|
|
433
|
+
| 步骤 | `pathname` | 主要表单项 / 区块 | 底栏主按钮 |
|
|
434
|
+
| --- | --- | --- | --- |
|
|
435
|
+
| 生成科学假说 | `/workbench/nsfc-v2/topic` | **假说来源**:`我没有科学假说,请帮我生成` / `我已有科学假说,立即填写`;**拟申请的项目类型***:`getByRole('radio', { name: '面上项目'|'地区项目'|'青年项目' })`;**您的情况***:`我有前期研究结果` / `我没有前期研究结果`;有前期时 **`button` 上传**「将文件拖到此处,或点击上传」(`.pdf`/`.docx`);无前期时 **研究主题*** `getByRole('textbox', { name: /请详细描述您想要研究的主题/ })`;有前期时 **疾病名称*** `getByRole('textbox', { name: '例如:乳腺癌' })` | `下一步` |
|
|
436
|
+
| 确认科学假说 | `/workbench/nsfc-v2/hypothesis_confirm` | AI 生成多卡片假说(可选中切换) | `上一步`;`下一步` |
|
|
437
|
+
| 生成申请书提纲 | `/workbench/nsfc-v2/outline` | 国自然提纲树;节点级 `重新生成` | `重新生成提纲`;`下载提纲`;`上一步`;`下一步` |
|
|
438
|
+
| 校对申请书全文 | `/workbench/nsfc-v2/proofread` | 分块正文(含国自然固定章节标题) | `返回提纲`;`下载文档`;`完成校对` |
|
|
439
|
+
| 精修申请书全文 | `/workbench/nsfc-v2/writing` | 全文编辑 | `返回提纲`;`新建论文`;`下载文档` |
|
|
440
|
+
|
|
441
|
+
**WB-3 关键动作(无前期结果快速路径,已实测)**
|
|
442
|
+
|
|
443
|
+
1. `click` → `getByText('我没有科学假说,请帮我生成', { exact: true })` → `getByRole('radio', { name: '我没有前期研究结果' })` → 填研究主题 textbox → `下一步` → `/hypothesis_confirm`。
|
|
444
|
+
2. 等待「科学假说生成中…」→ `下一步` → `/outline` → 等待提纲就绪 → `下一步` → `/proofread` → 等待全文生成 → `完成校对` → dialog「进入全文编辑模式」→ `确认` → `/writing`。
|
|
445
|
+
|
|
446
|
+
**有前期结果路径**:选 `我有前期研究结果` → 上传 PDF/DOCX → 填疾病名称 → `下一步`(其余同上)。
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
##### WB-4:临床申请书工作台(`clinical`)
|
|
451
|
+
|
|
452
|
+
**步骤条(4 步)**:选题评估 → 四要素校验 → 初稿校对 → 全文精修。
|
|
453
|
+
|
|
454
|
+
| 步骤 | `pathname` | 主要表单项 / 区块 | 底栏主按钮 |
|
|
455
|
+
| --- | --- | --- | --- |
|
|
456
|
+
| 选题评估 | `/workbench/clinical/topic` | **研究类别***:`疗效`/`病因`/`预后`/`诊断`(radio);**研究设计***:`队列研究`/`临床试验`;**时间方向***:`前瞻性`/`回顾性`;**中心数***:`单中心`/`多中心`;**Population** `getByRole('textbox', { name: /研究人群/ })`;**Intervention** `getByRole('textbox', { name: /干预/ })`;**Control** `getByRole('textbox', { name: /对照/ })`(选填);右侧「预检结果」面板 | `清 空`;`预 检`;预检完成后出现 `下一步` |
|
|
457
|
+
| 四要素校验 | `/workbench/clinical/outline` | 自动展开 P/I/C/O/结局/样本量等;块内 `自定义编辑`;样本量区 `立即计算`(按钮文案,部分环境需 `locator('main').getByText('立即计算', { exact: true })` 且注意可见性) | `上一步`;`预 检`;预检通过后 `下一步` |
|
|
458
|
+
| 初稿校对 | `/workbench/clinical/proofread` | 申请书全文(含「请校对本段内容」) | `上一步`;`下一步`;`完成校对` |
|
|
459
|
+
| 全文精修 | `/workbench/clinical/writing` | 全文编辑 | `返回四要素校验`;`新建论文`;`下载文档` |
|
|
460
|
+
|
|
461
|
+
**WB-4 关键动作**
|
|
462
|
+
|
|
463
|
+
1. 填 PICOS 必填项 → `预 检` → 右侧出现选题评估文案 → `下一步` → `/outline`。
|
|
464
|
+
2. 等待「四要素生成中…」结束 → (可选)`预 检` 更新右侧结果 → `下一步` → `/proofread`。
|
|
465
|
+
3. 等待「文章内容生成中…」→ `完成校对` → dialog → `确认` → `/writing`。
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
##### WB-0:Meta 工作台(未上线)
|
|
470
|
+
|
|
471
|
+
- 卡片可点但 **URL 不变**(仍为 `/workbench`)。
|
|
472
|
+
- **禁止**编写 `goto /workbench/meta/...` 或假设存在子路由。
|
|
473
|
+
- 用户若要 Meta 功能:说明「开发中」,改做其他子工具或等待产品上线后回写 slug。
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
##### 跨工具对照(Agent 快查)
|
|
478
|
+
|
|
479
|
+
| 能力 | jova 综述 | sci 论著 | nsfc-v2 基金 | clinical 临床 |
|
|
480
|
+
| --- | --- | --- | --- | --- |
|
|
481
|
+
| 独立「校对」步骤条 | 有 | **无**(outline→writing) | 有 | 有 |
|
|
482
|
+
| 全文生成前 dialog | 语言 + 中文文献 | 仅语言 | (outline 直接下一步进 proofread) | (outline 下一步进 proofread) |
|
|
483
|
+
| 校对完成二次确认 | `确认` | — | `确认` | `确认` |
|
|
484
|
+
| 精修页「返回」按钮 | `返回提纲` | `返回提纲` | `返回提纲` | `返回四要素校验` |
|
|
485
|
+
| 精修页修订模式 | 有 | 无(实测) | 无(实测) | 无(实测) |
|
|
486
|
+
|
|
487
|
+
**用户说法映射**
|
|
488
|
+
|
|
489
|
+
- 「写综述 / 综述工作台」→ WB-1,`getByText('综述工作台')`。
|
|
490
|
+
- 「SCI / 英文论文 / 论著工作台」→ WB-2,`getByText('SCI论著工作台')`。
|
|
491
|
+
- 「国自然 / 基金 / 标书」→ WB-3,`getByText('基金工作台')`(**不是**旧称「基金申请书工作台」)。
|
|
492
|
+
- 「临床申请书 / PICOS / 临床工作台」→ WB-4;面包屑为「临床申请书工作台」。
|
|
493
|
+
- 「Meta / Meta分析工作台」→ WB-0,仅提示未上线。
|
|
342
494
|
|
|
343
495
|
#### AI 应用 `/application/`(列表 · 详情 · 多块输入输出 · mindmap · 级联分类)
|
|
344
496
|
|
|
@@ -1098,9 +1250,12 @@ async page => {
|
|
|
1098
1250
|
|
|
1099
1251
|
#### S-11:AI 工作台 · 进入子工作台
|
|
1100
1252
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1253
|
+
> **细则**见上文「AI 工作台 `/workbench`」(WB-0~WB-4)。本节为最短路径;进入后按目标工具执行 WB-1~WB-4,**勿**对 Meta 假设子路由。
|
|
1254
|
+
|
|
1255
|
+
1. S-07 → `/workbench`(或面包屑回 `AI工作台`)。
|
|
1256
|
+
2. `click` → `getByText('<首页卡片文案>', { exact: true })`(见 WB-0 表:综述 / SCI论著 / 基金 / 临床)。
|
|
1257
|
+
3. **验收**:`location.pathname === '/workbench/{slug}/topic'`(Meta 除外:点击后仍为 `/workbench`)。
|
|
1258
|
+
4. **后续**:综述 → WB-1;SCI → WB-2;基金 → WB-3;临床 → WB-4。
|
|
1104
1259
|
|
|
1105
1260
|
#### S-12:AI 应用 · 标准操作
|
|
1106
1261
|
|