
6
補 : 上了官方模版 https://n8n.io/workflows/6264-auto-expense-tracker-from-line-messages-with-gpt-4-and-google-sheets/
一開始在 threads 看到有人分享 n8n藥單辨識 Extract Structured Data from Medical Documents with Google Gemini AI 的模板
好像可以把裡面的圖片辨識節點拿來用?
自己常常忘記記帳,月底想不起來刷卡外的花費,於是萌生了做一個可以口述文字/圖片辨識記帳機器人的想法
因為是要隨時回傳的,這次部署在 Zeabur
先用 postman 測試 Zeabur 能不能收到圖片
找了張圖片上傳 Postimages
Webhook
點 listen for event
到 Postman,選 POST + 貼上 test url
Header
Body
Send -> 回到 Webhook
確定有收到

創一個 provider -> create new channel 選 Messaging API -> 填寫 Channel 資訊




Line 有更新,現在需要進到 LINE Official Account Manager 啟用 API
點進設好的 Provider -> 右上角齒輪 設定 -> Messaging API -> 啟用




創建成功
回到 line,會看到設好的 line bot
現在再回到 Line Developers 重整
剛才的 Provider 就會有資料了

n9n 新增第一個節點
test 的網址複製下來 -> 按 listen for the test event
回到 Line Developers
點 Messaging API settings
把 n8n Webhook test-url 貼上 -> verify
出現 success 代表有成功收到
PS 下方 use Webhook 要打開
點選 Listen for the test event (接下來都是在測試環境,傳任何訊息都要先點一次) -> line bot 發送訊息
Webhook 就會出現了
line bot 回覆的訊息先不管它,最後再來改
回到 n8n Webhook 節點有收到資料就可以繼續
新增分流節點
分出收到的訊息是文字 text / 圖片 image

