上週從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 發文支援
- …
這些就以後有空再實作練習囉。




你好,请问可以分享 Karakeep_webhook_queue.json 和 Karakeep_share.json 这两个文件吗
好的,請至 https://blog.serv.idv.tw/wp-content/uploads/2025/09/karakeep_share_export.zip 下載。
多谢多谢