open ai APIでフォームに自動入力するChrome拡張を簡単に作る

# はじめに
結論から言うと完璧を目指すならbrowser-useを使えって感じですが、
料金を抑えつつ、ある程度カジュアルに作るならこれでいい気がします。

# 前提
- Chrome拡張の作り方の説明はしない
- 拡張機能のインストール方法の説明はしない

# 各ファイル

全てディレクトリ直下に格納してます。

## manifest.json
```manifest.json
{
  "manifest_version": 2,
  "name": "AIフォーム自動入力",
  "version": "1.0",
  "description": "AI支援によるフォーム自動入力機能を持つChrome拡張機能",
  "browser_action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "images/icon16.png", // iconは適当に準備してください。
      "48": "images/icon48.png",
      "128": "images/icon128.png"
    }
  },
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "permissions": [
    "storage", 
    "tabs", 
    "activeTab",
    "<all_urls>",
    "https://api.openai.com/*"
  ],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content-script.js"],
      "run_at": "document_idle"
    }
  ],
  "web_accessible_resources": [
    "content-script.js"
  ]
} 
```

## popup.html
ポップアップの見た目部分です。

```popup.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>AIフォーム自動入力</title>
  <style>
    body {
      width: 350px;
      padding: 10px;
      font-family: Arial, sans-serif;
    }
    .form-group {
      margin-bottom: 10px;
    }
    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }
    input, textarea, select {
      width: 100%;
      padding: 6px;
      box-sizing: border-box;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    textarea {
      min-height: 80px;
      resize: vertical;
    }
    small {
      color: #666;
      display: block;
      margin-top: 3px;
      font-size: 0.8em;
    }
    button {
      background-color: #4285F4;
      color: white;
      border: none;
      padding: 8px 12px;
      border-radius: 4px;
      cursor: pointer;
      margin-top: 10px;
      margin-right: 5px;
    }
    button:hover {
      background-color: #3367D6;
    }
    .saved-message {
      color: green;
      margin-top: 10px;
      display: none;
    }
    h2 {
      margin-top: 0;
      color: #333;
      border-bottom: 1px solid #eee;
      padding-bottom: 10px;
    }
    .container {
      padding: 6px 12px;
    }
    .checkbox-group {
      display: flex;
      align-items: center;
      margin-top: 5px;
    }
    .checkbox-group input[type="checkbox"] {
      width: auto;
      margin-right: 8px;
    }
    .checkbox-group label {
      display: inline;
      font-weight: normal;
    }
  </style>
</head>
<body>
  <h2>AIフォーム自動入力</h2>
  
  <div class="container">
    <div class="form-group">
      <label for="apiKey">OpenAI APIキー:</label>
      <input type="password" id="apiKey" placeholder="sk-..." autocomplete="off">
      <small>APIキーは安全に保存され、フォーム自動入力のために使用されます</small>
    </div>
    
    <div class="form-group">
      <label for="model">AIモデル:</label>
      <select id="model">
        <option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
        <option value="gpt-4">GPT-4</option>
      </select>
    </div>
    
    <div class="form-group">
      <label for="formContext">フォームコンテキスト情報:</label>
      <textarea id="formContext" placeholder="フォームの目的や必要な入力情報について説明してください。例: 「これは求人応募フォームで、エンジニアのポジションに応募したいです」"></textarea>
      <small>AIがフォームをより適切に理解し、最適な値を入力するための補助情報を入力します</small>
    </div>
    
    <button id="saveSettingsButton">設定を保存</button>
    <button id="autoFillButton">フォームを自動入力</button>
    <p id="settingsSavedMessage" class="saved-message">設定を保存しました！</p>
    <p id="autoFillMessage" class="saved-message">自動入力を開始しました！</p>
  </div>

  <script src="popup.js"></script>
</body>
</html> 
```

## popup.js
入力やボタン押下時の制御周りの処理です。

