在昨天的 [Day 22] 中,我們成功開發了 Chrome Extension 與 Side Panel,實現了網頁與影片的即時側邊欄提煉。
雖然網頁端與瀏覽器外掛解決了桌面端的場景,但在行動端(特別是台灣與亞洲市場),使用者最頻繁使用的互動管道其實是通訊軟體。當朋友在 LINE 群組裡丟了一支 1 小時的 Podcast 連結、或是發送了一段長達 2 分鐘的語音訊息時,如果每次都要複製連結切換應用程式,體驗依然不夠順暢。
今天,我們將把 OmniVibe AI 拓展為多通道 Agent,打造 LINE Bot + Gemini 1.5 原生多模態 Webhook 整合!使用者只要在 LINE 聊天室中轉傳影片連結、文字專欄,甚至直接錄製語音訊息,LINE Bot 就能利用 Gemini 1.5 Flash 秒級處理語音與影片,並透過美麗的 LINE Flex Message 回傳精華提煉結果!
sequenceDiagram
autonumber
actor User as 使用者 (LINE App)
participant LineGateway as LINE Messaging API
participant Webhook as Next.js Webhook Router
participant LineSDK as @line/bot-sdk
participant Gemini as Gemini 1.5 Flash API
User->>LineGateway: 發送語音訊息 (.m4a) / YouTube 連結
LineGateway->>Webhook: POST /api/line/webhook (帶有 HMAC 簽章)
Webhook->>LineSDK: 驗證 Signature & 解析 Event
alt 訊息類型:語音 (Audio)
Webhook->>LineGateway: 下載語音 Binary Stream
Webhook->>Gemini: 直接傳送 Audio Buffer (Multi-modal)
else 訊息類型:連結 / 文字 (Text)
Webhook->>Gemini: 呼叫 Fast Distill 提煉引擎
end
Gemini-->>Webhook: 回傳結構化摘要與洞見
Webhook->>LineGateway: 回傳 Flex Message 結構化卡片
LineGateway-->>User: 於 LINE 聊天室渲染漂亮提煉卡片
src/app/api/line/webhook/route.ts)LINE Messaging API 會透過 HTTP POST 發送 Webhook 請求,我們必須使用通道密鑰(Channel Secret)驗證 x-line-signature 標頭,確保請求確實來自 LINE 伺服器,防止惡意偽造請求:
// src/app/api/line/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Client, WebhookEvent, validateSignature } from '@line/bot-sdk';
import { handleLineEvent } from '@/lib/line/event-handler';
const lineConfig = {
channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN || '',
channelSecret: process.env.LINE_CHANNEL_SECRET || '',
};
export const lineClient = new Client(lineConfig);
export async function POST(req: NextRequest) {
try {
const bodyText = await req.text();
const signature = req.headers.get('x-line-signature') || '';
// 1. 嚴格驗證 LINE 簽章 (HMAC-SHA256)
if (!validateSignature(bodyText, lineConfig.channelSecret, signature)) {
console.error('[LINE Webhook] 簽章驗證失敗!');
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const { events } = JSON.parse(bodyText);
// 2. 異步處理所有 LINE 事件 (避免 LINE Webhook Timeout)
Promise.all(events.map((event: WebhookEvent) => handleLineEvent(event))).catch((err) =>
console.error('[LINE Event Processing Error]', err)
);
// 迅速回應 LINE 伺服器 200 OK
return NextResponse.json({ success: true });
} catch (err: any) {
console.error('[LINE Webhook Error]', err);
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
src/lib/line/event-handler.ts)Gemini 1.5 Flash 具備強大的原生語音音訊處理能力(不需要先通過 Whisper 轉文字再送 LLM),我們可以將 LINE 使用者錄製的 .m4a 語音檔直接串接至 Gemini API:
// src/lib/line/event-handler.ts
import { WebhookEvent, MessageEvent } from '@line/bot-sdk';
import { lineClient } from '@/app/api/line/webhook/route';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { buildFlexMessage } from './flex-builder';
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');
export async function handleLineEvent(event: WebhookEvent) {
if (event.type !== 'message') return;
const replyToken = event.replyToken;
const message = event.message;
try {
// 處理情境 A:使用者發送「語音訊息 (Audio)」
if (message.type === 'audio') {
// 1. 回覆讀取中提示
await lineClient.showLoadingAnimation({ chatGuid: event.source.userId!, loadingSeconds: 10 });
// 2. 自 LINE 伺服器串流下載語音檔 Binary
const audioStream = await lineClient.getMessageContent(message.id);
const chunks: Buffer[] = [];
for await (const chunk of audioStream) {
chunks.push(chunk as Buffer);
}
const audioBuffer = Buffer.concat(chunks);
// 3. 直接將 Audio Buffer 餵給 Gemini 1.5 Flash 進行語音理解與提煉
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
const result = await model.generateContent([
{
inlineData: {
mimeType: 'audio/m4a',
data: audioBuffer.toString('base64'),
},
},
'這是一段使用者錄製的語音訊息。請幫我摘要這段語音的核心重點,並列出 3 個關鍵 Action Items。',
]);
const responseText = result.response.text();
// 4. 以 LINE Flex Message 回傳卡片
const flexMsg = buildFlexMessage('🎙️ 語音訊息精華提煉', responseText);
await lineClient.replyMessage(replyToken, flexMsg);
}
// 處理情境 B:使用者發送「文字 / YouTube 連結」
else if (message.type === 'text') {
const userText = message.text;
// 判斷是否包含 URL 連結
const urlRegex = /(https?:\/\/[^\s]+)/g;
const hasUrl = urlRegex.test(userText);
if (hasUrl) {
await lineClient.showLoadingAnimation({ chatGuid: event.source.userId!, loadingSeconds: 15 });
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
const result = await model.generateContent([
`請針對以下網址/內容進行內容提煉與重點總結:\n${userText}`,
]);
const flexMsg = buildFlexMessage('🎬 網址/影片內容速報', result.response.text());
await lineClient.replyMessage(replyToken, flexMsg);
}
}
} catch (error) {
console.error('[LINE Handler Error]', error);
await lineClient.replyMessage(replyToken, {
type: 'text',
text: '🤖 抱歉,OmniVibe 提煉過程中發生異常,請稍後再試!',
});
}
}
src/lib/line/flex-builder.ts)為了讓提煉成果在手機螢幕上易於閱讀,我們利用 LINE 原生的 Flex Message (JSON UI Component) 來建構排版:
// src/lib/line/flex-builder.ts
import { FlexMessage } from '@line/bot-sdk';
export function buildFlexMessage(title: string, content: string): FlexMessage {
return {
type: 'flex',
altText: `OmniVibe AI 提煉結果:${title}`,
contents: {
type: 'bubble',
styles: {
header: { backgroundColor: '#0F172A' },
body: { backgroundColor: '#1E293B' },
footer: { backgroundColor: '#0F172A' },
},
header: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'text',
text: title,
weight: 'bold',
color: '#38BDF8',
size: 'sm',
},
],
},
body: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'text',
text: content,
color: '#E2E8F0',
size: 'xs',
wrap: true,
maxLines: 15,
},
],
},
footer: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'button',
action: {
type: 'uri',
label: '🌐 開啟 Web 完整畫廊',
uri: 'https://app.omnivibe.ai/dashboard',
},
style: 'primary',
color: '#6366F1',
height: 'sm',
},
],
},
},
};
}
今天我們成功為 OmniVibe AI 建立了跨平台多通道(Multi-channel)Agent 架構:
截至目前為止,我們已經建構了完整且強大的全棧 AI 服務!但當我們需要將這個包含 Web、Extension 與 LINE Bot 的系統進行大規模自動化測試與跨跨部署時,我們該如何構建穩健的 CI/CD 管線?
👉 明天(Day 24),我們將進入【自動化與雲端部署篇】:實戰 Google Cloud Build + Cloud Run + Docker 自動化 CI/CD 流水線!看我們如何實現零停機(Zero-downtime)一鍵部署!
我們明天見!🔥