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_permissionsにhttps://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を運用していますが、以下の最適化を施しています:
- リクエストまとめ:複数のAIリクエストはバッチ化し、1つのAPI呼び出しに統合
- コンテキスト管理:pageContentは10,000文字でクリップし、不要なトークン消費を防止
- モデル選択:単純な質問にはGemini 2.5 Flash ($2.50/MTok)、複雑な分析にはGPT-4oを自動選択
- ローカルキャッシュ:chrome.storage.localに類似クエリの結果をキャッシュし、重複リクエストを削減
- ストリーミング表示:Real-time Feedbackでユーザーの待機感を軽減
まとめ
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 に登録して無料クレジットを獲得