```popup.js
document.addEventListener('DOMContentLoaded', function() {
  // DOM要素の取得
  const apiKeyInput = document.getElementById('apiKey');
  const modelSelect = document.getElementById('model');
  const formContextInput = document.getElementById('formContext');
  const saveSettingsButton = document.getElementById('saveSettingsButton');
  const settingsSavedMessage = document.getElementById('settingsSavedMessage');
  const autoFillButton = document.getElementById('autoFillButton');
  const autoFillMessage = document.getElementById('autoFillMessage');
  
  // 保存されたAPIキー設定を読み込む
  chrome.storage.sync.get(['apiSettings'], function(result) {
    if (result.apiSettings) {
      apiKeyInput.value = result.apiSettings.apiKey || '';
      modelSelect.value = result.apiSettings.model || 'gpt-3.5-turbo';
      if (formContextInput) {
        formContextInput.value = result.apiSettings.formContext || '';
      }
    }
  });
  
  // APIキー設定保存ボタンのクリックイベント
  saveSettingsButton.addEventListener('click', function() {
    // APIキー設定を取得
    const apiSettings = {
      apiKey: apiKeyInput.value,
      model: modelSelect.value,
      formContext: formContextInput ? formContextInput.value : ''
    };
    
    // データを保存
    chrome.storage.sync.set({apiSettings: apiSettings}, function() {
      // 保存完了メッセージを表示
      settingsSavedMessage.style.display = 'block';
      
      // 3秒後にメッセージを非表示にする
      setTimeout(function() {
        settingsSavedMessage.style.display = 'none';
      }, 3000);
    });
  });
  
  // フォーム自動入力ボタンのクリックイベント
  autoFillButton.addEventListener('click', function() {
    console.log('自動入力ボタンがクリックされました');
    // APIキー設定を取得
    chrome.storage.sync.get(['apiSettings'], function(result) {
      console.log('APIキー設定を取得しました:', result.apiSettings ? 'あり' : 'なし');
      if (!result.apiSettings || !result.apiSettings.apiKey) {
        alert('OpenAI APIキーを設定してください');
        return;
      }
      
      // 現在のタブにメッセージを送信
      chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
        if (!tabs || tabs.length === 0) {
          console.error('アクティブなタブが見つかりません');
          autoFillMessage.textContent = 'エラー: アクティブなタブが見つかりません';
          autoFillMessage.style.color = 'red';
          autoFillMessage.style.display = 'block';
          return;
        }
        
        const activeTab = tabs[0];
        console.log('アクティブなタブ:', activeTab.id);
        
        // 代替アプローチ: 直接フォーム自動入力を実行する
        autoFillMessage.textContent = 'フォーム分析中...';
        autoFillMessage.style.color = 'blue';
        autoFillMessage.style.display = 'block';
        
        // まずcontent-scriptを強制的に挿入
        chrome.tabs.executeScript(
          activeTab.id,
          { file: 'content-script.js' },
          function(injectionResults) {
            if (chrome.runtime.lastError) {
              console.error('スクリプト注入エラー:', chrome.runtime.lastError);
              autoFillMessage.textContent = `エラー: ${chrome.runtime.lastError.message}`;
              autoFillMessage.style.color = 'red';
              autoFillMessage.style.display = 'block';
              return;
            }
            
            console.log('content-scriptを注入または再読み込みしました');
            
            // 少し待ってからメッセージを送信してみる
            setTimeout(function() {
              try {
                chrome.tabs.sendMessage(
                  activeTab.id,
                  {
                    action: 'autoFillForms',
                    apiKey: result.apiSettings.apiKey,
                    model: result.apiSettings.model || 'gpt-3.5-turbo',
                    formContext: result.apiSettings.formContext || ''
                  },
                  function(response) {
                    console.log('content-scriptからのレスポンス:', response);
                    
                    if (chrome.runtime.lastError) {
                      console.warn('メッセージ送信エラー (フォールバック実行を試みます):', chrome.runtime.lastError);
                      
                      // フォールバック: 直接executeScriptで実行
                      executeDirectFill(activeTab.id, result.apiSettings, autoFillMessage);
                      return;
                    }
                    
                    if (response && response.success) {
                      // 成功メッセージを表示
                      autoFillMessage.textContent = response.message;
                      autoFillMessage.style.color = 'green';
                      autoFillMessage.style.display = 'block';
                      
                      // 3秒後にメッセージを非表示にする
                      setTimeout(function() {
                        autoFillMessage.style.display = 'none';
                      }, 3000);
                    } else {
                      // エラーメッセージを表示
                      autoFillMessage.textContent = response ? response.message : 'フォーム自動入力中にエラーが発生しました';
                      autoFillMessage.style.color = 'red';
                      autoFillMessage.style.display = 'block';
                    }
                  }
                );
              } catch (error) {
                console.error('メッセージ送信エラー:', error);
                
                // エラーの場合も直接実行を試みる
                executeDirectFill(activeTab.id, result.apiSettings, autoFillMessage);
              }
            }, 500);
          }
        );
      });
    });
  });
});

// 直接実行関数
function executeDirectFill(tabId, apiSettings, messageElement) {
  console.log('直接実行を試みます', apiSettings);
  
  // フォームコンテキストをエスケープして安全に扱う
  const safeFormContext = apiSettings.formContext ? 
    apiSettings.formContext.replace(/'/g, "\\'").replace(/\n/g, "\\n") : '';
  
  chrome.tabs.executeScript(
    tabId,
    {
      code: `
        // content-scriptの関数を直接呼び出す
        if (typeof window.directExecute === 'function') {
          console.log('directExecuteを実行します');
          window.directExecute('autoFillForms', '${apiSettings.apiKey}', '${apiSettings.model || 'gpt-3.5-turbo'}', { 
            formContext: '${safeFormContext}'
          });
        } else if (typeof window.detectForms === 'function') {
          console.log('content-scriptの関数は存在しますが、directExecuteがありません');
          { success: false, message: 'バージョンの不一致: 拡張機能を再読み込みしてください' };
        } else {
          console.error('content-scriptの関数が見つかりません');
          { success: false, message: 'content-scriptが正しく読み込まれていません' };
        }
      `
    },
    function(results) {
      console.log('直接実行結果:', results);
      
      if (chrome.runtime.lastError) {
        console.error('直接実行エラー:', chrome.runtime.lastError);
        messageElement.textContent = `エラー: ${chrome.runtime.lastError.message}`;
        messageElement.style.color = 'red';
        messageElement.style.display = 'block';
        return;
      }
      
      // 直接実行では正確な結果が取得できないため、処理中メッセージを表示
      messageElement.textContent = 'フォーム処理中...完了まで少々お待ちください';
      messageElement.style.color = 'blue';
      
      // 5秒後に完了メッセージを表示（非同期処理なので厳密な完了確認はできない）
      setTimeout(function() {
        messageElement.textContent = 'フォーム処理が完了しました（自動推測）';
        messageElement.style.color = 'green';
        
        // さらに3秒後に非表示
        setTimeout(function() {
          messageElement.style.display = 'none';
        }, 3000);
      }, 5000);
    }
  );
} 
```

