iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Build on Google AI

用 Google AI 生態系 30 天從零打造一個全棧 AI SaaS 服務系列 第 22 篇

Day 22 -【跨平台生態擴充】實戰 Chrome Extension 瀏覽器外掛開發與 Side Panel 側邊欄雙端同步

  • 分享至 

  • xImage
  •  

在昨天的 [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 助手,實現網頁內即時提煉與帳號/額度雙端同步!


🧩 瀏覽器外掛架構設計 (Manifest V3 + Side Panel)

為了符合 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


🛠️ 第一步:配置 Manifest V3 (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"
  }
}


⚡ 第二步:建立 Background Service Worker (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 },
      });
    });
  }
});


🖥️ 第三步:打造 Side Panel 側邊欄 UI 元件 (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>
  );
}


🔄 第四步:Web 主站與 Chrome 外掛 Auth Token 雙端同步

為了讓使用者不必在 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;
}


🧪 實測驗證:YouTube 網頁直接彈出 Side Panel 秒級提煉

  1. 安裝外掛:開啟 Chrome chrome://extensions,選擇「載入未打包擴充功能」載入專案目錄。
  2. 開啟 YouTube:點選任意一支 30 分鐘的 Tech Talk 影片。
  3. 一鍵觸發:
  • 點擊右鍵選單「🎬 OmniVibe AI:提煉當前頁面/影片重點」。
  • Chrome 右側原生 Side Panel 自動滑出。
  • 觸發 Day 21 的雙層快取 API,僅耗時 1.2 秒 即於側邊欄渲染出該影片的 3 大洞見與 Threads 腳本!

🎯 總結與明日預告

今天我們成功完成 OmniVibe AI 跨平台生態擴充的核心拼圖:

  1. 採用最新的 Manifest V3 標準建構了輕量化 Chrome Extension。
  2. 整合原生 Chrome Side Panel API,提供不打擾當前瀏覽體驗的雙螢幕提煉介面。
  3. 實作 Web 主站與 Extension 憑證同步機制,讓使用者跨平台無縫享用 Quota 額度與歷史紀錄。

現在,OmniVibe AI 已能覆蓋 Web Dashboard 與 Chrome 瀏覽器環境。但在移動端,使用者最習慣的互動管道往往是通訊軟體。

👉 明天(Day 23),我們將進入【多通道 Agent 擴充篇】:實戰 LINE Bot + Gemini Multi-modal Webhook 整合!讓使用者在 LINE 群組裡轉傳影片連結或語音訊息,就能自動觸發 OmniVibe AI 提煉!

我們明天見!🔥


上一篇
Day 21 -【效能與成本極限優化】Gemini Context Caching 快取機制與 Qdrant 語意快取 (Semantic Cache) 實戰
下一篇
Day 23 -【多通道 Agent 擴充】實戰 LINE Bot + Gemini Multi-modal Webhook 整合與語音 / 連結極速提煉
系列文
用 Google AI 生態系 30 天從零打造一個全棧 AI SaaS 服務 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言