helixlife-v5-cli 1.2.3 → 1.2.5

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
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('node:fs');
3
4
  const path = require('node:path');
4
5
  const { spawnSync } = require('node:child_process');
5
6
 
@@ -199,11 +200,67 @@ function helixDoctor(options) {
199
200
  return helixHome(options);
200
201
  }
201
202
 
203
+ /** npm 包内 scripts/ 目录(WorkBuddy 等工作区无 scripts/ 时回退) */
204
+ function packageScriptsDir() {
205
+ return path.join(__dirname, '..', 'scripts');
206
+ }
207
+
208
+ /**
209
+ * run-code --filename=scripts/foo.js:cwd 下不存在时,回退到 helixlife-v5-cli 包内 scripts/
210
+ * @param {string[]} tokens
211
+ * @returns {string[]}
212
+ */
213
+ function resolveRunCodeFilename(tokens) {
214
+ const runIdx = tokens.indexOf('run-code');
215
+ if (runIdx === -1) {
216
+ return tokens;
217
+ }
218
+
219
+ const out = [...tokens];
220
+ for (let i = runIdx + 1; i < out.length; i += 1) {
221
+ let filePath = null;
222
+ let mode = null;
223
+ const t = out[i];
224
+ if (t.startsWith('--filename=')) {
225
+ filePath = t.slice('--filename='.length).replace(/^["']|["']$/g, '');
226
+ mode = 'eq';
227
+ } else if (t === '--filename' && out[i + 1]) {
228
+ filePath = out[i + 1].replace(/^["']|["']$/g, '');
229
+ mode = 'next';
230
+ }
231
+ if (!filePath) {
232
+ continue;
233
+ }
234
+
235
+ const cwdResolved = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
236
+ if (fs.existsSync(cwdResolved)) {
237
+ continue;
238
+ }
239
+
240
+ const base = path.basename(filePath.replace(/\\/g, '/'));
241
+ const pkgScript = path.join(packageScriptsDir(), base);
242
+ if (!fs.existsSync(pkgScript)) {
243
+ continue;
244
+ }
245
+
246
+ if (mode === 'eq') {
247
+ out[i] = `--filename=${pkgScript}`;
248
+ } else {
249
+ out[i + 1] = pkgScript;
250
+ }
251
+ if (process.env.HELIX_DEBUG_SCRIPT_RESOLVE) {
252
+ console.error(`[${PACKAGE_JSON.name}] run-code: ${filePath} → ${pkgScript}`);
253
+ }
254
+ }
255
+ return out;
256
+ }
257
+
202
258
  async function runPassthroughWithVisual(tokens) {
259
+ const resolved = resolveRunCodeFilename(tokens);
203
260
  let visual;
204
261
  let stripped;
205
262
  try {
206
- ({ tokens: stripped, visual } = parseVisualFlags(tokens));
263
+ ({ tokens: stripped, visual } = parseVisualFlags(resolved));
207
264
  } catch (e) {
208
265
  console.error(`[${PACKAGE_JSON.name}] ${e.message}`);
209
266
  return 3;
@@ -276,4 +333,6 @@ module.exports = {
276
333
  buildChildEnv,
277
334
  DEFAULT_OUTPUT_DIR_NAME,
278
335
  runPassthroughWithVisual,
336
+ resolveRunCodeFilename,
337
+ packageScriptsDir,
279
338
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixlife-v5-cli",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Helix(vip.helixlife.cn)精细化浏览器自动化命令行工具。",
5
5
  "main": "helix-cli.js",
6
6
  "bin": {
@@ -24,6 +24,59 @@
24
24
  - **对用户回复的门禁**:在 `await fr.getByLabel('页码').inputValue()` 严格等于 `String(N)` 之前,禁止向用户输出「已跳到 / 已切换到 / 已完成第 N 页」等成功表述。验收未通过时如实说明未完成,给出读到的 `inputValue()` 或脚本抛错摘要、已执行步骤与下一步建议。
25
25
  - **禁止误导性归因**:不得以「跨域 iframe / 同源策略导致父页脚本无法访问子 frame」作为**无法自动化跳页**的结论。父页 JS 同源限制不等于 Playwright 无法在 `fr` 上操作阅读器工具栏。若仍失败应报告**未找到 `fr`、定位超时、语言包下 `aria-label` 变更**等可核验原因,并指向 S-25 与 `scripts/chrome-pdf-enumerate-a11y.js`。
26
26
 
27
+ #### 工作台 · 「扩写前言」等章节 AI · Agent 硬门禁(操作前必审 · 与 PDF 跳页同级)
28
+
29
+ **触发词**(任一命中即适用本门禁):用户说 **「扩写 / 精简 / 润色 / 改写 / 重写 / 优化 + 章节名」**(如「扩写前言」「润色 1. 乳腺癌…」),或 **「校对 + 章节」**,或 **writing 页工具 + 章节**(如「翻译 前言」「降重 摘要」)。
30
+
31
+ **语义(最高优先级)**
32
+
33
+ | 用户说法 | 含义 | **绝不是** |
34
+ | --- | --- | --- |
35
+ | 扩写前言 | AI **重写**「前言」节点的**正文内容** | 提纲树 **添加子级** / **添加同级** |
36
+ | 润色 / 精简 / 改写 + 章节 | 同上,改该节文字 | 在树下 **新增条目** |
37
+
38
+ `snapshot` 里出现 `添加子级` 按钮 **不代表** 用户要添加子级;该按钮**仅**在用户明确说「加子标题 / 加一条同级提纲 / 添加子级」时使用。
39
+
40
+ **禁止(违规则视为任务失败,勿向用户报「已完成扩写」)**
41
+
42
+ - `click` → `getByRole('button', { name: '添加子级' })`
43
+ - `click` → `getByRole('button', { name: '添加同级' })`
44
+ - 在 **未** `run-code` 脚本前,对 outline 提纲树手写 `hover` + `click` 猜图标
45
+ - 因 snapshot 里有「添加子级」文案而推断用户要「扩写」
46
+
47
+ **必须(唯一合法第一步)**
48
+
49
+ ```bash
50
+ # ① 定页
51
+ helixlife-v5-cli --raw eval "location.pathname"
52
+
53
+ # ② 统一入口(WorkBuddy 默认只跑这一条;用环境变量传 userIntent,勿改脚本文件)
54
+ # PowerShell: $env:HELIX_JOVA_INTENT='扩写前言'
55
+ # bash: HELIX_JOVA_INTENT='扩写前言' helixlife-v5-cli run-code --filename=scripts/workbench-jova-dispatch.js
56
+ helixlife-v5-cli run-code --filename=scripts/workbench-jova-dispatch.js
57
+ ```
58
+
59
+ 工作区无 `scripts/` 时 CLI **自动回退**到 `node_modules/helixlife-v5-cli/scripts/` 同名文件;**禁止**复制脚本到 WorkBuddy 目录再改 `userIntent`。
60
+
61
+ 脚本内按 `pathname` 自动路由;**outline / writing 页**在 dispatch 内 **一次 run-code 跑完全流程**(含替换原文),**绝不**点「添加子级」。proofread 仍返回子脚本名。
62
+
63
+ **WorkBuddy · 执行约束(与 PDF 跳页同级)**
64
+
65
+ 1. **只执行一条命令**:上表 `run-code` + `HELIX_JOVA_INTENT`;**禁止**在成功后追加 `hover` / `click e*` / `fill` / `sleep` / `snapshot` 手工重放。
66
+ 2. **验收**:读 `Result` JSON 末尾 `"ok": true` → 向用户报完成;`"ok": false` → 只报 `reason`,**禁止**改用手动逐步操作代替。
67
+ 3. **禁止**:将脚本复制到 `C:/Users/.../WorkBuddy/.../scripts/` 再编辑;禁止用绝对路径指向 WorkBuddy 副本。
68
+ 4. **禁止**:`run-code` 返回 `ok:true` 后仍「深度思考 + snapshot + 逐步 click」——这是导致数分钟慢路径的根因。
69
+
70
+ **子脚本(仅 proofread 等 dispatch 未内联时)**
71
+
72
+ | `pathname` | 示例 userIntent | 子脚本 |
73
+ | --- | --- | --- |
74
+ | `…/jova/outline` | `扩写前言` | (dispatch 内直接执行) |
75
+ | `…/jova/writing` | `扩写前言` / `翻译 前言` | (dispatch 内直接执行;扩写类自动替换原文) |
76
+ | `…/jova/proofread` | `校对 前言` | `workbench-jova-proofread-section.js` |
77
+
78
+ 细则与按钮对照表:下文 **WB-1 · 用户说法与脚本路由**。
79
+
27
80
  #### 用户说法映射
28
81
 
29
82
  - **深浅色**:用户说「切换颜色 / 换颜色 / 深色浅色 / 外观主题」等,已登录壳下**一律视为切换深浅色模式**(深色 ⇄ 浅色),见 S-16。
@@ -44,6 +97,7 @@
44
97
  - **定位与多命中**:推荐 `page.locator('main').getByText('部分章节匹配', { exact: true })`;多卡并列时**必须先缩小到目标课程卡片**,避免 strict 或误触(`scripts/hover-partial-chapter-match.js` 的 `cardTitle` 参数可直接用于此场景)。
45
98
  - 自测(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
99
  - **数据分析 · 详情页 · 上传文件**:用户说「上传文件 / 上传数据 / 换数据文件 / 导入 csv xlsx」等,且 `pathname` 为 `/analysis/{toolId}` 时,**一律走 D-2 三步弹窗流程**(打开弹窗 → 弹窗内选文件 → 弹窗内「确认」),**禁止**把左侧参数卡片底部「确认」(D-6 生成结果)当成上传确认。细则见下文「上传文件 · Agent 硬门禁」。
100
+ - **工作台 · 扩写/润色/精简/改写 + 章节**(如「扩写前言」):**一律**先 `run-code --filename=scripts/workbench-jova-dispatch.js`(改 `userIntent`),**禁止**点提纲树「添加子级 / 添加同级」。语义是 AI 重写该节正文(outline 上为 **编辑**→重新生成),不是新增子条目。细则见上文「工作台 · 扩写前言等 · Agent 硬门禁」与 WB-1。
47
101
 
48
102
  #### 任务范围与跨域
49
103
 
@@ -155,7 +209,7 @@
155
209
  | 对话 · 新建会话 | S-09 |
156
210
  | 工作站 · 新建任务弹窗 | S-10 |
157
211
  | 工作台 · 进入子工作台 | S-11 |
158
- | 工作台 · 综述全流程(拟定主题→精修);**扩写/润色/校对/翻译等必先定页+run-code 脚本** | WB-1 |
212
+ | 工作台 · 综述全流程;**「扩写前言」等必先 dispatch.js,禁止添加子级** | WB-1 |
159
213
  | 工作台 · SCI 论著全流程 | WB-2 |
160
214
  | 工作台 · 国自然基金全流程 | WB-3 |
161
215
  | 工作台 · 临床申请书全流程 | WB-4 |
@@ -394,7 +448,7 @@ div.side-menu--item[to='/edu/courses']
394
448
  | 步骤 | `pathname` | 主要表单项 / 区块 | 底栏主按钮(`getByRole('button', …)`) |
395
449
  | --- | --- | --- | --- |
396
450
  | 拟定主题 | `/workbench/jova/topic` | **生成模式**(三选一,`click` 文案):`短篇`(6000-10000字)、`中篇`(10000-15000字)、`长篇`(>15000字);**示例主题**列表 + `getByText('换一组', { exact: true })`;**综述主题** `getByRole('textbox', { name: '请输入综述主题概要内容' })`(上限 2000 字,界面显示 `0 / 2000`) | `继续编辑本地文档`;`生成提纲` |
397
- | 确认提纲 | `/workbench/jova/outline` | 左侧「论文提纲」树;节点行内为**无文案图标按钮**(增删改,需时用 `generate-locator` 固化,勿记 `e*`) | `返回主题`;`下载提纲`;`生成全文` |
451
+ | 确认提纲 | `/workbench/jova/outline` | 左侧「论文提纲」树;节点 hover 后出现 **编辑**(AI 扩写/润色/精简)与 **添加同级/添加子级**(仅改树结构,**禁止**用于「扩写前言」)。章节 AI 操作 **必须** `run-code` `scripts/workbench-jova-dispatch.js` | `返回主题`;`下载提纲`;`生成全文` |
398
452
  | 初稿校对 | `/workbench/jova/proofread` | 左目录 + 右正文;段落带「请校对本段内容」提示 | `返回提纲`;`下载文档`;`完成校对` |
399
453
  | 初稿精修 | `/workbench/jova/writing` | 左目录 + 中央 `textbox` 富文本;顶栏 **修订模式**(含接受/拒绝修订) | `返回提纲`;`修订模式` 切换钮(文案含 `接受修订` / `拒绝修订`);`新建论文`;`下载文档` |
400
454
 
@@ -413,19 +467,25 @@ div.side-menu--item[to='/edu/courses']
413
467
 
414
468
  **0 · 定页(第一步,不可跳过)**
415
469
 
470
+ > 与上文「工作台 · 扩写前言等 · Agent 硬门禁」一致;**WorkBuddy 默认只执行 `workbench-jova-dispatch.js`**,勿手写 click「添加子级」。
471
+
416
472
  ```bash
417
473
  helixlife-v5-cli --raw eval "location.pathname"
474
+ # PowerShell: $env:HELIX_JOVA_INTENT='扩写前言'
475
+ helixlife-v5-cli run-code --filename=scripts/workbench-jova-dispatch.js
418
476
  ```
419
477
 
478
+ (用 `HELIX_JOVA_INTENT` 传用户说法;outline/writing 在 dispatch 内直接完成,禁止点「添加子级」。工作区无 `scripts/` 时 CLI 自动用 npm 包内脚本。)
479
+
420
480
  **1 · 说法 → 脚本(jova 综述)**
421
481
 
422
482
  | 当前 `pathname` | 用户说法(示例) | 脚本(工作区 `scripts/`) | Agent 改动的变量 |
423
483
  | --- | --- | --- | --- |
424
- | `…/jova/outline` | `扩写前言`、`扩写 前言`、`精简结论`、`润色 1. 乳腺癌…` | `workbench-jova-outline-regenerate-node.js` | `intent`(动词+章节;支持连写或空格) |
425
- | `…/jova/proofread` | `校对 前言`、`确认校对`、`AI 改写`、`替换原文` | `workbench-jova-proofread-section.js` | `intent` + `action` |
426
- | `…/jova/writing` | `扩写 前言`、`翻译 前言`、`润色 摘要`、`降重`、`文献列表` | `workbench-jova-writing-tool.js` | `intent`(**工具与章节间建议空格**:`扩写 前言`) |
484
+ | `…/jova/outline` | `扩写前言`、`扩写 前言`、`精简结论`、`润色 1. 乳腺癌…` | **`workbench-jova-dispatch.js`** | `HELIX_JOVA_INTENT` |
485
+ | `…/jova/writing` | `扩写前言`、`翻译 前言`、`润色 摘要`、`降重`、`文献列表` | **`workbench-jova-dispatch.js`** | `HELIX_JOVA_INTENT` |
486
+ | `…/jova/proofread` | `校对 前言`、`确认校对`、`AI 改写`、`替换原文` | dispatch 返回 → `workbench-jova-proofread-section.js` | `HELIX_JOVA_INTENT` + `HELIX_JOVA_ACTION` |
427
487
 
428
- 执行:`helixlife-v5-cli run-code --filename=scripts/<上表脚本名>`
488
+ 执行:`HELIX_JOVA_INTENT='扩写前言' helixlife-v5-cli run-code --filename=scripts/workbench-jova-dispatch.js`(**唯一首选**;**禁止**成功后手工 hover/click/fill)
429
489
 
430
490
  **2 · outline 页:扩写 ≠ 添加子级(反模式)**
431
491
 
@@ -462,8 +522,9 @@ helixlife-v5-cli --raw eval "location.pathname"
462
522
 
463
523
  1. 在 **outline** 页对用户「扩写/润色/精简 + 章节」点「添加子级」或「添加同级」——**永远不对**。
464
524
  2. 不 `eval location.pathname` 就在 **proofread / writing** 页找提纲树「编辑」或「添加子级」。
465
- 3. 不用 `run-code --filename=scripts/workbench-jova-*.js`,改用手动 `snapshot` + `click` hover 才出现的图标按钮。
466
- 4. 同一轮任务内反复 `snapshot` `e*` ref 定位提纲节点(须脚本内 `aria-label*="操作区"` + 章节文案匹配)。
525
+ 3. `run-code` 已返回 `"ok": true` 仍用手动 `snapshot` + `hover` + `click e*` + `fill` + `sleep` 重放(WorkBuddy 慢路径根因)。
526
+ 4. `workbench-jova-dispatch.js` **复制到 WorkBuddy 工作区**再改 `userIntent`(应设 `HELIX_JOVA_INTENT`,CLI 自动回退 npm 包内脚本)。
527
+ 5. 同一轮任务内反复 `snapshot` 猜 `e*` ref 定位提纲节点(须脚本内 `aria-label*="操作区"` + 章节文案匹配)。
467
528
 
468
529
  **6 · 执行命令模板**
469
530
 
@@ -471,11 +532,12 @@ helixlife-v5-cli --raw eval "location.pathname"
471
532
  # ① 定页(不可跳过)
472
533
  helixlife-v5-cli --raw eval "location.pathname"
473
534
 
474
- # ② 按上表改脚本内 intent / action 后执行,示例:
475
- helixlife-v5-cli run-code --filename=scripts/workbench-jova-outline-regenerate-node.js
535
+ # ② 一条命令完成(示例 PowerShell)
536
+ $env:HELIX_JOVA_INTENT='扩写前言'
537
+ helixlife-v5-cli run-code --filename=scripts/workbench-jova-dispatch.js
476
538
  ```
477
539
 
478
- 脚本路径相对于**打开浏览器时的工作区根**(helix-cli 仓库即 `scripts/...`)。
540
+ `--filename=scripts/...` 相对于 cwd;cwd 无该文件时自动解析为 `node_modules/helixlife-v5-cli/scripts/...`。
479
541
 
480
542
  ---
481
543
 
@@ -557,6 +619,7 @@ helixlife-v5-cli run-code --filename=scripts/workbench-jova-outline-regenerate-n
557
619
  **用户说法映射**
558
620
 
559
621
  - 「写综述 / 综述工作台」→ WB-1,`getByText('综述工作台')`。
622
+ - **「扩写前言 / 扩写 前言 / 润色摘要 / 精简结论 / 改写 xxx」**→ **禁止**提纲树「添加子级」;**必须** `run-code --filename=scripts/workbench-jova-dispatch.js`(改 `userIntent`)。见上文硬门禁与 WB-1 §2。
560
623
  - 「SCI / 英文论文 / 论著工作台」→ WB-2,`getByText('SCI论著工作台')`。
561
624
  - 「国自然 / 基金 / 标书」→ WB-3,`getByText('基金工作台')`(**不是**旧称「基金申请书工作台」)。
562
625
  - 「临床申请书 / PICOS / 临床工作台」→ WB-4;面包屑为「临床申请书工作台」。
@@ -0,0 +1,370 @@
1
+ /**
2
+ * 综述工作台(jova)· 统一入口(WorkBuddy 默认只用本脚本)
3
+ *
4
+ * 改 userIntent 一行;自动按 pathname 路由。
5
+ * outline 页「扩写前言」等:本脚本内直接执行(hover → 编辑,禁止添加子级)。
6
+ *
7
+ * 用法:
8
+ * set HELIX_JOVA_INTENT=扩写前言
9
+ * helixlife-v5-cli run-code --filename=scripts/workbench-jova-dispatch.js
10
+ *
11
+ * WorkBuddy 等工作区无 scripts/ 时,CLI 自动回退到 npm 包内同名脚本;勿复制到 WorkBuddy 目录。
12
+ */
13
+ async page => {
14
+ const userIntent = (process.env.HELIX_JOVA_INTENT || '扩写前言').trim();
15
+ const action = process.env.HELIX_JOVA_ACTION?.trim() || null;
16
+
17
+ const pathname = new URL(page.url()).pathname;
18
+ const FORBIDDEN_BUTTONS = ['添加子级', '添加同级'];
19
+
20
+ const outlineVerbs = ['扩写', '精简', '改写', '润色', '重写', '优化'];
21
+ const isOutlineVerbIntent = outlineVerbs.some(v => userIntent.trim().startsWith(v));
22
+ const isProofreadIntent = /^校对/.test(userIntent.trim());
23
+
24
+ function parseOutlineIntent(raw) {
25
+ const text = raw.trim();
26
+ for (const verb of outlineVerbs) {
27
+ if (text.startsWith(verb)) {
28
+ const title = text.slice(verb.length).trim();
29
+ if (title) return { nodeTitle: title, userRequirement: verb };
30
+ }
31
+ }
32
+ return { nodeTitle: '前言', userRequirement: text };
33
+ }
34
+
35
+ if (!pathname.includes('/workbench/jova/')) {
36
+ return {
37
+ ok: false,
38
+ userIntent,
39
+ pathname,
40
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
41
+ reason: '当前不在综述工作台 jova 流程页',
42
+ };
43
+ }
44
+
45
+ // ── outline:扩写/润色/精简 + 章节(禁止添加子级)──
46
+ if (pathname.includes('/jova/outline') && isOutlineVerbIntent) {
47
+ const { nodeTitle, userRequirement } = parseOutlineIntent(userIntent);
48
+ const main = page.locator('main');
49
+
50
+ async function resetEditingState() {
51
+ const closeIcons = main.getByRole('img', { name: 'close' });
52
+ for (let i = 0; i < (await closeIcons.count()); i++) {
53
+ const icon = closeIcons.nth(i);
54
+ if (await icon.isVisible()) {
55
+ await icon.click();
56
+ await page.waitForTimeout(400);
57
+ }
58
+ }
59
+ const cancelReplace = main.getByRole('button', { name: '取 消' });
60
+ if ((await cancelReplace.count()) > 0 && (await cancelReplace.first().isVisible())) {
61
+ await cancelReplace.first().click();
62
+ await page.waitForTimeout(400);
63
+ }
64
+ const saveBtn = main.getByRole('button', { name: '保 存' });
65
+ if ((await saveBtn.count()) > 0 && (await saveBtn.first().isVisible())) {
66
+ await saveBtn.first().click();
67
+ await page.waitForTimeout(400);
68
+ }
69
+ }
70
+
71
+ function outlineNode(title) {
72
+ const base = title.replace(/[::]\s*$/, '');
73
+ const titleRe = new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[::]?$`);
74
+ return main
75
+ .locator('[aria-label*="操作区"]')
76
+ .filter({
77
+ has: page.locator(':scope > *').first().filter({ hasText: titleRe }),
78
+ })
79
+ .first();
80
+ }
81
+
82
+ await resetEditingState();
83
+
84
+ const opArea = outlineNode(nodeTitle);
85
+ if ((await opArea.count()) === 0) {
86
+ return {
87
+ ok: false,
88
+ route: 'outline',
89
+ userIntent,
90
+ nodeTitle,
91
+ userRequirement,
92
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
93
+ reason: `未找到提纲节点「${nodeTitle}」`,
94
+ };
95
+ }
96
+
97
+ await opArea.hover();
98
+ await page.waitForTimeout(400);
99
+ await opArea.getByRole('button', { name: '编辑' }).click();
100
+
101
+ const regenTitle = main.getByText('重新生成', { exact: true });
102
+ await regenTitle.waitFor({ state: 'visible', timeout: 10000 });
103
+
104
+ const userReqBox = main.getByRole('textbox', { name: '用户需求' });
105
+ await userReqBox.waitFor({ state: 'visible', timeout: 5000 });
106
+ await userReqBox.fill(userRequirement);
107
+
108
+ await main.getByRole('button', { name: '一键生成' }).click();
109
+ const replaceBtn = main.getByRole('button', { name: '替换原文' });
110
+ await replaceBtn.waitFor({ state: 'visible', timeout: 120000 });
111
+ await page.waitForTimeout(800);
112
+
113
+ await replaceBtn.click();
114
+ await main.getByText('是否将本次生成结果替换原文?', { exact: true }).waitFor({ state: 'visible', timeout: 10000 });
115
+ await main.getByRole('button', { name: '确 定' }).click();
116
+ await page.waitForTimeout(800);
117
+
118
+ const saveBtn = main.getByRole('button', { name: '保 存' });
119
+ await saveBtn.waitFor({ state: 'visible', timeout: 10000 });
120
+ await saveBtn.click();
121
+ await page.waitForTimeout(1000);
122
+
123
+ return {
124
+ ok: true,
125
+ route: 'outline',
126
+ userIntent,
127
+ nodeTitle,
128
+ userRequirement,
129
+ clickedButton: '编辑',
130
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
131
+ message: `已通过「编辑→重新生成」完成「${userRequirement}」节点「${nodeTitle}」(未使用添加子级)。`,
132
+ url: page.url(),
133
+ };
134
+ }
135
+
136
+ // ── proofread / writing:返回子脚本,仍禁止添加子级 ──
137
+ if (pathname.includes('/jova/proofread') || isProofreadIntent) {
138
+ return {
139
+ ok: false,
140
+ route: 'proofread',
141
+ userIntent,
142
+ pathname,
143
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
144
+ nextScript: 'scripts/workbench-jova-proofread-section.js',
145
+ setIntent: userIntent,
146
+ setAction: action,
147
+ message: '请改 proofread-section.js 的 intent/action 后 run-code;禁止点添加子级。',
148
+ };
149
+ }
150
+
151
+ if (pathname.includes('/jova/writing')) {
152
+ const WRITING_TOOLS = ['校对', '文风', '润色', '降重', '翻译', '改写', '扩写', '缩写', '文献列表'];
153
+
154
+ function parseWritingIntent(raw) {
155
+ const text = raw.trim();
156
+ for (const tool of WRITING_TOOLS) {
157
+ if (text === tool || text.startsWith(`${tool} `) || text.startsWith(tool)) {
158
+ const sectionTitle = text.slice(tool.length).trim();
159
+ return { tool, sectionTitle: sectionTitle || null };
160
+ }
161
+ }
162
+ for (const verb of outlineVerbs) {
163
+ if (text.startsWith(verb)) {
164
+ const sectionTitle = text.slice(verb.length).trim();
165
+ if (sectionTitle) return { tool: verb === '优化' ? '润色' : verb, sectionTitle };
166
+ }
167
+ }
168
+ return { tool: null, sectionTitle: null };
169
+ }
170
+
171
+ const { tool, sectionTitle } = parseWritingIntent(userIntent);
172
+ if (!tool || !WRITING_TOOLS.includes(tool)) {
173
+ return {
174
+ ok: false,
175
+ route: 'writing',
176
+ userIntent,
177
+ pathname,
178
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
179
+ reason: `writing 页无法解析工具,支持:${WRITING_TOOLS.join('、')} 或 ${outlineVerbs.join('/')} + 章节`,
180
+ };
181
+ }
182
+ if (tool !== '文献列表' && !sectionTitle) {
183
+ return {
184
+ ok: false,
185
+ route: 'writing',
186
+ userIntent,
187
+ pathname,
188
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
189
+ reason: '请指定章节,如「扩写前言」或「翻译 前言」',
190
+ };
191
+ }
192
+
193
+ const main = page.locator('main');
194
+
195
+ async function closeWritingPanel() {
196
+ const closeIcons = main.getByRole('img', { name: 'close' });
197
+ for (let i = 0; i < (await closeIcons.count()); i++) {
198
+ const icon = closeIcons.nth(i);
199
+ if (await icon.isVisible().catch(() => false)) {
200
+ await icon.click();
201
+ await page.waitForTimeout(400);
202
+ }
203
+ }
204
+ }
205
+
206
+ async function selectWritingSection(title) {
207
+ if (title) {
208
+ const toc = main.locator('[class*="cursor-pointer"]').getByText(title, { exact: true });
209
+ if ((await toc.count()) > 0) {
210
+ await toc.first().click({ timeout: 5000 }).catch(() => {});
211
+ await page.waitForTimeout(500);
212
+ }
213
+ const heading = main.getByRole('heading', { name: title, exact: true }).first();
214
+ if ((await heading.count()) > 0) {
215
+ await heading.scrollIntoViewIfNeeded();
216
+ await page.waitForTimeout(300);
217
+ }
218
+ }
219
+ const selected = await page.evaluate(t => {
220
+ const editor = document.querySelector('main [role="textbox"]');
221
+ if (!editor) return { ok: false, reason: '未找到富文本编辑器' };
222
+ if (!t) {
223
+ const range = document.createRange();
224
+ range.selectNodeContents(editor);
225
+ const sel = window.getSelection();
226
+ sel?.removeAllRanges();
227
+ sel?.addRange(range);
228
+ return { ok: true };
229
+ }
230
+ const headings = [...editor.querySelectorAll('h1,h2,h3,h4,h5,h6')];
231
+ const idx = headings.findIndex(h => h.textContent?.trim() === t);
232
+ if (idx < 0) return { ok: false, reason: `编辑器内未找到章节「${t}」` };
233
+ const start = headings[idx];
234
+ const level = parseInt(start.tagName[1], 10);
235
+ let end = null;
236
+ for (let i = idx + 1; i < headings.length; i++) {
237
+ if (parseInt(headings[i].tagName[1], 10) <= level) {
238
+ end = headings[i];
239
+ break;
240
+ }
241
+ }
242
+ const range = document.createRange();
243
+ range.setStartBefore(start);
244
+ range.setEndBefore(end || editor.lastChild || editor);
245
+ const sel = window.getSelection();
246
+ sel?.removeAllRanges();
247
+ sel?.addRange(range);
248
+ return { ok: true, sectionText: range.toString().slice(0, 200) };
249
+ }, title);
250
+ await page.waitForTimeout(400);
251
+ return selected;
252
+ }
253
+
254
+ await closeWritingPanel();
255
+ const selected = await selectWritingSection(sectionTitle);
256
+ if (!selected.ok) {
257
+ return {
258
+ ok: false,
259
+ route: 'writing',
260
+ userIntent,
261
+ tool,
262
+ sectionTitle,
263
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
264
+ reason: selected.reason,
265
+ };
266
+ }
267
+
268
+ if (tool === '文献列表') {
269
+ await main.getByText(tool, { exact: true }).first().click();
270
+ await page.waitForTimeout(800);
271
+ return {
272
+ ok: true,
273
+ route: 'writing',
274
+ userIntent,
275
+ tool,
276
+ sectionTitle,
277
+ mode: 'list',
278
+ message: '已打开文献列表面板。',
279
+ url: page.url(),
280
+ };
281
+ }
282
+
283
+ await main.getByText(tool, { exact: true }).first().click();
284
+ await page.waitForTimeout(800);
285
+ await main.getByText('原文内容', { exact: true }).waitFor({ state: 'visible', timeout: 10000 }).catch(() => {});
286
+
287
+ if (tool === '文风') {
288
+ const refBox = main.getByRole('textbox', { name: '参考文风' });
289
+ if ((await refBox.count()) > 0 && (await refBox.first().isVisible().catch(() => false))) {
290
+ const ref =
291
+ selected.sectionText ||
292
+ 'This review synthesizes molecular subtyping and precision therapeutic strategies in breast cancer.';
293
+ await refBox.first().fill(ref);
294
+ }
295
+ }
296
+ if (tool === '翻译') {
297
+ for (const label of ['English', '英文']) {
298
+ const opt = main.getByText(label, { exact: true });
299
+ if ((await opt.count()) > 0 && (await opt.first().isVisible().catch(() => false))) {
300
+ await opt.first().click();
301
+ await page.waitForTimeout(300);
302
+ break;
303
+ }
304
+ }
305
+ }
306
+
307
+ await main.getByRole('button', { name: '一键生成' }).click();
308
+ const replaceBtn = main.getByRole('button', { name: '替换原文' });
309
+ await replaceBtn.waitFor({ state: 'visible', timeout: 120000 });
310
+ const deadline = Date.now() + 120000;
311
+ while (Date.now() < deadline) {
312
+ if (await replaceBtn.isEnabled()) break;
313
+ await page.waitForTimeout(1000);
314
+ }
315
+
316
+ const shouldReplace = action === 'replace' || outlineVerbs.some(v => userIntent.trim().startsWith(v));
317
+ if (shouldReplace && (await replaceBtn.isEnabled())) {
318
+ await replaceBtn.click();
319
+ await main
320
+ .getByText('是否将本次生成结果替换原文?', { exact: true })
321
+ .waitFor({ state: 'visible', timeout: 10000 });
322
+ await main.getByRole('button', { name: '确 定' }).click();
323
+ await page.waitForTimeout(1000);
324
+ return {
325
+ ok: true,
326
+ route: 'writing',
327
+ userIntent,
328
+ tool,
329
+ sectionTitle,
330
+ replaced: true,
331
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
332
+ message: `writing 页已通过「${tool}→一键生成→替换原文」完成「${sectionTitle}」。`,
333
+ url: page.url(),
334
+ };
335
+ }
336
+
337
+ return {
338
+ ok: true,
339
+ route: 'writing',
340
+ userIntent,
341
+ tool,
342
+ sectionTitle,
343
+ needsReplaceChoice: true,
344
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
345
+ message: `「${tool}」已生成;用户确认后再设 HELIX_JOVA_ACTION=replace 或说「替换原文」。`,
346
+ url: page.url(),
347
+ };
348
+ }
349
+
350
+ if (pathname.includes('/jova/outline')) {
351
+ return {
352
+ ok: false,
353
+ route: 'outline',
354
+ userIntent,
355
+ pathname,
356
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
357
+ nextScript: 'scripts/workbench-jova-outline-regenerate-node.js',
358
+ setIntent: userIntent,
359
+ reason: 'userIntent 未匹配扩写/精简/润色/改写/重写/优化 前缀',
360
+ };
361
+ }
362
+
363
+ return {
364
+ ok: false,
365
+ userIntent,
366
+ pathname,
367
+ forbiddenNeverClick: FORBIDDEN_BUTTONS,
368
+ reason: '当前 jova 步骤页不支持本章 AI 操作(topic/proofread 以外)',
369
+ };
370
+ };
@@ -11,7 +11,7 @@
11
11
  * 悬停提纲节点 → 编辑(不点添加子级/添加同级)→ 填写「用户需求」→「一键生成」
12
12
  * → 等待「生成结果」→「替换原文」→ Popconfirm「确 定」→「保 存」
13
13
  *
14
- * 用法(仅需改 intent 一行,或显式指定 nodeTitle / userRequirement):
14
+ * 用法(WorkBuddy 请优先用 workbench-jova-dispatch.js;本文件为 outline 子脚本):
15
15
  * helixlife-v5-cli run-code --filename=scripts/workbench-jova-outline-regenerate-node.js
16
16
  *
17
17
  * intent 解析规则(动词与章节可连写或空格):