关于 hexagone-web-feature-extractor
hexagone-web-feature-extractor 是用于探索 Hexagone Web 功能空间的技能,可自动浏览页面和标签并截图,再生成面向产品负责人的 Markdown 功能说明。它通过 Playwright 操作浏览器,并将截图直接保存到磁盘。
主要功能
- 01自动登录并浏览 Hexagone Web
- 02发现侧边栏页面和内部标签
- 03将页面截图保存到磁盘
- 04收集页面元数据
- 05根据 JSON 校验并生成 Markdown
适用场景
- 01记录 Hexagone Web 空间中的页面
- 02采集多个页面和标签的截图
- 03为产品负责人生成产品功能文档
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dedalus-erp-pas/ foundation-skills --skill hexagone-web-feature-extractor安装后在 Claude Code 中输入/hexagone-web-feature-extractor确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
reference/default-structures-nomenclatures.jsonscripts/generate-md.jspackage.json详细介绍
Hexagone Web 功能提取器
探索 Hexagone Web 的功能空间,为每个页面/标签页截取屏幕截图,并生成一份面向产品负责人的 Markdown 文档(.md),其中包含功能说明和嵌入的屏幕截图。
前提条件
- 已安装 Node.js
- Playwright npm 包(
npm install playwright)——会自动安装无头 Chromium - 可访问 Hexagone Web 服务器的网络连接(默认地址:
https://ws004202.dedalus.lan:8065/hexagone-01/vue/login)
配置
默认值适用于 1920x1080 的标准 Hexagone Web 布局。若布局不同,请进行调整。
| 参数 | 默认值 | 说明 |
|---|---|---|
| 视口 | 1920x1080 | 浏览器视口大小 |
| 侧边栏点击 X 坐标 | 38 | 点击侧边栏图标的水平像素位置(折叠模式) |
| 侧边栏最远左边界 | 280 | 用于识别侧边栏链接的最大 rect.left 值(展开模式) |
| 页眉高度偏移 | 55 | 用于排除页眉元素的最小 rect.top 值 |
| 登录等待超时 | 30s | 轮询登录成功的最长时间 |
| 页面加载等待超时 | 10s | 导航后轮询页面加载的最长时间 |
| 屏幕截图目录 | ./screenshots | 屏幕截图的保存位置(相对于工作目录) |
工作流概览
1. SETUP → Install Playwright, launch headless Chromium
2. CONNECTION → Log in to Hexagone Web
3. NAVIGATION → Navigate to the target space
4. DISCOVERY → Expand sidebar, list all menu pages
5. EXPLORATION → Visit each page, capture screenshots + metadata
6. GENERATION → Produce the Markdown document with embedded screenshots
相比 Chrome 扩展方案的主要优势:屏幕截图通过 page.screenshot() 直接保存到磁盘,无需桥接服务器或传输步骤。
步骤 1:设置
1.1 安装 Playwright
npm install playwright
npx playwright install chromium
1.2 启动浏览器
const { chromium } = require('playwright');
const browser = await chromium.launch({
headless: true,
args: ['--ignore-certificate-errors', '--no-sandbox']
});
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
ignoreHTTPSErrors: true // Handles self-signed certs automatically
});
const page = await context.newPage();
为什么使用无头 Chromium? 无需手动接受 SSL 证书、设置 Chrome 扩展或传输屏幕截图。ignoreHTTPSErrors: true 选项会以编程方式处理自签名证书。
步骤 2:连接到 Hexagone Web
2.1 导航到登录页面
await page.goto(LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await sleep(3000); // Wait for Vue.js to mount
2.2 填写登录表单
Hexagone Web 登录表单有 3 个字段:用户名、密码、管理器代码。默认凭据:用户名为 apvhn,密码为随机密码,除非用户另行提供。
使用 page.evaluate() 和原生 setter 模式——Vue.js 必须采用此方式,因为它无法检测直接注入的值变化:
await page.evaluate(({ username, password }) => {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
const userInput = document.querySelector('input[type="text"]');
if (userInput) {
nativeSetter.call(userInput, username);
userInput.dispatchEvent(new Event('input', { bubbles: true }));
}
const pwdInput = document.querySelector('input[type="password"]');
if (pwdInput) {
nativeSetter.call(pwdInput, password);
pwdInput.dispatchEvent(new Event('input', { bubbles: true }));
}
const loginBtn = Array.from(document.querySelectorAll('button'))
.find(b => /connect/i.test(b.textContent));
if (loginBtn) loginBtn.click();
}, { username: USERNAME, password: PASSWORD });
2.3 验证连接
每 2s 轮询一次,最长等待 30s,直到 URL 不再包含 /login:
for (let i = 0; i < 15; i++) {
await sleep(2000);
if (!page.url().includes('/login')) break;
}
如果登录失败:使用 page.screenshot() 截取调试屏幕截图,并报告失败情况。
步骤 3:导航到目标空间
3.1 打开空间选择器
重要:使用 page.mouse.click()——不要通过 page.evaluate() 使用 el.click()。
Vue.js 事件处理程序需要原生鼠标事件(mousedown + mouseup + click)。JavaScript 的 el.click() 只会触发 click 事件,无法触发空间下拉菜单。这是开发过程中发现的首要问题。
空间选择器是橙色面包屑栏中 class 为 bg:orange-dark 的 div。其中包含图标 <i class="hexa-icons">changer_espaces</i>,后接一个显示当前空间名称的 <span>。
// Find the space selector coordinates
const selectorRect = await page.evaluate(() => {
for (const el of document.querySelectorAll('div, span')) {
const cls = typeof el.className === 'string' ? el.className : '';
if (cls.includes('bg:orange-dark') && !cls.includes('uppercase') && !cls.includes('hover:')) {
const rect = el.getBoundingClientRect();
if (rect.top > 30 && rect.top < 70 && rect.height > 15) {
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}
}
}
return null;
});
// Click with REAL mouse events (mandatory for Vue.js)
await page.mouse.click(selectorRect.x, selectorRect.y);
await sleep(3000);
3.2 选择空间
下拉菜单会在侧边栏区域内以一组 <div> 元素呈现,其 class 为 px:1 py:3/4 hover:bg:orange-dark cursor:pointer。空间按字母顺序排列。
// Find the target space element
const target = await page.evaluate((spaceName) => {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
while (walker.nextNode()) {
const el = walker.currentNode;
if (el.textContent.trim() === spaceName) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0 && rect.top > 30) {
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}
}
}
return null;
}, TARGET_SPACE);
// Click with mouse (not el.click())
await page.mouse.click(target.x, target.y);
3.3 等待加载
Hexagone Web 会通过“Connexion... Redirection...”中间页面进行重定向。每 2s 轮询一次,最长等待 24s,直到 URL 不再包含默认的着陆空间 patient-portal:
for (let i = 0; i < 12; i++) {
await sleep(2000);
if (!page.url().includes('patient-portal')) break;
}
await sleep(3000); // Extra wait for Vue.js rendering
步骤 4:发现页面
4.1 展开侧边栏
侧边栏默认处于折叠状态(仅显示图标,宽度约为 65px)。点击汉堡菜单以展开侧边栏并显示文本标签:
await page.mouse.click(34, 50); // Hamburger icon position
await sleep(2000);
4.2 识别侧边栏菜单项
主要方法:查找左侧 280px 范围内带有 cursor:pointer 类的元素。从 <i class="hexa-icons"> 子元素中移除图标文本:
const menuItems = await page.evaluate((excludeLabels) => {
const items = [];
const seen = new Set();
const allEls = document.querySelectorAll('[class*="cursor:pointer"], a');
for (const el of allEls) {
const rect = el.getBoundingClientRect();
if (rect.left < 280 && rect.top > 55 && rect.height > 15 && rect.height < 60) {
let text = el.textContent.trim();
// Strip icon prefix text
const icon = el.querySelector('i');
if (icon) text = text.replace(icon.textContent.trim(), '').trim();
if (!text || text.length <= 1 || text.length >= 60 || seen.has(text)) continue;
if (excludeLabels.includes(text)) continue;
// Skip section headers (all-caps short text like "ACHATS")
if (/^[A-Z ]+$/.test(text) && text.length < 15) continue;
seen.add(text);
items.push({
label: text,
y: Math.round(rect.top + rect.height / 2),
x: Math.round(rect.left + rect.width / 2)
});
}
}
return items;
}, ['Trier par Importance', 'Trier par Emetteur']);
侧边栏 DOM 结构(观察所得):
<div class="sidebar--section min-w:sidebar bg:teal-darker">
<div class="py:1 transition cursor:pointer w:inherit hover:bg:teal-dark">
<div class="flex items:center whitespace:no-wrap">
<i class="hexa-icons text:3/2" aria-label="Fournisseurs">fournisseurs</i>
<span class="sidebar--label">Fournisseurs</span>
</div>
</div>
</div>
注意:旧的 a.hexa 选择器并不适用于所有空间。侧边栏元素是带有 cursor:pointer 类的 <div> 元素,而不是 <a> 标签。
4.3 排除的项目
过滤掉不是页面的 UI 元素:
Trier par Importance/Trier par Emetteur— Mes Post-Its 面板中的排序按钮- 分区标题(如
ACHATS这样的全大写短文本)— 分区分隔符,不是可点击的页面
4.4 验证关卡
如果发现后没有找到任何项目:停止探索并截取调试屏幕截图。不要继续处理空页面列表。将 DOM 保存到调试文件以便分析。
4.5 折叠侧边栏
发现菜单项后,在开始探索前折叠侧边栏:
await page.mouse.click(34, 50); // Toggle hamburger
await sleep(1000);
第 5 步:探索与截图
5.1 遍历菜单中的每个页面
重要:点击菜单项后,侧边栏会折叠。每次点击前,你都必须重新展开侧边栏并重新发现该项目的位置。
For each menu item:
1. Expand sidebar: page.mouse.click(34, 50), wait 1.5s
2. Re-discover the item position via page.evaluate() (label matching)
3. Click the item: page.mouse.click(freshPos.x, freshPos.y)
4. Wait for page load (networkidle + 1.5s extra)
5. Take screenshot: page.screenshot({ path: ... })
6. Extract metadata via page.evaluate()
7. If tabs found, explore them (see 5.2)
8. Build feature description with PO-oriented text
为何每次都要重新发现? 侧边栏展开时会重新渲染内容。项目坐标会根据滚动位置和当前可见的项目而变化。使用初次发现时的过期坐标,会点击到主内容区域,而不是侧边栏。
5.2 识别内部标签页
某些页面包含内部标签页(例如,Fournisseurs → Saisie / Consultation / Réalisé,Marchés → SAISIE / CONSULTATION / DÉBLOCAGE)。标签页也要使用 page.mouse.click():
const tabCoords = await page.evaluate((label) => {
for (const tab of document.querySelectorAll('[role="tab"], .v-tab')) {
if (tab.textContent.trim() === label) {
const rect = tab.getBoundingClientRect();
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}
}
return null;
}, tabLabel);
if (tabCoords) {
await page.mouse.click(tabCoords.x, tabCoords.y);
await sleep(2500);
await page.screenshot({ path: tabScreenshotPath });
}
警告:选择器 [class*="tab"] 范围太广,会匹配分页控件、筛选器下拉菜单以及之前渲染页面中的元素。检测标签页时,优先使用 [role="tab"] 或 .v-tab。
5.3 截取屏幕截图
屏幕截图会直接保存到磁盘,无需桥接或传输:
await page.screenshot({
path: path.join(SCREENSHOT_DIR, `${index}-${sanitize(pageName)}.png`),
fullPage: false // Capture viewport only (1920x1080)
});
5.4 存储元数据
在探索过程中构建 features.json 数组,并根据页面内容分析(表格、表单、操作、标签页)补充面向 PO 的描述。
第 6 步:生成 Markdown 文档
6.1 准备输入
根据第 5 步收集的元数据创建 features.json 文件。文件必须符合以下结构:
{
"space": "Name of the explored space",
"features": [
{
"title": "Feature name (string, required)",
"description": "PO-oriented functional description (string, required)",
"capabilities": ["Capability 1", "Capability 2"],
"businessValue": "Business value description (string)",
"screenshots": [
{ "file": "filename.png", "caption": "Screenshot description" }
]
}
]
}
验证规则:
space必须是非空字符串features必须是非空数组- 每个 feature 必须包含
title(字符串)和description(字符串) capabilities必须是字符串数组(可以为空)screenshots必须是包含file(字符串)和caption(字符串)的对象数组
6.2 生成文档
使用脚本 scripts/generate-md.js:
node scripts/generate-md.js --input features.json --output /path/to/output.md --screenshots /path/to/screenshots
--input 参数为必填项。脚本会验证输入;如果 JSON 格式错误,会显示清晰的错误消息并退出。无需运行 npm install——该脚本仅使用 Node.js 内置模块。
6.3 文档结构
# Title (space name)
> Subtitle with date
## Table of contents (linked)
For each feature:
## N. Feature title
- Functional description
- Screenshot(s) as 
### Key capabilities (numbered list)
### Business value
---
6.4 验证和输出
确认生成的文件存在,并包含所有预期的功能:
grep -c '^## [0-9]' output.md # Should match the number of features
输入参数
用户必须提供:
- 登录 URL (可选):默认为
https://ws004202.dedalus.lan:8065/hexagone-01/vue/login。如有需要,用户可以提供其他 URL。 - 用户名 (可选):默认为
apvhn。如有需要,用户可以提供其他代码。 - 密码 (可选):默认为随机值。如有需要,用户可以提供指定的密码。
- 目标空间:要探索的空间的准确名称(例如,“HA GHT”、“STRUCTURES / NOMENCLATURES”)
关键规则(从生产运行中总结)
- 始终使用
page.mouse.click()进行任何 Vue.js 交互——切勿通过page.evaluate()使用el.click()。Vue.js 需要原生的 mousedown/mouseup 事件,只有page.mouse.click()才会触发这些事件。 - 每次点击页面前都重新展开侧边栏——每次导航后侧边栏都会折叠。使用初次发现时的过期坐标会导致完全点不到侧边栏。
- 每次迭代都重新发现条目位置——侧边栏坐标会在重新渲染时发生变化。始终查询 DOM 获取最新坐标。
- 侧边栏条目使用
[class*="cursor:pointer"],而不是a.hexa——侧边栏元素是<div>,不是<a>标签。 - 从侧边栏标签中移除
<i class="hexa-icons">文本——图标文本会被添加到前面(例如,tdbTableau de bord→Tableau de bord)。 - 从页面列表中排除“按重要性排序”等实用按钮——它们是便签面板中的排序控件,不是页面。
- 使用范围更窄的标签页选择器(
[role="tab"]、.v-tab)——[class*="tab"]范围过宽,会选中分页、筛选器以及之前渲染页面中的过期元素。
故障排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 空间下拉菜单未打开 | 使用了 el.click(),而不是 page.mouse.click() | Vue.js 交互始终使用 page.mouse.click() |
| 所有页面截图都相同 | 侧边栏已折叠,点击未命中侧边栏条目 | 每次点击前重新展开侧边栏并重新发现坐标 |
| 找不到侧边栏条目 | 使用了 a.hexa 选择器 | 使用 [class*="cursor:pointer"] 并移除图标文本 |
| 检测到过多“标签页” | 选择器 [class*="tab"] 范围过宽 | 仅使用 [role="tab"] 或 .v-tab |
| 页面列表中出现“按重要性排序” | 将排序按钮误认为页面 | 添加到排除列表:['Trier par Importance', 'Trier par Emetteur'] |
| Vue.js 未检测到字段 | 直接注入值但未触发事件 | 使用 nativeInputValueSetter + dispatchEvent('input') |
| 点击登录按钮后选中了错误的按钮 | 页面上有多个按钮 | 使用文本内容匹配:buttons.find(b => /connect/i.test(b.textContent)) |
| 点击后页面内容未加载 | 服务器缓慢或页面内容繁重 | 使用 page.waitForLoadState('networkidle') + 额外等待 |
| SSL 证书错误 | Hexagone Web 服务器使用自签名证书 | 在浏览器上下文中设置 ignoreHTTPSErrors: true(自动处理) |
generate-md.js 验证失败 | features.json 格式错误 | 检查必需字段:space、features[].title、features[].description |