hexagone-web-feature-extractor

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Hexagone Web Feature Extractor

Hexagone Web功能提取器

Explore a Hexagone Web functional space, capture screenshots of every page/tab, and produce a Markdown document (.md) oriented for Product Owners with functional descriptions and embedded screenshots.
探索Hexagone Web功能空间,截取每个页面/标签页的截图,生成面向产品负责人(PO)的Markdown文档(.md),包含功能描述和嵌入的截图。

Prerequisites

前置依赖

  • Node.js installed
  • Playwright npm package (
    npm install playwright
    ) — installs headless Chromium automatically
  • Network access to the Hexagone Web server (default:
    https://ws004202.dedalus.lan:8065/hexagone-01/vue/login
    )
  • Node.js 已安装
  • Playwright npm包(
    npm install playwright
    )—— 会自动安装无头Chromium
  • 可访问Hexagone Web服务器的网络(默认地址:
    https://ws004202.dedalus.lan:8065/hexagone-01/vue/login

Configuration

配置项

Default values calibrated for the standard Hexagone Web layout at 1920x1080. Adjust if the layout differs.
ParameterDefaultDescription
Viewport
1920x1080
Browser viewport size
Sidebar click X coordinate
38
Horizontal pixel position for sidebar icon clicks (collapsed mode)
Sidebar max left boundary
280
Max
rect.left
value to identify sidebar links (expanded mode)
Header height offset
55
Min
rect.top
value to exclude header elements
Login wait timeout
30s
Max time to poll for successful login
Page load wait timeout
10s
Max time to poll for page load after navigation
Screenshots directory
./screenshots
Where screenshots are saved (relative to working directory)
默认值适配1920x1080分辨率下的标准Hexagone Web布局,若布局不同可自行调整。
参数默认值描述
Viewport
1920x1080
浏览器视口大小
Sidebar click X coordinate
38
点击侧边栏图标的水平像素位置(收起模式下)
Sidebar max left boundary
280
用于识别侧边栏链接的最大
rect.left
值(展开模式下)
Header height offset
55
用于排除头部元素的最小
rect.top
Login wait timeout
30s
轮询等待登录成功的最大时长
Page load wait timeout
10s
导航后轮询等待页面加载的最大时长
Screenshots directory
./screenshots
截图保存的位置(相对于工作目录)

Workflow Overview

工作流概览

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
Key advantage over Chrome extension approach: Screenshots save directly to disk via
page.screenshot()
— no bridge server or transfer step needed.

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()
直接将截图保存到磁盘——无需桥接服务器或传输步骤。

Step 1: Setup

步骤1:环境搭建

1.1 Install Playwright

1.1 安装Playwright

bash
npm install playwright
npx playwright install chromium
bash
npm install playwright
npx playwright install chromium

1.2 Launch Browser

1.2 启动浏览器

javascript
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();
Why headless Chromium? Eliminates the need for manual SSL certificate acceptance, Chrome extension setup, and screenshot bridge transfers. The
ignoreHTTPSErrors: true
option handles self-signed certificates programmatically.

javascript
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
选项可自动处理自签名证书。

Step 2: Connection to Hexagone Web

步骤2:连接Hexagone Web

2.1 Navigate to Login Page

2.1 跳转到登录页

javascript
await page.goto(LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await sleep(3000); // Wait for Vue.js to mount
javascript
await page.goto(LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await sleep(3000); // Wait for Vue.js to mount

2.2 Fill the Login Form

2.2 填写登录表单

The Hexagone Web login form has 3 fields: Username, Password, Manager code. Default credentials: username
apvhn
with a random password, unless the user provides others.
Use
page.evaluate()
with the native setter pattern — required for Vue.js which does not detect value changes injected directly:
javascript
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 });
Hexagone Web登录表单有3个字段:用户名、密码、管理员代码。默认凭证:用户名
apvhn
,随机密码,用户也可自行提供其他凭证。
使用
page.evaluate()
搭配原生setter模式——Vue.js必须使用该方式,因为Vue.js无法检测直接注入的值变更:
javascript
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 Verify Connection

2.3 验证连接状态

Poll every 2s for up to 30s until the URL no longer contains
/login
:
javascript
for (let i = 0; i < 15; i++) {
  await sleep(2000);
  if (!page.url().includes('/login')) break;
}
If login fails: Take a debug screenshot with
page.screenshot()
and report the failure.

每2秒轮询一次,最长等待30秒,直到URL不再包含
/login
javascript
for (let i = 0; i < 15; i++) {
  await sleep(2000);
  if (!page.url().includes('/login')) break;
}
如果登录失败:调用
page.screenshot()
截取调试用截图并上报失败。

Step 3: Navigation to the Target Space

步骤3:导航到目标空间

3.1 Open the Space Selector

3.1 打开空间选择器

CRITICAL: Use
page.mouse.click()
— NOT
el.click()
via
page.evaluate()
.
Vue.js event handlers require native mouse events (mousedown + mouseup + click). JavaScript's
el.click()
only dispatches the
click
event and will not trigger the space dropdown. This was the #1 bug found during development.
The space selector is the
div
with class
bg:orange-dark
in the orange breadcrumb bar. It contains an icon
<i class="hexa-icons">changer_espaces</i>
followed by a
<span>
with the current space name.
javascript
// 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);
关键注意事项:使用
page.mouse.click()
——不要通过
page.evaluate()
调用
el.click()
Vue.js事件处理器需要原生鼠标事件(mousedown + mouseup + click)。JavaScript的
el.click()
只会派发
click
事件,无法触发空间下拉菜单,这是开发过程中发现的头号Bug。
空间选择器是橙色面包屑栏中类名为
bg:orange-dark
div
元素,内部包含
<i class="hexa-icons">changer_espaces</i>
图标,后面跟随显示当前空间名称的
<span>
元素。
javascript
// 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 Select the Space

3.2 选择目标空间

The dropdown renders inside the sidebar area as a list of
<div>
elements with class
px:1 py:3/4 hover:bg:orange-dark cursor:pointer
. Spaces are listed alphabetically.
javascript
// 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);
下拉菜单渲染在侧边栏区域,是类名为
px:1 py:3/4 hover:bg:orange-dark cursor:pointer
<div>
元素列表,空间按字母顺序排列。
javascript
// 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 Wait for Loading

3.3 等待加载完成

Hexagone Web redirects via an intermediate "Connexion... Redirection..." page. Poll every 2s for up to 24s until the URL no longer contains
patient-portal
(the default landing space):
javascript
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

Hexagone Web会通过中间的"Connexion... Redirection..."页面跳转。每2秒轮询一次,最长等待24秒,直到URL不再包含
patient-portal
(默认落地空间):
javascript
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

Step 4: Page Discovery

步骤4:页面发现

4.1 Expand the Sidebar

4.1 展开侧边栏

The sidebar is collapsed by default (icons only, width ~65px). Click the hamburger menu to expand it and reveal text labels:
javascript
await page.mouse.click(34, 50); // Hamburger icon position
await sleep(2000);
侧边栏默认是收起状态(仅显示图标,宽度约65px)。点击汉堡菜单展开侧边栏,显示文本标签:
javascript
await page.mouse.click(34, 50); // Hamburger icon position
await sleep(2000);

4.2 Identify Sidebar Menu Entries

4.2 识别侧边栏菜单条目

Primary method: Look for elements with
cursor:pointer
class in the left 280px. Strip icon text from
<i class="hexa-icons">
children:
javascript
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']);
Sidebar DOM structure (observed):
html
<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>
Note: The old
a.hexa
selector does NOT work for all spaces. The sidebar elements are
<div>
elements with
cursor:pointer
class, not
<a>
tags.
主要方法:查找左侧280px范围内类名包含
cursor:pointer
的元素,剔除
<i class="hexa-icons">
子元素的图标文本:
javascript
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结构(实际观测):
html
<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 Items to Exclude

4.3 需要排除的条目

Filter out UI elements that are not pages:
  • Trier par Importance
    /
    Trier par Emetteur
    — sort buttons in the Mes Post-Its panel
  • Section headers (all-caps short text like
    ACHATS
    ) — section dividers, not clickable pages
过滤掉不属于页面的UI元素:
  • Trier par Importance
    /
    Trier par Emetteur
    —— 我的便签面板中的排序按钮
  • 章节头部(全大写短文本如
    ACHATS
    )—— 章节分隔符,不是可点击的页面

4.4 Validation Gate

4.4 校验关卡

If zero items are found after discovery: Stop the exploration and take a debug screenshot. Do NOT proceed with an empty page list. Save the DOM to a debug file for analysis.
如果发现后页面条目数量为0:停止探索并截取调试截图,不要使用空页面列表继续执行。保存DOM到调试文件用于分析。

4.5 Collapse Sidebar

4.5 收起侧边栏

After discovery, collapse the sidebar before starting exploration:
javascript
await page.mouse.click(34, 50); // Toggle hamburger
await sleep(1000);

页面发现完成后,在开始探索前收起侧边栏:
javascript
await page.mouse.click(34, 50); // Toggle hamburger
await sleep(1000);

Step 5: Exploration and Capture

步骤5:探索与截图

5.1 For Each Page in the Menu

5.1 遍历菜单中的每个页面

CRITICAL: The sidebar collapses after clicking a menu item. You MUST re-expand the sidebar and re-discover the item's position before each click.
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
Why re-discover each time? The sidebar re-renders its content when expanded. Item coordinates shift depending on scroll position and which items are visible. Using stale coordinates from the initial discovery will click on the main content area instead of the sidebar.
关键注意事项:点击菜单项后边栏会自动收起。每次点击前必须重新展开侧边栏,重新获取条目的位置。
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 Identify Internal Tabs

5.2 识别内部标签页

Some pages have internal tabs (e.g., Fournisseurs → Saisie / Consultation / Réalisé, Marchés → SAISIE / CONSULTATION / DÉBLOCAGE). Use
page.mouse.click()
for tabs too:
javascript
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 });
}
Warning: The selector
[class*="tab"]
is too broad — it matches pagination controls, filter dropdowns, and elements from previously rendered pages. Prefer
[role="tab"]
or
.v-tab
for tab detection.
部分页面有内部标签(例如:供应商 → 录入 / 查询 / 已完成,采购 → 录入 / 查询 / 解锁)。标签页也需要使用
page.mouse.click()
操作:
javascript
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 Take Screenshots

