专注时间
中级
这是一个Personal Productivity领域的自动化工作流,包含 11 个节点。主要使用 If, Code, ItemLists, GoogleCalendar, ScheduleTrigger 等节点。 在繁忙日程中自动在Google日历中屏蔽专注时间
前置要求
- •无特殊前置要求,导入即可使用
分类
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "H1q8R1MfhEk8F0ni",
"meta": {
"instanceId": "bdc54da2c96840612a04bf3fd3a4cd97a7a7bd7c1152bbe41d5615f09311c097"
},
"name": "专注时间",
"tags": [
{
"id": "SauVYJKjA9yiw2uI",
"name": "calendar-automation",
"createdAt": "2025-07-20T22:06:21.623Z",
"updatedAt": "2025-07-20T22:06:21.623Z"
}
],
"nodes": [
{
"id": "965166ee-bf7e-48e6-b559-624c4a5bb72f",
"name": "计划触发器",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-880,
96
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * *"
}
]
}
},
"typeVersion": 1.1
},
{
"id": "af8970e0-6d2c-48d4-907b-5e022ab8d5ac",
"name": "获取整周事件",
"type": "n8n-nodes-base.googleCalendar",
"position": [
-656,
96
],
"parameters": {
"options": {},
"calendar": {
"__rl": true,
"mode": "list",
"value": "primary",
"cachedResultName": "Primary"
},
"operation": "getAll"
},
"credentials": {
"googleCalendarOAuth2Api": {
"id": "Isqbn5j7Czj35HLS",
"name": "Google Calendar account"
}
},
"typeVersion": 1
},
{
"id": "76c503e8-926f-4254-be1b-f2c5b55938e8",
"name": "计算整周专注时间段",
"type": "n8n-nodes-base.code",
"position": [
-448,
96
],
"parameters": {
"jsCode": "const events = $input.all();\nconst today = new Date();\n\n// Get Sunday of current week (start of week)\nconst sunday = new Date(today);\nsunday.setDate(today.getDate() - today.getDay());\nsunday.setHours(0, 0, 0, 0);\n\n// Create array of all 7 days (Sunday-Saturday)\nconst allDays = [];\nfor (let i = 0; i < 7; i++) {\n const day = new Date(sunday);\n day.setDate(sunday.getDate() + i);\n allDays.push(day);\n}\n\nconst dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\nconsole.log('Analyzing full week:', allDays.map((d, i) => `${dayNames[i]} ${d.toDateString()}`));\n\nconst allFocusSlots = [];\n\n// Process each day of the week\nfor (let dayIndex = 0; dayIndex < allDays.length; dayIndex++) {\n const currentDay = allDays[dayIndex];\n const dayName = dayNames[dayIndex];\n \n const workStart = new Date(currentDay);\n workStart.setHours(9, 0, 0, 0);\n const workEnd = new Date(currentDay);\n workEnd.setHours(17, 0, 0, 0);\n \n // Filter events for this specific day\n const dayEvents = events\n .filter(event => {\n const eventStart = new Date(event.json.start.dateTime || event.json.start.date);\n return eventStart.toDateString() === currentDay.toDateString();\n })\n .sort((a, b) => {\n const aStart = new Date(a.json.start.dateTime || a.json.start.date);\n const bStart = new Date(b.json.start.dateTime || b.json.start.date);\n return aStart - bStart;\n });\n\n // Calculate total booked time for this day\n let totalBookedMinutes = 0;\n for (const event of dayEvents) {\n const startTime = new Date(event.json.start.dateTime || event.json.start.date);\n const endTime = new Date(event.json.end.dateTime || event.json.end.date);\n \n const effectiveStart = new Date(Math.max(startTime.getTime(), workStart.getTime()));\n const effectiveEnd = new Date(Math.min(endTime.getTime(), workEnd.getTime()));\n \n if (effectiveStart < effectiveEnd) {\n totalBookedMinutes += (effectiveEnd - effectiveStart) / (1000 * 60);\n }\n }\n\n const totalBookedHours = totalBookedMinutes / 60;\n console.log(`${dayName} ${currentDay.toDateString()}: ${totalBookedHours.toFixed(1)} hours booked`);\n\n // Only process days with 6+ hours booked\n if (totalBookedHours >= 6) {\n console.log(`Creating focus time for ${dayName} (${totalBookedHours.toFixed(1)} hours booked)`);\n \n const freeSlots = [];\n let currentTime = new Date(workStart);\n\n // Check time before first event\n if (dayEvents.length > 0) {\n const firstEventStart = new Date(dayEvents[0].json.start.dateTime || dayEvents[0].json.start.date);\n if (currentTime < firstEventStart) {\n const slotEnd = new Date(Math.min(firstEventStart.getTime(), workEnd.getTime()));\n if (currentTime < slotEnd) {\n freeSlots.push({\n start: new Date(currentTime),\n end: slotEnd\n });\n }\n }\n }\n\n // Check time between events\n for (let i = 0; i < dayEvents.length - 1; i++) {\n const currentEventEnd = new Date(dayEvents[i].json.end.dateTime || dayEvents[i].json.end.date);\n const nextEventStart = new Date(dayEvents[i + 1].json.start.dateTime || dayEvents[i + 1].json.start.date);\n \n const slotStart = new Date(Math.max(currentEventEnd.getTime(), workStart.getTime()));\n const slotEnd = new Date(Math.min(nextEventStart.getTime(), workEnd.getTime()));\n \n if (slotStart < slotEnd) {\n freeSlots.push({\n start: slotStart,\n end: slotEnd\n });\n }\n }\n\n // Check time after last event\n if (dayEvents.length > 0) {\n const lastEventEnd = new Date(dayEvents[dayEvents.length - 1].json.end.dateTime || dayEvents[dayEvents.length - 1].json.end.date);\n const slotStart = new Date(Math.max(lastEventEnd.getTime(), workStart.getTime()));\n \n if (slotStart < workEnd) {\n freeSlots.push({\n start: slotStart,\n end: new Date(workEnd)\n });\n }\n } else {\n // No events this day, but 6+ hours requirement met somehow (shouldn't happen, but handle it)\n console.log(`${dayName}: No events found but met 6+ hour criteria`);\n }\n\n // Filter slots that are at least 15 minutes long and add to all slots\n const validSlots = freeSlots.filter(slot => \n (slot.end - slot.start) >= 15 * 60 * 1000\n );\n\n validSlots.forEach(slot => {\n allFocusSlots.push({\n start: slot.start.toISOString(),\n end: slot.end.toISOString(),\n duration: Math.round((slot.end - slot.start) / (1000 * 60)),\n summary: \"Focus Time\",\n day: currentDay.toDateString(),\n dayName: dayName,\n bookedHours: totalBookedHours.toFixed(1)\n });\n });\n }\n}\n\nconsole.log(`Found ${allFocusSlots.length} focus time slots across the full week`);\n\nif (allFocusSlots.length === 0) {\n return [{\n json: {\n shouldCreateFocusTime: false,\n message: 'No days this week have 6+ hours booked. Focus time not needed.',\n weekAnalyzed: allDays.map((d, i) => `${dayNames[i]} ${d.toDateString()}`)\n }\n }];\n}\n\nreturn [{\n json: {\n shouldCreateFocusTime: true,\n totalSlotsFound: allFocusSlots.length,\n freeSlots: allFocusSlots,\n weekAnalyzed: allDays.map((d, i) => `${dayNames[i]} ${d.toDateString()}`)\n }\n}];"
},
"typeVersion": 2
},
{
"id": "a6d17dd6-3aea-4860-b3c1-cd95d8e6a78c",
"name": "是否创建专注时间?",
"type": "n8n-nodes-base.if",
"position": [
-224,
96
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "condition-001",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.shouldCreateFocusTime }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "49a4675b-2a0f-4219-9500-aa1803fd794b",
"name": "分割空闲时间段",
"type": "n8n-nodes-base.itemLists",
"position": [
0,
0
],
"parameters": {
"options": {},
"fieldToSplitOut": "freeSlots"
},
"typeVersion": 3
},
{
"id": "a7191014-8661-41c2-b121-7f7f924c895e",
"name": "创建专注时间事件",
"type": "n8n-nodes-base.googleCalendar",
"position": [
224,
0
],
"parameters": {
"end": "={{ $json.end }}",
"start": "={{ $json.start }}",
"calendar": {
"__rl": true,
"mode": "list",
"value": "primary",
"cachedResultName": "Primary"
},
"additionalFields": {}
},
"credentials": {
"googleCalendarOAuth2Api": {
"id": "Isqbn5j7Czj35HLS",
"name": "Google Calendar account"
}
},
"typeVersion": 1
},
{
"id": "019babfa-6ac8-4028-951c-0cb776ef24cd",
"name": "记录结果",
"type": "n8n-nodes-base.code",
"position": [
448,
0
],
"parameters": {
"jsCode": "const items = $input.all();\nconst summary = {\n message: 'Focus time blocks created successfully for the week',\n totalSlots: items.length,\n slots: items.map(item => ({\n start: item.json.start.dateTime,\n end: item.json.end.dateTime,\n summary: item.json.summary\n }))\n};\n\nconsole.log('Weekly Focus Time Creation Summary:', JSON.stringify(summary, null, 2));\n\nreturn [{\n json: summary\n}];"
},
"typeVersion": 2
},
{
"id": "7ecbdb56-9195-4d13-891b-e307d10417f5",
"name": "记录无需专注时间",
"type": "n8n-nodes-base.code",
"position": [
0,
208
],
"parameters": {
"jsCode": "const data = $input.first().json;\nconsole.log('Weekly Focus Time Analysis:', data.message);\n\nreturn [{\n json: {\n message: data.message,\n weekAnalyzed: data.weekAnalyzed,\n action: 'no_focus_time_needed'\n }\n}];"
},
"typeVersion": 2
},
{
"id": "5a76f16a-5931-45d8-8319-440b39491a65",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-944,
-80
],
"parameters": {
"height": 400,
"content": "设置工作流运行的计划时间(建议设置在用户工作日开始前)。"
},
"typeVersion": 1
},
{
"id": "f9e74c3f-ab4e-4e70-bddc-08f4903dc6f5",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-688,
-80
],
"parameters": {
"width": 592,
"height": 400,
"content": "工作流检查用户从周日到周六的当前周日历。此部分的目标是确定某天是否已有6小时或更长时间被预订"
},
"typeVersion": 1
},
{
"id": "8ac0657c-c682-414e-b56e-884f517bcda2",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-80,
-176
],
"parameters": {
"width": 704,
"height": 544,
"content": "对于已有6小时或更长时间被预订的天数,工作流会自动将剩余时间块设置为专用专注时间。工作流假设每周工作8小时。例如,如果周一已有6.5小时被预订(用于会议、任务等),工作流将阻塞剩余的1.5小时。记录结果以帮助进行故障排除"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "9c49a780-08ab-4198-9a16-8cdb241b5425",
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Get Full Week Events",
"type": "main",
"index": 0
}
]
]
},
"Split Free Slots": {
"main": [
[
{
"node": "Create Focus Time Event",
"type": "main",
"index": 0
}
]
]
},
"Get Full Week Events": {
"main": [
[
{
"node": "Calculate Full Week Focus Time Slots",
"type": "main",
"index": 0
}
]
]
},
"Create Focus Time Event": {
"main": [
[
{
"node": "Log Results",
"type": "main",
"index": 0
}
]
]
},
"Should Create Focus Time?": {
"main": [
[
{
"node": "Split Free Slots",
"type": "main",
"index": 0
}
],
[
{
"node": "Log No Focus Time Needed",
"type": "main",
"index": 0
}
]
]
},
"Calculate Full Week Focus Time Slots": {
"main": [
[
{
"node": "Should Create Focus Time?",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
中级 - 个人效率
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
Telegram日历机器人
从Google日历通过Telegram发送每日日历摘要通知
If
Code
Telegram
+3
11 节点Yassin Zehar
个人效率
全球节假日冲突检测与会议重新安排
使用Google Calendar和Slack检测节假日冲突并建议会议重新安排
If
Set
Code
+6
23 节点Takuya Ojima
个人效率
免费赠品:每日提醒模板
通过 Google Calendar、Twilio 和 Claude AI 获取每日日历摘要短信
If
Set
Code
+6
13 节点Anne Uy Gothong
个人效率
使用Python和AI的自定义天气邮件
使用OpenWeatherMap、Python和GPT-4.1-mini生成个性化天气报告
Code
Gmail
Form Trigger
+4
11 节点Moe Ahad
个人效率
工作日日志记录
AI工时表生成器 - 集成Gmail、日历和GitHub到Google表格
If
Set
Code
+11
31 节点Luka Zivkovic
个人效率
学生和教师的自动化作业提醒与截止日期跟踪器
使用Notion和邮箱为师生提供作业截止日期提醒
If
Notion
Email Send
+4
7 节点Oneclick AI Squad
个人效率