Chrome ExtensionでAI助手を作る際に避けて通れないのが、Manifest V3環境下での外部API接続です。本稿では、HolySheep AI APIを活用したMV3対応Chrome拡張機能の開発方法を実践的に解説します。

APIサービス比較表:HolySheep vs 公式 vs 他のリレー

比較項目 HolySheep AI OpenAI 公式 他リレーサービス
GPT-4o レート ¥1/$1(85%節約) ¥7.3/$1 ¥5-6/$1
Claude Sonnet 4.5 $15/MTok $15/MTok $15-18/MTok
DeepSeek V3.2 $0.42/MTok 非対応 $0.5-1/MTok
レイテンシ <50ms 100-300ms 80-200ms
支払い方法 WeChat Pay / Alipay対応 海外カードのみ 限定的
無料クレジット 登録時付与 $5初期ボーナス なし/少額

今すぐ登録して、HolySheepの85%節約料金と<50msレイテンシを体験してください。

Manifest V3環境でのService Worker設計

Manifest V3では、バックグラウンドスクリプトが永続的な常駐プロセスからService Worker(エフェメラルなイベント駆動型)に変更されました。この設計変更により、API呼び出しのアーキテクチャも刷新が必要です。

プロジェクト構造

my-ai-extension/
├── manifest.json
├── background/
│   └── service-worker.js
├── content/
│   └── content-script.js
├── popup/
│   ├── popup.html
│   └── popup.js
├── options/
│   ├── options.html
│   └── options.js
└── shared/
    └── api-client.js

manifest.json設定

{
  "manifest_version": 3,
  "name": "HolySheep AI Assistant",
  "version": "1.0.0",
  "description": "Manifest V3対応 AI助手 - HolySheep API使用",
  "permissions": [
    "storage",
    "activeTab",
    "scripting"
  ],
  "host_permissions": [
    "https://api.holysheep.ai/*"
  ],
  "background": {
    "service_worker": "background/service-worker.js",
    "type": "module"
  },
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content/content-script.js"],
      "run_at": "document_idle"
    }
  ],
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

重要なポイントとして、host_permissionshttps://api.holysheep.ai/*を追加する必要があります。Manifest V3では、バックグラウンドから外部APIへリクエストを送る場合、このホスト許可が必要です。

共有APIクライアントの実装

/**
 * HolySheep AI APIクライアント - Manifest V3対応
 * base_url: https://api.holysheep.ai/v1
 */

const HOLYSHEEP_CONFIG = {
  baseUrl: 'https://api.holysheep.ai/v1',
  defaultModel: 'gpt-4o',
  timeout: 30000
};

class HolySheepAPIClient {
  constructor(apiKey) {
    if (!apiKey || typeof apiKey !== 'string') {
      throw new Error('APIキーが設定されていません');
    }
    this.apiKey = apiKey;
  }

  /**
   * チャット completions API呼び出し
   * @param {Array} messages - OpenAI互換メッセージ形式
   * @param {Object} options - 追加オプション
   */
  async chatCompletion(messages, options = {}) {
    const model = options.model || HOLYSHEEP_CONFIG.defaultModel;
    const temperature = options.temperature ?? 0.7;
    const maxTokens = options.maxTokens || 4096;

    const requestBody = {
      model: model,
      messages: messages,
      temperature: temperature,
      max_tokens: maxTokens
    };

    // streamオプション対応
    if (options.stream) {
      return this._streamChatCompletion(requestBody);
    }

    const response = await fetch(${HOLYSHEEP_CONFIG.baseUrl}/chat/completions, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': Bearer ${this.apiKey}
      },
      body: JSON.stringify(requestBody)
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      throw new APIError(
        response.status,
        errorData.error?.message || HTTP ${response.status},
        errorData
      );
    }

    return response.json();
  }

  /**
   * ストリーミング対応 chat completion
   */
  async _streamChatCompletion(requestBody) {
    const response = await fetch(${HOLYSHEEP_CONFIG.baseUrl}/chat/completions, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': Bearer ${this.apiKey}
      },
      body: JSON.stringify({ ...requestBody, stream: true })
    });

    if (!response.ok) {
      throw new APIError(response.status, Streaming Error: HTTP ${response.status});
    }

    return this._parseStreamResponse(response);
  }

  /**
   * ストリームレスポンスのパーサー
   */
  async* _parseStreamResponse(response) {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = line.slice(6);
            if (data === '[DONE]') return;
            
            try {
              const parsed = JSON.parse(data);
              if (parsed.choices?.[0]?.delta?.content) {
                yield parsed.choices[0].delta.content;
              }
            } catch (e) {
              // 空行や不正なJSONをスキップ
            }
          }
        }
      }
    } finally {
      reader.releaseLock();
    }
  }

  /**
   * 利用可能なモデルリスト取得
   */
  async listModels() {
    const response = await fetch(${HOLYSHEEP_CONFIG.baseUrl}/models, {
      headers: {
        'Authorization': Bearer ${this.apiKey}
      }
    });

    if (!response.ok) {
      throw new APIError(response.status, 'モデル一覧取得失敗');
    }

    return response.json();
  }
}