5.3 截取截图

Screenshots save directly to disk — no bridge or transfer needed:
javascript
await page.screenshot({
  path: path.join(SCREENSHOT_DIR, `${index}-${sanitize(pageName)}.png`),
  fullPage: false  // Capture viewport only (1920x1080)
});
截图直接保存到磁盘——无需桥接或传输步骤:
javascript
await page.screenshot({
  path: path.join(SCREENSHOT_DIR, `${index}-${sanitize(pageName)}.png`),
  fullPage: false  // Capture viewport only (1920x1080)
});

5.4 Store Metadata

5.4 存储元数据

Build a
features.json
array during exploration with PO-oriented descriptions enriched from page content analysis (tables, forms, actions, tabs).

探索过程中构建
features.json
数组,包含面向PO的功能描述,通过页面内容分析(表格、表单、操作、标签页)补充信息。

Step 6: Markdown Document Generation

步骤6:Markdown文档生成

6.1 Prepare the Input

6.1 准备输入数据

Create a
features.json
file from the metadata collected in Step 5. The file must conform to this structure:
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" }
      ]
    }
  ]
}
Validation rules:
  • space
    must be a non-empty string
  • features
    must be a non-empty array
  • Each feature must have
    title
    (string) and
    description
    (string)
  • capabilities
    must be an array of strings (can be empty)
  • screenshots
    must be an array of objects with
    file
    (string) and
    caption
    (string)