## background.js
スクリプトの読み込みなどの準備
```background.js
// Chrome拡張機能のインストール/更新時に実行
chrome.runtime.onInstalled.addListener(function() {
  console.log('AI フォーム自動入力拡張機能がインストールされました');
});

// エラーを検出するリスナー
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
  if (message.type === 'error') {
    console.error('エラーが報告されました:', message.error);
  }
  
  if (message.type === 'debug') {
    console.log('デバッグメッセージ:', message.data);
  }
  
  // sendResponseが使われる可能性があるので、trueを返す
  return true;
});

// コンテンツスクリプトとの接続確認
chrome.runtime.onConnect.addListener(function(port) {
  console.log('新しい接続:', port.name);
  
  port.onMessage.addListener(function(message) {
    console.log('ポートメッセージ:', message);
  });
  
  port.onDisconnect.addListener(function() {
    console.log('ポート接続が終了しました:', port.name);
  });
});

// コンテンツスクリプトが読み込まれたときのメッセージリスナー
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
  if (message.action === 'contentScriptLoaded') {
    console.log('Content Scriptが読み込まれました:', sender.tab ? sender.tab.url : 'unknown');
    // 応答を返す
    sendResponse({status: 'confirmed'});
  }
  
  // 非同期レスポンスのためにtrueを返す
  return true;
}); 
```

