helixlife-v5-cli 1.2.0 → 1.2.2
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/scripts/workbench-jova-outline-regenerate-node.js +149 -0
- package/scripts/workbench-jova-proofread-section.js +262 -0
- package/scripts/workbench-jova-writing-tool.js +308 -0
- package/lib/automation-overlay.browser.js +0 -76
- package/lib/automation-overlay.js +0 -238
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 综述工作台(jova)· 初稿精修页 · 右侧 AI 工具
|
|
3
|
+
*
|
|
4
|
+
* 支持工具(右侧栏):校对、文风、润色、降重、翻译、改写、扩写、缩写、文献列表
|
|
5
|
+
*
|
|
6
|
+
* Agent 调用约定(严格遵守):
|
|
7
|
+
*
|
|
8
|
+
* 1) 用户说「<工具> <章节>」、未指明后续操作时:
|
|
9
|
+
* action = null → 选中章节 → 打开工具 →(文风填参考文风)→ 一键生成
|
|
10
|
+
* → 返回 needsReplaceChoice,禁止自动点「替换原文」
|
|
11
|
+
* 例外:文献列表仅打开列表面板,无生成步骤
|
|
12
|
+
*
|
|
13
|
+
* 2) 用户明确说「替换原文」:action = 'replace'
|
|
14
|
+
*
|
|
15
|
+
* 3) 自检全部工具:probeAll = true(开发/验收用,依次跑 9 项,不替换原文)
|
|
16
|
+
*
|
|
17
|
+
* intent 示例:
|
|
18
|
+
* 「翻译 前言」 → tool=翻译, sectionTitle=前言
|
|
19
|
+
* 「文风 摘要」 → tool=文风, sectionTitle=摘要(须 referenceStyle 或自动兜底)
|
|
20
|
+
* 「文献列表」 → tool=文献列表, sectionTitle 可空(选中全文参考文献)
|
|
21
|
+
*
|
|
22
|
+
* 用法:
|
|
23
|
+
* helixlife-v5-cli run-code --filename=scripts/workbench-jova-writing-tool.js
|
|
24
|
+
*/
|
|
25
|
+
async page => {
|
|
26
|
+
const intent = '翻译 前言';
|
|
27
|
+
const action = null; // null | 'replace'
|
|
28
|
+
const extraRequirement = ''; // 额外需求(可选)
|
|
29
|
+
const referenceStyle = ''; // 文风专用;空则从选中段落截取兜底
|
|
30
|
+
const probeAll = false; // true = 验收 9 个工具(用「关键词」短段)
|
|
31
|
+
|
|
32
|
+
const TOOLS = ['校对', '文风', '润色', '降重', '翻译', '改写', '扩写', '缩写', '文献列表'];
|
|
33
|
+
|
|
34
|
+
const main = page.locator('main');
|
|
35
|
+
|
|
36
|
+
if (!page.url().includes('/workbench/jova/writing')) {
|
|
37
|
+
return { ok: false, reason: '当前不在初稿精修页 /workbench/jova/writing', intent };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseIntent(raw) {
|
|
41
|
+
const text = raw.trim();
|
|
42
|
+
for (const tool of TOOLS) {
|
|
43
|
+
if (text === tool || text.startsWith(`${tool} `) || text.startsWith(tool)) {
|
|
44
|
+
const sectionTitle = text.slice(tool.length).trim();
|
|
45
|
+
return { tool, sectionTitle: sectionTitle || null };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { tool: null, sectionTitle: text || null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function closePanel() {
|
|
52
|
+
const closeIcons = main.getByRole('img', { name: 'close' });
|
|
53
|
+
for (let i = 0; i < (await closeIcons.count()); i++) {
|
|
54
|
+
const icon = closeIcons.nth(i);
|
|
55
|
+
if (await icon.isVisible().catch(() => false)) {
|
|
56
|
+
await icon.click();
|
|
57
|
+
await page.waitForTimeout(400);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const cancel = main.getByRole('button', { name: '取 消' });
|
|
61
|
+
if ((await cancel.count()) > 0 && (await cancel.first().isVisible().catch(() => false))) {
|
|
62
|
+
await cancel.first().click();
|
|
63
|
+
await page.waitForTimeout(300);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function navigateAndSelectSection(title) {
|
|
68
|
+
if (title) {
|
|
69
|
+
const toc = main.locator('[class*="cursor-pointer"]').getByText(title, { exact: true });
|
|
70
|
+
if ((await toc.count()) > 0) {
|
|
71
|
+
await toc.first().click({ timeout: 5000 }).catch(() => {});
|
|
72
|
+
await page.waitForTimeout(500);
|
|
73
|
+
}
|
|
74
|
+
const heading = main.getByRole('heading', { name: title, exact: true }).first();
|
|
75
|
+
if ((await heading.count()) > 0) {
|
|
76
|
+
await heading.scrollIntoViewIfNeeded();
|
|
77
|
+
await page.waitForTimeout(300);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const selected = await page.evaluate(t => {
|
|
82
|
+
const editor = document.querySelector('main [role="textbox"]');
|
|
83
|
+
if (!editor) return { ok: false, reason: '未找到富文本编辑器' };
|
|
84
|
+
|
|
85
|
+
if (!t) {
|
|
86
|
+
const body = editor.textContent?.trim() || '';
|
|
87
|
+
const range = document.createRange();
|
|
88
|
+
range.selectNodeContents(editor);
|
|
89
|
+
const sel = window.getSelection();
|
|
90
|
+
sel?.removeAllRanges();
|
|
91
|
+
sel?.addRange(range);
|
|
92
|
+
return { ok: true, textLength: body.length, preview: body.slice(0, 80), wholeDoc: true };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const headings = [...editor.querySelectorAll('h1,h2,h3,h4,h5,h6')];
|
|
96
|
+
const idx = headings.findIndex(h => h.textContent?.trim() === t);
|
|
97
|
+
if (idx < 0) return { ok: false, reason: `编辑器内未找到章节「${t}」` };
|
|
98
|
+
|
|
99
|
+
const start = headings[idx];
|
|
100
|
+
const level = parseInt(start.tagName[1], 10);
|
|
101
|
+
let end = null;
|
|
102
|
+
for (let i = idx + 1; i < headings.length; i++) {
|
|
103
|
+
if (parseInt(headings[i].tagName[1], 10) <= level) {
|
|
104
|
+
end = headings[i];
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const range = document.createRange();
|
|
110
|
+
range.setStartBefore(start);
|
|
111
|
+
range.setEndBefore(end || editor.lastChild || editor);
|
|
112
|
+
const sel = window.getSelection();
|
|
113
|
+
sel?.removeAllRanges();
|
|
114
|
+
sel?.addRange(range);
|
|
115
|
+
|
|
116
|
+
const text = range.toString();
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
textLength: text.length,
|
|
120
|
+
preview: text.slice(0, 80),
|
|
121
|
+
sectionText: text.slice(0, 500),
|
|
122
|
+
};
|
|
123
|
+
}, title);
|
|
124
|
+
|
|
125
|
+
await page.waitForTimeout(400);
|
|
126
|
+
return selected;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function waitReplaceReady(timeoutMs = 120000) {
|
|
130
|
+
const replaceBtn = main.getByRole('button', { name: '替换原文' });
|
|
131
|
+
await replaceBtn.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
132
|
+
const deadline = Date.now() + timeoutMs;
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
if (await replaceBtn.isEnabled()) return true;
|
|
135
|
+
await page.waitForTimeout(1000);
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function fillExtraIfNeeded() {
|
|
141
|
+
if (!extraRequirement) return;
|
|
142
|
+
const box = main.getByRole('textbox', { name: '额外需求' });
|
|
143
|
+
if ((await box.count()) > 0 && (await box.first().isVisible().catch(() => false))) {
|
|
144
|
+
await box.first().fill(extraRequirement);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function fillReferenceStyleIfNeeded(selected) {
|
|
149
|
+
const refBox = main.getByRole('textbox', { name: '参考文风' });
|
|
150
|
+
if ((await refBox.count()) === 0 || !(await refBox.first().isVisible().catch(() => false))) {
|
|
151
|
+
return { filled: false };
|
|
152
|
+
}
|
|
153
|
+
const ref =
|
|
154
|
+
referenceStyle ||
|
|
155
|
+
(selected.sectionText
|
|
156
|
+
? selected.sectionText.slice(0, 200)
|
|
157
|
+
: 'This review synthesizes molecular subtyping and precision therapeutic strategies in breast cancer.');
|
|
158
|
+
await refBox.first().fill(ref);
|
|
159
|
+
return { filled: true, referenceStyle: ref.slice(0, 80) };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function pickTranslationLanguage() {
|
|
163
|
+
for (const label of ['English', '英文']) {
|
|
164
|
+
const opt = main.getByText(label, { exact: true });
|
|
165
|
+
if ((await opt.count()) > 0 && (await opt.first().isVisible().catch(() => false))) {
|
|
166
|
+
await opt.first().click();
|
|
167
|
+
await page.waitForTimeout(300);
|
|
168
|
+
return label;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function openToolAndGenerate(tool, sectionTitle) {
|
|
175
|
+
await closePanel();
|
|
176
|
+
|
|
177
|
+
const selected = await navigateAndSelectSection(sectionTitle);
|
|
178
|
+
if (!selected.ok) return { ok: false, tool, sectionTitle, ...selected };
|
|
179
|
+
|
|
180
|
+
const toolBtn = main.getByText(tool, { exact: true });
|
|
181
|
+
if ((await toolBtn.count()) === 0) {
|
|
182
|
+
return { ok: false, tool, sectionTitle, reason: `未找到工具「${tool}」` };
|
|
183
|
+
}
|
|
184
|
+
await toolBtn.first().click();
|
|
185
|
+
await page.waitForTimeout(800);
|
|
186
|
+
|
|
187
|
+
if (tool === '文献列表') {
|
|
188
|
+
const panel = main.getByText('文献列表', { exact: true });
|
|
189
|
+
const visible = (await panel.count()) > 0;
|
|
190
|
+
const refLinks = await main.locator('a[href*="pubmed"], a[href*="doi"], a[href*="clmd"]').count();
|
|
191
|
+
return {
|
|
192
|
+
ok: visible,
|
|
193
|
+
tool,
|
|
194
|
+
sectionTitle,
|
|
195
|
+
selected,
|
|
196
|
+
mode: 'list',
|
|
197
|
+
refLinkCount: refLinks,
|
|
198
|
+
message: '已打开文献列表面板。',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await main.getByText('原文内容', { exact: true }).waitFor({ state: 'visible', timeout: 10000 }).catch(() => {});
|
|
203
|
+
|
|
204
|
+
let refStyle = null;
|
|
205
|
+
if (tool === '文风') {
|
|
206
|
+
refStyle = await fillReferenceStyleIfNeeded(selected);
|
|
207
|
+
}
|
|
208
|
+
if (tool === '翻译') {
|
|
209
|
+
await pickTranslationLanguage();
|
|
210
|
+
}
|
|
211
|
+
await fillExtraIfNeeded();
|
|
212
|
+
|
|
213
|
+
const genBtn = main.getByRole('button', { name: '一键生成' });
|
|
214
|
+
if ((await genBtn.count()) === 0 || !(await genBtn.first().isVisible().catch(() => false))) {
|
|
215
|
+
return { ok: false, tool, sectionTitle, reason: '未找到「一键生成」按钮' };
|
|
216
|
+
}
|
|
217
|
+
await genBtn.first().click();
|
|
218
|
+
|
|
219
|
+
const replaceReady = await waitReplaceReady();
|
|
220
|
+
return {
|
|
221
|
+
ok: replaceReady,
|
|
222
|
+
tool,
|
|
223
|
+
sectionTitle,
|
|
224
|
+
selected,
|
|
225
|
+
refStyle,
|
|
226
|
+
replaceReady,
|
|
227
|
+
mode: 'generate',
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function replaceGenerated() {
|
|
232
|
+
const replaceBtn = main.getByRole('button', { name: '替换原文' });
|
|
233
|
+
if ((await replaceBtn.count()) === 0 || !(await replaceBtn.isEnabled().catch(() => false))) {
|
|
234
|
+
return { ok: false, reason: '「替换原文」不可用' };
|
|
235
|
+
}
|
|
236
|
+
await replaceBtn.click();
|
|
237
|
+
await main.getByText('是否将本次生成结果替换原文?', { exact: true }).waitFor({ state: 'visible', timeout: 10000 });
|
|
238
|
+
await main.getByRole('button', { name: '确 定' }).click();
|
|
239
|
+
await page.waitForTimeout(1000);
|
|
240
|
+
return { ok: true, replaced: true };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (probeAll) {
|
|
244
|
+
const probeSection = '关键词';
|
|
245
|
+
const results = [];
|
|
246
|
+
for (const tool of TOOLS) {
|
|
247
|
+
const r = await openToolAndGenerate(tool, probeSection);
|
|
248
|
+
results.push({ tool, ok: r.ok, mode: r.mode, replaceReady: r.replaceReady, reason: r.reason });
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
ok: results.every(r => r.ok),
|
|
252
|
+
probeAll: true,
|
|
253
|
+
probeSection,
|
|
254
|
+
results,
|
|
255
|
+
url: page.url(),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const { tool, sectionTitle } = parseIntent(intent);
|
|
260
|
+
if (!tool || !TOOLS.includes(tool)) {
|
|
261
|
+
return {
|
|
262
|
+
ok: false,
|
|
263
|
+
intent,
|
|
264
|
+
reason: `无法解析工具,支持:${TOOLS.join('、')}`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (tool !== '文献列表' && !sectionTitle) {
|
|
268
|
+
return { ok: false, intent, reason: '请指定章节标题,如「翻译 前言」' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (action === 'replace') {
|
|
272
|
+
const replaced = await replaceGenerated();
|
|
273
|
+
return {
|
|
274
|
+
ok: replaced.ok,
|
|
275
|
+
intent,
|
|
276
|
+
tool,
|
|
277
|
+
sectionTitle,
|
|
278
|
+
action,
|
|
279
|
+
...replaced,
|
|
280
|
+
message: replaced.ok ? '已替换原文。' : replaced.reason,
|
|
281
|
+
url: page.url(),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const generated = await openToolAndGenerate(tool, sectionTitle);
|
|
286
|
+
|
|
287
|
+
if (generated.mode === 'list') {
|
|
288
|
+
return { ok: generated.ok, intent, tool, sectionTitle, action: null, ...generated, url: page.url() };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!generated.ok) {
|
|
292
|
+
return { ok: false, intent, tool, sectionTitle, action: null, ...generated, url: page.url() };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
ok: true,
|
|
297
|
+
intent,
|
|
298
|
+
tool,
|
|
299
|
+
sectionTitle,
|
|
300
|
+
action: null,
|
|
301
|
+
needsReplaceChoice: true,
|
|
302
|
+
availableActions: ['replace'],
|
|
303
|
+
replaceReady: generated.replaceReady,
|
|
304
|
+
selected: generated.selected,
|
|
305
|
+
message: `「${tool}」已生成结果,请在右侧查看;确认后告知我「替换原文」以应用。`,
|
|
306
|
+
url: page.url(),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -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
|
-
};
|