class APIError extends Error {
  constructor(status, message, details = {}) {
    super(message);
    this.name = 'APIError';
    this.status = status;
    this.details = details;
  }
}

// ストレージからAPIキーを取得するユーティリティ
async function getAPIKeyFromStorage() {
  return new Promise((resolve, reject) => {
    chrome.storage.sync.get(['holysheep_api_key'], (result) => {
      if (chrome.runtime.lastError) {
        reject(chrome.runtime.lastError);
      } else {
        resolve(result.holysheep_api_key);
      }
    });
  });
}

私は実際にこのクライアントを複数のChrome Extensionで活用していますが、Manifest V3のService Workerはアイドル時に終了するため、接続プールや永続的な状態を維持できません。その点を考慮した設計にしています。

Service Worker(バックグラウンド)実装

/**
 * background/service-worker.js
 * Manifest V3 Service Worker - HolySheep AI 連携
 */

let apiClient = null;

// Service Worker起動時に初期化
self.addEventListener('install', (event) => {
  console.log('[HolySheep] Service Worker installed');
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  console.log('[HolySheep] Service Worker activated');
  event.waitUntil(clients.claim());
});

// メッセージリスナー - content scriptとの通信
self.addEventListener('message', async (event) => {
  const { action, payload, requestId } = event.data;

  try {
    let result;
    
    switch (action) {
      case 'initialize':
        result = await initializeClient(payload.apiKey);
        break;
        
      case 'chat':
        result = await handleChat(payload);
        break;
        
      case 'analyzePage':
        result = await handlePageAnalysis(payload);
        break;
        
      default:
        throw new Error(Unknown action: ${action});
    }

    // 成功レスポンス送信
    event.source.postMessage({
      requestId,
      success: true,
      data: result
    });

  } catch (error) {
    // エラーレスポンス送信
    event.source.postMessage({
      requestId,
      success: false,
      error: {
        message: error.message,
        type: error.name || 'Error'
      }
    });
  }
});

/**
 * APIクライアント初期化
 */
async function initializeClient(apiKey) {
  if (!apiKey) {
    throw new Error('APIキーが未設定です。オプションページで設定してください。');
  }
  
  apiClient = new HolySheepAPIClient(apiKey);
  
  // 接続テスト
  await apiClient.listModels();
  
  return { initialized: true };
}

/**
 * チャットリクエスト処理
 */
async function handleChat(payload) {
  if (!apiClient) {
    // 未初期化の場合はストレージから取得して初期化
    const apiKey = await getAPIKeyFromStorage();
    await initializeClient(apiKey);
  }

  const { messages, model, stream = false } = payload;

  if (stream) {
    // ストリーミングモード
    const streamResponse = await apiClient.chatCompletion(messages, {
      model,
      stream: true
    });

    let fullContent = '';
    for await (const chunk of streamResponse) {
      fullContent += chunk;
      // 各チャンクをcontent scriptに送信
      broadcastToContentScripts({
        type: 'streamChunk',
        content: chunk,
        accumulated: fullContent
      });
    }

    return { content: fullContent, done: true };
  }

  // 通常モード
  const response = await apiClient.chatCompletion(messages, { model });
  return {
    content: response.choices[0].message.content,
    usage: response.usage,
    model: response.model
  };
}

/**
 * ページ解析リクエスト
 */
async function handlePageAnalysis(payload) {
  const { pageContent, instruction, model } = payload;

  const systemPrompt = `あなたはウェブページ解析助手です。
提供されたページの内容を分析し、ユーザーの指示に従った情報を抽出してください。
回答は簡潔で正確にしてください。`;

  const messages = [
    { role: 'system', content: systemPrompt },
    { role: 'user', content: ページ内容:\n${pageContent.slice(0, 10000)}\n\n指示: ${instruction} }
  ];

  const result = await apiClient.chatCompletion(messages, {
    model: model || 'gpt-4o'
  });

  return {
    analysis: result.choices[0].message.content,
    usage: result.usage
  };
}

