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. 01Hexagone Web へのログインと画面遷移
  2. 02サイドバーのページと内部タブの検出
  3. 03ページのスクリーンショットを保存
  4. 04ページメタデータの収集
  5. 05JSON を検証して Markdown を生成

活用シーン

  1. 01Hexagone 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、パスワードがランダムな値です。ユーザーから別の認証情報が提供された場合は、そちらを使用してください。

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 ![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. Vue.js の操作には必ず page.mouse.click() を使用してください。page.evaluate() 経由の el.click() は絶対に使用しないでください。Vue.js ではネイティブの mousedown/mouseup イベントが必要ですが、それを発生させられるのは page.mouse.click() だけです。
  2. ページをクリックする前に、毎回サイドバーを再展開してください。ナビゲーションのたびにサイドバーは折りたたまれます。最初に検出した古い座標を使うと、サイドバーをまったくクリックできません。
  3. 反復のたびに項目の位置を再検出してください。再レンダリング時にサイドバーの座標は変わります。必ず DOM を照会して新しい座標を取得してください。
  4. サイドバー項目には a.hexa ではなく [class*="cursor:pointer"] を使用してください。サイドバーの要素は <a> タグではなく <div> 要素です。
  5. サイドバーのラベルから <i class="hexa-icons"> のテキストを削除してください。アイコンのテキストが先頭に付加されています(例:tdbTableau de bord → Tableau de bord)。
  6. 「Trier par Importance」などのユーティリティボタンはページ一覧から除外してください。これらはページではなく、Post-Its パネルの並べ替えコントロールです。
  7. 範囲を絞ったタブセレクター([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 を確認してください

コメント