mcpField
H
hexagone-web-feature-extractor

hexagone-web-feature-extractor

github.com/dedalus-erp-pas文档处理106 次安装
代码仓库

关于 hexagone-web-feature-extractor

hexagone-web-feature-extractor 是用于探索 Hexagone Web 功能空间的技能,可自动浏览页面和标签并截图,再生成面向产品负责人的 Markdown 功能说明。它通过 Playwright 操作浏览器,并将截图直接保存到磁盘。

主要功能

  1. 01自动登录并浏览 Hexagone Web
  2. 02发现侧边栏页面和内部标签
  3. 03将页面截图保存到磁盘
  4. 04收集页面元数据
  5. 05根据 JSON 校验并生成 Markdown

适用场景

  1. 01记录 Hexagone Web 空间中的页面
  2. 02采集多个页面和标签的截图
  3. 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.json
scripts/
generate-md.js
package.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 ![caption](relative/path)
  ### 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”)

关键规则(从生产运行中总结)

  1. 始终使用 page.mouse.click() 进行任何 Vue.js 交互——切勿通过 page.evaluate() 使用 el.click()。Vue.js 需要原生的 mousedown/mouseup 事件,只有 page.mouse.click() 才会触发这些事件。
  2. 每次点击页面前都重新展开侧边栏——每次导航后侧边栏都会折叠。使用初次发现时的过期坐标会导致完全点不到侧边栏。
  3. 每次迭代都重新发现条目位置——侧边栏坐标会在重新渲染时发生变化。始终查询 DOM 获取最新坐标。
  4. 侧边栏条目使用 [class*="cursor:pointer"],而不是 a.hexa——侧边栏元素是 <div>,不是 <a> 标签。
  5. 从侧边栏标签中移除 <i class="hexa-icons"> 文本——图标文本会被添加到前面(例如,tdbTableau de bord → Tableau de bord)。
  6. 从页面列表中排除“按重要性排序”等实用按钮——它们是便签面板中的排序控件,不是页面。
  7. 使用范围更窄的标签页选择器([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

评论

支持 Markdown