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)
默认值针对标准Hexagone Web布局(1920x1080)校准。如果布局不同,请进行调整。
参数默认值描述
视口尺寸
1920x1080
浏览器视口大小
侧边栏点击X坐标
38
侧边栏图标点击的水平像素位置(折叠模式)
侧边栏最大左边界
280
识别侧边栏链接的最大
rect.left
值(展开模式)
头部高度偏移
55
排除头部元素的最小
rect.top
登录等待超时
30s
轮询登录成功的最长时间
页面加载等待超时
10s
导航后轮询页面加载完成的最长时间
截图保存目录
./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. 环境搭建 → 安装Playwright,启动无头Chromium
2. 连接系统 → 登录Hexagone Web
3. 导航定位 → 跳转至目标空间
4. 页面发现 → 展开侧边栏,列出所有菜单页面
5. 探索捕获 → 访问每个页面,捕获截图和元数据
6. 文档生成 → 生成嵌入截图的Markdown文档
相较于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  // 自动处理自签名证书
});
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); // 等待Vue.js挂载完成

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()
结合原生属性设置模式——这是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
事件,无法触发空间下拉菜单。这是开发过程中发现的头号问题。
空间选择器是橙色面包屑栏中带有
bg:orange-dark
类的
div
元素,包含图标
<i class="hexa-icons">changer_espaces</i>
和显示当前空间名称的
<span>
javascript
// 获取空间选择器坐标
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;
});

// 使用原生鼠标事件点击(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
// 查找目标空间元素
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);

// 使用鼠标点击(不要用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); // 额外等待Vue.js渲染完成

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); // 汉堡图标位置
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();
      // 移除图标前缀文本
      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;
      // 排除全大写的短文本(如"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--section">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
    —— Mes Post-Its面板中的排序按钮
  • 全大写短文本(如
    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.
如果发现后没有找到任何项:停止探索并截取调试截图,不要继续处理空页面列表,将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); // 切换汉堡菜单
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.
关键注意事项:点击菜单项后侧边栏会折叠,因此每次点击前必须重新展开侧边栏并重新获取项的位置。
遍历每个菜单项:
  1. 展开侧边栏:page.mouse.click(34, 50),等待1.5秒
  2. 通过page.evaluate()重新获取该项的位置(标签匹配)
  3. 点击该项:page.mouse.click(freshPos.x, freshPos.y)
  4. 等待页面加载(networkidle + 额外1.5秒等待)
  5. 截取截图:page.screenshot({ path: ... })
  6. 通过page.evaluate()提取元数据
  7. 如果发现标签页,探索它们(见5.2)
  8. 构建面向PO的功能描述
为何每次都要重新获取位置? 侧边栏在重新展开时会重新渲染内容,项的坐标会根据滚动位置和可见项发生变化。使用初始发现的旧坐标会点击主内容区域而非侧边栏。

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.
部分页面包含内部标签页(例如:Fournisseurs → Saisie / Consultation / Réalisé,Marchés → SAISIE / CONSULTATION / DÉBLOCAGE)。标签页也需使用
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  // 仅捕获视口内容(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": "探索的空间名称",
  "features": [
    {
      "title": "功能名称(字符串,必填)",
      "description": "面向PO的功能描述(字符串,必填)",
      "capabilities": ["功能1", "功能2"],
      "businessValue": "业务价值描述(字符串)",
      "screenshots": [
        { "file": "filename.png", "caption": "截图描述" }
      ]
    }
  ]
}
验证规则
  • 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 install
——脚本仅使用Node.js内置模块。

6.3 Document Structure

6.3 文档结构

undefined
undefined

Title (space name)

标题(空间名称)

Subtitle with date
带日期的副标题

Table of contents (linked)

目录(带链接)

For each feature:

N. Feature title

  • Functional description
  • Screenshot(s) as caption

Key capabilities (numbered list)

Business value


undefined
每个功能对应:

N. 功能标题

  • 功能描述
  • 截图:描述

核心功能(编号列表)

业务价值


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  # 结果应与功能数量一致

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
    ,用户可根据需要提供其他URL。
  • 用户名(可选):默认值为
    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. **始终使用
    page.mouse.click()
    **处理所有Vue.js交互——绝不要通过
    page.evaluate()
    使用
    el.click()
    。Vue.js需要
    page.mouse.click()
    提供的原生mousedown/mouseup事件。
  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