設一個 set 提取 text
接 AI Agent
prompt
分析出資訊:
先判斷是否為記帳相關明細、發票,若不是則不需要處理,直接停止所有流程。若是記帳相關,分析出六個資訊:
1. 日期(如果只提到'今天',用{{ $now.format('YYYY-MM-DD') }} ) 2. 通路 3. 通路類型(只能為:便利商店、個人用品店、量販超市、傳統市場、網路購物、藥局、五金百貨、餐廳小吃店、醫療院所、3C商場、航空客運、軟體儲值、加油交通儲值、線上課程、電信公司)4. 花費明細 5. 金額 6. 類別(只能為:家用、正餐飲食、飲料甜點、生活用品、美妝、衣服飾品、交通、娛樂、電信、醫療、3C、軟體、學習、旅遊)。若通路類型判斷為航空客運,類別一定為旅遊。若無法判斷或沒資訊,請填入'DN',每格都要有資料。請輸出為 JSON 格式,例如:{\"日期\": \"...\", \"通路\": \"...\", \"通路類型\": \"...\", \"花費明細\": \"...\", \"金額\": \"...\", \"類別\": \"...\"}"
下面 Chat Model 點開,放 api key + 選模型

設一個 Structured Output Parser,指定輸出格式

設一個 Https
回到 Line Developers
點 Messaging API settings
滑到下面,產生一個 Channel access token 複製下來
Send Headers 打開
Name: Authorization
Value: Bearer Channel access token


之後一樣接 AI Agent
PS 這裡只是想試試藥單辨識模板的做法,如果不想用 gemini,可以直接跳過這段,最後我的模板只接 AI Agent

設一個 Https
這裡改用 gemini
回到 Line Developers
點 Messaging API settings
滑到下面,產生一個 Channel access token 複製下來
Send Headers 打開
Name: Authorization
Value: Bearer Channel access token


設一個 Extract from File
從檔案中擷取出特定的資料內
設一個 set (prepare for AI)
只提取裡面的 data
設一個 Https
image post 到gemini
https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent

設一個 code 整理
請 gpt 幫我寫的
return items.map(item => {
// 1. 提取 JSON 數據
if (item.json.candidates?.[0]?.content?.parts?.[0]?.text) {
const textContent = item.json.candidates[0].content.parts[0].text;
// 移除 Markdown 的 ```json\n 和 \n``` 標記
const jsonString = textContent.replace(/^```json\n/, '').replace(/\n```$/, '');
try {
const parsedData = JSON.parse(jsonString);
// 將解析後的數據合併到 item.json 中
item.json = { ...item.json, ...parsedData };
// 可選:刪除原始的 Gemini API 響應結構,如果你不再需要它
delete item.json.candidates;
delete item.json.usageMetadata;
delete item.json.modelVersion;
delete item.json.responseId;
} catch (e) {
console.error("Error parsing JSON from Gemini text content:", e);
// 處理解析錯誤,例如設置一個錯誤標誌或保留原始數據
}
}
// 2. 提取 replyToken (如果 Gemini API 的輸出中也包含 LINE 的 body/events 結構)
// 這通常發生在 Line Trigger 直接連接到 Gemini 節點時
if (item.json.body?.events?.[0]?.replyToken) {
item.json.replyToken = item.json.body.events[0].replyToken;
delete item.json.body; // 刪除原始的 body 結構
}
return item; // 返回處理後的項目
});

為了防止自己重複紀錄,這裡提取去重使用欄位,之後用來比對

寫入資料前,比對 google 去重使用欄位是不是一樣
一樣 -> 不寫入
不一樣 -> 寫入
設一個 google sheet
設一個 get rows in sheet
假設有資料,output 會讀到
接著要判斷,這次新資料的去重使用欄位,是否存在於 get rows in sheet 的去重使用欄位
一開始我直接用 if 判斷,發現不能這樣做,就先提取到 list,再做比對
之後 Darrell 教我使用了 Aggregate
設一個 Aggregate
設一個 merge_all 把要比對的兩份資料合起來
選 combine position
我希望不管寫入或不寫入,都要回傳訊息,Line Bot 使用當個會話的 replytoken 來 reply
一開始我用 if 判斷,要回傳資料發現 Line Bot 的 replyToken 只能使用一次,但 n8n 會每個分支都跑過一遍,就改成用 userId POST, Darrell review 幫我改成 Switch,方便很多

邏輯是
去重使用欄位沒有重複 -> 寫入,回傳"✅ 記帳成功 <明細> "
去重使用欄位 = 'DN-DN-DN-DN' -> 不寫入,回傳"不相關明細或圖片,不會計入"
(前面下的prompt,判斷不出來,所有欄位會是DN,去重使用欄會出現'DN-DN-DN-DN')
去重使用欄位 正則表達配對 '^.*-DN-DN-DN


設一個 google sheet
設一個 Https
url = https://api.line.me/v2/bot/message/reply
Send Headers 打開
Name: Authorization
Value: Bearer Channel access token
Name: Content-Type
Value: application/json
Body json
{
"replyToken": "{{ $('Webhook').item.json.body.events[0].replyToken }}",
"messages": [
{
"type": "text",
"text": "✅ 記帳成功: {{ $('Merge_all').item.json['去重使用'] }}"
}
]
}


設一個 Https
url = https://api.line.me/v2/bot/message/reply
Send Headers 打開
Name: Authorization
Value: Bearer Channel access token
Name: Content-Type
Value: application/json
Body json
{
"replyToken": "{{ $('Webhook').item.json.body.events[0].replyToken }}",
"messages": [
{
"type": "text",
"text": "不相關明細或圖片,不會計入"
}
]
}


設一個 Https
url = https://api.line.me/v2/bot/message/reply
Send Headers 打開
Name: Authorization
Value: Bearer Channel access token
Name: Content-Type
Value: application/json
Body json
{
"replyToken": "{{ $('Webhook').item.json.body.events[0].replyToken }}",
"messages": [
{
"type": "text",
"text": "⚠️ 此筆資料已記錄過,不會重複記帳"
}
]
}


進到 LINE Official Account Manager
點頭像可以直接改

自動回應訊息

基本檔案 -> 改背景圖

回到 n8n
上面的 Active 打開
Webhook,複製 Production URL
回到 Line Developers
點 Messaging API settings
改 n8n Webhook url -> verify
出現 success 代表有成功收到,部署成功

月底拉個樞紐分析圖表,就會很清楚花費佔比了
-> 不寫入,回傳"不相關明細或圖片,不會計入" (我有時候手殘,打到一半就按到送出,去重使用欄會出現'2025-01-01-DN-DN-DN) 去重使用欄位 = '---' ->不寫入,回傳"不相關明細或圖片,不會計入" (傳不相關照片時,有可能會全空值,去重使用欄會出現'---') 去重使用欄位重複 -> 不寫入,回傳"⚠️ 此筆資料已記錄過,不會重複記帳"


設一個 google sheet
設一個 Https
url = https://api.line.me/v2/bot/message/reply
Send Headers 打開
Name: Authorization
Value: Bearer Channel access token
Name: Content-Type
Value: application/json
Body json


設一個 Https
url = https://api.line.me/v2/bot/message/reply
Send Headers 打開
Name: Authorization
Value: Bearer Channel access token
Name: Content-Type
Value: application/json
Body json


設一個 Https
url = https://api.line.me/v2/bot/message/reply
Send Headers 打開
Name: Authorization
Value: Bearer Channel access token
Name: Content-Type
Value: application/json
Body json


進到 LINE Official Account Manager
點頭像可以直接改

自動回應訊息

基本檔案 -> 改背景圖

回到 n8n
上面的 Active 打開
Webhook,複製 Production URL
回到 Line Developers
點 Messaging API settings
改 n8n Webhook url -> verify
出現 success 代表有成功收到,部署成功

月底拉個樞紐分析圖表,就會很清楚花費佔比了