上週從Pocket跳船到 Karakeep 之後,由於 IFTTT 不支援 Karakeep,之前 IFTTT 上的「打星就分享到 twitter流程」就沒得用了。
正好也想用 n8n 玩點花樣,於是就到 n8n 上試著寫個 workflow 來做這件事。
個人血淚提醒:
使用 docker compose down 時千萬不要加 “- v” (也就是不要下 docker compose down -v)。
一般來說 -v 會讓人想到 –verbose;但在 docker compose 裡是把已經建好的 volume 移除 (remove),包含之前所有輸入的資料,workflow,以及設定。我寫到一半要加個功能,想要rebuild container時,粗心大意直接 copy & paste chatGPT 給的指令,然後兩天的心血就…消失了。後面問 chatGPT 他還理直氣壯說我又沒有說要保存 volume 資料… Orz
以下的內容是先請 AI 分析我寫的 workflow,然後我再補充。
這樣產生說明文件的方式真的很快。不過某些我覺得重要的節點(node)還是會被略過。得要手動指定或是手動加入。
workflow 拆成兩個部份:
- Karakeep_webhook_queue.json:接收 Karakeep 更新書籤時所送出的 webhook,稍後由 Karakeep_share 批次處理。
- Karakeep_share.json:把已經打星的書籤分享到 Twitter,並把已經分享過的書籤歸到 “shared” 列表中。
我請AI 從功能、架構與流程、重要節點的設定方式等三個面向進行分析。
1. Karakeep_webhook_queue.json