## content-script.js
アクティブなタブ上にあるフォームを検出して自動入力するコア機能
```content-script.js
// フォーム要素を検出する関数
function detectForms() {
  // まず標準的な<form>要素を探す
  const forms = document.querySelectorAll('form');
  if (forms.length > 0) {
    console.log('フォームが検出されました:', forms.length, '件');
    return forms;
  }
  
  // フォームが見つからない場合、入力フィールドの集まりを探す
  // 入力フィールドを集めて仮想的なフォームを作成
  console.log('標準フォームが見つからないため、入力フィールドを探します');
  
  const inputElements = document.querySelectorAll('input, textarea, select');
  if (inputElements.length > 0) {
    console.log('入力フィールドが見つかりました:', inputElements.length, '件');
    
    // 仮想フォーム（div要素）を作成
    const virtualForm = document.createElement('div');
    virtualForm.setAttribute('data-virtual-form', 'true');
    
    // 入力要素の参照を保持（実際にDOM操作はしない）
    virtualForm.inputElements = inputElements;
    
    // 仮想フォーム用のquerySelectorAll関数をオーバーライド
    virtualForm.querySelectorAll = function(selector) {
      if (selector === 'input, textarea, select') {
        return this.inputElements;
      } 
      return [];
    };
    
    return [virtualForm];
  }
  
  console.log('フォームも入力フィールドも見つかりませんでした');
  return [];
}

// OpenAIのAPIを使ってフォームフィールドを解析する関数
async function analyzeFormWithAI(formElement, apiKey, model, formContext = '') {
  // フォーム内の入力要素を取得
  const inputElements = formElement.querySelectorAll('input, textarea, select');
  if (!inputElements || inputElements.length === 0) {
    throw new Error('フォーム内に入力要素が見つかりません');
  }
  
  console.log('入力要素数:', inputElements.length);
  
  const formFields = Array.from(inputElements).map(element => {
    // 基本的なラベル検出
    let label = findLabelForElement(element);
    
    return {
      id: element.id,
      name: element.name,
      type: element.type,
      placeholder: element.placeholder,
      label: label,
      required: element.required
    };
  });

  console.log('解析するフォームフィールド:', formFields);
  
  // 入力可能な要素のみをフィルタリング
  const editableFields = formFields.filter(field => {
    const type = field.type.toLowerCase();
    return type !== 'submit' && type !== 'button' && type !== 'reset' && 
           type !== 'hidden' && type !== 'file' && type !== 'image';
  });
  
  if (editableFields.length === 0) {
    throw new Error('フォーム内に入力可能な要素が見つかりません');
  }

  // OpenAI APIへのリクエスト
  try {
    console.log(`OpenAI API (${model})にリクエストを送信中...`);
    
    const systemMessage = `
あなたはウェブフォームの自動入力を支援するAIアシスタントです。
以下のフォームフィールド情報に基づいて、各フィールドに適切な値を入力してください。
必ず、以下の形式でJSON形式で回答してください:

{
  "フィールドID1": "入力値1",
  "フィールドID2": "入力値2",
  ...
}

注意:
- フィールドIDがない場合は、nameまたはlabelを使用してください
- 実際の値のみを含め、説明やコメントは含めないでください
- 妥当な日本語の値を使用してください（例: 名前なら「山田太郎」など）
- メールアドレスには有効な形式を使用してください（例: user@example.com）
- パスワードには複雑なパスワードを使用してください
- checkboxやradioボタンには "true"/"false" または "1"/"0" で応答してください
`;
    
    let userMessage = `
このウェブフォームに適切な値を入力してください。以下はフォームフィールドの詳細情報です:
${JSON.stringify(editableFields, null, 2)}

各フィールドに適切な値を割り当てたJSONオブジェクトで回答してください。
フィールドIDまたは名前をキーとして使用してください。
`;

    // ユーザーが提供したコンテキスト情報があれば追加
    if (formContext && formContext.trim() !== '') {
      userMessage += `

フォームに関する追加コンテキスト情報:
${formContext}
この情報に基づいて、フォームフィールドに適切な値を入力してください。
`;
    }

    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`
      },
      body: JSON.stringify({
        model: model,
        messages: [
          {
            role: 'system',
            content: systemMessage
          },
          {
            role: 'user',
            content: userMessage
          }
        ],
        temperature: 0.2,  // より決定論的な結果を得るために低い値を設定
        ...(model.includes('gpt-4') ? {} : { response_format: { type: "json_object" } })  // JSON形式の応答を要求
      })
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => null);
      console.error('API応答エラー:', response.status, errorData);
      throw new Error(`APIエラー: ${response.status} - ${errorData?.error?.message || '不明なエラー'}`);
    }

    const data = await response.json();
    console.log('AIレスポンス:', data);
    
    if (data.choices && data.choices.length > 0) {
      const contentResult = parseAIResponse(data.choices[0].message.content, editableFields);
      if (Object.keys(contentResult).length === 0) {
        console.warn('AIレスポンスのパースに失敗しました。レスポンス:', data.choices[0].message.content);
        return null;
      }
      return contentResult;
    } else {
      throw new Error('AIからの有効なレスポンスがありません');
    }
  } catch (error) {
    console.error('AI APIエラー:', error);
    throw error; // 上位の関数でキャッチできるようにエラーを再スロー
  }
}

