03|接入 Gemini
先看答案: 這條路用你自己的 Gemini API Key 與 Google Apps Script,把官方 LINE 接到 AI;部署後拿到自己的 Web App URL,再填回 LINE。前置條件: 已完成 02,並有自己的 Google 帳戶與 Channel ID 與 Channel secret。做完會得到什麼: 一個可交給 LINE Webhook 的自己的網址與完成設定;真正收得到回覆要到 04|看到第一個 AI 回覆 實測。AI 助理免費安裝服務標準路徑不要求信用卡或啟用 billing,但免費方案有配額;API Key 與 Channel secret 只留在自己的設定,不交給我方。
前置條件
| 你需要什麼 | 怎麼取得 |
|---|---|
| 完成 02|建立官方 LINE | 你能回答該章〈這一章的成功判定〉的每一項 |
| 你自己的 Google 帳戶 | 你自己申請;02 的〈請準備自己的帳戶〉已把它列為必備項 |
| 你自己的 Channel ID 與 Channel secret | 你在 02 的 Step 3 從同一個帳號複製的兩個設定 |
這一章不需要把任何帳號或憑證交給我方。這項免費安裝服務全程免費,不需要信用卡。 你使用自己的 Google、LINE 與 Apps Script 帳戶,走標準免費方案完成第一個 AI 回覆。
Google 的 gemini-3.5-flash-lite Standard Free Tier 目前提供免費的輸入與輸出;新帳戶先從 Free Tier 開始。免費不等於無限量:Gemini 有速率/使用量限制,Apps Script 有 quota,LINE 免費方案與 Reply API 也有各自的服務限制。超過限制時會回報錯誤、暫停或等待恢復,不會自動替你啟用 billing、Paid Tier 或付款。
如果第三方畫面顯示 billing、Paid Tier 或付款設定,那不是完成這項免費安裝服務的必要步驟;不要為了完成AI 助理免費安裝服務而綁卡。建立 API key 本身不等於啟用 billing;若畫面與本標準路徑不同,先停下確認官方頁面與目前帳戶狀態。
這一章也不需要你會寫程式——程式碼下面直接給你,你只要複製、貼上、一次貼入設定。
Gemini 在這裡扮演什麼角色
這項免費安裝服務預設使用 gemini-3.5-flash-lite。模型可以替換,實際可用性仍以你的專案、配額與當下服務狀態為準。
它負責協助處理:
- 讀懂收到的文字。
- 依照你提供的工作背景產生回覆。
- 在最小測試中回傳一段可以觀察的結果。
Gemini 不是AI 助理免費安裝服務的產品本體。
未來模型可以替換,但使用者要建立自己的 AI 工作入口這個目標不變。
你會做什麼
這一章結束時,你會有一個自己的網址,貼進 LINE 之後,客人傳訊息就會收到 AI 回覆。
- 取得自己的 Gemini API Key。
- 開一個 Google Apps Script 專案。
- 把程式碼貼進去。
- 一次匯入設定。
- 檢查設定有沒有存進去。
- 把它「部署」出去——部署的意思是讓這段程式有一個外面連得到的網址——然後拿到那個網址。
- 把網址填回 LINE。
一次做一步,每一步做完都有東西可以看。
API Key 是秘密
API Key 的處理原則:
- 不要貼到聊天室。
- 不要貼到公開 issue 或貼文。
- 不要放進截圖。
- 不要放進影片錄影畫面。
- 不要把完整值放入 log。
- 不要因為 Troubleshooter 要求,就把完整值交出去。
這份AI 助理免費安裝服務會教你如何把秘密留在自己的目的地,但不會要求你把秘密交給我方。
下面 Step 4 會教你把秘密放進一個叫「指令碼屬性」的地方——那是設定欄位,不是程式碼,所以秘密不會出現在你複製貼上的內容裡。
人類操作步驟
Step 1:建立自己的 API Key
「API Key」是一把鑰匙。你的程式拿著它去問 Gemini,Gemini 才知道是你在問。
- 用你自己的 Google 帳戶打開 https://aistudio.google.com/apikey。
這個網址會直接到建立 API Key 的頁面,不用自己在網站裡找。沒登入的話會先要你登入。
- 按右上角「Create API key」。
- 「Create a new key」視窗:幫金鑰取個名字(隨意就好)、專案用它預設給的那個 → 按「Create key」。建立金鑰本身不等於啟用 billing;標準免費路徑不要求付費設定或信用卡。若畫面與這條路徑不同,先停下確認,不要為了完成AI 助理免費安裝服務綁卡。
- 「API key details」視窗出現你的金鑰——按旁邊的複製鈕,直接貼在十步安裝頁的第4步,僅留在當前頁面。
🔴 如果畫面出現付費方案、信用卡或 billing 選項,先停下確認。 那不是完成這項免費安裝服務的必要步驟;這項免費安裝服務全程免費,不需要信用卡,也不會替你啟用 billing。免費方案有使用量上限,超過時回報限制、暫停或等待恢復,不會自動升級 Paid Tier。
做完你應該看到:頁面上多了一把新的金鑰,旁邊是一長串字。那就是你的 API Key。 🖼 對照圖:安裝載體 START_HERE 步驟 4,點圖可放大比對。
這串字現在多半是
AQ.開頭(2026-08 實測),但那只是常見的樣子,不是規定。開頭跟這裡寫的不一樣,不代表你拿錯了,也不用因此重做一次。它到底能不能用,要等 04 真的傳一則訊息出去才知道。
(如果你想先看官方怎麼說,可以查 Gemini API 金鑰說明。)
Step 2:開一個 Google Apps Script 專案
「Google Apps Script」是 Google 提供的一個免費地方,可以放一小段程式,並且讓它有自己的網址。你不需要自己買主機。
- 用你自己的 Google 帳戶打開 script.google.com。
- 按「新增專案」。
- 左上角把專案名稱改成你認得的名字,例如「我的 LINE AI 回覆」。
做完你應該看到:一個編輯畫面,中間有一個叫 Code.gs 的檔案,裡面有幾行預設的程式。
Step 3:把程式碼貼進去
- 在中間的編輯區全選(Windows 按
Ctrl+A,Mac 按Cmd+A),按 Delete 全部刪掉。 - 把下面整段複製起來,貼進那個空白的編輯區。
- 按上方的儲存圖示(或
Ctrl+S/Cmd+S)。
🔴 要整段貼,不要只貼一部分。 從第一行
/**到最後一個}都要。
/** 愛智理- AI 助理免費安裝服務:LINE × Gemini 十步教學 */
function cfg_(name) {
return PropertiesService.getScriptProperties().getProperty(name);
}
/** 秘密只留在自己的指令碼屬性,不寫進程式碼或執行紀錄。 */
function importSetup_() {
var props = PropertiesService.getScriptProperties();
var raw = props.getProperty('SETUP_INPUT');
if (!raw) return;
var data;
try { data = JSON.parse(raw); } catch (_) { throw new Error('設定格式不完整:回第6步重新複製整份設定'); }
if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('設定格式錯誤');
var required = ['LINE_CHANNEL_ID', 'LINE_CHANNEL_SECRET', 'GEMINI_API_KEY', 'KNOWLEDGE'];
var allowed = required.concat(['PROMPT', 'GEMINI_MODEL']);
if (Object.keys(data).some(function(k) { return allowed.indexOf(k) < 0; })) throw new Error('設定含未知欄位');
required.forEach(function(k) {
if (typeof data[k] !== 'string' || !data[k].trim()) throw new Error('缺少設定:' + k);
});
if (!/^\d+$/.test(data.LINE_CHANNEL_ID.trim())) throw new Error('Channel ID 應只有數字');
var clean = {};
allowed.forEach(function(k) {
if (data[k] !== undefined && typeof data[k] !== 'string') throw new Error('設定型態錯誤:' + k);
clean[k] = (data[k] || '').trim();
});
props.setProperties(clean, false);
props.deleteProperty('SETUP_INPUT');
console.log('設定已匯入;暫存設定已清除');
}
/** 只快取到到期前60秒。快取遺失時重發,不沿用失效憑證。 */
function lineToken_() {
var id = cfg_('LINE_CHANNEL_ID');
var secret = cfg_('LINE_CHANNEL_SECRET');
if (!id || !secret) throw new Error('LINE 設定缺少 Channel ID 或 Channel secret');
var digest = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, id + '\n' + secret);
var key = 'fs_line_' + Utilities.base64EncodeWebSafe(digest);
var cache = CacheService.getScriptCache();
var token = cache.get(key);
if (token) return token;
var res;
try {
res = UrlFetchApp.fetch('https://api.line.me/oauth2/v3/token', {
method: 'post', contentType: 'application/x-www-form-urlencoded',
payload: { grant_type: 'client_credentials', client_id: id, client_secret: secret },
muteHttpExceptions: true
});
} catch (_) { throw new Error('LINE 權杖連線失敗'); }
if (res.getResponseCode() !== 200) throw new Error('LINE 權杖 HTTP ' + res.getResponseCode());
var data;
try { data = JSON.parse(res.getContentText()); } catch (_) { throw new Error('LINE 權杖回應格式錯誤'); }
if (!data.access_token || typeof data.access_token !== 'string' || !isFinite(data.expires_in) || Number(data.expires_in) <= 60)
throw new Error('LINE 權杖回應不完整');
cache.put(key, data.access_token, Math.min(840, Math.floor(Number(data.expires_in) - 60)));
return data.access_token;
}
function ok_() {
return ContentService.createTextOutput(JSON.stringify({ ok: true })).setMimeType(ContentService.MimeType.JSON);
}
function doPost(e) {
try {
if (!e || !e.postData || !e.postData.contents) return ok_();
var body;
try { body = JSON.parse(e.postData.contents); } catch (_) { throw new Error('Webhook 內容格式錯誤'); }
var events = body.events || [];
if (!Array.isArray(events)) throw new Error('Webhook events 格式錯誤');
events.forEach(function(ev) {
if (ev.type !== 'message' || !ev.message || ev.message.type !== 'text' || !ev.replyToken) return;
var answer;
try { answer = askGemini_(ev.message.text); }
catch (err) {
console.error('Gemini 失敗:' + err.message);
answer = '抱歉,我這邊暫時有點問題,請稍等一下由真人回覆你。';
}
replyToLine_(ev.replyToken, answer);
});
} catch (err) { console.error('doPost 失敗:' + err.message); }
return ok_();
}
function askGemini_(userText) {
var key = cfg_('GEMINI_API_KEY');
if (!key) throw new Error('Gemini 設定缺少 API Key');
var model = cfg_('GEMINI_MODEL') || 'gemini-3.5-flash-lite';
var persona = cfg_('PROMPT') || '你是這家店的客服助理。用繁體中文,簡短、有禮貌地回答。';
var facts = cfg_('KNOWLEDGE') || '';
var systemText = '【你可以依據的資料】\n' + (facts || '(目前沒有提供資料)') +
'\n\n【預設風格;下面你的身分可以修改風格】\n回答控制在三句話以內。\n' +
'\n【你的身分與說話方式】\n' + persona +
'\n\n【安全底線;請最優先遵守】\n' +
'1. 只根據上面的資料回答;資料裡沒有的,就說你不知道。\n' +
'2. 遇到價格變動、退款、客訴,或你不確定的事,回答「這部分我幫你轉給真人確認」。\n' +
'3. 不要承諾我們做不到的事。';
var res;
try {
res = UrlFetchApp.fetch('https://generativelanguage.googleapis.com/v1beta/models/' + encodeURIComponent(model) + ':generateContent', {
method: 'post', contentType: 'application/json', headers: { 'x-goog-api-key': key },
payload: JSON.stringify({systemInstruction: {parts: [{text: systemText}]},
contents: [{role: 'user', parts: [{text: userText}]}], generationConfig: {maxOutputTokens: 1024}}),
muteHttpExceptions: true
});
} catch (_) { throw new Error('Gemini 連線失敗'); }
if (res.getResponseCode() !== 200) throw new Error('Gemini HTTP ' + res.getResponseCode());
var data;
try { data = JSON.parse(res.getContentText()); } catch (_) { throw new Error('Gemini 回應格式錯誤'); }
var parts = data.candidates && data.candidates[0] && data.candidates[0].content && data.candidates[0].content.parts;
var out = parts && parts.filter(function(p) { return p.text && !p.thought; }).map(function(p) { return p.text; }).join('');
if (!out) throw new Error('Gemini 沒有文字回覆:請檢查模型與安全限制');
return out;
}
function replyToLine_(replyToken, text) {
var res;
var token = lineToken_();
try {
res = UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
method: 'post', contentType: 'application/json', headers: {Authorization: 'Bearer ' + token},
payload: JSON.stringify({replyToken: replyToken, messages: [{type: 'text', text: String(text).slice(0, 4900)}]}),
muteHttpExceptions: true
});
} catch (_) { throw new Error('LINE 回覆連線失敗'); }
if (res.getResponseCode() !== 200) throw new Error('LINE 回覆 HTTP ' + res.getResponseCode());
}
/** 先檢查設定,再驗證 LINE 與 Gemini;失敗即停,不印秘密或模型回覆。 */
function checkSetup() {
importSetup_();
['LINE_CHANNEL_ID', 'LINE_CHANNEL_SECRET', 'GEMINI_API_KEY', 'KNOWLEDGE'].forEach(function(k) {
if (!cfg_(k)) throw new Error('缺少設定:' + k + ',回第6步');
});
console.log('設定:完整');
var token = lineToken_();
var res;
try {
res = UrlFetchApp.fetch('https://api.line.me/v2/bot/info', {headers: {Authorization: 'Bearer ' + token}, muteHttpExceptions: true});
} catch (_) { throw new Error('LINE 帳號檢查連線失敗'); }
if (res.getResponseCode() !== 200) throw new Error('LINE 帳號檢查 HTTP ' + res.getResponseCode());
console.log('LINE:連線成功');
askGemini_('請問你們幾點開?');
console.log('Gemini:連線成功');
console.log('下一步:部署並用手機收第一個 AI 回覆;本檢查尚未驗證 Webhook 回程');
}
做完你應該看到:編輯區裡是上面這段內容,而且上方出現「已儲存」之類的提示。 🖼 對照圖:安裝載體 START_HERE 步驟 5。
Step 4:一次匯入設定
- 開十步安裝頁,第3、4步貼好 ID、secret、Gemini API Key。
- 第6步已填測試範例,會帶入小工作與真人界線。確認後複製設定名稱及整份設定。
- 回自己的 Apps Script 專案設定 → 指令碼屬性,新增一列
SETUP_INPUT,貼到值欄並儲存。
秘密不貼進程式碼;匯入成功後暫存列自動刪除。貼好後清除剪貼簿秘密。
Step 5:驗證 LINE 與 Gemini 連線
- 回編輯畫面選
checkSetup→ 執行,僅授權自己的專案。 - 依序看到「設定:完整」「LINE:連線成功」「Gemini:連線成功」;失敗先修指出的層級。
使用一次 Gemini 免費額度,不啟用付費;手機回程尚未驗證。跑通後在獨立屬性換成自己的 KNOWLEDGE,PROMPT 或 GEMINI_MODEL 可選填。改屬性不必重新部署,改程式必須重新部署。
Step 6:部署,拿到自己的網址
「部署」的意思是:讓這段程式有一個外面連得到的網址。
- 右上角按「部署」→「新增部署」。
- 視窗打開後,按「選取類型」旁邊的齒輪圖示,選「網頁應用程式」。
- 「執行身分」選我(你自己的帳戶)。
- 「誰可以存取」選所有人。
- 按「部署」。
🔴 第 3、4 兩點是兩格設定,一格都不能錯。 第 3 點「執行身分」要選的「我」,那格會顯示你自己的 email(例:我([email protected]))。絕對不要選「存取網頁應用程式的使用者」——選了它,後面每一關檢查都會過(
checkSetup全綠、手動執行全成功),但客人真的傳訊息進來時程式半秒內就失敗、而且不留任何錯誤訊息,你會完全查不出原因(2026-08 實測案例:改回「我」立刻就通)。 第 4 點一定要選「所有人」。這不是把你的資料公開——是讓 LINE 的伺服器連得到你這個網址。選錯的話 LINE 會連不上。 🖼 兩格正確時的對照圖:安裝載體 START_HERE 步驟 8。
做完你應該看到:一個以 https://script.google.com/macros/s/ 開頭、/exec 結尾的網址。按「複製」。
🔴 部署畫面上可能同時出現兩個網址,只有一個是對的:「網頁應用程式」底下、
/exec結尾的才是要用的;「資料庫」底下、網址裡有library字樣的不要拿——貼給 LINE 會直接連不通。口訣:給 LINE 的網址一定是/exec結尾;看到library=拿錯張了。 🖼 兩個網址並列的對照圖:安裝載體 START_HERE 步驟 8。
這就是 Step 7 要交給 LINE 的網址(自檢清單 對應步驟 8)。
🔴 以後每次改程式碼,都要再部署一次:「部署」→「管理部署作業」→ 按編輯(鉛筆)圖示 → 版本選「新版本」→ 按「部署」。只按儲存不會生效,網址不變,但內容不會更新。
Step 7:把網址交給 LINE,打開 Webhook
「Webhook」是一個約定:有人傳訊息給你的官方 LINE 時,LINE 就自動把訊息送到你指定的網址。你要告訴 LINE 的,就是 Step 6 那個網址。這一步全程在中文介面的 LINE Official Account Manager 完成,不需要回英文的 Console。
- 開 LINE Official Account Manager,選你的帳號 → 右上「設定」→ 左邊「Messaging API」(啟用 Messaging API 時用過的那一頁)。
- 把 Step 6 的網址貼進「Webhook網址」那格,按「儲存」。
- 左邊「回應設定」→ 把「Webhook」開關打開成綠色。
做完你應該看到:「回應設定」頁的 Webhook 開關是綠的。 🖼 對照圖:安裝載體 START_HERE 步驟 9。
同一頁的「自動回應訊息」「加入好友的歡迎訊息」「聊天」開不開由你決定——它們跟 AI 回覆不衝突(2026-08 實測)。只有一件事要注意:自動回應開著時,你測試收到的可能是它的罐頭回應、而不是 AI——測試時以回覆內容判斷;想讓客人只收到 AI 的回答,就把自動回應關掉。
這一章的成功判定
只要你確認:
- API Key 是你自己的,而且留在你自己的環境。
checkSetup執行後,三個必填屬性都顯示「已填」。- 你有一個
/exec結尾的網址。 - Manager 的「Webhook網址」填了那個網址,而且按過「儲存」。
- 「回應設定」的「Webhook」開關是綠色。
🔴 這幾項的意思是「動作都做完了」,不是「設定都是對的」。 每一項都只證明它自己:
checkSetup顯示「已填」,只代表那一格不是空的——貼錯的、過期的值一樣顯示「已填」。- 「Webhook」開關是綠的,只代表開關開了——不代表對面接得住。
這幾件事互相推不出對方。 唯一能證明整條路真的通的,是 04:你自己傳一則訊息,收到回覆。 在那之前,這一章都只是「做完了」,還不是「成功了」。
你不需要把 API Key 貼出來。
留下非秘密的設定紀錄
你可以記錄以下資料,但不要記錄 API Key 原文:
使用的模型供應商:Gemini
使用的執行環境:自己的 Google Apps Script
秘密保存位置:指令碼屬性
Webhook 開關狀態:
建立日期:
最後一次驗證日期:這一版做到哪裡,沒做到哪裡
誠實說明這段程式的邊界:
- 它看不到是誰傳的訊息,也不記得上一句話。每一則訊息都是獨立回答。
- 它只回文字。貼圖、照片、語音一律不回應。
- 你的網址是公開可連的。Google Apps Script 不讓程式讀取請求的標頭,所以這一版無法驗證訊息是不是真的來自 LINE。實際影響:別人若猜到你的網址,可以讓你多用一點 Gemini 額度,但沒辦法讓你的帳號去回覆你的客人——因為回覆需要 LINE 當下才會發出的一次性代碼。
- 免費額度有上限。用量大的時候會回不出來,這是額度問題不是設定問題。另一種回不出來是模型本身當下全球過載——不是你的用量造成的,處理方式見〈如果卡住〉。
- 它是一步一步做完才回覆:先問 Gemini,等它回答,再送回 LINE。所以回覆通常慢個一兩秒。LINE 官方建議這類程式改成「先答應、再處理」,那需要多一層設定,這一版刻意不做——多那一層會讓安裝變難,而這一版的目標是先讓你跑通一次。
- 你的官方帳號後台可能會出現
request_timeout之類的統計。只要你手機收得到回覆,那是統計數字,不是壞掉。 - 有兩個上限是程式寫死的,改
PROMPT沒有用:①每一則回覆的長度上限(程式碼裡的maxOutputTokens)②送去 LINE 的訊息太長會被截短(這是 LINE 平台自己的上限,不是我們加的)。 - 還有一條安全規則:「只根據
KNOWLEDGE裡的內容回答,沒寫的就說不知道」——這是為了不讓它自己編出價格和規則,我們刻意把它放在最後、要它最優先。🔴 但要跟你說實話:這一條是寫給 AI 的指示,不是程式鎖。你如果在PROMPT裡下一個正好相反的命令,它有可能就不照這條走了。所以請不要那樣寫。 - 「回答控制在三句話以內」只是預設,不是規定——你在
PROMPT裡叫它回答詳細一點,它通常就會照你的。
這些不是缺陷,是這一版刻意的範圍。
如果卡住
常見原因可能包括:
- 登入了不同的 Google 帳戶。
- API Key 建立在另一個專案。
- 指令碼屬性的名稱打錯字(大小寫也要一樣)。
- 改完程式碼只按了儲存,沒有重新部署。
- 「誰可以存取」沒有選「所有人」。
- 「執行身分」選成「存取網頁應用程式的使用者」——特徵:手動執行都成功,但真訊息進來時執行紀錄半秒內失敗、點進去沒有任何內容。改回「我」+新版本重新部署。
- 權限、配額或模型設定不一致。
- 模型當下全球過載:執行紀錄出現「
Gemini 回傳 503」加上「high demand」字樣,每則訊息拖到一分鐘以上——這不是你裝錯。
不要先重建整套 LINE。
「503/high demand」有現成解法:到 AI Studio 的模型選單挑另一顆 flash 系列模型,把它顯示的確切名稱填進指令碼屬性 GEMINI_MODEL(就是 Step 4 那張表的可選欄位;存了立刻生效,不用重新部署)。這一版預設使用 gemini-3.5-flash-lite;若當下塞車,再依 AI Studio 顯示的確切名稱切換。
請進入 05|AI Troubleshooter,從最後一個確定成功的步驟開始。
下一步
進入 04|看到第一個 AI 回覆。