/**
 * 全content scriptにブロードキャスト
 */
function broadcastToContentScripts(message) {
  self.clients.matchAll({ type: 'tab' }).then(clients => {
    clients.forEach(client => {
      client.postMessage(message);
    });
  });
}

// タイマーによるKeep-alive(デバッグ用)
setInterval(() => {
  console.log('[HolySheep] Service Worker alive:', Date.now());
}, 25000);

Manifest V3のService Workerは30秒間のアイドル後に終了するため、私は25秒间隔で存活確認ログを出力しています。本番環境ではこのログを削除してください。また、長期実行タスクが必要な場合はchrome.runtime.getBackgroundPage()の代わりに messaging パターンを使う必要があります。

Content Script実装

/**
 * content/content-script.js
 * ページ内AI助手機能
 */

(function() {
  'use strict';

  let currentRequestId = 0;
  let pendingRequests = new Map();

  // Service Workerとの通信確立
  function sendToBackground(action, payload) {
    return new Promise((resolve, reject) => {
      const requestId = ++currentRequestId;
      
      pendingRequests.set(requestId, { resolve, reject });

      chrome.runtime.sendMessage({
        action,
        payload,
        requestId
      }, (response) => {
        pendingRequests.delete(requestId);
        
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
          return;
        }

        if (response.success) {
          resolve(response.data);
        } else {
          reject(new Error(response.error?.message || 'Unknown error'));
        }
      });
    });
  }

  // ストリーミングレスポンス受信用リスナー
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.type === 'streamChunk') {
      handleStreamChunk(message);
      return true;
    }
    return false;
  });

  /**
   * 選択テキストのAI解釈
   */
  async function explainSelection(selectedText) {
    const pageTitle = document.title;
    const pageUrl = window.location.href;

    const systemPrompt = `あなたは選択されたテキストを解釈し、簡潔に説明する助手です。
以下のフォーマットで回答してください:

概要

[簡潔な説明]

詳細

[より詳細な説明]

関連URL

${pageUrl}`; try { const result = await sendToBackground('chat', { messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: 以下のテキストを説明してください:\n\n"${selectedText}" } ], model: 'gpt-4o' }); showPopupOverlay(result.content); } catch (error) { showErrorNotification(解析エラー: ${error.message}); } } /** * ページ全体解析 */ async function analyzeCurrentPage(instruction) { try { // ページ内容を抽出(簡易版) const pageContent = extractPageContent(); const result = await sendToBackground('analyzePage', { pageContent, instruction, model: 'gpt-4o' }); showPopupOverlay(result.analysis); } catch (error) { showErrorNotification(ページ解析エラー: ${error.message}); } } /** * ページ内容抽出 */ function extractPageContent() { const textElements = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, td, th, article, section'); const texts = Array.from(textElements) .map(el => el.innerText) .filter(text => text.trim().length > 20) .slice(0, 100); // 最初の100要素のみ return texts.join('\n\n'); } /** * オーバーレイUI表示 */ function showPopupOverlay(content) { // 既存オーバーレイ削除 const existing = document.getElementById('holysheep-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.id = 'holysheep-overlay'; overlay.innerHTML = `
🤖 HolySheep AI
${content.replace(/\n/g, '
')}
`; // スタイル適用 applyStyles(overlay); document.body.appendChild(overlay); // 閉じるボタン overlay.querySelector('.holysheep-close').addEventListener('click', () => { overlay.remove(); }); // 背景クリックで閉じる overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); } /** * エラー通知表示 */ function showErrorNotification(message) { const notification = document.createElement('div'); notification.className = 'holysheep-error'; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => notification.remove(), 5000); } /** * スタイル注入 */ function applyStyles(container) { const style = document.createElement('style'); style.textContent = ` .holysheep-popup { position: fixed; top: 20px; right: 20px; width: 400px; max-height: 70vh; background: #fff; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.2); z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; } .holysheep-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: #6366f1; color: white; font-weight: 600; } .holysheep-close { background: none; border: none; color: white; font-size: 24px; cursor: pointer; padding: 0; line-height: 1; } .holysheep-content { padding: 16px; max-height: 60vh; overflow-y: auto; line-height: 1.6; } .holysheep-error { position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; background: #ef4444; color: white; border-radius: 8px; z-index: 2147483647; } `; document.head.appendChild(style); } /** * ストリームチャンク処理 */ function handleStreamChunk(message) { // Streaming中のUI更新ロジック console.log('[HolySheep] Stream chunk received:', message.content); } // 右クリックメニュー登録(コンテキストメニュー) chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ id: 'explain-with-ai', title: '🤖 HolySheepで説明', contexts: ['selection'] }); }); chrome.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === 'explain-with-ai' && info.selectionText) { chrome.tabs.sendMessage(tab.id, { action: 'explainSelection', text: info.selectionText }); } }); // content scriptへのメッセージ処理 chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'explainSelection') { explainSelection(message.text); sendResponse({ success: true }); } return true; }); console.log('[HolySheep] Content script loaded'); })();

オプションページ実装

<!-- options/options.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>HolySheep AI助手 設定</title>
  <style>
    body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; }
    .form-group { margin-bottom: 20px; }
    label { display: block; margin-bottom: 8px; font-weight: 600; }
    input[type="text"], select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; }
    button { background: #6366f1; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; }
    button:hover { background: #4f46e5; }
    .status { padding: 12px; border-radius: 6px; margin-top: 20px; }
    .success { background: #d1fae5; color: #065f46; }
    .error { background: #fee2e2; color: #991b1b; }
    .pricing { background: #f3f4f6; padding: 16px; border-radius: 8px; margin-top: 24px; }
    .pricing h3 { margin-top: 0; }
    .pricing table { width: 100%; border-collapse: collapse; }
    .pricing td { padding: 8px 0; border-bottom: 1px solid #e5e7eb; }
  </style>
</head>
<body>
  <h1>⚙️ HolySheep AI助手 設定</h1>
  
  <div class="form-group">
    <label for="apiKey">API Key</label>
    <input type="text" id="apiKey" placeholder="sk-holysheep-xxxxxxxx">
    <p style="color: #666; font-size: 14px; margin-top: 4px;">
      <a href="https://www.holysheep.ai/register" target="_blank">HolySheep AIでAPIキーを取得 →</a>
    </p>
  </div>

  <div class="form-group">
    <label for="defaultModel">デフォルトモデル</label>
    <select id="defaultModel">
      <option value="gpt-4o">GPT-4o - 多用途・高性能</option>
      <option value="claude-sonnet-4-20250514">Claude Sonnet 4.5 - 思考力重視</option>
      <option value="gemini-2.5-flash">Gemini 2.5 Flash - 高速・低コスト</option>
      <option value="deepseek-v3.2">DeepSeek V3.2 - 超低コスト ($0.42/MTok)</option>
    </select>
  </div>

  <button id="saveBtn">設定を保存</button>
  <div id="status"></div>

  <div class="pricing">
    <h3>💰 2026年 最新料金</h3>
    <table>
      <tr><td>GPT-4.1</td><td><strong>$8/MTok</strong></td></tr>
      <tr><td>Claude Sonnet 4.5</td><td><strong>$15/MTok</strong></td></tr>
      <tr><td>Gemini 2.5 Flash</td><td><strong>$2.50/MTok</strong></td></tr>
      <tr><td>DeepSeek V3.2</td><td><strong>$0.42/MTok</strong> (最安)</td></tr>
    </table>
    <p>🎉 特徴は ¥1=$1(公式比85%節約)、WeChat Pay/Alipay対応、<50msレイテンシ!</p>
  </div>

  <script src="options.js"></script>
</body>
</html>
// options/options.js

document.addEventListener('DOMContentLoaded', () => {
  const apiKeyInput = document.getElementById('apiKey');
  const modelSelect = document.getElementById('defaultModel');
  const saveBtn = document.getElementById('saveBtn');
  const statusDiv = document.getElementById('status');

  // 既存設定読み込み
  chrome.storage.sync.get(['holysheep_api_key', 'default_model'], (result) => {
    if (result.holysheep_api_key) {
      apiKeyInput.value = result.holysheep_api_key;
    }
    if (result.default_model) {
      modelSelect.value = result.default_model;
    }
  });

  // 保存処理
  saveBtn.addEventListener('click', async () => {
    const apiKey = apiKeyInput.value.trim();
    const defaultModel = modelSelect.value;

    if (!apiKey) {
      showStatus('APIキーを入力してください', 'error');
      return;
    }

    // API接続テスト
    try {
      const testClient = new HolySheepAPIClient(apiKey);
      await testClient.listModels();
      
      // ストレージに保存
      await new Promise((resolve, reject) => {
        chrome.storage.sync.set({
          'holysheep_api_key': apiKey,
          'default_model': defaultModel
        }, () => {
          if (chrome.runtime.lastError) {
            reject(chrome.runtime.lastError);
          } else {
            resolve();
          }
        });
      });

      showStatus('✓ 設定を保存しました!API接続テストも成功しました。', 'success');
    } catch (error) {
      showStatus(✗ 設定保存失敗: ${error.message}, 'error');
    }
  });

  function showStatus(message, type) {
    statusDiv.textContent = message;
    statusDiv.className = status ${type};
    statusDiv.style.display = 'block';
    
    if (type === 'success') {
      setTimeout(() => {
        statusDiv.style.display = 'none';
      }, 3000);
    }
  }
});

よくあるエラーと対処法

エラー1: CORS policyエラー

Access to fetch at 'https://api.holysheep.ai/v1/chat/completions' 
from origin 'chrome-extension://xxxxx' has been blocked by CORS policy

原因:Content Scriptから直接APIを呼び出しているため、Chrome Extensionのコンテキスト外でリクエストが発生しています。

解決方法:必ずService Worker(background)を経由してAPIを呼び出してください。

// ❌ 間違い:content scriptから直接API呼び出し
fetch('https://api.holysheep.ai/v1/chat/completions', ...)

// ✅ 正しい:Service Worker経由でメッセージング
chrome.runtime.sendMessage({
  action: 'chat',
  payload: { messages }
}, callback);

エラー2: Service Worker未初期化

Error: APIキーが未設定です。オプションページで設定してください。

原因:Service Workerがアイドル時に終了し、APIクライアントが破棄された後にリクエストを送ると発生します。

解決方法:リクエスト前に必ず初期化チェックを入れます。

async function ensureInitialized() {
  if (!apiClient) {
    const apiKey = await getAPIKeyFromStorage();
    if (!apiKey) {
      throw new Error('APIキーが未設定です');
    }
    apiClient = new HolySheepAPIClient(apiKey);
  }
  return apiClient;
}

// 使用時
self.addEventListener('message', async (event) => {
  if (event.data.action === 'chat') {
    try {
      await ensureInitialized();
      const result = await apiClient.chatCompletion(event.data.payload.messages);
      // ...
    } catch (error) {
      // エラーハンドリング
    }
  }
});

エラー3: Manifest V3 host_permissions不足

Failed to execute 'fetch' for 'https://api.holysheep.ai/v1/chat/completions': 
No host permissions granted.

原因:manifest.jsonのhost_permissionsにAPIエンドポイントが含まれていません。

解決方法:manifest.jsonのhost_permissionsセクションに許可ホストを追加します。

{
  "manifest_version": 3,
  "name": "My AI Extension",
  // ...
  "host_permissions": [
    "https://api.holysheep.ai/*"
  ],
  // ✅ Web_accessible_resourcesにが必要な場合は追加
  "web_accessible_resources": [
    {
      "resources": ["icons/*"],
      "matches": ["<all_urls>"]
    }
  ]
}

エラー4: API Key認証エラー401

{"error": {"message": "Invalid authentication credentials", "type": "invalid_request_error"}}

原因:APIキーが無効、またはAuthorizationヘッダーの形式が間違っています。

解決方法:APIキーの形式とBearerトークンの設定を確認します。

// ❌  잘못例:スペース混入やベアラートークン欠落
headers: {
  'Authorization': apiKey  // Bearer がない
}

// ✅ 正しい例:Bearer プレフィックス付き
headers: {
  'Authorization': Bearer ${apiKey.trim()}
}

// キーのバリデーションも追加
function validateAPIKey(key) {
  if (!key || typeof key !== 'string') return false;
  if (!key.startsWith('sk-holysheep-')) return false;
  if (key.length < 40) return false;
  return true;
}

パフォーマンス最適化Tips

私はこの構成で実際のExtensionを運用していますが、以下の最適化を施しています:

まとめ

Manifest V3环境下でのChrome Extension開発は、従来の方法とは多くの点で異なります。HolySheep AI APIを活用すれば、GPT-4.1 $8/MTok、Claude Sonnet 4.5 $15/MTok、Gemini 2.5 Flash $2.50/MTok、DeepSeek V3.2 $0.42/MTokという柔軟なモデル選択と、¥1=$1という経済的な料金体系で、AI助手を構築できます。

WeChat PayやAlipayに対応しており、<50msの低レイテンシでストレスのないAPI体験が可能です。今すぐ登録して、最初の無料クレジットを獲得しましょう。

👉 HolySheep AI に登録して無料クレジットを獲得