// AIのレスポンスをパースする関数
function parseAIResponse(responseText, formFields) {
  console.log('AIレスポンスのパース開始:', responseText.substring(0, 100) + '...');
  
  // この関数はAIのレスポンスをパースして、各フォームフィールドの値を返す
  const result = {};
  
  try {
    // ステップ1: JSONとして解析を試みる
    let parsedJson = null;
    
    if (responseText.includes('{') && responseText.includes('}')) {
      try {
        // JSON部分を抽出する試み
        const jsonRegex = /{[\s\S]*?}/g;
        const jsonMatches = responseText.match(jsonRegex);
        
        if (jsonMatches && jsonMatches.length > 0) {
          // 最も長いJSONを試す
          const sortedMatches = [...jsonMatches].sort((a, b) => b.length - a.length);
          
          for (const jsonString of sortedMatches) {
            try {
              parsedJson = JSON.parse(jsonString);
              console.log('JSONとして解析成功:', parsedJson);
              break;
            } catch (e) {
              console.log('JSONパース失敗、次の候補を試します');
            }
          }
        }
      } catch (jsonError) {
        console.log('JSON抽出エラー:', jsonError);
      }
    }
    
    // ステップ2: 解析したJSONから値を抽出
    if (parsedJson) {
      console.log('JSONからフィールド値を抽出します');
      
      for (const field of formFields) {
        const fieldId = field.id || field.name;
        const fieldLabel = field.label ? field.label.toLowerCase() : '';
        
        if (!fieldId && !fieldLabel) continue;
        
        // プロパティを直接検索
        if (fieldId && parsedJson[fieldId] !== undefined) {
          result[fieldId] = parsedJson[fieldId];
          continue;
        }
        
        // ラベルで検索
        if (fieldLabel && parsedJson[fieldLabel] !== undefined) {
          result[fieldId || fieldLabel] = parsedJson[fieldLabel];
          continue;
        }
        
        // プロパティ名の一部一致での検索
        for (const key in parsedJson) {
          const keyLower = key.toLowerCase();
          
          if (fieldId && keyLower.includes(fieldId.toLowerCase())) {
            result[fieldId] = parsedJson[key];
            break;
          }
          if (fieldLabel && keyLower.includes(fieldLabel)) {
            result[fieldId || fieldLabel] = parsedJson[key];
            break;
          }
        }
      }
    }
    
    // ステップ3: 行ごとの解析（JSON解析が失敗した場合、または結果が不十分な場合）
    if (Object.keys(result).length < formFields.length / 2) {
      console.log('行ごとの解析を実行します');
      
      const lines = responseText.split('\n');
      for (const line of lines) {
        if (line.includes(':')) {
          // フォーマット例: "フィールド名: 値" または "フィールドID: 値"
          const parts = line.split(':', 2);
          if (parts.length === 2) {
            const key = parts[0].trim().toLowerCase();
            const value = parts[1].trim();
            
            // 対応するフィールドを検索
            for (const field of formFields) {
              const fieldId = field.id || field.name;
              const fieldLabel = field.label ? field.label.toLowerCase() : '';
              
              if (!fieldId && !fieldLabel) continue;
              
              // 完全一致
              if ((fieldId && key === fieldId.toLowerCase()) || 
                  (fieldLabel && key === fieldLabel)) {
                result[fieldId || fieldLabel] = value;
                break;
              }
              
              // 部分一致
              if ((fieldId && key.includes(fieldId.toLowerCase())) || 
                  (fieldLabel && key.includes(fieldLabel)) ||
                  (fieldId && fieldId.toLowerCase().includes(key)) ||
                  (fieldLabel && fieldLabel.includes(key))) {
                result[fieldId || fieldLabel] = value;
                break;
              }
            }
          }
        }
      }
    }
    
    // ステップ4: プレースホルダーやフィールドタイプに基づく推測
    if (Object.keys(result).length < formFields.length) {
      console.log('デフォルト値の推測を実行します');
      
      for (const field of formFields) {
        const fieldId = field.id || field.name || field.label;
        if (!fieldId || result[fieldId]) continue;
        
        // 既に値が設定されていなければデフォルト値を設定
        if (field.type === 'email') {
          result[fieldId] = 'user@example.com';
        } else if (field.type === 'password') {
          result[fieldId] = 'SecurePassword123!';
        } else if (field.type === 'tel') {
          result[fieldId] = '090-1234-5678';
        } else if (field.type === 'number') {
          result[fieldId] = '123';
        } else if (field.name && field.name.toLowerCase().includes('name')) {
          result[fieldId] = '山田太郎';
        } else if (field.placeholder) {
          // プレースホルダーから推測
          if (field.placeholder.toLowerCase().includes('email')) {
            result[fieldId] = 'user@example.com';
          } else if (field.placeholder.toLowerCase().includes('name')) {
            result[fieldId] = '山田太郎';
          }
        }
      }
    }
    
    console.log('パース結果:', result);
    return result;
  } catch (error) {
    console.error('AIレスポンスのパースエラー:', error);
    // エラーがあっても部分的な結果を返す
    return result;
  }
}

