helixlife-v5-cli 1.2.8 → 1.2.9

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.
Files changed (46) hide show
  1. package/README.md +33 -58
  2. package/helix-cli.js +7 -2
  3. package/lib/.cli.runtime.generated.json +10 -1
  4. package/lib/automation-file-chooser-guard.js +182 -0
  5. package/lib/automation-overlay.init.js +332 -68
  6. package/lib/browser-css-cache-guard.init-page.js +49 -0
  7. package/lib/browser-window-maximize.init-page.js +64 -0
  8. package/lib/cli.js +316 -14
  9. package/lib/helix-reload-bypass-cache.run-code.js +7 -0
  10. package/lib/helix-styles-guard.js +77 -0
  11. package/lib/hide-nested-skills.js +120 -0
  12. package/lib/native-mode-patch.js +288 -0
  13. package/lib/postinstall.js +39 -0
  14. package/lib/profile-lock-guard.js +187 -0
  15. package/lib/runtime-config.js +90 -0
  16. package/lib/token-file.js +189 -0
  17. package/lib/visual-page-wrap.js +311 -0
  18. package/lib/visual.js +255 -86
  19. package/package.json +9 -21
  20. package/scripts/README.md +81 -0
  21. package/scripts/{analysis-detail-upload-file.js → analysis/detail-upload-file.js} +1 -1
  22. package/scripts/{jigsaw-inspect-dom.js → analysis/jigsaw-inspect-dom.js} +15 -15
  23. package/scripts/{jigsaw-verify-enumerate.js → analysis/jigsaw-verify-enumerate.js} +1 -1
  24. package/scripts/common/.gitkeep +0 -0
  25. package/scripts/{chrome-pdf-enumerate-a11y.js → edu/chrome-pdf-enumerate-a11y.js} +1 -1
  26. package/scripts/{chrome-pdf-goto-page.js → edu/chrome-pdf-goto-page.js} +1 -1
  27. package/scripts/{chrome-pdf-zoom-out.js → edu/chrome-pdf-zoom-out.js} +1 -1
  28. package/scripts/{hover-partial-chapter-match.js → edu/hover-partial-chapter-match.js} +1 -1
  29. package/scripts/maintainer/strip-extra-skills.ps1 +27 -0
  30. package/scripts/workbench/jova/outline-edit-root-title.js +33 -0
  31. package/scripts/workbench/jova/outline-keyword-edit.js +122 -0
  32. package/scripts/workbench/jova/outline-ops.js +344 -0
  33. package/scripts/workbench/jova/outline-probe-hover.js +56 -0
  34. package/scripts/workbench/jova/proofread-ai-rewrite-section.js +94 -0
  35. package/scripts/workbench/jova/proofread-ops.js +261 -0
  36. package/scripts/workbench/jova/writing-ops.js +376 -0
  37. package/scripts/workbench/jova/writing-proofread-abstract.js +103 -0
  38. package/lib/cli.runtime.config.json +0 -5
  39. package/references/vip.helixlife.cn.site-ops.md +0 -1469
  40. package/scripts/workbench-jova-outline-regenerate-node.js +0 -173
  41. package/scripts/workbench-jova-proofread-section.js +0 -262
  42. package/scripts/workbench-jova-writing-tool.js +0 -308
  43. /package/scripts/{analysis-detail-upload-both.js → analysis/detail-upload-both.js} +0 -0
  44. /package/scripts/{jigsaw-verify-bio-tools.js → analysis/jigsaw-verify-bio-tools.js} +0 -0
  45. /package/scripts/{jigsaw-verify-drag-tool.js → analysis/jigsaw-verify-drag-tool.js} +0 -0
  46. /package/scripts/{jigsaw-verify-reference-lines.js → analysis/jigsaw-verify-reference-lines.js} +0 -0
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * native-mode-patch
5
+ *
6
+ * 对 `node_modules/playwright-core/lib/coreBundle.js` 做幂等的纯文本补丁,
7
+ * 让 Playwright 启动的浏览器在交互层向「CDP 直连原生浏览器」对齐:
8
+ *
9
+ * 1) 上传:`Tab` 默认不再急切订阅 `filechooser`,原生选择框可被手动唤起;
10
+ * 仍以环境变量 `HELIX_NATIVE_FILE_CHOOSER=0` 切回旧行为(保留 `browser_file_upload` 工具)。
11
+ * 2) 下载:`acceptDownloads` 默认改为 `internal-browser-default`,不再下发
12
+ * `Browser.setDownloadBehavior`,浏览器使用原生下载面板(真实文件名 + 原生图标);
13
+ * `HELIX_NATIVE_DOWNLOADS=0` 切回旧行为。
14
+ * 3) 沙箱:补全 `validateBrowserConfig` 的 `browser.browserName` 推断,
15
+ * 让 Windows/macOS 上的 `chromiumSandbox` 默认为 `true`,消除 `--no-sandbox` 黄条。
16
+ *
17
+ * 设计要点:
18
+ * - 全部使用「精确字符串替换」,命中失败时仅打印 warning,不让安装失败;
19
+ * - 每条补丁带哨兵注释(`/​* helix-native-mode-patch:<id> *​/`),第二次执行幂等跳过;
20
+ * - 任意时刻可通过删除 `node_modules` + `npm i` 还原(postinstall 会再次打补丁)。
21
+ */
22
+
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+
26
+ const SENTINEL_PREFIX = '/* helix-native-mode-patch:';
27
+
28
+ /**
29
+ * @typedef {Object} Patch
30
+ * @property {string} id 哨兵 id(同一文件内唯一)。
31
+ * @property {string[]} rel 相对 SKILL_ROOT 的路径片段。
32
+ * @property {string} find 精确匹配的源串(必须在文件中唯一)。
33
+ * @property {string} replace 替换串(首行写哨兵注释)。
34
+ */
35
+
36
+ /** @type {Patch[]} */
37
+ const PATCHES = [
38
+ {
39
+ id: 'tab-filechooser-lazy-v1',
40
+ rel: ['node_modules', 'playwright-core', 'lib', 'coreBundle.js'],
41
+ find:
42
+ ` eventsHelper.addEventListener(p, "filechooser", (chooser) => {
43
+ this.setModalState({
44
+ type: "fileChooser",
45
+ description: "File chooser",
46
+ fileChooser: chooser,
47
+ clearedBy: { tool: uploadFile.schema.name, skill: "upload" }
48
+ });
49
+ }),`,
50
+ replace:
51
+ ` /* helix-native-mode-patch:tab-filechooser-lazy-v1 */
52
+ (process.env.HELIX_NATIVE_FILE_CHOOSER === "0"
53
+ ? eventsHelper.addEventListener(p, "filechooser", (chooser) => {
54
+ this.setModalState({
55
+ type: "fileChooser",
56
+ description: "File chooser",
57
+ fileChooser: chooser,
58
+ clearedBy: { tool: uploadFile.schema.name, skill: "upload" }
59
+ });
60
+ })
61
+ : { dispose: () => {} }),`,
62
+ },
63
+ {
64
+ id: 'context-options-default-v1',
65
+ rel: ['node_modules', 'playwright-core', 'lib', 'coreBundle.js'],
66
+ find:
67
+ ` if (options.acceptDownloads === void 0 && browserOptions.name !== "electron")
68
+ options.acceptDownloads = "accept";`,
69
+ replace:
70
+ ` /* helix-native-mode-patch:context-options-default-v1 */
71
+ if (options.acceptDownloads === void 0 && browserOptions.name !== "electron")
72
+ options.acceptDownloads = process.env.HELIX_NATIVE_DOWNLOADS === "0" ? "accept" : "internal-browser-default";`,
73
+ },
74
+ {
75
+ id: 'sandbox-windows-default-v1',
76
+ rel: ['node_modules', 'playwright-core', 'lib', 'coreBundle.js'],
77
+ find:
78
+ ` if (browser.browserName === "chromium" && browser.launchOptions.chromiumSandbox === void 0) {`,
79
+ replace:
80
+ ` /* helix-native-mode-patch:sandbox-windows-default-v1 */
81
+ if (browser.browserName === void 0 && (browser.launchOptions.channel || browserName === "chromium")) browser.browserName = "chromium";
82
+ if (browser.browserName === "chromium" && browser.launchOptions.chromiumSandbox === void 0) {`,
83
+ },
84
+ {
85
+ id: 'tab-download-lazy-v1',
86
+ rel: ['node_modules', 'playwright-core', 'lib', 'coreBundle.js'],
87
+ find:
88
+ ` eventsHelper.addEventListener(p, "dialog", (dialog) => this._dialogShown(dialog)),
89
+ eventsHelper.addEventListener(p, "download", (download) => {
90
+ void this._downloadStarted(download);
91
+ })`,
92
+ replace:
93
+ ` eventsHelper.addEventListener(p, "dialog", (dialog) => this._dialogShown(dialog)),
94
+ /* helix-native-mode-patch:tab-download-lazy-v1 */
95
+ (process.env.HELIX_NATIVE_DOWNLOADS === "0"
96
+ ? eventsHelper.addEventListener(p, "download", (download) => {
97
+ void this._downloadStarted(download);
98
+ })
99
+ : { dispose: () => {} })`,
100
+ },
101
+ {
102
+ id: 'cr-download-skip-v1',
103
+ rel: ['node_modules', 'playwright-core', 'lib', 'coreBundle.js'],
104
+ find:
105
+ ` if (this._browser.options.name !== "clank" && this._options.acceptDownloads !== "internal-browser-default") {
106
+ promises.push(this._browser._session.send("Browser.setDownloadBehavior", {
107
+ behavior: this._options.acceptDownloads === "accept" ? "allowAndName" : "deny",`,
108
+ replace:
109
+ ` /* helix-native-mode-patch:cr-download-skip-v1 */
110
+ if (this._browser.options.name !== "clank" && this._options.acceptDownloads !== "internal-browser-default" && process.env.HELIX_NATIVE_DOWNLOADS === "0") {
111
+ promises.push(this._browser._session.send("Browser.setDownloadBehavior", {
112
+ behavior: this._options.acceptDownloads === "accept" ? "allowAndName" : "deny",`,
113
+ },
114
+ {
115
+ id: 'bidi-download-skip-v1',
116
+ rel: ['node_modules', 'playwright-core', 'lib', 'coreBundle.js'],
117
+ find:
118
+ ` const downloadBehavior = this._options.acceptDownloads === "accept" ? { type: "allowed", destinationFolder: this._browser.options.downloadsPath } : { type: "denied" };
119
+ promises.push(this._browser._browserSession.send("browser.setDownloadBehavior", {
120
+ downloadBehavior,
121
+ userContexts: [this._userContextId()]
122
+ }));`,
123
+ replace:
124
+ ` /* helix-native-mode-patch:bidi-download-skip-v1 */
125
+ if (this._options.acceptDownloads !== "internal-browser-default" && process.env.HELIX_NATIVE_DOWNLOADS === "0") {
126
+ const downloadBehavior = this._options.acceptDownloads === "accept" ? { type: "allowed", destinationFolder: this._browser.options.downloadsPath } : { type: "denied" };
127
+ promises.push(this._browser._browserSession.send("browser.setDownloadBehavior", {
128
+ downloadBehavior,
129
+ userContexts: [this._userContextId()]
130
+ }));
131
+ }`,
132
+ },
133
+ // 注意:曾有 skip-automation-controlled-v1 补丁,会在原生模式下抑制
134
+ // `--disable-blink-features=AutomationControlled` 启动参数,导致
135
+ // `navigator.webdriver === true` 被阿里云图形验证码风控判定为自动化设备。
136
+ // 该补丁已下线,由下方 REVERSE_PATCHES 在 postinstall 时自动回退到 Playwright 原行为。
137
+ ];
138
+
139
+ /**
140
+ * @typedef {Object} ReversePatch
141
+ * @property {string} id 被回退的补丁 id(与历史哨兵注释 id 一致)。
142
+ * @property {string[]} rel 相对 SKILL_ROOT 的路径片段。
143
+ * @property {string} find 当前被打过补丁的代码块(含哨兵注释)。
144
+ * @property {string} replace 回退后的原始 Playwright 代码块。
145
+ */
146
+
147
+ /**
148
+ * 历史补丁回退表:已经写进 node_modules 的旧补丁,在新版本安装/postinstall 时自动撤销,
149
+ * 保证未删除 node_modules 的用户也能拿到修复。
150
+ * 命中失败(说明本机其实没打过该补丁)会静默跳过。
151
+ *
152
+ * @type {ReversePatch[]}
153
+ */
154
+ const REVERSE_PATCHES = [
155
+ {
156
+ id: 'skip-automation-controlled-v1',
157
+ rel: ['node_modules', 'playwright-core', 'lib', 'coreBundle.js'],
158
+ find:
159
+ ' /* helix-native-mode-patch:skip-automation-controlled-v1 */\n' +
160
+ ' if (browserName === "chromium") {\n' +
161
+ ' browser.launchOptions.args = browser.launchOptions.args ?? [];\n' +
162
+ ' if (process.env.HELIX_NATIVE_FILE_CHOOSER === "0" && !browser.launchOptions.args.some((a) => a.includes("--disable-blink-features")))\n' +
163
+ ' browser.launchOptions.args.push(`--disable-blink-features=AutomationControlled`);\n' +
164
+ ' }',
165
+ replace:
166
+ ' if (browserName === "chromium") {\n' +
167
+ ' browser.launchOptions.args = browser.launchOptions.args ?? [];\n' +
168
+ ' if (!browser.launchOptions.args.some((a) => a.includes("--disable-blink-features")))\n' +
169
+ ' browser.launchOptions.args.push(`--disable-blink-features=AutomationControlled`);\n' +
170
+ ' }',
171
+ },
172
+ ];
173
+
174
+ /**
175
+ * 文件内是否已写入指定哨兵。
176
+ * @param {string} content
177
+ * @param {string} id
178
+ */
179
+ function alreadyPatched(content, id) {
180
+ return content.includes(`${SENTINEL_PREFIX}${id} */`);
181
+ }
182
+
183
+ /**
184
+ * 回退历史补丁:把 REVERSE_PATCHES 中已写入 node_modules 的旧补丁还原成 Playwright 原始代码。
185
+ * 仅命中带哨兵的旧补丁,普通源码不受影响。
186
+ * @param {string} skillRoot
187
+ * @param {(msg: string) => void} log
188
+ * @returns {{ reverted: number }}
189
+ */
190
+ function revertLegacyPatches(skillRoot, log) {
191
+ let reverted = 0;
192
+ for (const patch of REVERSE_PATCHES) {
193
+ const file = path.join(skillRoot, ...patch.rel);
194
+ if (!fs.existsSync(file)) {
195
+ continue;
196
+ }
197
+ let content;
198
+ try {
199
+ content = fs.readFileSync(file, 'utf8');
200
+ } catch (e) {
201
+ log(`无法读取 ${file}: ${e && e.message ? e.message : e}`);
202
+ continue;
203
+ }
204
+ if (!content.includes(patch.find)) {
205
+ continue;
206
+ }
207
+ const next = content.replace(patch.find, patch.replace);
208
+ if (next === content) {
209
+ continue;
210
+ }
211
+ try {
212
+ fs.writeFileSync(file, next, 'utf8');
213
+ reverted += 1;
214
+ log(`已回退历史补丁: ${patch.id}`);
215
+ } catch (e) {
216
+ log(`写入 ${file} 失败: ${e && e.message ? e.message : e}`);
217
+ }
218
+ }
219
+ return { reverted };
220
+ }
221
+
222
+ /**
223
+ * 在 skillRoot 上幂等应用全部补丁;执行前先回退历史 REVERSE_PATCHES,再依次应用 PATCHES。
224
+ * @param {string} skillRoot
225
+ * @param {{ log?: (msg: string) => void }} [options]
226
+ * @returns {{ applied: number, skipped: number, missed: string[] }}
227
+ */
228
+ function applyAll(skillRoot, options = {}) {
229
+ const log = options.log || (() => {});
230
+ revertLegacyPatches(skillRoot, log);
231
+ let applied = 0;
232
+ let skipped = 0;
233
+ const missed = [];
234
+
235
+ for (const patch of PATCHES) {
236
+ const file = path.join(skillRoot, ...patch.rel);
237
+ if (!fs.existsSync(file)) {
238
+ // node_modules 还没装好(首次执行 postinstall 之前可能命中),正常跳过。
239
+ continue;
240
+ }
241
+
242
+ let content;
243
+ try {
244
+ content = fs.readFileSync(file, 'utf8');
245
+ } catch (e) {
246
+ log(`无法读取 ${file}: ${e && e.message ? e.message : e}`);
247
+ continue;
248
+ }
249
+
250
+ if (alreadyPatched(content, patch.id)) {
251
+ skipped += 1;
252
+ continue;
253
+ }
254
+
255
+ if (!content.includes(patch.find)) {
256
+ missed.push(patch.id);
257
+ log(
258
+ `patch "${patch.id}" 未命中目标片段,可能 playwright-core 版本已变化;` +
259
+ `请核对源码后更新 lib/native-mode-patch.js 中的 PATCHES 表。`
260
+ );
261
+ continue;
262
+ }
263
+
264
+ const next = content.replace(patch.find, patch.replace);
265
+ if (next === content) {
266
+ missed.push(patch.id);
267
+ continue;
268
+ }
269
+
270
+ try {
271
+ fs.writeFileSync(file, next, 'utf8');
272
+ applied += 1;
273
+ log(`已应用补丁: ${patch.id}`);
274
+ } catch (e) {
275
+ log(`写入 ${file} 失败: ${e && e.message ? e.message : e}`);
276
+ }
277
+ }
278
+
279
+ return { applied, skipped, missed };
280
+ }
281
+
282
+ module.exports = {
283
+ applyAll,
284
+ revertLegacyPatches,
285
+ PATCHES,
286
+ REVERSE_PATCHES,
287
+ SENTINEL_PREFIX,
288
+ };
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * npm `postinstall` 钩子:依赖安装完成后,立即隐藏依赖包内自带的嵌套 Skill,
6
+ * 确保对外只暴露唯一入口 `helixlife-v5-cli`,不会出现 playwright-cli /
7
+ * playwright-trace 等拆分工具项。
8
+ *
9
+ * 在 `npm install` 返回前同步执行,因此安装结束时嵌套 Skill 已不可被技能扫描器发现。
10
+ * 任何失败都不应让安装失败,故全程吞错并以 0 退出。
11
+ */
12
+
13
+ const path = require('node:path');
14
+
15
+ const skillRoot = path.join(__dirname, '..');
16
+
17
+ try {
18
+ const { hideNestedSkills } = require('./hide-nested-skills');
19
+ hideNestedSkills(skillRoot, {
20
+ log: (msg) => console.error(`[helixlife-v5-cli] ${msg}`),
21
+ });
22
+ } catch (e) {
23
+ if (process.env.HELIX_DEBUG) {
24
+ console.error(e);
25
+ }
26
+ }
27
+
28
+ try {
29
+ const { applyAll } = require('./native-mode-patch');
30
+ applyAll(skillRoot, {
31
+ log: (msg) => console.error(`[helixlife-v5-cli][native-mode-patch] ${msg}`),
32
+ });
33
+ } catch (e) {
34
+ if (process.env.HELIX_DEBUG) {
35
+ console.error(e);
36
+ }
37
+ }
38
+
39
+ process.exit(0);
@@ -0,0 +1,187 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
6
+
7
+ /** Playwright/Chromium 报 profile 被占用时的典型日志片段 */
8
+ const PROFILE_IN_USE_MARKERS = [
9
+ 'Failed to create a ProcessSingleton for your profile directory.',
10
+ 'profile is already in use',
11
+ 'Opening in existing browser session.',
12
+ 'Browser is already in use',
13
+ ];
14
+
15
+ const UNIX_LOCK_FILES = ['SingletonLock', 'SingletonCookie', 'SingletonSocket'];
16
+ const WIN_LOCK_FILES = ['lockfile'];
17
+
18
+ function lockFilesForPlatform() {
19
+ return process.platform === 'win32' ? WIN_LOCK_FILES : UNIX_LOCK_FILES;
20
+ }
21
+
22
+ function isProfileInUseError(text) {
23
+ const t = String(text || '');
24
+ return PROFILE_IN_USE_MARKERS.some((m) => t.includes(m));
25
+ }
26
+
27
+ function normalizeForMatch(dir) {
28
+ return path.normalize(dir).replace(/\\/g, '/');
29
+ }
30
+
31
+ /**
32
+ * 是否有 Chrome/Chromium 进程正在占用该 user-data-dir。
33
+ * @param {string} profileDir
34
+ * @returns {boolean}
35
+ */
36
+ function isChromeUsingProfile(profileDir) {
37
+ const needle = normalizeForMatch(profileDir);
38
+ if (process.platform === 'win32') {
39
+ const script = [
40
+ 'Get-CimInstance Win32_Process |',
41
+ "Where-Object { $_.CommandLine -and ($_.CommandLine -like '*chrome*' -or $_.CommandLine -like '*chromium*') }",
42
+ '| Select-Object -ExpandProperty CommandLine',
43
+ ].join(' ');
44
+ const result = spawnSync(
45
+ 'powershell',
46
+ ['-NoProfile', '-NonInteractive', '-Command', script],
47
+ { encoding: 'utf8', windowsHide: true },
48
+ );
49
+ const out = String(result.stdout || '');
50
+ return out.split(/\r?\n/).some((line) => {
51
+ const normalized = line.replace(/\\/g, '/');
52
+ return normalized.includes('user-data-dir') && normalized.includes(needle);
53
+ });
54
+ }
55
+
56
+ const result = spawnSync('ps', ['auxww'], { encoding: 'utf8' });
57
+ const out = String(result.stdout || '');
58
+ return out.split(/\r?\n/).some((line) => {
59
+ const lower = line.toLowerCase();
60
+ if (!lower.includes('user-data-dir')) return false;
61
+ const normalized = line.replace(/\\/g, '/');
62
+ return normalized.includes(needle);
63
+ });
64
+ }
65
+
66
+ /**
67
+ * 在无活跃 Chrome 进程时移除残留的 profile 锁文件。
68
+ * macOS 上用户手动关闭浏览器窗口后,SingletonLock 等文件常会残留,导致下次 open 失败。
69
+ * @param {string} profileDir
70
+ * @param {(msg: string) => void} [log]
71
+ * @returns {{ removed: string[], skipped?: boolean }}
72
+ */
73
+ function removeStaleProfileLocks(profileDir, log) {
74
+ if (!profileDir || !fs.existsSync(profileDir)) {
75
+ return { removed: [] };
76
+ }
77
+ if (isChromeUsingProfile(profileDir)) {
78
+ return { removed: [], skipped: true };
79
+ }
80
+
81
+ const removed = [];
82
+ for (const name of lockFilesForPlatform()) {
83
+ const file = path.join(profileDir, name);
84
+ try {
85
+ if (fs.existsSync(file)) {
86
+ fs.rmSync(file, { force: true });
87
+ removed.push(name);
88
+ }
89
+ } catch (e) {
90
+ log?.(`无法删除 profile 锁文件 ${file}: ${e.message}`);
91
+ }
92
+ }
93
+ if (removed.length > 0) {
94
+ log?.(`已清理残留 profile 锁: ${removed.join(', ')}`);
95
+ }
96
+ return { removed };
97
+ }
98
+
99
+ /**
100
+ * 从 open 命令参数中解析 --profile 目录。
101
+ * @param {string[]} tokens
102
+ * @param {(dir?: string) => string} resolveProfileDir
103
+ * @returns {string | null}
104
+ */
105
+ function extractProfileDirFromOpenArgs(tokens, resolveProfileDir) {
106
+ let i = 0;
107
+ if (tokens[i] === '--raw') i += 1;
108
+ if (tokens[i] === '--json') i += 1;
109
+ if (tokens[i] && tokens[i].startsWith('-s=')) i += 1;
110
+ if (tokens[i] !== 'open') return null;
111
+ i += 1;
112
+ for (; i < tokens.length; i += 1) {
113
+ const t = tokens[i];
114
+ if (t === '--profile') {
115
+ const next = tokens[i + 1];
116
+ if (next !== undefined) return resolveProfileDir(next);
117
+ } else if (t.startsWith('--profile=')) {
118
+ return resolveProfileDir(t.slice('--profile='.length));
119
+ } else if (t.startsWith('--') && !t.includes('=')) {
120
+ const next = tokens[i + 1];
121
+ if (next !== undefined && !next.startsWith('-')) i += 1;
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * open 前温和清理:结束可能已断开的 daemon 会话,并移除无进程占用的残留锁。
129
+ * @param {string} profileDir
130
+ * @param {(args: string[], opts?: object) => unknown} runPassthrough
131
+ * @param {(msg: string) => void} [log]
132
+ */
133
+ function prepareProfileBeforeOpen(profileDir, runPassthrough, log) {
134
+ runPassthrough(['close'], { capture: true });
135
+ removeStaleProfileLocks(profileDir, log);
136
+ }
137
+
138
+ /**
139
+ * 对带 --profile 的 open 命令做锁文件自愈:先预防性清理,失败后再 kill-all 并重试一次。
140
+ * @param {string[]} tokens
141
+ * @param {(args: string[], opts?: object) => number | { code: number, stdout: string, stderr: string }} runPassthrough
142
+ * @param {{ resolveProfileDir: (dir?: string) => string, log?: (msg: string) => void }} options
143
+ * @returns {number | { code: number, stdout: string, stderr: string }}
144
+ */
145
+ function runOpenWithProfileRecovery(tokens, runPassthrough, options) {
146
+ const profileDir = extractProfileDirFromOpenArgs(tokens, options.resolveProfileDir);
147
+ if (!profileDir) {
148
+ return runPassthrough(tokens);
149
+ }
150
+
151
+ prepareProfileBeforeOpen(profileDir, runPassthrough, options.log);
152
+
153
+ const first = runPassthrough(tokens, { capture: true });
154
+ if (first.code === 0) {
155
+ if (first.stdout) process.stdout.write(first.stdout);
156
+ if (first.stderr) process.stderr.write(first.stderr);
157
+ return 0;
158
+ }
159
+
160
+ const errText = `${first.stderr || ''}\n${first.stdout || ''}`;
161
+ if (!isProfileInUseError(errText)) {
162
+ if (first.stdout) process.stdout.write(first.stdout);
163
+ if (first.stderr) process.stderr.write(first.stderr);
164
+ return first.code;
165
+ }
166
+
167
+ options.log?.(
168
+ '[helixlife-v5-cli] 检测到 profile 被占用(常见于 macOS 手动关闭浏览器后),正在强制清理并重试 open…',
169
+ );
170
+ runPassthrough(['kill-all'], { capture: true });
171
+ removeStaleProfileLocks(profileDir, options.log);
172
+
173
+ const retry = runPassthrough(tokens, { capture: true });
174
+ if (retry.stdout) process.stdout.write(retry.stdout);
175
+ if (retry.stderr) process.stderr.write(retry.stderr);
176
+ return retry.code;
177
+ }
178
+
179
+ module.exports = {
180
+ PROFILE_IN_USE_MARKERS,
181
+ isProfileInUseError,
182
+ isChromeUsingProfile,
183
+ removeStaleProfileLocks,
184
+ extractProfileDirFromOpenArgs,
185
+ prepareProfileBeforeOpen,
186
+ runOpenWithProfileRecovery,
187
+ };
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * runtime-config
5
+ *
6
+ * 输出与 playwright-cli `--config` schema 兼容的「原生交互模式」浏览器配置片段。
7
+ * 由 `lib/visual.js` 的 `buildRuntimeBrowserConfig` 在生成 `.cli.runtime.generated.json`
8
+ * 时一并合并进 `browser` 对象,所有 open/attach 命令自动生效:
9
+ *
10
+ * - `browser.contextOptions.acceptDownloads = "internal-browser-default"`
11
+ * 不再下发 `Browser.setDownloadBehavior`,浏览器使用原生下载面板(真实文件名 + 原生图标)。
12
+ * - `browser.launchOptions.chromiumSandbox = true`
13
+ * 消除 `--no-sandbox` 黄条(Windows / macOS 上原本就该开启沙箱)。
14
+ * - `browser.launchOptions.downloadsPath = <用户主目录>/Downloads`
15
+ * 用户回退到 `acceptDownloads:"accept"` 时也能落到熟悉的下载目录。
16
+ *
17
+ * 通过环境变量 `HELIX_NATIVE_DOWNLOADS=0` 关闭原生下载(保留 Playwright `download` 事件抓取)。
18
+ * 通过 `HELIX_NATIVE_FILE_CHOOSER=0` 关闭原生上传(恢复旧的 `browser_file_upload` 工具行为)。
19
+ * 这两个开关由 `lib/native-mode-patch.js` 在 `coreBundle.js` 中读取。
20
+ *
21
+ * 另外两个环境变量在本模块内生效:
22
+ * - `HELIX_CHROMIUM_SANDBOX=0` 关闭 chromium 沙箱(仅排障,默认开启)。
23
+ * - `HELIX_HIDE_WEBDRIVER=0` 跳过 `--disable-blink-features=AutomationControlled` 注入,
24
+ * `navigator.webdriver` 将被暴露为 true(仅自我测试场景使用,正式环境会被风控判定为自动化)。
25
+ */
26
+
27
+ const os = require('node:os');
28
+ const path = require('node:path');
29
+
30
+ /** Skill 根目录(含本仓库 package.json 的目录)。 */
31
+ const SKILL_ROOT = path.join(__dirname, '..');
32
+
33
+ /**
34
+ * 解析下载默认目录:
35
+ * - 优先使用 `HELIX_DOWNLOADS_DIR` 环境变量;
36
+ * - 否则统一落在用户主目录下的 `Downloads`(Win/macOS/Linux 均一致)。
37
+ */
38
+ function resolveDownloadsPath() {
39
+ const explicit = process.env.HELIX_DOWNLOADS_DIR;
40
+ if (explicit && explicit.trim()) return explicit.trim();
41
+ return path.join(os.homedir(), 'Downloads');
42
+ }
43
+
44
+ /**
45
+ * 把原生交互模式的 launchOptions / contextOptions 合并进给定的 `browser` 对象。
46
+ * 已显式设值的字段不会被覆盖(用户/上层注入优先)。
47
+ *
48
+ * @param {Record<string, any>} browser
49
+ * @returns {Record<string, any>}
50
+ */
51
+ function applyNativeBrowserOverrides(browser) {
52
+ const next = { ...(browser || {}) };
53
+
54
+ const launchOptions = { ...(next.launchOptions || {}) };
55
+ if (launchOptions.chromiumSandbox === undefined) {
56
+ launchOptions.chromiumSandbox = process.env.HELIX_CHROMIUM_SANDBOX !== '0';
57
+ }
58
+ if (launchOptions.downloadsPath === undefined) {
59
+ launchOptions.downloadsPath = resolveDownloadsPath();
60
+ }
61
+ // 防御性显式注入 `--disable-blink-features=AutomationControlled`:
62
+ // - 与 Playwright 内部 `validateBrowserConfig` 的注入逻辑互不冲突(对方检测到任一
63
+ // `--disable-blink-features` 即跳过,不会重复注入);
64
+ // - 即便未来上游默认行为变化或本仓库回退掉 `coreBundle.js` 的相关补丁,
65
+ // `navigator.webdriver` 也始终为 undefined,避免被阿里云等图形验证码风控误判为自动化设备。
66
+ if (process.env.HELIX_HIDE_WEBDRIVER !== '0') {
67
+ const args = [...(launchOptions.args || [])];
68
+ if (!args.some((a) => String(a).includes('--disable-blink-features'))) {
69
+ args.push('--disable-blink-features=AutomationControlled');
70
+ }
71
+ launchOptions.args = args;
72
+ }
73
+ next.launchOptions = launchOptions;
74
+
75
+ const contextOptions = { ...(next.contextOptions || {}) };
76
+ const acceptDownloads =
77
+ contextOptions.acceptDownloads ?? 'internal-browser-default';
78
+ contextOptions.acceptDownloads = acceptDownloads;
79
+ next.contextOptions = contextOptions;
80
+ // launchPersistentContext 合并时 contextOptions 会 spread 到顶层;双写以防 merge 顺序问题。
81
+ next.launchOptions.acceptDownloads = acceptDownloads;
82
+
83
+ return next;
84
+ }
85
+
86
+ module.exports = {
87
+ applyNativeBrowserOverrides,
88
+ resolveDownloadsPath,
89
+ SKILL_ROOT,
90
+ };