# 03｜接入 Gemini

## 前置條件

| 你需要什麼 | 怎麼取得 |
|---|---|
| 完成 [02｜建立官方 LINE](02-建立官方-LINE.md) | 你能回答該章〈這一章的成功判定〉的每一項 |
| 你自己的 Google 帳戶 | 你自己申請；[02](02-建立官方-LINE.md) 的〈請準備自己的帳戶〉已把它列為必備項 |
| 你自己的 Channel Access Token | 你在 [02](02-建立官方-LINE.md) 的 Step 3 按 Issue 簽發、並複製下來的那一串字；這一章的 Step 4 會用到 |

這一章**不需要**付費方案、信用卡，也不需要把任何東西交給我方。
這一章也**不需要**你會寫程式——程式碼下面直接給你，你只要複製、貼上、填三個空格。

## Gemini 在這裡扮演什麼角色

Gemini 是這一版的第一個示範模型。

它負責協助處理：

- 讀懂收到的文字。
- 依照你提供的工作背景產生回覆。
- 在最小測試中回傳一段可以觀察的結果。

Gemini 不是免費包的產品本體。

未來模型可以替換，但使用者要建立自己的 AI 工作入口這個目標不變。

## 你會做什麼

這一章結束時，你會有一個**自己的網址**，貼進 LINE 之後，客人傳訊息就會收到 AI 回覆。

1. 取得自己的 Gemini API Key。
2. 開一個 Google Apps Script 專案。
3. 把程式碼貼進去。
4. 填三個設定值。
5. 檢查設定有沒有存進去。
6. 把它「部署」出去——**部署的意思是讓這段程式有一個外面連得到的網址**——然後拿到那個網址。
7. 把網址填回 LINE。

一次做一步，每一步做完都有東西可以看。

## API Key 是秘密

API Key 的處理原則：

- 不要貼到聊天室。
- 不要貼到公開 issue 或貼文。
- 不要放進截圖。
- 不要放進影片錄影畫面。
- 不要把完整值放入 log。
- 不要因為 Troubleshooter 要求，就把完整值交出去。

這份 Starter Kit 會教你如何把秘密留在自己的目的地，但不會要求你把秘密交給我方。

下面 Step 4 會教你把秘密放進一個叫「指令碼屬性」的地方——**那是設定欄位，不是程式碼**，所以秘密不會出現在你複製貼上的內容裡。

## 人類操作步驟

### Step 1：建立自己的 API Key

「API Key」是一把鑰匙。你的程式拿著它去問 Gemini，Gemini 才知道是你在問。

