這篇文章將引導你使用 n8n 建立一個每週自動從你過去解過的題目中,抽選 1 題來複習,並將它發送到你的 Discord 頻道
讓我們開始吧!
我們的目標是讓這個流程每週自動執行 1 次
來到儀表板,新增一個流程「Create Workflow」
初始節點選擇排程「On a schedule」
選擇禮拜日早上 08:00
接下來,我們要從 LeetCode 的 API 獲取你所有已解答(Accepted)的題目列表
下個節點選擇「HTTP Request」去傳送網路請求
方法選擇「POST」
網址是
https://leetcode.com/graphql
「Send Body」打開,並選擇格式為「Using JSON」
請求的 JSON 填入以下資訊,「USERNAME」改為自己的
{
"query": "query recentAcSubmissions($username: String!, $limit: Int!) { recentAcSubmissionList(username: $username, limit: $limit) { id title titleSlug timestamp } }",
"variables": {
"username": "USERNAME",
"limit": 2000
}
}
我們已經拿到了題目列表,但裡面可能包含重複的題目,而且我們只想複習那些有點久以前解過的題目
下個節點選擇「Code」,來撰寫抽選機制
程式碼如下
// 1. 取得從上個節點 (HTTP Request) 傳來的題目列表
const submissions = $input.item.json.data.recentAcSubmissionList;
// 2. 使用 Map 整理並去除重複的題目
const uniqueProblemsMap = new Map();
submissions.forEach((sub) => {
uniqueProblemsMap.set(sub.titleSlug, {
// 使用 .set() 方法
title: sub.title,
titleSlug: sub.titleSlug,
timestamp: sub.timestamp,
});
});
// 從 Map 的 values() 取出所有物件,並轉換成陣列
const uniqueAccepted = Array.from(uniqueProblemsMap.values());
// 3. 計算 30 天前的 Unix timestamp (單位:秒)
const now = new Date(); // 取得當前時間
const thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000; // 30 天的毫秒數
const thirtyDaysAgoTimestampInSeconds = Math.floor(
(now.getTime() - thirtyDaysInMs) / 1000
);
// 4. 進行篩選:只挑出在 30 天前解過的題目
const oldProblems = uniqueAccepted.filter(
(p) => parseInt(p.timestamp) < thirtyDaysAgoTimestampInSeconds
);
// 5. 決定抽籤池 (Pool) - 這是防呆機制
// 如果有超過30天的舊題目,就從舊題目中抽籤;
// 如果沒有(例如你是新帳號,所有題目都在30天內解的),就從全部題目中抽,避免 workflow 出錯。
const pool = oldProblems.length > 0 ? oldProblems : uniqueAccepted;
// 6. 從最終的抽籤池中隨機挑選 1 題
const randomIndex = Math.floor(Math.random() * pool.length);
const randomProblem = pool[randomIndex];
// 7. 回傳選中的題目物件,包含所有資訊以便後續使用
return randomProblem;
最後一步,就是將我們精心挑選出來的題目發送到 Discord,提醒自己該複習了
接下來選擇「Discord」把抽選結果傳出去
「Connection Type」選擇「Webhook」,憑證的串接在之前的文章有撰寫過,這邊就不重複惹
「Message」填入以下內容
**今週 LeetCode 複習!** 🧠
**隨機抽選:** [{{ $json.title }}](https://leetcode.com/problems/{{ $json.titleSlug }}/)
**上次解題時間:** {{ DateTime.fromSeconds(parseInt($json.timestamp)).setZone('Asia/Taipei').toFormat('yyyy-MM-dd') }}
接著點選上方的「Execute step」來測試一下
Discord 看到訊息代表成功啦
記得要到設定的地方設定時區
設定為台灣時間
完成的流程會長這樣,記得到上方切換為「Active」哦
恭喜!現在你有了一個自動化的 LeetCode 複習小幫手,每週都會貼心地提醒你該溫故知新惹
最後也附上完整流程的 JSON
{
"name": "Weekdays LeetCode",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtHour": 8
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [0, 0],
"id": "e247a697-9ea9-4c94-a7c2-1f3922b5b858",
"name": "Schedule Trigger"
},
{
"parameters": {
"method": "POST",
"url": "https://leetcode.com/graphql",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\n \"query\": \"query recentAcSubmissions($username: String!, $limit: Int!) { recentAcSubmissionList(username: $username, limit: $limit) { id title titleSlug timestamp } }\",\n \"variables\": {\n \"username\": \"YOUR_LEETCODE_USERNAME\",\n \"limit\": 2000\n }\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [220, 0],
"id": "0b45f22a-6ec1-404d-af5c-9b6e1b5aedb8",
"name": "HTTP Request"
},
{
"parameters": {
"jsCode": "// 1. 取得從上個節點 (HTTP Request) 傳來的題目列表\nconst submissions = $input.item.json.data.recentAcSubmissionList;\n\n// 2. 使用 Map 整理並去除重複的題目\nconst uniqueProblemsMap = new Map();\nsubmissions.forEach(sub => {\n uniqueProblemsMap.set(sub.titleSlug, { // 使用 .set() 方法\n title: sub.title,\n titleSlug: sub.titleSlug,\n timestamp: sub.timestamp\n });\n});\n// 從 Map 的 values() 取出所有物件,並轉換成陣列\nconst uniqueAccepted = Array.from(uniqueProblemsMap.values()); \n\n// 3. 計算 30 天前的 Unix timestamp (單位:秒)\nconst now = new Date(); // 取得當前時間\nconst thirtyDaysInMs = 30 * 24 * 60 * 60 * 1000; // 30 天的毫秒數\nconst thirtyDaysAgoTimestampInSeconds = Math.floor((now.getTime() - thirtyDaysInMs) / 1000);\n\n// 4. 進行篩選:只挑出在 30 天前解過的題目\nconst oldProblems = uniqueAccepted.filter(p => parseInt(p.timestamp) < thirtyDaysAgoTimestampInSeconds);\n\n// 5. 決定抽籤池 (Pool) - 這是防呆機制\n// 如果有超過30天的舊題目,就從舊題目中抽籤;\n// 如果沒有(例如你是新帳號,所有題目都在30天內解的),就從全部題目中抽,避免 workflow 出錯。\nconst pool = oldProblems.length > 0 ? oldProblems : uniqueAccepted;\n\n// 6. 從最終的抽籤池中隨機挑選 1 題\nconst randomIndex = Math.floor(Math.random() * pool.length);\nconst randomProblem = pool[randomIndex];\n\n// 7. 回傳選中的題目物件,包含所有資訊以便後續使用\nreturn randomProblem;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [440, 0],
"id": "84f4439a-e18e-49fb-868b-8c2c555632b9",
"name": "Code"
},
{
"parameters": {
"authentication": "webhook",
"content": "=**今週 LeetCode 複習!** 🧠\n\n**隨機抽選:** [{{ $json.title }}](https://leetcode.com/problems/{{ $json.titleSlug }}/)\n**上次解題時間:** {{ DateTime.fromSeconds(parseInt($json.timestamp)).setZone('Asia/Taipei').toFormat('yyyy-MM-dd') }}\n",
"options": {}
},
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [660, 0],
"id": "fb300201-1a50-4dcf-805f-d12f225f2ea5",
"name": "Discord"
}
],
"pinData": {},
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "Discord",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"timezone": "Asia/Taipei",
"callerPolicy": "workflowsFromSameOwner",
"executionTimeout": -1
},
"versionId": "16b54aac-760b-40c3-8598-b525bb88d7d7",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "GHWECbpBcPSCRF1A",
"tags": []
}