根据步骤5收集的元数据创建
features.json
文件,文件必须符合以下结构:
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
    必须为非空数组
  • 每个功能必须包含
    title
    (字符串)和
    description
    (字符串)
  • capabilities
    必须为字符串数组(可为空)
  • screenshots
    必须为对象数组,每个对象包含
    file
    (字符串)和
    caption
    (字符串)

6.2 Generate the Document

6.2 生成文档

Use the script
scripts/generate-md.js
:
bash
node scripts/generate-md.js --input features.json --output /path/to/output.md --screenshots /path/to/screenshots
The
--input
argument is required. The script validates the input and fails with clear error messages if the JSON is malformed. No
npm install
needed — the script uses only Node.js built-in modules.
使用脚本
scripts/generate-md.js
bash
node scripts/generate-md.js --input features.json --output /path/to/output.md --screenshots /path/to/screenshots
--input
参数是必填项。脚本会校验输入内容,如果JSON格式错误会返回清晰的错误信息。无需安装额外
npm
依赖——脚本仅使用Node.js内置模块。

6.3 Document Structure

6.3 文档结构

undefined
undefined

Title (space name)

Title (space name)

Subtitle with date
Subtitle with date

Table of contents (linked)

Table of contents (linked)

For each feature:

N. Feature title

  • Functional description
  • Screenshot(s) as caption

Key capabilities (numbered list)

Business value


undefined
For each feature:

