用 GAS + LINE Bot 把「隨手想法」自動丟進 Notion
完全不會寫程式也能做完這篇。 唯一要做的是「複製貼上一段程式碼」,就像你在 Word 貼文字一樣。跟著五步驟走,30 分鐘設定好,之後不用再碰。
為什麼要做這個
三種人會用得上:
- 想到一個點子但 Apple 備忘錄 / Google Keep 越開越雜,最後等於沒記
- 用 Notion 做知識庫但每次要打開 app、找到 database、新增、輸入,麻煩到不想記
- 想要「想到 → 一句話 → 進系統」就好,不要再開 app 翻 database
LINE 你本來就一直開著、傳訊息順手。Notion 有 API、GAS(Google Apps Script,就是 Google 幫你免費跑的小程式)又能當中間橋梁,黏起來就是這篇做的事。
整體架構
你的手機 LINE
↓ 傳訊息給「自己建的官方帳號」
LINE Messaging API
↓ Webhook POST
Google Apps Script (Web App)
↓ 呼叫 Notion API
Notion Database
三個環節個人用都免費,沒 SaaS 月費、沒主機要顧。設定完一次,幾年不用碰。
第一步:設定 Notion 資料庫
1. 建立 Database 並準備 4 個欄位
| 欄位名稱 | 類型 | 用途 |
|---|---|---|
| 訊息內容 | Title | 預設標題欄,存 LINE 訊息原文 |
| 建立時間 | Date | 寫入時間 |
| 分類 | Select | 例如:工作 / 生活 / 靈感 / 待辦 |
| 狀態 | Status | 例如:待處理 / 已完成 |
2. 複製 Database ID
打開你的 database 頁面,網址長這樣:
https://www.notion.so/2d742f1e4f33810ab51bd78dc1975fff?v=2
└─────── Database ID ───────┘
notion.so/ 之後到 ?v= 之前那串 32 位英數字就是 Database ID。
3. 建立 Integration
- 前往 https://www.notion.so/profile/integrations
- 點 New integration → 取個名字(例如
LINE-to-Notion)→ 儲存 - 複製
ntn_開頭的 Internal Integration Token
4. 把 Integration 加入你的 Database
這步最容易漏,沒做的話 API 會回 404。
- 回到你的 Notion database 頁面
- 右上角
⋯→ Connections - 加入你剛剛建立的 Integration
第二步:設定 LINE 官方帳號
1. 建 Provider 和 Channel
- 前往 LINE Developers Console
- Create a new provider(隨便取名)
- 在 provider 底下 → Create a Messaging API channel
- 填基本資料 → 建立
2. 啟用 Messaging API
- 前往 LINE Official Account Manager
- 選擇剛建立的官方帳號
- 設定 → Messaging API → 啟用 Messaging API
3. 關掉自動回覆(不然每則都會回機器人預設訊息,超吵)
- 同一個 Manager 介面
- 主頁 → 自動回應訊息 → 全部關掉
4. 拿兩個密鑰
回到 LINE Developers Console → 你的 channel:
- Basic settings 分頁 → Channel secret(複製)
- Messaging API 分頁 → Channel access token → 點 Issue 產生 → 複製
第三步:開 Google Sheet 貼 GAS 程式碼
開一個全新的 Google Sheet(內容會被 init 函式自動填,先不用做任何事)。
1. 進 Apps Script 貼程式碼
Sheet 上方選單 → 擴充功能 → Apps Script → 把預設的 Code.gs 內容全部刪掉,貼下面這份。
不懂程式沒關係,這段 code 不需要看懂,只要完整複製貼上就好。就像你在 Word 貼一段文字,貼完之後按存檔,系統就會自己跑。
原作者是 麥哥(maigo.tom),v1.3.0 開源版本,已實戰過。
/**
* LINE to Notion 自動化串接
*
* 作者:maigo.tom
* 版本:1.3.0
*/
const SETUP_SHEET_NAME = 'Setup';
const ERROR_LOG_SHEET_NAME = 'Error Log';
const NOTION_API_VERSION = '2022-06-28';
const NOTION_API_ENDPOINT = 'https://api.notion.com/v1/pages';
const LINE_REPLY_API = 'https://api.line.me/v2/bot/message/reply';
const NOTION_TITLE_MAX_LENGTH = 2000;
const ERROR_LOG_MAX_ROWS = 1000;
// ============================================
// 初始化
// ============================================
function initializeSheet() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
initializeSetupSheet(spreadsheet);
initializeErrorLogSheet(spreadsheet);
try {
SpreadsheetApp.getUi().alert(
'初始化完成!\n\n請在 Setup 分頁的 B 欄填入 4 個 token / id。\n\nNotion Database 需要這 4 個欄位:\n- 訊息內容 (Title)\n- 建立時間 (Date)\n- 分類 (Select)\n- 狀態 (Status)'
);
} catch (e) { Logger.log('初始化完成'); }
}
function initializeSetupSheet(spreadsheet) {
let setupSheet = spreadsheet.getSheetByName(SETUP_SHEET_NAME);
if (!setupSheet) setupSheet = spreadsheet.insertSheet(SETUP_SHEET_NAME);
const labels = [
['LINE Channel Access Token', ''],
['LINE Channel Secret', ''],
['Notion Integration Token', ''],
['Notion Database ID', ''],
['允許的 LINE User ID', '']
];
setupSheet.getRange('A1:B5').setValues(labels);
setupSheet.getRange('A1:A5').setFontWeight('bold').setBackground('#f3f3f3');
setupSheet.getRange('B1:B5').setBackground('#fff9c4');
setupSheet.setColumnWidth(1, 250);
setupSheet.setColumnWidth(2, 400);
}
function initializeErrorLogSheet(spreadsheet) {
let errorSheet = spreadsheet.getSheetByName(ERROR_LOG_SHEET_NAME);
if (!errorSheet) errorSheet = spreadsheet.insertSheet(ERROR_LOG_SHEET_NAME);
errorSheet.getRange('A1:D1').setValues([['時間', '錯誤類型', '訊息內容', '錯誤詳情']]);
errorSheet.getRange('A1:D1').setFontWeight('bold').setBackground('#e3f2fd');
errorSheet.setColumnWidth(1, 180);
errorSheet.setColumnWidth(2, 120);
errorSheet.setColumnWidth(3, 300);
errorSheet.setColumnWidth(4, 400);
}
// ============================================
// 設定讀取
// ============================================
function getConfig() {
const setupSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SETUP_SHEET_NAME);
if (!setupSheet) throw new Error('找不到 Setup 分頁,請先執行 initializeSheet()');
const values = setupSheet.getRange('B1:B5').getValues();
const allowedRaw = values[4][0] ? values[4][0].toString().trim() : '';
const config = {
lineChannelAccessToken: values[0][0] ? values[0][0].toString().trim() : '',
lineChannelSecret: values[1][0] ? values[1][0].toString().trim() : '',
notionToken: values[2][0] ? values[2][0].toString().trim() : '',
notionDatabaseId: values[3][0] ? values[3][0].toString().trim() : '',
allowedUserIds: allowedRaw
? allowedRaw.split(',').map(id => id.trim()).filter(id => id)
: []
};
const required = [
{ key: 'lineChannelAccessToken', label: 'LINE Channel Access Token' },
{ key: 'lineChannelSecret', label: 'LINE Channel Secret' },
{ key: 'notionToken', label: 'Notion Integration Token' },
{ key: 'notionDatabaseId', label: 'Notion Database ID' }
];
const missing = required.filter(f => !config[f.key]).map(f => f.label);
if (missing.length > 0) throw new Error('設定缺失:' + missing.join(', '));
return config;
}
// ============================================
// Notion / LINE
// ============================================
function truncateText(text, maxLength) {
if (!text || text.length <= maxLength) return text || '';
const suffix = '... (訊息過長已截斷)';
return text.substring(0, maxLength - suffix.length) + suffix;
}
function replyToLine(replyToken, message, accessToken) {
if (!replyToken || replyToken === '00000000000000000000000000000000') return;
try {
UrlFetchApp.fetch(LINE_REPLY_API, {
method: 'post',
contentType: 'application/json',
headers: { 'Authorization': 'Bearer ' + accessToken },
payload: JSON.stringify({ replyToken, messages: [{ type: 'text', text: message }] }),
muteHttpExceptions: true
});
} catch (e) { console.log('LINE 回覆失敗:' + e.message); }
}
function saveToNotion(messageData, config) {
const createdTime = new Date(messageData.timestamp).toISOString();
const truncated = truncateText(messageData.text, NOTION_TITLE_MAX_LENGTH);
const payload = {
parent: { database_id: config.notionDatabaseId },
properties: {
'訊息內容': { title: [{ text: { content: truncated } }] },
'建立時間': { date: { start: createdTime } },
'分類': { select: { name: '生活' } },
'狀態': { status: { name: '待處理' } }
}
};
const response = UrlFetchApp.fetch(NOTION_API_ENDPOINT, {
method: 'post',
contentType: 'application/json',
headers: {
'Authorization': 'Bearer ' + config.notionToken,
'Notion-Version': NOTION_API_VERSION
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const code = response.getResponseCode();
const body = JSON.parse(response.getContentText());
if (code === 200) return body;
if (code === 401) throw new Error('Notion 認證失敗:請檢查 Integration Token');
if (code === 404) throw new Error('Notion Database 不存在或無權限:請確認 ID 正確、Integration 已連結');
if (code === 429) throw new Error('Notion API 請求過於頻繁,請稍後再試');
throw new Error('Notion API 錯誤 (' + code + '): ' + (body.message || '未知錯誤'));
}
// ============================================
// Error Log
// ============================================
function logError(errorType, messageContent, errorDetail) {
try {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let errorSheet = spreadsheet.getSheetByName(ERROR_LOG_SHEET_NAME);
if (!errorSheet) {
initializeErrorLogSheet(spreadsheet);
errorSheet = spreadsheet.getSheetByName(ERROR_LOG_SHEET_NAME);
}
const ts = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss');
errorSheet.appendRow([
ts, errorType,
(messageContent || '').toString().substring(0, 100),
(errorDetail || '').toString().substring(0, 500)
]);
const lastRow = errorSheet.getLastRow();
if (lastRow > ERROR_LOG_MAX_ROWS + 1) {
errorSheet.deleteRows(2, lastRow - ERROR_LOG_MAX_ROWS - 1);
}
} catch (e) { console.error('記錄錯誤失敗:', e.message); }
}
// ============================================
// Webhook 主函式
// ============================================
function doPost(e) {
const ok = ContentService.createTextOutput(JSON.stringify({ status: 'ok' }))
.setMimeType(ContentService.MimeType.JSON);
try {
if (!e || !e.postData || !e.postData.contents) {
logError('請求錯誤', '', '無效的請求內容');
return ok;
}
let config;
try { config = getConfig(); }
catch (err) { logError('設定錯誤', '', err.message); return ok; }
let payload;
try { payload = JSON.parse(e.postData.contents); }
catch (err) { logError('JSON 解析錯誤', '', err.message); return ok; }
if (payload.events && Array.isArray(payload.events)) {
for (const event of payload.events) processEvent(event, config);
}
} catch (error) {
logError('系統錯誤', '', error.message);
}
return ok;
}
function processEvent(event, config) {
if (!event || event.type !== 'message' || !event.message || event.message.type !== 'text') return;
if (!event.source || event.source.type !== 'user') return;
const userId = event.source.userId || '';
if (config.allowedUserIds.length > 0 && !config.allowedUserIds.includes(userId)) return;
const messageText = event.message.text || '';
if (messageText.trim() === '我的ID') {
if (event.replyToken) {
replyToLine(
event.replyToken,
'你的 LINE User ID 是:\n' + userId +
'\n\n請複製到 Setup 分頁的 B5(允許的 LINE User ID)',
config.lineChannelAccessToken
);
}
return;
}
if (!messageText.trim()) return;
const messageData = {
text: messageText,
userId: userId || 'unknown',
timestamp: event.timestamp || Date.now()
};
try {
saveToNotion(messageData, config);
if (event.replyToken) {
replyToLine(event.replyToken, '已記錄到 Notion', config.lineChannelAccessToken);
}
} catch (error) {
logError('Notion 寫入失敗', messageData.text.substring(0, 50), error.message);
if (event.replyToken) {
replyToLine(event.replyToken, '記錄失敗,請稍後再試', config.lineChannelAccessToken);
}
}
}
function doGet(e) {
return ContentService.createTextOutput(JSON.stringify({
status: 'ok',
message: 'LINE to Notion Webhook is running',
version: '1.3.0',
timestamp: new Date().toISOString()
})).setMimeType(ContentService.MimeType.JSON);
}
// ============================================
// 測試函式
// ============================================
function testConfig() {
try { getConfig(); SpreadsheetApp.getUi().alert('設定讀取成功,所有必要欄位都填了。'); }
catch (error) { SpreadsheetApp.getUi().alert('設定讀取失敗:' + error.message); }
}
function testNotionConnection() {
try {
const config = getConfig();
saveToNotion({
text: '測試訊息 - ' + new Date().toLocaleString('zh-TW'),
userId: 'TEST_USER',
timestamp: Date.now()
}, config);
SpreadsheetApp.getUi().alert('Notion 連線測試成功!請至 Notion Database 確認。');
} catch (error) {
SpreadsheetApp.getUi().alert('Notion 連線測試失敗:' + error.message);
}
}
完整版(含 cleanup helper、更詳細註解)在 這份 GSheet 的 Apps Script。上面是精華版,能跑。
2. 跑 initializeSheet 自動建分頁
存檔(⌘S)→ 上方下拉選 initializeSheet → 點 執行。
第一次會跳授權,授完後 Sheet 會自動多兩個分頁:Setup 和 Error Log。
3. 填 Setup 分頁
回 Sheet → Setup 分頁 → B 欄填:
| 欄位 | 值 |
|---|---|
| B1 | LINE Channel Access Token |
| B2 | LINE Channel Secret |
| B3 | Notion Integration Token(ntn_ 開頭) |
| B4 | Notion Database ID |
| B5 | 允許的 LINE User ID(選填,先空著) |
注意:貼上時前後不要有空白,最常見的踩雷點。
4. 跑兩個測試函式驗證
回 Apps Script → 跑 testConfig → 應該跳「設定讀取成功」。
再跑 testNotionConnection → 應該跳「Notion 連線測試成功」+ Notion 資料庫多一筆「測試訊息」。
兩個都過才繼續,紅了照訊息回頭檢查 Setup 設定 / Notion Integration 是否有加進 database。
第四步:部署為 Web App
- Apps Script 介面 → 右上 部署 → 新增部署作業
- 類型選 網頁應用程式
- 設定:
- 執行身分:我
- 存取權:所有人
- 點 部署
第一次部署會跳警告
Google 會說「未經驗證的應用程式」,照下面做:
- 授予資料存取權
- 跳到警示頁 → 左下角 Advanced
- 點 Go to (your project name) (unsafe)
- 勾 Select all → Continue
- 完成後複製產生的網址(這是你的 Webhook URL)
程式碼是你自己貼的(不是惡意),警告是 Google 對未驗證 OAuth 的標準提示,可以忽略。
第五步:把 Webhook 接回 LINE
- 回 LINE Developers Console → 你的 channel → Messaging API 分頁
- Webhook URL 貼上剛剛複製的網址
- 打開 Use webhook 開關
- 點 Verify 測試
顯示 Success 就完成了。
測試
加你的官方帳號為 LINE 好友 → 傳一句話 → 打開 Notion database → 應該秒看到那句話新增進去。
進階:只允許自己的訊息進 Notion
預設下任何加你官方帳號的人,傳訊息都會進你的 Notion。要鎖只有自己能用:
- 用 LINE 傳「我的ID」給你的官方帳號(GAS 範本支援這個指令)
- LINE 會回覆你的 User ID
- 把這個 ID 貼到 Sheet 的
B5 - 之後只有這個 User ID 的訊息會被處理,其他人傳的會被忽略
常見問題
訊息沒進 Notion?
- 看 Sheet 的
Error Log分頁有沒有錯誤 - 檢查 4 個設定值都對(特別是前後空白)
- 確認 Integration 已加進 database 的 Connections
LINE Verify 失敗? 重新部署:部署 → 管理部署作業 → 編輯 → 建立新版本 → 部署。
改完程式碼沒效果? GAS 跟一般網站不一樣,改完每次都要重新部署才會吃到新版本。新手最常卡這裡。
完整範本下載
完整 GAS 程式碼 + Google Sheet 範本連結在這邊:直接複製這份 GSheet,裡面的 Apps Script 已經貼好了,照 Setup 分頁的說明填入 4 個設定值就能跑。
想做更複雜的自動化
LINE → Notion 是最簡單的入門款。同樣的 webhook + GAS pattern 可以擴出去:
- LINE → Google Calendar 建行程
- LINE → Trello / Asana 建任務
- LINE 傳照片 → Google Drive 自動分類存檔
- LINE 傳語音 → AI 轉文字 → Notion 自動分類
如果你的事業有「重複手工想變自動」的場景,寫信給我聊 15 分鐘,看 fit 不 fit。
常見問題
- 完全免費嗎?
- 是。GAS 每天有 6 分鐘執行配額(免費版),LINE 官方帳號免費版每月 200 則訊息可寄出,Notion API 個人用戶完全免費。對個人用途綽綽有餘。
- 訊息會延遲多久進到 Notion?
- GAS 接到 LINE webhook 通常 1-3 秒內就寫入 Notion。冷啟動偶爾會慢到 5-10 秒,但體感像即時。
- 別人用我的 LINE 官方帳號傳訊息會不會也進我的 Notion?
- 預設會。教學最後有「只允許特定 User ID」的設定步驟,可以鎖定只有你的訊息會處理,其他人傳的會被忽略。
數位轉型/創新/自動化與 AI 愛好者