helixlife-v5-cli 1.2.7 → 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,189 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { isHelixSiteOpen, resolveCommandName } = require('./helix-styles-guard');
7
+
8
+ /** Helix 用户配置目录名 */
9
+ const HELIX_CONFIG_DIR_NAME = 'helix';
10
+
11
+ /** 默认 token 文件名 */
12
+ const DEFAULT_TOKEN_FILE_NAME = 'token-key.txt';
13
+
14
+ const TOKEN_COOKIE_NAME = 'token';
15
+
16
+ /** open 后 cookie 可能尚未从 profile 注入,重试读取 */
17
+ const TOKEN_READ_RETRIES = 4;
18
+ const TOKEN_READ_RETRY_DELAY_MS = 600;
19
+
20
+ /**
21
+ * 解析 Helix 用户配置目录(固定路径,与 SKILL 安装位置无关)。
22
+ * 全平台统一:`$XDG_CONFIG_HOME/helix` 或 `~/.config/helix`
23
+ * (Windows 示例:`C:\Users\<你>\.config\helix`)
24
+ * @returns {string}
25
+ */
26
+ function resolveHelixConfigDir() {
27
+ const override = (process.env.HELIX_CONFIG_DIR ?? process.env.HELIX_TOKEN_DIR)?.trim();
28
+ if (override) {
29
+ if (path.isAbsolute(override)) {
30
+ return path.normalize(override);
31
+ }
32
+ return path.join(os.homedir(), override);
33
+ }
34
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
35
+ return path.join(configHome, HELIX_CONFIG_DIR_NAME);
36
+ }
37
+
38
+ /**
39
+ * 默认 token 文件绝对路径。
40
+ * @returns {string}
41
+ */
42
+ function defaultTokenFilePath() {
43
+ return path.join(resolveHelixConfigDir(), DEFAULT_TOKEN_FILE_NAME);
44
+ }
45
+
46
+ /**
47
+ * 解析 token 文件绝对路径。
48
+ * - 未设置 HELIX_TOKEN_FILE:平台默认配置目录下的 token-key.txt
49
+ * - HELIX_TOKEN_FILE 为绝对路径:原样使用
50
+ * - HELIX_TOKEN_FILE 为相对路径:基于 resolveHelixConfigDir() 解析
51
+ * @param {string | undefined} [file]
52
+ * @returns {string}
53
+ */
54
+ function resolveTokenFile(file) {
55
+ const raw = (file ?? process.env.HELIX_TOKEN_FILE)?.trim();
56
+ if (!raw) {
57
+ return defaultTokenFilePath();
58
+ }
59
+ if (path.isAbsolute(raw)) {
60
+ return path.normalize(raw);
61
+ }
62
+ return path.resolve(resolveHelixConfigDir(), raw);
63
+ }
64
+
65
+ /**
66
+ * 命令完成后是否尝试将 token 落盘。
67
+ * @param {string[]} tokens
68
+ * @returns {boolean}
69
+ */
70
+ function shouldTryPersistTokenAfterCommand(tokens) {
71
+ const cmd = resolveCommandName(tokens);
72
+ if (cmd === 'open') {
73
+ return isHelixSiteOpen(tokens);
74
+ }
75
+ // Agent 常用 snapshot 验收登录态,此时 cookie 通常已就绪
76
+ if (cmd === 'snapshot') {
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+
82
+ /**
83
+ * 从 `cookie-get token` 的 --raw 输出解析 token 值。
84
+ * @param {string} line
85
+ * @returns {string | null}
86
+ */
87
+ function parseTokenFromCookieLine(line) {
88
+ const trimmed = String(line ?? '').trim();
89
+ if (!trimmed) {
90
+ return null;
91
+ }
92
+ const match = trimmed.match(/^token=([^\s(]+)/);
93
+ return match ? match[1] : null;
94
+ }
95
+
96
+ /**
97
+ * 从当前浏览器会话读取登录 token cookie。
98
+ * @param {(args: string[], options?: { capture?: boolean }) => number | { code: number, stdout: string, stderr: string }} runPassthrough
99
+ * @returns {{ ok: true, token: string } | { ok: false, reason: string, detail?: string }}
100
+ */
101
+ function readTokenFromBrowser(runPassthrough) {
102
+ const result = runPassthrough(['--raw', 'cookie-get', TOKEN_COOKIE_NAME], { capture: true });
103
+ if (typeof result !== 'object' || result === null || !('code' in result)) {
104
+ return { ok: false, reason: 'cookie-get_unexpected_result' };
105
+ }
106
+ if (result.code !== 0) {
107
+ const detail = (result.stderr || result.stdout || '').trim();
108
+ return { ok: false, reason: 'cookie-get_failed', detail: detail || undefined };
109
+ }
110
+ const token = parseTokenFromCookieLine(result.stdout);
111
+ if (!token) {
112
+ return { ok: false, reason: 'not_logged_in' };
113
+ }
114
+ return { ok: true, token };
115
+ }
116
+
117
+ /**
118
+ * 将 token 写入 token-key.txt(或 HELIX_TOKEN_FILE 指定路径)。
119
+ * @param {string} token
120
+ * @param {string} [tokenFile]
121
+ * @returns {string} 写入文件的绝对路径
122
+ */
123
+ function writeTokenFile(token, tokenFile) {
124
+ const filePath = tokenFile ?? resolveTokenFile();
125
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
126
+ fs.writeFileSync(filePath, `${token}\n`, { encoding: 'utf8', mode: 0o600 });
127
+ if (process.platform !== 'win32') {
128
+ try {
129
+ fs.chmodSync(filePath, 0o600);
130
+ } catch {
131
+ // 权限收紧失败不阻断写入
132
+ }
133
+ }
134
+ return filePath;
135
+ }
136
+
137
+ function sleepMs(ms) {
138
+ return new Promise((resolve) => {
139
+ setTimeout(resolve, ms);
140
+ });
141
+ }
142
+
143
+ /**
144
+ * 若当前浏览器会话已登录,则将 token 落盘(含重试,应对 open 后 cookie 尚未就绪)。
145
+ * @param {(args: string[], options?: { capture?: boolean }) => number | { code: number, stdout: string, stderr: string }} runPassthrough
146
+ * @param {{ log?: (msg: string) => void, retries?: number, delayMs?: number }} [options]
147
+ * @returns {Promise<boolean>} 是否已保存
148
+ */
149
+ async function maybePersistLoginToken(runPassthrough, options = {}) {
150
+ const retries = options.retries ?? TOKEN_READ_RETRIES;
151
+ const delayMs = options.delayMs ?? TOKEN_READ_RETRY_DELAY_MS;
152
+
153
+ for (let attempt = 0; attempt < retries; attempt += 1) {
154
+ const read = readTokenFromBrowser(runPassthrough);
155
+ if (read.ok) {
156
+ const filePath = writeTokenFile(read.token);
157
+ if (options.log) {
158
+ options.log(`已登录,token 已保存:${filePath}`);
159
+ }
160
+ return true;
161
+ }
162
+ if (read.reason !== 'not_logged_in') {
163
+ if (options.log) {
164
+ options.log(`token 未保存:${read.detail || read.reason}`);
165
+ }
166
+ return false;
167
+ }
168
+ if (attempt < retries - 1) {
169
+ await sleepMs(delayMs);
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+
175
+ module.exports = {
176
+ HELIX_CONFIG_DIR_NAME,
177
+ DEFAULT_TOKEN_FILE_NAME,
178
+ TOKEN_COOKIE_NAME,
179
+ TOKEN_READ_RETRIES,
180
+ TOKEN_READ_RETRY_DELAY_MS,
181
+ resolveHelixConfigDir,
182
+ resolveTokenFile,
183
+ defaultTokenFilePath,
184
+ shouldTryPersistTokenAfterCommand,
185
+ parseTokenFromCookieLine,
186
+ readTokenFromBrowser,
187
+ writeTokenFile,
188
+ maybePersistLoginToken,
189
+ };
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { GUARD_SETUP, GUARD_TEARDOWN } = require("./automation-file-chooser-guard");
6
+
7
+ /**
8
+ * 解析 run-code 参数,读取 --filename 或内联代码。
9
+ * @param {string[]} tokens 已剥离 visual flags、保留 run-code 命令
10
+ */
11
+ function parseRunCodePayload(tokens) {
12
+ let i = 0;
13
+ if (tokens[i] === "--raw") i += 1;
14
+ if (tokens[i] === "--json") i += 1;
15
+ if (tokens[i] && tokens[i].startsWith("-s=")) i += 1;
16
+ if (tokens[i] !== "run-code") {
17
+ throw Object.assign(new Error("wrapRunCodeTokens 需要 run-code 命令"), {
18
+ code: "E_BAD_ARG",
19
+ });
20
+ }
21
+ i += 1;
22
+
23
+ let filename = null;
24
+ let code = null;
25
+ const passthroughFlags = [];
26
+ for (; i < tokens.length; i += 1) {
27
+ const t = tokens[i];
28
+ if (t === "--filename" || t === "-f") {
29
+ filename = tokens[i + 1];
30
+ i += 1;
31
+ } else if (t.startsWith("--filename=")) {
32
+ filename = t.slice("--filename=".length);
33
+ } else if (!t.startsWith("--") && code == null) {
34
+ code = t;
35
+ } else {
36
+ passthroughFlags.push(t);
37
+ if (t.startsWith("--") && !t.includes("=")) {
38
+ const next = tokens[i + 1];
39
+ if (next !== undefined && !next.startsWith("-")) {
40
+ passthroughFlags.push(next);
41
+ i += 1;
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ if (filename) {
48
+ const resolved = path.isAbsolute(filename)
49
+ ? filename
50
+ : path.resolve(process.cwd(), filename);
51
+ code = fs.readFileSync(resolved, "utf8");
52
+ }
53
+ if (!code || !String(code).trim()) {
54
+ throw Object.assign(new Error("run-code 缺少内联代码或 --filename"), {
55
+ code: "E_BAD_ARG",
56
+ });
57
+ }
58
+ return {
59
+ code: String(code).trim(),
60
+ filename,
61
+ passthroughFlags,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * 将用户 `async page => { ... }` 包装为带逐步视觉反馈的版本。
67
+ * @param {string} userSource
68
+ * @param {{ initScript: string, scriptName?: string }} meta
69
+ */
70
+ function wrapRunCodeSource(userSource, meta) {
71
+ const userFn = String(userSource).trim();
72
+ const initScriptJson = JSON.stringify(meta.initScript);
73
+ const cfgJson = JSON.stringify({
74
+ scriptName: meta.scriptName || "脚本",
75
+ });
76
+
77
+ return `async page => {
78
+ const __INIT__ = ${initScriptJson};
79
+ const __CFG__ = ${cfgJson};
80
+
81
+ await page.evaluate((src) => {
82
+ if (window.__helix_visual__) return;
83
+ const run = new Function(src);
84
+ run();
85
+ }, __INIT__);
86
+
87
+ await page.evaluate(() => {
88
+ window.__helix_visual__?.setVisualOptions({ enabled: true, persist: true });
89
+ });
90
+
91
+ const __ACTIONS__ = {
92
+ click: { pending: "正在点击", success: "已点击", tone: "pending", ok: "success" },
93
+ dblclick: { pending: "正在双击", success: "已双击", tone: "pending", ok: "success" },
94
+ hover: { pending: "正在悬停", success: "已悬停", tone: "info", ok: "info" },
95
+ fill: { pending: "正在填写", success: "已填写", tone: "pending", ok: "success" },
96
+ check: { pending: "正在勾选", success: "已勾选", tone: "pending", ok: "success" },
97
+ uncheck: { pending: "正在取消勾选", success: "已取消勾选", tone: "pending", ok: "info" },
98
+ };
99
+
100
+ const __PAGE_METHODS__ = [
101
+ "locator",
102
+ "getByRole",
103
+ "getByText",
104
+ "getByLabel",
105
+ "getByPlaceholder",
106
+ "getByAltText",
107
+ "getByTitle",
108
+ "getByTestId",
109
+ ];
110
+ const __ACT__ = [
111
+ "click",
112
+ "dblclick",
113
+ "hover",
114
+ "fill",
115
+ "check",
116
+ "uncheck",
117
+ "selectOption",
118
+ "press",
119
+ "type",
120
+ "tap",
121
+ "focus",
122
+ ];
123
+
124
+ const __describe__ = async (locator) => {
125
+ try {
126
+ return await locator.evaluate((el) => {
127
+ const t = (el.innerText || el.textContent || el.getAttribute("aria-label") || "")
128
+ .replace(/\\s+/g, " ")
129
+ .trim();
130
+ return t.slice(0, 72);
131
+ });
132
+ } catch {
133
+ return "";
134
+ }
135
+ };
136
+
137
+ const __resolveHighlight__ = async (locator) => {
138
+ try {
139
+ const row = locator.locator('xpath=ancestor::*[contains(@title,"操作区")][1]');
140
+ if ((await row.count()) > 0) return row.first();
141
+ const li = locator.locator("xpath=ancestor::li[1]/div[1]");
142
+ if ((await li.count()) > 0) return li.first();
143
+ } catch {}
144
+ return locator;
145
+ };
146
+
147
+ const __highlight__ = async (locator, args) => {
148
+ try {
149
+ const target = await __resolveHighlight__(locator);
150
+ await target.evaluate(
151
+ (el, payload) => window.__helix_visual__?.select(el, payload),
152
+ args,
153
+ );
154
+ } catch {}
155
+ };
156
+
157
+ const __wrapLoc__ = (locator, page) =>
158
+ new Proxy(locator, {
159
+ get(target, prop) {
160
+ const val = target[prop];
161
+ if (__ACT__.includes(prop) && typeof val === "function") {
162
+ return async (...args) => {
163
+ const def = __ACTIONS__[prop] || {
164
+ pending: String(prop),
165
+ success: "完成",
166
+ tone: "info",
167
+ ok: "success",
168
+ };
169
+ const desc = await __describe__(target);
170
+ await __highlight__(target, {
171
+ tone: def.tone,
172
+ label: desc || def.pending,
173
+ action: String(prop),
174
+ target: desc,
175
+ detail: def.pending + "…",
176
+ });
177
+ try {
178
+ const result = await val.apply(target, args);
179
+ await __highlight__(target, {
180
+ tone: def.ok,
181
+ label: desc || def.success,
182
+ action: String(prop),
183
+ target: desc,
184
+ detail: def.success,
185
+ });
186
+ return result;
187
+ } catch (err) {
188
+ await page.evaluate(
189
+ (payload) => window.__helix_visual__?.showHud(payload),
190
+ {
191
+ action: String(prop),
192
+ target: desc,
193
+ status: "danger",
194
+ detail: String(err && err.message ? err.message : err),
195
+ },
196
+ );
197
+ throw err;
198
+ }
199
+ };
200
+ }
201
+ if (typeof val === "function") {
202
+ return (...args) => {
203
+ const out = val.apply(target, args);
204
+ if (out && typeof out === "object" && typeof out.click === "function") {
205
+ return __wrapLoc__(out, page);
206
+ }
207
+ return out;
208
+ };
209
+ }
210
+ return val;
211
+ },
212
+ });
213
+
214
+ const __wrapPage__ = (page) =>
215
+ new Proxy(page, {
216
+ get(target, prop) {
217
+ if (prop === "__helixVisual") {
218
+ return {
219
+ select: async (locator, opts = {}) => {
220
+ await __highlight__(locator, {
221
+ tone: opts.status || "pending",
222
+ label: opts.label || opts.target || "",
223
+ action: opts.action || "步骤",
224
+ target: opts.target || opts.label || "",
225
+ detail: opts.detail || "执行中…",
226
+ });
227
+ },
228
+ hud: async (opts = {}) => {
229
+ await page.evaluate(
230
+ (payload) => window.__helix_visual__?.showHud(payload),
231
+ opts,
232
+ );
233
+ },
234
+ cleanup: async () => {
235
+ await page.evaluate(() => window.__helix_visual__?.cleanup());
236
+ },
237
+ };
238
+ }
239
+ const val = target[prop];
240
+ if (__PAGE_METHODS__.includes(prop) && typeof val === "function") {
241
+ return (...args) => __wrapLoc__(val.apply(target, args), target);
242
+ }
243
+ if (typeof val === "function") return val.bind(target);
244
+ return val;
245
+ },
246
+ });
247
+
248
+ await page.evaluate(
249
+ (cfg) =>
250
+ window.__helix_visual__?.showHud({
251
+ action: "执行脚本",
252
+ target: cfg.scriptName,
253
+ status: "pending",
254
+ detail: "执行中…",
255
+ }),
256
+ __CFG__,
257
+ );
258
+
259
+ const __userFn = ${userFn};
260
+ try {${GUARD_SETUP}
261
+ const result = await __userFn(__wrapPage__(page));
262
+ await page.evaluate(
263
+ (cfg) =>
264
+ window.__helix_visual__?.showHud({
265
+ action: "执行脚本",
266
+ target: cfg.scriptName,
267
+ status: "success",
268
+ detail: "完成",
269
+ }),
270
+ __CFG__,
271
+ );
272
+ return result;
273
+ } catch (err) {
274
+ await page.evaluate(
275
+ (payload) => window.__helix_visual__?.showHud(payload),
276
+ {
277
+ action: "执行脚本",
278
+ target: __CFG__.scriptName,
279
+ status: "danger",
280
+ detail: String(err && err.message ? err.message : err),
281
+ },
282
+ );
283
+ throw err;
284
+ } finally {${GUARD_TEARDOWN}
285
+ await page.evaluate(() => {
286
+ window.__helix_visual__?.setVisualOptions({ persist: false });
287
+ });
288
+ }
289
+ `;
290
+ }
291
+
292
+ /**
293
+ * 将 run-code tokens 替换为注入视觉反馈的内联脚本。
294
+ * @param {string[]} tokens
295
+ * @param {string} initScript
296
+ */
297
+ function wrapRunCodeTokens(tokens, initScript) {
298
+ const { code, filename, passthroughFlags } = parseRunCodePayload(tokens);
299
+ const scriptName = filename ? path.basename(filename, path.extname(filename)) : "脚本";
300
+ const wrapped = wrapRunCodeSource(code, {
301
+ initScript,
302
+ scriptName,
303
+ });
304
+ return ["run-code", wrapped, ...passthroughFlags];
305
+ }
306
+
307
+ module.exports = {
308
+ parseRunCodePayload,
309
+ wrapRunCodeSource,
310
+ wrapRunCodeTokens,
311
+ };