在昨天的 [Day 21] 中,我們成功導入了 Gemini Context Caching(上下文快取) 與 Qdrant 語意快取 (Semantic Cache),大幅降低了 75% 的 Token 成本,並實現了 50ms 級別的毫秒 responses。
然而,至今為止我們的 OmniVibe AI 都是以獨立 Web Dashboard 的形式運作。當使用者在瀏覽 YouTube 影片、閱讀 Medium 專欄或解析外文 PDF 時,傳統做法必須「複製網址 -> 切換分頁 -> 開啟 OmniVibe -> 貼上連結 -> 點擊提煉」,過程充滿摩擦。
今天,我們將把 OmniVibe AI 的多模態提煉能力封裝為 Chrome Extension (Manifest V3),並整合 Chrome 最新原生的 Side Panel (側邊欄 API),讓使用者在任何網頁上一鍵召喚 AI 助手,實現網頁內即時提煉與帳號/額度雙端同步!
為了符合 Google Chrome 最新規範並提供零延遲的互動體驗,我們採用 Manifest V3 架構:
graph TD
subgraph Browser Tab (YouTube / Article)
Page[使用者正在瀏覽的網頁] --> ContextMenu[右鍵選單 / 快捷鍵]
Page --> ContentScript[Content Script (擷取內文/DOM)]
end
subgraph Chrome Extension Ecosystem
ContextMenu --> ServiceWorker[Background Service Worker (background.ts)]
ContentScript --> ServiceWorker
ServiceWorker --> SidePanel[Chrome Native Side Panel UI (React)]
ServiceWorker <--> ChromeStorage[chrome.storage.sync (Token / Quota 同步)]
end
subgraph OmniVibe Cloud Backend
SidePanel -- SSE 串流 / Fast Distill API --> API[Next.js Backend Route Handler]
API <--> Gemini[Gemini 1.5 Flash API]
end
public/manifest.json)我們在 manifest.json 中宣告必要的權限,包含 sidePanel、activeTab 與 storage:
{
"manifest_version": 3,
"name": "OmniVibe AI - 隨手影音與文章提煉助手",
"version": "1.0.0",
"description": "一鍵提煉 YouTube 影片、Podcasts 與長文專欄精華,直接於側邊欄即時生成 Threads 爆款貼文與短影音腳本。",
"permissions": [
"sidePanel",
"activeTab",
"scripting",
"storage",
"contextMenus"
],
"host_permissions": [
"https://*.youtube.com/*",
"https://*/*"
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_title": "開啟 OmniVibe AI 側邊欄"
},
"side_panel": {
"default_path": "sidepanel.html"
},
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}
src/background/index.ts)Service Worker 負責監聽工具列點擊事件、註冊右鍵快選選單,並在點擊時自動展開 Chrome Side Panel 側邊欄:
// src/background/index.ts
// 1. 設定點擊外掛 Icon 時直接開啟 Side Panel
chrome.sidePanel
.setPanelBehavior({ openPanelOnActionClick: true })
.catch((error) => console.error(error));
// 2. 註冊右鍵上下文選單 (Context Menu)
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'omnivibe-distill-selection',
title: '🚀 OmniVibe AI:提煉選取文字精華',
contexts: ['selection'],
});
chrome.contextMenus.create({
id: 'omnivibe-distill-page',
title: '🎬 OmniVibe AI:提煉當前頁面/影片重點',
contexts: ['page'],
});
});
// 3. 監聽右鍵選單點擊
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (!tab?.id) return;
if (info.menuItemId === 'omnivibe-distill-selection') {
const selectedText = info.selectionText || '';
// 開啟 Side Panel 並發送選取的內容
chrome.sidePanel.open({ windowId: tab.windowId }, () => {
chrome.runtime.sendMessage({
type: 'QUICK_DISTILL_TEXT',
payload: { text: selectedText, url: tab.url },
});
});
} else if (info.menuItemId === 'omnivibe-distill-page') {
chrome.sidePanel.open({ windowId: tab.windowId }, () => {
chrome.runtime.sendMessage({
type: 'QUICK_DISTILL_PAGE',
payload: { url: tab.url, title: tab.title },
});
});
}
});
src/sidepanel/SidePanel.tsx)側邊欄使用 React + Tailwind CSS 打造,能自動偵測使用者當前分頁的網址(例如是否為 YouTube 影片),並直接呼叫我們在 Day 21 封裝的高效能後端 API:
// src/sidepanel/SidePanel.tsx
import React, { useEffect, useState } from 'react';
import { Sparkles, Youtube, Globe, ArrowRight, Loader2, RefreshCw } from 'lucide-react';
export function SidePanel() {
const [currentUrl, setCurrentUrl] = useState<string>('');
const [pageTitle, setPageTitle] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [result, setResult] = useState<any>(null);
const [token, setToken] = useState<string | null>(null);
// 1. 初始化時取得當前 Active Tab 網址與存取的 Auth Token
useEffect(() => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
setCurrentUrl(tabs[0].url || '');
setPageTitle(tabs[0].title || '');
}
});
// 從 chrome.storage.sync 取得與 Web 版同步的 JWT Token
chrome.storage.sync.get(['omnivibe_token'], (res) => {
if (res.omnivibe_token) setToken(res.omnivibe_token);
});
// 監聽 Background 發送的廣播消息
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'QUICK_DISTILL_PAGE') {
setCurrentUrl(message.payload.url);
handleDistill(message.payload.url);
}
});
}, []);
const isYouTube = currentUrl.includes('youtube.com/watch') || currentUrl.includes('youtu.be');
// 2. 觸發 AI 提煉流程
const handleDistill = async (targetUrl = currentUrl) => {
setLoading(true);
setResult(null);
try {
const response = await fetch('https://app.omnivibe.ai/api/ai/fast-distill', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
fileUri: targetUrl,
mimeType: isYouTube ? 'video/mp4' : 'text/html',
prompt: '請將此頁面/影片內容提煉為核心摘要與爆款 Threads 貼文。',
tokenCount: 10000,
}),
});
const data = await response.json();
setResult(data.result);
} catch (err) {
console.error('[Extension Error]:', err);
} finally {
setLoading(false);
}
};
return (
<div className="w-full h-screen bg-slate-950 text-slate-100 p-4 flex flex-col space-y-4 font-sans border-l border-slate-800">
{/* 標頭 */}
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<div className="flex items-center space-x-2">
<div className="p-1.5 bg-indigo-600/20 text-indigo-400 rounded-lg">
<Sparkles className="w-4 h-4" />
</div>
<span className="font-bold text-sm tracking-wide">OmniVibe Companion</span>
</div>
<span className="text-[10px] bg-slate-800 text-slate-400 px-2 py-0.5 rounded-full">v1.0</span>
</div>
{/* 當前分頁卡片 */}
<div className="bg-slate-900/80 border border-slate-800 rounded-lg p-3 space-y-2">
<div className="flex items-center space-x-2 text-xs text-slate-400">
{isYouTube ? <Youtube className="w-4 h-4 text-red-500" /> : <Globe className="w-4 h-4 text-blue-400" />}
<span className="truncate">{isYouTube ? 'YouTube 影片偵測' : '網頁文章偵測'}</span>
</div>
<p className="text-xs font-medium text-slate-200 line-clamp-2">{pageTitle || currentUrl}</p>
<button
onClick={() => handleDistill()}
disabled={loading || !currentUrl}
className="w-full mt-2 py-2 px-3 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 text-white font-medium text-xs rounded-md transition flex items-center justify-center space-x-1.5 shadow-lg shadow-indigo-500/20 disabled:opacity-50"
>
{loading ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
<span>Gemini 提煉中...</span>
</>
) : (
<>
<span>一鍵秒級提煉</span>
<ArrowRight className="w-3.5 h-3.5" />
</>
)}
</button>
</div>
{/* 提煉結果顯示區 */}
<div className="flex-1 overflow-y-auto space-y-3 pr-1">
{result && (
<div className="space-y-3 animate-fade-in">
{/* 核心摘要 */}
<div className="bg-slate-900/60 border border-slate-800/80 rounded-lg p-3">
<h4 className="text-xs font-semibold text-indigo-400 mb-1">📌 核心洞見 (Takeaways)</h4>
<ul className="text-xs text-slate-300 space-y-1 list-disc pl-4">
{result.takeaways?.map((item: string, i: number) => (
<li key={i}>{item}</li>
))}
</ul>
</div>
{/* Threads 文案 */}
<div className="bg-slate-900/60 border border-slate-800/80 rounded-lg p-3">
<h4 className="text-xs font-semibold text-purple-400 mb-1">💬 爆款 Threads 貼文預覽</h4>
<p className="text-xs text-slate-300 whitespace-pre-line leading-relaxed">
{result.socialPosts?.threads}
</p>
</div>
</div>
)}
</div>
</div>
);
}
為了讓使用者不必在 Extension 中重新輸入帳號密碼,我們在 Web Dashboard 頁面中透過 Web Messaging API 與 chrome.storage.sync 實現一鍵授權:
// src/app/dashboard/page.tsx (Web 主站中的同步腳本)
'use client';
import { useEffect } from 'react';
export function AuthExtensionSync({ userToken }: { userToken: string }) {
useEffect(() => {
// 檢查是否有安裝 OmniVibe Extension,並將 Token 自動寫入 chrome.storage
if (typeof window !== 'undefined' && (window as any).chrome?.storage) {
(window as any).chrome.storage.sync.set({ omniVibe_token: userToken }, () => {
console.log('[OmniVibe Sync] 成功將認證 Token 同步至 Chrome Extension!');
});
}
}, [userToken]);
return null;
}
chrome://extensions,選擇「載入未打包擴充功能」載入專案目錄。今天我們成功完成 OmniVibe AI 跨平台生態擴充的核心拼圖:
現在,OmniVibe AI 已能覆蓋 Web Dashboard 與 Chrome 瀏覽器環境。但在移動端,使用者最習慣的互動管道往往是通訊軟體。
👉 明天(Day 23),我們將進入【多通道 Agent 擴充篇】:實戰 LINE Bot + Gemini Multi-modal Webhook 整合!讓使用者在 LINE 群組裡轉傳影片連結或語音訊息,就能自動觸發 OmniVibe AI 提煉!
我們明天見!🔥