N. Feature title

  • Functional description
  • Screenshot(s) as caption

Key capabilities (numbered list)

Business value


undefined

6.4 Validation and Output

6.4 校验与输出

Verify the generated file exists and contains all expected features:
bash
grep -c '^## [0-9]' output.md  # Should match the number of features

验证生成的文件存在且包含所有预期功能:
bash
grep -c '^## [0-9]' output.md  # Should match the number of features

Input Parameters

输入参数

The user must provide:
  • Login URL (optional): defaults to
    https://ws004202.dedalus.lan:8065/hexagone-01/vue/login
    . The user can provide a different URL if needed.
  • Username (optional): defaults to
    apvhn
    . The user can provide a different code if needed.
  • Password (optional): defaults to a random value. The user can provide a specific password if needed.
  • Target space: Exact name of the space to explore (e.g., "HA GHT", "STRUCTURES / NOMENCLATURES")
用户需要提供:
  • 登录URL (可选):默认值为
    https://ws004202.dedalus.lan:8065/hexagone-01/vue/login
    ,用户可按需提供其他地址
  • 用户名 (可选):默认值为
    apvhn
    ,用户可按需提供其他代码
  • 密码 (可选):默认值为随机值,用户可按需提供特定密码
  • 目标空间:要探索的空间的准确名称(例如:"HA GHT"、"STRUCTURES / NOMENCLATURES")

Critical Rules (learned from production runs)

关键规则(来自生产运行经验总结)

  1. Always use
    page.mouse.click()
    for any Vue.js interaction — NEVER use
    el.click()
    via
    page.evaluate()
    . Vue.js requires native mousedown/mouseup events that only
    page.mouse.click()
    provides.
  2. Re-expand sidebar before every page click — the sidebar collapses after each navigation. Using stale coordinates from initial discovery will miss the sidebar entirely.
  3. Re-discover item positions each iteration — sidebar coordinates shift on re-render. Always query the DOM for fresh coordinates.
  4. Use
    [class*="cursor:pointer"]
    not
    a.hexa
    for sidebar items — sidebar elements are
    <div>
    elements, not
    <a>
    tags.
  5. Strip
    <i class="hexa-icons">
    text
    from sidebar labels — icon text is prepended (e.g.,
    tdbTableau de bord
    Tableau de bord
    ).
  6. Exclude utility buttons like "Trier par Importance" from the page list — they are sort controls in the Post-Its panel, not pages.
  7. Use narrow tab selectors (
    [role="tab"]
    ,
    .v-tab
    ) —
    [class*="tab"]
    is too broad and picks up pagination, filters, and stale elements from previously rendered pages.
  1. 所有Vue.js交互必须使用
    page.mouse.click()
    —— 永远不要通过
    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. 排除实用按钮 如页面列表中的"Trier par Importance" —— 它们是便签面板中的排序控件,不是页面。
  7. 使用范围更窄的标签页选择器
    [role="tab"]
    .v-tab
    )——
    [class*="tab"]
    范围过宽,会匹配分页、筛选器以及之前渲染页面的残留元素。

Troubleshooting

故障排查

ProblemCauseSolution
Space dropdown does not openUsed
el.click()
instead of
page.mouse.click()
Always use
page.mouse.click()
for Vue.js interactions
All page screenshots are identicalSidebar collapsed, clicks miss sidebar itemsRe-expand sidebar + re-discover coordinates before each click
Sidebar items not foundUsed
a.hexa
selector
Use
[class*="cursor:pointer"]
with icon text stripping
Too many "tabs" detectedBroad selector
[class*="tab"]
Use
[role="tab"]
or
.v-tab
only
"Trier par Importance" in page listSort button mistaken for pageAdd to exclude list:
['Trier par Importance', 'Trier par Emetteur']
Fields not detected by Vue.jsDirect value injection without eventsUse
nativeInputValueSetter
+
dispatchEvent('input')
Login button click selects wrong buttonMultiple buttons on pageUse text-content matching:
buttons.find(b => /connect/i.test(b.textContent))
Page content not loaded after clickSlow server or heavy pageUse
page.waitForLoadState('networkidle')
+ extra sleep
SSL certificate errorSelf-signed cert on Hexagone Web server
ignoreHTTPSErrors: true
in browser context (handled automatically)
generate-md.js
fails with validation error
Malformed features.jsonCheck required fields:
space
,
features[].title
,
features[].description
问题原因解决方案
空间下拉菜单无法打开使用了
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 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