hexagone-web-feature-extractor について
hexagone-web-feature-extractor は、Hexagone Web の機能スペースを調査し、ページやタブのスクリーンショット付きでプロダクトオーナー向けの Markdown 説明を作成するスキルです。Playwright でブラウザー操作を自動化し、画像を直接ディスクに保存します。
主な機能
- 01Hexagone Web へのログインと画面遷移
- 02サイドバーのページと内部タブの検出
- 03ページのスクリーンショットを保存
- 04ページメタデータの収集
- 05JSON を検証して Markdown を生成
活用シーン
- 01Hexagone 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、パスワードがランダムな値です。ユーザーから別の認証情報が提供された場合は、そちらを使用してください。
Vue.js は直接挿入された値の変更を検出しないため、ネイティブセッターパターンを使って page.evaluate() を実行します。これは必須です。
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 接続を確認する
URL に /login が含まれなくなるまで、最大 30 秒間、2 秒ごとにポーリングします。
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 イベントのみをディスパッチするため、領域のドロップダウンは開きません。これは開発中に見つかった最大のバグです。
領域セレクターは、オレンジ色のパンくずリストバーにある 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 領域を選択する
ドロップダウンはサイドバー領域内に、px:1 py:3/4 hover:bg:orange-dark cursor:pointer クラスを持つ <div> のリストとして表示されます。領域はアルファベット順に並んでいます。
// 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...」という中間ページを経由してリダイレクトします。URL に patient-portal(デフォルトのランディング領域)が含まれなくなるまで、最大 24 秒間、2 秒ごとにポーリングします。
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セレクターは、すべてのスペースで機能するわけではありません。サイドバーの要素は<a>タグではなく、cursor:pointerクラスを持つ<div>要素です。
4.3 除外する項目
ページではないUI要素を除外します:
Trier par Importance/Trier par Emetteur— Mes Post-Itsパネル内の並べ替えボタン- セクション見出し(
ACHATSのような短い大文字のテキスト)— クリック可能なページではなく、セクションの区切りです
4.4 検証ゲート
検出後に項目が1つも見つからない場合:探索を中止し、デバッグ用のスクリーンショットを撮影します。空のページリストのまま先に進んではいけません。分析用に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 メタデータを保存する
探索中に、ページ内容の分析(テーブル、フォーム、アクション、タブ)を反映したPO向けの説明を含むfeatures.json配列を作成します。
ステップ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」)
本番実行から学んだ重要なルール
- Vue.js の操作には必ず
page.mouse.click()を使用してください。page.evaluate()経由のel.click()は絶対に使用しないでください。Vue.js ではネイティブの mousedown/mouseup イベントが必要ですが、それを発生させられるのはpage.mouse.click()だけです。 - ページをクリックする前に、毎回サイドバーを再展開してください。ナビゲーションのたびにサイドバーは折りたたまれます。最初に検出した古い座標を使うと、サイドバーをまったくクリックできません。
- 反復のたびに項目の位置を再検出してください。再レンダリング時にサイドバーの座標は変わります。必ず DOM を照会して新しい座標を取得してください。
- サイドバー項目には
a.hexaではなく[class*="cursor:pointer"]を使用してください。サイドバーの要素は<a>タグではなく<div>要素です。 - サイドバーのラベルから
<i class="hexa-icons">のテキストを削除してください。アイコンのテキストが先頭に付加されています(例:tdbTableau de bord→Tableau de bord)。 - 「Trier par Importance」などのユーティリティボタンはページ一覧から除外してください。これらはページではなく、Post-Its パネルの並べ替えコントロールです。
- 範囲を絞ったタブセレクター(
[role="tab"]、.v-tab)を使用してください。[class*="tab"]は範囲が広すぎるため、ページネーション、フィルター、以前にレンダリングされたページの古い要素まで拾ってしまいます。
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| Space ドロップダウンが開かない | page.mouse.click() ではなく el.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 を確認してください |