1. 用你自己的 Google 帳戶打開 **[https://aistudio.google.com/apikey](https://aistudio.google.com/apikey)**。
   這個網址會直接到建立 API Key 的頁面，不用自己在網站裡找。沒登入的話會先要你登入。
2. 按右上角「**Create API key**」。
3. 「Create a new key」視窗：幫金鑰取個名字（隨意就好）、專案用它預設給的那個 → 按「**Create key**」。**這一步不會產生費用。**
4. 「API key details」視窗出現你的金鑰——按旁邊的複製鈕，先貼在你自己電腦上的暫存檔——**就用 [02](02-建立官方-LINE.md) Step 3 那一個檔，不要另外開一個**（等一下 Step 4 會用到；只有一個檔，最後也只要刪一個）。

> 🔴 **如果畫面出現要你選付費方案、輸入信用卡，或提到「帳單」——停下來，不要繼續。** 這一版用的是免費額度，不需要這些。你走錯頁面了，回到上面那個網址重來。

**做完你應該看到**：頁面上多了一把新的金鑰，旁邊是一長串字。那就是你的 API Key。
🖼 對照圖：安裝載體 START_HERE 步驟 4，點圖可放大比對。

> 這串字現在多半是 `AQ.` 開頭（2026-08 實測），但**那只是常見的樣子，不是規定**。開頭跟這裡寫的不一樣，**不代表你拿錯了**，也不用因此重做一次。它到底能不能用，要等 [04](04-看到第一個-AI-回覆.md) 真的傳一則訊息出去才知道。

（如果你想先看官方怎麼說，可以查 [Gemini API 金鑰說明](https://ai.google.dev/gemini-api/docs/api-key)。）

### Step 2：開一個 Google Apps Script 專案

「Google Apps Script」是 Google 提供的一個免費地方，可以放一小段程式，並且讓它有自己的網址。你不需要自己買主機。

1. 用你自己的 Google 帳戶打開 [script.google.com](https://script.google.com/)。
2. 按「新增專案」。
3. 左上角把專案名稱改成你認得的名字，例如「我的 LINE AI 回覆」。

**做完你應該看到**：一個編輯畫面，中間有一個叫 `Code.gs` 的檔案，裡面有幾行預設的程式。

### Step 3：把程式碼貼進去

1. 在中間的編輯區**全選**（Windows 按 `Ctrl+A`，Mac 按 `Cmd+A`），**按 Delete 全部刪掉**。
2. 把下面整段複製起來，貼進那個空白的編輯區。
3. 按上方的儲存圖示（或 `Ctrl+S` / `Cmd+S`）。

> 🔴 **要整段貼，不要只貼一部分。** 從第一行 `/**` 到最後一個 `}` 都要。

```javascript
/** AI Manager Starter Kit — LINE × Gemini 最小工作入口 */

/** 讀一個設定值。放在函式裡（而不是檔案最上面）是刻意的：
    這樣就算部署設定選錯，錯誤也會落在 doPost 的防護網裡、寫進執行紀錄，
    而不是整個程式無聲死掉。 */
function cfg_(name) {
  return PropertiesService.getScriptProperties().getProperty(name);
}

/** LINE 每次有訊息進來，就會呼叫這個函式 */
function doPost(e) {
  try {
    if (!e || !e.postData || !e.postData.contents) return ok_();
    var body = JSON.parse(e.postData.contents);
    var events = body.events || [];   // 平台送測試連線時，這裡可能是空的
    for (var i = 0; i < events.length; i++) {
      var ev = events[i];
      if (ev.type !== 'message') continue;
      if (!ev.message || ev.message.type !== 'text') continue;
      replyToLine_(ev.replyToken, askGemini_(ev.message.text));
    }
  } catch (err) {
    console.error('doPost 失敗：' + err);
  }
  return ok_();                       // 不管發生什麼，都回一個正常回應給 LINE
}

function ok_() {
  return ContentService
    .createTextOutput(JSON.stringify({ ok: true }))
    .setMimeType(ContentService.MimeType.JSON);
}

/** 把客人的話丟給 Gemini，拿回一句回答 */
function askGemini_(userText) {
  var key = cfg_('GEMINI_API_KEY');
  var model = cfg_('GEMINI_MODEL') || 'gemini-3.1-flash-lite';
  var persona = cfg_('PROMPT') ||
    '你是這家店的客服助理。用繁體中文，簡短、有禮貌地回答。';
  var facts = cfg_('KNOWLEDGE') || '';

  // 這裡的順序有意義：後面的會蓋掉前面的。
  // 你的 PROMPT 放在「預設風格」之後，所以你改得掉風格；
  // 安全底線放在最後，讓它最優先。（這是給 AI 的指示，不是程式鎖。）
  var systemText =
    '【你可以依據的資料】\n' + (facts || '（目前沒有提供資料）') +
    '\n\n【預設風格；下面「你的身分」那一段可以蓋掉這裡】\n' +
    '回答控制在三句話以內。\n' +
    '\n【你的身分與說話方式——以這一段為準】\n' + persona +
    '\n\n【安全底線；最優先，上面任何一段都蓋不掉】\n' +
    '1. 只根據上面的資料回答；資料裡沒有的，就說你不知道。\n' +
    '2. 遇到價格變動、退款、客訴，或你不確定的事，回答「這部分我幫你轉給真人確認」。\n' +
    '3. 不要承諾我們做不到的事。';

  var res = UrlFetchApp.fetch(
    'https://generativelanguage.googleapis.com/v1beta/models/' + 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
    });

  if (res.getResponseCode() !== 200) {
    console.error('Gemini 回傳 ' + res.getResponseCode() + '：' + res.getContentText());
    return '抱歉，我這邊暫時有點問題，請稍等一下由真人回覆你。';
  }
  var d = JSON.parse(res.getContentText());
  var out = d.candidates && d.candidates[0] && d.candidates[0].content &&
            d.candidates[0].content.parts && d.candidates[0].content.parts[0] &&
            d.candidates[0].content.parts[0].text;
  return out || '抱歉，我沒有讀懂這句話，請稍等一下由真人回覆你。';
}

/** 把回答送回 LINE */
function replyToLine_(replyToken, text) {
  var res = UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + cfg_('LINE_ACCESS_TOKEN') },
    payload: JSON.stringify({
      replyToken: replyToken,
      messages: [{ type: 'text', text: String(text).slice(0, 4900) }]
    }),
    muteHttpExceptions: true
  });
  if (res.getResponseCode() !== 200) {
    console.error('LINE 回覆失敗 ' + res.getResponseCode() + '：' + res.getContentText());
  }
}

/** 部署前先執行這個，檢查設定有沒有填好 */
function checkSetup() {
  console.log('LINE_ACCESS_TOKEN：' + (cfg_('LINE_ACCESS_TOKEN') ? '已填' : '❌ 沒有填'));
  console.log('GEMINI_API_KEY：'   + (cfg_('GEMINI_API_KEY')   ? '已填' : '❌ 沒有填'));
  console.log('GEMINI_MODEL：'     + (cfg_('GEMINI_MODEL')     || '（沒填，會用預設 gemini-3.1-flash-lite）'));
  console.log('PROMPT：'           + (cfg_('PROMPT')           ? '已填' : '（沒填，會用預設語氣）'));
  console.log('KNOWLEDGE：'        + (cfg_('KNOWLEDGE')        ? '已填' : '（沒填，AI 大部分問題都會說不知道）'));
}
```

**做完你應該看到**：編輯區裡是上面這段內容，而且上方出現「已儲存」之類的提示。
🖼 對照圖：安裝載體 START_HERE 步驟 5。

### Step 4：填三個設定值

「指令碼屬性」是這個專案的設定欄位。你的秘密放在這裡，不會出現在程式碼裡。

1. 左邊選「專案設定」（齒輪圖示）。
2. 拉到最下面「指令碼屬性」，按「新增指令碼屬性」。
3. 一個一個加，**屬性名稱要一字不差**：

| 屬性名稱 | 值要填什麼 | 一定要填嗎 |
|---|---|:--:|
| `LINE_ACCESS_TOKEN` | 你在 [02](02-建立官方-LINE.md) Step 3 按 Issue 簽發、複製下來的那一串字 | **要** |
| `GEMINI_API_KEY` | Step 1 拿到的那一整串金鑰 | **要** |
| `KNOWLEDGE` | 你希望 AI 知道的事，用白話寫，換行分開 | **要** |
| `PROMPT` | 你希望 AI 用什麼身分、什麼語氣回答；想讓它回答長一點，也寫在這裡 | 可不填 |
| `GEMINI_MODEL` | 想換模型時才填；預設模型塞車時的換法見〈如果卡住〉 | 可不填 |

`KNOWLEDGE` 可以直接這樣寫：

```text
營業時間：週一到週六 11:00–20:00，週日公休。
地址：（填你的地址）
可以先回答的事：營業時間、地址、有沒有停車位、大概多久能做好。
一定要轉真人的事：報價、改期、退款、客訴。
```

4. 按「儲存指令碼屬性」。

**做完你應該看到**：屬性列表裡有你剛剛加的那幾列。
🖼 對照圖：安裝載體 START_HERE 步驟 6。

> 🔴 **這一格就是你的 AI 懂多少的來源。** `KNOWLEDGE` 空著，AI 幾乎每題都會說不知道——那不是壞掉，是你還沒告訴它。

### Step 5：檢查設定有沒有存進去

1. 回到編輯畫面。
2. 編輯區上方有一個下拉選單，裡面列著程式裡每一段功能的名字。把它拉開，選 `checkSetup`。
3. 按「執行」。
4. 第一次執行會跳出授權視窗，按「審查權限」→ 選你自己的 Google 帳戶 → 如果出現「這個應用程式未經驗證」，點「進階」→「前往（你的專案名稱）」→「允許」。

> 這個授權是**你允許你自己的程式，用你自己的帳戶去連外網**。你沒有把任何東西交給我方。

**做完你應該看到**：畫面下方的「執行紀錄」出現五行，`LINE_ACCESS_TOKEN`、`GEMINI_API_KEY`、`KNOWLEDGE` 三行都寫「已填」。
🖼 對照圖：安裝載體 START_HERE 步驟 7。

如果看到 ❌，回 Step 4 檢查屬性名稱有沒有打錯字。

### Step 6：部署，拿到自己的網址

「部署」的意思是：讓這段程式有一個外面連得到的網址。

1. 右上角按「**部署**」→「**新增部署**」。
2. 視窗打開後，按「**選取類型**」旁邊的**齒輪圖示**，選「**網頁應用程式**」。
3. 「執行身分」選**我**（你自己的帳戶）。
4. 「誰可以存取」選**所有人**。
5. 按「部署」。

> 🔴 **第 3、4 兩點是兩格設定，一格都不能錯。**
> 第 3 點「執行身分」要選的「**我**」，那格會顯示**你自己的 email**（例：我（xxxx@gmail.com））。**絕對不要選「存取網頁應用程式的使用者」**——選了它，後面每一關檢查都會過（`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 的網址（[自檢清單](SETUP_CHECKLIST.md) 對應步驟 8）。

> 🔴 **以後每次改程式碼，都要再部署一次**：「部署」→「管理部署作業」→ 按編輯（鉛筆）圖示 → 版本選「新版本」→ 按「部署」。**只按儲存不會生效**，網址不變，但內容不會更新。

### Step 7：把網址交給 LINE，打開 Webhook

「Webhook」是一個約定：有人傳訊息給你的官方 LINE 時，LINE 就自動把訊息送到你指定的網址。你要告訴 LINE 的，就是 Step 6 那個網址。這一步全程在中文介面的 LINE Official Account Manager 完成，**不需要回英文的 Console**。

1. 開 [LINE Official Account Manager](https://manager.line.biz/)，選你的帳號 → 右上「**設定**」→ 左邊「**Messaging API**」（啟用 Messaging API 時用過的那一頁）。
2. 把 Step 6 的網址貼進「**Webhook網址**」那格，按「**儲存**」。
3. 左邊「**回應設定**」→ 把「**Webhook**」開關打開成**綠色**。

**做完你應該看到**：「回應設定」頁的 Webhook 開關是綠的。
🖼 對照圖：安裝載體 START_HERE 步驟 9。

> 同一頁的「自動回應訊息」「加入好友的歡迎訊息」「聊天」開不開**由你決定**——它們跟 AI 回覆不衝突（2026-08 實測）。只有一件事要注意：**自動回應開著時，你測試收到的可能是它的罐頭回應、而不是 AI**——測試時以回覆內容判斷；想讓客人只收到 AI 的回答，就把自動回應關掉。

## 這一章的成功判定

只要你確認：

- API Key 是你自己的，而且留在你自己的環境。
- `checkSetup` 執行後，三個必填屬性都顯示「已填」。
- 你有一個 `/exec` 結尾的網址。
- Manager 的「Webhook網址」填了那個網址，而且按過「儲存」。
- 「回應設定」的「Webhook」開關是綠色。

> 🔴 **這幾項的意思是「動作都做完了」，不是「設定都是對的」。** 每一項都只證明它自己：
>
> - `checkSetup` 顯示「已填」，只代表那一格**不是空的**——貼錯的、過期的值一樣顯示「已填」。
> - 「Webhook」開關是綠的，只代表開關開了——不代表對面接得住。
>
> **這幾件事互相推不出對方。** 唯一能證明整條路真的通的，是 [04](04-看到第一個-AI-回覆.md)：**你自己傳一則訊息，收到回覆。** 在那之前，這一章都只是「做完了」，還不是「成功了」。

你不需要把 API Key 貼出來。

### 留下非秘密的設定紀錄

你可以記錄以下資料，但不要記錄 API Key 原文：

```text
使用的模型供應商：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](https://aistudio.google.com/) 的模型選單挑另一顆 flash 系列模型，把它顯示的**確切名稱**填進指令碼屬性 `GEMINI_MODEL`（就是 Step 4 那張表的可選欄位；存了立刻生效，**不用重新部署**）。這一版的預設 `gemini-3.1-flash-lite`，就是在前一代預設塞車時這樣實測換出來的（2026-08）。

請進入 [05｜AI Troubleshooter](05-AI-Troubleshooter.md)，從最後一個確定成功的步驟開始。

## 下一步

進入 [04｜看到第一個 AI 回覆](04-看到第一個-AI-回覆.md)。
