Bạn vẫn đang copy CV từ Chat sang Gmail thủ công mỗi ngày? Đã đến lúc để Google Apps Script làm việc đó trong 0.5 giây.
1. Bối cảnh (Context)
Trong môi trường làm việc sử dụng hệ sinh thái Google Workspace, đội ngũ Tuyển dụng (Talent Acquisition – TA) thường xuyên phải tiếp nhận và phân phối CV của ứng viên cho các Quản lý hoặc Người phỏng vấn (Reviewer) đánh giá.
Thông thường, quy trình này diễn ra thủ công trên một Không gian (Space) của Google Chat: TA tải CV lên Google Drive, copy link, gửi vào nhóm Chat và tag (@mention) tên người cần xem. Tuy nhiên, luồng thông tin trong Chat trôi rất nhanh, dẫn đến việc Reviewer dễ dàng bỏ sót tin nhắn và làm chậm trễ quá trình tuyển dụng.
Bài toán đặt ra: Cần xây dựng một luồng tự động hoá (automation workflow). Mỗi khi TA gửi một tin nhắn chứa link Google Drive của CV và tag tên Reviewer cùng với một con Bot, hệ thống sẽ tự động trích xuất các đường link này và gửi những email thông báo riêng biệt đến đích danh hòm thư Gmail của từng Reviewer.
2. Hành trình tìm kiếm giải pháp (Exploring the Approach)
Để giải quyết bài toán trên, quá trình tìm kiếm giải pháp đã đi qua nhiều giai đoạn thử nghiệm khác nhau:
- Nghiên cứu nền tảng (Fundamental Research): Bắt đầu bằng việc tìm kiếm trên Google với từ khoá “Google Chat automation flow”. Quá trình này dẫn đến một video trên YouTube hướng dẫn sử dụng công cụ nội bộ của Google là Google Workspace Studio để tạo luồng Chat-to-Gmail. Tuy nhiên, sau khi tìm hiểu sâu, các tính năng của nền tảng này không đủ linh hoạt và không đáp ứng được logic trích xuất phức tạp của bài toán.
- Tham vấn AI (Gemini): Từ bế tắc ban đầu, quá trình thảo luận với Gemini đã mở ra 4 hướng tiếp cận khả thi. Sau khi kiểm thử và đánh giá từng phương án:
- Zapier/Make: Nền tảng no-code rất dễ dùng, nhưng Zapier hiện tại không hỗ trợ trigger linh hoạt cho tin nhắn Google Chat thông thường, khiến phương án này bị loại bỏ.
- Google Workspace Events API & Google Cloud Pub/Sub: Đây là giải pháp cực kỳ mạnh mẽ, có thể “nghe” mọi tin nhắn mà không cần @mention. Tuy nhiên, kiến trúc quá phức tạp, đòi hỏi thiết lập hạ tầng Cloud tốn kém và rườm rà.
- Google Workspace Data Loss Prevention (DLP): Không phù hợp với mục đích tối ưu năng suất, và chỉ dành cho các phiên bản Enterprise để rà soát bảo mật.
- Chốt phương án cuối cùng: Google Apps Script. Đây là giải pháp “native” (bản địa) hoàn hảo nhất của Google, hoàn toàn miễn phí, nằm sẵn trong hệ sinh thái Workspace và có thể tuỳ biến logic bằng code (JavaScript). Dưới sự hỗ trợ viết code của Gemini, giải pháp này đã được lựa chọn để xây dựng.
3. Quá trình giải quyết vấn đề (The Problem-Solving Process)
Quá trình xây dựng không diễn ra trong một bước mà là sự phát triển lặp đi lặp lại (iterative), từ những suy nghĩ đầu tiên cho đến một hệ thống mạnh mẽ (robust).
- Ý tưởng ban đầu (Initial Logic): Do giới hạn bảo mật của Google, Bot không thể tự động đọc mọi tin nhắn. Giải pháp chốt lại là yêu cầu người dùng phải chủ động @mention con Bot để kích hoạt luồng.
- Nguyên mẫu đầu tiên (First Prototype): Bắt đầu bằng một đoạn script đơn giản: Khi Bot được tag, nó sẽ lấy nội dung tin nhắn và gửi thẳng một email thô sơ đến một địa chỉ cố định.
- Tăng cường sự chặt chẽ (Adding Robustness): Để tránh việc gửi email rác mỗi khi ai đó vô tình tag Bot, 3 bộ lọc (Filters) khắt khe được đưa vào:
- Phải có từ khoá ” CV ” (có dấu cách 2 bên).
- Phải mention một người dùng thật để làm người nhận (Reviewer).
- Phải chứa link Google Drive/Docs hợp lệ bằng Regular Expression (Regex).
- Hoàn thiện và xử lý các ca hóc búa (Edge Cases): * Cập nhật Regex để bắt được cả link Google Docs (document/d/) và loại bỏ các ký tự < > bị Chat tự động chèn vào.
- Bài toán đa người nhận: Khi TA tag nhiều Reviewer, hệ thống ban đầu chỉ lấy người đầu tiên. Sau khi cấu trúc lại, bot đã gom được tất cả Reviewer. Nhưng thay vì CC tất cả vào một email lộn xộn, logic được nâng cấp thành một vòng lặp: gửi các email hoàn toàn riêng biệt đến từng Reviewer.
- Thêm logic “Ping”: Nếu người dùng chỉ tag Bot mà không có nội dung gì, nó sẽ trả về một câu chào và hướng dẫn sử dụng thay vì báo lỗi cứng nhắc.
4. Giải thích chi tiết mã nguồn và Logic (Detailed Logic & Code)
Hệ thống được viết hoàn toàn bằng JavaScript trên nền tảng Google Apps Script. Dưới đây là kiến trúc và chức năng của từng phần trong mã nguồn cuối cùng.
Kiến trúc các hàm (Functions)
- buildChatResponse(responseText): * Do Google Chat mới cập nhật kiến trúc lên Workspace Add-ons, bot không thể trả về chuỗi text đơn giản. Hàm helper này chịu trách nhiệm đóng gói câu trả lời của Bot vào một cấu trúc JSON nhiều lớp (hostAppDataAction > chatDataAction > createMessageAction) để Google Chat có thể hiểu và hiển thị.
- onMessage(event): Hàm cốt lõi (Core function) kích hoạt khi có tin nhắn gửi đến Bot.
- Defensive Guard: Kiểm tra xem payload có rỗng không để tránh sập (crash) ứng dụng. Dữ liệu tin nhắn giờ nằm ở event.chat.messagePayload.message.
- Extract Mentions: Quét mảng annotations của tin nhắn. Lọc bỏ các tag của Bot, chỉ giữ lại tag con người (HUMAN), lấy tên và email của họ đưa vào mảng targetEmails.
- Greeting Trigger: Kiểm tra nếu argumentText rỗng (tức là chỉ có tag tên Bot và không có text nào khác), Bot sẽ trả về câu hướng dẫn sử dụng.
- The Filters: Chạy các lệnh kiểm tra từ khoá ” CV “, độ dài mảng email, và chạy Regex để trích xuất mảng urls.
- Execution: Dùng lệnh for lặp qua mảng targetEmails. Trong mỗi vòng lặp, sử dụng GmailApp.sendEmail() để gửi thư riêng biệt. Áp dụng try…catch để đếm số lượng gửi thành công/thất bại và báo cáo lại vào Chat.
- onAddedToSpace(event) & onRemovedFromSpace(event): Các hàm vòng đời (Lifecycle) bắt buộc phải có để Bot không bị lỗi khi được mời vào hoặc bị kick ra khỏi một Space.
Toàn văn mã nguồn cuối cùng (Final Source Code)
/**
* Helper function to format responses correctly for the Add-on API architecture
*/
function buildChatResponse(responseText) {
return {
hostAppDataAction: {
chatDataAction: {
createMessageAction: {
message: {
text: responseText
}
}
}
}
};
}
/**
* Triggers when the bot is @mentioned in a Space.
*/
function onMessage(event) {
// ==========================================
// DEFENSIVE GUARD (Updated for Add-ons Architecture)
// ==========================================
if (!event || !event.chat || !event.chat.messagePayload || !event.chat.messagePayload.message) {
console.error(“Ignored an event without a message payload.”);
return;
}
// Extract the message and user from the new payload structure
const message = event.chat.messagePayload.message;
const user = event.chat.user;
// Fallback to empty string if text is undefined, preventing crashes
const messageText = message.text || “”;
if (messageText === “”) {
console.error(“Error: Received a message with no readable text.”);
return;
}
// ==========================================
// EXTRACT MENTIONS FIRST
// ==========================================
let targetEmails = [];
let targetNames = [];
let hasHumanMention = false;
// Annotations are now found inside the nested message object
if (message.annotations) {
for (const annotation of message.annotations) {
if (annotation.type === “USER_MENTION” && annotation.userMention) {
const mentionedUser = annotation.userMention.user;
if (mentionedUser.type === “HUMAN” && mentionedUser.email) {
// Prevent adding duplicates if someone is mentioned twice
if (!targetEmails.includes(mentionedUser.email)) {
targetEmails.push(mentionedUser.email);
targetNames.push(mentionedUser.displayName ? mentionedUser.displayName.split(‘ ‘)[0] : “there”);
hasHumanMention = true;
}
}
}
}
}
// Pre-check keywords and URLs
const hasCVKeyword = messageText.includes(” CV “);
const driveRegex = /https?:\/\/(?:drive|docs)\.google\.com\/(?:(?:u\/\d+\/)?(?:folderview|open|file\/d\/|folders\/|forms\/d\/|spreadsheets\/d\/|presentation\/d\/|document\/d\/))[\w\d_-]+[^\s<>|]*/g;
const urls = messageText.match(driveRegex);
// ==========================================
// GREETING TRIGGER: Simple Ping
// ==========================================
const argumentText = typeof message.argumentText !== ‘undefined’ ? message.argumentText.trim() : messageText.replace(/@[\w\s]+/g, ”).trim();
if (argumentText === “”) {
return buildChatResponse(“Xin chào! Tôi là TA Alert Bot – trợ lý giúp thông báo các CV mới nhất từ team TA đến những đầu mối review qua email. Để bắt đầu sử dụng, bạn hãy nhớ các điều kiện tiên quyết sau. Tin nhắn bắt buộc phải chứa 3 thông tin: (1) Từ khoá ‘ CV ‘ + (2) Danh sách link Google Drive hoặc Google Docs + (3) Phải mention đầu mối review và @TA Alert Bot.”);
}
// ==========================================
// FILTER 1: Contains ” CV “
// ==========================================
if (!hasCVKeyword) {
console.error(“Format Error: Message must contain ‘ CV ‘ (with spaces) to be processed.”);
return;
}
// ==========================================
// FILTER 2: Contains Human Mentions
// ==========================================
if (!hasHumanMention || targetEmails.length === 0) {
console.error(“Format Error: You must @mention the reviewer in your message.”);
return;
}
// ==========================================
// FILTER 3: Contains Google Drive URLs
// =========================================
if (!urls || urls.length === 0) {
console.error(“Format Error: Message must contain valid Google Drive or Google Docs URLs.”);
return;
}
// ==========================================
// EXECUTION: All filters passed, send email
// ==========================================
const senderName = user && user.displayName ? user.displayName : “A team member”;
const senderEmail = user && user.email ? user.email : “Unknown email”;
const subject = `TA – Alert: New CVs to review from ${senderName}`;
const formattedUrlList = urls.map(url => `- ${url}`).join(“\n”);
console.log(`— CV Routing Details —
Sender: ${senderName} (${senderEmail})
Reviewers (${targetEmails.length}): ${targetNames.join(‘, ‘)} (${targetEmails.join(‘, ‘)})
Extracted URLs (${urls.length}):\n${formattedUrlList}`);
let successCount = 0;
let errors = [];
// Loop through targets and send a separate email to each reviewer
for (let i = 0; i < targetEmails.length; i++) {
const currentEmail = targetEmails[i];
const currentName = targetNames[i];
const body = `Dear ${currentName},\n\nYou have new CVs to review from the Talent Acquisition team.\n\nHere are the CV links:\n${formattedUrlList}\n\nBest regards,\n${senderName} – sent via TA Alert Bot`;
try {
GmailApp.sendEmail(currentEmail, subject, body);
successCount++;
} catch (error) {
errors.push(`${currentName} (${error.message})`);
console.error(`Failed to send email to ${currentEmail}: ${error.message}`);
}
}
// Build the final response message based on success rate
if (errors.length === 0) {
return buildChatResponse(`Hoàn tất! Tôi đã gửi email đính kèm ${urls.length} CV(s) thông báo đến các đầu mối review.`);
} else if (successCount > 0) {
return buildChatResponse(`Hoàn tất một phần! Tôi đã gửi email thông báo đến ${successCount} reviewer(s), nhưng đã gặp lỗi: ${errors.join(“, “)}`);
} else {
return buildChatResponse(`Lỗi khi gửi email đến các reviewer(s): ${errors.join(“, “)}`);
}
}
/**
* Responds to being added to a Google Chat space.
*/
function onAddedToSpace(event) {
console.log(`Bot added to Space!`);
return buildChatResponse(“Xin chào! Tôi là TA Alert Bot – trợ lý giúp thông báo các CV mới nhất từ team TA đến những đầu mối review qua email. Để bắt đầu sử dụng, bạn hãy nhớ các điều kiện tiên quyết sau. Tin nhắn bắt buộc phải chứa 3 thông tin: (1) Từ khoá ‘ CV ‘ + (2) Danh sách link Google Drive hoặc Google Docs + (3) Phải mention đầu mối review và @TA Alert Bot.”);
}
/**
* Responds to being removed from a Google Chat space.
*/
function onRemovedFromSpace(event) {
const space = event.chat && event.chat.removedFromSpacePayload ? event.chat.removedFromSpacePayload.space : {};
console.info(`Chat app removed from ${(space.name || “this chat”)}`);
}
5. Hướng dẫn cài đặt và triển khai (Setup & Deployment)
Để thiết lập con Bot này đi vào hoạt động thực tế, bạn cần thực hiện theo các bước sau:
Giai đoạn 1: Chuẩn bị Apps Script
- Truy cập script.google.com và tạo một Project mới (Ví dụ: “TA Alert Bot”).
- Dán toàn bộ mã nguồn ở phần 4 vào trình chỉnh sửa.
- Bấm Lưu (biểu tượng đĩa mềm).
- Bấm Chạy (Run) hàm onMessage một lần. Google sẽ yêu cầu Cấp quyền (Review Permissions) để cho phép script gửi email dưới danh nghĩa của bạn. Hãy chấp nhận các quyền này (Việc chạy thử sẽ báo lỗi do không có sự kiện chat, điều này là bình thường).
Giai đoạn 2: Khởi tạo Deployment
- Ở góc trên bên phải màn hình Apps Script, chọn Deploy > New deployment.
- Bấm vào biểu tượng bánh răng, chọn Add-on. Nhập mô tả và bấm Deploy.
- Sao chép lại chuỗi ký tự Deployment ID.
Giai đoạn 3: Cấu hình API trên Google Cloud
- Từ Apps Script, vào mục Project Settings (biểu tượng bánh răng bên trái) và nhấp vào link dự án Google Cloud Platform (GCP).
- Tại Google Cloud Console, tìm kiếm Google Chat API và bấm Enable.
- Chuyển sang tab Configuration của Google Chat API.
- Nhập Tên ứng dụng (App Name), Avatar URL và Mô tả.
- Bật Interactive features và tích chọn Join workspaces and group conversations.
- Dưới mục Connection settings, chọn Apps Script project và dán cái Deployment ID vừa copy ở Giai đoạn 2 vào.
- Ở mục Visibility, điền email của bạn để cấp quyền quản trị bot. Bấm Save.
Giai đoạn 4: Đưa vào hoạt động
- Mở Không gian (Space) của đội ngũ TA trên Google Chat.
- Bấm vào tên Space > Apps & integrations > + Add apps.
- Tìm tên Bot bạn vừa cấu hình và thêm vào Space. Bot sẽ gửi lời chào đầu tiên và sẵn sàng xử lý các tin nhắn được tag!
6. Kết quả (Result)
Sau khi thiết lập và cấu hình thành công, chúng ta đã có một quy trình liền mạch, bảo mật và hoàn toàn tự động:
- Hành động của TA: Trong không gian nhóm, TA nhắn: “E gửi 2 CV mới vị trí PM ạ @Linh @John @TA Alert Bot https://drive.google.com/… https://docs.google.com/…”
- Xử lý của Bot: * Nhận diện thành công từ khoá ” CV “.
- Trích xuất thành công 2 URL hợp lệ.
- Nhận diện được 2 email của Linh và John.
- Phản hồi của Bot: Trả lời trực tiếp vào đoạn chat: “Hoàn tất! Tôi đã gửi email đính kèm 2 CV(s) thông báo đến các đầu mối review.”
- Phía Reviewer: Linh và John nhận được 2 email riêng biệt trong hộp thư đến Gmail, với nội dung lịch sự báo có CV mới kèm danh sách link dạng bullet gọn gàng.
- Logging: Hệ thống lưu lại nhật ký (Log) rõ ràng về người gửi, người nhận và số lượng link trên màn hình console của Apps Script để dễ dàng audit (kiểm tra) sau này.
7. Kết luận (Conclusion)
Việc sử dụng Google Apps Script để kết nối Google Chat và Gmail mang lại một giải pháp tự động hóa “in-house” vô cùng hiệu quả, không phát sinh chi phí, và tận dụng tối đa hệ sinh thái bảo mật của Google Workspace.
Không chỉ giải quyết triệt để vấn đề “trôi tin nhắn, rớt CV”, giải pháp này còn chuẩn hoá định dạng giao tiếp của đội ngũ TA. Với các tính năng bẫy lỗi nghiêm ngặt và phản hồi trực tiếp của Bot, người dùng ngay lập tức biết được mình đang thao tác sai ở đâu để sửa lại. Đây là một ví dụ điển hình về việc ứng dụng Automation/No-code & Low-code nhằm tối ưu hóa quy trình vận hành và nâng cao năng suất nhân sự.
