helixlife-v5-cli 1.2.6 → 1.2.8
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/package.json
CHANGED
|
@@ -404,10 +404,41 @@ div.side-menu--item[to='/edu/courses']
|
|
|
404
404
|
2. **生成提纲**:填主题后 `click` → `getByRole('button', { name: '生成提纲' })` → 验收 `/workbench/jova/outline`;等待「提纲内容生成中…」结束。
|
|
405
405
|
3. **生成全文(含弹窗)**:`click` → `getByRole('button', { name: '生成全文' })` → `dialog "是否已完成提纲修改,继续生成全文"` 内:`getByText('中文'|'English', { exact: true })`;综述另须 `getByText('是'|'否', { exact: true })`(「需要中文文献」);`click` → `getByRole('dialog', { name: '是否已完成提纲修改,继续生成全文' }).getByRole('button', { name: '确 认' })`(注意「确」「认」间有空格)。验收:`/workbench/jova/proofread`。
|
|
406
406
|
4. **完成校对 → 精修**:`click` → `getByRole('button', { name: '完成校对' })` → 二次 `dialog` 文案「是否确认完成所有段落内容校对,进入全文编辑模式」→ `getByRole('button', { name: '确认' })`(无空格)。验收:`/workbench/jova/writing`。
|
|
407
|
-
5. **节点 AI 重写(扩写 / 精简 / 润色 / 改写 / 重写 /
|
|
407
|
+
5. **节点 AI 重写(扩写 / 精简 / 润色 / 改写 / 重写 / 优化)**:用户说「扩写前言」「**扩写摘要**」「精简结论」「润色 1. xxx」等时,**严禁**点 `添加子级` / `添加同级`(那只会插入空节点)。正确流程:`hover` 目标提纲节点 → 点该节点行内 `编辑`(铅笔图标)→ 右侧出现「重新生成」面板 → 把**动词**(扩写/精简/…)填入 `getByRole('textbox', { name: '用户需求' })` → `getByRole('button', { name: '一键生成' })` → 等待 `替换原文` 可点 → `click` `替换原文` → Popconfirm「是否将本次生成结果替换原文?」`getByRole('button', { name: '确 定' })` → 左侧内联编辑器 `getByRole('button', { name: '保 存' })`。**意图拆分**:「<动词><节点标题>」(动词与标题间可有可无空格)→ `节点标题` 用于定位节点、`动词` 填入「用户需求」框;动词集合 = {扩写, 精简, 改写, 润色, 重写, 优化}。**Agent 硬门禁 · 提纲节点 AI 重写**(细则见下节):**一律** `run-code --filename=scripts/workbench-jova-outline-regenerate-node.js`,将用户**原话**传入 `HELIX_INTENT`(如 `扩写摘要`);**禁止**为不同节点标题新建脚本或手点逐步操作。
|
|
408
408
|
|
|
409
409
|
**取消全文生成弹窗**:同 dialog 内 `getByRole('button', { name: '取 消' })` 或 `Close`。
|
|
410
410
|
|
|
411
|
+
###### 提纲节点 AI 重写 · Agent 硬门禁(WB-1 / outline · 必读)
|
|
412
|
+
|
|
413
|
+
> **触发**:`/workbench/jova/outline`(或结构相同的 `sci`/`nsfc-v2`/`clinical` outline 步)且用户指令含 {扩写, 精简, 改写, 润色, 重写, 优化} + 提纲节点名(前言、摘要、结论、1. xxx …)。
|
|
414
|
+
|
|
415
|
+
| 用户说法示例 | `HELIX_INTENT` | 解析结果 |
|
|
416
|
+
| --- | --- | --- |
|
|
417
|
+
| 扩写前言 | `扩写前言` | 节点=前言,用户需求=扩写 |
|
|
418
|
+
| 扩写摘要 | `扩写摘要` | 节点=摘要,用户需求=扩写 |
|
|
419
|
+
| 精简结论 | `精简结论` | 节点=结论,用户需求=精简 |
|
|
420
|
+
| 润色 1. 乳腺癌… | `润色 1. 乳腺癌…` | 节点=1. 乳腺癌…,用户需求=润色 |
|
|
421
|
+
|
|
422
|
+
**执行(必用同一脚本,禁止复制改写)**
|
|
423
|
+
|
|
424
|
+
```powershell
|
|
425
|
+
# PowerShell — 将引号内换成用户原话
|
|
426
|
+
$env:HELIX_INTENT='扩写摘要'; helixlife-v5-cli run-code --filename=scripts/workbench-jova-outline-regenerate-node.js
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
# Bash / macOS
|
|
431
|
+
HELIX_INTENT='扩写摘要' helixlife-v5-cli run-code --filename=scripts/workbench-jova-outline-regenerate-node.js
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**反模式(常见导致误操作「前言」或重复造脚本)**
|
|
435
|
+
|
|
436
|
+
- 直接 `run-code` 而不设 `HELIX_INTENT` → 脚本会报错并拒绝执行(勿改脚本内默认值绕过)。
|
|
437
|
+
- 用户说「扩写摘要」却新建 `workbench-jova-outline-regenerate-abstract.js` 等副本 → **禁止**;节点标题差异仅体现在 `HELIX_INTENT`。
|
|
438
|
+
- 用 `添加子级` / `添加同级` 代替 AI 重写 → 只会插入空节点。
|
|
439
|
+
|
|
440
|
+
**显式拆分(可选)**:`HELIX_NODE_TITLE` + `HELIX_USER_REQUIREMENT` 同时设置时优先于 `HELIX_INTENT`。
|
|
441
|
+
|
|
411
442
|
---
|
|
412
443
|
|
|
413
444
|
##### WB-2:SCI 论著工作台(`sci`)
|
|
@@ -1,44 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 综述工作台(jova)· 确认提纲页 · 节点 AI 重新生成
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 典型场景:用户说「扩写前言」「扩写摘要」「精简结论」「润色 1. xxx」等。
|
|
5
5
|
*
|
|
6
6
|
* 流程:
|
|
7
7
|
* 悬停提纲节点 → 编辑 → 填写「用户需求」→「一键生成」
|
|
8
8
|
* → 等待「生成结果」→「替换原文」→ Popconfirm「确 定」→「保 存」
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Agent 调用约定(严格遵守):
|
|
11
|
+
* 1) 无论节点标题是「前言」「摘要」「结论」或任意提纲节名,一律复用本脚本,禁止新建同类脚本。
|
|
12
|
+
* 2) 将用户原话传入 HELIX_INTENT 后执行 run-code(勿依赖脚本内默认 intent):
|
|
13
|
+
* PowerShell: $env:HELIX_INTENT='扩写摘要'; helixlife-v5-cli run-code --filename=scripts/workbench-jova-outline-regenerate-node.js
|
|
14
|
+
* Bash: HELIX_INTENT='扩写摘要' helixlife-v5-cli run-code --filename=scripts/workbench-jova-outline-regenerate-node.js
|
|
15
|
+
* 3) 也可显式拆分:HELIX_NODE_TITLE + HELIX_USER_REQUIREMENT(同时设置时优先于 HELIX_INTENT)
|
|
12
16
|
*
|
|
13
|
-
* intent
|
|
17
|
+
* intent / HELIX_INTENT 解析规则(<动词><节点标题>,动词与标题间可有可无空格):
|
|
14
18
|
* 「扩写前言」 → nodeTitle=前言, userRequirement=扩写
|
|
19
|
+
* 「扩写摘要」 → nodeTitle=摘要, userRequirement=扩写
|
|
15
20
|
* 「精简结论」 → nodeTitle=结论, userRequirement=精简
|
|
16
|
-
*
|
|
21
|
+
* 「润色 1. xxx」 → nodeTitle=1. xxx, userRequirement=润色
|
|
17
22
|
* 无法解析时整句作为 userRequirement,nodeTitle 回退为「前言」
|
|
18
23
|
*/
|
|
19
24
|
async page => {
|
|
20
|
-
|
|
21
|
-
const intent = '扩写前言';
|
|
25
|
+
const intent = (process.env.HELIX_INTENT || '').trim();
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
27
|
+
const nodeTitleOverride = process.env.HELIX_NODE_TITLE?.trim() || null;
|
|
28
|
+
const userRequirementOverride = process.env.HELIX_USER_REQUIREMENT?.trim() || null;
|
|
29
|
+
|
|
30
|
+
const VERBS = ['扩写', '精简', '改写', '润色', '重写', '优化'];
|
|
26
31
|
|
|
27
32
|
function parseIntent(raw) {
|
|
28
|
-
const
|
|
29
|
-
for (const verb of
|
|
30
|
-
if (
|
|
31
|
-
const title =
|
|
33
|
+
const normalized = raw.replace(/[::]\s*$/, '').trim();
|
|
34
|
+
for (const verb of VERBS) {
|
|
35
|
+
if (normalized.startsWith(verb)) {
|
|
36
|
+
const title = normalized.slice(verb.length).trim();
|
|
32
37
|
if (title) return { nodeTitle: title, userRequirement: verb };
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
|
-
return { nodeTitle: '前言', userRequirement: raw };
|
|
40
|
+
return { nodeTitle: '前言', userRequirement: normalized || raw };
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
if (!intent && !nodeTitleOverride) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
reason:
|
|
47
|
+
'缺少 HELIX_INTENT(用户原话,如「扩写摘要」)或 HELIX_NODE_TITLE。禁止直接 run-code 而不传意图——否则会误操作默认节点。',
|
|
48
|
+
hint: "PowerShell: $env:HELIX_INTENT='扩写摘要'; helixlife-v5-cli run-code --filename=scripts/workbench-jova-outline-regenerate-node.js",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parsed = intent ? parseIntent(intent) : { nodeTitle: '', userRequirement: '' };
|
|
39
53
|
const nodeTitle = nodeTitleOverride ?? parsed.nodeTitle;
|
|
40
54
|
const userRequirement = userRequirementOverride ?? parsed.userRequirement;
|
|
41
55
|
|
|
56
|
+
if (!nodeTitle || !userRequirement) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason: '无法解析节点或动词。请传 HELIX_INTENT(如「扩写摘要」)或同时传 HELIX_NODE_TITLE + HELIX_USER_REQUIREMENT。',
|
|
60
|
+
intent,
|
|
61
|
+
nodeTitle,
|
|
62
|
+
userRequirement,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
const main = page.locator('main');
|
|
43
67
|
|
|
44
68
|
if (!page.url().includes('/workbench/jova/outline')) {
|
|
@@ -1,262 +1,262 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 综述工作台(jova)· 初稿校对页 · 章节校对 / AI 改写
|
|
3
|
-
*
|
|
4
|
-
* Agent 调用约定(严格遵守):
|
|
5
|
-
*
|
|
6
|
-
* 1) 用户只说「校对 <章节>」、未指明操作时:
|
|
7
|
-
* action = null → 仅滚动定位 → 悬停 → 点「点击本段进入编辑校对」或「重新编辑」
|
|
8
|
-
* → 返回 needsUserChoice,由 Agent 询问用户选「确认校对」或「AI 改写」
|
|
9
|
-
* → 禁止自动点「确认校对」
|
|
10
|
-
*
|
|
11
|
-
* 2) 用户明确说「确认校对」:action = 'confirm'
|
|
12
|
-
*
|
|
13
|
-
* 3) 用户明确说「AI 改写」:
|
|
14
|
-
* action = 'ai_rewrite' → 点「AI 改写」→(有额外需求则填写)→「一键重写」→ 等待生成
|
|
15
|
-
* → 返回 needsReplaceChoice,提醒用户是否替换原文,禁止自动点「替换原文」
|
|
16
|
-
*
|
|
17
|
-
* 4) 用户明确同意替换原文:action = 'replace'
|
|
18
|
-
*
|
|
19
|
-
* 用法:
|
|
20
|
-
* helixlife-v5-cli run-code --filename=scripts/workbench-jova-proofread-section.js
|
|
21
|
-
*/
|
|
22
|
-
async page => {
|
|
23
|
-
const intent = '校对 1. 乳腺癌分子分型体系及其分子特征';
|
|
24
|
-
const action = null; // null | 'confirm' | 'ai_rewrite' | 'replace'
|
|
25
|
-
const extraRequirement = ''; // 仅 ai_rewrite 时可选
|
|
26
|
-
|
|
27
|
-
const main = page.locator('main');
|
|
28
|
-
|
|
29
|
-
function parseIntent(raw) {
|
|
30
|
-
const m = raw.match(/^校对\s*(.+)$/);
|
|
31
|
-
return { sectionTitle: (m ? m[1] : raw).trim() };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const { sectionTitle } = parseIntent(intent);
|
|
35
|
-
|
|
36
|
-
if (!page.url().includes('/workbench/jova/proofread')) {
|
|
37
|
-
return {
|
|
38
|
-
ok: false,
|
|
39
|
-
reason: '当前不在综述工作台初稿校对页 /workbench/jova/proofread',
|
|
40
|
-
intent,
|
|
41
|
-
sectionTitle,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function dismissPopconfirmOnly() {
|
|
46
|
-
const cancel = main.getByRole('button', { name: '取 消' });
|
|
47
|
-
if ((await cancel.count()) > 0 && (await cancel.first().isVisible())) {
|
|
48
|
-
await cancel.first().click();
|
|
49
|
-
await page.waitForTimeout(300);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function sectionHeading(title) {
|
|
54
|
-
return main.getByRole('heading', { name: title, exact: true }).first();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function sectionBlock(title) {
|
|
58
|
-
return sectionHeading(title).locator('xpath=ancestor::div[contains(@class,"group")][1]');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function navigateToSection(title) {
|
|
62
|
-
const toc = main.locator('[class*="cursor-pointer"]').getByText(title, { exact: true });
|
|
63
|
-
if ((await toc.count()) > 0) {
|
|
64
|
-
await toc.first().click({ timeout: 3000 }).catch(() => {});
|
|
65
|
-
await page.waitForTimeout(500);
|
|
66
|
-
}
|
|
67
|
-
const heading = sectionHeading(title);
|
|
68
|
-
if ((await heading.count()) === 0) {
|
|
69
|
-
return { ok: false, reason: `未找到章节「${title}」` };
|
|
70
|
-
}
|
|
71
|
-
await heading.scrollIntoViewIfNeeded();
|
|
72
|
-
await page.waitForTimeout(400);
|
|
73
|
-
return { ok: true };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function detectProofreadStatus(block) {
|
|
77
|
-
const pending = block.getByText('请校对本段内容', { exact: true });
|
|
78
|
-
const done = block.getByText('本段完成校对', { exact: true });
|
|
79
|
-
if ((await pending.count()) > 0) return 'pending';
|
|
80
|
-
if ((await done.count()) > 0) return 'done';
|
|
81
|
-
return 'completed_or_editing';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function clickEnterEdit(block) {
|
|
85
|
-
const enterLabels = [
|
|
86
|
-
'点击本段进入编辑校对',
|
|
87
|
-
'点击本段进行校对',
|
|
88
|
-
'点击本段进行编辑校对',
|
|
89
|
-
];
|
|
90
|
-
for (const label of enterLabels) {
|
|
91
|
-
const btn = block.getByText(label, { exact: true });
|
|
92
|
-
if ((await btn.count()) > 0 && (await btn.first().isVisible())) {
|
|
93
|
-
await btn.first().click();
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function openSectionEdit(title) {
|
|
101
|
-
const nav = await navigateToSection(title);
|
|
102
|
-
if (!nav.ok) return nav;
|
|
103
|
-
|
|
104
|
-
const block = sectionBlock(title);
|
|
105
|
-
|
|
106
|
-
if ((await block.getByRole('button', { name: '确认校对' }).count()) > 0) {
|
|
107
|
-
return { ok: true, proofreadStatus: 'already_editing' };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const proofreadStatus = await detectProofreadStatus(block);
|
|
111
|
-
|
|
112
|
-
if (proofreadStatus === 'pending') {
|
|
113
|
-
await block.getByText('请校对本段内容', { exact: true }).first().hover();
|
|
114
|
-
await page.waitForTimeout(600);
|
|
115
|
-
if (!(await clickEnterEdit(block))) {
|
|
116
|
-
return { ok: false, reason: '未找到「点击本段进入编辑校对」入口', proofreadStatus };
|
|
117
|
-
}
|
|
118
|
-
} else if (proofreadStatus === 'done') {
|
|
119
|
-
await block.getByText('本段完成校对', { exact: true }).first().hover();
|
|
120
|
-
await page.waitForTimeout(600);
|
|
121
|
-
const reEdit = block.getByText('重新编辑', { exact: true });
|
|
122
|
-
if ((await reEdit.count()) === 0 || !(await reEdit.first().isVisible())) {
|
|
123
|
-
return { ok: false, reason: '未找到「重新编辑」入口', proofreadStatus };
|
|
124
|
-
}
|
|
125
|
-
await reEdit.first().click();
|
|
126
|
-
} else {
|
|
127
|
-
await block.hover();
|
|
128
|
-
await page.waitForTimeout(600);
|
|
129
|
-
const reEdit = block.getByText('重新编辑', { exact: true });
|
|
130
|
-
if ((await reEdit.count()) > 0 && (await reEdit.first().isVisible())) {
|
|
131
|
-
await reEdit.first().click();
|
|
132
|
-
} else if (await clickEnterEdit(block)) {
|
|
133
|
-
// ok
|
|
134
|
-
} else {
|
|
135
|
-
return { ok: false, reason: '无法进入编辑模式(未找到入口)', proofreadStatus };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
await block.getByRole('button', { name: '确认校对' }).waitFor({ state: 'visible', timeout: 10000 });
|
|
140
|
-
await page.waitForTimeout(400);
|
|
141
|
-
return { ok: true, proofreadStatus };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function confirmProofread(title) {
|
|
145
|
-
const block = sectionBlock(title);
|
|
146
|
-
await block.getByRole('button', { name: '确认校对' }).click();
|
|
147
|
-
await page.waitForTimeout(1000);
|
|
148
|
-
return { ok: true };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function waitReplaceReady(timeoutMs = 120000) {
|
|
152
|
-
const replaceBtn = main.getByRole('button', { name: '替换原文' });
|
|
153
|
-
await replaceBtn.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
154
|
-
const deadline = Date.now() + timeoutMs;
|
|
155
|
-
while (Date.now() < deadline) {
|
|
156
|
-
if (await replaceBtn.isEnabled()) return replaceBtn;
|
|
157
|
-
await page.waitForTimeout(1000);
|
|
158
|
-
}
|
|
159
|
-
throw new Error('等待「替换原文」可点击超时');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function aiRewriteGenerate(title, extra) {
|
|
163
|
-
const block = sectionBlock(title);
|
|
164
|
-
await block.getByRole('button', { name: /AI\s*改写/ }).click();
|
|
165
|
-
await main.getByText('原文内容', { exact: true }).waitFor({ state: 'visible', timeout: 10000 });
|
|
166
|
-
|
|
167
|
-
if (extra) {
|
|
168
|
-
const extraBox = main.getByRole('textbox', { name: '额外需求' });
|
|
169
|
-
if ((await extraBox.count()) > 0) await extraBox.fill(extra);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
await main.getByRole('button', { name: '一键重写' }).click();
|
|
173
|
-
await waitReplaceReady();
|
|
174
|
-
|
|
175
|
-
return { ok: true, generated: true, replaceReady: true };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
async function aiRewriteReplace() {
|
|
179
|
-
const replaceBtn = main.getByRole('button', { name: '替换原文' });
|
|
180
|
-
await replaceBtn.click();
|
|
181
|
-
await main.getByText('是否将本次生成结果替换原文?', { exact: true }).waitFor({ state: 'visible', timeout: 10000 });
|
|
182
|
-
await main.getByRole('button', { name: '确 定' }).click();
|
|
183
|
-
await page.waitForTimeout(1000);
|
|
184
|
-
return { ok: true, replaced: true };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
await dismissPopconfirmOnly();
|
|
188
|
-
|
|
189
|
-
const needOpenEdit = action === null || action === '' || action === undefined || action === 'confirm' || action === 'ai_rewrite';
|
|
190
|
-
let opened = { ok: true, proofreadStatus: 'already_editing' };
|
|
191
|
-
|
|
192
|
-
if (needOpenEdit && action !== 'replace') {
|
|
193
|
-
opened = await openSectionEdit(sectionTitle);
|
|
194
|
-
if (!opened.ok) return { ok: false, intent, sectionTitle, ...opened };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (action === null || action === undefined || action === '') {
|
|
198
|
-
return {
|
|
199
|
-
ok: true,
|
|
200
|
-
needsUserChoice: true,
|
|
201
|
-
intent,
|
|
202
|
-
sectionTitle,
|
|
203
|
-
proofreadStatus: opened.proofreadStatus,
|
|
204
|
-
availableActions: ['confirm', 'ai_rewrite'],
|
|
205
|
-
message: '已进入该段编辑模式。请选择:① 确认校对(直接完成本段校对)② AI 改写(AI 重新生成内容)',
|
|
206
|
-
url: page.url(),
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (action === 'confirm') {
|
|
211
|
-
await confirmProofread(sectionTitle);
|
|
212
|
-
const block = sectionBlock(sectionTitle);
|
|
213
|
-
const afterStatus = await detectProofreadStatus(block);
|
|
214
|
-
return {
|
|
215
|
-
ok: true,
|
|
216
|
-
intent,
|
|
217
|
-
sectionTitle,
|
|
218
|
-
action,
|
|
219
|
-
proofreadStatusBefore: opened.proofreadStatus,
|
|
220
|
-
proofreadStatusAfter: afterStatus,
|
|
221
|
-
message: '本段已确认校对完成。',
|
|
222
|
-
url: page.url(),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (action === 'ai_rewrite') {
|
|
227
|
-
const generated = await aiRewriteGenerate(sectionTitle, extraRequirement);
|
|
228
|
-
return {
|
|
229
|
-
ok: generated.ok,
|
|
230
|
-
intent,
|
|
231
|
-
sectionTitle,
|
|
232
|
-
action,
|
|
233
|
-
extraRequirement: extraRequirement || null,
|
|
234
|
-
generated: true,
|
|
235
|
-
needsReplaceChoice: true,
|
|
236
|
-
availableActions: ['replace'],
|
|
237
|
-
message:
|
|
238
|
-
'AI 改写结果已生成。请在右侧查看「生成结果」,确认无误后告知我「替换原文」以应用;若不满意可直接在页面调整或重新生成。',
|
|
239
|
-
url: page.url(),
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (action === 'replace') {
|
|
244
|
-
const replaced = await aiRewriteReplace();
|
|
245
|
-
return {
|
|
246
|
-
ok: replaced.ok,
|
|
247
|
-
intent,
|
|
248
|
-
sectionTitle,
|
|
249
|
-
action,
|
|
250
|
-
replaced: true,
|
|
251
|
-
message: '已替换原文。如需完成本段校对,请明确告知「确认校对」。',
|
|
252
|
-
url: page.url(),
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
ok: false,
|
|
258
|
-
reason: `未知 action「${action}」,应为 null | confirm | ai_rewrite | replace`,
|
|
259
|
-
intent,
|
|
260
|
-
sectionTitle,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 综述工作台(jova)· 初稿校对页 · 章节校对 / AI 改写
|
|
3
|
+
*
|
|
4
|
+
* Agent 调用约定(严格遵守):
|
|
5
|
+
*
|
|
6
|
+
* 1) 用户只说「校对 <章节>」、未指明操作时:
|
|
7
|
+
* action = null → 仅滚动定位 → 悬停 → 点「点击本段进入编辑校对」或「重新编辑」
|
|
8
|
+
* → 返回 needsUserChoice,由 Agent 询问用户选「确认校对」或「AI 改写」
|
|
9
|
+
* → 禁止自动点「确认校对」
|
|
10
|
+
*
|
|
11
|
+
* 2) 用户明确说「确认校对」:action = 'confirm'
|
|
12
|
+
*
|
|
13
|
+
* 3) 用户明确说「AI 改写」:
|
|
14
|
+
* action = 'ai_rewrite' → 点「AI 改写」→(有额外需求则填写)→「一键重写」→ 等待生成
|
|
15
|
+
* → 返回 needsReplaceChoice,提醒用户是否替换原文,禁止自动点「替换原文」
|
|
16
|
+
*
|
|
17
|
+
* 4) 用户明确同意替换原文:action = 'replace'
|
|
18
|
+
*
|
|
19
|
+
* 用法:
|
|
20
|
+
* helixlife-v5-cli run-code --filename=scripts/workbench-jova-proofread-section.js
|
|
21
|
+
*/
|
|
22
|
+
async page => {
|
|
23
|
+
const intent = '校对 1. 乳腺癌分子分型体系及其分子特征';
|
|
24
|
+
const action = null; // null | 'confirm' | 'ai_rewrite' | 'replace'
|
|
25
|
+
const extraRequirement = ''; // 仅 ai_rewrite 时可选
|
|
26
|
+
|
|
27
|
+
const main = page.locator('main');
|
|
28
|
+
|
|
29
|
+
function parseIntent(raw) {
|
|
30
|
+
const m = raw.match(/^校对\s*(.+)$/);
|
|
31
|
+
return { sectionTitle: (m ? m[1] : raw).trim() };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { sectionTitle } = parseIntent(intent);
|
|
35
|
+
|
|
36
|
+
if (!page.url().includes('/workbench/jova/proofread')) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: '当前不在综述工作台初稿校对页 /workbench/jova/proofread',
|
|
40
|
+
intent,
|
|
41
|
+
sectionTitle,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function dismissPopconfirmOnly() {
|
|
46
|
+
const cancel = main.getByRole('button', { name: '取 消' });
|
|
47
|
+
if ((await cancel.count()) > 0 && (await cancel.first().isVisible())) {
|
|
48
|
+
await cancel.first().click();
|
|
49
|
+
await page.waitForTimeout(300);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sectionHeading(title) {
|
|
54
|
+
return main.getByRole('heading', { name: title, exact: true }).first();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sectionBlock(title) {
|
|
58
|
+
return sectionHeading(title).locator('xpath=ancestor::div[contains(@class,"group")][1]');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function navigateToSection(title) {
|
|
62
|
+
const toc = main.locator('[class*="cursor-pointer"]').getByText(title, { exact: true });
|
|
63
|
+
if ((await toc.count()) > 0) {
|
|
64
|
+
await toc.first().click({ timeout: 3000 }).catch(() => {});
|
|
65
|
+
await page.waitForTimeout(500);
|
|
66
|
+
}
|
|
67
|
+
const heading = sectionHeading(title);
|
|
68
|
+
if ((await heading.count()) === 0) {
|
|
69
|
+
return { ok: false, reason: `未找到章节「${title}」` };
|
|
70
|
+
}
|
|
71
|
+
await heading.scrollIntoViewIfNeeded();
|
|
72
|
+
await page.waitForTimeout(400);
|
|
73
|
+
return { ok: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function detectProofreadStatus(block) {
|
|
77
|
+
const pending = block.getByText('请校对本段内容', { exact: true });
|
|
78
|
+
const done = block.getByText('本段完成校对', { exact: true });
|
|
79
|
+
if ((await pending.count()) > 0) return 'pending';
|
|
80
|
+
if ((await done.count()) > 0) return 'done';
|
|
81
|
+
return 'completed_or_editing';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function clickEnterEdit(block) {
|
|
85
|
+
const enterLabels = [
|
|
86
|
+
'点击本段进入编辑校对',
|
|
87
|
+
'点击本段进行校对',
|
|
88
|
+
'点击本段进行编辑校对',
|
|
89
|
+
];
|
|
90
|
+
for (const label of enterLabels) {
|
|
91
|
+
const btn = block.getByText(label, { exact: true });
|
|
92
|
+
if ((await btn.count()) > 0 && (await btn.first().isVisible())) {
|
|
93
|
+
await btn.first().click();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function openSectionEdit(title) {
|
|
101
|
+
const nav = await navigateToSection(title);
|
|
102
|
+
if (!nav.ok) return nav;
|
|
103
|
+
|
|
104
|
+
const block = sectionBlock(title);
|
|
105
|
+
|
|
106
|
+
if ((await block.getByRole('button', { name: '确认校对' }).count()) > 0) {
|
|
107
|
+
return { ok: true, proofreadStatus: 'already_editing' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const proofreadStatus = await detectProofreadStatus(block);
|
|
111
|
+
|
|
112
|
+
if (proofreadStatus === 'pending') {
|
|
113
|
+
await block.getByText('请校对本段内容', { exact: true }).first().hover();
|
|
114
|
+
await page.waitForTimeout(600);
|
|
115
|
+
if (!(await clickEnterEdit(block))) {
|
|
116
|
+
return { ok: false, reason: '未找到「点击本段进入编辑校对」入口', proofreadStatus };
|
|
117
|
+
}
|
|
118
|
+
} else if (proofreadStatus === 'done') {
|
|
119
|
+
await block.getByText('本段完成校对', { exact: true }).first().hover();
|
|
120
|
+
await page.waitForTimeout(600);
|
|
121
|
+
const reEdit = block.getByText('重新编辑', { exact: true });
|
|
122
|
+
if ((await reEdit.count()) === 0 || !(await reEdit.first().isVisible())) {
|
|
123
|
+
return { ok: false, reason: '未找到「重新编辑」入口', proofreadStatus };
|
|
124
|
+
}
|
|
125
|
+
await reEdit.first().click();
|
|
126
|
+
} else {
|
|
127
|
+
await block.hover();
|
|
128
|
+
await page.waitForTimeout(600);
|
|
129
|
+
const reEdit = block.getByText('重新编辑', { exact: true });
|
|
130
|
+
if ((await reEdit.count()) > 0 && (await reEdit.first().isVisible())) {
|
|
131
|
+
await reEdit.first().click();
|
|
132
|
+
} else if (await clickEnterEdit(block)) {
|
|
133
|
+
// ok
|
|
134
|
+
} else {
|
|
135
|
+
return { ok: false, reason: '无法进入编辑模式(未找到入口)', proofreadStatus };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await block.getByRole('button', { name: '确认校对' }).waitFor({ state: 'visible', timeout: 10000 });
|
|
140
|
+
await page.waitForTimeout(400);
|
|
141
|
+
return { ok: true, proofreadStatus };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function confirmProofread(title) {
|
|
145
|
+
const block = sectionBlock(title);
|
|
146
|
+
await block.getByRole('button', { name: '确认校对' }).click();
|
|
147
|
+
await page.waitForTimeout(1000);
|
|
148
|
+
return { ok: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function waitReplaceReady(timeoutMs = 120000) {
|
|
152
|
+
const replaceBtn = main.getByRole('button', { name: '替换原文' });
|
|
153
|
+
await replaceBtn.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
154
|
+
const deadline = Date.now() + timeoutMs;
|
|
155
|
+
while (Date.now() < deadline) {
|
|
156
|
+
if (await replaceBtn.isEnabled()) return replaceBtn;
|
|
157
|
+
await page.waitForTimeout(1000);
|
|
158
|
+
}
|
|
159
|
+
throw new Error('等待「替换原文」可点击超时');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function aiRewriteGenerate(title, extra) {
|
|
163
|
+
const block = sectionBlock(title);
|
|
164
|
+
await block.getByRole('button', { name: /AI\s*改写/ }).click();
|
|
165
|
+
await main.getByText('原文内容', { exact: true }).waitFor({ state: 'visible', timeout: 10000 });
|
|
166
|
+
|
|
167
|
+
if (extra) {
|
|
168
|
+
const extraBox = main.getByRole('textbox', { name: '额外需求' });
|
|
169
|
+
if ((await extraBox.count()) > 0) await extraBox.fill(extra);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await main.getByRole('button', { name: '一键重写' }).click();
|
|
173
|
+
await waitReplaceReady();
|
|
174
|
+
|
|
175
|
+
return { ok: true, generated: true, replaceReady: true };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function aiRewriteReplace() {
|
|
179
|
+
const replaceBtn = main.getByRole('button', { name: '替换原文' });
|
|
180
|
+
await replaceBtn.click();
|
|
181
|
+
await main.getByText('是否将本次生成结果替换原文?', { exact: true }).waitFor({ state: 'visible', timeout: 10000 });
|
|
182
|
+
await main.getByRole('button', { name: '确 定' }).click();
|
|
183
|
+
await page.waitForTimeout(1000);
|
|
184
|
+
return { ok: true, replaced: true };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await dismissPopconfirmOnly();
|
|
188
|
+
|
|
189
|
+
const needOpenEdit = action === null || action === '' || action === undefined || action === 'confirm' || action === 'ai_rewrite';
|
|
190
|
+
let opened = { ok: true, proofreadStatus: 'already_editing' };
|
|
191
|
+
|
|
192
|
+
if (needOpenEdit && action !== 'replace') {
|
|
193
|
+
opened = await openSectionEdit(sectionTitle);
|
|
194
|
+
if (!opened.ok) return { ok: false, intent, sectionTitle, ...opened };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (action === null || action === undefined || action === '') {
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
needsUserChoice: true,
|
|
201
|
+
intent,
|
|
202
|
+
sectionTitle,
|
|
203
|
+
proofreadStatus: opened.proofreadStatus,
|
|
204
|
+
availableActions: ['confirm', 'ai_rewrite'],
|
|
205
|
+
message: '已进入该段编辑模式。请选择:① 确认校对(直接完成本段校对)② AI 改写(AI 重新生成内容)',
|
|
206
|
+
url: page.url(),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (action === 'confirm') {
|
|
211
|
+
await confirmProofread(sectionTitle);
|
|
212
|
+
const block = sectionBlock(sectionTitle);
|
|
213
|
+
const afterStatus = await detectProofreadStatus(block);
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
intent,
|
|
217
|
+
sectionTitle,
|
|
218
|
+
action,
|
|
219
|
+
proofreadStatusBefore: opened.proofreadStatus,
|
|
220
|
+
proofreadStatusAfter: afterStatus,
|
|
221
|
+
message: '本段已确认校对完成。',
|
|
222
|
+
url: page.url(),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (action === 'ai_rewrite') {
|
|
227
|
+
const generated = await aiRewriteGenerate(sectionTitle, extraRequirement);
|
|
228
|
+
return {
|
|
229
|
+
ok: generated.ok,
|
|
230
|
+
intent,
|
|
231
|
+
sectionTitle,
|
|
232
|
+
action,
|
|
233
|
+
extraRequirement: extraRequirement || null,
|
|
234
|
+
generated: true,
|
|
235
|
+
needsReplaceChoice: true,
|
|
236
|
+
availableActions: ['replace'],
|
|
237
|
+
message:
|
|
238
|
+
'AI 改写结果已生成。请在右侧查看「生成结果」,确认无误后告知我「替换原文」以应用;若不满意可直接在页面调整或重新生成。',
|
|
239
|
+
url: page.url(),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (action === 'replace') {
|
|
244
|
+
const replaced = await aiRewriteReplace();
|
|
245
|
+
return {
|
|
246
|
+
ok: replaced.ok,
|
|
247
|
+
intent,
|
|
248
|
+
sectionTitle,
|
|
249
|
+
action,
|
|
250
|
+
replaced: true,
|
|
251
|
+
message: '已替换原文。如需完成本段校对,请明确告知「确认校对」。',
|
|
252
|
+
url: page.url(),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
reason: `未知 action「${action}」,应为 null | confirm | ai_rewrite | replace`,
|
|
259
|
+
intent,
|
|
260
|
+
sectionTitle,
|
|
261
|
+
};
|
|
262
|
+
}
|