helixlife-v5-cli 1.1.3

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,1205 @@
1
+ ## Helix 站点专用操作(https://vip.helixlife.cn)
2
+
3
+ > **权威说明(Agent 必读)**:本文件是 Helix 精细化 UI 工作流(任务范围、会话与导航、定位器与验收、弹窗与安全、站点入口与定位器维护)的**唯一权威来源**。执行任何 `vip.helixlife.cn` 相关自动化前须完整阅读。
4
+ >
5
+ > - **命令与参数**:见 Cursor skill `helixlife-v5-cli`(`.cursor/skills/helixlife-v5-cli/SKILL.md`,通常不随 npm 包发布)。
6
+ > - **本文路径**:工作区 `references/`,或包内 `node_modules/helixlife-v5-cli/references/`。
7
+ > - **快捷**:需要「打开有头会话并进入 HELIX 首页」时使用 `helixlife-v5-cli helix home`(仍受下文「仅在必要时启动」与 `list` 复用规则约束)。
8
+
9
+ ### 本文档与「快照 ref」的关系(重要)
10
+
11
+ - 相同功能优先复用本文**稳定定位器**(`getByRole` / `getByText` / 固定 `href` 等),**不依赖** 每次 `snapshot` 产生的 `e15` 等临时 ref。
12
+ - `helixlife-v5-cli snapshot` 仅用于 **验收** 与 **排障**;发现 UI 变更时,用 `generate-locator <ref> --raw` 反推新策略,并**回写本文件**。
13
+
14
+ ---
15
+
16
+ ### Helix 精细化任务规则(Agent 必读)
17
+
18
+ #### PDF 页码跳转 · Agent 硬门禁(对用户回复前必审)
19
+
20
+ **适用范围**:用户要求「切换到 PDF 第 N 页 / 翻到第 N 页 / 去第 N 页」等,且当前为学习中心播放页 **Chrome 内置 PDF**(见 S-25)。
21
+
22
+ - **流程唯一性**:先满足 S-25 前置(S-23 已切到「课件」、课件为 PDF、阅读器已加载),再在 **`fr`** 上完成 `getByLabel('页码')` 三击 → `fill(String(N))` → `press('Enter')` 一次(或等价 `run-code --filename=scripts/chrome-pdf-goto-page.js`)。
23
+ - **禁止的捷径**:禁止在 `vip.helixlife.cn` 父页改写业务 `iframe` 的 `src` 或拼接 `#page=N` 作为跳页主手段并据此结束任务,此类**不视为**已通过 S-25。
24
+ - **对用户回复的门禁**:在 `await fr.getByLabel('页码').inputValue()` 严格等于 `String(N)` 之前,禁止向用户输出「已跳到 / 已切换到 / 已完成第 N 页」等成功表述。验收未通过时如实说明未完成,给出读到的 `inputValue()` 或脚本抛错摘要、已执行步骤与下一步建议。
25
+ - **禁止误导性归因**:不得以「跨域 iframe / 同源策略导致父页脚本无法访问子 frame」作为**无法自动化跳页**的结论。父页 JS 同源限制不等于 Playwright 无法在 `fr` 上操作阅读器工具栏。若仍失败应报告**未找到 `fr`、定位超时、语言包下 `aria-label` 变更**等可核验原因,并指向 S-25 与 `scripts/chrome-pdf-enumerate-a11y.js`。
26
+
27
+ #### 用户说法映射
28
+
29
+ - **深浅色**:用户说「切换颜色 / 换颜色 / 深色浅色 / 外观主题」等,已登录壳下**一律视为切换深浅色模式**(深色 ⇄ 浅色),见 S-16。
30
+ - **个人中心**:用户说「个人中心 / 我的账号 / 账号中心 / 账户设置」等账号资料/绑定/安全相关说法,已登录壳下**一律走头像菜单的「设置」**(见 S-26)。侧栏头像菜单**不出现"个人中心"四字**。
31
+ - **直播分类(必辨路由)**:
32
+ - `pathname` 为 `/edu/search`(顶栏搜索打开的页面):仅在 `main` 内点 `getByText('直播')`(与"课程"并列的结果 Tab),见 S-18 第 3 步;URL 常追加 `activeTab=live`。**禁止**改点侧栏 `to='/edu/courses'` 或套用 S-14。
33
+ - `pathname` 为 `/edu/courses` 或其它学习中心列表(非 `/edu/search`):「直播」指 S-14 顶栏「直播」 + 子类 → `/edu/lives?category=…`。
34
+ - **播放页内切节(同课程:视频 / PDF / 作业等)**:用户说「下一个作业 / 下一节 / 下一份 PDF / 下一套小测」等,意图为**同一门课、同一大纲内**换到另一节时:
35
+ 1. 先用 `eval`(如 `location.pathname`)或 `snapshot` 确认当前路由。
36
+ 2. 若已在 `/edu/study/courses/{courseId}`:**必须优先**在本页 `main` 左侧「目录」(必要时先 `click` → `getByText('查看全部')` 展开)中,用 `getByText('<节完整标题>', { exact: true })` 直接 `click` 切节。视频、PDF、作业/小测等**均在本页完成**。
37
+ 3. **反模式(禁止)**:不要为了切节而先 `goto`、先点面包屑回到 `/edu/courses/{courseId}/learn`,再在详情里找入口或点「继续学习」——属于多余往返。
38
+ 4. 若 `eval` 显示当前**不在** `/edu/study/...`:再按 S-19 用 `getByRole('button', { name: '继续学习' })` 等进入,进入后仍在**目录**中点选目标节。
39
+ - **PDF 阅读器页码(防「多翻一页」)**:用户说「切换到第 N 页」等时,**一律指阿拉伯数字 N**(从 1 起,与目录/印刷页码一致)。禁止按数组下标填 N-1 或误填 N+1。常见误操作:跳转生效后再点「下一页」或按 `ArrowRight` / `PageDown` 做"确认"——会把当前页再推进 1。**输入规范**:对 `fr.getByLabel('页码')` 先 `click({ clickCount: 3 })` 全选,再 `fill(String(N))`,再 `press('Enter')` 一次;**验收**:同一控件 `inputValue()` 须等于 `String(N)`(见 S-25 与 `scripts/chrome-pdf-goto-page.js`)。自测(2026-05-11):真实课件 Tab / 简体中文 Chrome 下 `target=5` 返回 `inputValue: "5"`。
40
+ - **悬停与点击 ·「部分章节匹配」等说明标签**:搜索等列表卡片上常有一行灰色小字(典型文案「部分章节匹配」)。**分流规则**:
41
+ - **仅展示 / 阅读意图 → `hover`**:用户说「查看 / 看一下 / 想看 / 悬停 / 指到 … 上」等,且对象是该说明标签本身 → 使用 `helixlife-v5-cli hover …` 或 `run-code` 内 `locator(...).hover()`;勿用 `click` 代替。
42
+ - **明确激活 / 导航意图 → `click`**:用户说「进入 / 打开 / 点进去 / 点击」等 → `click` 应落在**卡片主点击区**(封面、标题等);若用户点名要「点击部分章节匹配」才把 `click` 落在该文案上。
43
+ - **定位与多命中**:推荐 `page.locator('main').getByText('部分章节匹配', { exact: true })`;多卡并列时**必须先缩小到目标课程卡片**,避免 strict 或误触。
44
+ - 自测(2026-05-11):`/edu/search?keywords=…` 课程结果首条 `hover` 后,可见 tooltip 浮层(`helixlife-v5-cli run-code --filename=scripts/hover-partial-chapter-match.js` 返回 `tooltipLikeCount ≥ 1`)。
45
+ - **数据分析 · 详情页 · 上传文件**:用户说「上传文件 / 上传数据 / 换数据文件 / 导入 csv xlsx」等,且 `pathname` 为 `/analysis/{toolId}` 时,**一律走 D-2 三步弹窗流程**(打开弹窗 → 弹窗内选文件 → 弹窗内「确认」),**禁止**把左侧参数卡片底部「确认」(D-6 生成结果)当成上传确认。细则见下文「上传文件 · Agent 硬门禁」。
46
+
47
+ #### 任务范围与跨域
48
+
49
+ - 仅处理 `vip.helixlife.cn` 域内的"精细化 UI 工作流"(登录入口定位、表单填写、弹窗确认、页面状态校验、进入页面模块等)。
50
+ - 所有"跳转/切换"优先通过"页面内点击/填写"完成;除"启动会话/导航到首页"外,禁止用 `helixlife-v5-cli goto` 或 `helixlife-v5-cli open` 直达目标子路径。
51
+ - **跨域**:营销首页「登录 / 立即体验」进入 `passport.helixlife.cn`;「回旧版」进入 `www.helixlife.cn`。各域内的稳定操作见下文。
52
+
53
+ #### 会话与导航策略
54
+
55
+ - **何时启动浏览器**:仅当"需要启动会话/导航到首页"时启动;命令固定为 `helixlife-v5-cli open https://vip.helixlife.cn/ --profile=.helixlife-profile --headed`(如使用其它 profile 名以实际为准;须把首页 URL 与 `open` 写在同一条命令里,避免先空白页再 `goto` 引发首屏样式竞态)。
56
+ - **"开始会话/打开网站"类请求**:先 `helixlife-v5-cli list` 找 `status: open` 且 `headed: true` 的会话;当前 URL 等价于首页则直接复用,否则尝试页面内操作回到所需起点;若页面内无法回到,停止并向用户求助,**不要**直接 `open` / `goto`。
57
+ - **清理策略(禁止一刀切先关再开)**:可操作会话必须优先复用;仅当会话不可操作(命令报错 / 无响应 / 明显卡住)或用户要求"全新会话"时清理,先温和(`close` / `-s=<name> close` / `close-all`)后强制(`kill-all`),清理后再 `open … --profile=.helixlife-profile --headed` 进首页。
58
+
59
+ #### 定位器与交互要求
60
+
61
+ - **首选**:本文「稳定定位器速查」与「标准操作流程(S-xx)」中的字符串。若站点后续增加 `data-testid`,应在对应控件上优先采用并更新本文。
62
+ - 任何"关键字段填写/提交"后都应立即 `snapshot`(每个关键步骤只 snapshot 一次),直到页面出现下一步应有的 UI 状态。
63
+ - 验收失败:依据 snapshot 回退到上一步定位并修正(仅重试一次,避免死循环)。
64
+
65
+ #### 页面状态验收
66
+
67
+ - 一个步骤完成的标准不是"我点了按钮",而是页面出现了可验证的结果(URL、`document.title`、关键文案或模块标题出现)。
68
+ - 验收方式优先级:
69
+ 1. 本文给出的**验收条件**
70
+ 2. `snapshot` 中的目标元素/文本
71
+ 3. `eval` 读取关键 DOM(如 `location.href`、`document.title`)
72
+ - **特例 · PDF 阅读器第 N 页**:整页 `snapshot` 常无法暴露阅读器工具栏 ref,不得以"已执行某命令"代替验收。须按 S-25 在 `fr` 上 `getByLabel('页码').inputValue() === String(N)` 后方可作成功表述;细则见上文「PDF 页码跳转 · Agent 硬门禁」。
73
+
74
+ #### 弹窗/对话框处理
75
+
76
+ - **原生 `window.alert` / `confirm` / `prompt`**:使用 `helixlife-v5-cli dialog-accept` / `dialog-dismiss`(必要时带确认文案参数)。
77
+ - **通行证「条款确认」层(页面内 Modal,非原生对话框)**:表现为 `role="dialog"`,标题「请阅读并同意以下条款」,主按钮「同意并继续」,次按钮「取 消」(含空格,用 `getByRole('button', { name: '取 消' })`)。**禁止**对其使用 `dialog-accept` / `dialog-dismiss`。
78
+ - **触发条件**(2026-05-12 自测,`passport.helixlife.cn/ai/login`):
79
+ - **账号登录** Tab:已填手机号/邮箱与密码,**未**勾选底部「同意」时点击「登录」→ 弹出。
80
+ - **验证码登录** Tab:已填手机号,**未**勾选底部「同意」时点击「获取验证码」→ 弹出**同一套** dialog。
81
+ - **默认策略**:用户未明确要求取消时,执行 `click` → `getByRole('button', { name: '同意并继续' })`;关闭后底部「同意」checkbox 通常变为已勾选,继续原动作。仅当用户明示取消时,改用「取 消」。
82
+ - **验收**:`snapshot` 中该 `dialog` 消失。
83
+ - 出现不可预期弹窗(验证码/风控/扫码登录等):停止并向用户求助。
84
+
85
+ #### 敏感信息与安全
86
+
87
+ - 不要在版本库文档(含本文件与 `.cursor/skills/helixlife-v5-cli/SKILL.md`)中硬编码账号密码或 token。
88
+ - 若流程需要账号/密码:由用户在对话中提供后再用 `helixlife-v5-cli fill ...` 输入。
89
+ - 不要把会话导出的 cookie/存储状态上传到不受控位置。
90
+
91
+ ---
92
+
93
+ ### 站点域与信息架构(固化)
94
+
95
+ | 域 | 用途 | 典型入口 |
96
+ | --- | --- | --- |
97
+ | `vip.helixlife.cn` | 解螺旋 5.0 营销落地页 | `helixlife-v5-cli helix home` |
98
+ | `passport.helixlife.cn` | 统一登录/注册(账号、验证码、微信扫码等) | 营销页「登录」「立即体验」 |
99
+ | `www.helixlife.cn` | 旧版站点(本文仅记录入口) | 营销页「回旧版」→ `https://www.helixlife.cn/` |
100
+ | `vip.helixlife.cn/agreement/*` | 5.0 侧协议 HTML | 见下文「协议与备案链接」 |
101
+
102
+ ---
103
+
104
+ ### 稳定定位器速查(Playwright 风格)
105
+
106
+ #### 营销首页 `https://vip.helixlife.cn/`
107
+
108
+ | 控件 | 推荐定位器 | 备注 |
109
+ | --- | --- | --- |
110
+ | 顶栏 Logo(图) | `getByRole('img', { name: '解螺旋-AI驱动科学创新' })` | 可点击区以 snapshot 为准 |
111
+ | 回旧版 | `getByRole('button', { name: '回旧版' })` | 跳转 `https://www.helixlife.cn/` |
112
+ | 登录 | `getByRole('button', { name: '登录' })` | 主路径。DOM 中可能出现多枚同文案按钮(吸顶/响应式重复);遇 strict violation 时用 `header getByRole('button', { name: '登录' }).first()` 等缩小作用域 |
113
+ | 首屏 CTA | `getByText('立即体验')` | 可选/以线上为准:2026-05-07 实测无该文案,未登录请只用「登录」 |
114
+
115
+ #### 通行证登录页 `passport.helixlife.cn/ai/login`
116
+
117
+ | 控件 | 推荐定位器 | 备注 |
118
+ | --- | --- | --- |
119
+ | 账号登录 Tab | `getByText('账号登录')` | |
120
+ | 验证码登录 Tab | `getByText('验证码登录')` | URL 会出现 `type=code` |
121
+ | 手机号/邮箱 | `getByRole('textbox', { name: '请输入手机号或邮箱' })` | 账号登录 |
122
+ | 密码 | `getByRole('textbox', { name: '请输入密码' })` | |
123
+ | 记住密码 | `getByRole('checkbox').first()` | 仅在「账号登录」Tab 且存在「记住密码」时出现 |
124
+ | 同意协议(底部) | `getByRole('checkbox').last()` | 「账号登录」下通常为第 2 个 checkbox;若页面仅有 1 个 checkbox,则 `last()` 与 `first()` 同指「同意」。改版后请以 `eval` 统计 `input[type=checkbox]` 数量校验 |
125
+ | 提交登录 | `getByRole('button', { name: '登录' })` | **账号 Tab**:未填账号/密码时多为 disabled;两者皆填后即使未勾「同意」也常可点,此时点击会弹**条款确认 dialog**。失败后可能出现「您还有 N 次尝试机会」(凭据/风控),勿高频重试;常见密码框被清空、按钮重回 disabled,须重新填密码再提交(见 S-03) |
126
+ | 获取验证码 | `getByText('获取验证码')` | 验证码登录;未勾「同意」即点击会弹同一**条款确认 dialog** |
127
+ | 手机号(验证码) | `getByRole('textbox', { name: '请输入手机号码' })` | |
128
+ | 短信验证码 | `getByRole('textbox', { name: '请输入验证码' })` | |
129
+ | 条款确认 · 同意并继续 | `getByRole('button', { name: '同意并继续' })` | 默认点击 |
130
+ | 条款确认 · 取消 | `getByRole('button', { name: '取 消' })` | 文案含空格,仅用户明示取消时使用 |
131
+ | 条款确认 · 关闭 | `getByRole('button', { name: 'Close' })` | 弹层角标关闭;默认不推荐 |
132
+ | 解螺旋 APP 登录 | `getByText('解螺旋APP登录')` | 次路径 |
133
+ | 免费注册 | `getByText('免费注册')` | 后续表单未在本次遍历中固化 |
134
+ | 协议链接(行内) | `getByRole('link', { name: '《用户服务协议》' })` 等 | 可能新开标签 |
135
+
136
+ **微信登录**:依赖扫码与微信客户端,不作为标准化自动化步骤;需要时请用户扫码完成。
137
+
138
+ **协议未勾选(微信区提示)**:出现「请先阅读并同意 …」类遮挡文案时,先在表单区勾选 `getByRole('checkbox').last()`(同意),再切换扫码或其它登录方式。
139
+
140
+ ---
141
+
142
+ ### 可复用路径矩阵(执行清单)
143
+
144
+ | 目标 | 步骤序列 |
145
+ | --- | --- |
146
+ | 打开 5.0 首页 | S-01 |
147
+ | 进入通行证登录 | S-02 |
148
+ | 账号密码登录 | S-03 |
149
+ | 验证码登录 | S-04 |
150
+ | 打开用户协议等 | S-05 |
151
+ | 进入旧版门户 | S-06 |
152
+ | 进入站内七大模块 | S-07 |
153
+ | 首页提问并发送 | S-08 |
154
+ | 对话 · 新建会话 | S-09 |
155
+ | 工作站 · 新建任务弹窗 | S-10 |
156
+ | 工作台 · 进入子工作台 | S-11 |
157
+ | 应用 · 标准操作(搜索 / 级联分类 / 滚动加载 / 收藏 / 进入详情 / 多块复制 / mindmap) | S-12(细则 AP-1~AP-13 + 规约 A~H) |
158
+ | 应用 · 取消收藏 · 二次确认弹窗 | AP-8(规约 E) |
159
+ | 应用 · 多块输出 · 单块复制 vs 全部复制 | AP-10 / AP-11(规约 F) |
160
+ | 应用 · mindmap 缩放 vs 页面滚动 | AP-12 / AP-13(规约 G) |
161
+ | 应用 · 级联分类(必选到叶子级) | AP-3(规约 H) |
162
+ | 数据分析 · 列表 / 主·子分类 / 拼图 / 全局设置 / 卡片收藏 | S-13 |
163
+ | 数据分析 · 拼图工具子页(ABC 标注 · 生信工具 · 拖图 · 参考线 · TIFF/PDF 导出) | S-27(细则见「数据分析 · 拼图工具页」) |
164
+ | 数据分析 · 工具详情页(上传 / 验证 / 参数重置保存 / 确认出结果 / 顶栏文档) | S-28(细则见「数据分析 · 工具详情页」) |
165
+ | 学习中心 · 进入与 Tab/子类 | S-14 |
166
+ | 学习中心 · 课程排序/个人筛选 | S-17 |
167
+ | 学习中心 · 顶栏关键字搜索(新标签) | S-18 |
168
+ | 搜索页 · 同关键词下切「课程 / 直播」结果 | S-18 第 3 步(非 S-14) |
169
+ | 搜索页 · 结果卡片 ·「部分章节匹配」等说明:仅查看 vs 进入 | S-18 第 5 步 |
170
+ | 学习中心 · 课程详情与播放 | S-19 |
171
+ | 学习中心 · 播放页 · 同课内切节 | S-19 第 5 步 |
172
+ | 学习中心 · 播放页 · 辅学 Tab(课件/文稿/资料/笔记) | S-23 |
173
+ | 学习中心 · 播放页 · 笔记 · wangEditor 输入与提交 | S-24 |
174
+ | 学习中心 · 播放页 · 课件 · Chrome 内置 PDF 阅读器 | S-25 |
175
+ | 学习中心 · 课程播放器控件(暂停/进度/清晰度/倍速/音量/全屏) | S-22 |
176
+ | 学习中心 · 训练营列表与详情 | S-20 |
177
+ | 学习中心 · 直播列表与详情 | S-21 |
178
+ | 退出登录 | S-15 |
179
+ | 个人中心(账号设置) | S-26 |
180
+ | 深浅色切换 | S-16 |
181
+
182
+ ---
183
+
184
+ ### 标准操作流程(S-xx):可复用步骤
185
+
186
+ #### S-01:进入 5.0 营销首页
187
+
188
+ 1. `helixlife-v5-cli list` → 若有 `headed: true` 且 `open` 会话则复用。
189
+ 2. 否则 `helixlife-v5-cli helix home`(或等价 `open https://vip.helixlife.cn/ --profile=.helixlife-profile --headed`)。
190
+ 3. **验收**:`location.href` 为 `https://vip.helixlife.cn/`(允许多余 `/`);`document.title` 为 `解螺旋-AI驱动科学创新`。
191
+
192
+ #### S-02:进入通行证登录(未登录前置)
193
+
194
+ 1. 营销首页:`click` → `getByRole('button', { name: '登录' })`。若线上恢复「立即体验」可增 `getByText('立即体验')`。
195
+ 2. **验收**:`host` 为 `passport.helixlife.cn`,路径含 `/ai/login`;标题含「登录」与「解螺旋」。
196
+
197
+ #### S-03:账号密码登录(需用户现场提供凭据)
198
+
199
+ > 若任务需真实登录,必须在对话中请用户提供账号信息后再填写。
200
+
201
+ 1. `click` → `getByText('账号登录')`(若当前不在该 Tab)。
202
+ 2. `fill` → 手机号/邮箱、`fill` → 密码(值由用户提供)。
203
+ 3. (推荐)`check` → `getByRole('checkbox').last()`(同意协议),避免步骤 6 弹层;若有「记住密码」且需勾,再 `check` → `.first()`。
204
+ 4. **提交前自检**:`snapshot` 或 `eval` 确认密码框仍有值;若登录钮 disabled,多为未填全(填全后即使未勾同意也可能可点,此时下一步弹条款确认层)。
205
+ 5. `click` → `getByRole('button', { name: '登录' })`。
206
+ 6. **若出现条款确认 dialog**(跳过步骤 3 时常见):默认 `click` → `getByRole('button', { name: '同意并继续' })`;除非用户明确要求取消。关闭后底部同意通常会变为已勾选并继续提交(2026-05-12 自测)。
207
+ 7. **验收**:离开通行证域名;落地 `host` 为 `vip.helixlife.cn`,常见路径 `/home`。
208
+ 8. **失败处理**:若停留通行证且出现剩余尝试次数提示,停止自动化重试,核对密码或改 S-04 / 人工处理风控。
209
+ 9. **优化建议**:登录后可用 `state-save` 保存授权状态;持久化 profile(`--profile`)亦可复用 Cookie。
210
+
211
+ #### S-04:验证码登录分支
212
+
213
+ 1. `click` → `getByText('验证码登录')`。
214
+ 2. **验收**:URL 带 `type=code`。
215
+ 3. (推荐)`check` → `getByRole('checkbox').last()`(同意协议),避免步骤 5 弹层。
216
+ 4. `fill` → `getByRole('textbox', { name: '请输入手机号码' })` → `click` → `getByText('获取验证码')`。
217
+ 5. **若出现条款确认 dialog**:默认「同意并继续」,关闭后继续发起获取验证码。
218
+ 6. 等用户收短信后 `fill` → `getByRole('textbox', { name: '请输入验证码' })` → `click` → `getByRole('button', { name: '登录' })`。
219
+ 7. **优化建议**:获取验证码后有频控,需加显式等待与用户确认。
220
+
221
+ #### S-05:打开协议(推荐点击,避免业务路径 `goto`)
222
+
223
+ - **未登录 · 营销首页 / 通行证页脚**:在页脚区域点击链接,推荐 `getByRole('link', { name: '用户服务协议' })`;通行证行内亦可 `getByRole('link', { name: '《用户服务协议》' })`。
224
+ - **已登录 · 产品壳**:主画布往往无营销页脚;协议入口常在**侧栏底栏「备案 / 合规」**展开的 tooltip/浮层内:`click` → `locator('.side-wrap__content .side-bottom--copyright')` → 在同一可见层内 `getByRole('link', { name: '用户服务协议' })`。
225
+ - **标签页**:协议常新开标签,用 `tab-list` → `tab-select 1` 切到新标签再 `eval location.href`。
226
+ - **验收**:当前/新标签 URL 落在 `vip.helixlife.cn/agreement/*`。
227
+
228
+ #### S-06:进入旧版门户
229
+
230
+ 1. 营销首页:`click` → `getByRole('button', { name: '回旧版' })`。
231
+ 2. **验收**:`location.href` 为 `https://www.helixlife.cn/`。`document.title` 实测可为 `解螺旋-科研AI智能体平台`(随旧版运营改名,不作为硬门禁)。
232
+
233
+ ---
234
+
235
+ ### 协议与备案链接
236
+
237
+ 页脚实际 `href`(营销域下协议均指向 `vip.helixlife.cn`):
238
+
239
+ | 文案 | URL |
240
+ | --- | --- |
241
+ | 用户服务协议 | `https://vip.helixlife.cn/agreement/register_agreement` |
242
+ | 个人信息保护政策 | `https://vip.helixlife.cn/agreement/privacy_agreement` |
243
+ | 智能服务专有条款 | `https://vip.helixlife.cn/agreement/intelligent_services_terms` |
244
+ | 关于我们(外链) | `https://www.helixlife.cn/main/about` |
245
+
246
+ 备案与监管外链(外部站点):页脚「沪ICP备…」「沪公网安备…」「生成式人工智能服务登记…」等,以页面当前 `href` 为准。
247
+
248
+ ---
249
+
250
+ ### 登录后产品区(已登录 `vip.helixlife.cn`)
251
+
252
+ > 基于真实登录会话(S-03)梳理:不依赖快照 `e*` ref;侧边栏以 `div.side-menu--item[to='…']`(Vue 路由锚点 + class)为主。
253
+
254
+ #### 登录落地 URL
255
+
256
+ - 典型:`https://vip.helixlife.cn/home`
257
+ - `document.title` 多为 `解螺旋-AI驱动科学创新`(子模块会带前缀如 `AI工作站-`)。
258
+
259
+ #### 全局壳:左侧主导航(7 项)
260
+
261
+ | 展示名 | `to` 属性 | 规范化 URL | 页面标题(示例) |
262
+ | --- | --- | --- | --- |
263
+ | 首页 | `/home` | `https://vip.helixlife.cn/home` | 解螺旋-AI驱动科学创新 |
264
+ | 对话 | `/chat` | `https://vip.helixlife.cn/chat` | 解螺旋-AI驱动科学创新 |
265
+ | AI工作站 | `/workstation` | `https://vip.helixlife.cn/workstation/`(常带尾 `/`) | AI工作站-… |
266
+ | AI工作台 | `/workbench` | `https://vip.helixlife.cn/workbench` | 解螺旋-AI驱动科学创新 |
267
+ | AI应用 | `/application` | `https://vip.helixlife.cn/application/` | AI应用-… |
268
+ | 数据分析 | `/analysis` | `https://vip.helixlife.cn/analysis/` | 数据分析-… |
269
+ | 学习中心 | `/edu/courses` | `https://vip.helixlife.cn/edu/courses` | 课程-… |
270
+
271
+ **推荐自动化写法**:
272
+
273
+ ```text
274
+ div.side-menu--item[to='/home']
275
+ div.side-menu--item[to='/chat']
276
+ div.side-menu--item[to='/workstation']
277
+ div.side-menu--item[to='/workbench']
278
+ div.side-menu--item[to='/application']
279
+ div.side-menu--item[to='/analysis']
280
+ div.side-menu--item[to='/edu/courses']
281
+ ```
282
+
283
+ **验收**:`location.pathname` 与上表一致(`/workstation/`、`/application/`、`/analysis/` 可能带尾随斜杠)。
284
+
285
+ #### 全局壳:侧栏底栏图标 + 用户菜单
286
+
287
+ > **固化规则**(2026-05-07 全流程自测):自动化直接使用下表字符串,禁止枚举侧栏 DOM 重新识别按钮;仅当改版失效时先跑回归再修订本文。
288
+
289
+ ##### 底栏三图标(深浅色 / 邮件 / 备案)
290
+
291
+ 位于七个路由图标**之下**、圆形头像**之上**。三枚按钮在 `.side-wrap__content` 下为 `a.cursor-pointer.flex.justify-center.items-center`,**恰好 3 个**(顺序固定)。
292
+
293
+ | 语义 | 用户映射 | `helixlife-v5-cli click`(主推荐) | 验收 / 备注 |
294
+ | --- | --- | --- | --- |
295
+ | 深浅色切换 | 「切换颜色」「深色 / 浅色」「外观主题」 | `locator('.side-wrap__content a.cursor-pointer.flex.justify-center.items-center').first()` | `document.documentElement.classList.contains('dark')`:`true`=深色,`false`=浅色;一次点击翻转一次 |
296
+ | 邮件 / 意见反馈 | — | `.nth(1)`(同链) | 实测不改变 `location.pathname`(弹层或抽屉) |
297
+ | 备案 / 合规提示 | — | `locator('.side-wrap__content .side-bottom--copyright')` | 与 `.nth(2)` 等价;不改变 `location.pathname` |
298
+
299
+ **等价序号**(兼容旧脚本,不推荐新逻辑依赖):`.side-wrap__content .cursor-pointer` 全体共 11 项 — `0–6` 为七个路由;`7`=深浅色;`8`=邮件;`9`=备案;`10`=头像。
300
+
301
+ ##### 用户头像与菜单
302
+
303
+ **打开菜单**:`locator('.side-wrap__content .rounded-full.bg-bg-default.cursor-pointer').first()`;类名变更回退 `locator('.m-sidebar-wrap .rounded-full.bg-bg-default').first()`。
304
+
305
+ | 操作 | 推荐定位器 |
306
+ | --- | --- |
307
+ | 回旧版 | `getByText('回旧版')` |
308
+ | 设置 | `getByText('设置')` |
309
+ | 退出登录 | `getByText('退出登录')` |
310
+
311
+ **产品与用语**:菜单文案为「设置」,等价于「个人中心」(账号资料、安全、第三方绑定、注销等)。落地 `pathname: /user/account`,标题常见 `账号设置-解螺旋-AI驱动科学创新`。详见 S-26。
312
+
313
+ **主题状态辅助**:`localStorage` 键 `h_v5_app_store` 内 JSON 的 `config.theme` 常见 `"dark"` / `"light"`;若与 `classList` 矛盾以界面实际为准。
314
+
315
+ ### 登录后 · 稳定定位器补充(按模块)
316
+
317
+ #### 首页 `/home`(助手大厅)
318
+
319
+ | 控件 | 推荐定位器 |
320
+ | --- | --- |
321
+ | 主输入框 | `getByRole('textbox', { name: '你可以问我任何和医学科研相关的问题...' })` |
322
+ | 快捷能力 | `getByRole('button', { name: 'Wiki' })`、`getByRole('button', { name: 'PubMed' })`、`getByRole('button', { name: '文献阅读' })` |
323
+ | 发送消息 | `getByRole('textbox', { name: '你可以问我任何和医学科研相关的问题...' }).locator('..').locator('> div > button').last()` — 相对主输入框父级下"输入区+工具行"容器的最后一个直接子 button(纸飞机);同容器另有无文案按钮为附件,勿混淆;未输入时多为 disabled |
324
+
325
+ 页脚提示文案固定:「内容由AI生成,仅供学习参考」(可作页面就绪验收)。
326
+
327
+ #### AI 对话 `/chat`
328
+
329
+ 左侧会话列表常见按钮:「新建对话」「全部」「收藏」 → `getByRole('button', { name: '新建对话' })` 等。`snapshot` 可能将侧栏列为空的 `complementary`(深度限制),不影响 role 定位。
330
+
331
+ 主对话区与首页类似:同一占位文案的输入框 + Wiki/PubMed/文献阅读快捷按钮;发送钮同首页表(`textbox` → `..` → `> div > button` → `last()`)。
332
+
333
+ #### AI 工作站 `/workstation/`
334
+
335
+ - 显著入口:`getByRole('button', { name: '创建新任务' })`
336
+ - 页面含任务统计、「我的任务」、「演示视频」等区块(文案可能微调)。
337
+
338
+ #### AI 工作台 `/workbench`
339
+
340
+ 卡片式入口(示例文案):「SCI论著工作台」「综述工作台」「基金申请书工作台」 → `getByText('SCI论著工作台')` 等精确匹配。
341
+
342
+ #### AI 应用 `/application/`(列表 · 详情 · 多块输入输出 · mindmap · 级联分类)
343
+
344
+ > 基于产品交互规范的稳定交互模式与必接动作。一律使用 role/文案/`aria-label`/结构类名,禁止把某次 `snapshot` 的 `e*` 当长期定位器。
345
+
346
+ ##### URL / 路由形态
347
+
348
+ | 页面 | `pathname` | `document.title` | 备注 |
349
+ | --- | --- | --- | --- |
350
+ | 应用列表 | `/application` 或 `/application/`(可能带筛选/分类查询) | `AI应用-…` | 顶栏含搜索与分类;列表为应用卡片网格 |
351
+ | 应用详情 | `/application/{appId}`(**新标签页**,见规约 D) | 以应用名为准 | 输入区 + 输出区 + 顶/底工具条 |
352
+
353
+ > 不要在文档中硬编码 `appId`;进入详情统一走 AP-6,禁止对 `/application/{appId}` 无文档化 `goto`。
354
+
355
+ ##### 通用 UI 交互规约(所有 AP-xx 共同遵守)
356
+
357
+ **规约 A · `aria-label` 优先**(历史记录 / 回到顶部 / 取消收藏 / 复制等高频图标按钮)
358
+
359
+ 页面内多枚图标按钮已挂 `aria-label`:
360
+
361
+ - 优先 `getByLabel('<aria-label 文本>')`(精确);勿依赖 `e*` 或临时 CSS。
362
+ - 多枚同 `aria-label` 共存时,先用 `main` 或目标块容器作用域缩小,再 `.first()` / `.nth(k)` 或借同块标题/文本辅助锁定。
363
+ - 未命中再回退 `getByRole('button', { name: '…' })` / `getByText('…', { exact: true })`,最后才用 `snapshot` 排障并回写本文。
364
+
365
+ **规约 B · 滚动加载列表**(必识别底部状态文本 + 鼠标先入容器)
366
+
367
+ 本模块所有列表(应用列表/历史记录/收藏夹等)均为滚动加载(非分页)。涉及「翻看更多 / 取最后一条 / 滚到末尾」时:
368
+
369
+ 1. **先读底部状态文本**(常见:`加载中…` / `没有更多了` / `已全部加载` / `下拉加载更多`;以页面实文案为准)。
370
+ 2. **判定**:
371
+ - 空态文案 → 直接退出,勿反复滚动;
372
+ - 末尾 `加载中…` → 短暂等待后重读状态,勿同时再触发新滚动;
373
+ - 末尾 `已全部加载` / `没有更多了` → 已到底,不再滚动;
374
+ - 有数据且非加载中、非已全部加载 → 可继续滚动加载。
375
+ 3. **滚动方式**:滚动前必须先 `mousemove`(或 `hover`)至滚动容器内任意非交互元素,再执行 `mousewheel 0 <Δy>`;勿直接对 `document`/`body` 滚轮。脚本化推荐 `run-code` 内:先 `await page.locator('<滚动容器>').hover()`,再循环 `await page.mouse.wheel(0, <Δy>)` + 短等待 + 重读底部文案。
376
+
377
+ **规约 C · 悬停才显示的隐藏操作按钮**
378
+
379
+ 列表项/输入输出块上部分操作按钮(复制、收藏、删除、重新生成、举报等)默认隐藏,仅在 `hover` 时出现:
380
+
381
+ 1. 先 `hover` 目标项目块;
382
+ 2. 再 `click` 出现的按钮(按规约 A 优先 `aria-label`)。
383
+
384
+ > 直接对未 hover 的按钮 `click`:常因 `visibility: hidden` / `pointer-events: none` 超时或被遮挡;勿用 `force: true` 绕过业务态。
385
+
386
+ **规约 D · 卡片以「新标签页」方式打开详情**
387
+
388
+ 应用列表点击卡片 → `target="_blank"` 新开标签页进入详情:
389
+
390
+ 1. 点击前 `tab-list` 记录当前标签集合;
391
+ 2. `click` 卡片(推荐主点击区:标题或封面,按规约 A 优先 `aria-label`,否则 `getByText('<应用主标题>', { exact: true })` 缩到目标卡片);
392
+ 3. `tab-list` 取新标签索引 → `tab-select <新索引>`;
393
+ 4. 后续详情交互在新标签完成;返回列表用 `tab-close` 详情标签后再 `tab-select` 回列表,**不要**在详情标签上 `go-back`。
394
+
395
+ **规约 E · 取消收藏 · 二次确认弹窗**
396
+
397
+ 收藏为单击直成;取消收藏会在按钮附近弹**确认气泡**(Popconfirm 形态:主「确认」/ 次「取消」,文案以页面实文案为准):
398
+
399
+ 1. `hover` 目标卡片以露出收藏按钮(规约 C);
400
+ 2. `click`「取消收藏」(规约 A 用 `getByLabel('取消收藏')`);
401
+ 3. **必须**再 `click` 弹层 `getByRole('button', { name: '确认' })`(文案可能为「确定」「移出收藏」);
402
+ 4. **验收**:按钮文案/`aria-label` 由「取消收藏」/「已收藏」恢复为「收藏」,或卡片从「我的收藏」消失。
403
+
404
+ > 勿省略步骤 3,否则误判已完成;勿把此处弹层当 `window.confirm`(`dialog-accept`/`dialog-dismiss` 无效)。
405
+
406
+ **规约 F · 多块内容输入/输出 · 整块 vs 单块**
407
+
408
+ 应用的输入/输出区可能由多个独立内容块组成(多段提示、分章节回答、多张图、多段代码/表格/mindmap 等):
409
+
410
+ - **单块操作**(单段「复制」、「重新生成」、「评分」):必须先 `hover` 目标块内部才会出现该块浮动按钮(规约 C),再使用 `aria-label`(规约 A)定位**该块作用域内**的按钮;勿直接命中全局同名按钮。
411
+ - **整块操作**(「全部复制」、「导出全部」、「全部重新生成」):通常位于输出区顶/底全局工具条,不需要悬停某块;用 `getByLabel('全部复制')` 或 `getByRole('button', { name: '全部复制' })` 直接命中。
412
+ - **分流原则**:根据用户原话——「复制这段/这一块/第 N 段」→ **单块**(先 `hover`);「全部复制/复制全部/复制整份输出」→ **整块**。`全部复制` 与 `复制` 在 `aria-label` 上严格区分,勿混用。
413
+
414
+ **规约 G · mindmap 缩放与页面滚动(鼠标位置决定)**
415
+
416
+ 部分应用输出以 mindmap 渲染,滚轮事件被画布自身捕获用于缩放:
417
+
418
+ - **缩放 mindmap**:`hover` 到 mindmap 画布区内,再 `mousewheel 0 <Δy>`;
419
+ - **滚动页面/外层容器**:`hover` 至 mindmap 外侧但仍在外层滚动容器内的安全空白区(卡片边缘、章节标题旁等),再滚轮;勿在 mindmap 内试图滚页面。
420
+ - `run-code` 写法:先 `const box = await page.locator('<mindmap 画布>').boundingBox()`;再 `await page.mouse.move(box.x - 16, box.y + box.height/2)`(左侧外缘安全点)后 `await page.mouse.wheel(0, Δy)`。
421
+
422
+ **规约 H · 应用分类(级联菜单 · 各级独立滚动 · 必选到叶子级)**
423
+
424
+ 分类入口点击后展开多级级联菜单(左→右逐级;每级为独立列表,可独立上下滚动):
425
+
426
+ 1. `click` 分类入口(规约 A 优先 `getByLabel('分类')`,或 `getByRole('button', { name: '分类' })`);
427
+ 2. **逐级选择**:在第一级若目标项需滚动暴露,先 `mousemove`/`hover` 至该级列表中心(规约 B),再 `mousewheel 0 <Δy>` 滚到目标项再 `click`,触发下一级展开;
428
+ 3. 在后续每一级重复 step 2,直至点中**最后一级(叶子节点)**;
429
+ 4. **验收**:级联菜单收起,应用列表筛选随之刷新;URL 可能新增分类查询参数。
430
+
431
+ > 勿仅停在中间层后离开——该状态下分类未真正生效;各级列表互不联动,要滚动第 K 级必须先把鼠标移入第 K 级自己的容器内再 `mousewheel`。
432
+
433
+ ##### 功能与稳定操作(执行清单 AP-xx)
434
+
435
+ | # | 用户语义 | 推荐操作 | 验收 / 备注 |
436
+ | --- | --- | --- | --- |
437
+ | AP-1 | **进入 AI 应用列表** | S-07 → `click` → `div.side-menu--item[to='/application']` | `pathname` 为 `/application` 或 `/application/`;`title` 以 `AI应用-` 开头 |
438
+ | AP-2 | **顶栏搜索** | `click` → `getByRole('button', { name: '搜索' })` → `fill` → `getByRole('textbox', { name: '请输入搜索关键词' })` → `press('Enter')` 或 `click` → `getByRole('button', { name: '搜索' })` | 列表刷新;与 S-12 一致 |
439
+ | AP-3 | **分类筛选(级联,必选到叶子级)** | 见规约 H:分类入口 → 逐级在每级容器内先 `hover` 再 `mousewheel` 再 `click` → 点中叶子项 | 列表按所选叶子分类过滤;URL 可能带分类参数 |
440
+ | AP-4 | **滚动加载更多** | 见规约 B:先读底部状态 → `hover` 滚动容器 → 循环 `mousewheel` + 重读状态 | 命中 `已全部加载` / `没有更多了` 时停止 |
441
+ | AP-5 | **进入历史记录 / 回到顶部 等 `aria-label` 操作** | `click` → `getByLabel('历史记录')` / `getByLabel('回到顶部')` | 进入对应面板/滚动至顶;见规约 A |
442
+ | AP-6 | **进入某一应用(新标签)** | 见规约 D:`tab-list` 记录 → `click` 卡片标题 → `tab-list` 取新索引 → `tab-select <索引>` | 新标签落在 `/application/{appId}`;返回用 `tab-close` + `tab-select` 回列表 |
443
+ | AP-7 | **收藏(单击直成)** | 先 `hover` 卡片(规约 C)→ `click` → `getByLabel('收藏')` | 文案/`aria-label` 切换为「已收藏」;无二次确认 |
444
+ | AP-8 | **取消收藏(需二次确认)** | 见规约 E:`hover` → `click`「取消收藏」 → `click` 弹层「确认」 | 验收文案回到「收藏」,或卡片移出「我的收藏」;勿用 `dialog-accept` |
445
+ | AP-9 | **多块输入 · 单块操作** | 先 `hover` 目标输入块 → 在该块作用域内用 `getByLabel('<操作>')` → `click` | 同规约 F;勿误触全局/其它块按钮 |
446
+ | AP-10 | **多块输出 · 单块复制** | 先 `hover` 目标输出块 → 在该块作用域内 `click` → `getByLabel('复制')` | 出现复制成功 toast 或剪贴板更新 |
447
+ | AP-11 | **多块输出 · 全部复制(整块)** | `click` → `getByLabel('全部复制')` 或 `getByRole('button', { name: '全部复制' })` | 同上提示;勿用「复制」代替 |
448
+ | AP-12 | **mindmap · 缩放** | 见规约 G:`mousemove` 到画布内 → `mousewheel` | 节点尺寸变化 |
449
+ | AP-13 | **mindmap 区域附近 · 滚动页面** | 见规约 G:`mousemove` 到 mindmap 外的安全空白区(仍在外层容器内) → `mousewheel` | 页面/外层容器滚动,mindmap 大小不变 |
450
+
451
+ ##### AI 应用 · 稳定定位器速查
452
+
453
+ | 控件 | 推荐定位器 | 备注 |
454
+ | --- | --- | --- |
455
+ | 进入模块(侧栏) | `div.side-menu--item[to='/application']` | 与 S-07 一致 |
456
+ | 顶栏 · 搜索按钮 | `getByRole('button', { name: '搜索' })` | 与数据分析/学习中心一致 |
457
+ | 顶栏 · 搜索框 | `getByRole('textbox', { name: '请输入搜索关键词' })` | 同上 |
458
+ | 历史记录 | `getByLabel('历史记录')` | `aria-label` 优先;多枚共存按规约 A |
459
+ | 回到顶部 | `getByLabel('回到顶部')` | 同上 |
460
+ | 收藏 / 已收藏 | `getByLabel('收藏')` / `getByLabel('已收藏')`(兼用 `getByText('收藏', { exact: true })`) | 取消收藏须见规约 E |
461
+ | 取消收藏 | `getByLabel('取消收藏')` | 触发二次确认 |
462
+ | 取消收藏 · 确认 | 弹层 `getByRole('button', { name: '确认' })`(文案以实测为准) | 必接动作;禁用 `dialog-accept` |
463
+ | 单块复制 | 目标块作用域内 `getByLabel('复制')` | 先 `hover` 该块 |
464
+ | 整块复制 | `getByLabel('全部复制')` | 勿与「复制」混用 |
465
+ | 重新生成(单块) | 目标块作用域内 `getByLabel('重新生成')` | 先 `hover` 该块 |
466
+ | 分类入口 | `getByLabel('分类')` 优先;否则 `getByRole('button', { name: '分类' })` | 级联见规约 H |
467
+ | 应用卡片 · 标题 | `getByText('<应用主标题>', { exact: true })` | 多卡时先缩到目标卡片 |
468
+ | mindmap 画布(缩放靶) | 以 `snapshot`/`eval` 取该输出块内 mindmap 容器选择器(运营配置而异) | 发现稳定 `data-*` 时回写本表 |
469
+
470
+ #### 数据分析 `/analysis/`(列表 · 拼图 · 全局设置)
471
+
472
+ > **核验记录**(2026-05-13 / 2026-05-15 / 2026-05-21):全局设置弹窗、顶区**主分类下拉**(如「分析工具」→「交互网络」;URL `primary=`)与「拼图工具」形态 Tab、搜索框 + 搜索按钮、**子分类**标签条「展开 ⇄ 收起」(URL `subcategory=` / `isExpanded`)、工具卡片「收藏 / 已收藏」互斥文案均已核验。主分类项(如「交互网络」)**不在**标签条内,须展开顶栏下拉才可见——见下文「分类体系」。工具详情(`/analysis/{toolId}`,D-1~D-11)、拼图工具子页(`/analysis/jigsaw`)见 S-28 / S-27。
473
+
474
+ ##### URL / 路由形态
475
+
476
+ | 页面 | `pathname` | `document.title` |
477
+ | --- | --- | --- |
478
+ | 分析工具列表(默认) | `/analysis/` 或 `/analysis`,常带 `?primary=…&subcategory=&isExpanded=true|false` | `数据分析-解螺旋-AI驱动科学创新` |
479
+ | 拼图工具 | `/analysis/jigsaw` | `拼图工具-解螺旋-AI驱动科学创新` |
480
+ | 某一工具详情 | `/analysis/{toolId}`(`toolId` 为 UUID,勿写死) | 多为 `数据分析-…`;工具名在 `main` 面包屑第二段(如配对图、豆荚图) |
481
+
482
+ ##### 数据分析 · 工具详情页 `/analysis/{toolId}`(左侧参数 · 右侧结果 · 顶栏入口)
483
+
484
+ > **核验记录**(2026-05-15):在 `/analysis/{uuid}`(如配对图、豆荚图)逐项核验:**数据参数区「重置参数」**、**上传文件弹窗**(须在弹窗内点「确认」才落盘)、**主要参数区「重置参数 / 保存参数」**(保存后出现 `保存成功` toast)、**左侧底部「确认」**(验证成功后右侧由「暂无内容,去生成内容吧!」变为「主要结果 / 补充结果 / 方法学」)、**「全局设置」**(`dialog "全局设置"`)、**「教程文档」**(`dialog "教程文档"` 内 `iframe`)。
485
+ >
486
+ > **上传核验补充**(2026-05-21,差异分析等双文件工具):左侧「数据参数」区每行参数(如「数据矩阵」「样本信息」)各自独立上传;**打开弹窗**须点该行 **`aria-label="上传文件 - 打开上传弹窗"`**(产品已加;旧版可能仍为同行 `aria-label="上传文件"` 的文件名区)。弹窗 `dialog "上传文件"` 内须再点「将文件拖到此处,或点击上传」触发文件选择,最后点弹窗底部「确认」才落盘——**仅选文件不点弹窗「确认」不算上传成功**。
487
+
488
+ ###### URL / 页面结构
489
+
490
+ | 项 | 说明 |
491
+ | --- | --- |
492
+ | **路由** | `pathname` 为 `/analysis/{toolId}`(单段 UUID);`document.title` 常为 `数据分析-…`(不以工具名开头,以面包屑为准)。 |
493
+ | **进入方式** | S-13 在 `/analysis/` 搜索或点分类后 `click` → `getByText('<卡片主标题>', { exact: true })`;禁止对 `/analysis/{toolId}` 无文档化 `goto`。返回列表:`click` → `locator('main').getByText('数据分析', { exact: true }).first()`(面包屑)或侧栏 `div.side-menu--item[to='/analysis']`。 |
494
+ | **布局** | 左侧(约半宽):`.u-filter-card` — 数据参数、上传文件、校验态、主要参数折叠组、左侧底部「确认」。右侧:未生成时为 `暂无内容,去生成内容吧!`;生成后为结果 Tab + 表格/图表区 + 「保存结果」「下载整份报告」。 |
495
+ | **顶栏** | 面包屑「数据分析 / {工具名}」;`getByText('全局设置', { exact: true })`(`main` 作用域)。说明条下方常见「教程文档」;部分工具另有「GITHUB文档」「更新情况」(均为 `main` 内可点文案)。 |
496
+ | **两个「重置参数」** | 第 1 个(数据参数标题同行):`locator('main').getByRole('button', { name: '重置参数' }).first()`;第 2 个(主要参数标题同行):`.nth(1)`。勿混用 `dialog` 内「确认」(上传弹窗/错误提示)与左侧底部「确认」。 |
497
+ | **校验态文案** | `验证成功` / `验证失败`(静态文案);或 `button`「验证」。底部「确认」前置:`main` 内可见 `验证成功`;否则弹 `dialog`「错误信息」:「数据未进行验证 或者 验证未通过…」。部分工具在仅用默认样例文件名点「验证」时会提示「[上传文件]需要上传文件,不能为默认数据」——须走 D-2 实际上传。 |
498
+
499
+ ###### 上传文件 · Agent 硬门禁(D-2 细则 · 必读)
500
+
501
+ **适用范围**:`pathname === '/analysis/{toolId}'`;用户要求上传 / 更换数据文件;或校验提示须实际上传时。
502
+
503
+ **页面 DOM(左侧 `.u-filter-card` · 数据参数区)**
504
+
505
+ | 区域 | 说明 |
506
+ | --- | --- |
507
+ | 参数行 | 每行左侧为参数名(如 `数据矩阵`、`样本信息`),右侧为上传入口 + 当前文件名 + 「下载示例数据」 |
508
+ | **步骤 1 · 打开弹窗** | 点该行 **`aria-label="上传文件 - 打开上传弹窗"`** 的控件(规约 A)。多行并存时**必须先按参数名缩到单行**,再点该行上传入口 |
509
+ | **步骤 2 · 弹窗内选文件** | 出现 `getByRole('dialog', { name: '上传文件' })` 后,在**弹窗内**点 `button`,文案匹配 `/将文件拖到此处\|点击上传/`,配合 `filechooser.setFiles(<本地绝对路径>)` |
510
+ | **步骤 3 · 弹窗内确认** | 仍在**同一弹窗**内 `click` → `getByRole('button', { name: '确认' })`;弹窗关闭后,对应参数行文件名更新 |
511
+
512
+ **两个「确认」勿混淆**
513
+
514
+ | 按钮 | 位置 | 用途 |
515
+ | --- | --- | --- |
516
+ | 弹窗「确认」 | `getByRole('dialog', { name: '上传文件' })` 底部 | **D-2 上传落盘**(本节前 3 步之第 3 步) |
517
+ | 左侧底部「确认」 | `locator('main .u-filter-card').getByRole('button', { name: '确认' })` | **D-6 生成右侧结果**(须先 `验证成功`) |
518
+
519
+ **推荐定位器(按优先级)**
520
+
521
+ 1. **打开弹窗(首选)**:`locator('main .u-filter-card').getByLabel('上传文件 - 打开上传弹窗')`;多文件工具须先缩到**参数名所在行**再点:
522
+
523
+ ```js
524
+ const row = page
525
+ .locator('main .u-filter-card')
526
+ .getByText('数据矩阵', { exact: true })
527
+ .locator('..');
528
+ await row.getByLabel('上传文件 - 打开上传弹窗').click();
529
+ ```
530
+
531
+ 参数名即行首灰色标签(如 `数据矩阵`、`样本信息`);该行 DOM 为 `.flex.items-center`,内含参数名 `span`、上传入口、`aria-label="上传文件"` 文件名区、「下载示例数据」。
532
+
533
+ 2. **打开弹窗(兼容旧版)**:同行 `getByLabel('上传文件')`(文件名展示区)。可与首选写法 `.or(getByLabel('上传文件'))` 并用。**勿**再使用已失效的 `getByText('上传文件').locator('..').locator('img')` 链——当前布局下图标 `img` 与文件名区为**兄弟节点**,不在同一父级内;且 `img` 本身**不会**打开弹窗。
534
+
535
+ 3. **弹窗**:`page.getByRole('dialog', { name: '上传文件' })`(须 `waitFor({ state: 'visible' })`)
536
+
537
+ 4. **弹窗内选文件**:`dlg.getByRole('button', { name: /将文件拖到此处\|点击上传/ })`
538
+
539
+ 5. **弹窗内确认**:`dlg.getByRole('button', { name: '确认' })`
540
+
541
+ **`helixlife-v5-cli` 逐步命令示例**(单文件 · 参数名 `数据矩阵` · 文件路径按任务替换):
542
+
543
+ ```bash
544
+ # 1) 打开上传弹窗(参数名按任务替换;.or 兼容尚未发布的新 aria-label)
545
+ helixlife-v5-cli click "locator('main .u-filter-card').getByText('数据矩阵', { exact: true }).locator('..').getByLabel('上传文件 - 打开上传弹窗').or(locator('main .u-filter-card').getByText('数据矩阵', { exact: true }).locator('..').getByLabel('上传文件'))"
546
+
547
+ # 2) 弹窗内触发文件选择(须 run-code 绑定 filechooser;勿在未开弹窗时用 upload)
548
+ helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-file.js
549
+
550
+ # 或整段三步合一:同上 run-code 脚本(推荐)
551
+ ```
552
+
553
+ **反模式(常见导致 Agent「找不到」)**
554
+
555
+ - **禁止**在未出现 `dialog "上传文件"` 时对页面根节点执行 `helixlife-v5-cli upload`——文件选择器只在弹窗内的拖拽区触发。
556
+ - **禁止**把 `getByLabel('上传文件')` 当成「直接选文件」:在当前实现下它**先打开弹窗**;须在弹窗内再点「将文件拖到此处,或点击上传」。
557
+ - **禁止**混淆两个「确认」:上传未完成前不要点左侧 `.u-filter-card` 底部「确认」。
558
+ - **禁止**用整页 `getByLabel('上传文件')` 当唯一定位器——双文件工具会 strict 或多点;必须带参数名 `filter({ has: getByText('<参数名>') })`。
559
+ - **禁止**依赖某次 `snapshot` 的 `e*` ref 或肉眼猜 `img` 位置。
560
+
561
+ **多文件工具**(如差异分析:数据矩阵 + 样本信息):对每个参数**重复完整 3 步**;参考 `scripts/analysis-detail-upload-both.js`(顺序执行,勿并行多个 `filechooser`)。
562
+
563
+ **验收(对用户回复前必审)**
564
+
565
+ 1. `getByRole('dialog', { name: '上传文件' })` 已关闭。
566
+ 2. 目标参数行 `.input-box` 或同行文案已显示新文件名(非仅弹窗内预览)。
567
+ 3. (若任务含校验)按需 D-3:`main` 内出现 `验证成功`。
568
+ 4. 未完成步骤 3 前,禁止向用户表述「已上传成功」。
569
+
570
+ ###### 功能与稳定操作(D-xx)
571
+
572
+ | # | 用户语义 | 推荐操作 | 验收 / 备注 |
573
+ | --- | --- | --- | --- |
574
+ | D-1 | **数据参数 · 重置** | `click` → `locator('main').getByRole('button', { name: '重置参数' }).first()` | 数据参数区恢复默认;可能清除已上传文件名 |
575
+ | 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` 未开弹窗;勿点左侧底部「确认」代替弹窗「确认」 | 弹窗关闭;对应参数行展示新文件名;格式须符合工具模板 |
576
+ | D-3 | **数据校验(验证)** | 若 `main` 出现 `getByRole('button', { name: '验证' })`:`click` 一次;若已为 `验证成功` 可跳过 | `验证成功` → 可 D-6;`验证失败` → 调整文件后重试 D-2~D-3 |
577
+ | D-4 | **主要参数 · 重置** | `click` → `locator('main').getByRole('button', { name: '重置参数' }).nth(1)` | 主要参数折叠组恢复默认 |
578
+ | D-5 | **主要参数 · 保存** | `click` → `locator('main').getByRole('button', { name: '保存参数' })` | 页面级 toast `保存成功`;`pathname` 不变 |
579
+ | D-6 | **生成右侧结果** | 先满足 D-3 → `验证成功` → `click` → `locator('main .u-filter-card').getByRole('button', { name: '确认' })`(左侧底部主按钮,非上传弹窗「确认」) | 右侧不再含 `暂无内容…`;出现 `getByText('主要结果', { exact: true })` 等 |
580
+ | D-7 | **右上角 · 全局设置** | `click` → `locator('main').getByText('全局设置', { exact: true })` → 弹窗内重置/保存 → `click` → `getByRole('dialog', { name: '全局设置' }).getByRole('button', { name: 'Close' })` | `dialog "全局设置"` 关闭;`pathname` 仍为 `/analysis/{toolId}` |
581
+ | D-8 | **教程文档** | `click` → `locator('main').getByText('教程文档', { exact: true })` → `click` → `getByRole('dialog', { name: '教程文档' }).getByRole('button', { name: 'Close' })` | `dialog` 内含 `iframe` |
582
+ | D-9 | **(可选)GITHUB 文档 / 更新情况** | `click` → `getByText('GITHUB文档', { exact: true })` / `getByText('更新情况', { exact: true })` | 以 `snapshot`/新标签/弹层为准 |
583
+ | D-10 | **(可选)结果区 · 保存 / 下载** | D-6 之后:`click` → `locator('main').getByRole('button', { name: '保存结果' })` / `下载整份报告` | 共享环境默认仅验收按钮可点 |
584
+ | D-11 | **(可选)结果 Tab 切换** | `click` → `getByText('主要结果', { exact: true })` / `补充结果` / `方法学` | 右侧主内容切换;`pathname` 不变 |
585
+
586
+ ###### 工具详情 · 稳定定位器速查
587
+
588
+ | 控件 | 推荐定位器 |
589
+ | --- | --- |
590
+ | 左侧参数卡片 | `locator('main .u-filter-card')` |
591
+ | 数据参数 · 重置 | `locator('main').getByRole('button', { name: '重置参数' }).first()` |
592
+ | 打开上传弹窗(首选) | `locator('main .u-filter-card').getByLabel('上传文件 - 打开上传弹窗')`;多参数时 `getByText('<参数名>', { exact: true }).locator('..').getByLabel('上传文件 - 打开上传弹窗')` |
593
+ | 打开上传弹窗(旧版兼容) | 同上结构,`.or(getByLabel('上传文件'))` 或直接用同行 `getByLabel('上传文件')` |
594
+ | 上传弹窗 | `getByRole('dialog', { name: '上传文件' })` |
595
+ | 弹窗 · 选文件 | `getByRole('dialog', { name: '上传文件' }).getByRole('button', { name: /将文件拖到此处\|点击上传/ })` |
596
+ | 弹窗 · 确认(上传落盘) | `getByRole('dialog', { name: '上传文件' }).getByRole('button', { name: '确认' })` |
597
+ | 左侧底部 · 确认(生成结果,非上传) | `locator('main .u-filter-card').getByRole('button', { name: '确认' })` |
598
+ | 手动验证 | `locator('main').getByRole('button', { name: '验证' })` |
599
+ | 主要参数 · 重置 / 保存 | `.getByRole('button', { name: '重置参数' }).nth(1)` / `.getByRole('button', { name: '保存参数' })` |
600
+ | 生成结果(底部) | `locator('main .u-filter-card').getByRole('button', { name: '确认' })` |
601
+ | 右侧空态 | `getByText('暂无内容,去生成内容吧!', { exact: true })` |
602
+ | 全局设置 | `locator('main').getByText('全局设置', { exact: true })` |
603
+ | 教程文档 | `locator('main').getByText('教程文档', { exact: true })` |
604
+
605
+ ###### 可复用 `run-code`(可选)
606
+
607
+ - **单参数上传并尝试验证**:`helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-file.js`(三步弹窗;脚本内改 `filePath` 与可选 `paramLabel`;格式须符合工具模板,大小以页面上限为准)。
608
+ - **多参数顺序上传**(如差异分析):`helixlife-v5-cli run-code --filename=scripts/analysis-detail-upload-both.js`(每个参数各走一遍打开弹窗 → 选文件 → 弹窗确认)。
609
+
610
+ ##### 数据分析 · 分类体系(主分类 vs 子分类 · Agent 必读)
611
+
612
+ > **核验记录**(2026-05-21):列表页顶栏**最左侧**为**主分类下拉**(带下箭头;选中后 Tab 文案变为该主分类名,如「交互网络」)。「交互网络」等主分类项**不在**搜索框下方标签条内,须先 `click` 展开当前主分类名(默认「分析工具」)后才可见。URL `primary=` 对应该下拉;`subcategory=` 对应标签条。
613
+
614
+ ###### 页面分区
615
+
616
+ ```
617
+ 顶栏左侧 [主分类 ▼] [拼图工具] 顶栏右侧 [全局设置]
618
+ ↓ primary=
619
+ 搜索框 [请输入搜索关键词] [搜索]
620
+ 标签条 [全部] [类别比较] [差异分析] … [展开 | 收起]
621
+ ↓ subcategory=
622
+ 工具卡片列表
623
+ ```
624
+
625
+ | 层级 | UI 位置 | URL 参数 | 典型选项 | 用户常见说法 |
626
+ | --- | --- | --- | --- | --- |
627
+ | **主分类** | 顶栏**最左侧**带下箭头的文案(选中后显示为该主分类名) | `primary=` | 分析工具、交互网络、…(**以展开下拉 snapshot 为准**,勿写死枚举) | 「分析工具下的交互网络」「切到交互网络」 |
628
+ | **子分类** | 搜索框**下方**横排可点标签 | `subcategory=` | 全部、差异分析、网络图相关、互作分析、…(**随当前主分类变化**) | 「切到差异分析」「网络图相关分类」 |
629
+ | **形态 Tab** | 主分类右侧「拼图工具」 | 路由 → `/analysis/jigsaw` | 拼图工具 | 「去拼图工具」 |
630
+
631
+ > **易混点**:「网络图相关」「互作分析」是**子分类标签**,与主分类「交互网络」**不是同一控件**;勿因语义相近而误点标签条或搜索。
632
+
633
+ ###### 用户话术 → 操作映射
634
+
635
+ | 用户说法 | 操作层 | 推荐操作 |
636
+ | --- | --- | --- |
637
+ | 「分析工具下的 XX」「切到 XX 主分类」 | 主分类 | 展开顶栏主分类下拉 → `click` → `getByText('XX', { exact: true })` |
638
+ | 「XX 分类」「筛选 XX」且 XX 在标签条可见 | 子分类 | `click` → 标签条 `getByText('XX', { exact: true })` |
639
+ | 「去拼图工具」 | 形态 Tab | `click` → `getByText('拼图工具', { exact: true })` |
640
+ | 「找 Limma / 配对图 / 某工具名」 | 工具卡片 | 搜索或 `click` 卡片主标题(S-13 步骤 7) |
641
+
642
+ ###### Agent 决策树(切换/选中分类时必走)
643
+
644
+ 1. **`snapshot`(必做)**。
645
+ 2. 目标是**具体工具名**(卡片标题)→ 搜索或点卡片;**勿**当分类处理。
646
+ 3. 目标名是否出现在**搜索框下方标签条**(当前可见,或点「展开」后可见)→ 是:`click` 该标签 → 验收 URL `subcategory=` 变化。
647
+ 4. 顶栏主分类文案**已是**目标名 → 已选中,结束。
648
+ 5. 否则:`click` **顶栏当前主分类名**(带下箭头;以 snapshot 顶栏左侧块为准)展开下拉 → 若菜单含目标名:`click` → 验收 URL `primary=` 变化且顶栏文案更新为目标名。
649
+ 6. 下拉与标签条**均无**目标名 → **最后手段**用搜索找工具(非切换主分类);仍无则向用户确认是「分类名」还是「工具名」。
650
+ 7. **切换主分类后**:若搜索框有残留关键词,先清空(搜索区清除钮或 `fill` 空串再搜),再 `snapshot` 验收列表。
651
+
652
+ **何时才向用户追问**
653
+
654
+ | 情况 | 处理 |
655
+ | --- | --- |
656
+ | 下拉与标签条**都有**同名项 | 问:「是顶栏主分类还是下方标签?」 |
657
+ | 两处都找不到目标名 | 问:「是分类名还是具体工具名?」 |
658
+ | 用户只说「切换分类」且语义不明 | 可先 `snapshot` 列出当前可见主分类/子分类供其选择 |
659
+
660
+ **反模式**
661
+
662
+ - **禁止**未展开顶栏主分类下拉就假定目标在标签条。
663
+ - **禁止**用搜索框切换**主分类**(搜索仅筛工具卡片)。
664
+ - **禁止**因语义相近误点子分类(如用「网络图相关」代替「交互网络」)。
665
+ - **禁止**把「展开/收起」当作切换主分类——它仅控制标签条 `isExpanded` 显示更多子分类。
666
+
667
+ ###### 分类 · 稳定定位器速查
668
+
669
+ | 控件 | 推荐定位器 |
670
+ | --- | --- |
671
+ | 展开主分类下拉 | `click` snapshot 顶栏左侧主分类块(当前主分类名 + 箭头);勿与标签条同名项混淆 |
672
+ | 主分类菜单项 | 展开后 `getByText('<目标主分类>', { exact: true })` |
673
+ | 子分类标签 | 搜索框下方 `getByText('<子分类名>', { exact: true })` |
674
+ | 拼图工具 Tab | `getByText('拼图工具', { exact: true })` |
675
+ | 标签条展开/收起 | `getByText('展开', { exact: true })` / `getByText('收起', { exact: true })` |
676
+
677
+ ##### 数据分析 · 功能区块(列表态)
678
+
679
+ | # | 语义 | 推荐操作 | 验收 / 备注 |
680
+ | --- | --- | --- | --- |
681
+ | 1 | **全局设置**(顶区右侧文案按钮) | `click` → `locator('main').getByText('全局设置')`(必须加 `main`) | 出现 `dialog`,可访问名为 `全局设置`。弹窗内可见「配色方案」「全局字体」「颜色面板」三块:配色为下拉 + 预览色块(示例 `NPG方案`);全局字体、颜色面板各带 `combobox`(示例 `Arial`、`随机颜色`)。底部 button:「重置」「保存」。关闭:`click` → `getByRole('dialog', { name: '全局设置' }).getByRole('button', { name: 'Close' })` |
682
+ | 2 | **顶区 · 主分类下拉** | `click` 顶栏当前主分类名展开菜单 → `click` → `getByText('<主分类名>', { exact: true })`。默认项为「分析工具」;选中后顶栏文案变为该主分类名(如「交互网络」) | URL `primary=` 变化;顶栏文案更新。细则见「分类体系 · Agent 决策树」 |
683
+ | 3 | **顶区 · 形态 Tab:拼图工具** | `click` → `getByText('拼图工具', { exact: true })`;从拼图回列表:`click` → `getByText('分析工具', { exact: true })` 或 `div.side-menu--item[to='/analysis']` | 拼图:`pathname === '/analysis/jigsaw'`,`title` 以 `拼图工具-` 开头。列表:`pathname` 为 `/analysis`/`/analysis/`,`title` 以 `数据分析-` 开头 |
684
+ | 4 | **搜索(找工具,非切主分类)** | `fill` → `getByRole('textbox', { name: '请输入搜索关键词' })`;`click` → `getByRole('button', { name: '搜索' })` | 列表随关键字刷新(以 `snapshot`/卡片集合变化验收)。**勿**用搜索代替主分类切换 |
685
+ | 5 | **子分类标签 + 展开 / 收起** | 搜索框下方为**子分类**可点文案(如「全部」「类别比较」「差异分析」「网络图相关」…),**仅在当前主分类下生效**。分类行右侧「展开」/「收起」:`click` → `getByText('展开', { exact: true })` → URL 中 `isExpanded=true`;再点 `getByText('收起', { exact: true })` → `isExpanded=false` | URL `subcategory=` / `isExpanded` 与选中标签、展开态一致 |
686
+ | 6 | **工具卡片 · 收藏** | **收藏**:`main` 内对目标卡片上的 `getByText('收藏', { exact: true })` `click`(多卡并列时先 `locator('main').getByText('<卡片标题>', { exact: true })` 缩小到单卡,或 `nth(k)` 由任务明确)。**已收藏**:同区 `getByText('已收藏', { exact: true })`。再 `click` → `已收藏` → 回「收藏」 | 状态文案在「收藏」与「已收藏」间切换 |
687
+
688
+ ##### 数据分析 · 拼图工具页 `/analysis/jigsaw`(左侧工具条 · 右侧画布 · 参考线 · 导出)
689
+
690
+ > **核验记录**(2026-05-15):在 `pathname === '/analysis/jigsaw'` 下逐项核验:ABC 标注折叠面板(8 项控件)、生信工具 Ant Select、`.jigsaw-images .cursor-move` → `.analysis-right-box` 拖图、横向/竖向/清除参考线、空画布时 TIFF/PDF 为 `disabled`、画布有内容后解禁。
691
+
692
+ ###### URL / 页面结构
693
+
694
+ | 项 | 说明 |
695
+ | --- | --- |
696
+ | 路由 | `pathname === '/analysis/jigsaw'`;`title` 以 `拼图工具-` 开头 |
697
+ | 顶区 Tab | 与列表页同:「分析工具」/「拼图工具」 |
698
+ | DOM 分区 | 左侧工具条:`.jigsaw-left`(含 ABC 折叠、`.jigsaw-Select` 生信工具、`.jigsaw-images` 可拖缩略图)。右侧画布:`.analysis-right-box`(拖放落点;外围标尺 `abeam-cm-box` 非主目标)。下载钮:`main` 内 `getByRole('button', { name: 'TIFF下载' })` / `PDF下载` |
699
+
700
+ ###### 功能与稳定操作(J-xx)
701
+
702
+ | # | 用户语义 | 推荐操作 | 验收 / 备注 |
703
+ | --- | --- | --- | --- |
704
+ | J-1 | **进入拼图工具** | S-07 → `div.side-menu--item[to='/analysis']` → `click` → `getByText('拼图工具', { exact: true })` | `pathname === '/analysis/jigsaw'`,`title` 以 `拼图工具-` 开头 |
705
+ | J-2 | **顶区 · 回到分析工具列表** | `click` → `getByText('分析工具', { exact: true })`;无效则 `click` → `div.side-menu--item[to='/analysis']` | `pathname` 为 `/analysis`/`/analysis/` |
706
+ | J-3 | **ABC 标注 · 展开 / 收起** | `click` → `getByRole('button', { name: 'ABC标注' })` | `aria-expanded` 切换;展开后可见 J-4~J-9 |
707
+ | J-4 | **是否标注 · 开关** | `click` → `getByText('是否标注', { exact: true }).locator('..').getByRole('switch')` | `aria-checked` 切换 |
708
+ | J-5 | **字体 · 下拉** | `click` → `getByText('字体', { exact: true }).locator('..').getByRole('combobox')` → 选目标字体(示例 Arial) | 展示值更新 |
709
+ | J-6 | **标注 · 文本** | `fill` → `getByRole('textbox', { name: '逗号间隔' })` | `inputValue()` 与填入一致(默认 `A, B, C, D, E, F, G`) |
710
+ | J-7 | **标注大小 · 下拉** | `click` → `getByText('标注大小', { exact: true }).locator('..').getByRole('combobox')` → 选 pt(示例 15pt) | 展示值更新 |
711
+ | J-8 | **左偏斜 / 上偏斜** | 对各行 `combobox` `click` 后选 cm(示例 0.20cm) | 两轴展示值独立更新 |
712
+ | J-9 | **固定输出图片宽 / 高** | `click` → `getByText('固定输出图片宽度', { exact: true }).locator('..').getByRole('switch')`(高度同理) | `aria-checked` 切换 |
713
+ | J-10 | **生信工具 · 选择器** | `click` → `locator('main .jigsaw-Select .ant-select-selector')` | 下拉展开;切换后 `.jigsaw-images` 缩略图可能刷新 |
714
+ | J-11 | **工具图 · 拖入拼图区** | 源:`locator('.jigsaw-images .cursor-move').first()`(或 `nth(k)`)。靶:`locator('.analysis-right-box')`,`dragTo(..., { targetPosition: { x, y } })`。**反模式**:勿仅拖到标尺数字格 | `.analysis-right-box` 有子内容;TIFF/PDF 由 `disabled` → 可点 |
715
+ | J-12 | **参考线** | `click` → `getByText('横向参考线', { exact: true })` / `竖向参考线` / `清除参考线` | 清除后参考线消失 |
716
+ | J-13 | **TIFF / PDF 下载** | 先 J-11 → `click` → `getByRole('button', { name: 'TIFF下载' })` 或 `PDF下载` | 空画布两钮 `disabled`;可能触发浏览器下载 |
717
+
718
+ ###### 拼图工具 · 稳定定位器速查
719
+
720
+ | 控件 | 推荐定位器 |
721
+ | --- | --- |
722
+ | ABC 折叠 | `getByRole('button', { name: 'ABC标注' })` |
723
+ | 是否标注 | `getByText('是否标注', { exact: true }).locator('..').getByRole('switch')` |
724
+ | 标注输入 | `getByRole('textbox', { name: '逗号间隔' })` |
725
+ | 字体 / 大小 / 偏斜 | `getByText('<标签>', { exact: true }).locator('..').getByRole('combobox')` |
726
+ | 固定宽/高 | `getByText('固定输出图片宽度', { exact: true }).locator('..').getByRole('switch')` |
727
+ | 生信工具 | `locator('main .jigsaw-Select .ant-select-selector')` |
728
+ | 可拖缩略图 | `locator('.jigsaw-images .cursor-move')` |
729
+ | 画布落点 | `locator('.analysis-right-box')` |
730
+ | 参考线 | `getByText('横向参考线', { exact: true })` 等 |
731
+ | 导出 | `getByRole('button', { name: 'TIFF下载' })` / `PDF下载` |
732
+
733
+ ###### 拼图工具 · 可复用 `run-code`
734
+
735
+ - **拖图并验收下载解禁**:`helixlife-v5-cli run-code --filename=scripts/jigsaw-verify-drag-tool.js`
736
+ - **ABC 字段枚举**:`helixlife-v5-cli run-code --filename=scripts/jigsaw-verify-enumerate.js`
737
+
738
+ ---
739
+
740
+ ### 学习中心模块(全量:`/edu/**`)
741
+
742
+ > **核验记录**(2026-05-07):课程列表 → 训练营子类 → 直播子类与讲师筛选 → 课程 `/learn` → 「继续学习」播放器 → 训练营详情 → 直播详情(回放态 / 「无需预约」态)→ 顶栏搜索(新开标签)结果页均已核验。不写死 URL 中的业务 `uuid`/`category`,一律用 `getByText`/角色定位器从 UI 进入;只固化 `pathname` 形态。
743
+
744
+ #### URL / 路由形态
745
+
746
+ | 页面 | `pathname` 形态 | `document.title` 前缀 |
747
+ | --- | --- | --- |
748
+ | 侧栏切入学习中心(默认课程聚合) | `/edu/courses` | `课程-` |
749
+ | 训练营列表(选一子类后) | `/edu/trainings` + `?category=…` | `训练营-` |
750
+ | 直播列表(选一子类后) | `/edu/lives` + `?category=…` | `直播-` |
751
+ | 课程介绍 / 大纲 / 讲师 | `/edu/courses/{courseId}/learn` | 「课程名」+ `-解螺旋-AI驱动科学创新` |
752
+ | 播放 / 沉浸式学习 | `/edu/study/courses/{courseId}` | `学习中-解螺旋-AI驱动科学创新` |
753
+ | 训练营详情 | `/edu/trainings/{trainingId}` | 「营名」+ `-解螺旋-AI驱动科学创新` |
754
+ | 直播详情 | `/edu/lives/{liveId}` | 「直播标题」+ `-解螺旋-AI驱动科学创新` |
755
+ | 学习中心全局搜索 | `/edu/search` + `?keywords=…` | `搜索-解螺旋-AI驱动科学创新` |
756
+
757
+ **说明**:`/edu/courses` 上切换顶栏「课程/训练营/直播」时,只有「课程」保持上述路径;点选训练营/直播子类会 push 到 `/edu/trainings?…`、`/edu/lives?…`(`category` 由后端配置,勿硬编码)。需确定回到课程列表时优先 `click` → `div.side-menu--item[to='/edu/courses']`,比顶栏 `getByText('课程')` 更稳。
758
+
759
+ #### 信息架构与功能区块
760
+
761
+ | 层级 | 区块 | 功能要点 |
762
+ | --- | --- | --- |
763
+ | 全局 | 左侧主导航 | `div.side-menu--item[to='/edu/courses']`(S-07) |
764
+ | 列表公共顶栏 | 搜索 | 见 S-18;与应用/数据分析同一占位「请输入搜索关键词」+ 按钮「搜索」。数据分析同页内搜索见 S-13 |
765
+ | 列表 | 一级 Tab | 「课程」「训练营」「直播」:`getByText('课程')` 等 |
766
+ | 课程列表 | 排序与筛选 | 「最新」「最热」(`paragraph` 可点);`getByRole('checkbox', { name: '只显示我学习的' })` |
767
+ | 课程列表 | 课程卡片 | 难度、已学人数、时长、已学进度(如「已学4%」);点击进入 `/edu/courses/…/learn`(S-19) |
768
+ | 训练营 | 子类 | 全部训练营、7天主题营、14天成长营、21天讲席营、30天陪读营 → `/edu/trainings?category=…` |
769
+ | 训练营列表 | 筛选 | 「全部」;「最新」「最热」;`getByRole('checkbox', { name: '只显示我报名的' })` |
770
+ | 训练营列表 | 卡片 | 营名、营类型、人数/状态(如「待开营」)、时长;点击进入 `/edu/trainings/{id}`(S-20) |
771
+ | 直播 | 子类 | 全部直播、直播季、名师直播、陪读营、三十六策、直播大课 → `/edu/lives?category=…` |
772
+ | 直播列表 | 讲师筛选 | 全部、酸菜、老谈、雪球、虾仁、猫大、AW、大空熠等;`getByText('雪球')` 等 |
773
+ | 直播列表 | 个人筛选 | `getByRole('checkbox', { name: '只显示我预约的' })` |
774
+ | 直播列表 | 卡片 | 时间、标题、讲师·预约人数;状态标签「预约中」「回放」;推荐直接点标题进入详情(S-21) |
775
+ | 课程 `/learn` | 面包屑 | 「学习中心 / 课程 / 课程名」;若遇 Ant 运营弹窗遮挡,先 `getByRole('button', { name: 'Close' })` 或 `dialog-dismiss` |
776
+ | 课程 `/learn` | 学习进度 | 「上次学至…」「已学 *%」;`getByRole('button', { name: '继续学习' })` → `/edu/study/courses/{id}` |
777
+ | 课程 `/learn` | 大纲与侧栏 | 「简介」「目录」;节状态:未学习/最近学习/未学完等 |
778
+ | 课程 `/learn` | 推荐 | 「推荐课程」卡片网格 |
779
+ | 播放页 `/edu/study/...` | 侧栏 | 「目录」+「查看全部」;节列表宜 `getByText` 精确到节标题。**同课内换节**只要仍在 `/edu/study/courses/{id}`,一律在目录树中点目标节即可(详见用户说法映射) |
780
+ | 播放页 | 辅学 Tab | 「课件」「文稿」「资料」「笔记」(在 `main` 内缩小作用域避免与侧栏重复);详见下文与 S-23/S-24 |
781
+ | 播放页 | 播放器控件 | 阿里云 Prism(`.prism-player` / `.prism-controlbar`),稳定类名定位见 S-22 |
782
+ | 训练营详情 | 操作 | 主 CTA 随状态变化(实测「报名已结束」disabled);「立即报名」等以线上为准 |
783
+ | 训练营详情 | 内容 | 简介、目录(节含时长)、推荐等 |
784
+ | 直播详情 | 信息 | 标题、预约人数、浏览量、直播时间或「直播已结束,回放可随时观看~」 |
785
+ | 直播详情 | 操作 | `getByRole('button', { name: '查看回放' })`(回放态);未开始场可能示「无需预约」等 |
786
+ | 直播详情 | 推荐 | 「推荐直播」卡片 |
787
+ | 搜索 `/edu/search` | 结果 | 顶栏关键词回显 + 「搜索」;「课程」「直播」两个 `paragraph` Tab 切换结果;「共找到 * 个相关内容」计数 |
788
+
789
+ #### 学习中心 · 课程模块 · 播放页右侧辅学区(课件 / 文稿 / 资料 / 笔记)
790
+
791
+ > **核验记录**(2026-05-08):在 `/edu/study/courses/{courseId}`(`document.title` 含 `学习中-`)对四 Tab 逐项切换并用 `run-code`/`eval` 核对 DOM(wangEditor 工具栏 `data-menu-key`、资料空列表、文稿正文等)。
792
+
793
+ ##### 页面与区域边界
794
+
795
+ | 概念 | 说明 |
796
+ | --- | --- |
797
+ | 播放页 | `pathname` 为 `/edu/study/courses/{courseId}`;右侧辅学区 + 左侧目录树 + 中部 Prism 播放器(见 S-22) |
798
+ | 辅学区 DOM | 业务子应用挂载在 `micro-app[name="edu"]` 内;与左侧「目录」、章节列表共享子树,故**禁止**仅用全页 `getByText('课件')` 而不加 `main`:必须 `page.locator('main').getByText('…', { exact: true })` 切 Tab |
799
+ | Tab 行结构 | 横向 `paragraph`:「课件」「文稿」「资料」「笔记」;右侧一枚 `img`(可点击)实测无统一 `aria-label`,可能用于收起/展开辅学面板,**默认自动化不依赖该 `img`** |
800
+
801
+ ##### 四 Tab 功能清单
802
+
803
+ | Tab | 模块职责 | 主要 UI 与按钮 | 典型验收 |
804
+ | --- | --- | --- | --- |
805
+ | **课件** | 当前小节课件预览(多为 PDF 分页/配图) | 预览区可见多页图片(`img` 随课件变化)。**Chrome 内置 PDF 查看器打开时**:整页 `snapshot` 无法获得阅读器工具栏 ref,固定操作见下文「课件区 · Chrome 内置 PDF 查看器」与 S-25 | 切换后 `main` 内可见「课件」 |
806
+ | **文稿** | 口播稿/逐字稿只读展示 | 无 button 提交类控件;主体为可滚动正文 | 正文含当前小节相关解说(随课程变化) |
807
+ | **资料** | 本节扩展资料列表(附件/链接) | 列表区常见 `共 * 条数据`;空态 `暂无相关数据` | `innerText` 出现 `共 ` + 数字 + ` 条数据` 或空态文案 |
808
+ | **笔记** | 富文本笔记:编辑、格式化、提交、历史列表 | **编辑器**:wangEditor(`main [contenteditable=true]`,滚动容器 `.w-e-scroll`)。**工具栏**:同 `main` 下 `button[data-menu-key]`。**提交**:`getByRole('button', { name: '提交' })`。**列表**:「我的笔记」+ `共 * 条数据`;无数据 `暂无相关数据` | 提交后条数递增;编辑器可 `eval` 读 `innerHTML` |
809
+
810
+ ##### 笔记模块 · 工具栏按钮(`data-menu-key`)
811
+
812
+ 均在 `main` 内定位(如 `main.locator('button[data-menu-key=bold]')`)。
813
+
814
+ | `data-menu-key` | 能力 | 标准化操作提示 |
815
+ | --- | --- | --- |
816
+ | `bold` / `underline` / `italic` | 加粗 / 下划线 / 斜体 | 先 `click` 再输入;再点一次关闭 |
817
+ | `uploadImage` | 插入图片 | 触发系统文件选择;无人值守通常不可跨平台稳定完成 |
818
+ | `bulletedList` / `numberedList` | 无序 / 有序列表 | `click` 后输入列表项 |
819
+ | `blockquote` | 引用块 | `click` 后输入 |
820
+ | `redo` / `undo` | 重做 / 撤销 | 对当前编辑器历史栈操作 |
821
+ | `emotion` | 表情 | 弹出浮层;表情项不固化定位器 |
822
+
823
+ **占位提示**:编辑器空态常见 `请输入笔记内容…`(占位非独立 `textbox`;笔记区共 12 个工具栏 + 「提交」)。
824
+
825
+ ##### 可复用 · 标准化操作
826
+
827
+ **A. 切换辅学 Tab(必用 `main`,推荐 `run-code`)**
828
+
829
+ ```js
830
+ async page => {
831
+ await page.locator('main').getByText('<Tab>', { exact: true }).click();
832
+ }
833
+ ```
834
+
835
+ - **验收**:`location.pathname` 仍为 `/edu/study/courses/...`(切换 Tab 不改路由);辅学区 `innerText` 出现对应模块特征(如笔记出现「我的笔记」或「提交」)。
836
+
837
+ **B. 文稿**:切换后即可;长内容 `mousemove`/`mousewheel` 在 `micro-app[name=edu]` 区域按需滚动。
838
+
839
+ **C. 资料**:切换后验收 `共 * 条数据` 或 `暂无相关数据`;有链接时 `click` 或下载以线上控件为准。
840
+
841
+ **D. 笔记 · 纯文本提交**
842
+
843
+ 1. `main → getByText('笔记', { exact: true }).click()`
844
+ 2. `main.locator('[contenteditable=true]').first().click()`
845
+ 3. `keyboard.press('Control+a')` → `Backspace` 清空(按需)
846
+ 4. `keyboard.type('<正文>')`
847
+ 5. `main.getByRole('button', { name: '提交' }).click()`
848
+ 6. **验收**:「我的笔记」`共 N 条数据` 且 `N≥1`
849
+
850
+ **E. 笔记 · 局部富文本(仅一句加粗)**
851
+
852
+ 1. 同 D 的 1~3
853
+ 2. `main.locator('button[data-menu-key=bold]').click()`
854
+ 3. `keyboard.type('已学习第三章')`
855
+ 4. `main.getByRole('button', { name: '提交' }).click()`
856
+ 5. **验收**:可 `eval` 读 `main [contenteditable=true]` 的 `innerHTML` 是否含 `<strong>`/`font-weight`
857
+
858
+ **F. 笔记 · 混合(普通 + 加粗)**
859
+
860
+ 1. 清空编辑器
861
+ 2. `keyboard.type('前缀普通字')`
862
+ 3. `click` → `button[data-menu-key=bold]`
863
+ 4. `keyboard.type('加粗部分')`
864
+ 5. `click` → `button[data-menu-key=bold]`(关闭加粗)
865
+ 6. `keyboard.type('后缀普通字')`
866
+ 7. `click` → 「提交」
867
+
868
+ ##### Shell 与 CLI 约束(Windows)
869
+
870
+ - `helixlife-v5-cli click "main >> getByText('资料')"` 类链式选择器在 PowerShell 下易被错误解析;推荐一律用 `run-code` 执行 `page.locator('main').getByText(...).click()`。
871
+ - `eval` 字符串含逗号时易被 shell 拆参;复杂逻辑用 `run-code`。
872
+
873
+ ##### 与快照 ref 的关系(本节)
874
+
875
+ - Tab 切换、笔记工具栏、提交按钮:优先 `getByText(..., { exact: true })` + `data-menu-key` + `getByRole('button', { name: '提交' })`,不需要每次 `snapshot` 找 `e*`。
876
+ - 资料链接 / 附件行:改版后用 `snapshot --depth=8` 定位后回写稳定 `getByRole`/文本。
877
+
878
+ ##### 课件区 · Chrome 内置 PDF 查看器(标准 frame + ARIA)
879
+
880
+ > **跳页对用户回复门禁**:见上文「PDF 页码跳转 · Agent 硬门禁」。
881
+
882
+ > **核验记录**(2026-05-08):PDF 由内嵌 `iframe` 调起 Chrome 内置 PDF 查看器。下表枚举工具栏控件(含 shadow DOM 内 `aria-label`),可自动化项均通过 `run-code` 单次成功(缩放、适应页面、逆时针旋转、打开/关闭边栏、缩放级别填值、页码跳转)。
883
+ >
884
+ > - **侧栏缩略图**:点缩略图前侧栏必须为展开态,否则 `getByRole('tab', { name: '第 N 页的缩略图' })` 会超时。
885
+ > - **绘制**:单次 `click` 成功;**撤销/重做**在无标注历史时为 `disabled`。
886
+ > - **下载/打印/更多/Google 云端/关闭**:未端到端核验(易触发浏览器或系统对话框)。
887
+
888
+ **与整页 `snapshot` 的关系**:父页面树中课件区常表现为空 `iframe`;所有阅读器操作必须在内置查看器 Frame 上进行,不要用顶层 `snapshot` 的 `e*` 去点缩放与页码。
889
+
890
+ ###### P-框架:解析 PDF 控件 Frame
891
+
892
+ 内置查看器顶层文档 URL `pathname` 为 `…/index.html`,`page.frames()` 中可依前缀筛选:
893
+
894
+ - **常量**:扩展 ID 前缀 `chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai`
895
+ - **禁止**:误取子 frame(仅承载 `.pdf` 网络地址、无工具栏 DOM)。
896
+ - **取得 `Frame`(下文记为 `fr`)**:在 `run-code --filename=<脚本>` 所加载的 `async page => …` 内对 `page.frames()` `find`,命中第一个 `frame.url()` 以该前缀开头的项即为 `fr`;后续 `fr.getByRole`/`fr.getByLabel` 与下文表格一致。
897
+
898
+ ###### 执行方式(`scripts/` + `run-code --filename`)
899
+
900
+ | 脚本 | 作用 | 备注 |
901
+ | --- | --- | --- |
902
+ | `scripts/chrome-pdf-goto-page.js` | 页码跳转 | 编辑文件中 `target` 为目标第 N 页(1 起计),再 `run-code --filename=scripts/chrome-pdf-goto-page.js`;脚本内已用三击 + 单次 Enter + 读回验收 |
903
+ | `scripts/chrome-pdf-zoom-out.js` | 点击「缩小」 | `run-code --filename=scripts/chrome-pdf-zoom-out.js` |
904
+ | `scripts/chrome-pdf-enumerate-a11y.js` | 枚举当前 `aria-label`(含 shadow) | 维护/改版排障时用 |
905
+
906
+ 其余控件(放大、适应页面、旋转、绘制等):可复制 `chrome-pdf-zoom-out.js` 改 `name` 或合并多 `await` 到同脚本后 `run-code --filename`。
907
+
908
+ ###### 控件全量清单(简体中文 Chrome)
909
+
910
+ 无障碍名 `name` 与 Playwright `getByRole(..., { name })` / `getByLabel` 完全一致。
911
+
912
+ | 能力分组 | UI 文案 | DOM 摘要 | 推荐 Playwright API | 核验 | 说明 |
913
+ | --- | --- | --- | --- | --- | --- |
914
+ | 侧栏 | 打开/关闭边栏 | `cr-icon-button` `role=button` | `fr.getByRole('button', { name: '打开/关闭边栏' }).click()` | 已核验 | **缩略图导航前置**:要点第 N 页缩略图前,若超时,先点此直至侧栏展开 |
915
+ | 导航 · 直达页 | 第 N 页的缩略图(示例:第 4 页) | `viewer-thumbnail` `role=tab` | `fr.getByRole('tab', { name: '第 4 页的缩略图' }).click()`(把 `4` 换为 `N`) | 已核验(先展开侧栏) | `name` 全串必须为 `第 {N} 页的缩略图`(语言变更需改写) |
916
+ | 导航 · 页码输入 | 页码 | `input`,`aria-label=页码` | `fr.getByLabel('页码')`:`click({ clickCount: 3 })` → `fill('<N>')` → `press('Enter')` 一次 | 已核验 | 优先于 `getByRole('spinbutton', { name: '页码' })`(后者重复超时)。勿单次轻点后直接 `fill`;勿连按两次 Enter 或跳后再按下一页(见用户说法映射) |
917
+ | 缩放 · 按钮 | 缩小 / 放大 | `role=button` | `fr.getByRole('button', { name: '缩小' \| '放大' }).click()` | 已核验 | 可串联多次 |
918
+ | 缩放 · 输入 | 缩放级别 | `input`,`aria-label=缩放级别` | `fr.getByLabel('缩放级别')`:`click()` → `fill('<百分比>')` → `press('Enter')` | 已核验(示例 `100`) | 失败时用放大/缩小回退 |
919
+ | 版式 | 适应页面 | `role=button` | `fr.getByRole('button', { name: '适应页面' }).click()` | 已核验 | 一键恢复版面 |
920
+ | 旋转 | 逆时针旋转 | `role=button` | `fr.getByRole('button', { name: '逆时针旋转' }).click()` | 已核验 | 本次枚举未见独立「顺时针旋转」 |
921
+ | 标注 | 绘制 | `role=button` | `fr.getByRole('button', { name: '绘制' }).click()` | 已核验(单次) | 进入绘制后才有撤销栈 |
922
+ | 标注 | 撤销 / 重做 | `role=button`(常 `disabled`) | `fr.getByRole('button', { name: '撤销' \| '重做' }).click()` | 有条件 | 仅在有可撤销操作时;勿用 `force` 规避 |
923
+ | 工具 | 下载 / 打印 / 更多操作 / 保存到 Google 云端硬盘 / 关闭 | `role=button` | `fr.getByRole('button', { name: '<对应名>' }).click()` | 未核验 | 易触发下载或系统对话框;默认跳过 |
924
+ | 大纲 · 书签 | 章节标记 | 多枚 `cr-icon-button` 同源文案 | `fr.getByRole('button', { name: '章节标记' }).nth(k)` | 未逐项核验 | 必须用 `.nth()` 或上下文缩小,勿 strict |
925
+
926
+ **语言脚注**:表中 `name` 文案取自 Chrome 界面语言为简体中文版本。跨平台/语言时用 `scripts/chrome-pdf-enumerate-a11y.js` 枚举的 `aria-label` 替换上表 `name`。
927
+
928
+ **拼装建议**:多步可在同一脚本顺序 `await` 后一次 `run-code --filename`;只做页码导航时可避免反复开关侧栏。
929
+
930
+ > **快照 ref 反模式**:禁止在 `vip.helixlife.cn` 顶层 `snapshot` 中选 `iframe` 下第一个 `img/generic` 当「缩小」——不可用。必须用 `fr` + `getByLabel('页码'|'缩放级别')` + `getByRole('button'|'tab', { name: '…' })`。
931
+
932
+ #### 学习中心 · 稳定定位器速查(补充表)
933
+
934
+ | 控件 | 推荐定位器 | 备注 |
935
+ | --- | --- | --- |
936
+ | 进入学习中心 | `div.side-menu--item[to='/edu/courses']` | 与全局侧栏表一致 |
937
+ | 顶栏搜索框 | `getByRole('textbox', { name: '请输入搜索关键词' })` | 列表/搜索页通用 |
938
+ | 顶栏搜索按钮 | `getByRole('button', { name: '搜索' })` | 点击后常**新开标签**(S-18) |
939
+ | 一级 Tab | `getByText('课程')`、`getByText('训练营')`、`getByText('直播')` | 必要时 `.first()` 或缩小到 `main` |
940
+ | 训练营子类 | `getByText('7天主题营')` 等 | 须先展开训练营 Tab |
941
+ | 直播子类 | `getByText('名师直播')` 等 | 须先展开直播 Tab |
942
+ | 课程 · 最新/最热 | `getByText('最新')`、`getByText('最热')` | 与直播/训练营列表同文案 |
943
+ | 课程 · 仅看我的学习 | `getByRole('checkbox', { name: '只显示我学习的' })` | |
944
+ | 训练营 · 仅看我的报名 | `getByRole('checkbox', { name: '只显示我报名的' })` | |
945
+ | 直播 · 仅看我的预约 | `getByRole('checkbox', { name: '只显示我预约的' })` | |
946
+ | 打开某一门课 | `getByText('<课程完整标题>')` | 标题以列表为准,**精确匹配** |
947
+ | 继续学习 | `getByRole('button', { name: '继续学习' })` | 仅在 `/edu/courses/…/learn` |
948
+ | 直播回放 | `getByRole('button', { name: '查看回放' })` | 详情页,已结束且有回放时 |
949
+ | 搜索页 · 课程/直播筛 | `main getByText('课程')` + `main getByText('直播')` | 必要时加 `main` |
950
+ | 搜索页 · 卡片 · 匹配说明 | `main → getByText('部分章节匹配', { exact: true })`(多卡时先缩到目标) | 查看类 → `hover`;进入/点名 → `click`,见 S-18 第 5 步 |
951
+ | 播放页 · 课件 · Chrome PDF | 见「课件区 · Chrome 内置 PDF 查看器」;流程 S-25 | `run-code --filename=scripts/chrome-pdf-*.js`;勿对顶层 `snapshot` ref(`e*`)操作阅读器 |
952
+
953
+ #### 学习中心 · 课程播放器(`/edu/study/courses/{id}` · 阿里云 Prism)
954
+
955
+ > **核验记录**(2026-05-07):在真实播放页(`学习中-…` 标题、`document.querySelector('video')` 存在)逐项验收:唤醒控件、暂停/播放、进度条跳转、清晰度、倍速、音量开关、进入/退出全屏。
956
+
957
+ **播放内核**:阿里云 Prism(控制条 `.prism-controlbar`,根容器 `.prism-player`)。勿用 `hover video`:易因 pointer 被拦截超时;控件显隐依赖指针在画面区域内移动。
958
+
959
+ ##### P-00:先「唤醒」控制条(所有播放器操作前置)
960
+
961
+ 1. **推荐**:用 `helixlife-v5-cli --raw eval` 读 `video.getBoundingClientRect()`,计算 `(cx, cy) = (left + width/2, bottom - 50)`,再 `helixlife-v5-cli mousemove <cx> <cy>`。全屏或窗口尺寸变化后须重新计算。
962
+ 2. **禁止**:`hover video`——易被 Prism 叠加层拦截。
963
+ 3. **验收**:执行后 `.prism-controlbar` 的 `innerText` 应出现 `超清`/`倍速`/时间码片段;或 `snapshot ".prism-controlbar"` 可见子项。
964
+
965
+ ##### 稳定定位器
966
+
967
+ | 能力 | 推荐选择器 | 说明与验收 |
968
+ | --- | --- | --- |
969
+ | 暂停/播放 | `locator('.prism-play-btn')` | P-00 后 `click`;`--raw eval "document.querySelector('video').paused"` 应在 `true`/`false` 间切换 |
970
+ | 快进/进度(跳播) | `locator('.prism-progress')` | P-00 后 `click`:按点击位置 seek;默认 `click()` 落点近似条中。需按时间比例精调:`run-code` 内对 `.prism-progress` 取 `boundingBox()` 后 `page.mouse.click(box.x + box.width * ratio, box.y + box.height/2)` |
971
+ | 清晰度 | `locator('.current-quality')` → 再点列表项 | 先 `click '.current-quality'` 展开 `.quality-list`;选项 `li[data-def]`:`LD`=标清、`SD`=高清、`HD`=超清。**推荐** `locator('.quality-list li[data-def=SD]')` 避免歧义。验收:`.current-quality` 文案或 `data-def` 一致 |
972
+ | 倍速 | `locator('.speed-btn')` → `locator('.speed-list').getByText('1.5X')` 等 | 选项 `.speed-li`(3.0X / 2.0X / 1.5X / 1.25X / 1.0X / 0.5X)。验收:`--raw eval "document.querySelector('video').playbackRate"` |
973
+ | 声音开关(静音) | `locator('.prism-volume')` | P-00 后 `click`;勿仅点 `.volume-icon`(未切 `muted`)。验收:`video.muted`、`video.volume`。仅用 `eval` 改 `muted`/`volume` 时 UI 图标可能不同步,见下文 |
974
+ | 调整音量(非静音比例) | `.volume-icon` | 先 P-00 再 `hover` → `locator('.volume-icon')`;竖向滑条多不在无障碍快照树。精调建议 `run-code` 内对可见滑块区域 `drag`/`mouse` 点击;或 `eval` 设 `video.volume` 仅作调试旁路 |
975
+ | 全屏 | `locator('.prism-fullscreen-btn')` | P-00 后 `click`;验收:`document.fullscreenElement !== null` |
976
+ | 退出全屏 | 见 S-22 第 9 步 | 全屏下控制条常默认隐藏,`click` 可能 not visible |
977
+
978
+ ##### 音量:`video` 与 Prism UI 同步
979
+
980
+ - **现象**:仅用 `eval`/`run-code` 直接写 `video.muted`/`.volume` 时,实际出声或音量可对,但 Prism 控制条上的静音按钮/音量图标仍停留在旧状态(播放器内核与 Prism UI 状态未绑定在同一更新路径)。
981
+ - **原则**:涉及开声/关声/调比例若需图标一致,应**同时处理 `<video>` 与 UI**:
982
+ - 优先(与图标一致):开/关声音一律 P-00 → `click` → `locator('.prism-volume')`(必要时连点两次);比例优先 `hover .volume-icon` + 滑条 / `run-code` 点滑条。
983
+ - 已用 `eval` 改过后需对齐图标:再 P-00,根据 `video.muted` 与界面是否一致补点 `.prism-volume`(一次 `click` 切换一次静音);最省事是避免混用。
984
+ - 调试专用:仅 `eval` 音量、不要求 UI 一致时可沿用调试旁路。
985
+
986
+ ##### 易错点
987
+
988
+ - **控制条消失**:两次操作间隔过长会自动隐藏,重复 P-00 即可。
989
+ - **下拉项「存在但不可点」**:清晰度/倍速 `li` 须在菜单展开且控制条仍可见时点击;若超时,先 P-00 再 `click` 父按钮重开菜单。
990
+ - **`eval` 逗号陷阱**:含未转义逗号会被拆参;复杂表达式用 `run-code` 或单字符串 `eval`。
991
+
992
+ ---
993
+
994
+ ### 标准操作流程(续):登录后
995
+
996
+ #### S-07:通过左侧栏进入指定模块
997
+
998
+ 1. 已处于 `vip.helixlife.cn` 任意路径(若否先 S-03)。
999
+ 2. **仅此一条**:`click` → `div.side-menu--item[to='<目标路径>']`(路径见上表)。
1000
+ 3. **验收**:`location.pathname` 与目标一致(允许 `/workstation` 与 `/workstation/` 等价)。
1001
+
1002
+ #### S-08:首页提问(助手大厅)
1003
+
1004
+ 1. S-07 进入 `/home`。
1005
+ 2. `fill` → `getByRole('textbox', { name: '你可以问我任何和医学科研相关的问题...' })`。
1006
+ 3. `click` → `getByRole('textbox', { name: '你可以问我任何和医学科研相关的问题...' }).locator('..').locator('> div > button').last()`(发送钮)。
1007
+ 4. **验收**:出现对话气泡或 loading;常见 `location.pathname` 变为 `/chat` 且带 `source=home`。
1008
+
1009
+ #### S-09:AI 对话 · 新建会话
1010
+
1011
+ 1. S-07 → `/chat`。
1012
+ 2. `click` → `getByRole('button', { name: '新建对话' })`。
1013
+ 3. **验收**:左侧列表新增会话或焦点切换。
1014
+
1015
+ #### S-10:AI 工作站 · 创建任务
1016
+
1017
+ 1. S-07 → `/workstation/`。
1018
+ 2. `click` → `getByRole('button', { name: '创建新任务' })`。
1019
+ 3. **验收**:出现 `dialog "创建新任务"`;内含类型切换(「综述」「期刊论著」「基金申请书」…)与表单字段。关闭:`click` → `getByRole('button', { name: 'Close' })`。
1020
+ 4. **后续**:按任务类型填字段属于业务子流程,另拆 S-xx 维护。
1021
+
1022
+ #### S-11:AI 工作台 · 进入子工作台
1023
+
1024
+ 1. S-07 → `/workbench`。
1025
+ 2. `click` → `getByText('SCI论著工作台')`(或目标卡片名称)。
1026
+ 3. **验收(示例)**:「SCI论著工作台」 → `location.pathname` 为 `/workbench/sci/topic`。
1027
+
1028
+ #### S-12:AI 应用 · 标准操作
1029
+
1030
+ > **细则**见上文「AI 应用 `/application/`」(规约 A~H + AP-1~AP-13)。本节为最短串联步骤;每个关键交互后 `snapshot` 一次直至验收通过。
1031
+
1032
+ 1. **进入模块**:AP-1。验收:`pathname` `/application`/`/application/`;`title` 以 `AI应用-` 开头。
1033
+ 2. **(可选)搜索**:AP-2。验收:列表刷新。
1034
+ 3. **(可选)分类筛选**:AP-3(规约 H · 各级独立滚动 · 必选到叶子级)。
1035
+ 4. **(可选)滚动加载更多**:AP-4(规约 B · 先读底部状态 · 鼠标先入容器)。
1036
+ 5. **(可选)`aria-label` 高频图标**(历史记录/回到顶部 等):AP-5(规约 A)。
1037
+ 6. **(可选)收藏/取消收藏**:AP-7/AP-8(取消收藏必接二次确认弹层,见规约 E)。
1038
+ 7. **进入某一应用(新标签)**:AP-6(规约 D);`tab-list` 记录 → `click` 卡片 → `tab-select` 切到新标签。勿在详情标签上 `go-back`。
1039
+ 8. **应用详情 · 多块输入输出**(规约 C/F):
1040
+ - 单块操作(复制某段/重新生成某段)→ 先 `hover` 该块 → AP-9/AP-10;
1041
+ - 整块操作(全部复制等)→ AP-11;
1042
+ - 勿把 `复制` 与 `全部复制` 互换。
1043
+ 9. **(可选)mindmap**:缩放 → AP-12(`hover` 画布内);滚动页面/外层 → AP-13(`hover` 画布外、容器内安全空白区)。
1044
+ 10. **收尾**:返回列表用 `tab-close`(详情标签)→ `tab-select` 回 `/application` 标签。
1045
+
1046
+ #### S-13:数据分析 · 标准操作(列表态)
1047
+
1048
+ > **细则**见上文「数据分析 `/analysis/`」全节;工具详情见 S-28,拼图工具子页见 S-27。本节为最短串联步骤。
1049
+
1050
+ 1. **进入模块**:S-07 → `div.side-menu--item[to='/analysis']`。验收:`pathname` 为 `/analysis`/`/analysis/`,`title` 以 `数据分析-` 开头。
1051
+ 2. **(可选)全局设置**:`click` → `locator('main').getByText('全局设置')` → 弹窗内调整配色方案/全局字体/颜色面板 → `保存` 或 `重置` → `click` → `getByRole('dialog', { name: '全局设置' }).getByRole('button', { name: 'Close' })`。
1052
+ 3. **(可选)形态 Tab**:`click` → `getByText('拼图工具', { exact: true })` → 验收 `/analysis/jigsaw`。回列表优先 `click` → `div.side-menu--item[to='/analysis']` 或顶栏主分类「分析工具」。
1053
+ 4. **(可选)搜索**:`fill` → `getByRole('textbox', { name: '请输入搜索关键词' })` → `click` → `getByRole('button', { name: '搜索' })`(找工具,非切主分类)。
1054
+ 5. **(可选)切换分类**(细则见「分类体系 · Agent 决策树」):
1055
+ - **子分类**:`click` 标签条文案 → 验收 `subcategory=`;按需点「展开」/「收起」验收 `isExpanded`。
1056
+ - **主分类**:`click` 顶栏当前主分类名展开下拉 → `click` 目标 → 验收 `primary=` 且顶栏文案更新;**清空搜索框残留**后再验收列表。
1057
+ 6. **(可选)收藏/取消收藏**:目标卡片上 `click` → `收藏` → 验收 `已收藏`;再 `click` → `已收藏` → 回 `收藏`。
1058
+ 7. **进入某一工具详情**:`main` 内 `click` → `getByText('<卡片主标题>', { exact: true })`。验收 `pathname` 为 `/analysis/{toolId}`;面包屑「数据分析 / {工具名}」。后续 → S-28(D-1~D-11)。
1059
+
1060
+ #### S-27:数据分析 · 拼图工具子页(`/analysis/jigsaw`)
1061
+
1062
+ > **细则**见上文「数据分析 · 拼图工具页」(J-1~J-13)。本节为最短串联路径。
1063
+
1064
+ 1. **进入**:S-13 步骤 1 → 步骤 3(或 J-1)。验收 `/analysis/jigsaw`。
1065
+ 2. **(可选)ABC 标注**:J-3 展开 → 按需 J-4~J-9。
1066
+ 3. **(可选)生信工具**:J-10。
1067
+ 4. **(可选)拖入拼图**:J-11(落点 `.analysis-right-box`)。
1068
+ 5. **(可选)参考线**:J-12。
1069
+ 6. **(可选)导出**:J-13(须先完成步骤 4)。
1070
+
1071
+ #### S-28:数据分析 · 工具详情页(`/analysis/{toolId}`)
1072
+
1073
+ > **细则**见上文「数据分析 · 工具详情页」(D-1~D-11)。本节为最短串联路径;关键步骤后 `snapshot` 一次。
1074
+
1075
+ 1. **进入**:S-13 步骤 1 → 步骤 4(可选搜索) → 步骤 7。验收 `/analysis/{toolId}`;面包屑含工具名。
1076
+ 2. **(可选)上传数据**:D-2 **三步弹窗**(`getByLabel('上传文件 - 打开上传弹窗')` → 弹窗内选文件 → 弹窗内「确认」;细则见「上传文件 · Agent 硬门禁」)→ D-3(`验证成功`)。仅用默认样例名且校验要求真实上传时必须完成 D-2。
1077
+ 3. **(可选)主要参数**:按需 D-4/D-5。
1078
+ 4. **生成结果**:D-6。验收:右侧出现「主要结果」等,无「暂无内容…」。
1079
+ 5. **(可选)结果 Tab / 保存下载**:D-11、D-10。
1080
+ 6. **(可选)数据参数重置**:D-1(可能需重新上传)。
1081
+ 7. **(可选)顶栏**:D-7 全局设置、D-8 教程文档、D-9 其它文档入口。
1082
+
1083
+ #### S-14:学习中心 · 侧栏切入与「课程 / 训练营 / 直播」骨架
1084
+
1085
+ 1. S-07 → `click` → `div.side-menu--item[to='/edu/courses']`。
1086
+ 2. **验收**:`location.pathname` 为 `/edu/courses`;`title` 以 `课程-` 开头;`main` 内可见顶栏「搜索」与「课程/训练营/直播」。
1087
+ 3. **切换一级形态**(按需,均禁止对 `/edu/**` 使用 `goto`):
1088
+ - **课程**:`click` → `getByText('课程')`(若当前在 `/edu/trainings` 或 `/edu/lives`,更稳为 `click` 侧栏 `to='/edu/courses'`)。
1089
+ - **训练营**:`click` → `getByText('训练营')` → 展开后 `click` → `getByText('<子类名>')`(如 7天主题营)。验收:`pathname` 为 `/edu/trainings` 且带 `category=`;`title` 以 `训练营-` 开头。
1090
+ - **直播**:`click` → `getByText('直播')` → `click` → `getByText('<子类名>')`(如名师直播)。验收:`pathname` 为 `/edu/lives` 且带 `category=`;`title` 以 `直播-` 开头。
1091
+ 4. **优化**:子类名、讲师名以运营配置为准;失效时用 `snapshot` 更新本文枚举,不要猜 `category` UUID。
1092
+
1093
+ #### S-17:学习中心 · 课程列表排序与个人课单筛选
1094
+
1095
+ 1. S-14 已处于 `/edu/courses`。
1096
+ 2. 排序(可选):`click` → `getByText('最新')` 或 `getByText('最热')`。
1097
+ 3. 仅看已学(可选):`check`/`uncheck` → `getByRole('checkbox', { name: '只显示我学习的' })`。
1098
+ 4. **验收**:列表刷新或筛选状态符合预期(`snapshot` 看卡片集合变化)。
1099
+
1100
+ #### S-18:学习中心 · 顶栏关键字搜索(注意新标签)
1101
+
1102
+ 1. 在任意列表页(常 `/edu/courses`):`fill` → `getByRole('textbox', { name: '请输入搜索关键词' })`;`click` → `getByRole('button', { name: '搜索' })`。
1103
+ 2. **验收**:出现**新标签**,URL 为 `https://vip.helixlife.cn/edu/search?keywords=…`;`title` 为 `搜索-…`。继续操作需 `tab-list` → `tab-select <新索引>`。
1104
+ 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 更新。
1105
+ 4. **收尾**(可选):`tab-close` → `tab-select 0` 回学习中心。
1106
+ 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`。
1107
+
1108
+ #### S-19:学习中心 · 课程详情页与进入播放
1109
+
1110
+ 1. S-14 课程列表下:`click` → `getByText('<课程标题>')`(须与卡片完全一致)。
1111
+ 2. **验收 A**:`pathname` 匹配 `/edu/courses/{id}/learn`;可见「继续学习」、「简介」「目录」、「讲师」「推荐课程」等。
1112
+ 3. 进入播放器:`click` → `getByRole('button', { name: '继续学习' })`。
1113
+ 4. **验收 B**:`pathname` 变为 `/edu/study/courses/{id}`;`title` 含 `学习中-`;可见「目录」「查看全部」与辅学「课件 / 文稿 / 资料 / 笔记」。
1114
+ 5. **目录切节**(同课内任意小节:视频/PDF 课件/作业·小测):在 `main` 内 `click` → `getByText('<节完整标题>', { exact: true })`(与目录中节名逐字一致)。用户要「下一节/下一作业/下一份 PDF」等时:按大纲顺序查找目标节直接点击;切到视频节后 UI 见 S-22;切到 PDF 课件节后辅学/阅读器见 S-23/S-25。**禁止**在已处于 `/edu/study/courses/{id}` 时为切到同课内另一节而先回 `/edu/courses/{id}/learn` 再绕一圈。
1115
+ 6. **辅学 Tab/笔记**(可选):切换课件/文稿/资料/笔记、资料列表验收、笔记编辑提交 → S-23、S-24。
1116
+
1117
+ #### S-22:学习中心 · 课程播放器 · 标准控件遍历(Prism)
1118
+
1119
+ > 前置:S-19 或已处于 `/edu/study/courses/{id}`,`document.title` 含 `学习中-`。
1120
+
1121
+ 1. **P-00 唤醒控制条**:`--raw eval` 取 `video.getBoundingClientRect()` → `mousemove` 至 `(left+width/2, bottom-50)`;必要时 `snapshot ".prism-controlbar"` 验收。
1122
+ 2. **暂停/播放**:`click` → `locator('.prism-play-btn')`;`eval` 查 `video.paused`。
1123
+ 3. **进度/快进**:`click` → `locator('.prism-progress')`(或 `run-code` 按 `ratio` 精确点击);`eval` 查 `video.currentTime`。
1124
+ 4. **清晰度**:`click` → `locator('.current-quality')` → `click` → `locator('.quality-list li[data-def=SD]')`(`SD`/`LD`/`HD`);复核 `.current-quality`。
1125
+ 5. **倍速**:`click` → `locator('.speed-btn')` → `click` → `locator('.speed-list').getByText('<如 1.5X>')`;`eval` 查 `video.playbackRate`。
1126
+ 6. **静音开关**:`click` → `locator('.prism-volume')`(两次可恢复);`eval` 查 `muted`/`volume`。勿仅靠 `eval` 改 `muted`;若需图标一致见上文「音量同步」。
1127
+ 7. **音量微调**(可选):`hover` → `locator('.volume-icon')` 后 `run-code` 拖滑块;或仅记录步骤 6。`eval` 改 `video.volume` 同样需要 UI 同步。
1128
+ 8. **全屏**:`click` → `locator('.prism-fullscreen-btn')`;`eval`:`fullscreenElement !== null`。
1129
+ 9. **退出全屏**:优先 `run-code`:`async page => { await page.evaluate(() => document.exitFullscreen()); }`;若控制条可见,亦可再次 `click` → `locator('.prism-fullscreen-btn')`。
1130
+ 10. **收尾**:关键交互各 1 次快照即可。
1131
+
1132
+ #### S-23:学习中心 · 播放页 · 辅学 Tab(课件 / 文稿 / 资料 / 笔记)
1133
+
1134
+ > 前置:同 S-22 播放页;本节不涉及 Prism。
1135
+
1136
+ 1. **切换 Tab(必选写法)**:`run-code` 内:`await page.locator('main').getByText('<课件|文稿|资料|笔记>', { exact: true }).click()`。禁止省略 `main`。
1137
+ 2. **课件**:切换后若需在内嵌 PDF 阅读器内缩放/翻页/旋转等 → S-25(勿用顶层 `snapshot` ref 点阅读器)。
1138
+ 3. **文稿**:验收 `micro-app[name=edu]` 内出现连续口播正文(不写死句子)。
1139
+ 4. **资料**:验收 `共 * 条数据` 或 `暂无相关数据`;有条目时再 `click` 下载/打开。
1140
+ 5. **笔记**:仅切换到 Tab → 新建/编辑提交见 S-24。
1141
+ 6. **验收**:`location.pathname` 不变;各 Tab 对应区块特征与「四 Tab 功能清单」一致。
1142
+
1143
+ #### S-24:学习中心 · 播放页 · 笔记 · wangEditor 输入与提交
1144
+
1145
+ > 前置:S-23 已切换到「笔记」;编辑器为 wangEditor(`main [contenteditable=true]`)。
1146
+
1147
+ 1. `run-code`:`await page.locator('main').getByText('笔记', { exact: true }).click()`(已在可跳过)。
1148
+ 2. **聚焦编辑器**:`await page.locator('main').locator('[contenteditable=true]').first().click()`。
1149
+ 3. **清空(按需)**:`await page.keyboard.press('Control+a')` → `Backspace`。
1150
+ 4. **格式化(按需)**:对 `main.locator('button[data-menu-key=<bold|underline|italic|…>]')` 逐 `click`(详见上文工具栏表)。
1151
+ 5. **输入正文**:`await page.keyboard.type('<文本>')`(中文实测可用)。
1152
+ 6. **提交**:`await page.locator('main').getByRole('button', { name: '提交' }).click()`。
1153
+ 7. **验收**:「我的笔记」`共 N 条数据`,`N≥1`;或列表出现新笔记。**插入图片**(`uploadImage`)涉及系统文件框 → 默认不作为无人值守必选步骤。
1154
+ 8. **优化**:同一 `main` 路径完成编辑,避免 `goto`;失败时用 `snapshot` 对照是否被遮挡。
1155
+
1156
+ #### S-25:学习中心 · 播放页 · 课件 Tab · Chrome 内置 PDF 阅读器
1157
+
1158
+ > 前置:S-23 已切换到「课件」,且小节课件为 PDF(纯图片拼图不适用)。**一律 `helixlife-v5-cli run-code --filename=<脚本>`**,脚本内解析 Frame `fr`(见「P-框架」「执行方式」)。
1159
+
1160
+ > **对用户回复门禁**:凡用户指令含「跳到/翻到 PDF 第 N 页」等,须完整执行本节跳页步骤并完成步骤 2 的读回验收后,方可向用户表述成功;验收标准与禁止项见上文「PDF 页码跳转 · Agent 硬门禁」。
1161
+
1162
+ 1. **执行**:仓库根 `helixlife-v5-cli run-code --filename=scripts/chrome-pdf-goto-page.js`(跳转前 edit `target`);若抛错「未找到 frame」,确认课件已加载、当前 Tab 为课件。
1163
+ 2. **翻页(标准,推荐)**:`await fr.getByLabel('页码').click({ clickCount: 3 });` → `fill('<目标页>')` → `press('Enter')` 一次。**验收**:`await fr.getByLabel('页码').inputValue()` 须等于 `String(目标页)`;未相等则不得向用户报成功。
1164
+ 3. **翻页(侧栏缩略图)**:先 `await fr.getByRole('button', { name: '打开/关闭边栏' }).click()`(仅在侧栏未展开或 tab 定位超时时);再 `await fr.getByRole('tab', { name: '第 ' + String(N) + ' 页的缩略图' }).click()`。
1165
+ 4. **缩小/放大/适应页面/逆时针旋转**:对各 `name` `click()` 一次或多次。
1166
+ 5. **缩放级别**:`fr.getByLabel('缩放级别')` → `fill` 目标百分比 → `Enter`;不受理时改用步骤 4。
1167
+ 6. **绘制/撤销/重做**:仅在任务明确要求标注时执行;撤销/重做前须存在可撤销栈,否则跳过(避免 30s 超时)。
1168
+ 7. **下载/打印/更多/Google/关闭**:默认跳过;用户显式要求时见「控件全量清单」风险列。
1169
+ 8. **收尾**:每类能力至多 `snapshot` 一次;改版时运行 `scripts/chrome-pdf-enumerate-a11y.js`,将 `aria-label` 变更回写控件清单表。
1170
+
1171
+ #### S-20:学习中心 · 训练营列表与详情
1172
+
1173
+ 1. S-14 进入训练营某一子类列表(`/edu/trainings?category=…`)。
1174
+ 2. 筛选(可选):`getByText('全部')`;最新/最热;`getByRole('checkbox', { name: '只显示我报名的' })`。
1175
+ 3. 打开详情:`click` → `getByText('<训练营标题>')`。
1176
+ 4. **验收**:`pathname` 为 `/edu/trainings/{id}`;可见简介、目录与报名类按钮(如「报名已结束」disabled)。
1177
+
1178
+ #### S-21:学习中心 · 直播列表、讲师筛选与详情
1179
+
1180
+ 1. S-14 进入直播某一子类列表(`/edu/lives?category=…`)。
1181
+ 2. 讲师筛选(可选):`click` → `getByText('全部')` 或某讲师(如 `雪球`)。
1182
+ 3. 仅看预约(可选):`getByRole('checkbox', { name: '只显示我预约的' })`。
1183
+ 4. 打开详情:优先 `click` → `getByText('<直播完整标题>')`(「查看详情」在部分布局下不可见,不宜作为唯一依赖)。
1184
+ 5. **验收**:`pathname` 为 `/edu/lives/{id}`;含预约人数、浏览量、直播时间或结束说明;回放场可见 `getByRole('button', { name: '查看回放' })`。未开始场可能出现「无需预约」等,不强制预约按钮。
1185
+
1186
+ #### S-15:用户菜单 · 退出登录
1187
+
1188
+ 1. `click` → `locator('.side-wrap__content .rounded-full.bg-bg-default.cursor-pointer').first()`(见「用户头像与菜单」);若 strict/类名变更,回退 `locator('.m-sidebar-wrap .rounded-full.bg-bg-default').first()`。
1189
+ 2. 菜单展开后 `click` → `getByText('退出登录')`(若命中隐藏节点,用 `snapshot` 取菜单内 ref 再点)。
1190
+ 3. **验收**:回到未登录态(常见 `https://vip.helixlife.cn/` 营销根路径或通行证)。
1191
+
1192
+ #### S-26:用户菜单 · 进入个人中心(账号设置)
1193
+
1194
+ > 用户口中「个人中心」与菜单项「设置」所指相同;菜单内无「个人中心」文案。
1195
+
1196
+ 1. 已登录 vip.helixlife.cn 下任意页面(若未登录先 S-03)。
1197
+ 2. **打开头像菜单**:与 S-15 步骤 1 相同。
1198
+ 3. `click` → `getByText('设置')`。
1199
+ 4. **验收**:`location.pathname` 为 `/user/account`;`document.title` 常见前缀 `账号设置-`;主区可见「账号设置」及账号安全、第三方绑定、注销等区块。
1200
+
1201
+ #### S-16:切换深浅色模式(用户指令「切换颜色」)
1202
+
1203
+ 1. 已登录且处于 vip.helixlife.cn 下任意页面(若未登录先 S-03)。
1204
+ 2. `click` → `locator('.side-wrap__content a.cursor-pointer.flex.justify-center.items-center').first()`(见「底栏三图标」);一次点击完成浅色 ⇄ 深色翻转。勿每次任务重新枚举侧栏查找主题按钮。
1205
+ 3. **验收**:`helixlife-v5-cli --raw eval "document.documentElement.classList.contains('dark')"` — 深色 `true`,浅色 `false`;异常时用 `snapshot` 复核是否误点邮件/备案图标。