部落格

不定期分享最新資訊文章

  • article-如何打造 Google 表單作業繳交與自動寄信催繳系統? | (EP.4) n8n 自動化講師應用教學

    2026/5/4

    AI自動化 n8n 自動化講師應用
    如何打造 Google 表單作業繳交與自動寄信催繳系統? | (EP.4) n8n 自動化講師應用教學
    每次批改作業前,你是否還在手動打開試算表,一行一行比對學生名單?這種「人工核對」方式不只費時,更容易出現遺漏。這篇教學,我們將實作一套完整的 n8n 自動化系統,讓表單填交、狀態判斷到催繳通知全程零人工介入。 這套系統的核心概念是:「資料收集 → 標準化 → 期限比對 → 自動記錄」。只要學生一送出表單,整個流程就自動啟動,你只需要定期查看試算表即可。接下來,我們拆解 Flow A:Google Form 提交作業 → 寫入 submissions 的每一個節點。 整體流程架構這個自動化系統由兩個工作流組成: 工作流 功能 Flow A(本篇) Google 表單提交 → 標準化欄位 → 查詢作業設定 → 判斷準時/遲交 → 寫入 submissions Flow B(下一篇) 每日定時觸發 → 比對未交名單 → 自動寄信催繳 本篇聚焦在 Flow A,帶你從觸發器開始,逐步建立整個接收與記錄流程。 步驟一:Google Sheets Trigger — 監聽新提交許多人會直覺選用「Google Forms Trigger」,但我們這裡刻意改用 Google Sheets Trigger,原因在於 Google Form 的回覆會自動同步到「原始回覆」工作表,而 Sheets Trigger 更穩定、也更容易控制觸發時機。 設定方式: Document:選擇你的試算表 Sheet:選擇「原始回覆」分頁 Event:設為 Row Added(有新資料列加入時觸發) 每當學生提交 Google 表單,這個 Trigger 就會收到一筆新資料,並自動啟動後續流程。 步驟二:Set Submission Fields — 標準化欄位名稱Google 表單的欄位名稱是中文(例如「學號」、「時間戳記」),在跨節點傳遞時容易因編碼問題出錯。這個節點的任務,就是把中文欄位名稱對應成英文標準欄位: 表單原始欄位 標準化後欄位名稱 時間戳記 submitted_at 學號 student_id 姓名 student_name Email email 作業代號 assignment_id 作業檔案 file_url 另外也固定寫入 source: "google_form",方便未來如果有其他提交來源(如 LINE Bot、API)時,能快速識別資料來源。 步驟三:Get Assignment — 查詢作業設定資料標準化後,系統需要知道「這份作業的截止時間是什麼時候?」。這些資訊不寫死在程式碼裡,而是統一存放在 assignments 工作表中,讓你隨時調整而不需要修改 workflow。 assignments 工作表的欄位結構建議如下: 欄位 說明 作業代號 與表單選項一致,例如 homework_0102 作業名稱 作業的顯示名稱 due_at 截止時間,格式為 ISO 8601(例如 2025-01-15T23:59:00+08:00) is_active 是否啟用,填 1 表示啟用、0 表示關閉 這個節點以 assignment_id 和 is_active = 1 作為篩選條件,一次查詢就能取得對應的截止時間與作業名稱。is_active 的設計讓你在作業結束後,只需要把值從 1 改成 0,系統就不會再接受該代號的新提交。 步驟四:Compute Status — 計算準時 / 遲交狀態這是整個 Flow A 最關鍵的節點。由於 Google 表單的「時間戳記」格式是台灣慣用的 2025/1/15 上午 9:30:00,並非標準的 ISO 格式,JavaScript 原生的 new Date() 無法直接解析,因此我們需要自己撰寫解析函式。 以下是這個 Code 節點執行的三件事: 1. 解析台灣時間格式1234567891011121314function parseTaiwanDateTime(text) { const m = String(text).trim().match( /^(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(上午|下午)\s+(\d{1,2}):(\d{2}):(\d{2})$/ ); if (!m) return null; let [, y, mo, d, ap, h, mi, s] = m; h = Number(h); if (ap === '下午' && h !== 12) h += 12; if (ap === '上午' && h === 12) h = 0; return { y: Number(y), mo: Number(mo), d: Number(d), h, mi: Number(mi), s: Number(s) };} 這個函式用正規表達式拆解時間字串,並正確處理「上午 12 點 = 午夜」、「下午 12 點 = 中午」這兩個 12 小時制的常見陷阱。 2. 比對截止時間,判斷狀態123456const submittedAtDate = new Date(parts.y, parts.mo - 1, parts.d, parts.h, parts.mi, parts.s);const dueAtDate = new Date(dueAtText); // due_at 為 ISO 格式,可直接解析const delayMinutes = Math.max(0, Math.floor((submittedAtDate - dueAtDate) / 60000));const status = submittedAtDate <= dueAtDate ? 'on_time' : 'late'; delayMinutes 只有在遲交時才有意義,Math.max(0, ...) 確保準時繳交的延遲值不會出現負數。 3. 提取 Google Drive 檔案 ID學生上傳的作業檔案,Google 表單只記錄一個 Drive 分享連結,實際的檔案 ID 藏在 URL 裡。我們用以下邏輯把它抽出來,方便後續流程(例如自動下載或批改)直接取用: 123456789101112function extractDriveFileId(url) { const patterns = [ /[?&]id=([a-zA-Z0-9_-]+)/, /\/d\/([a-zA-Z0-9_-]+)/, /\/file\/d\/([a-zA-Z0-9_-]+)/ ]; for (const pattern of patterns) { const match = String(url).match(pattern); if (match) return match[1]; } return '';} 這個函式相容三種常見的 Google Drive 連結格式,只要是合法的 Drive URL,都能正確提取。 步驟五:Append to submissions — 寫入資料庫完成狀態計算後,最後一步是將所有資料追加寫入 submissions 工作表,這張表就是你的「繳交記錄資料庫」。 每一筆記錄包含: 欄位 說明 assignment_id 作業代號 student_id / student_name 學生學號與姓名 email 聯絡信箱(催繳用) submitted_at 原始台灣時間字串 submitted_at_iso 轉換後的 ISO 格式時間(+08:00) due_at 作業截止時間 status on_time 或 late delay_minutes 遲交分鐘數(準時者為 0) file_url 原始分享連結 drive_file_id 抽取出的 Drive 檔案 ID 有了這張表,Flow B(自動催繳)只需要用 status = 'late' 或比對「誰的名字不在 submissions 裡」,就能精準找出需要提醒的人。 應用延伸這套流程的設計概念可以輕鬆複用到各種情境: 企業培訓:員工每月繳交學習心得,逾期自動提醒主管 合約文件催收:客戶 onboarding 必備文件,系統自動追蹤回收率 跨部門進度回報:專案每週回報表,自動標記哪些人尚未填寫 活動報名核對:比對報名名單與實際繳費記錄 只要情境符合「收件 → 對照截止時間 → 記錄狀態」的邏輯,這套架構都能直接套用。 常見問答 (FAQ)Q:為什麼用 Google Sheets Trigger,而不是直接用 Google Forms Trigger?A:Google Sheets Trigger 比 Google Forms Trigger 更穩定,觸發延遲也更低。此外,Google Form 回覆預設就會寫入連動的試算表,因此監聽 Sheets 的「新增列」事件,功能上完全等效,且更容易在試算表直接觀察與除錯資料。 Q:台灣時間的「上午/下午」格式真的不能直接用 new Date() 解析嗎?A:對,new Date("2025/1/15 上午 9:30:00") 在不同 JavaScript 環境下行為不一致,部分環境會回傳 Invalid Date。在 n8n 的 Code 節點環境中,含有「上午」、「下午」中文字符的時間字串無法被原生 Date() 正確解析,必須自行用正規表達式拆解後再重組為 Date 物件。 Q:assignments 表的 is_active 欄位有什麼用?A:這是一個「開關」設計。當一份作業的截止日已過,你只需要把 is_active 從 1 改為 0,Get Assignment 節點就找不到這筆資料,後續流程會中斷並報錯,避免過期作業繼續被記錄。這讓你不需要修改任何程式碼,純粹靠試算表資料來控制哪些作業「仍在接收中」。 Q:delay_minutes 為什麼要用 Math.max(0, …) 防止負數?A:遲交判斷式是 submittedAt - dueAt。準時繳交時,這個差值是負數,代表「提早了幾分鐘」。但 delay_minutes 欄位的語義是「延遲了多少分鐘」,對準時的同學來說應該是 0 而不是負數,Math.max(0, ...) 確保寫入資料庫的值語義清晰。 Q:drive_file_id 提取有什麼用途?A:Google Drive 的分享連結有多種格式(含 /d/、?id=、/file/d/ 等),直接儲存 URL 不方便後續程式化操作。提取 drive_file_id 後,後續節點可以直接呼叫 Google Drive API,例如:自動將檔案移到指定資料夾、設定閱讀權限、或讓 AI 節點直接讀取文件內容進行自動批改。 Q:如果學生重複提交怎麼辦?系統會判斷成最新一筆嗎?A:目前 Flow A 的設計是「每筆提交都寫入」,不會自動去重。如果你需要「同一個學生只保留最後一筆」,可以在 Append 節點之前加入一個 Google Sheets 查詢節點,先確認該 student_id + assignment_id 是否已有資料,再決定要覆寫還是新增。或者在 submissions 表的後處理(如 Flow B)中,以 student_id 分組取最新時間的那筆即可。 Q:這套系統可以只用 Google 試算表和 n8n,不需要其他付費工具嗎?A:可以,本篇所有流程都只使用 Google Sheets(免費)+ n8n(可自架 Community 版免費使用)。整套系統零額外費用,適合教育機構或預算有限的團隊直接部署。

  • article-如何建立自動化課程提醒系統?串接 Google Sheets 與自動寄信流程教學 | (EP.3) n8n 自動化講師應用教學

    2026/5/3

    AI自動化 n8n 自動化講師應用
    如何建立自動化課程提醒系統?串接 Google Sheets 與自動寄信流程教學 | (EP.3) n8n 自動化講師應用教學
    為什麼需要升級你的課程提醒系統?在上一堂課中,我們完成了基礎的課程確認信機制。今天這一集要往上一層,打造「課前提醒信」的升級版流程。 升級的核心差異在於:這次要跨兩張工作表整合資料。 正式名單:記錄學員姓名、學號、信箱、上課日期、寄信狀態 課程主表:記錄各課程的教材連結、Google Meet 連結、注意事項 光靠正式名單無法完成完整的提醒信,必須同時讀取課程主表,動態帶入課程資訊後才能寄出。這樣設計的好處是:只要更新課程主表,所有學員收到的資訊就自動跟著更新,不需要逐一手動調整。 完整工作流架構(6 個節點)1234567891011Schedule Trigger ↓Get row(s) in 課程主表 ↓Get row(s) in 正式名單 ↓篩出明天有課並合併課程主表(Code 節點) ↓Send a message(Gmail 節點) ↓Append or update row in sheet(回寫狀態) 節點詳解節點 1|Schedule Trigger — 定時觸發設定每天固定時間自動執行整條流程。測試階段可先設為「每分鐘」,正式上線後改成每天早上 08:00,讓提醒信固定在前一天早上寄出。 💡 重點:排程觸發並不代表一定會寄信。後面的 Code 節點會做篩選,若當天沒有符合條件的學員,整條流程會安靜地結束,不做任何動作。 節點 2|Get row(s) in 課程主表 — 讀取課程資訊從 Google Sheets 的「課程主表」工作表讀取所有課程資料,包含: 欄位 說明 課程名稱 用來做跨表比對的 key 上課日期 輔助比對,確認課程場次 上課方式 線上 / 實體,輔助比對 教材連結 帶入提醒信 Google meeting 連結 帶入提醒信 注意事項 帶入提醒信 這個節點先執行,讓後面的 Code 節點可以引用課程主表的資料做合併。 節點 3|Get row(s) in 正式名單 — 讀取學員名單從「正式名單」工作表讀取所有學員資料。關鍵欄位: 欄位 說明 姓名 / 學號 / 信箱 學員基本資料 課程名稱 / 上課日期 / 上課方式 用來對應課程主表 已寄確認信(Yes/No) 篩選條件之一 已寄提醒信(Yes/No) 篩選條件之一(防重複寄信的關鍵) ⚠️ 此節點開啟 Execute Once 模式,確保不論上一節點輸出幾筆課程資料,名單只會被讀取一次。 節點 4|Code 節點 — 篩選與合併(核心邏輯)這是整條流程最重要的節點。它做兩件事: 篩選:從正式名單中找出「明天上課 + 已寄確認信 + 尚未寄提醒信」的學員 合併:依課程名稱、日期、上課方式對應課程主表,帶入教材連結、會議連結與注意事項 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162const result = [];// 取得課程主表所有資料const courseRows = $("Get row(s) in 課程主表").all().map(item => item.json);// 計算明天的年月日const tomorrow = new Date();tomorrow.setDate(tomorrow.getDate() + 1);const tYear = tomorrow.getFullYear();const tMonth = tomorrow.getMonth() + 1;const tDay = tomorrow.getDate();for (const item of items) { const row = item.json; const date = row["上課日期"]; // 格式:YYYY/M/D const confirm = row["已寄確認信(Yes/No)"]; const reminder = row["已寄提醒信(Yes/No)"]; if (!date) continue; // 解析日期字串 const parts = String(date).trim().split('/'); if (parts.length !== 3) continue; const y = parseInt(parts[0], 10); const m = parseInt(parts[1], 10); const d = parseInt(parts[2], 10); // 三個篩選條件:明天上課 + 已寄確認信 + 尚未寄提醒信 if ( y === tYear && m === tMonth && d === tDay && confirm === "Yes" && reminder !== "Yes" ) { // 從課程主表找出對應課程(以課程名稱為主,日期與方式為輔) const matchedCourse = courseRows.find(course => { const sameName = String(course["課程名稱"] || "").trim() === String(row["課程名稱"] || "").trim(); const sameDate = !course["上課日期"] || String(course["上課日期"]).trim() === String(row["上課日期"] || "").trim(); const sameType = !course["上課方式"] || String(course["上課方式"]).trim() === String(row["上課方式"] || "").trim(); return sameName && sameDate && sameType; }) || {}; result.push({ json: { row_number: row["row_number"], name: row["姓名"], student_id: row["學號"], email: row["信箱"], course: row["課程名稱"], date: row["上課日期"], type: row["上課方式"], register_time: row["報名時間"], material_url: matchedCourse["教材連結"] || "", meeting_url: matchedCourse["Google meeting 連結"] || "", note: matchedCourse["注意事項"] || "" } }); }}return result; 💡 關鍵設計:reminder !== "Yes" 而非 === "No",是為了相容欄位空白的初始狀態,不需要預先手動填入 No。 節點 5|Send a message — 寄送 Gmail 提醒信使用 Gmail 節點寄送個人化課前提醒信,以動態變數帶入所有學員與課程資訊: 信件主旨: 1課前提醒|{{ $json.course }}(明天上課) 信件內文: 123456789101112131415161718Hi {{ $json.name }}({{ $json.student_id }}),您好:提醒您,您報名的課程將於明天開始。📘 課程名稱:{{ $json.course }}📅 上課日期:{{ $json.date }}📍 上課方式:{{ $json.type }}📚 教材連結:{{ $json.material_url }}💻 Google meeting 連結:{{ $json.meeting_url }}📝 注意事項:{{ $json.note }}請提前確認上課安排,謝謝! 每一位符合條件的學員都會收到專屬的個人化信件,課程資訊直接來自課程主表,不需要人工填寫。 節點 6|Append or update row — 回寫寄信狀態信件成功寄出後,立刻回寫正式名單: 欄位 寫入值 已寄確認信(Yes/No) YES 已寄提醒信(Yes/No) YES 使用學號作為 matching key,精準對應每一位學員的那一行,不會誤改到其他人的資料。 這是防止重複寄信的最後防線:下次排程觸發時,這筆學員的 已寄提醒信 已經是 YES,Code 節點的篩選條件 reminder !== "Yes" 就不會再把他納入,信件不會重複寄出。 節點式設計觀念:切入 AI Agent 前的必修課養成加 Sticky Note 的習慣工作流每個節點旁都應該加上「Sticky Note 說明卡」,清楚標明這個節點的作用、篩選條件與注意事項。工作流越長越複雜,這個習慣越重要。六個月後回來看自己的流程,不需要重新推理就能立刻讀懂。 先搞懂資料流,再用 AI AgentAI Agent 非常擅長處理彈性指令,但它並不擅長「精確控制資料欄位的讀取與寫入」。如果你還不清楚「這個節點的 input 長什麼樣、output 輸出什麼欄位」,直接交給 AI Agent 很容易產生幻覺或資料串接錯誤,而且你不知道問題在哪裡。 節點式自動化是 AI Agent 的基礎。搞懂每個節點的輸入輸出、資料結構與篩選邏輯之後,你在指揮 AI Agent 時才能給出精確指令,真正發揮自動化的價值。 常見問答 (FAQ)Q:為什麼篩選條件用 reminder !== "Yes" 而不是 === "No"?A:因為新學員剛登錄到正式名單時,「已寄提醒信」欄位通常是空白,而不是填了 No。如果用 === "No" 篩選,空白欄位的學員就會被漏掉,永遠收不到提醒信。改用 !== "Yes" 可以同時涵蓋「空白」與「No」這兩種初始狀態,新學員不需要預先手動填任何值,流程就能正確運作。 Q:日期格式不對會怎樣?流程會報錯嗎?A:Code 節點已做防護處理:若 split('/') 切割後不是三段,就直接 continue 跳過該筆資料,不會讓整條流程中斷。但學員仍然不會收到信,因此務必統一正式名單的日期格式為 YYYY/M/D(例如 2026/5/10)。可以在 Google Sheets 設定欄位格式或加入資料驗證,從來源端防止格式錯誤。 Q:課程主表找不到對應的課程時,信件會少哪些資訊?A:Code 節點在找不到對應課程時,matchedCourse 會是空物件 {},material_url、meeting_url、note 都會變成空字串 ""。信件仍然會寄出,但這三個欄位會是空白。建議在寄信後檢查一下是否有空白連結,可以在 Code 節點加一行 console.log 印出未匹配的課程名稱,方便追查是哪裡打錯字或格式不一致。 Q:已寄確認信還不是 Yes 的學員,為什麼不寄提醒信給他們?A:這是刻意設計的業務邏輯:確認信代表這位學員的報名資料已被人工審核過。如果一位學員還沒收到確認信,表示他的資料可能還在審核中,貿然寄出提醒信可能會造成混亂(例如報名失敗的學員收到提醒)。流程設計的順序是:確認信(審核通過)→ 提醒信(課前一天)。 Q:如果同一位學員報名了多門課,會正確對應到每門課的教材嗎?A:會。課程主表的比對邏輯是「課程名稱 + 上課日期 + 上課方式」三欄都相符才算匹配。只要正式名單每一行對應一筆報名記錄(一行 = 一位學員 + 一門課),就能各自找到對應的課程主表資料,不會混用。 Q:Google Sheets 回寫時,為什麼要用「學號」當 matching key,而不是行號?A:行號(row_number)在 Google Sheets 中不是穩定的識別碼。只要有人新增或刪除其他行,同一位學員的行號就會改變,回寫時就會寫到錯誤的行。「學號」是每位學員唯一且不變的識別碼,能確保 appendOrUpdate 精準更新到正確那一行,是更安全的設計。 Q:我想加入「上課前兩天」也寄一次提醒,該怎麼做?A:有兩種做法: 加一個新欄位:在正式名單新增「已寄兩天前提醒信(Yes/No)」欄位,複製一套相同的流程,把 tomorrow.setDate(tomorrow.getDate() + 1) 改為 + 2,並把篩選與回寫條件改為對應新欄位。 用排程 + 天數變數:讓 Code 節點讀取一個「提前天數」變數,由外部控制要篩選幾天後的名單,一套流程就能同時處理不同時間點的提醒。 對初學者來說,做法一比較直覺,不容易出錯。 Q:排程一直跑,但完全沒有寄出任何信,怎麼除錯?A:按以下順序逐步檢查: Code 節點輸出:手動執行流程,查看 Code 節點輸出了幾筆資料。如果是 0 筆,表示篩選條件沒有命中任何學員。 確認日期格式:正式名單的上課日期是否真的是 YYYY/M/D 格式?有沒有多餘空格或全形斜線? 確認狀態欄位:已寄確認信 是否已填 Yes(注意大小寫)?已寄提醒信 是否是空白或 No? 確認明天日期:把 Code 節點加一行 console.log(tYear, tMonth, tDay) 確認系統時區計算是否正確(n8n 的時區設定可能與你的本地時區不同)。 課程主表對應:確認課程主表中的課程名稱和正式名單完全一致,包括空格與標點符號。

  • article-如何打造全自動「課前提醒」工作流?告別手動寄信的自動化教學 | (EP.2) n8n 自動化講師應用教學

    2026/5/3

    AI自動化 n8n 自動化講師應用
    如何打造全自動「課前提醒」工作流?告別手動寄信的自動化教學 | (EP.2) n8n 自動化講師應用教學
    為什麼你需要自動化「課前提醒」流程?開課前一天,你還在逐一比對報名名單、複製貼上學員 Email、手動確認哪些人已寄、哪些人還沒寄嗎? 這樣的手動流程不只耗費時間,更容易因疏失而遺漏學員,導致出席率下降。對於同時管理多門課程的講師來說,這更是沉重的行政負擔。 透過 n8n 建立自動化工作流,系統會每天自動執行以下動作: 讀取你的 Google Sheets 報名名單 篩選出「明天上課」且「尚未收到提醒信」的學員 自動寄出客製化提醒信件 將發送狀態回寫至試算表,確保不重複發送 從此,課前提醒變成零人工介入的全自動流程。 工作流架構總覽在開始設定前,先了解整體流程的設計邏輯,有助於你在遇到問題時快速定位: 1排程觸發 → 讀取 Google Sheets → Code 節點篩選 → 發送 Email → 回寫狀態 節點 功能 說明 Schedule Trigger 定時啟動 每天指定時間自動執行 Google Sheets 讀取名單 取得所有報名學員資料 Code 條件篩選 過濾出需要提醒的對象 Email 發送信件 寄出客製化提醒信 Google Sheets 回寫狀態 標記「已寄送」避免重複 Google Sheets 欄位設定建議在開始建立工作流之前,請確認你的 Google Sheets「正式名單」工作表包含以下欄位: 欄位名稱 說明 範例值 姓名 學員姓名 王小明 學號 學員學號(作為唯一識別碼) A001 信箱 學員 Email [email protected] 課程名稱 課程名稱 AI自動化入門班 上課日期 上課日期 2026/05/10 上課方式 實體 / 線上 / 混合等 線上 已寄確認信(Yes/No) 是否已寄出報名確認信 Yes 已寄提醒信(Yes/No) 是否已寄出課前提醒信 No 重要: 上課日期 欄位請使用 YYYY/MM/DD 格式(斜線分隔),Code 節點的篩選邏輯是以此格式進行比對。 如何快速完成工作流環境部署?這套自動化流程的設計邏輯非常清晰:定時觸發 ➔ 讀取名單 ➔ 條件篩選 ➔ 發送信件 ➔ 更新狀態。以下是具體的節點設定步驟: 步驟一:設定排程觸發器 (Schedule Trigger)工作流的第一步是設定啟動時間。加入 Schedule Trigger 節點,建議設定為每天早上 8 點自動執行,確保學員在上課前一天有充裕時間收到提醒。 實際工作流中同時保留了 Manual Trigger,方便你在測試時用手動方式隨時觸發,無需等待排程時間。 開發測試建議: 先使用 Manual Trigger(點擊「Execute workflow」)手動測試整條流程 或將 Schedule Trigger 的頻率暫時改為「每分鐘」,方便即時驗證 確認整體流程無誤後,再改回每天一次的排程 步驟二:讀取 Google Sheets 名單加入 Google Sheets 節點,選擇「Get Many Rows」操作,一次讀取「正式名單」工作表的所有資料。 設定要點: Spreadsheet:選擇你的報名表試算表 Sheet:選擇「正式名單」工作表 資料全部抓回後,交給後面的 Code 節點篩選——這樣最穩,不容易因試算表結構變動而出錯 步驟三:篩選需要提醒的學員 (Code 節點)這是整個工作流的核心邏輯。加入 Code 節點(節點名稱:「篩出明天有課」),貼入以下篩選程式碼: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748const result = [];const tomorrow = new Date();tomorrow.setDate(tomorrow.getDate() + 1);const tYear = tomorrow.getFullYear();const tMonth = tomorrow.getMonth() + 1;const tDay = tomorrow.getDate();for (const item of items) { const row = item.json; const date = row["上課日期"]; const confirm = row["已寄確認信(Yes/No)"]; const reminder = row["已寄提醒信(Yes/No)"]; if (!date) continue; const parts = String(date).trim().split('/'); if (parts.length !== 3) continue; const y = parseInt(parts[0], 10); const m = parseInt(parts[1], 10); const d = parseInt(parts[2], 10); if ( y === tYear && m === tMonth && d === tDay && confirm === "Yes" && reminder !== "Yes" ) { result.push({ json: { row_number: row["row_number"], name: row["姓名"], student_id: row["學號"], email: row["信箱"], course: row["課程名稱"], date: row["上課日期"], type: row["上課方式"], register_time: row["報名時間"] } }); }}return result; 程式碼說明: 明天日期拆解為年、月、日三個數字,再與試算表的 YYYY/MM/DD 格式逐項比對,避免字串比較因格式差異失敗 三重條件過濾:日期符合 且 已寄確認信 = Yes 且 尚未寄提醒信 「已寄確認信」的前提很重要——代表學員已完成報名流程,才需要發提醒 Code 節點同時將中文欄位名稱重新命名為英文(name、email、student_id…),讓後續的 Gmail 節點可以用更簡潔的變數名稱取值 步驟四:自動發送客製化 Email 提醒信確認篩選名單後,加入 Gmail 節點(使用 Gmail OAuth2 授權)。因為前面的 Code 節點已將欄位重新命名為英文,這裡可以直接使用簡潔的變數名稱: 收件人:{{ $json.email }} 主旨:課前提醒|{{ $json.course }} 信件內文範例: 123456789Hi {{ $json.name }}({{ $json.student_id }}),您好:提醒您,您報名的課程即將開始。📘 課程名稱:{{ $json.course }}📅 上課日期:{{ $json.date }}📍 上課方式:{{ $json.type }}請提前確認上課安排,謝謝! 注意:Gmail 節點的「Append Attribution」建議關閉(appendAttribution: false),避免信件底部出現 n8n 的廣告署名,影響專業形象。 步驟五:回寫狀態,杜絕重複發送這是整個工作流中最不能遺漏的一環。信件成功寄出後,必須回到 Google Sheets 將學員的提醒信狀態更新為 Yes。 加入第二個 Google Sheets 節點,操作選擇「Append or Update Row」: Matching Column(比對欄位):學號——以學號作為唯一識別,確保更新到正確的列 更新欄位:已寄提醒信(Yes/No) 設定為 Yes 來源資料從 Code 節點取得:{{ $('篩出明天有課').item.json.student_id }} 使用「學號」而非「列號」做比對有一個重要優點:即使試算表的排序或列數改變,更新也不會寫入錯誤的列。 若沒有這個步驟,下次排程執行時,系統會對同一批學員再次寄信,造成學員困擾與信任損失。 下一步:讓工作流更進階完成這套課前提醒後,你已省下每次開課前大量的手動作業時間。在下一堂進階課程中,我們將教你如何: 在信件中動態帶入專屬課程連結與 Google Meet 視訊網址 加入課前注意事項附件,打造更專業的學員體驗 建立多課程並行管理的工作流架構,適用於同時開設多門課的講師 常見問答 (FAQ)Q:Google Sheets 欄位名稱和教學不一樣,程式碼需要完整改寫嗎?A:不需要。只需在 Code 節點中修改對應的欄位名稱即可。例如,若你的日期欄位叫做「課程日期」而非「上課日期」,只需將程式碼中的 row["上課日期"] 替換為 row["課程日期"],其餘篩選邏輯完全不變。Google Sheets 回寫節點的 Matching Column 也需要同步修改。建議先在試算表統一欄位命名,後續維護會更輕鬆。 Q:如果想改成「開課前三天」寄送提醒,如何調整?A:在 Code 節點中,將日期計算的 +1 改為 +3 即可: 12345// 修改前(明天)tomorrow.setDate(tomorrow.getDate() + 1);// 修改後(三天後)tomorrow.setDate(tomorrow.getDate() + 3); 其餘的 Gmail 節點與回寫邏輯完全不需要變動。若你想同時在「三天前」和「一天前」各發一次提醒,建議分別建立兩條獨立的工作流,各自有各自的回寫欄位(如「已寄三日提醒信」和「已寄前日提醒信」),這樣狀態管理最清晰,不容易互相干擾。 Q:測試時不小心寄出太多信怎麼辦?A:在開發與測試階段,請採取以下保護措施: 修改收件人:在 Email 節點暫時將收件人從 {{ $json.Email }} 改為你自己的測試信箱 停用 Email 節點:在工作流中右鍵點擊 Email 節點,選擇「Disable」,僅觀察資料流輸出的 JSON 內容 限制資料筆數:在 Google Sheets 節點設定「Limit」,一次只讀取 1~2 筆資料進行測試 確認篩選條件與回寫狀態(Yes/No)都正確後,再關閉所有限制、改回真實收件人。 Q:為什麼系統會重複寄信給同一位學員?A:這是最常見的錯誤,通常由以下原因造成: 步驟五的回寫節點未正確執行:請確認工作流最後有成功更新「已寄提醒信(Yes/No)」欄位。執行後手動打開試算表,確認欄位值是否已改為 Yes。 欄位名稱不完全一致:Code 節點中的 row["已寄提醒信(Yes/No)"] 必須與 Google Sheets 的欄位標題逐字相同,包含括號與標點符號,差一個字就會讀到 undefined,導致條件永遠成立。 Matching Column 設定錯誤:回寫節點若 Matching Column 設定不正確,更新可能寫入到錯誤的列,讓原始列的狀態維持 No。 除錯建議:在 Code 節點篩選結果中,暫時加入 console.log(JSON.stringify(row)) 印出每筆資料的完整欄位名稱,就能快速確認試算表傳回的欄位名稱是否與程式碼一致。 Q:我的課程同時有多個梯次,同一門課有不同上課日期,工作流支援嗎?A:完全支援,工作流的設計本身就是逐筆比對每位學員的 ClassDate,因此同一門課的不同梯次只要在 Google Sheets 中各自有一列記錄,系統就能精準對應每位學員的實際上課日期,不會混淆。若你的報名表裡有多門課程,同樣適用,篩選邏輯只關心「日期」和「狀態」,與課程名稱無關。 Q:工作流使用的是 Gmail 節點,還是一般的 Email 節點?有什麼差別?A:本工作流使用的是 Gmail 節點(搭配 Gmail OAuth2 授權),不是通用的 Email (SMTP) 節點。兩者的差異如下: 節點 授權方式 適合情境 Gmail Google OAuth 授權 使用 Gmail 帳號發信,設定簡單、顯示名稱友善 Email (SMTP) 填入 SMTP 伺服器設定 使用自訂網域信箱(如 [email protected]) 若你已有 Google Workspace 帳號,Gmail 節點是最快速的選擇——完成 OAuth 授權後即可使用,無需額外的 SMTP 設定。若你希望以機構網域信箱發信,則改用 SMTP 節點,品牌形象更專業,但需要向你的郵件服務商取得 SMTP 憑證。 Q:如果某位學員的 Email 信箱填寫錯誤,工作流會報錯嗎?A:當 Email 節點遇到無效信箱(如格式錯誤或不存在的地址),工作流預設會中斷並報錯,導致後續學員的信件也無法寄出,狀態也不會被回寫。 建議解法:在 Email 節點的「Settings」中開啟「Continue On Fail」選項,讓工作流在遇到單一錯誤時跳過該筆資料,繼續處理下一位學員。同時,可以在後方加入一個通知節點(如 Line Notify 或 Slack),在寄信失敗時主動告警,讓你能即時追蹤並手動補寄。 Q:n8n 自架版(Self-hosted)和 n8n Cloud 的設定方式有差異嗎?A:工作流的節點邏輯和設定方式完全相同,差異主要在環境維護層面: n8n Cloud:免維護、開箱即用,適合個人講師或小型工作室,但有使用量限制與月費 Self-hosted:需自行部署伺服器(如 Railway、Render 或 VPS),免費且可無限使用,但需負責更新與備份 若你是第一次建立自動化工作流,建議先從 n8n Cloud 的免費方案開始體驗,熟悉流程後再考慮自架。

  • article-打造 Google 表單與 Gmail 自動回信報名系統 | (EP.1) n8n 自動化講師應用教學

    2026/5/2

    AI自動化 n8n 自動化講師應用
    打造 Google 表單與 Gmail 自動回信報名系統 | (EP.1) n8n 自動化講師應用教學
    為什麼你需要一套自動化報名系統?今天我們要實作一個很多講師與活動主辦人夢寐以求的工具:用 n8n 打造 Google 表單 × Gmail 全自動報名回信系統。 你是否曾有這樣的經驗:活動開放報名後,一個下午都在反覆開啟試算表,手動一筆一筆貼上確認信收件人,再逐一寄出?這種流程不只費時,還容易漏寄或寄錯。 透過本文介紹的 n8n 自動化流程,只要學員送出 Google 表單,系統就會自動: 彙整報名資料到試算表 篩選並建立正式名單 發送個人化確認信 更新寄件完成狀態 從此,報名通知這件事,你完全不需要再碰。 自動化工作流的五大關鍵步驟這套系統的核心邏輯分為以下五個步驟,環環相扣、自成閉環: 學員提交表單: 學員填寫包含姓名、學號、Email、課程名稱等資訊的 Google 報名表單並送出。 原始資料彙整: 表單回應自動同步至 Google 試算表的「原始資料」工作表,作為所有後續處理的來源。 正式名單拋轉: n8n 定期掃描原始資料,將尚未處理的新報名者篩選出來,整理後寫入「正式名單」工作表。 自動觸發寄信: 根據正式名單中的 Email 與報名資訊,系統透過 Gmail 自動發送個人化確認信,信件內容可包含上課時間、地點或 Google Meet 連結。 更新寄件狀態: 信件成功送出後,系統自動將該筆資料的寄信欄位標記為「Yes」,完成整個自動化閉環,方便隨時查閱進度。 關鍵設計概念: 以「寄信狀態是否為 Yes」作為判斷依據,確保每位報名者只收到一封確認信,不重複、不漏寄。 拆解 n8n 節點:五個節點完成全流程在 n8n 畫布中,我們用五個節點串起整套流程。以下逐一說明各節點的用途與設定重點: 1. Schedule Trigger(定時排程觸發)設定每分鐘(或自訂頻率)自動觸發流程。n8n 會定期「醒來」掃描試算表,確認是否有新報名者需要處理。 2. Google Sheets:讀取原始資料撈取「原始資料」工作表中所有尚未同步至正式名單的新資料列,作為後續節點的輸入來源。 3. Google Sheets:寫入正式名單將清洗過的報名資料(姓名、Email、課程代碼等)寫入正式名單工作表,同時預設「確認信」欄位為空白,作為待處理的標記。 4. Gmail Node(發送確認信)從正式名單中讀取 Email,套入預先設計好的信件範本,自動發送報名成功通知。信件內容可使用 n8n 的表達式(Expression)動態帶入學員姓名、課程名稱等個人化資訊。 5. Google Sheets:更新寄件狀態這是整套流程最關鍵的一步。Gmail 節點成功執行後,立即回寫試算表,將該筆資料的「確認信」欄位更新為「Yes」。若寄信失敗,此步驟不會執行,狀態維持空白,讓管理員一眼識別異常並手動補寄。 延伸應用:上課前提醒信這套邏輯同樣可以套用在「活動前一天的提醒信」:新增一個獨立的 n8n 排程,掃描正式名單中「提醒信」欄位為空白的學員,批次發信並標記「Yes」,一套模板即可重複使用。 常見問答 (FAQ)Q:我沒有程式背景,能自己做出這套流程嗎?A:完全可以!n8n 採用視覺化的節點拖拉介面,不需要撰寫任何程式碼。本文及對應的 YouTube 教學影片都有完整的逐步示範,只要跟著操作就能建立起整套流程。建議先用 n8n Cloud 試用版(免費)快速上手,再視需求考慮自架。 Q:n8n 的觸發頻率可以自訂嗎?每分鐘跑一次會不會太頻繁?A:可以完全自訂。「每分鐘」是教學中用於快速測試的設定。正式部署時,建議根據活動規模調整:一般課程每 5~15 分鐘跑一次即可。n8n 的 Schedule Trigger 支援 cron 語法,可設定任意時間間隔或指定時段(例如僅在工作時間執行)。 Q:如果報名者填錯 Email,系統會怎麼處理?A:若 Email 格式錯誤,Gmail Node 會拋出錯誤並中斷後續流程,「更新寄件狀態為 Yes」的步驟便不會執行。管理員只需過濾試算表中「確認信」欄位為空白的列,即可快速找出所有未成功寄達的報名者,進行人工確認與補寄。這個設計確保了問題的可追蹤性。 Q:這套流程使用的 Google 服務需要付費嗎?A:不需要。Google Forms、Google Sheets 與 Gmail 均為免費方案即可使用,沒有特殊 API 費用。n8n 本身提供免費的雲端試用方案(n8n Cloud),若需大量執行或長期使用,可選擇付費方案或自架免費的開源版本。 Q:確認信的內容可以個人化嗎?能自動帶入學員姓名嗎?A:可以。n8n 的 Gmail Node 支援 Expression(表達式)功能,可以直接引用前置節點傳入的欄位變數,例如 {{ $json.name }} 帶入姓名、{{ $json.course }} 帶入課程名稱。不需要任何程式基礎,直接在信件編輯框中點選「插入變數」即可完成個人化設定。 Q:這套系統可以同時管理多個課程或活動嗎?A:可以。有兩種常見做法:(1)分開工作表: 在同一份 Google 試算表中,為每個課程新增獨立的工作表,搭配對應的 n8n 流程或分支判斷節點;(2)統一管理: 在表單中加入「課程名稱」欄位,以此欄位進行篩選,在同一套 n8n 流程中以條件分支(If Node)分流處理不同課程的信件範本。 Q:這套流程可以套用到企業內部的報名系統嗎?A:完全適用。企業內部的講座、教育訓練、會議邀約等場景,只需替換 Google 表單欄位與 Gmail 信件範本即可直接套用。更進一步,企業用戶可搭配 Google Workspace 的共享試算表,讓多位管理員同時查看報名狀態,達到跨部門協作的效果。

  • article-n8n 專案前端架構指南:需要用 PWA 嗎?何時該選擇 Next.js 或 Vite?

    2026/4/30

    AI自動化 n8n 前端架構 PWA
    n8n 專案前端架構指南:需要用 PWA 嗎?何時該選擇 Next.js 或 Vite?
    在執行眾多 n8n 專案時,開發者與系統架構師最常面臨的一個核心問題就是:「前端要不要做成 PWA?如果要,那還需要用 Next.js 嗎?」 這篇文章將為你釐清技術盲點,提供一份實務上的前端架構決策指南,幫助你把預算與開發時間花在刀口上。 什麼是 PWA?為何它與 n8n 專案是絕配?PWA(Progressive Web App,漸進式網路應用程式)用一句話來解釋就是: 讓網站用起來像 App,但本質上它仍然是一個網站。 初學者這樣理解: 你可以把 PWA 想像成「偽裝成 App 的網頁」。使用者不需要去 App Store 下載安裝,直接在手機瀏覽器打開你的網址,按一下「加入主畫面」,之後就能像一般 App 一樣點 Icon 啟動,看不到網址列,有全螢幕體驗,甚至能收到推播通知。 PWA 賦予了網頁端以下幾個強大的能力: 可以直接加入手機主畫面(擁有專屬 App Icon) 支援全螢幕開啟(隱藏瀏覽器的網址列與 UI) 支援系統級的推播通知 (Push Notifications) 具備基本離線快取能力(即使暫時斷網,頁面也不會整片空白) 啟動速度大幅提升 為什麼 n8n 非常適合導入 PWA?n8n 是什麼? 簡單說,n8n 是一套「自動化流程工具」,讓你不用寫程式也能串接各種服務(例如:收到一封 Email 就自動發 Slack 通知、表單送出就自動建立一筆資料庫記錄)。n8n 本身負責後端邏輯,而前端介面(也就是使用者看到、點擊的那個畫面)就是我們這篇文章要討論的重點。 通常 n8n 的前端應用場景會包含:表單填寫送出 Webhook、點擊按鈕觸發 Workflow、透過 Dashboard 查看自動化執行結果,以及接收 Workflow 的成功或失敗通知。 這類型的介面本質上是「操作型工具 + 行動使用場景」,這恰好就是 PWA 技術最能發揮價值的領域。 導入 PWA 的 3 大實質優勢1. 沉浸式的「工具感」大幅提升使用 PWA,使用者的心智模型會從「打開一個網頁」轉換成「打開一個專屬 App」。對於企業內部工具 (Internal Tools) 或提供給特定客戶的專用系統來說,這種體驗上的升級是非常顯著的。 2. 終極的低安裝門檻無需經過 App Store 或 Google Play Store 繁瑣的流程: 免審核、免上架費用 不用等待應用程式更新發佈 使用者只需打開網址,點擊「加入主畫面」即可完成安裝 3. 即時推播通知(完美契合自動化流程)這點對於 n8n 專案極具價值。你可以針對以下情境發送推播: Workflow 成功/失敗通知 Webhook 觸發的即時提醒 長時任務完成提示這能讓自動化流程變得「即時且有感」。 導入前必看的現實面與限制在決定使用 PWA 前,你必須了解以下幾個現實狀況: PWA 不會自動拯救糟糕的 UX: 真正影響使用者體驗的,是你的 UI 介面是否有做到 Mobile-first(行動優先)以及操作流程是否足夠簡化。 iOS 系統的先天限制: 雖然蘋果已開放支援 Web Push,但在背景執行能力上依然較弱,且部分進階硬體 API 仍受限。 離線功能不是 n8n 專案的重點: n8n 的核心極度依賴 Server 與 API。PWA 的離線功能頂多讓前端「開得起來」,但在沒有網路的情況下,並無法執行實質的自動化操作。 架構抉擇:你的 n8n 專案真的需要 Next.js 嗎?先講結論:多數 n8n 前端的 PWA 專案,根本不需要用到 Next.js。 初學者補充:Vite 與 Next.js 都是什麼? Vite(發音:維特)是一個超快速的前端開發工具,幫你打包程式碼、啟動本地開發伺服器。它做出來的網站是「純前端網頁」,由瀏覽器負責全部的渲染工作。 Next.js 則是建立在 React 上的全端框架,除了前端以外,它還能在伺服器端產生 HTML(SSR),讓搜尋引擎更容易抓取頁面內容,也可以寫後端 API。代價是架構更複雜、部署要求更高。 什麼情況推薦「使用 Vite 就好」?如果你的專案屬於以下類型,強烈建議使用 Vite: 企業內部營運工具(員工才看得到,不需要被 Google 搜尋到) 封閉式的客戶操作介面(要登入才進得去) 單純的 Webhook 觸發頁面或資料表單 登入後才能使用的系統 完全不需要 SEO(搜尋引擎優化) 💡 推薦技術組合(Vite 方案): 1234// 前端核心配置建議Framework: Vite + React (或 Vue)Styling: Tailwind CSSPWA Plugin: vite-plugin-pwa 優點: 開發極快、架構單純、靜態部署超輕量且成本極低(部署到 Cloudflare Pages 或 Netlify 免費方案就夠用)。 什麼情況才「必須使用 Next.js」?當你的專案具有以下需求時,Next.js 才是正確的選擇: 需要 SEO 排名的公開頁面(如官方網站、部落格、產品介紹頁) 包含 Landing Page 或大量的內容行銷頁面 需要 SSR(伺服器端渲染)來優化首次載入速度 需要內建後端 API(透過 API Routes,例如自行處理付款或發送 Email 而不想另外建後端) 具備複雜的登入與權限控管機制 想將「公開行銷頁 + 客戶入口網站 + 後台系統」整合在同一個全端專案中 一個簡單的判斷標準: 問自己「這個頁面,我希望在 Google 上搜尋得到嗎?」如果答案是「不需要,使用者都是登入後才進入的」,那麼 Vite 就夠了。 n8n 專案實務推薦架構與 PWA 最小實作指南🏆 最常用且高效的推薦架構這是目前業界開發 n8n 前端最穩定且快速的技術棧: 核心框架: Vite + React 樣式管理: Tailwind CSS 狀態與資料獲取: TanStack Query (React Query) 會員與權限: Supabase 或 Firebase Auth 後端自動化: n8n Webhook / API PWA 支援: vite-plugin-pwa 🚀 PWA 最小可行性實作 (MVP) 先做這 4 步不需要一開始就追求完美的 PWA 功能,先用最低成本完成以下設定,體驗就能立即升級: 建立並配置 manifest.json 檔案。這個檔案告訴瀏覽器「我這個網站是一個 App」,包含 App 名稱、顏色主題、Icon 路徑等基本資訊。 將顯示模式設定為全螢幕:"display": "standalone"。standalone 模式讓使用者從主畫面點開 App 時,不會看到瀏覽器的網址列,視覺上更像一個真正的 App。 準備並配置各尺寸的 App Icon。至少需要 192x192 和 512x512 兩種尺寸的 PNG 圖示,分別對應一般螢幕和高解析度螢幕。 設定基本的 Service Worker(僅針對 UI 靜態資源進行快取)。Service Worker 是一段在背景執行的程式,負責快取你的 HTML、CSS、JS 等靜態檔案。使用 vite-plugin-pwa 時,這部分大多可以自動產生,不需要手動撰寫。 (進階優化:未來可再逐步加入搭配 n8n 的 Push 通知、離線 Fallback 頁面及 Background Sync)。 常見問答 (FAQ)Q:我完全沒有前端開發經驗,這篇文章說的 Vite、React、Next.js 到底是什麼關係?A:用一個比喻來解釋:你想蓋一棟房子(網站)。 React 是你蓋房子用的「磚塊材料」,負責讓畫面變得互動、動態。 Vite 是「建築工具箱」,幫你把磚塊快速組裝起來,在本機開發時極速預覽成果。它蓋出來的是一棟「只有客戶端(瀏覽器)能自己運作」的房子。 Next.js 則是一套「更豪華的建築解決方案」,除了客戶端以外,還內建了伺服器(工廠),可以在送到瀏覽器之前就先把頁面做好,讓 Google 搜尋引擎更容易讀懂。代價是架構更複雜、需要付費的伺服器來部署。 對於大多數 n8n 的配套工具頁面來說,Vite + React 就已經非常足夠。 Q:我的 n8n 專案主要是內部人員每天使用,導入 PWA 值得嗎?A:非常值得!內部工具是 PWA 效益最高的場景。員工只要將網頁加入手機主畫面,就能獲得像原生 App 一樣的全螢幕體驗與快速啟動,大幅降低每天開啟瀏覽器輸入網址的摩擦力。此外,若搭配推播通知,工作流程完成時能主動提醒員工,不需要員工自行刷新頁面確認。 Q:如果我的前端介面只是偶爾用來看 n8n 的執行 Dashboard,還需要做 PWA 嗎?A:幫助有限。如果使用頻率極低(例如一週看一次的報表或儀表板),使用者通常不會有意願將它加入主畫面。這種情況下,只需確保網頁具備良好的**響應式設計 (RWD)**(也就是在手機上瀏覽時,版面能自動調整,不會跑版)即可,PWA 帶來的額外好處相對有限。 Q:為了追求 SEO,我是不是一定要把架構改成 Next.js?A:取決於你的頁面性質。如果該頁面是「產品官網」或「部落格」,那麼需要 Next.js 的 SSR/SSG 來優化 SEO(因為這些技術讓 Google 的爬蟲更容易讀取頁面內容)。但如果你只是做一個「登入後才能操作的 n8n 自動化工具台」,搜尋引擎爬蟲根本進不去,此時用 Vite 建立 SPA (單頁應用程式) 反而是最省時省力的做法。 什麼是 SSR 和 SSG? SSR(Server-Side Rendering,伺服器端渲染): 每次有人打開頁面,伺服器臨時產生完整的 HTML 再回傳,適合內容會頻繁更新的頁面(如新聞網站)。 SSG(Static Site Generation,靜態網站生成): 在部署前就把所有頁面預先產生成靜態 HTML 檔案,速度最快,適合不常變動的頁面(如部落格、產品介紹頁)。 Q:我該怎麼讓使用者在 iPhone 上也能「加入主畫面」?A:在 iPhone 上,使用者需要用 Safari 瀏覽器(不能是 Chrome)打開你的網址,點選下方的「分享」按鈕(方塊加箭頭圖示),再選擇「加入主畫面」。iOS 的限制是,只有 Safari 支援 PWA 的加入主畫面功能,其他瀏覽器不行。建議在你的網頁上放置一段操作說明或引導提示,方便不熟悉的使用者找到這個功能。 Q:Push 通知(推播)要怎麼跟 n8n 搭配使用?A:整體流程是這樣的: 使用者在你的前端網頁上點擊「允許通知」。 瀏覽器產生一組專屬於該使用者設備的「推播憑證」(Push Subscription),你的前端把這組憑證儲存到資料庫(例如 Supabase)。 當 n8n 的 Workflow 完成或出現特定事件時,n8n 呼叫一個負責發送推播的 API(例如用 Web Push 協議發送)。 使用者的手機收到通知。 這個流程不需要額外建立推播伺服器,整合成本相對可控。 Q:我現有的 n8n 前端已經用 Vite 做好了,中途改成 Next.js 值得嗎?A:通常不建議,除非出現明確的需求驅動(例如你突然決定要把這個工具做成公開的 SaaS 產品、需要 SEO 流量)。架構遷移的成本遠比想像中高:除了移植元件外,還需要調整路由邏輯、部署方式,以及原本不需要的伺服器設定。先問自己「Vite 的哪個限制讓你卡住了?」,如果說不出具體的問題,那就不需要換。

  • article-Instagram Graph API 打造 IG 自動發文系統 (結合 Cloudinary) | (EP11) n8n 自動化 API 串接教學

    2026/4/18

    AI自動化 n8n API串接 社群行銷
    Instagram Graph API 打造 IG 自動發文系統 (結合 Cloudinary) | (EP11) n8n 自動化 API 串接教學
    在上一篇教學中,我們介紹了 如何透過 Facebook Graph API 自動發布多圖貼文。今天,我們將進一步把自動化版圖擴展到 Instagram (IG)。 透過 n8n、Instagram Graph API,並結合 Cloudinary 作為圖片的公開圖床,我們可以打造一套「一鍵自動多平台發文」的系統。這篇文章將帶你拆解整個 n8n 工作流程,並一步步完成必要的 API 權限申請。 系統運作流程解析:IG 自動發文的底層邏輯在正式動手之前,我們先理解 n8n 節點的運作邏輯。要透過 API 發布 IG 貼文,流程如下: 表單提交 (Form Submission): 接收要發布的文字內容與圖片檔案。 圖片檢查與圖床轉換: IG API 嚴格規定,圖片必須是一個「公開可存取的網址 (Public URL)」。因此,我們必須先將圖片上傳至 Cloudinary 以取得公開網址。 建立 Instagram 容器 (Create Container): 根據 Meta 官方內容發佈文件,我們需要先用圖片網址與貼文內容建立一個 Media Container。 等待緩衝 (Wait): 設定約 30 秒的緩衝時間,避免 Meta 伺服器處理過慢導致後續請求失敗。 正式發佈 (Publish): 獲取 Container ID 後,執行最終的發佈動作。 延伸閱讀: 更詳細的 n8n IG API 串接原理,可參考 n8n Instagram API 實戰指南。 步驟一:如何設定 Meta 開發者應用程式與取得 IG 權杖?要讓系統代為發文,我們必須先在 Meta 開發者平台申請專屬的應用程式與存取權杖 (Access Token)。 1. 建立 Meta 應用程式前往 Meta 開發者應用程式介面 (Apps): 點擊「建立應用程式」,選擇「其他」>「企業商家」。 填寫應用程式名稱(例如:n8n-ig-auto-post)與聯絡信箱。 2. 設定隱私權政策網址為了讓應用程式能順利上線,Meta 要求提供隱私權政策網址: 可利用免費工具 PrivacyPolicies.com 快速生成一份基本的隱私權條款。 將生成的專屬網址複製,貼回到 Meta 應用程式的「基本資料 > 隱私權政策網址」中並儲存。 將應用程式狀態切換為 「上線 (Live)」。 3. 新增 Instagram 測試人員與轉換專業帳號由於應用程式尚未經過繁瑣的官方審查,我們需要將自己的 IG 帳號加入「測試人員」: 在 Meta 後台左側選單進入「應用程式角色 > 角色」。 新增「Instagram 測試人員」,輸入你的 IG 帳號。 重要提醒: 你的 IG 帳號必須切換為 「專業帳號 (商業或創作者)」,否則無法透過 API 發文! 打開手機 IG App,進入「設定 > 網站權限 > 應用程式和網站 > 測試人員邀請」,點擊「接受」。 4. 取得 Access Token 與 IG ID 回到 Meta 開發者後台,設定「Instagram 圖形 API」。 使用「圖形 API 測試工具 (Graph API Explorer)」。 勾選所有與 instagram_manage_* 及 instagram_content_publish 相關的權限。 點擊「Generate Access Token (產生存取權杖)」,並將這串落長的 Token 複製保存。 同時記下你的 Instagram ID,這兩個值將會填入 n8n 的設定中。(註:測試用的 Token 預設有效限期約為 2 個月,過期需重新產生。) 步驟二:為什麼需要 Cloudinary?如何設定免憑證上傳?如前所述,IG Graph API 不接受直接傳送本機圖片,只接受公開網址。我們使用 Cloudinary 來做自動化圖床。 1. 取得 Cloud Name登入 Cloudinary 控制台,在 Dashboard (儀表板) 找到你的 Cloud Name,這會是 n8n API 呼叫的基礎路徑。 2. 設定 Unsigned Upload Preset (無簽章上傳預設)在設定自動化時,為了安全性與便利性,我們 不使用 API Key 與 API Secret,而是改用 Upload Preset Name。 進入 Cloudinary 的「Settings > API Keys > Upload」。 點擊「Add Upload Preset」。 將 Signing mode 設定為 Unsigned。 設定一個你專屬的 Upload preset name (例如:n8n-try-2026),並設定要存放的資料夾名稱 (Folder)。 儲存後,將這個 Preset Name 複製下來。 為什麼不直接使用 API Key?將 API Secret 寫死在前端或簡單的工作流中存在資安風險。Unsigned Upload Preset 允許你在不暴露核心密鑰的情況下,讓系統把檔案上傳到指定資料夾,是自動化流程中最推薦的簡化做法。詳情可參考這篇探討:為何 n8n 教學愛用 Cloudinary Upload Preset?安全與便利的平衡。 步驟三:整合 n8n 節點並執行測試最後,我們回到 n8n 介面,將剛剛取得的參數填入對應的資料表或節點中: Cloudinary 節點設定: 將 Cloud Name 與 Upload Preset Name 填入。 Instagram 節點設定: 將 IG ID 與 Access Token 填入 Authorization 欄位。 觸發流程: 打開 n8n 的表單提交 (Form Trigger),上傳一張圖片並輸入測試文字。 驗證結果: 執行流程後,打開你的 Instagram 頁面,就可以看到圖片已經成功透過 n8n 自動發布了! 常見問答 (FAQ)Q:為什麼流程執行成功,但 IG 上卻沒有出現貼文,或出現權限錯誤?A:這通常是最常見的設定問題,建議依序檢查以下幾項: 你的 IG 帳號是否已切換為 專業帳號(創作者或商業)。 該 IG 帳號是否已正確加入 Meta App 的 Instagram 測試人員,並且已在手機上接受邀請。 產生 Access Token 時,是否有勾選 instagram_content_publish、instagram_basic 等必要權限。 你的 IG 是否有正確綁定對應的 Facebook Page。許多 Meta API 權限是透過粉專與商業資產關聯來驗證的。 如果以上都確認無誤,建議回頭檢查 n8n 的 HTTP Request 回傳內容,Meta 通常會在錯誤訊息中直接指出是權限不足、帳號資格不符,還是參數格式有問題。 Q:為什麼圖片已經上傳到 Cloudinary,但 Instagram API 還是說 image_url 無效?A:因為「有網址」不等於「符合 IG API 可讀取的公開網址」。你需要確認: 該圖片網址可以在 無痕模式 直接開啟,不需要登入、不會跳轉、也不會出現權限限制。 網址指向的是 實際圖片檔,而不是預覽頁、下載頁或帶驗證機制的暫時連結。 圖片格式為常見可支援格式,例如 jpg 或 png。 圖片尺寸與比例不要過於極端,避免 Meta 在建立容器時失敗。 最穩定的做法,就是使用 Cloudinary 上傳後取得的 secure_url 當作 image_url,不要自行拼接其他可能會失效的連結。 Q:為什麼流程中要加 Wait 節點?可以直接建立容器後馬上 Publish 嗎?A:理論上可以連續呼叫,但實務上不建議。因為 Meta 在建立 Media Container 後,還需要一點時間處理圖片內容;若你太快發送 Publish 請求,就可能出現容器尚未就緒、發文失敗,或偶發性的 API 錯誤。 因此在 n8n 中加入約 20 到 30 秒的 Wait,是一種很常見也很實用的穩定化做法。如果你想再更精準一些,也可以改成輪詢 Container 狀態,確認處理完成後再執行 Publish。 Q:這套流程可以直接發 Reels、限時動態或多張輪播貼文嗎?A:這篇教學的流程主要針對 單張圖片貼文。若你要發: 輪播貼文:通常需要建立多個媒體項目,再組成 Carousel Container,流程會比單張圖複雜。 Reels:需要改用影片上傳與對應的發佈流程,參數與等待時間也不同。 限時動態:支援情況與發佈方式需依 Meta API 當前規範確認,不能直接套用一般貼文邏輯。 也就是說,這篇文章適合作為「IG 自動發文入門版」,如果你後續要延伸到更多貼文型態,建議再額外拆成不同 workflow 來做。 Q:Cloudinary 的 Unsigned Upload 安全嗎?會不會被別人亂傳圖片?A:Unsigned Upload 本質上是只要知道 Preset Name 就能上傳,因此有一定風險。但你可以透過 Cloudinary 後台針對該 Preset 設定「檔案格式限制」、「檔案大小限制」甚至是「上傳後轉檔規則」,來避免遭惡意濫用。對於個人自動化用途而言,比起洩漏 API Secret,這是相對安全且輕量的做法。 如果你是要提供給團隊或正式商業場景使用,建議再往前一步: 將上傳來源限制在特定流程或後端。 規劃專用資料夾,方便管理與清理。 定期輪替 Upload Preset,避免長期暴露固定入口。 監控 Cloudinary 使用量,避免異常流量造成額外成本。 Q:我的自動發文系統突然失效了,發生了什麼事?A:最可能的原因是 Meta Access Token 過期。透過 Graph API 測試工具生成的 Token 通常只有 60 天左右的效期。若要長期自動化,建議在到期前透過 API 延長權杖效期,或是定期手動重新生成一次並更新至 n8n 中。 除了 Token 之外,也建議同步檢查: Meta App 是否仍維持上線狀態。 測試帳號是否仍保有測試人員身分。 Cloudinary Preset 是否被修改、停用或刪除。 n8n 中的節點參數是否被其他人誤改。 Q:這套流程適合正式營運使用嗎?還是只適合測試?A:可以用於正式營運,但前提是你要把它從「可跑」升級成「可維護」: 權杖更新機制要制度化,不能等過期才手動補救。 n8n 流程要加上錯誤通知,例如失敗時寄 Email、發 Slack 或 LINE Notify。 圖片、文案、發文時間最好有資料表或資料庫可追蹤。 若有多人使用,建議加入審核機制,避免錯發或重複發文。 如果你只是個人品牌、小型工作室,這篇教學已足夠作為第一版;但若你是企業團隊,後續應把監控、權限管理與例外處理一併補上。 Q:如果我想把這個流程擴充成「一鍵同步發 Facebook + Instagram」,要怎麼做?A:這正是 n8n 很適合發揮的地方。你可以把「表單輸入」或「內容資料表」當成單一來源,接著分流到不同平台節點: 一條流程處理 Cloudinary 圖片上傳。 一條分支送往 Facebook Graph API。 另一條分支送往 Instagram Graph API。 最後把各平台回傳結果寫回 Google Sheets、Airtable 或 Notion。 這樣你就能把原本重複操作的社群發文,整理成一套真正可複用的多平台內容發布系統。

  • article-使用 Facebook Graph API 自動發布多圖貼文 | (EP10) n8n 自動化 API 串接教學

    2026/4/4

    AI自動化 n8n API串接 社群行銷
    使用 Facebook Graph API 自動發布多圖貼文 | (EP10) n8n 自動化 API 串接教學
    如果你正在找「n8n Facebook 多圖貼文教學」、「Facebook attached_media 怎麼設定」或「n8n 如何一次發布多張圖片到 Facebook」,這篇就是針對這些問題整理的實戰版本。 先前我們分享過如何透過 n8n 模板自動發布「單圖加單文」到 Facebook。今天這篇文章,我們將進一步把流程升級成一次發布多張圖片的 Facebook 貼文,並直接拆解多數人最常卡住的 attached_media、/photos 與 binary 判斷問題。 如果你還沒完成 Page ID、長期 Page Access Token 或單圖發文設定,建議先閱讀上一篇:n8n 串接 Facebook 自動發文教學:Meta API、Page ID、長期 Token 完整指南。 這篇文章可以把它理解成上一篇的進階版:前一篇解決「先發得出去」,這一篇解決「如何穩定改成多圖發文」。 如何在 n8n 表單中開啟多檔案上傳功能?在原本的預設模板中,表單 (Form) 節點僅支援單張圖片與一篇文章。若要實現多圖上傳,我們必須先從觸發器 (Trigger) 著手修改。 請進入 n8n 的 Form Trigger 節點,找到設定並勾選 Multiple Files 功能。開啟此選項後,表單就能一次接收多張圖片的輸入。為了確認功能正常運作,建議先進行測試,例如輸入標題「test 0403 多圖」並同時上傳三張圖片,藉此確認資料是否成功進入後續的工作流中。 破解 API 迷思:為什麼 Facebook Graph API 其實不吃 JSON?這是在串接 Facebook Graph API 時最容易踩到的坑!許多人在查閱 Meta 官方文件時,會看到官方範例給的都是 JSON 格式。然而,當你實際在 n8n 中將 Content-Type 設為 JSON 時,常常會遇到發文失敗或行為異常的狀況。 經過測試與 AI 的輔助分析,我們得出了一個重要結論:Facebook Graph API 預設其實是接收 url-encoded 與 form-data 格式,而非 JSON。 在 n8n 的 HTTP Request 節點中,最佳的實踐方式是: 主體內容 (Body): 使用 form-urlencoded 模式,將 Token、Message 與整理好的 attached_media 傳送出去,這樣不僅容易發送,穩定度也最高。 圖片上傳 (Photo Upload): 使用 form-data 的方式來傳送二進位 (Binary) 的圖片檔案。 請果斷放棄使用 JSON 格式來修改多圖貼文,直接採用 form-urlencoded 與 form-data,能幫你省下大量的除錯時間。 實戰解析:多圖貼文真正要改的是什麼?很多人第一次改 Facebook 多圖貼文時,會以為只要把 attached_media[0] 改成 attached_media[1]、attached_media[2] 就好。實際上不是這樣。 多圖貼文的核心不是「多加幾個欄位」,而是整個流程要改成先上傳圖片、再集中發文。 正確流程如下: 表單允許一次上傳多個檔案 把每張圖拆成獨立 item 每張圖各自呼叫 POST /photos,並設定為未發佈 收集每張圖回傳的 photo id 最後只呼叫一次 POST /feed,把所有 media_fbid 一起送進 attached_media 換句話說,你不是直接把多張圖片塞進同一次照片上傳請求,而是先把每張圖變成 temporary photo,最後再用一篇貼文把它們組起來。 最短版修改清單如果你手上已經有上一篇的單圖工作流,要改成多圖版,實務上先完成這 5 件事就夠了: Form Trigger 把 Multiple Files 改成 true 原本的「如果圖片不存在」條件,改成檢查 Object.keys($binary ?? {}).length === 0 新增一個 Code 節點,把多張圖片展開成多個 item 新增一個 Code 節點,把所有 photo id 整理成 attached_media 最後發文的節點,把 attached_media[0] 改成整個 attached_media 陣列 如果你是用既有模板直接改,這 5 項通常就是變更範圍的 80%。剩下的問題,多半都是權限、Graph API 版本,或請求格式設定錯誤。 多圖上傳的資料處理與判斷邏輯1. 先展開圖片,再逐張上傳表單收到多張圖片後,n8n 內部通常不會直接變成多筆獨立資料,而是先包在同一個輸入裡。這時要先新增一個節點,把圖片展開成一張圖一個 item,後面的 Facebook 上傳節點才能逐張處理。 這一步的重點不是「看起來有三張圖」,而是流程裡真的要變成三個可迭代的 item。只有這樣,後面的 POST /photos 才能對每張圖各自拿到一個 photo id。 2. 每張圖都先走 POST /photosFacebook 多圖貼文不是直接把多張 binary 一次送到 /feed。正確做法是每張圖先打一次 /{page_id}/photos,並設定為不立即發佈,讓 Facebook 先建立暫存照片。 等每張圖都成功回傳 ID 之後,才進到最後一個發文節點。這也是很多人一直測不通的原因,因為他們跳過了「先建立 temporary photo」這一步。 3. 最後一次把所有 media_fbid 組回 attached_media當你拿到多張圖的 photo id 後,接下來不是再一張一張發貼文,而是把所有 ID 整理成 Facebook 要的格式,例如: 12345[ { "media_fbid": "photo_id_1" }, { "media_fbid": "photo_id_2" }, { "media_fbid": "photo_id_3" }] 最後只呼叫一次 POST /feed,並把整包 attached_media 傳出去,這樣 Facebook 才會把它視為同一篇多圖貼文。 4. 「是否有圖片」的判斷邏輯也要一起改單圖流程常見的寫法,通常是假設只有一個固定欄位,例如 binary.data 或某個指定圖片名稱。但多圖流程下,這種判斷很容易失準,因為二進位檔案不再只有單一鍵值。 比較穩的做法,是直接檢查目前是否真的存在任何 binary 檔案: 12345if (Object.keys($binary ?? {}).length === 0) { // 沒有圖片,走純文字貼文流程} else { // 有圖片,走圖片上傳與多圖發文流程} 這段寫法的好處是,不需要綁死某個圖片欄位名稱,對單張圖、多張圖、甚至沒有圖片的情境都比較穩。 可直接參考的 Code Node 範例如果你想更快落地,下面這兩段可以作為 Code 節點的參考起點。實際欄位名稱還是要依你的工作流資料結構微調,但核心思路就是這樣。 範例 1:展開多張圖片這個節點的目的是把同一筆輸入裡的多張 binary 圖片,拆成多個 item,讓後面的 POST /photos 可以逐張執行。 1234567891011121314const binaryEntries = Object.entries($binary ?? {});return binaryEntries.map(([key, file]) => { return { json: { ...$json, binaryKey: key, fileName: file.fileName ?? key, }, binary: { [key]: file, }, };}); 這段的重點是: 先用 Object.entries($binary ?? {}) 取出所有圖片 每張圖各自回傳成一個 item 保留原本的 $json,避免後面發文文案或其他欄位不見 範例 2:整理成 attached_media當每張圖片都上傳到 POST /photos 後,Facebook 會回傳各自的 id。接下來就要把這些 id 整理成最後發文節點要吃的格式。 12345678910111213const attached_media = $input.all().map((item) => { return { media_fbid: item.json.id, };});return [ { json: { attached_media, }, },]; 如果你的最後一個發文節點還需要貼文文字、Page ID 或其他欄位,就記得在這裡一起帶回去,不要只剩 attached_media。 最後發文節點要注意什麼?整理完之後,最後一個 POST /feed 節點要送的,不再是單一的 attached_media[0],而是整包 attached_media。也就是說,這一步的重點不只是欄位名稱改掉,而是前面的資料形狀已經從單張圖邏輯,變成多張圖陣列邏輯。 跨平台自動發文的策略與考量在學會了 Facebook 多圖發文後,你可能會想:「我能不能把這個多圖工作流直接套用到 Instagram、Threads、X (Twitter) 或 LinkedIn 上?」 這裡要特別提醒,每個社群平台的 API 設計與支援度皆不相同。Facebook 接受的 attached_media 多圖陣列格式,放到其他平台可能會導致報錯。如果你追求的是「一鍵同時發布到所有平台」,那麼維持「一圖一文」會是最安全、兼容性最高的方式。 然而,如果你是為了深入學習 API 串接與自動化邏輯,強烈建議你親手實作一次專屬於 Facebook 的多圖發文流程。這能幫助你透徹理解 API 格式的差異、陣列資料的轉換,大幅提升你的 n8n 實戰能力! 常見問答 (FAQ)Q:多圖貼文是不是只要多加幾個 attached_media[1]、attached_media[2] 就可以?A:不行。這是最常見的誤解。Facebook 多圖貼文的重點不是多塞幾個欄位,而是要先讓每張圖各自上傳成未發佈照片,再把所有 photo id 組成 attached_media 後一次送進 POST /feed。如果你只是在原本單圖節點上多加幾個欄位,通常流程會不穩,或根本不會被 Facebook 正確識別成同一篇多圖貼文。 Q:在 n8n 裡,正確的多圖發文順序是什麼?A:最穩的順序是這樣: 表單開啟多檔上傳 將多張圖展開成多個 item 每張圖各自呼叫 POST /photos 收集所有回傳的 photo id 最後呼叫一次 POST /feed,把所有 media_fbid 傳進 attached_media 如果你的流程不是這個順序,通常就是之後會卡住的地方。 Q:為什麼我照著官方文件用 JSON 傳多圖,還是一直失敗?A:因為官方文件的範例格式,不代表在 n8n 裡就是最穩的做法。實務上,Facebook Graph API 在這類發文情境下,通常用 application/x-www-form-urlencoded 與 multipart/form-data 會更穩。簡單說: 圖片上傳用 form-data 最後發文用 form-urlencoded 如果你整段流程都硬用 JSON,常見結果就是欄位格式對了,但請求仍然失敗,或 Facebook 不照你預期解析。 Q:為什麼「是否有圖片」這個判斷在多圖版容易壞掉?A:因為單圖流程通常只檢查某一個固定欄位,例如 binary.data。但多圖上傳時,binary 的結構不一定只會有單一鍵值,所以原本那種寫死欄位名稱的判斷方式常常會失準。比較穩的寫法是: 1Object.keys($binary ?? {}).length === 0 這樣你判斷的是「現在到底有沒有任何圖片檔」,而不是賭某個欄位名稱剛好存在。 Q:表單已經開了 Multiple Files,為什麼還是不能直接發多圖?A:因為 Multiple Files 只解決「前端可以一次上傳多張圖」,沒有幫你完成後面的資料整理。你還是得自己把圖片展開、逐張上傳、收集 ID、再整理成 attached_media。也就是說,表單只是入口,多圖貼文能不能成功,關鍵仍然在後面的工作流設計。 Q:多圖流程測試時,最少要驗證哪些情境?A:至少要測這 3 種: 沒有圖片,只發純文字 只有 1 張圖片 一次上傳多張圖片 如果這三種都能正常執行,代表你的條件分流、binary 判斷、圖片上傳與最後發文邏輯大致是穩的。只測「三張圖剛好成功一次」其實不夠,因為很多錯誤都發生在無圖或單圖情境切換時。 Q:如果我要沿用上一篇單圖模板,最少要改哪些地方?A:最小修改範圍就是本文前面那 5 項: Form Trigger 開啟 Multiple Files 調整「是否有圖片」判斷 新增圖片展開節點 新增 attached_media 整理節點 把最後發文欄位從單一 attached_media[0] 改成整個 attached_media 你可以把它理解成:前面多了一段「圖片預處理」,最後一段則從「單圖發文」改成「多圖組裝後發文」。 Q:如果發文節點一直報權限錯誤,是多圖流程寫錯了嗎?A:不一定。多圖流程錯誤與權限錯誤是兩件事。如果你看到的是 OAuth、permission、scope 相關訊息,通常先檢查的不是 attached_media,而是: 你現在用的是不是正確的 Page Token 權限是否包含 pages_manage_posts Page ID 是否對應到同一個粉專 這類問題比較接近授權設定,而不是多圖組裝本身。需要的話可以回頭對照上一篇的 Token 教學一起排查。 Q:這篇流程可以直接套到 Instagram 或其他平台嗎?A:不建議直接照搬。這篇的重點是 Facebook Page 的多圖貼文流程,而 attached_media 這種組法本身就帶有平台特性。若你是做跨平台自動發文,最穩的策略通常還是先維持「一圖一文」,再依各平台 API 能力逐一擴充,而不是假設 Facebook 的多圖格式能通用。 參考資料 Meta Developers:Page Posts 上一篇:n8n 串接 Facebook 自動發文教學

  • article-n8n 串接 Facebook 自動發文:從 Meta API 到取得長期 Token 完全指南 | (EP.9) n8n 自動化 API 串接教學

    2026/4/3

    AI自動化 n8n API串接 社群行銷
    n8n 串接 Facebook 自動發文:從 Meta API 到取得長期 Token 完全指南 | (EP.9) n8n 自動化 API 串接教學
    如果你正在找「n8n 串接 Facebook 自動發文教學」、「Facebook Page ID 怎麼找」或「Meta 長期 Token 怎麼拿」,這篇就是針對這些問題整理的實戰指南。 在這篇教學中,我們將探討如何利用 n8n 工作流自動化工具,串接 Facebook Graph API,實現粉絲專頁的一鍵自動發文功能。 如果你不想從零開始刻節點,本篇教學將會使用由(Darks)開源提供的多平台自動發文模板進行示範,並專注解決串接過程中最容易卡關的 Meta 應用程式建立與 長期 Access Token 獲取。 你可以把這篇文章理解成一份實戰排雷手冊:不是只告訴你「理論上可以串接」,而是直接處理多數人在實作時最常遇到的三個問題: Token 很快失效,導致昨天能發、今天不能發 Meta 權限沒有勾完整,n8n 執行後直接報錯 模板本身可用,但一到自己的帳號與粉專環境就卡住 只要你把 Page ID、長期 Page Access Token、HTTP 節點版本 這三件事設定正確,Facebook 自動發文流程通常就能穩定跑起來。 n8n 串接 Facebook 自動發文前,你需要先準備什麼?在開始設定之前,你至少要先備好以下 4 個元素: 一個可正常登入的 n8n 環境 一個你有管理權限的 Facebook 粉絲專頁 一個 Meta for Developers 應用程式 可用於發文的 Facebook Page ID 與長期 Page Access Token 如果這四個條件都具備,後面的設定會順很多;如果少其中任何一項,通常就會卡在權限、授權或 endpoint 錯誤。 如何快速導入 n8n 自動發文模板?1. 取得並匯入工作流模板你可以前往 n8n 官方 Templates 庫 或創作者 Darks 的 Portaly 頁面 獲取一鍵自動發文模板。 作法: 複製模板內容後,直接在你的 n8n 畫布上按下 Ctrl+V (或 Cmd+V) 即可貼上完整的工作流。 2. 設定社群金鑰 (Data tables)在新版的 n8n 中,我們可以使用 Data tables 來集中管理各個社群平台的 API 金鑰,取代過去分散在各個節點填寫的麻煩。 在 n8n 左側選單點擊 Data tables,選擇 Create data table。 將表格命名為 社群金鑰(若使用模板,請務必與模板預設名稱完全一致)。 新增所需欄位(例如:main_scope、attribute、value)。 針對 Facebook 發文,我們至少需要準備並填入兩項核心資料: Facebook Page ID Facebook Access Token 接下來,我們將進入 Meta 開發者後台,去獲取這兩項關鍵資料。 如何建立 Meta 開發者應用程式,讓 n8n 可以串接 Facebook?要透過 API 發文到 Facebook 粉絲專頁,你必須先在 Meta 建立一支應用程式來取得權限。 1. 新增應用程式 前往 Meta for Developers。 點擊右上角的 建立應用程式。 應用程式類型請選擇 「商家 (Business)」 或 「其他 → 商家」。 輸入易於辨識的名稱(例如:n8n-fb-autopost),並填寫聯絡電子郵件。 2. 解決「隱私權政策」網址要求建立應用程式後,前往 應用程式設定 → 基本資料。系統會要求填寫「隱私權政策網址 (Privacy Policy URL)」才能將應用程式狀態切換為「上線」。 專家建議: 如果你沒有個人網站的隱私權頁面,可以使用免費的 Privacy Policy Generator 生成一份公版隱私權條款,獲取連結後貼回 Meta 後台即可過關。 3. 將應用程式切換為「上線」模式在基本資料設定完成並儲存後,務必將應用程式頂部的狀態由「開發中」切換為 「上線」,這樣 n8n 才能順利調用 API。 如何取得 Facebook 長期 Access Token 與 Page Token?這是整個流程中最容易出錯的環節。預設取得的 Token 通常只有 1 小時的壽命,我們必須將其轉換為「粉絲專頁專用的長期 Token」。 第一步:取得短期 User Token 在 Meta 開發者後台,點擊頂部選單的 「工具」→「圖形 API 測試工具 (Graph API Explorer)」。 右側面板設定: Meta 應用程式:選擇你剛建立的 App。 用戶或粉絲專頁:選擇 「取得粉絲專頁存取權杖」,並授權勾選你的粉絲專頁。 新增權限 (Permissions): 這是發文成功的關鍵,請務必加入以下 5 個權限: pages_show_list pages_read_engagement pages_read_user_content pages_manage_posts pages_manage_engagement 點擊 Generate Access Token(產生存取權杖),並複製這串短效期金鑰。 第二步:轉換為長效期 Token 在「圖形 API 測試工具」中,點擊上方的 「存取權杖偵錯工具」 或點擊權杖旁的「i」圖示展開詳情。 點擊底部的 「延伸存取權杖 (Extend Access Token)」,取得約 2-3 個月效期的長期 User Token。 關鍵轉換: 我們需要將 User Token 換成 Page Token。回到圖形 API 測試工具,在 GET 請求欄位輸入以下端點(請替換為你的粉絲專頁 ID):1/{你的粉絲專頁編號}?fields=access_token&access_token={剛剛取得的長效_User_Token} 點擊提交,返回的 JSON 數據中,access_token 欄位的值就是你最終需要的 長期粉絲專頁發文金鑰! 將這個 Token 與你的 Page ID 填回 n8n 的 Data tables 中,前置作業就大功告成了。 如果你想先驗證 Token 是否真的可用,建議在 n8n 正式執行前,先用 Graph API Explorer 或 HTTP Request 測一次最小請求。只要能成功打到粉絲專頁資料,後續發文流程通常就不會差太遠。 補充:Facebook Page ID 怎麼找最快?如果你手上還沒有 Page ID,最穩的方式不是直接猜名稱,而是透過 Meta 工具或 Graph API 查詢實際 ID。因為在 n8n 串接 Facebook 發文時,真正用來指定目標粉專的是 Page ID,不是粉專顯示名稱。 你可以把這個觀念記住: 粉專名稱是給人看的 Page ID 是給 API 用的 只要這裡填錯,後面就算 Token 正確,發文也可能失敗或打到錯的目標。 n8n Facebook 自動發文失敗怎麼排查?常見報錯修復整理如果你在執行 n8n 工作流時遇到錯誤,通常是以下兩個原因: 1. Graph API 版本過舊Meta API 更新頻繁,模板中預設的 API 版本可能是 v23.0。若報錯,請進入 n8n 的 HTTP Request 節點(負責發布貼文的節點),將 URL 中的版本號手動更改為最新版,例如 v25.0: 1https://graph.facebook.com/v25.0/{{ $json.facebook_id }}/feed 2. If 節點 (判斷圖片是否存在) 報錯舊版的 n8n If 節點在讀取空值時容易發生中斷錯誤。若你的流程在判斷「是否上傳圖片」時卡住,建議將該原有的 If 節點刪除,重新拖曳一個新的 If 節點,並重新設定判斷條件(如判斷 binary.data 是否存在),即可解決錯亂問題。 3. 發文成功回傳 200,但粉專上看不到貼文這種情況通常不是 n8n 沒有送出,而是你打到的目標不是預期中的粉絲專頁、權限對錯頁、或貼文被發到不同類型的內容區塊。建議依序檢查: Data table 裡的 Facebook Page ID 是否填成正確粉專,而不是個人帳號 ID。 Access Token 是否真的是該粉專對應的 Page Token。 HTTP Request 節點送出的 endpoint 是否為 /{page_id}/feed,而不是其他物件路徑。 粉專角色是否足夠,且授權帳號仍然是該粉專的管理者或具備可發文權限的人員。 4. 明明有 Token,卻跳出權限不足或 OAuth 相關錯誤這通常代表問題不在「有沒有 Token」,而在於 Token 綁定的權限範圍不夠。最常見的修法是重新生成權杖,並重新勾選 pages_manage_posts、pages_read_engagement 等必要權限,再重新做一次長效與 Page Token 轉換。只換節點設定、不重拿權杖,很多時候是修不好的。 實務建議:讓 Facebook 自動發文流程更穩定的 4 個做法如果你是要把這個流程真的用在營運,而不只是測試一次,建議多做以下幾件事: 把 Token 到期日記錄下來長期 Token 不是永久有效。建議在 Notion、行事曆或任務系統中記錄建立日期與預估到期時間,避免某天排程突然中斷才回頭找原因。 先做最小發文測試一開始不要直接串完整的 AI 文案、自動抓圖、自動排程。先測最簡單的純文字貼文,確認 Page ID、Token 與 endpoint 都正確,再逐步加功能。 把錯誤訊息完整保留n8n 的執行紀錄、HTTP status code、Meta 回傳的錯誤訊息都很重要。很多 Facebook API 問題不是節點壞掉,而是錯誤訊息裡早就明講是版本、權限或參數不符。 避免把憑證硬寫在節點裡如果你未來還要串接 Instagram、Threads 或其他平台,建議持續用 Data tables 或統一憑證管理方式來控管金鑰,後續維護會簡單很多。 如果你接下來要改成 Facebook 多圖貼文這篇文章處理的是「先把 Facebook 自動發文打通」,也就是 Page ID、長期 Token、基本發文與常見權限問題。如果你已經能穩定發單圖或純文字,下一步通常就是改成多圖貼文。 但這一步不是只把欄位多複製幾份而已。Facebook 多圖貼文的正確做法,會需要: 表單支援多檔上傳 每張圖各自呼叫 POST /photos 收集所有 photo id 最後用 attached_media 一次組回 POST /feed 完整做法我另外拆成一篇:使用 Facebook Graph API 自動發布多圖貼文。如果你現在已經跑通這篇的單圖流程,建議直接接著看下一篇。 n8n 串接 Facebook 自動發文 FAQ以下這一段我特別整理成「真的會遇到的卡點」,如果你不是要理解原理,而是要把流程跑起來,這些問題通常最值得先看。 Q:為什麼我的 Facebook 發文權限一小時就失效了?A:因為你一開始拿到的通常不是最終可用的 Page Token,而是「短效期用戶權杖 (Short-lived User Token)」。這種 Token 常常只有約 1 小時效期,適合測試,不適合正式排程。 正確流程應該是: 先取得短期 User Token 再延長成長期 User Token 最後透過 API 轉換成對應粉專的 Page Token 只有完成第三步,你放進 n8n 裡的憑證才比較適合長期自動發文。很多人以為「已經拿到 Token」就可以了,結果其實只是停在第一步。 Q:建立 Meta 應用程式時,強制要求填寫「隱私權政策網址」該如何解決?A:Meta 為了合規性,要求上線的 API 應用程式必須具備隱私權政策。若你沒有自己的官網,最簡單的做法就是先用 Privacy Policy Generator 產生一份可公開存取的頁面,再把該 URL 貼回 Meta 後台。 實務上要注意兩件事: 這個網址必須能從外部正常開啟,不能是內網或尚未發布的頁面。 就算你現在只是自己測試,Meta 仍然常要求基本資料填完整,否則某些功能或狀態切換會卡住。 Q:執行 n8n 流程時出現 Graph API 版本錯誤怎麼辦?A:Meta 會定期淘汰舊版的 Graph API,所以你拿到的工作流模板就算之前能跑,過一陣子也可能因為版本過期而失效。最直接的做法,就是打開 n8n 中負責發文的 HTTP Request 節點,把 URL 裡的版本號更新成目前支援的版本,例如 v25.0。 如果你更新版本後還是報錯,不要只看版本本身,也要同步檢查: endpoint 路徑有沒有寫錯 權限是否完整 請求方法是否為 POST 送出的欄位名稱是否符合該 endpoint 要求 Q:Page ID 要去哪裡找?可以直接用粉專名稱代替嗎?A:不建議用粉專名稱硬猜,最穩的方式還是直接拿 Facebook Page ID。你可以在 Meta 的工具或相關 API 查到 Page ID,之後固定填進 n8n 的 Data table。因為實際發文 endpoint 是依照 ID 指向目標粉專,不是依名稱辨識。 如果你填錯 Page ID,常見結果有兩種: 直接報錯,表示找不到對應資源 成功執行,但其實打到錯的粉專或錯的頁面物件 Q:我明明是粉專管理員,為什麼還是無法發文?A:因為「你是管理員」不一定等於「這次授權出來的 Token 具備正確發文權限」。Facebook API 的世界裡,是否能發文不是只看帳號身份,還要看這次產生 Token 時到底勾了哪些 scopes。 換句話說,常見問題不是角色不夠,而是: 你勾漏了 pages_manage_posts 你拿的是 User Token,不是 Page Token 你授權的是 A 粉專,但實際要發的是 B 粉專 Q:n8n 裡建議用 Credentials 還是 Data tables 管理 Token?A:如果你現在是跟著模板快速實作,而且這份工作流本身就是用 Data tables 設計,那直接沿用 Data tables 會最快,也比較符合這篇教學的流程。它的優點是你可以把多個平台的憑證集中管理,後續替換比較方便。 但如果你的團隊之後會把這套流程做得更正式、更模組化,也可以評估改成 n8n Credentials 或其他集中式密鑰管理方案。重點不在工具名稱,而在於你要避免把 Token 分散寫死在不同節點裡,否則未來更新憑證會很痛苦。 Q:可以用這種方式排程每天自動發文嗎?A:可以,這正是 n8n 很適合的場景之一。你可以在前面接 Schedule Trigger,固定每天、每週或特定時段執行,再把產生好的文案送到 Facebook 發文節點。 不過正式排程前,建議先確認三件事: Token 已經換成長期可用的 Page Token 貼文內容來源是穩定的,不會臨時產出空值 流程裡有做基本錯誤處理,避免發文失敗卻沒人知道 Q:Facebook 自動發文可以順便帶圖片嗎?A:可以,但比起純文字貼文,圖片流程通常更容易出錯,因為你還要額外確認圖片檔案來源、格式、欄位名稱,以及 n8n 裡對 binary 資料的處理是否正常。這也是為什麼很多人在模板裡會卡在 If 節點或圖片判斷邏輯。 建議的做法不是一開始就硬上圖片版,而是: 先測純文字貼文 再測單張圖片貼文 最後才整合 AI 文案、圖片生成與排程 這樣你比較容易知道問題到底出在 Facebook API、n8n 節點,還是圖片資料本身。 Q:為什麼 n8n 測試時能發,排程時卻失敗?A:這種情況非常常見,因為手動測試與排程執行的上下文不一定完全一樣。最常見的原因包括: 測試時用的是手動輸入資料,排程時來源欄位其實是空的 Token 到排程執行時已失效 前面某個節點在排程模式下沒有成功輸出資料,導致發文節點吃到空值 所以你不能只看「手動跑一次有成功」,還要回頭檢查排程當下的 input/output 與 execution log。 Q:如果未來 Access Token 過期了,要整套重做嗎?A:通常不用整套重做,但你至少要重新完成「拿新 Token」這一段,並把新的值更新回 n8n 使用的地方。只要 App、粉專、流程本身都還在,通常不需要整套模板重建。 比較務實的做法是: 把 Token 更新流程寫成你自己的內部 SOP 記錄 Page ID、App 名稱、授權帳號 每次更換 Token 後立刻做一次最小發文測試 這樣下次出問題時,你不會又從零開始排查。 Q:這套流程適合哪些人先導入?A:最適合的是有固定社群內容產出需求的人,例如個人品牌經營者、顧問、行銷團隊、接案工作者,或本來就已經在用 n8n 串內容工作流的人。尤其如果你已經有固定的文案來源,例如 AI 產文、Notion 選題、Google Sheets 排程表,Facebook 自動發文會很容易接進去。 反過來說,如果你現在連貼文策略、審稿流程、內容節奏都還沒建立好,那先把 API 串起來不一定會立刻帶來效益。自動化放大的前提,是你原本的內容流程已經有基本穩定度。

  • article-社群發布自動化指南:使用 n8n 與 Notion 打造零失誤半自動工作流 | (EP.8) n8n 自動化 API 串接教學

    2026/4/1

    AI自動化 n8n API串接 社群行銷
    社群發布自動化指南:使用 n8n 與 Notion 打造零失誤半自動工作流 | (EP.8) n8n 自動化 API 串接教學
    為什麼社群自動化不能只追求「全自動」?許多人一接觸 AI 與自動化,第一個念頭就是:「能不能讓 AI 寫完文之後,直接自動發到 Facebook、Instagram、X、Threads?」 答案是技術上可以,但商業上通常不值得冒這個風險。 社群內容不是單純的文字搬運,而是品牌對外發聲。只要 AI 生成的資訊有誤、語氣偏掉、格式跑版,或誤用了不合時宜的字詞,就可能造成客服壓力、品牌信任受損,甚至引發公關危機。對大多數企業、自媒體團隊與接案行銷人員來說,更穩健的做法不是「全自動」,而是 半自動化(Human-in-the-loop)。 半自動化的核心精神很簡單: AI 負責加速產出:先生成貼文草稿、平台文案、標籤與 CTA。 人類負責最後把關:檢查事實、語氣、素材與排程是否正確。 系統負責穩定執行:核准後再由 n8n 自動分發到指定平台。 這種流程的優勢在於,你把最耗時的「重複性工作」交給系統,卻把最關鍵的「品牌判斷」留在人手上。效率有提升,風險也不會失控。 一套真正可落地的社群工作流長什麼樣?如果你想建立的是可以每天穩定運作,而不是只示範一次的流程,建議把整體架構拆成四層: 內容生成層:用 OpenAI 或其他模型產出各平台草稿。 內容管理層:用 Notion 當內容資料庫,集中管理文案、素材、狀態與排程。 流程執行層:用 n8n 根據狀態與時間條件執行發布邏輯。 結果回寫層:將發布結果、錯誤訊息、發布時間與平台連結回寫到 Notion。 這樣的設計有一個很大的好處:當流程某一段需要調整時,你只要修改那一層,不需要整套重做。例如你今天先發 Facebook,未來要擴充 LinkedIn 或 Threads,也只要增加分流與對應 API 邏輯即可。 內容資料庫該放哪裡?為什麼我更推薦 Notion在人工審核階段,你需要一個所有人都看得懂、也方便協作的內容資料庫。常見做法有兩種:Google Sheets 與 Notion。 Google Sheets 的優點是簡單、直觀、容易大量編修;但如果你的需求不只是存資料,而是希望團隊成員能更舒服地檢查文案、管理狀態、切換檢視與長期維護,那麼 Notion 會更適合作為內容中台。 原因很實際: 版面更適合內容審稿:不只是表格,而是可以用資料庫、看板、日曆等方式檢視。 欄位可讀性更高:小編、主管、客戶比較容易理解目前每篇文章卡在哪個階段。 更像內容管理系統(CMS):未來要擴充 SOP、素材區、發文規範、提示詞模板,也能都留在同一個工作空間。 如果你想做的是長期能交接、能維護、能多人使用的流程,Notion 通常比單純的試算表更穩。 建議建立的 Notion 欄位為了讓 n8n 能穩定讀取與判斷,至少準備以下欄位: 文章標題(Title):內部辨識用,不一定會直接對外發布。 主文內容(Content):長文或主敘述內容。 平台文案(Caption):針對社群平台的短文案,可依平台拆欄位。 發布平台(Platform):多選欄位,例如 FB、IG、X、Threads、LinkedIn。 審核狀態(Status):例如 Draft、Reviewing、Ready、Published、Failed。 立即發布(Publish Now):勾選後可讓 n8n 優先處理。 預約發布時間(Scheduled Time):做排程發布時會用到。 素材連結(Asset URL):圖片或影片 URL,特別是 IG 幾乎一定會用到。 發布結果(Post URL):成功後回寫平台貼文網址。 錯誤紀錄(Error Log):方便追蹤 API 錯誤與人工補救。 如果你預計經營多平台,建議一開始就把「平台文案」與「素材」拆開,不要把所有東西都塞在同一個欄位中,否則後期擴充會很痛苦。 狀態管理才是自動化成功的關鍵很多人以為自動化的核心在 API 串接,但實務上,真正決定流程穩不穩的,通常是 狀態管理設計得好不好。 建議在 Notion 中至少規劃以下 5 種狀態: Draft(草稿):AI 剛產出,尚未人工確認。 Reviewing(審核中):編輯、主管或客戶正在修改內容。 Ready(準備發布):內容、素材、平台都已確認,可交由系統處理。 Published(已發布):已成功送出到指定平台。 Failed(發布失敗):流程有執行,但 API 回傳錯誤或資料不完整。 多了 Failed 這個狀態很重要,因為真實世界的流程不可能永遠一次成功。當貼文因為字數超限、素材格式錯誤、Token 過期或 API 配額不足而失敗時,你需要明確知道它不是還沒發,而是「發送失敗,等待處理」。 n8n 要怎麼知道你準備好了?當文章在 Notion 中被標記為 Ready 後,n8n 需要一個觸發機制來接手。常見有兩種方式: 做法 A:排程巡檢(Schedule Trigger)這是最適合新手上手的方法。你可以設定 n8n 每隔 1 到 5 分鐘讀取一次 Notion,找出符合條件的資料。 優點: 設定簡單 不需要額外開發前端按鈕 容易除錯與觀察流程 缺點: 不是即時發布 如果排程太密,會增加不必要的 API 呼叫 做法 B:Webhook 即時觸發當你希望按下按鈕就立刻發文,或要從其他系統觸發 n8n,就可以用 Webhook。 優點: 幾乎即時執行 比較適合做人工核准後立即送出 缺點: 權限與安全性要處理好 建置成本比排程高一些 實務建議: 先用排程把流程跑穩,再考慮升級成 Webhook。多數團隊真正卡住的不是「延遲 3 分鐘」,而是資料格式與狀態管理不夠嚴謹。 一條穩定的 n8n 發布流程,至少要包含哪些節點?當 n8n 開始執行後,建議的基本邏輯如下: Schedule Trigger / Webhook:啟動流程。 Notion 查詢節點:讀取資料庫中符合條件的貼文。 IF / Filter:確認狀態為 Ready,且已達預約時間。 資料清理 / 格式化:處理文案長度、日期格式、平台欄位與素材欄位。 Loop / Split In Batches:逐筆處理,避免多篇一起失敗難以追蹤。 平台分流:依據平台呼叫不同 API,例如 Meta Graph API、X API、LinkedIn API。 成功回寫 Notion:更新狀態為 Published,記錄發布時間與貼文連結。 失敗回寫 Notion:更新狀態為 Failed,寫入錯誤訊息,方便人工補救。 如果你再往前走一步,還可以加入: 重試機制:暫時性 API 錯誤時自動重送。 Slack / Email 通知:發布成功或失敗時提醒相關人員。 審核紀錄:寫回最後編輯者與核准時間,讓流程更可追溯。 真正會踩雷的,不是 API,而是內容格式很多新手第一次做社群自動化,最容易忽略的是:不同平台根本不是把同一段文字複製貼上就能發。 例如: X(Twitter):字數限制嚴格,太長就直接失敗。 Instagram:通常需要搭配圖片或影片,純文字流程很容易卡住。 Facebook / LinkedIn:可接受較長文,但語氣與換行邏輯仍需要調整。 Threads:文案節奏與互動感通常要比部落格摘要更口語。 所以,部落格長文、電子報內容與社群貼文應該視為三種不同內容型態,而不是同一份文案的不同輸出位置。 比較好的做法在 AI 生成階段,就直接要求模型輸出: 部落格摘要版 Facebook / LinkedIn 版 X 短文版 Instagram Caption 版 Hashtags CTA 這樣做的好處是,你的資料庫會從一開始就長得像「可發布資料」,而不是一團等待人工重寫的草稿。自動化流程要穩,前面的資料結構就不能含糊。 給新手的落地建議:先做小,再做快如果你現在正準備開始做這套系統,建議先用最小可行版本(MVP)上線: 先只做 單一平台,例如先串 Facebook。 先只做 排程發布,不要一開始就追求即時按鈕。 先只處理 單張圖片 + 單段文案,避免一開始就挑戰輪播、影片或多素材邏輯。 先把 成功回寫與失敗紀錄 做好,再談擴充。 只要這四件事能穩定運作,你就已經打下很強的基礎。之後再加入更多平台、更多模板、更多審核流程,整體成本會低很多。 常見問答(FAQ)Q:我可以把整套流程做到全自動,完全不人工審核嗎?A:可以,但不建議作為日常商業流程的預設模式。AI 生成內容仍可能出現事實錯誤、語氣失準、錯別字、敏感用語或品牌不一致的問題。對企業與品牌來說,省下幾分鐘審稿時間,通常不值得拿品牌風險去交換。比較務實的做法是保留人工核准,只把重複性工作交給系統。 Q:我不會寫程式,也能做出這套 n8n + Notion 工作流嗎?A:可以。這套流程本質上屬於低程式碼(Low-code)範圍,大部分邏輯都能透過 n8n 節點與 Notion 資料庫完成。真正需要花時間的不是寫程式,而是把欄位、狀態、平台規則與錯誤處理想清楚。只要流程設計正確,新手也能先做出可用版本。 Q:為什麼我在 X 或 Instagram 發文時常常失敗?A:最常見原因不是 n8n 壞掉,而是資料不符合平台規則。X 常見問題是字數超限;Instagram 常見問題是缺少必要素材、圖片格式不符,或 API 權限未設定完整。建議把不同平台的文案與素材拆欄位管理,並在發送前加入資料檢查節點,先擋掉不合法資料。 Q:Google Sheets 跟 Notion,我到底該選哪一個?A:如果你只是想快速驗證概念、資料欄位也很單純,Google Sheets 會更快上手;但如果你希望流程能長期維護、多人審稿、清楚管理狀態與內容,Notion 會更適合。簡單說,Sheets 比較像暫時資料表,Notion 比較像內容中台。 Q:我應該先做排程發布,還是直接做 Webhook 即時發布?A:建議先做排程。因為新手最需要先驗證的是資料結構、狀態流程與平台發布是否穩定,而不是「有沒有即時」。等到排程模式穩定之後,再把同樣邏輯改成 Webhook 觸發,風險會低很多。 Q:如果同一篇內容要發到 Facebook、Instagram、X,需要共用同一份文案嗎?A:不建議。你可以共用同一個主題與核心訊息,但每個平台最好有自己的 Caption。不同平台的字數限制、閱讀節奏、Hashtag 使用方式與 CTA 風格都不同。若硬要共用同一段文案,失敗率與成效不佳的機率都會提高。 Q:預約發布該怎麼設計比較安全?A:最穩的做法是同時判斷兩個條件:Status = Ready 且 Scheduled Time <= 現在時間。此外,成功發送後要立刻把狀態改成 Published,避免下一次排程重複發文。若流程可能重跑,也可以額外加入「已發送平台 ID」或「發布鎖」欄位做保護。 Q:如果 API 發布失敗,應該怎麼補救?A:不要只讓流程報錯結束,最好把錯誤寫回 Notion。建議至少回寫 Failed 狀態、錯誤訊息、失敗時間,必要時再發 Slack 或 Email 通知。這樣團隊成員能快速知道是哪一篇、哪個平台失敗,以及該補哪一段資料。 Q:這套流程適合哪些人先導入?A:最適合的是有固定內容產出節奏的人或團隊,例如自媒體經營者、內容行銷團隊、接案代操、顧問型品牌、課程講師與中小企業。只要你每週都在重複做「整理文案、人工貼上、切平台、確認格式」這些事,就很適合先從半自動化開始。

  • article-如何用 n8n 打造 AI Agent 專屬記憶庫?Logging 實戰 | (EP.7) n8n 自動化 API 串接教學

    2026/3/31

    AI自動化 n8n API串接
    如何用 n8n 打造 AI Agent 專屬記憶庫?Logging 實戰 | (EP.7) n8n 自動化 API 串接教學
    為什麼 AI 需要 Logging?告別「金魚腦」的專屬黑盒子當我們將 LINE 等通訊軟體與 AI 結合時,常常會遇到一個災難現場:AI 沒有記憶。它無法記住上一秒的對話,導致每次回覆都像初次見面,甚至引發錯誤(Error)。 Logging 的本質,就是系統的專屬「黑盒子」。一句話講完:Logging 就是為了讓你在事後「看得懂發生過什麼事」。 在 n8n 或任何自動化流程中,Log 不僅僅是無聊的資料,它是拯救開發者的五大超能力: 除錯 (Debug): 沒紀錄只能瞎猜,有紀錄就能精準抓蟲。 重現 (Reproduce): 還原當下觸發錯誤的 Prompt。 監控 (Monitor): 追蹤系統的成功率與 API 呼叫狀況。 優化 (Optimize): 作為未來訓練 AI 的精華資料。 稽核 (Audit): 有紀錄才有證據,知道哪個環節出錯。 記憶的進化:從「給人看」到「給 AI 看」Logging 的應用可以分為兩個階段: 階段一:只做 Logging(給人看)為了事後查看、稽核執行結果。我們開始把資料寫入 Google Sheets,方便我們進行 Debug 與狀態確認。 階段二:Logging + 給 AI 查(AI 也要看)當 AI 需要記得前文、參考歷史紀錄,或是做進階的資料檢索(RAG)時,Log 就直接升級成了 AI 的 Context(上下文)。這能讓 AI 瞬間恢復記憶,甚至做到個人化預測與專屬知識庫的搭建。 踩雷警告!為什麼讓 AI 自動撈資料(Tool Calling)是一場災難?很多新手會有一個致命誘惑:「既然 AI 這麼聰明,不要在 Workflow 查了,直接寫個 Tool 讓 AI 自己去 Google Sheet 撈資料(Tool Calling)不是更簡單?」 想法很完美,但對於新手與固定規則的任務來說,這是一個巨大的陷阱! 失控的可控性: AI 可能亂查太多的資料、查錯條件,甚至決定「不查了」,看它心情做事。 成本超級高: 先讓 LLM 判斷 → 呼叫工具 → 再回傳 LLM,速度極慢且狂燒 Token。 除錯大地獄: 出錯時你根本不知道是 Prompt 寫壞、工具沒寫好,還是 AI 邏輯當機。 架構大 PK:Workflow 先查 vs. AI Agent 自己撈 比較項目 Workflow 先查 (Pre-fetch / 固定 Context) AI Agent 自己撈 (Tool Calling / 動態查詢) 適用情境 固定規則(例:每次都查最新 3 筆對話) 動態條件(例:查閱特定主題的歷史紀錄) 穩定度 極高 (100% 執行) 較低(看 AI 心情決定是否呼叫) 除錯難度 簡單清晰 複雜地獄 花費成本 低 高 專家建議: 在進入 AI 節點前,先由 Workflow 流程預先整理好必要資訊(固定 Context),確保 AI 每次都有穩定、可控的上下文。只有在遇到「不確定需求」或「延伸問題」時,才交由 Agent 進行動態查詢。 完整 Workflow 架構解析本次實作的完整流程共分為 三大區段,以下逐一拆解。 區段一:接收與解析 LINE 訊息1Webhook1 → Edit Fields1 Webhook1 接收 LINE Messaging API 傳入的 POST 請求(路徑:line-0324)。 Edit Fields1 負責從 LINE 事件結構中提取三個關鍵欄位: 欄位 來源路徑 說明 input_text body.events[0].message.text 使用者輸入的訊息 userId body.events[0].source.userId 識別使用者身份 replyToken body.events[0].replyToken LINE 回覆用的一次性 Token 區段二:建立固定 Context(Pre-fetch 記憶)1Get User History → Build History Context → Debug History Preview 這是整個架構的靈魂——在 AI 介入前,由 Workflow 自行整理好歷史記憶。 Get User History:從 Google Sheets 的 logging 工作表中讀取所有紀錄。 Build History Context:透過 Code 節點對資料進行篩選與格式化: const current = $('Edit Fields1').first().json; const currentUserId = current.userId || ''; const rows = $input.all().map(item => item.json); // 只取同一位使用者、狀態為 success 的最近 3 筆紀錄 const filtered = rows .filter(row => row.userId === currentUserId && row.status === 'success') .sort((a, b) => { const ta = new Date(a.timestamp || 0).getTime(); const tb = new Date(b.timestamp || 0).getTime(); return tb - ta; }) .slice(0, 3); const historyContext = filtered.length ? filtered.map((row, idx) => { return `${idx + 1}.\n時間:${row.timestamp || ''}\n輸入:${row.input_text || ''}\n摘要:${row.summary || ''}\n分類:${row.category || ''}\n關鍵字:${row.keywords || ''}`; }).join('\n\n') : '無歷史紀錄'; return [ { json: { ...current, history_context: historyContext, history_count: filtered.length } } ]; 核心邏輯: 過濾條件為「userId 相同」且 status === 'success',排除失敗紀錄後,取最新 3 筆,確保注入 AI 的都是可靠的高品質記憶。 Debug History Preview:在此節點新增 debug_ 前綴欄位(debug_userId、debug_history_count、debug_history_context 等),讓你在 n8n 執行面板中可以直接確認「AI 即將收到的歷史資料長什麼樣子」,是開發初期排查問題的透明窗口。 區段三:AI 分析、回覆 LINE 與寫入 Log1AI Agent1 → HTTP Request1 → Prepare Log1 → Google Sheets Log1 AI Agent1(搭配 Google Gemini)接收完整的 Context 後進行分析。 Prompt 設計如下: 請讀取以下內容,輸出摘要、主題分類、3 個關鍵字。 本次使用者輸入:{{$json.input_text}} 以下是此使用者最近的互動紀錄,僅供理解上下文與延續語意。 若與本次輸入無關,請以本次輸入為主: {{$json.history_context}} System Message 明確規範 AI 行為,防止幻覺與格式失控: 你是資料整理助手。 規則: 1. 使用繁體中文。 2. 不要捏造未提供的資訊。 3. 請只輸出 JSON。 4. 欄位必須包含 summary、category、language、keywords。 5. 若有歷史互動紀錄,僅可用來補足上下文,不可把過去內容誤當成這次輸入內容。 6. 若本次輸入與歷史紀錄無明顯關聯,請忽略歷史紀錄。 7. 若資訊不足,也必須輸出合法 JSON。 Structured Output Parser 確保 AI 輸出符合以下 JSON Schema,強制欄位驗證,防止格式亂跑: { "type": "object", "properties": { "summary": { "type": "string" }, "category": { "type": "string", "enum": ["AI工具", "程式開發", "商業", "教育", "其他"] }, "keywords": { "type": "array", "items": { "type": "string" } }, "language": { "type": "string" } }, "required": ["summary", "category", "keywords", "language"], "additionalProperties": false } HTTP Request1 呼叫 LINE Reply API,將 AI 分析結果回傳給使用者。 Prepare Log1 + Google Sheets Log1 將本次執行的完整資料寫回 Google Sheets,包含以下欄位: 欄位 說明 timestamp 執行時間($now) workflow Workflow 名稱 status 執行狀態(success) executionId n8n 執行 ID userId / replyToken 使用者識別資訊 input_text 本次輸入 summary / category / language / keywords AI 分析結果 history_count 本次帶入的歷史筆數 history_context 實際注入 AI 的歷史文字 設計亮點: 記錄 history_count 與 history_context 讓你未來可以回溯「AI 當時看到的是什麼」,是除錯 Hallucination 的關鍵利器。 🎁 附錄:完整 n8n Workflow JSON 腳本你可以直接複製以下 JSON 程式碼,並匯入至你的 n8n 專案中進行測試。匯入後請記得將 Google Sheets 文件 ID 替換為你自己的試算表 ID,並重新設定 Google Sheets 與 LINE Bearer Token 的憑證(Credentials)。 { "nodes": [ { "parameters": { "httpMethod": "POST", "path": "line-0324", "options": {} }, "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [-1392, -288], "id": "07a2efae-2d4c-4b63-ab6d-38366b22f782", "name": "Webhook1", "webhookId": "a6ff766a-b5f5-4d36-9066-963fc0e4407f" }, { "parameters": { "assignments": { "assignments": [ { "id": "820ce329-eb3e-46b7-8264-c13e6bab20e1", "name": "input_text", "value": "={{ $json.body.events[0].message.text }}", "type": "string" }, { "id": "b9d8194d-1c76-4ad3-a2ab-95fc0db6a001", "name": "userId", "value": "={{ $json.body.events[0].source.userId || '' }}", "type": "string" }, { "id": "6fbbf8c4-4044-41a6-9721-4f11ec8b0001", "name": "replyToken", "value": "={{ $json.body.events[0].replyToken || '' }}", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [-1152, -288], "id": "d03c1de9-2b49-4ed0-98a3-d79d0cf0bb96", "name": "Edit Fields1" }, { "parameters": { "documentId": { "__rl": true, "value": "YOUR_GOOGLE_SHEET_ID", "mode": "list", "cachedResultName": "google sheet logging" }, "sheetName": { "__rl": true, "value": "gid=0", "mode": "list", "cachedResultName": "logging" }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4, "position": [-912, -288], "id": "0ff96efd-5db8-45c3-a43e-0b1db98e31b3", "name": "Get User History" }, { "parameters": { "jsCode": "const current = $('Edit Fields1').first().json;\nconst currentUserId = current.userId || '';\n\nconst rows = $input.all().map(item => item.json);\n\nconst filtered = rows\n .filter(row => row.userId === currentUserId && row.status === 'success')\n .sort((a, b) => {\n const ta = new Date(a.timestamp || 0).getTime();\n const tb = new Date(b.timestamp || 0).getTime();\n return tb - ta;\n })\n .slice(0, 3);\n\nconst historyContext = filtered.length\n ? filtered.map((row, idx) => {\n return `${idx + 1}.\\n時間:${row.timestamp || ''}\\n輸入:${row.input_text || ''}\\n摘要:${row.summary || ''}\\n分類:${row.category || ''}\\n關鍵字:${row.keywords || ''}`;\n }).join('\\n\\n')\n : '無歷史紀錄';\n\nreturn [\n {\n json: {\n ...current,\n history_context: historyContext,\n history_count: filtered.length\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [-672, -288], "id": "fe67f1ce-515a-471c-8fda-cdabfe30b00b", "name": "Build History Context" }, { "parameters": { "assignments": { "assignments": [ { "name": "debug_userId", "value": "={{ $json.userId }}", "type": "string" }, { "name": "debug_input_text", "value": "={{ $json.input_text }}", "type": "string" }, { "name": "debug_history_count", "value": "={{ $json.history_count }}", "type": "string" }, { "name": "debug_history_context", "value": "={{ $json.history_context }}", "type": "string" } ] }, "includeOtherFields": true, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [-432, -288], "id": "61c5f27f-f9e9-4eb1-93eb-3f18ea2b586f", "name": "Debug History Preview" }, { "parameters": { "promptType": "define", "text": "=請讀取以下內容,輸出摘要、主題分類、3 個關鍵字。\n\n本次使用者輸入:{{$json.input_text}}\n\n以下是此使用者最近的互動紀錄,僅供理解上下文與延續語意。若與本次輸入無關,請以本次輸入為主:\n{{$json.history_context}}", "hasOutputParser": true, "options": { "systemMessage": "=你是資料整理助手。\n\n規則:\n1. 使用繁體中文。\n2. 不要捏造未提供的資訊。\n3. 請只輸出 JSON。\n4. 欄位必須包含 summary、category、language、keywords。\n5. 若有歷史互動紀錄,僅可用來補足上下文,不可把過去內容誤當成這次輸入內容。\n6. 若本次輸入與歷史紀錄無明顯關聯,請忽略歷史紀錄。\n7. 若資訊不足,也必須輸出合法 JSON。" } }, "type": "@n8n/n8n-nodes-langchain.agent", "typeVersion": 3.1, "position": [-176, -288], "id": "e969d51a-4654-4d55-85ae-4c8efe0c3fa0", "name": "AI Agent1" }, { "parameters": { "options": {} }, "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini", "typeVersion": 1, "position": [-224, -64], "id": "8454dd71-055e-43b5-b445-5289f37fc7f8", "name": "Google Gemini Chat Model1" }, { "parameters": { "schemaType": "manual", "inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"summary\": { \"type\": \"string\" },\n \"category\": {\n \"type\": \"string\",\n \"enum\": [\"AI工具\", \"程式開發\", \"商業\", \"教育\", \"其他\"]\n },\n \"keywords\": {\n \"type\": \"array\",\n \"items\": { \"type\": \"string\" }\n },\n \"language\": { \"type\": \"string\" }\n },\n \"required\": [\"summary\", \"category\", \"keywords\", \"language\"],\n \"additionalProperties\": false\n}" }, "type": "@n8n/n8n-nodes-langchain.outputParserStructured", "typeVersion": 1.3, "position": [16, -64], "id": "b9638a0d-bfc5-4af6-8dbc-91f225223b3a", "name": "Structured Output Parser1" }, { "parameters": { "method": "POST", "url": "https://api.line.me/v2/bot/message/reply", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "sendHeaders": true, "headerParameters": { "parameters": [{ "name": "Content-Type", "value": "application/json" }] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"replyToken\":\"{{ $('Webhook1').item.json.body.events[0].replyToken }}\",\n \"messages\":[\n {\n \"type\": \"text\",\n \"text\": \"【summary】{{ $json.output.summary }} \\n【category】{{ $json.output.category }} \\n【language】{{ $json.output.language }} \\n【關鍵字】{{ $json.output.keywords.join('、') }}\"\n }\n ]\n}", "options": {} }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.4, "position": [208, -288], "id": "371bad55-6ecb-4cc0-8ef2-3a8a0d9ffdc7", "name": "HTTP Request1" }, { "parameters": { "assignments": { "assignments": [ { "name": "timestamp", "value": "={{ $now }}", "type": "string" }, { "name": "workflow", "value": "={{ $workflow.name }}", "type": "string" }, { "name": "status", "value": "success", "type": "string" }, { "name": "executionId", "value": "={{ $execution.id }}", "type": "string" }, { "name": "userId", "value": "={{ $('Webhook1').item.json.body.events[0].source.userId || '' }}", "type": "string" }, { "name": "replyToken", "value": "={{ $('Webhook1').item.json.body.events[0].replyToken || '' }}", "type": "string" }, { "name": "input_text", "value": "={{ $('Edit Fields1').item.json.input_text || '' }}", "type": "string" }, { "name": "summary", "value": "={{ $('AI Agent1').item.json.output.summary || '' }}", "type": "string" }, { "name": "category", "value": "={{ $('AI Agent1').item.json.output.category || '' }}", "type": "string" }, { "name": "language", "value": "={{ $('AI Agent1').item.json.output.language || '' }}", "type": "string" }, { "name": "keywords", "value": "={{ $('AI Agent1').item.json.output.keywords ? $('AI Agent1').item.json.output.keywords.join('、') : '' }}", "type": "string" }, { "name": "history_count", "value": "={{ $('Debug History Preview').item.json.history_count || 0 }}", "type": "string" }, { "name": "history_context", "value": "={{ $('Debug History Preview').item.json.history_context || '' }}", "type": "string" } ] }, "options": {} }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [448, -288], "id": "920dbd89-1a14-47ce-85f2-cf2bd86b93ba", "name": "Prepare Log1" }, { "parameters": { "operation": "append", "documentId": { "__rl": true, "value": "YOUR_GOOGLE_SHEET_ID", "mode": "list", "cachedResultName": "google sheet logging" }, "sheetName": { "__rl": true, "value": "gid=0", "mode": "list", "cachedResultName": "logging" }, "columns": { "mappingMode": "defineBelow", "value": { "timestamp": "={{ $json.timestamp }}", "workflow": "={{ $json.workflow }}", "status": "={{ $json.status }}", "executionId": "={{ $json.executionId }}", "userId": "={{ $json.userId }}", "replyToken": "={{ $json.replyToken }}", "input_text": "={{ $json.input_text }}", "summary": "={{ $json.summary }}", "category": "={{ $json.category }}", "language": "={{ $json.language }}", "keywords": "={{ $json.keywords }}", "history_count": "={{ $json.history_count }}", "history_context": "={{ $json.history_context }}" } }, "options": {} }, "type": "n8n-nodes-base.googleSheets", "typeVersion": 4, "position": [704, -288], "id": "89ea098f-51c3-4aa2-8632-8c0114eb5b92", "name": "Google Sheets Log1" } ], "connections": { "Webhook1": { "main": [[{ "node": "Edit Fields1", "type": "main", "index": 0 }]] }, "Edit Fields1": { "main": [[{ "node": "Get User History", "type": "main", "index": 0 }]] }, "Get User History": { "main": [[{ "node": "Build History Context", "type": "main", "index": 0 }]] }, "Build History Context": { "main": [[{ "node": "Debug History Preview", "type": "main", "index": 0 }]] }, "Debug History Preview": { "main": [[{ "node": "AI Agent1", "type": "main", "index": 0 }]] }, "AI Agent1": { "main": [[{ "node": "HTTP Request1", "type": "main", "index": 0 }]] }, "Google Gemini Chat Model1": { "ai_languageModel": [[{ "node": "AI Agent1", "type": "ai_languageModel", "index": 0 }]] }, "Structured Output Parser1": { "ai_outputParser": [[{ "node": "AI Agent1", "type": "ai_outputParser", "index": 0 }]] }, "HTTP Request1": { "main": [[{ "node": "Prepare Log1", "type": "main", "index": 0 }]] }, "Prepare Log1": { "main": [[{ "node": "Google Sheets Log1", "type": "main", "index": 0 }]] } } } 常見問答 (FAQ)Q:為什麼不直接使用 OpenAI 或 Gemini 內建的 Memory 功能?A:雖然部分 LLM 提供內建對話記憶,但對於自動化流程開發者來說,那就像一個無法受控的「黑盒子」。將 Memory 獨立存放在 Google Sheets 或資料庫中,能讓你隨時監控、修改、除錯(Debug),並確保 AI 不會產生難以追蹤的幻覺(Hallucination)。 Q:Google Sheets 適合拿來當作長期的大型資料庫嗎?A:對於新手測試、輕量級專案或概念驗證(PoC),Google Sheets 是完美且直觀的工具,因為它「視覺化且易懂」。但當你的系統上線且流量增大時,建議將 Logging 系統轉移至 PostgreSQL、MySQL 或 Supabase 等正規關聯式資料庫,以確保效能與穩定性。 Q:什麼時候才真正需要用到 Tool Calling(動態查詢)?A:當用戶的需求不確定或需要跨時間區間搜尋時。例如,當用戶問:「幫我整理『上個月』關於『商業策略』的所有新聞」,這種條件不固定的問題,Workflow 先查(固定 Context)無法預測,此時才適合讓 AI 透過 Tool Calling 自行下達條件去資料庫撈取資料。 Q:為什麼過濾歷史紀錄時要加上 status === 'success' 的條件?A:這是防止「垃圾記憶污染」的關鍵設計。如果 AI 曾因為網路錯誤、格式異常或 Token 不足而失敗,那筆紀錄的 input_text 或 summary 可能是空的或不完整的。把這些「失敗記憶」注入 AI,反而會讓 AI 困惑或輸出錯誤的摘要。只保留 success 的紀錄才能確保記憶品質。 Q:Debug History Preview 節點有什麼用?正式上線後可以刪掉嗎?A:Debug History Preview 是一個「透明窗口」節點,讓你在 n8n 執行面板中可以直接看到「AI 即將收到的歷史資料長什麼樣子」,在開發初期排查問題時非常有價值。正式上線後可保留(幾乎沒有效能開銷),作為日後維護時的快速診斷工具。若確定不再需要除錯,可刪除以精簡流程。 Q:Structured Output Parser 是什麼?為什麼要用它?A:Structured Output Parser 的作用是強制 AI 必須按照你定義的 JSON Schema 格式輸出,而不是隨意回傳文字。如果 AI 輸出不符合格式(例如缺少 summary 欄位),n8n 會自動觸發重試。這對於後續的 keywords.join('、') 等欄位操作至關重要——若格式不穩定,後面的節點就會直接報錯。 Q:category 欄位為什麼要用 enum 限制固定選項?A:這是維持資料一致性的核心手段。如果不加 enum,AI 可能今天回傳「AI 工具」、明天回傳「人工智慧工具」、後天回傳「AI Tools」,同一個意思三種寫法,導致你在 Google Sheets 篩選或後續統計時出現資料亂象。使用 enum: ["AI工具", "程式開發", "商業", "教育", "其他"] 強制統一格式,是設計健壯資料管線的基本功。 Q:為什麼要在 Log 中記錄 history_count 和 history_context?A:這是為了讓未來的你能「重現 AI 當時的視角」。假設某次 AI 回傳了奇怪的摘要,你打開 Log 可以直接看到:「當時 AI 帶了幾筆記憶(history_count)」以及「那些記憶的完整內容是什麼(history_context)」。沒有這兩個欄位,你只能看到輸出結果,完全無法判斷問題出在 Prompt、歷史資料,還是 AI 本身。 Q:Prompt 中說「若與本次輸入無關,請以本次輸入為主」,這樣真的有效嗎?A:這條規則能降低 AI 誤用歷史資料的機率,但不能完全保證。因此 System Message 中同時加入了「不可把過去內容誤當成這次輸入內容」的明確規則,雙重防護。若主題相近偶爾仍可能混淆,此時可考慮在 Prompt 中加入更明確的分隔標記(如 ---歷史紀錄開始---)來強化區隔。 Q:replyToken 是什麼?為什麼要記錄在 Log 裡?A:LINE 的 replyToken 是一個一次性、有時效的 Token(約 30 秒有效),用來對應「這次訊息的回覆權限」。HTTP Request 節點使用它呼叫 LINE Reply API 回傳訊息給使用者。記錄在 Log 中,是為了當回覆失敗時,你可以確認 Token 是否正確傳遞(雖然過了時效無法重新使用,但至少能確認資料流程正確)。 Q:如果使用者是第一次傳訊息(沒有任何歷史紀錄),流程會出錯嗎?A:不會。Build History Context 節點在 filtered.length 為 0 時,會將 history_context 設為 '無歷史紀錄' 字串,並將此值傳給 AI Prompt。AI 收到這個明確的提示後,會直接以本次輸入為唯一依據進行分析,完全不受影響。