helixlife-v5-cli 1.1.5 → 1.1.7

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.js CHANGED
@@ -205,15 +205,8 @@ main.printHelp = printHelp;
205
205
  main.runPlaywrightCliPassthrough = runPlaywrightCliPassthrough;
206
206
  main.playwrightCliEntry = playwrightCliEntry;
207
207
 
208
- if (require.main === module) {
209
- main(process.argv).then(
210
- (code) => process.exit(code),
211
- (err) => {
212
- console.error(err);
213
- process.exit(2);
214
- }
215
- );
216
- }
208
+ // 注意:`require.main === module` 入口守卫统一放在 helix-cli.js;
209
+ // 本文件仅导出,不在此处重复启动。
217
210
 
218
211
  module.exports = {
219
212
  main,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixlife-v5-cli",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Helix(vip.helixlife.cn)精细化浏览器自动化命令行工具。",
5
5
  "main": "helix-cli.js",
6
6
  "bin": {
@@ -41,8 +41,8 @@
41
41
  - **悬停与点击 ·「部分章节匹配」等说明标签**:搜索等列表卡片上常有一行灰色小字(典型文案「部分章节匹配」)。**分流规则**:
42
42
  - **仅展示 / 阅读意图 → `hover`**:用户说「查看 / 看一下 / 想看 / 悬停 / 指到 … 上」等,且对象是该说明标签本身 → 使用 `helixlife-v5-cli hover …` 或 `run-code` 内 `locator(...).hover()`;勿用 `click` 代替。
43
43
  - **明确激活 / 导航意图 → `click`**:用户说「进入 / 打开 / 点进去 / 点击」等 → `click` 应落在**卡片主点击区**(封面、标题等);若用户点名要「点击部分章节匹配」才把 `click` 落在该文案上。
44
- - **定位与多命中**:推荐 `page.locator('main').getByText('部分章节匹配', { exact: true })`;多卡并列时**必须先缩小到目标课程卡片**,避免 strict 或误触。
45
- - 自测(2026-05-11):`/edu/search?keywords=…` 课程结果首条 `hover` 后,可见 tooltip 浮层(`helixlife-v5-cli run-code --filename=scripts/hover-partial-chapter-match.js` 返回 `tooltipLikeCount 1`)。
44
+ - **定位与多命中**:推荐 `page.locator('main').getByText('部分章节匹配', { exact: true })`;多卡并列时**必须先缩小到目标课程卡片**,避免 strict 或误触(`scripts/hover-partial-chapter-match.js` 的 `cardTitle` 参数可直接用于此场景)。
45
+ - 自测(2026-05-11):`/edu/search?keywords=…` 课程结果首条 `hover` 后,可见 tooltip 浮层(`helixlife-v5-cli run-code --filename=scripts/hover-partial-chapter-match.js` 返回 `ok: true`,即 `tooltipLikeAfter > tooltipLikeBefore`)。
46
46
  - **数据分析 · 详情页 · 上传文件**:用户说「上传文件 / 上传数据 / 换数据文件 / 导入 csv xlsx」等,且 `pathname` 为 `/analysis/{toolId}` 时,**一律走 D-2 三步弹窗流程**(打开弹窗 → 弹窗内选文件 → 弹窗内「确认」),**禁止**把左侧参数卡片底部「确认」(D-6 生成结果)当成上传确认。细则见下文「上传文件 · Agent 硬门禁」。
47
47
 
48
48
  #### 任务范围与跨域
@@ -507,7 +507,7 @@ div.side-menu--item[to='/edu/courses']
507
507
  | --- | --- |
508
508
  | 参数行 | 每行左侧为参数名(如 `数据矩阵`、`样本信息`),右侧为上传入口 + 当前文件名 + 「下载示例数据」 |
509
509
  | **步骤 1 · 打开弹窗** | 点该行 **`aria-label="上传文件 - 打开上传弹窗"`** 的控件(规约 A)。多行并存时**必须先按参数名缩到单行**,再点该行上传入口 |
510
- | **步骤 2 · 弹窗内选文件** | 出现 `getByRole('dialog', { name: '上传文件' })` 后,在**弹窗内**点 `button`,文案匹配 `/将文件拖到此处\|点击上传/`,配合 `filechooser.setFiles(<本地绝对路径>)` |
510
+ | **步骤 2 · 弹窗内选文件** | 出现 `getByRole('dialog', { name: '上传文件' })` 后,**任选其一**:**(A) filechooser 方式**:在**弹窗内**点 `button`,文案匹配 `/将文件拖到此处\|点击上传/`,配合 `filechooser.setFiles(<本地绝对路径>)`;**(B) setInputFiles 方式**(更简洁,推荐多文件脚本使用):直接 `dlg.locator('input[type="file"]').setInputFiles(<本地绝对路径>)`,无需等待 filechooser 事件 |
511
511
  | **步骤 3 · 弹窗内确认** | 仍在**同一弹窗**内 `click` → `getByRole('button', { name: '确认' })`;弹窗关闭后,对应参数行文件名更新 |
512
512
 
513
513
  **两个「确认」勿混淆**
@@ -535,9 +535,11 @@ div.side-menu--item[to='/edu/courses']
535
535
 
536
536
  3. **弹窗**:`page.getByRole('dialog', { name: '上传文件' })`(须 `waitFor({ state: 'visible' })`)
537
537
 
538
- 4. **弹窗内选文件**:`dlg.getByRole('button', { name: /将文件拖到此处\|点击上传/ })`
538
+ 4. **弹窗内选文件(A · filechooser)**:`dlg.getByRole('button', { name: /将文件拖到此处\|点击上传/ })`,配合 `page.waitForEvent('filechooser')` → `chooser.setFiles(<路径>)`
539
539
 
540
- 5. **弹窗内确认**:`dlg.getByRole('button', { name: '确认' })`
540
+ 5. **弹窗内选文件(B · setInputFiles,更简洁)**:`dlg.locator('input[type="file"]').setInputFiles(<路径>)`(无需 filechooser 事件;`scripts/analysis-detail-upload-both.js` 采用此方式)
541
+
542
+ 6. **弹窗内确认**:`dlg.getByRole('button', { name: '确认' })`
541
543
 
542
544
  **`helixlife-v5-cli` 逐步命令示例**(单文件 · 参数名 `数据矩阵` · 文件路径按任务替换):
543
545
 
@@ -559,7 +561,7 @@ helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-file.js
559
561
  - **禁止**用整页 `getByLabel('上传文件')` 当唯一定位器——双文件工具会 strict 或多点;必须带参数名 `filter({ has: getByText('<参数名>') })`。
560
562
  - **禁止**依赖某次 `snapshot` 的 `e*` ref 或肉眼猜 `img` 位置。
561
563
 
562
- **多文件工具**(如差异分析:数据矩阵 + 样本信息):对每个参数**重复完整 3 步**;参考 `scripts/analysis-detail-upload-both.js`(顺序执行,勿并行多个 `filechooser`)。
564
+ **多文件工具**(如差异分析:数据矩阵 + 样本信息):对每个参数**重复完整 3 步**;参考 `scripts/analysis-detail-upload-both.js`(采用 setInputFiles 方式顺序执行,勿并行多个 `filechooser`)。
563
565
 
564
566
  **验收(对用户回复前必审)**
565
567
 
@@ -573,7 +575,7 @@ helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-file.js
573
575
  | # | 用户语义 | 推荐操作 | 验收 / 备注 |
574
576
  | --- | --- | --- | --- |
575
577
  | D-1 | **数据参数 · 重置** | `click` → `locator('main').getByRole('button', { name: '重置参数' }).first()` | 数据参数区恢复默认;可能清除已上传文件名 |
576
- | D-2 | **上传文件** | **三步弹窗**(细则见上文「上传文件 · Agent 硬门禁」):① `click` → 目标参数行 `getByLabel('上传文件 - 打开上传弹窗')`(兼容旧版:同行 `getByLabel('上传文件')`)→ ② 等待 `dialog "上传文件"` → 弹窗内 `click` → `button` `/将文件拖到此处\|点击上传/` + `filechooser.setFiles(<本地绝对路径>)` → ③ 弹窗内 `click` → `button`「确认」。推荐 `run-code --filename=scripts/analysis-detail-upload-file.js`(单文件)或 `analysis-detail-upload-both.js`(多参数)。**反模式**:勿 `upload` 未开弹窗;勿点左侧底部「确认」代替弹窗「确认」 | 弹窗关闭;对应参数行展示新文件名;格式须符合工具模板 |
578
+ | D-2 | **上传文件** | **三步弹窗**(细则见上文「上传文件 · Agent 硬门禁」):① `click` → 目标参数行 `getByLabel('上传文件 - 打开上传弹窗')`(兼容旧版:同行 `getByLabel('上传文件')`)→ ② 等待 `dialog "上传文件"` → 弹窗内选文件:**A** `click` → `button` `/将文件拖到此处\|点击上传/` + `filechooser.setFiles(<路径>)`;或 **B**(更简洁)`dlg.locator('input[type="file"]').setInputFiles(<路径>)` → ③ 弹窗内 `click` → `button`「确认」。推荐 `run-code --filename=scripts/analysis-detail-upload-file.js`(单文件·A 方式)或 `analysis-detail-upload-both.js`(多参数·B 方式)。**反模式**:勿 `upload` 未开弹窗;勿点左侧底部「确认」代替弹窗「确认」 | 弹窗关闭;对应参数行展示新文件名;格式须符合工具模板 |
577
579
  | D-3 | **数据校验(验证)** | 若 `main` 出现 `getByRole('button', { name: '验证' })`:`click` 一次;若已为 `验证成功` 可跳过 | `验证成功` → 可 D-6;`验证失败` → 调整文件后重试 D-2~D-3 |
578
580
  | D-4 | **主要参数 · 重置** | `click` → `locator('main').getByRole('button', { name: '重置参数' }).nth(1)` | 主要参数折叠组恢复默认 |
579
581
  | D-5 | **主要参数 · 保存** | `click` → `locator('main').getByRole('button', { name: '保存参数' })` | 页面级 toast `保存成功`;`pathname` 不变 |
@@ -593,7 +595,8 @@ helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-file.js
593
595
  | 打开上传弹窗(首选) | `locator('main .u-filter-card').getByLabel('上传文件 - 打开上传弹窗')`;多参数时 `getByText('<参数名>', { exact: true }).locator('..').getByLabel('上传文件 - 打开上传弹窗')` |
594
596
  | 打开上传弹窗(旧版兼容) | 同上结构,`.or(getByLabel('上传文件'))` 或直接用同行 `getByLabel('上传文件')` |
595
597
  | 上传弹窗 | `getByRole('dialog', { name: '上传文件' })` |
596
- | 弹窗 · 选文件 | `getByRole('dialog', { name: '上传文件' }).getByRole('button', { name: /将文件拖到此处\|点击上传/ })` |
598
+ | 弹窗 · 选文件(A) | `getByRole('dialog', { name: '上传文件' }).getByRole('button', { name: /将文件拖到此处\|点击上传/ })` + filechooser |
599
+ | 弹窗 · 选文件(B) | `getByRole('dialog', { name: '上传文件' }).locator('input[type="file"]').setInputFiles(<路径>)`(更简洁) |
597
600
  | 弹窗 · 确认(上传落盘) | `getByRole('dialog', { name: '上传文件' }).getByRole('button', { name: '确认' })` |
598
601
  | 左侧底部 · 确认(生成结果,非上传) | `locator('main .u-filter-card').getByRole('button', { name: '确认' })` |
599
602
  | 手动验证 | `locator('main').getByRole('button', { name: '验证' })` |
@@ -605,8 +608,8 @@ helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-file.js
605
608
 
606
609
  ###### 可复用 `run-code`(可选)
607
610
 
608
- - **单参数上传并尝试验证**:`helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-file.js`(三步弹窗;脚本内改 `filePath` 与可选 `paramLabel`;格式须符合工具模板,大小以页面上限为准)。
609
- - **多参数顺序上传**(如差异分析):`helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-both.js`(每个参数各走一遍打开弹窗 → 选文件 → 弹窗确认)。
611
+ - **单参数上传并尝试验证**:`helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-file.js`(三步弹窗,采用 **filechooser** 方式;脚本内改 `filePath` 与可选 `paramLabel`;格式须符合工具模板,大小以页面上限为准)。
612
+ - **多参数顺序上传**(如差异分析):`helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-both.js`(采用 **setInputFiles** 方式,每个参数各走一遍打开弹窗 → 选文件 → 弹窗确认;无需 filechooser 事件)。
610
613
 
611
614
  ##### 数据分析 · 分类体系(主分类 vs 子分类 · Agent 必读)
612
615
 
@@ -1177,7 +1180,7 @@ async page => {
1177
1180
  2. **验收**:出现**新标签**,URL 为 `https://vip.helixlife.cn/edu/search?keywords=…`;`title` 为 `搜索-…`。继续操作需 `tab-list` → `tab-select <新索引>`。
1178
1181
  3. **同关键字下切结果类型**:在 `/edu/search` 内用 `main` 作用域 `click` → `main.getByText('课程', { exact: true })` / `main.getByText('直播', { exact: true })`(推荐 `run-code`)。**勿**为此 `click` → `div.side-menu--item[to='/edu/courses']` 或走 S-14。验收:URL 仍 `/edu/search?keywords=…`,常带 `activeTab=course`/`activeTab=live`;「共找到 * 个相关内容」随 Tab 更新。
1179
1182
  4. **收尾**(可选):`tab-close` → `tab-select 0` 回学习中心。
1180
- 5. **结果卡片 · 匹配说明(如「部分章节匹配」)**:按用户动词选 `hover` 或 `click`(细则见用户说法映射)。查看类 → `hover`(例:`run-code` 内 `await page.locator('main').getByText('部分章节匹配', { exact: true }).first().hover()`)。进入/点击类 → `click` 卡片主区或用户点名的控件。复现脚本:`helixlife-v5-cli run-code --filename=scripts/hover-partial-chapter-match.js`。
1183
+ 5. **结果卡片 · 匹配说明(如「部分章节匹配」)**:按用户动词选 `hover` 或 `click`(细则见用户说法映射)。查看类 → `hover`(例:`run-code` 内 `await page.locator('main').getByText('部分章节匹配', { exact: true }).first().hover()`)。进入/点击类 → `click` 卡片主区或用户点名的控件。复现脚本:`helixlife-v5-cli run-code --filename=scripts/hover-partial-chapter-match.js`(多卡并存时可在脚本顶部设 `cardTitle` 缩小到目标课程卡片)。
1181
1184
 
1182
1185
  #### S-19:学习中心 · 课程详情页与进入播放
1183
1186
 
@@ -0,0 +1,49 @@
1
+ /**
2
+ * 搜索页 · 结果卡片「部分章节匹配」等说明标签 hover 验证。
3
+ * 在 /edu/search?keywords=… 页面,对目标课程卡片上的说明文案执行 hover,
4
+ * 然后检查是否出现 tooltip 类浮层。
5
+ *
6
+ * 可选:在脚本顶部设置 cardTitle 以缩小到特定课程卡片(多卡场景);
7
+ * 留空则取首条含「部分章节匹配」的卡片。
8
+ *
9
+ * 运行:helixlife-v5-cli run-code --filename=scripts/hover-partial-chapter-match.js
10
+ */
11
+ async page => {
12
+ const cardTitle = null; // 如 '科研选题三十六策';null 表示取首条
13
+ const main = page.locator('main');
14
+
15
+ // 确定目标卡片作用域:用 exact 匹配标题文本,再向上一层到卡片根节点
16
+ // (避免 hasText 子串匹配命中外层容器或无关 div)
17
+ const scope = cardTitle
18
+ ? main.getByText(cardTitle, { exact: true }).first().locator('..')
19
+ : main;
20
+
21
+ // 定位目标卡片中的「部分章节匹配」文案
22
+ const matchLabel = scope.getByText('部分章节匹配', { exact: true }).first();
23
+ if ((await matchLabel.count()) === 0) {
24
+ return {
25
+ ok: false,
26
+ reason: cardTitle
27
+ ? `未在卡片「${cardTitle}」中找到「部分章节匹配」文案`
28
+ : '未找到「部分章节匹配」文案,可能当前搜索结果不含该标签',
29
+ };
30
+ }
31
+
32
+ // 仅使用语义明确的 tooltip 选择器,避免 [class*="tooltip"] 假阳性
33
+ const tooltipSelector = page.locator('[role="tooltip"], .ant-tooltip:not(.ant-tooltip-hidden)');
34
+
35
+ const beforeCount = await tooltipSelector.count();
36
+
37
+ await matchLabel.hover();
38
+ await page.waitForTimeout(1000);
39
+
40
+ const afterCount = await tooltipSelector.count();
41
+
42
+ return {
43
+ ok: afterCount > beforeCount,
44
+ tooltipLikeBefore: beforeCount,
45
+ tooltipLikeAfter: afterCount,
46
+ labelVisible: await matchLabel.isVisible(),
47
+ cardTitle: cardTitle || '(首条)',
48
+ };
49
+ }