helixlife-v5-cli 1.2.8 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -58
- package/helix-cli.js +7 -2
- package/lib/.cli.runtime.generated.json +10 -1
- package/lib/automation-file-chooser-guard.js +182 -0
- package/lib/automation-overlay.init.js +332 -68
- package/lib/browser-css-cache-guard.init-page.js +49 -0
- package/lib/browser-window-maximize.init-page.js +64 -0
- package/lib/cli.js +316 -14
- package/lib/helix-reload-bypass-cache.run-code.js +7 -0
- package/lib/helix-styles-guard.js +77 -0
- package/lib/hide-nested-skills.js +120 -0
- package/lib/native-mode-patch.js +288 -0
- package/lib/postinstall.js +39 -0
- package/lib/profile-lock-guard.js +187 -0
- package/lib/runtime-config.js +90 -0
- package/lib/token-file.js +189 -0
- package/lib/visual-page-wrap.js +311 -0
- package/lib/visual.js +255 -86
- package/package.json +9 -21
- package/scripts/README.md +81 -0
- package/scripts/{analysis-detail-upload-file.js → analysis/detail-upload-file.js} +1 -1
- package/scripts/{jigsaw-inspect-dom.js → analysis/jigsaw-inspect-dom.js} +15 -15
- package/scripts/{jigsaw-verify-enumerate.js → analysis/jigsaw-verify-enumerate.js} +1 -1
- package/scripts/common/.gitkeep +0 -0
- package/scripts/{chrome-pdf-enumerate-a11y.js → edu/chrome-pdf-enumerate-a11y.js} +1 -1
- package/scripts/{chrome-pdf-goto-page.js → edu/chrome-pdf-goto-page.js} +1 -1
- package/scripts/{chrome-pdf-zoom-out.js → edu/chrome-pdf-zoom-out.js} +1 -1
- package/scripts/{hover-partial-chapter-match.js → edu/hover-partial-chapter-match.js} +1 -1
- package/scripts/maintainer/strip-extra-skills.ps1 +27 -0
- package/scripts/workbench/jova/outline-edit-root-title.js +33 -0
- package/scripts/workbench/jova/outline-keyword-edit.js +122 -0
- package/scripts/workbench/jova/outline-ops.js +344 -0
- package/scripts/workbench/jova/outline-probe-hover.js +56 -0
- package/scripts/workbench/jova/proofread-ai-rewrite-section.js +94 -0
- package/scripts/workbench/jova/proofread-ops.js +261 -0
- package/scripts/workbench/jova/writing-ops.js +376 -0
- package/scripts/workbench/jova/writing-proofread-abstract.js +103 -0
- package/lib/cli.runtime.config.json +0 -5
- package/references/vip.helixlife.cn.site-ops.md +0 -1469
- package/scripts/workbench-jova-outline-regenerate-node.js +0 -173
- package/scripts/workbench-jova-proofread-section.js +0 -262
- package/scripts/workbench-jova-writing-tool.js +0 -308
- /package/scripts/{analysis-detail-upload-both.js → analysis/detail-upload-both.js} +0 -0
- /package/scripts/{jigsaw-verify-bio-tools.js → analysis/jigsaw-verify-bio-tools.js} +0 -0
- /package/scripts/{jigsaw-verify-drag-tool.js → analysis/jigsaw-verify-drag-tool.js} +0 -0
- /package/scripts/{jigsaw-verify-reference-lines.js → analysis/jigsaw-verify-reference-lines.js} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Helix 学习中心 · 课件 Tab · Chrome 内置 PDF 页码跳转(S-25)。
|
|
3
3
|
* 执行前将 target 改为用户口述的第 N 页(1 起计)。
|
|
4
|
-
* 运行:helixlife-v5-cli run-code --filename=scripts/chrome-pdf-goto-page.js
|
|
4
|
+
* 运行:helixlife-v5-cli run-code --filename=scripts/edu/chrome-pdf-goto-page.js
|
|
5
5
|
*/
|
|
6
6
|
async page => {
|
|
7
7
|
const PDF_VIEWER_PREFIX = 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chrome 内置 PDF 工具栏 · 点击「缩小」。
|
|
3
|
-
* 运行:helixlife-v5-cli run-code --filename=scripts/chrome-pdf-zoom-out.js
|
|
3
|
+
* 运行:helixlife-v5-cli run-code --filename=scripts/edu/chrome-pdf-zoom-out.js
|
|
4
4
|
*/
|
|
5
5
|
async page => {
|
|
6
6
|
const PDF_VIEWER_PREFIX = 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 可选:在脚本顶部设置 cardTitle 以缩小到特定课程卡片(多卡场景);
|
|
7
7
|
* 留空则取首条含「部分章节匹配」的卡片。
|
|
8
8
|
*
|
|
9
|
-
* 运行:helixlife-v5-cli run-code --filename=scripts/hover-partial-chapter-match.js
|
|
9
|
+
* 运行:helixlife-v5-cli run-code --filename=scripts/edu/hover-partial-chapter-match.js
|
|
10
10
|
*/
|
|
11
11
|
async page => {
|
|
12
12
|
const cardTitle = null; // 如 '科研选题三十六策';null 表示取首条
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 打包维护:剔除 node_modules 内会被误识别为 Skill 的文件
|
|
2
|
+
# 在 helixlife-v5-cli 目录执行 npm i 后运行本脚本(维护者打包用)。
|
|
3
|
+
$ErrorActionPreference = 'Stop'
|
|
4
|
+
$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
|
5
|
+
|
|
6
|
+
$targets = @(
|
|
7
|
+
Join-Path $root 'node_modules\@playwright\cli\skills'
|
|
8
|
+
Join-Path $root 'node_modules\playwright-core\lib\tools\cli-client\skill\SKILL.md'
|
|
9
|
+
Join-Path $root 'node_modules\playwright-core\lib\tools\trace\SKILL.md'
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
foreach ($t in $targets) {
|
|
13
|
+
if (Test-Path $t) {
|
|
14
|
+
Remove-Item -Recurse -Force $t
|
|
15
|
+
Write-Host "Removed: $t"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
$remaining = Get-ChildItem -Path $root -Recurse -Filter 'SKILL.md' -File |
|
|
20
|
+
Where-Object { $_.FullName -ne (Join-Path $root 'SKILL.md') }
|
|
21
|
+
if ($remaining) {
|
|
22
|
+
Write-Warning "仍有额外 SKILL.md:"
|
|
23
|
+
$remaining | ForEach-Object { Write-Warning $_.FullName }
|
|
24
|
+
exit 1
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Write-Host 'OK: 仅保留根目录 SKILL.md'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 综述工作台 · 确认提纲 · 根标题编辑探测。
|
|
3
|
+
* 运行:helixlife-v5-cli run-code --filename=scripts/workbench/jova/outline-edit-root-title.js
|
|
4
|
+
*/
|
|
5
|
+
async page => {
|
|
6
|
+
const title = '从实验室到临床:蛋白质脂化类型的研究进展与转化应用综述';
|
|
7
|
+
const row = page
|
|
8
|
+
.locator('main')
|
|
9
|
+
.locator('li')
|
|
10
|
+
.filter({
|
|
11
|
+
has: page.locator('> div').first().getByText(title, { exact: true }),
|
|
12
|
+
})
|
|
13
|
+
.first();
|
|
14
|
+
await row.scrollIntoViewIfNeeded();
|
|
15
|
+
const action = row.locator('> div').first();
|
|
16
|
+
await action.hover();
|
|
17
|
+
await page.waitForTimeout(400);
|
|
18
|
+
const editBtn = action.getByRole('button', { name: '编辑' });
|
|
19
|
+
await editBtn.click({ force: true });
|
|
20
|
+
await page.waitForTimeout(800);
|
|
21
|
+
|
|
22
|
+
const panelTitle = page.getByText('重新生成', { exact: true });
|
|
23
|
+
const inlineSave = row.getByRole('button', { name: '保 存' });
|
|
24
|
+
const userNeed = page.getByRole('textbox', { name: '用户需求' });
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
pathname: await page.evaluate(() => location.pathname),
|
|
28
|
+
panelOpen: (await panelTitle.count()) > 0,
|
|
29
|
+
inlineEditOpen: (await inlineSave.count()) > 0,
|
|
30
|
+
hasUserNeedBox: (await userNeed.count()) > 0,
|
|
31
|
+
buttonCount: await action.locator('button').count(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 综述提纲 · 关键词区一键编辑(改关键词 + 加同级 + 加子级)。
|
|
3
|
+
*
|
|
4
|
+
* 运行(须在 /workbench/jova/outline 且提纲已生成):
|
|
5
|
+
* helixlife-v5-cli run-code --filename=scripts/workbench/jova/outline-keyword-edit.js
|
|
6
|
+
*
|
|
7
|
+
* 修改顶部 CONFIG 即可复用。
|
|
8
|
+
*/
|
|
9
|
+
async page => {
|
|
10
|
+
const CONFIG = {
|
|
11
|
+
keywordOld: '关键词:蛋白质脂化;棕榈酰化;异戊二烯化',
|
|
12
|
+
keywordNew: '关键词:蛋白质脂化;棕榈酰化;异戊二烯化',
|
|
13
|
+
sibling: '英文关键词:protein lipidation; palmitoylation; prenylation',
|
|
14
|
+
children: [
|
|
15
|
+
'脂化酶:DHHC 棕榈酰转移酶与 N-肉豆蔻酰转移酶',
|
|
16
|
+
'检测技术:点击化学标记结合质谱定量分析',
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const main = page.locator('main');
|
|
21
|
+
const wait = ms => page.waitForTimeout(ms);
|
|
22
|
+
|
|
23
|
+
/** 只匹配「当前行」文案,避免误命中包住整棵树的根 li */
|
|
24
|
+
const rowByText = text =>
|
|
25
|
+
main
|
|
26
|
+
.locator('li')
|
|
27
|
+
.filter({
|
|
28
|
+
has: page.locator('> div').first().getByText(text, { exact: true }),
|
|
29
|
+
})
|
|
30
|
+
.first();
|
|
31
|
+
|
|
32
|
+
const actionOf = row => row.locator('> div').first();
|
|
33
|
+
|
|
34
|
+
/** 行内新建/编辑时的 textbox(与「用户需求」面板区分) */
|
|
35
|
+
const inlineTextbox = () =>
|
|
36
|
+
page
|
|
37
|
+
.locator('main li')
|
|
38
|
+
.filter({ has: page.getByRole('button', { name: '保 存' }) })
|
|
39
|
+
.getByRole('textbox')
|
|
40
|
+
.last();
|
|
41
|
+
|
|
42
|
+
const closeRegenPanel = async () => {
|
|
43
|
+
if ((await page.getByText('重新生成', { exact: true }).count()) === 0) return;
|
|
44
|
+
const close = page.getByRole('img', { name: 'close' });
|
|
45
|
+
if ((await close.count()) > 0) {
|
|
46
|
+
await close.last().click();
|
|
47
|
+
await wait(300);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const hoverRow = async text => {
|
|
52
|
+
const row = rowByText(text);
|
|
53
|
+
if ((await row.count()) === 0) throw new Error(`未找到节点:${text}`);
|
|
54
|
+
await row.scrollIntoViewIfNeeded();
|
|
55
|
+
await actionOf(row).hover();
|
|
56
|
+
await wait(500);
|
|
57
|
+
return row;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const clickRowButton = async (text, name) => {
|
|
61
|
+
const row = await hoverRow(text);
|
|
62
|
+
await actionOf(row).getByRole('button', { name }).click({ force: true });
|
|
63
|
+
await wait(400);
|
|
64
|
+
return row;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const fillAndSave = async content => {
|
|
68
|
+
const box = inlineTextbox();
|
|
69
|
+
await box.waitFor({ state: 'visible', timeout: 8000 });
|
|
70
|
+
await box.fill(content);
|
|
71
|
+
await page.getByRole('button', { name: '保 存' }).last().click();
|
|
72
|
+
await wait(400);
|
|
73
|
+
await closeRegenPanel();
|
|
74
|
+
return (await rowByText(content).count()) > 0;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const editNode = async (oldText, newText) => {
|
|
78
|
+
if ((await rowByText(newText).count()) > 0) return true;
|
|
79
|
+
const lookup = (await rowByText(oldText).count()) > 0 ? oldText : newText;
|
|
80
|
+
await clickRowButton(lookup, '编辑');
|
|
81
|
+
await closeRegenPanel();
|
|
82
|
+
return fillAndSave(newText);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const addNode = async (refText, buttonName, content) => {
|
|
86
|
+
if ((await rowByText(content).count()) > 0) return true;
|
|
87
|
+
await clickRowButton(refText, buttonName);
|
|
88
|
+
await closeRegenPanel();
|
|
89
|
+
return fillAndSave(content);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if ((await page.evaluate(() => location.pathname)) !== '/workbench/jova/outline') {
|
|
93
|
+
return { ok: false, reason: '不在确认提纲页' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await closeRegenPanel();
|
|
97
|
+
|
|
98
|
+
const keywordRef = (await rowByText(CONFIG.keywordNew).count()) > 0
|
|
99
|
+
? CONFIG.keywordNew
|
|
100
|
+
: CONFIG.keywordOld;
|
|
101
|
+
|
|
102
|
+
const results = {
|
|
103
|
+
keywordEdited: await editNode(CONFIG.keywordOld, CONFIG.keywordNew),
|
|
104
|
+
siblingAdded: await addNode(keywordRef, '添加同级', CONFIG.sibling),
|
|
105
|
+
childrenAdded: [],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const refAfterEdit = CONFIG.keywordNew;
|
|
109
|
+
for (const child of CONFIG.children) {
|
|
110
|
+
results.childrenAdded.push({
|
|
111
|
+
content: child,
|
|
112
|
+
saved: await addNode(refAfterEdit, '添加子级', child),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
results.ok =
|
|
117
|
+
results.keywordEdited &&
|
|
118
|
+
results.siblingAdded &&
|
|
119
|
+
results.childrenAdded.every(c => c.saved);
|
|
120
|
+
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 综述工作台 · 确认提纲(/workbench/jova/outline)· 精细化操作回归脚本
|
|
3
|
+
*
|
|
4
|
+
* 运行(须已在确认提纲页且提纲已生成完毕):
|
|
5
|
+
* helixlife-v5-cli run-code --filename=scripts/workbench/jova/outline-ops.js
|
|
6
|
+
*
|
|
7
|
+
* 可选:在脚本顶部修改 CONFIG,指定探测/编辑/增删的目标节点文案。
|
|
8
|
+
*
|
|
9
|
+
* 覆盖链路:
|
|
10
|
+
* 1. 悬浮按钮规则(根标题 vs 普通节点)
|
|
11
|
+
* 2. 编辑 → 重新生成面板(用户需求 / 一键生成 / 复制 / 替换确认)
|
|
12
|
+
* 3. 添加同级 / 添加子级 / 删除(含确认 Popover)
|
|
13
|
+
*
|
|
14
|
+
* 注意:
|
|
15
|
+
* - rowByText 必须限定 li > div 直接子文案,禁止 filter({ has: getByText }) 误命中根 li。
|
|
16
|
+
* - 行内按钮须先 hover 操作区行才可见;定位时缩小到 li > div 操作区,避免 strict。
|
|
17
|
+
* - 「替换原文」「删除」的确认层为 Popover(role=tooltip),非 window.confirm。
|
|
18
|
+
* - 一键生成需等待「生成中」结束(默认最长 90s)。
|
|
19
|
+
* - 添加同级/子级时须与参考节点格式一致(见 inferNodeFormat / formatNewNodeText)。
|
|
20
|
+
* - 脚本会在末尾清理 CONFIG 中写入的测试节点(若创建成功)。
|
|
21
|
+
*/
|
|
22
|
+
async page => {
|
|
23
|
+
const CONFIG = {
|
|
24
|
+
rootTitle: '从实验室到临床:蛋白质脂化类型的研究进展与转化应用综述',
|
|
25
|
+
editTarget: '关键词:蛋白质脂化;棕榈酰化;异戊二烯化;GPI锚定',
|
|
26
|
+
siblingParentText:
|
|
27
|
+
'蛋白质脂化是细胞中普遍存在的共价修饰过程,脂类分子的附着显著改变蛋白质的疏水性和膜亲和性,从而影响其在信号网络中的定位和功能。',
|
|
28
|
+
// 留空则按 siblingParentText 的格式自动生成
|
|
29
|
+
testSiblingText: '',
|
|
30
|
+
testChildText: '',
|
|
31
|
+
aiUserNeed: '将内容改写得更学术简洁',
|
|
32
|
+
runAiGenerate: false,
|
|
33
|
+
runReplaceConfirm: false,
|
|
34
|
+
cleanup: true,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const main = page.locator('main');
|
|
38
|
+
const wait = ms => page.waitForTimeout(ms);
|
|
39
|
+
|
|
40
|
+
const rowByText = text =>
|
|
41
|
+
main
|
|
42
|
+
.locator('li')
|
|
43
|
+
.filter({
|
|
44
|
+
has: page.locator('> div').first().getByText(text, { exact: true }),
|
|
45
|
+
})
|
|
46
|
+
.first();
|
|
47
|
+
|
|
48
|
+
const actionOf = row => row.locator('> div').first();
|
|
49
|
+
|
|
50
|
+
const inlineTextbox = () =>
|
|
51
|
+
page
|
|
52
|
+
.locator('main li')
|
|
53
|
+
.filter({ has: page.getByRole('button', { name: '保 存' }) })
|
|
54
|
+
.getByRole('textbox')
|
|
55
|
+
.last();
|
|
56
|
+
|
|
57
|
+
/** 读取操作区主文案(排除「-」bullet 标记与按钮) */
|
|
58
|
+
const readNodeText = async row => {
|
|
59
|
+
const action = actionOf(row);
|
|
60
|
+
return action.evaluate(el => {
|
|
61
|
+
const parts = [];
|
|
62
|
+
for (const node of el.childNodes) {
|
|
63
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
64
|
+
const tag = node.tagName.toLowerCase();
|
|
65
|
+
if (tag === 'button') continue;
|
|
66
|
+
const t = (node.textContent || '').trim();
|
|
67
|
+
if (!t || t === '-') continue;
|
|
68
|
+
parts.push(t);
|
|
69
|
+
}
|
|
70
|
+
return parts.join('');
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** 是否带 UI 层 bullet 标记(「-」与正文分列,勿写入 textbox) */
|
|
75
|
+
const hasBulletMarker = async row =>
|
|
76
|
+
(await actionOf(row).getByText('-', { exact: true }).count()) > 0;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 提纲节点格式(与线上 DOM 实测一致):
|
|
80
|
+
* - section:章节标签,如「摘要:」「前言:」
|
|
81
|
+
* - keyword:「关键词:…」
|
|
82
|
+
* - numbered:「1.1 …」「2.3.1 …」
|
|
83
|
+
* - bullet:带「-」标记的 bullet 段落(textbox 仅填正文句,不含「- 」)
|
|
84
|
+
* - plain:摘要等区块下的长段正文
|
|
85
|
+
*/
|
|
86
|
+
const inferNodeFormat = async (row, textHint) => {
|
|
87
|
+
const text = textHint || (await readNodeText(row));
|
|
88
|
+
if (await hasBulletMarker(row)) return { kind: 'bullet', text };
|
|
89
|
+
if (/^关键词:/.test(text)) return { kind: 'keyword', text };
|
|
90
|
+
if (/^\d+(?:\.\d+)+\s+\S/.test(text)) return { kind: 'numbered', text };
|
|
91
|
+
if (/:$/.test(text.trim()) && text.length <= 48) return { kind: 'section', text };
|
|
92
|
+
return { kind: 'plain', text };
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** 子级默认格式:章节下多为 bullet 句;bullet 下多为 plain 子句 */
|
|
96
|
+
const inferChildFormat = parentFmt => {
|
|
97
|
+
if (parentFmt.kind === 'section') return { kind: 'bullet', text: parentFmt.text };
|
|
98
|
+
if (parentFmt.kind === 'bullet') return { kind: 'plain', text: parentFmt.text };
|
|
99
|
+
if (parentFmt.kind === 'numbered') return { kind: 'plain', text: parentFmt.text };
|
|
100
|
+
return parentFmt;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 按参考格式生成待填文案(不擅自加「- 」前缀;bullet 由 UI 渲染)。
|
|
105
|
+
*/
|
|
106
|
+
const formatNewNodeText = (fmt, draft, role) => {
|
|
107
|
+
const d = (draft || '').trim();
|
|
108
|
+
switch (fmt.kind) {
|
|
109
|
+
case 'keyword':
|
|
110
|
+
if (d.startsWith('关键词:')) return d;
|
|
111
|
+
return d ? `关键词:${d}` : '关键词:待补充术语';
|
|
112
|
+
case 'section':
|
|
113
|
+
if (d.endsWith(':')) return d;
|
|
114
|
+
return d ? `${d}:` : '新章节:';
|
|
115
|
+
case 'numbered': {
|
|
116
|
+
if (/^\d+(?:\.\d+)+\s/.test(d)) return d;
|
|
117
|
+
const m = fmt.text.match(/^(\d+(?:\.\d+)*)/);
|
|
118
|
+
const base = m ? m[1] : '1.1';
|
|
119
|
+
const tail = d || '新小节标题';
|
|
120
|
+
return `${base}.1 ${tail}`;
|
|
121
|
+
}
|
|
122
|
+
case 'bullet':
|
|
123
|
+
return (
|
|
124
|
+
d ||
|
|
125
|
+
'【待补充】此处填写与同级 bullet 一致的完整陈述句,勿手动输入「- 」前缀。'
|
|
126
|
+
);
|
|
127
|
+
case 'plain':
|
|
128
|
+
default:
|
|
129
|
+
if (role === 'child') {
|
|
130
|
+
return d || '【待补充】此处填写作为下级节点的补充说明句。';
|
|
131
|
+
}
|
|
132
|
+
return d || '【待补充】此处填写与上下文一致的完整段落。';
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const addTreeNode = async (refText, buttonName, draftText) => {
|
|
137
|
+
const row = rowByText(refText);
|
|
138
|
+
if ((await row.count()) === 0) throw new Error(`未找到节点:${refText}`);
|
|
139
|
+
const parentFmt = await inferNodeFormat(row, refText);
|
|
140
|
+
const role = buttonName === '添加同级' ? 'sibling' : 'child';
|
|
141
|
+
const targetFmt =
|
|
142
|
+
role === 'child' ? inferChildFormat(parentFmt) : parentFmt;
|
|
143
|
+
const content = formatNewNodeText(targetFmt, draftText, role);
|
|
144
|
+
await clickRowButton(refText, buttonName);
|
|
145
|
+
const inline = inlineTextbox();
|
|
146
|
+
await inline.waitFor({ state: 'visible', timeout: 8000 });
|
|
147
|
+
await inline.fill(content);
|
|
148
|
+
await page.getByRole('button', { name: '保 存' }).last().click();
|
|
149
|
+
await wait(400);
|
|
150
|
+
const saved = (await rowByText(content).count()) > 0;
|
|
151
|
+
return {
|
|
152
|
+
role,
|
|
153
|
+
refText,
|
|
154
|
+
parentFormat: parentFmt.kind,
|
|
155
|
+
appliedFormat: targetFmt.kind,
|
|
156
|
+
content,
|
|
157
|
+
saved,
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const hoverRow = async text => {
|
|
162
|
+
const row = rowByText(text);
|
|
163
|
+
if ((await row.count()) === 0) throw new Error(`未找到节点:${text}`);
|
|
164
|
+
await row.scrollIntoViewIfNeeded();
|
|
165
|
+
await actionOf(row).hover();
|
|
166
|
+
await wait(500);
|
|
167
|
+
return row;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const visibleButtons = async row => {
|
|
171
|
+
const action = actionOf(row);
|
|
172
|
+
const btns = action.locator('button');
|
|
173
|
+
const n = await btns.count();
|
|
174
|
+
const names = [];
|
|
175
|
+
for (let i = 0; i < n; i++) {
|
|
176
|
+
const b = btns.nth(i);
|
|
177
|
+
const aria = await b.getAttribute('aria-label');
|
|
178
|
+
const raw = await b.textContent();
|
|
179
|
+
names.push(aria || (raw ? raw.trim() : '') || 'icon');
|
|
180
|
+
}
|
|
181
|
+
return names;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const clickRowButton = async (text, name) => {
|
|
185
|
+
const row = await hoverRow(text);
|
|
186
|
+
await actionOf(row).getByRole('button', { name }).click({ force: true });
|
|
187
|
+
await wait(400);
|
|
188
|
+
return row;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const saveInlineIfOpen = async row => {
|
|
192
|
+
const save = row.getByRole('button', { name: '保 存' });
|
|
193
|
+
if ((await save.count()) > 0 && (await save.isVisible())) {
|
|
194
|
+
await save.click();
|
|
195
|
+
await wait(400);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const closeRegenPanel = async () => {
|
|
202
|
+
if ((await page.getByText('重新生成', { exact: true }).count()) === 0) return;
|
|
203
|
+
const closeImg = page.getByRole('img', { name: 'close' });
|
|
204
|
+
if ((await closeImg.count()) > 0) {
|
|
205
|
+
await closeImg.last().click();
|
|
206
|
+
await wait(300);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const waitAiDone = async timeoutMs => {
|
|
211
|
+
const limit = timeoutMs || 90000;
|
|
212
|
+
let elapsed = 0;
|
|
213
|
+
while (elapsed < limit) {
|
|
214
|
+
const generating = page.getByRole('button', { name: '生成中' });
|
|
215
|
+
if ((await generating.count()) === 0) break;
|
|
216
|
+
await wait(1500);
|
|
217
|
+
elapsed += 1500;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const results = { pathname: await page.evaluate(() => location.pathname) };
|
|
222
|
+
|
|
223
|
+
if (results.pathname !== '/workbench/jova/outline') {
|
|
224
|
+
return { ok: false, reason: '当前不在确认提纲页', pathname: results.pathname };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- 1. 悬浮规则 ---
|
|
228
|
+
const rootRow = await hoverRow(CONFIG.rootTitle);
|
|
229
|
+
results.hoverRoot = await visibleButtons(rootRow);
|
|
230
|
+
|
|
231
|
+
const sectionRow = await hoverRow('摘要:');
|
|
232
|
+
results.hoverSection = await visibleButtons(sectionRow);
|
|
233
|
+
|
|
234
|
+
// --- 2. 编辑 + 重新生成面板结构 ---
|
|
235
|
+
await clickRowButton(CONFIG.editTarget, '编辑');
|
|
236
|
+
results.editPanel = {
|
|
237
|
+
inlineTextbox: (await rowByText(CONFIG.editTarget).locator('textbox').count()) > 0,
|
|
238
|
+
regenTitle: (await page.getByText('重新生成', { exact: true }).count()) > 0,
|
|
239
|
+
oneClickGen: (await page.getByRole('button', { name: '一键生成' }).count()) > 0,
|
|
240
|
+
userNeed: (await page.getByRole('textbox', { name: '用户需求' }).count()) > 0,
|
|
241
|
+
resultArea: (await page.getByText('生成结果', { exact: true }).count()) > 0,
|
|
242
|
+
copyBtn: (await page.getByRole('button', { name: '一键复制' }).count()) > 0,
|
|
243
|
+
replaceBtn: (await page.getByRole('button', { name: '替换原文' }).count()) > 0,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (CONFIG.runAiGenerate) {
|
|
247
|
+
await page.getByRole('textbox', { name: '用户需求' }).fill(CONFIG.aiUserNeed);
|
|
248
|
+
await page.getByRole('button', { name: '一键生成' }).click();
|
|
249
|
+
await waitAiDone();
|
|
250
|
+
results.aiDone = (await page.getByRole('button', { name: '一键复制' }).isEnabled());
|
|
251
|
+
|
|
252
|
+
if (results.aiDone) {
|
|
253
|
+
await page.getByRole('button', { name: '一键复制' }).click();
|
|
254
|
+
results.copyToast = (await page.getByText('复制成功').count()) > 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (CONFIG.runReplaceConfirm && results.aiDone) {
|
|
258
|
+
await page.getByRole('button', { name: '替换原文' }).click();
|
|
259
|
+
await page.getByRole('button', { name: '确 定' }).click();
|
|
260
|
+
await wait(500);
|
|
261
|
+
const row = rowByText(CONFIG.editTarget);
|
|
262
|
+
results.replaced = (await row.locator('textbox').inputValue()).length > 0;
|
|
263
|
+
await saveInlineIfOpen(row);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await closeRegenPanel();
|
|
268
|
+
const editRow = rowByText(CONFIG.editTarget);
|
|
269
|
+
if (await saveInlineIfOpen(editRow)) {
|
|
270
|
+
/* 若仍停留在内联编辑 */
|
|
271
|
+
} else {
|
|
272
|
+
const closeInline = editRow.getByRole('img', { name: 'close' });
|
|
273
|
+
if ((await closeInline.count()) > 0) await closeInline.click();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- 3. 添加同级(文案格式与参考节点一致) ---
|
|
277
|
+
let siblingCreated = false;
|
|
278
|
+
let siblingContent = '';
|
|
279
|
+
try {
|
|
280
|
+
const sibling = await addTreeNode(
|
|
281
|
+
CONFIG.siblingParentText,
|
|
282
|
+
'添加同级',
|
|
283
|
+
CONFIG.testSiblingText,
|
|
284
|
+
);
|
|
285
|
+
siblingContent = sibling.content;
|
|
286
|
+
siblingCreated = sibling.saved;
|
|
287
|
+
results.addSibling = sibling;
|
|
288
|
+
} catch (e) {
|
|
289
|
+
results.addSibling = { error: String(e) };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// --- 4. 添加子级(子级格式随父节点类型推断) ---
|
|
293
|
+
let childCreated = false;
|
|
294
|
+
let childContent = '';
|
|
295
|
+
try {
|
|
296
|
+
const child = await addTreeNode(
|
|
297
|
+
CONFIG.siblingParentText,
|
|
298
|
+
'添加子级',
|
|
299
|
+
CONFIG.testChildText,
|
|
300
|
+
);
|
|
301
|
+
childContent = child.content;
|
|
302
|
+
childCreated = child.saved;
|
|
303
|
+
results.addChild = child;
|
|
304
|
+
} catch (e) {
|
|
305
|
+
results.addChild = { error: String(e) };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// --- 5. 删除(对测试同级节点) ---
|
|
309
|
+
if (siblingCreated && siblingContent) {
|
|
310
|
+
try {
|
|
311
|
+
const row = await hoverRow(siblingContent);
|
|
312
|
+
await actionOf(row).getByRole('button', { name: '删除' }).click();
|
|
313
|
+
results.deletePopover =
|
|
314
|
+
(await page.getByText('是否删除标题及其包含的段落内容?').count()) > 0;
|
|
315
|
+
await page.getByRole('button', { name: '确 定' }).click();
|
|
316
|
+
await wait(400);
|
|
317
|
+
results.deleteSibling =
|
|
318
|
+
(await page.getByText(siblingContent, { exact: true }).count()) === 0;
|
|
319
|
+
} catch (e) {
|
|
320
|
+
results.deleteSibling = { error: String(e) };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// --- 清理测试子级 ---
|
|
325
|
+
if (CONFIG.cleanup && childCreated && childContent) {
|
|
326
|
+
try {
|
|
327
|
+
const row = await hoverRow(childContent);
|
|
328
|
+
await actionOf(row).getByRole('button', { name: '删除' }).click();
|
|
329
|
+
await page.getByRole('button', { name: '确 定' }).click();
|
|
330
|
+
} catch (_) {
|
|
331
|
+
/* 忽略清理失败 */
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
results.ok = true;
|
|
336
|
+
results.summary = {
|
|
337
|
+
rootOnlyEdit:
|
|
338
|
+
results.hoverRoot.length === 1 && results.hoverRoot[0] === '编辑',
|
|
339
|
+
sectionHasFour:
|
|
340
|
+
results.hoverSection.length === 4 &&
|
|
341
|
+
results.hoverSection.includes('添加同级'),
|
|
342
|
+
};
|
|
343
|
+
return results;
|
|
344
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 综述工作台 · 确认提纲 · 悬浮按钮探测(含可见性)。
|
|
3
|
+
* 运行:helixlife-v5-cli run-code --filename=scripts/workbench/jova/outline-probe-hover.js
|
|
4
|
+
*/
|
|
5
|
+
async page => {
|
|
6
|
+
const main = page.locator('main');
|
|
7
|
+
|
|
8
|
+
const probe = async (label, textMatch) => {
|
|
9
|
+
const row = main
|
|
10
|
+
.locator('li')
|
|
11
|
+
.filter({
|
|
12
|
+
has: page.locator('> div').first().getByText(textMatch, { exact: true }),
|
|
13
|
+
})
|
|
14
|
+
.first();
|
|
15
|
+
if ((await row.count()) === 0) {
|
|
16
|
+
return { label, text: textMatch, error: '未找到 li 行' };
|
|
17
|
+
}
|
|
18
|
+
await row.scrollIntoViewIfNeeded();
|
|
19
|
+
const action = row.locator('> div').first();
|
|
20
|
+
await action.hover();
|
|
21
|
+
await page.waitForTimeout(600);
|
|
22
|
+
const btns = action.locator('button');
|
|
23
|
+
const count = await btns.count();
|
|
24
|
+
const items = [];
|
|
25
|
+
for (let i = 0; i < count; i++) {
|
|
26
|
+
const btn = btns.nth(i);
|
|
27
|
+
const raw = await btn.textContent();
|
|
28
|
+
const name =
|
|
29
|
+
(await btn.getAttribute('aria-label')) ||
|
|
30
|
+
(raw ? raw.trim() : '') ||
|
|
31
|
+
'icon-only';
|
|
32
|
+
items.push({
|
|
33
|
+
name,
|
|
34
|
+
visible: await btn.isVisible(),
|
|
35
|
+
box: await btn.boundingBox(),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return { label, text: textMatch, count, items };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
rootTitle: await probe(
|
|
43
|
+
'rootTitle',
|
|
44
|
+
'从实验室到临床:蛋白质脂化类型的研究进展与转化应用综述',
|
|
45
|
+
),
|
|
46
|
+
sectionHeader: await probe('sectionHeader', '摘要:'),
|
|
47
|
+
bulletParagraph: await probe(
|
|
48
|
+
'bulletParagraph',
|
|
49
|
+
'蛋白质脂化是细胞中普遍存在的共价修饰过程,脂类分子的附着显著改变蛋白质的疏水性和膜亲和性,从而影响其在信号网络中的定位和功能。',
|
|
50
|
+
),
|
|
51
|
+
keywordLine: await probe(
|
|
52
|
+
'keywordLine',
|
|
53
|
+
'关键词:蛋白质脂化;棕榈酰化;异戊二烯化;GPI锚定',
|
|
54
|
+
),
|
|
55
|
+
};
|
|
56
|
+
}
|