// 要素に関連するラベルを見つける関数
function findLabelForElement(element) {
  // idがある場合、そのidを参照するlabelを探す
  if (element.id) {
    const label = document.querySelector(`label[for="${element.id}"]`);
    if (label) {
      return label.textContent.trim();
    }
  }
  
  // 親要素内のラベルを探す
  const parentElement = element.parentElement;
  if (parentElement) {
    const label = parentElement.querySelector('label');
    if (label) {
      return label.textContent.trim();
    }
  }
  
  return '';
}

// フォームフィールドに値を入力する関数
function fillFormFields(form, fieldValues) {
  const inputElements = form.querySelectorAll('input, textarea, select');
  
  inputElements.forEach(element => {
    const fieldId = element.id || element.name;
    if (fieldId && fieldValues[fieldId]) {
      if (element.type === 'checkbox' || element.type === 'radio') {
        if (fieldValues[fieldId].toLowerCase() === 'true' || 
            fieldValues[fieldId].toLowerCase() === 'yes' ||
            fieldValues[fieldId] === '1') {
          element.checked = true;
        }
      } else {
        element.value = fieldValues[fieldId];
      }
    } else if (!fieldId && element.labels && element.labels.length > 0) {
      // ラベルを使用して値を入力
      const labelText = element.labels[0].textContent.trim();
      if (labelText && fieldValues[labelText]) {
        if (element.type === 'checkbox' || element.type === 'radio') {
          if (fieldValues[labelText].toLowerCase() === 'true' || 
              fieldValues[labelText].toLowerCase() === 'yes' ||
              fieldValues[labelText] === '1') {
            element.checked = true;
          }
        } else {
          element.value = fieldValues[labelText];
        }
      }
    }
  });
}

// グローバルスコープで関数を定義（executeScriptから呼び出せるようにするため）
window.detectForms = detectForms;

// 直接実行用のグローバル関数
window.directExecute = function(action, apiKey, model, options = {}) {
  console.log('directExecuteが呼び出されました:', action, options);
  
  if (action === 'autoFillForms') {
    // フォームコンテキスト情報
    const formContext = options.formContext || '';
    
    // 非同期処理を即時実行関数で実行
    (async () => {
      try {
        // フォーム検出
        console.log('フォーム検出を実行します');
        const forms = detectForms();
          
        if (forms.length === 0) {
          console.error('フォームが見つかりませんでした');
          return { 
            success: false, 
            message: 'ページ内にフォームまたは入力フィールドが見つかりませんでした' 
          };
        }
        
        // 最初のフォームに対して処理を実行
        const fieldValues = await analyzeFormWithAI(forms[0], apiKey, model, formContext);
        if (fieldValues && Object.keys(fieldValues).length > 0) {
          fillFormFields(forms[0], fieldValues);
          return { 
            success: true, 
            message: 'フォームを自動入力しました',
            fields: Object.keys(fieldValues).length
          };
        } else {
          return { 
            success: false, 
            message: 'フォームの解析に失敗しました' 
          };
        }
      } catch (error) {
        console.error('フォーム処理エラー:', error);
        return { 
          success: false, 
          message: `エラー: ${error.message}` 
        };
      }
    })();
      
    return { status: 'processing' };
  }
  
  return { success: false, message: '不明なアクション' };
};

