經過前面幾天對 M2A Agent 任務鏈的強化,我的 AI 助理現在已經能夠產出相當水準的會議摘要與行動任務了。但是現在的所有結果都只靜靜地躺在 Notion 裡,如果沒有人主動去看,被指派任務的夥伴可能不知道有新任務。
我為了讓 M2A Agent 能夠朝成為一個主動、貼心的助理邁進,因此今天的目標是整合 Gmail,讓系統在指派任務的當下,自動寄出一封通知信給相關成員。
要讓 n8n 有權限透過自己的 Gmail 帳號寄信,我們必須先到 Google Cloud Console 進行設定,取得授權憑證,這個過程可以想像成要去申請一張「代理寄信許可證」。
第一步,我們要在 Google 的雲端平台建立一個專案來管理我們的 API。
點擊左上角的選取專案,選擇「新增專案」。
為專案取一個好記的名稱,例如 M2A Agent Notifications
,然後點擊「建立」。
專案建立後,從左側導覽列選擇「API 和服務」下的「程式庫」。
在搜尋框中輸入 Gmail API
,點擊進入後,按下「啟用」按鈕。
最後選擇我們剛剛建立的專案
即可完成
這是設定 OAuth 同意畫面的第一步,主要是建立應用程式的基本樣貌。
2. 在「OAuth 同意畫面」頁面後,點擊「開始」進入設定精靈。
3. 應用程式資訊:
* 應用程式名稱:M2A Agent 任務通知器
* 使用者支援電子郵件:選擇自己的 Gmail 信箱
4. 目標對象:
* 選擇外部
5. 聯絡資訊:
* 開發人員聯絡資訊:再次輸入自己的 Gmail 信箱
6. 點擊「儲存並繼續」,完成最初的品牌建立。
完成基本設定後,我們要明確告訴 Google,這個應用程式只需要「寄送郵件」的權限。
https://www.googleapis.com/auth/gmail.send
這個範圍,勾選它,然後點擊「更新」。在我們的應用程式通過 Google 官方驗證之前,只有被加入這個清單的測試使用者才能授權 n8n 存取 Gmail。
最後關鍵的一步,就是要取得 n8n 需要的「用戶端 ID」和「用戶端密鑰」。
在跟 Google 申請金鑰前,我們得先告訴 Google,當授權成功後,要將使用者導向回 n8n 的哪個地址。
Gmail OAuth2 API
並點選它。現在拿著 n8n 給我們的網址,回到 Google Cloud 進行註冊。
回到 Google Cloud Console,點擊左側導覽列選擇「API 和服務」下的「憑證」。
在「憑證」頁面,點擊上方的「+ 建立憑證」,然後在下拉選單中選擇「OAuth 用戶端 ID」。
應用程式類型:選擇「網頁應用程式」。
已授權的重新導向 URI:點擊「+ 新增 URI」,然後將剛剛從 n8n 複製的專屬網址完整地貼上。
點擊頁面最下方的「建立」。
建立後就會有一個彈出視窗會顯示專屬的「用戶端 ID」和「用戶端密碼」。請將這兩串文字複製下來,並且妥善的保管好。
最後我們要將 Google 給的鑰匙和權限範圍,都設定回 n8n。
Client ID
和 Client Secret
欄位。https://www.googleapis.com/auth/gmail.send
。Client ID
、Client Secret
和 Scope
三個欄位都已正確填寫。為了處理多位指派者的情況,因此要採用「先分組,再迴圈」的策略。我們需要先將所有任務「按人分組」,然後一個一個地處理每個人,為他們各自寄送郵件。
這個節點的的任務是把零散的任務,然後將它們重新打包,變成以「人」為單位的包裹。
// 從上游節點取得任務清單和成員資料
const enhancedTasks = $('提取結構化任務').first().json.task_info.tasks;
const memberData = $('格式化成員列表').first().json.member_data;
// 建立一個物件來存放按指派者分組的任務
const tasksByAssignee = {};
// 遍歷所有任務
for (const task of enhancedTasks) {
// 只處理有明確指派者的任務
if (task.assigned_to_id) {
const assigneeId = task.assigned_to_id;
// 如果這是第一次看到這位指派者,先為他建立一個基本資料結構
if (!tasksByAssignee[assigneeId]) {
const member = memberData.find(m => m.notion_user_id === assigneeId);
if (member && member.email) { // 確保成員存在且有 Email
tasksByAssignee[assigneeId] = {
name: member.name,
email: member.email,
tasks: [] // 一個用來存放他所有任務的陣列
};
}
}
// 將當前任務加入到對應指派者的任務清單中
if (tasksByAssignee[assigneeId]) {
tasksByAssignee[assigneeId].tasks.push(task);
}
}
}
// 最後,將物件轉換成陣列格式,方便下一個節點處理
// Object.values() 會將 { 'id1': { ... }, 'id2': { ... } } 轉換成 [ { ... }, { ... } ]
return Object.values(tasksByAssignee);
這個節點的輸出,會是一個陣列,陣列中的每個項目都代表一位被指派者,以及他需要完成的所有任務清單。
現在我們有了一個包含「張三包裹」和「李四包裹」的籃子,但後面的 Gmail 節點一次只能寄一封信,這時候「Loop Over Items」節點就派上用場了。它的任務是分發,也就是建立一個迴圈,把籃子裡的包裹一個一個拿出來,交給下一個節點處理。
1
。用「Code」節點來專門生成 HTML 郵件內容。
const assigneeInfo = $input.item.json;
const meetingInfo = $('提取結構化任務').first().json.meeting_info;
const meetingAttributes = $('提取結構化任務').first().json.meeting_attributes;
const assigneeName = assigneeInfo.name || '夥伴';
const projectName = meetingInfo.project_name || '未指定專案';
const taskCount = assigneeInfo.tasks.length;
const summary = meetingAttributes.summary || '暫無摘要';
const deadlineDate = meetingInfo.deadline_date || '未設定';
let tasksHtml = '';
for (let i = 0; i < assigneeInfo.tasks.length; i++) {
const task = assigneeInfo.tasks[i];
const taskNum = i + 1;
const title = task.title || '未命名任務';
const desc = task.description || '';
const timeExpr = task.time_expression || '';
const priority = task.priority || '';
const priBg = priority === '高' ? '#e74c3c' : '#f39c12';
const priBadge = priority ? '<span style="background-color: ' + priBg + '; color: white; font-size: 11px; padding: 3px 8px; border-radius: 10px; margin-left: 5px;">' + priority + '優先</span>' : '';
tasksHtml += '<div style="background-color: #fff; border: 2px solid #e3e8ef; border-left: 5px solid #667eea; padding: 18px; margin-bottom: 15px; border-radius: 6px;">';
tasksHtml += '<div style="margin-bottom: 10px;"><span style="background-color: #667eea; color: white; font-size: 12px; font-weight: bold; padding: 4px 10px; border-radius: 12px;">任務 ' + taskNum + '</span>' + priBadge + '</div>';
tasksHtml += '<h4 style="margin: 0 0 12px 0; color: #2c3e50; font-size: 16px; font-weight: bold;">' + title + '</h4>';
tasksHtml += '<p style="margin: 8px 0; color: #666; font-size: 13px;">' + desc + '</p>';
tasksHtml += '<p style="margin: 12px 0 0 0; color: #555; font-size: 14px;"><strong>📅 預計完成期限:</strong> <span style="color: #e74c3c; font-weight: bold;">' + deadlineDate + '</span>';
if (timeExpr) {
tasksHtml += ' <span style="color: #999; font-size: 13px;">(' + timeExpr + ')</span>';
}
tasksHtml += '</p></div>';
}
const htmlContent = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: Arial, sans-serif;"><div style="max-width: 600px; margin: 20px auto; background-color: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"><div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px 20px; text-align: center;"><h1 style="margin: 0; font-size: 24px; font-weight: bold;">📋 M2A Agent 任務指派通知</h1><p style="margin: 10px 0 0 0; font-size: 14px; opacity: 0.9;">您有新的任務等待處理</p></div><div style="padding: 30px 25px;"><p style="font-size: 16px; color: #333; margin: 0 0 20px 0;">Hi <strong>' + assigneeName + '</strong>,</p><div style="background-color: #f8f9fa; border-left: 4px solid #667eea; padding: 15px; margin-bottom: 25px; border-radius: 4px;"><p style="margin: 0; color: #555; font-size: 14px;">在剛剛的「<strong style="color: #667eea;">' + projectName + '</strong>」會議中,您被指派了 <strong style="color: #e74c3c;">' + taskCount + '</strong> 項新任務,請查收:</p></div><div style="margin-bottom: 30px;">' + tasksHtml + '</div><hr style="border: 0; border-top: 1px solid #e0e0e0; margin: 30px 0;"><div><h3 style="color: #667eea; font-size: 18px; margin: 0 0 15px 0; border-bottom: 2px solid #667eea; padding-bottom: 8px;">📝 相關會議摘要</h3><div style="background-color: #f9f9f9; padding: 15px; border-radius: 6px; border: 1px solid #e8e8e8;"><p style="margin: 0; color: #555; font-size: 14px; line-height: 1.6;">' + summary + '</p></div></div></div><div style="background-color: #f8f9fa; text-align: center; padding: 20px; border-top: 1px solid #e0e0e0;"><p style="margin: 0; font-size: 12px; color: #999;">🤖 這是一封由 M2A Agent 自動發送的通知郵件</p><p style="margin: 8px 0 0 0; font-size: 11px; color: #bbb;">請勿直接回覆此郵件</p></div></div></body></html>';
return {
email: assigneeInfo.email,
name: assigneeName,
projectName: projectName,
taskCount: taskCount,
htmlContent: htmlContent
};
這個節點會輸出一個包含 email
、projectName
、taskCount
和 htmlContent
的物件,方便下一個節點取用。
這個節點角色非常單純,只負責寄送上一個節點準備好的內容。
打開「Gmail」節點,選擇 Send a message
操作,並依序完成以下設定:
M2A Gmail
憑證。{{ $json.email }}
【M2A Agent 任務指派】關於「{{ $json.projectName }}」會議,您有 {{ $json.taskCount }} 項新任務
HTML
{{ $json.htmlContent }}
最後,將「Gmail」節點的輸出端點,連接回「Loop Over Items」節點的左側輸入端點,形成一個完整的閉環迴圈。
完成設定後,需要進行測試。
請生成會議摘要與提取行動任務
Gmail
n8n workflow
✅ 完成項目
看著 M2A Agent 在會議結束後,它能夠為被指派任務的夥伴寄出精美的任務通知信,這種將複雜流程化為無形、將人工庶務轉化為效率的成就感,正是我追求自動化的初衷。
🎯 明天計劃
新增可以連結到會議紀錄資料庫的連結按鈕,穩定迴圈的穩定性,並做測試驗證。