功能
Karakeep_webhook_queue.json
是一個專為處理 Karakeep webhook 事件設計的 workflow。其主要目的是接收來自 Karakeep 的 webhook 請求(例如書籤更新事件),將相關數據(如 jobId
和 bookmarkId
)緩存到文件中,並在一定時間後觸發另一個 workflow 進行批次處理。這種設計特別適用於應對可能短時間內大量觸發的 webhook 事件,通過緩存避免過載並實現批次處理。
架構與流程簡介
這個 workflow 的架構由以下核心節點組成:
- Webhook 節點:作為入口,接收 POST 請求。
- Set 節點:從請求中提取關鍵數據並格式化。
- Execute Command 節點:用於文件操作,包括寫入隊列 (queue) 和設置獨占標誌。
- IF 節點:判斷是否成為處理者。
- Wait 節點:延遲執行以收集更多事件。
- Execute Workflow 節點:觸發後續處理。
流程如下:
- Webhook 節點接收 Karakeep 的 POST 請求。
- Set 節點提取
jobId
和bookmarkId
,生成 JSON 對象。 - Execute Command 節點將數據追加到
/files/karakeep-webhook-queue.txt
,使用flock
避免同時寫入的競爭條件 (race condition)。 - 另一個 Execute Command 節點嘗試設置獨占標誌
/files/karakeep-process.flag
,檢查是否已有處理者在運行。 - 如果成為處理者,Wait 節點等待 10 秒,收集更多 webhook 事件 (當在 Karakeep 使用批次處理時)。
- Execute Workflow 節點觸發 “
Karakeep_share
” 這個 workflow,進行後續的處理。 - 最後清除隊列文件和獨占標誌。
重要節點的設定方式
以下說明各重要節點在 n8n 中的具體設定方式:
- Webhook 節點
- 節點類型:
n8n-nodes-base.webhook
- 設定步驟:
- 在 n8n 介面中添加 Webhook 節點。
- 設置 HTTP Method 為
POST
,用於接收 Karakeep 的 webhook 請求。 - 設置 Path 為
karakeep-bookmark-update-XXXX
,作為此 workflow 的唯一路徑。 - 在 Authentication 中選擇
Header Auth
,並配置自定義憑證(例如 API 密鑰),確保安全性。這裡的 key 要跟 Karakeep 那邊的 webhook 觸發設定一致。
- 用途:此節點作為 workflow 的觸發點,接收 Karakeep 請求並傳遞數據給後續節點。
- 節點類型:
- Set 節點
- 節點類型:
n8n-nodes-base.set
- 設定步驟:
- 添加 Set 節點並選擇 Mode 為 JSON,以手動定義輸出數據。
- 在 JSON Output 欄位中輸入表達式,例如:
{"jobid": "{{ $json.body.jobId }}", "bookmarkid": "{{ $json.body.bookmarkId }}", "time": {{ $now }}, "log_str": "{{ $now.toISO() }} - {{ $json.body.jobId }} - {{ $json.body.bookmarkId }}"}
- 確保表達式正確引用上游 Webhook 節點的輸入數據(如
body.jobId
)。
- 用途:用於從 webhook 請求中提取並格式化關鍵數據,生成結構化的 JSON 輸出。
- 節點類型:
- Execute Command 節點(寫入隊列)
- 節點類型:
n8n-nodes-base.executeCommand
- 設定步驟:
- 在 Command 欄位輸入以下 shell 命令:
echo "{{ $json.log_str }}" | flock -x /files/karakeep-webhook-queue.txt tee -a /files/karakeep-webhook-queue.txt
- 確保命令中使用
$json.log_str
引用 Set 節點生成的日誌字符串。
- 在 Command 欄位輸入以下 shell 命令:
- 用途:通過
flock
實現文件鎖定,將資料地安全追加到隊列文件中,避免同時寫入問題。
- 節點類型:
- Execute Command 節點(設立獨占標誌)
- 節點類型:
n8n-nodes-base.executeCommand
- 設定步驟:
- 在 Command 欄位輸入包含檔案檢查與鎖定邏輯的腳本,例如檢查檔案是否存在、過期處理(10 分鐘)並使用
flock
設置標誌。 - 確保腳本輸出 “Success” 或其他標識,以便後續 IF 節點判斷。
- 在 Command 欄位輸入包含檔案檢查與鎖定邏輯的腳本,例如檢查檔案是否存在、過期處理(10 分鐘)並使用
- 用途:管理獨占標誌,確保同一時間只有一個 workflow 實例處理隊列。
- 節點類型:
- IF 節點
- 節點類型:
n8n-nodes-base.if
- 設定步驟:
- 在 Conditions 中添加一個條件:
- 選擇
$json.stdout
(前一節點的輸出)。 - 設置操作為
Contains
,值為"Success"
。
- 選擇
- 配置 True 和 False 兩條分支,分別連接後續節點。
- 在 Conditions 中添加一個條件:
- 用途:根據獨占標誌設置的結果,決定是否繼續執行處理邏輯。
- 節點類型:
- Wait 節點
- 節點類型:
n8n-nodes-base.wait
- 設定步驟:
- 在 Amount 欄位設置為
10
,單位為Seconds
。
- 在 Amount 欄位設置為
- 用途:暫停 10 秒後再繼續,允許收集更多 webhook 事件以進行批次處理。
- 節點類型:
- Execute Workflow 節點
- 節點類型:
n8n-nodes-base.executeWorkflow
- 設定步驟:
- 在 Workflow ID 欄位選擇目標 workflow 的 ID,例如 (對應
Karakeep_share
)。 - 啟用 Wait for Sub-Workflow 選項,設為
true
,確保子流程完成後再繼續。
- 在 Workflow ID 欄位選擇目標 workflow 的 ID,例如 (對應
- 用途:觸發另一個 workflow 來處理緩存的隊列數據。
- 節點類型:
2. Karakeep_share.json

功能
Karakeep_share.json
的目標是從 Karakeep 獲取標記為收藏的書籤,過濾出鏈接類型書籤,根據其內容(note 或 highlight)生成 Twitter 推文內容,然後通過 IFTTT 的webhook發送推文 (這裡我偷懶了 XD),最後將書籤移動到 “shared” 列表。 (記得先在 Karakeep 建好 shared 這個list, 大小寫有分)
架構與流程簡介
這個 workflow 的架構包括:
- Manual Trigger / Execute Workflow Trigger 節點:觸發點。
- HTTP Request 節點:與 Karakeep API 交互。
- Split Out / Filter 節點:處理書籤數據。
- Set / IF / Code 節點:生成推文。
- Merge 節點:合併數據流。
- HTTP Request 節點:把推文送到 IFTTT 。
流程如下:
- 觸發 workflow。
- HTTP Request 節點查詢收藏書籤。
- Split Out 節點拆分書籤列表。
- Filter 節點篩選鏈接類型書籤。
- Set 節點提取關鍵資料。
- IF 節點檢查是否有
note
。 - 如果沒有 note,檢查是否有 highlight。若有,取得 highlight 內容。
- 如果連 highlight 也沒有,那就是最一般的情況。
- 用 Code 節點生成推文。
- 用 Merge 節點合併結果。
- 移動書籤並發送推文。
重要節點的設定方式
以下說明各重要節點在 n8n 中的具體設定方式:
- HTTP Request 節點(查詢書籤)
- 節點類型:
n8n-nodes-base.httpRequest
- 設定步驟:
- 設置 URL 為
https://[karakeep site]/api/v1/bookmarks/search
。 - 在 Authentication 中選擇
Generic Credential Type
,並配置Bearer Auth
憑證。 這裡的 API是要存取 Karakeep API 用的。需要在 Karakeep 的 user -> API 那邊建立並取得 API key。 - 在 Query Parameters 中添加:
q=is:fav -list:shared -list:Personal
includeContent=false
limit=20
- 設置 URL 為
- 用途:從 Karakeep API 獲取符合條件的書籤數據。
- 節點類型:
- Filter 節點
- 節點類型:
n8n-nodes-base.filter
- 設定步驟:
- 在 Conditions 中設置條件:
$json.content.type
等於"link"
。
- 在 Conditions 中設置條件:
- 用途:篩選出鏈接類型的書籤,過濾掉其他類型。
- 節點類型:
- Set 節點
- 節點類型:
n8n-nodes-base.set
- 設定步驟:
- 在 Assignments 中定義多個字段,例如:
id
:{{ $json.id }}
note
:{{ $json.note }}
content.url
:{{ $json.content.url }}
- 在 Assignments 中定義多個字段,例如:
- 用途:提取要分享的書籤內容資料。
- 節點類型:
- IF 節點(檢查 note)
- 節點類型:
n8n-nodes-base.if
- 設定步驟:
- 在 Conditions 中設置:
$json.note
不為空(Not Empty
)。
- 配置 True 和 False 分支。
- 在 Conditions 中設置:
- 用途:判斷書籤是否有 note,決定推文生成方式。
- 節點類型:
- HTTP Request 節點(取得Highlight)
- 節點類型:
n8n-nodes-base.httpRequest
- 設定步驟:
- 設置 URL 為
https://[karakeep site]/api/v1/bookmarks/{{ $json.id }}/highlights。
- 在 Authentication 中選擇
Generic Credential Type
,並配置Bearer Auth
憑證。 - 在 Query Parameters 中添加:
bookmarkId={{ $json.id }}
- 設置 URL 為
- 用途:從 Karakeep API 獲取符合條件的書籤數據。
- 節點類型:
- IF 節點(檢查 highlight)
- 節點類型:
n8n-nodes-base.if
- 設定步驟:
- 在 Conditions 中設置:
{{ $json.highlights[0].bookmarkId }}
不為空(Not Empty
)。
- 配置 True 和 False 分支。
- 在 Conditions 中設置:
- 用途:判斷書籤是否有 highlight,決定推文生成方式。
- 節點類型:
- Code 節點(生成推文)
- 節點類型:
n8n-nodes-base.code
- 設定步驟:
- 添加 Code 節點並設置 Mode 為
Run Once for Each Item
。 - 在 JS Code 欄位輸入自定義 JavaScript,例如計算字數、裁剪內容並生成推文。
- 添加 Code 節點並設置 Mode 為
- 用途:處理數據並生成符合 Twitter 要求的推文內容。
- 節點類型:
以下是我用來產生推文的程式碼,內容有點雜亂,供參考參考。
for note:
// 計算字數的函式
function countTwitterLength(text) {
const URL_LENGTH = 23;
let length = 0;
for (let char of text) {
length += char.charCodeAt(0) <= 0x7f ? 1 : 2;
}
return length;
}
const URL_LENGTH = 23;
const maxLen = 280;
const ending = '…';
// note 推文格式: Note (無引號) [讀] 標題 網址
// 計算標題長度
const tweet_title = '[讀] ' + $json.content.title;
const tweet_title_length = countTwitterLength(tweet_title);
$json.tweet_title = tweet_title;
$json.tweet_title_length = tweet_title_length;
const max_note_length = maxLen - URL_LENGTH - tweet_title_length - 3 - 3; // ... + 換行
// 裁剪 note 部份
let result = '';
let length = 0;
const tweet_note_orig = $json.note;
const tweet_note_orig_length = countTwitterLength(tweet_note_orig);
for (let i = 0; i < tweet_note_orig.length; i++) {
const char = tweet_note_orig[i];
const charLen = char.charCodeAt(0) <= 0x7f ? 1 : 2;
// 若加上這個字已經超過 maxLen,就停止,並補尾碼
if (length + charLen > max_note_length) {
result += ending;
break;
}
result += char;
length += charLen;
}
$json.tweet_note = result;
// 加回 標題 和 網址(每個網址算固定 23 字元)
$json.tweet_msg = $json.tweet_note + "\n\n" + $json.tweet_title + "\n" + $json.content.url;
return $json;
for hightlight:
// 計算字數的函式
function countTwitterLength(text) {
const URL_LENGTH = 23;
let length = 0;
for (let char of text) {
length += char.charCodeAt(0) <= 0x7f ? 1 : 2;
}
return length;
}
const URL_LENGTH = 23;
const maxLen = 280;
const ending = '…';
// note 推文格式: Note (無引號) [讀] 標題 網址
// 計算標題長度
const tweet_title = '[讀] ' + $json.content.title;
const tweet_title_length = countTwitterLength(tweet_title);
$json.tweet_title = tweet_title;
$json.tweet_title_length = tweet_title_length;
const max_note_length = maxLen - URL_LENGTH - tweet_title_length - 3 - 3 - 2; // ... + 換行 + 引號
// 裁剪 highlight 部份
let result = '';
let length = 0;
const tweet_note_orig = $json.highlights[0].text
const tweet_note_orig_length = countTwitterLength(tweet_note_orig);
for (let i = 0; i < tweet_note_orig.length; i++) {
const char = tweet_note_orig[i];
const charLen = char.charCodeAt(0) <= 0x7f ? 1 : 2;
// 若加上這個字已經超過 maxLen,就停止,並補尾碼
if (length + charLen > max_note_length) {
result += ending;
break;
}
result += char;
length += charLen;
}
$json.tweet_highlight = result;
// 加回 標題 和 網址(每個網址算固定 23 字元)
$json.tweet_msg = '"' + $json.tweet_highlight + '"' + "\n\n" + $json.tweet_title + "\n" + $json.content.url;
return $json;
- Merge 節點
- 節點類型:
n8n-nodes-base.merge
- 設定步驟:
- 添加 Merge 節點。
- 設置 Mode 為
Combine
,並選擇 Combine By 為Position
。
- 用途:合併來自不同分支的數據流,準備後續處理。
- 節點類型:
- HTTP Request 節點(get lists)
- 節點類型:n8n-nodes-base.httpRequest
- 設定步驟:
- 設置 URL 為 https://[karakeep site]/api/v1/lists。
- 在 Authentication 中選擇 Generic Credential Type,並配置 Bearer Auth 憑證。
- 啟用 Execute Once 選項設為 true,以避免對每條書籤數據重複請求。我只是要取得 “shared” 這個 list 的 id 而已…
- 可選:在 Response Format 中選擇 JSON,確保返回數據易於解析。
- 用途:從 Karakeep API 獲取所有列表的數據,包括 “shared” 列表的 ID,用於後續將書籤移動到指定列表。
- HTTP Request 節點(send to buffer via IFTTT)
- 節點類型:n8n-nodes-base.httpRequest
- 設定步驟:
- 設置 Method 為 POST。
- 設置 URL 為 IFTTT 的 webhook URL,例如 https://maker.ifttt.com/trigger/[IFTTT webhook trigger]/with/key/[IFTTT_webhook_key]。
- 在 Body Parameters 中添加:
- 名稱:value1,值:{{ $json.tweet_msg }}(引用生成的推文內容)。
- 在 Options 中啟用 Send Body 並選擇 JSON 格式。
- 用途:通過 IFTTT 的 webhook 將生成的推文內容發送到 Buffer,進而在 Twitter 上發布。
- HTTP Request 節點(move to shared list)
- 節點類型:n8n-nodes-base.httpRequest
- 設定步驟:
- 設置 Method 為 PUT。
- 設置 URL 為 https://[karakppe site]/api/v1/lists/{{ $json.list_id }}/bookmarks/{{ $json.id }},其中 list_id 從 get lists 節點獲取,id 為書籤 ID。
- 在 Authentication 中選擇 Generic Credential Type,並配置 Bearer Auth 憑證。
- 啟用 Never Error 選項設為 true,以避免因 API 錯誤中斷 workflow。
- 用途:將已分享的書籤移動到 “shared” 列表,標記其完成狀態。
Future Work:
之後還有一些 future work 可以做,像是:
- 不要透過 IFTTT 去發推文,直接從 n8n 去呼叫 twitter API。
- 在 n8n 中加上類似 buffer.app 定時發文的功能。
- 較長的文字不要裁剪,改用回覆的方式支援長文。
- Thread 發文支援
- …
這些就以後有空再實作練習囉。