// メイン処理
console.log('content-script が読み込まれました');

// バックグラウンドスクリプトに読み込みを通知
try {
  chrome.runtime.sendMessage({action: 'contentScriptLoaded'}, function(response) {
    console.log('バックグラウンドスクリプトからの応答:', response);
  });
} catch (err) {
  console.error('バックグラウンドスクリプトへの通知エラー:', err);
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('メッセージを受信しました:', message);
  
  // バックグラウンドスクリプトにデバッグメッセージを送信
  try {
    chrome.runtime.sendMessage({
      type: 'debug',
      data: { message: 'content-scriptがメッセージを受信', details: message }
    });
  } catch (err) {
    console.error('デバッグメッセージ送信エラー:', err);
  }
  
  if (message.action === 'autoFillForms') {
    const { apiKey, model, formContext } = message;
    
    if (!apiKey) {
      console.error('APIキーがありません');
      sendResponse({ success: false, message: 'APIキーが設定されていません' });
      return true;
    }
    
    // 非同期処理を開始
    (async () => {
      try {
        // フォーム検出
        console.log('フォーム検出を実行します');
        const forms = detectForms();
        
        if (forms.length === 0) {
          console.error('フォームが見つかりませんでした');
          sendResponse({ 
            success: false, 
            message: 'ページ内にフォームまたは入力フィールドが見つかりませんでした' 
          });
          return;
        }
        
        console.log('フォーム解析を開始します');
        
        // 検出された最初のフォームを処理
        const fieldValues = await analyzeFormWithAI(
          forms[0], 
          apiKey, 
          model, 
          formContext
        );
        console.log('解析結果:', fieldValues);
        
        if (fieldValues && Object.keys(fieldValues).length > 0) {
          fillFormFields(forms[0], fieldValues);
          sendResponse({ 
            success: true, 
            message: 'フォームを自動入力しました',
            fields: Object.keys(fieldValues).length
          });
        } else {
          sendResponse({ 
            success: false, 
            message: 'フォームの解析に失敗しました。入力フィールドに適切な値を見つけられませんでした。'
          });
        }
      } catch (error) {
        console.error('フォーム処理エラー:', error);
        
        // エラーメッセージの詳細化
        let errorMsg = error.message;
        if (errorMsg.includes('API')) {
          errorMsg = `OpenAI API エラー: ${errorMsg}`;
        }
        
        sendResponse({ 
          success: false, 
          message: `エラー: ${errorMsg}`
        });
        
        // バックグラウンドスクリプトにエラーを報告
        try {
          chrome.runtime.sendMessage({
            type: 'error',
            error: {
              message: errorMsg,
              stack: error.stack
            }
          });
        } catch (e) {
          console.error('エラー報告中のエラー:', e);
        }
      }
    })();
    
    return true; // 非同期レスポンスのために true を返す
  }
  
  // デフォルトのレスポンス
  sendResponse({ success: false, message: '不明なアクション' });
  return true;
}); 
```

# 実際に動かしてみる

https://www.form-mailer.jp/sample/

こちらのサイトにフォームの事例が載っているので、入力できるか試してみましょう。
下記がちょうど良さそうなのでやってみます。

https://pro.form-mailer.jp/fms/6d59e0a6280818

こんな感じの設定にしました。

![スクリーンショット 2025-04-20 14.10.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/7bdb103f-9b0b-43cd-a0a2-bc15f81514f9.png)

少し待つとこんな感じで入力できました。
生年月日も適当に入ってくれているのでそこそこ良さそうです

![screencapture-pro-form-mailer-jp-fms-6d59e0a6280818-2025-04-20-14_12_34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/1d4545d2-a531-4ce4-b1ab-5e0bce712c8e.png)