Google Calendar API脚本TypeError报错及功能扩展求助
问题解决:Google Apps Script日历同步脚本的两个问题
一、修复TypeError: user.getName is not a function报错
GroupsApp返回的User对象并非总能调用getName()方法——如果群组包含外部用户,或者脚本没有足够权限读取用户姓名,就会触发这个错误。要稳定获取用户全名,可借助Admin Directory API:
- 启用Admin Directory服务:在脚本编辑器的「服务」面板中添加「Admin Directory」,完成授权(需拥有Google Workspace管理员权限)。
- 替换姓名获取逻辑:
将原代码中的let name = user.getName();替换为:let userEmail = user.getEmail(); let userName; try { // 通过Admin Directory获取用户全名 let userInfo = AdminDirectory.Users.get(userEmail); userName = userInfo.name.fullName || userEmail.split('@')[0]; } catch (e) { // 权限不足或用户不存在时,回退到邮箱前缀 userName = userEmail.split('@')[0]; console.warn(`无法获取${userEmail}的姓名,使用邮箱前缀替代: ${e}`); } - 更新事件导入函数:
修改sync循环中的调用为importEvent(userName, event);,并同步更新importEvent的参数和逻辑:/** * 导入用户日历事件到共享团队日历 * @param {string} userName 团队成员姓名 * @param {Calendar.Event} event 要导入的事件 */ function importEvent(userName, event) { event.summary = `[${userName}] ${event.summary}`; // 其余原有代码保持不变 }
若没有Workspace管理员权限,可尝试通过日历显示名获取(可靠性较低):
let userName = user.getEmail().split('@')[0]; try { let calendar = Calendar.Calendars.get(user.getEmail()); userName = calendar.summary || userName; } catch (e) {}
二、添加Work Location事件识别
Work Location是Google Calendar专属属性,不在事件标题关键词中,需通过扩展属性或地点字段识别:
方案1:兼容手动与官方Work Location事件
- 新增关键词(可选):扩展关键词数组兼容手动输入的情况
let KEYWORDS = ['vacation', 'ooo', 'out of office', 'offline', 'work location', 'office', 'home']; - 增强事件过滤逻辑:修改
shoudImportEvent函数,添加地点字段检查function shoudImportEvent(user, keyword, event) { // 原有标题关键词检查逻辑 if (event.summary.toLowerCase().indexOf(keyword) < 0) { // 针对Work Location关键词,额外检查地点字段 if (['work location', 'office', 'home'].includes(keyword)) { if (!event.location || event.location.toLowerCase().indexOf(keyword) < 0) { return false; } } else { return false; } } // 原有事件权限检查逻辑保持不变 if (!event.organizer || event.organizer.email == user.getEmail()) { return true; } if (!event.attendees) return false; let matching = event.attendees.filter(function(attendee) { return attendee.self; }); return matching.length > 0 && matching[0].responseStatus == 'accepted'; }
方案2:精准识别官方Work Location事件
Google Workspace的官方Work Location事件会在extendedProperties.shared中存储workLocation属性,可直接筛选:
- 新增事件获取函数:
function findWorkLocationEvents(user, start, end, optSince) { let params = { timeMin: formatDateAsRFC3339(start), timeMax: formatDateAsRFC3339(end), showDeleted: true, singleEvents: true }; if (optSince) { params.updatedMin = formatDateAsRFC3339(optSince); } let pageToken = null; let events = []; do { params.pageToken = pageToken; let response; try { response = Calendar.Events.list(user.getEmail(), params); } catch (e) { console.error('获取用户%s的工作地点事件失败: %s', user.getEmail(), e.toString()); continue; } events = events.concat(response.items.filter(function(item) { // 优先识别官方Work Location属性,兼容手动设置的地点 return item.extendedProperties?.shared?.workLocation || (item.location && (item.location.toLowerCase().includes('office') || item.location.toLowerCase().includes('home'))); })); pageToken = response.nextPageToken; } while (pageToken); return events; } - 在sync函数中调用该函数:
users.forEach(function(user) { // ... 获取userName的代码 ... // 处理休假/OOO事件 KEYWORDS.forEach(function(keyword) { let events = findEvents(user, keyword, today, maxDate, lastRun); events.forEach(function(event) { importEvent(userName, event); count++; }); }); // 新增:处理Work Location事件 let workLocationEvents = findWorkLocationEvents(user, today, maxDate, lastRun); workLocationEvents.forEach(function(event) { event.summary = `[${userName}] 工作地点: ${event.location || '未指定'}`; importEvent(userName, event); count++; }); });
核心修改后的完整代码片段
let TEAM_CALENDAR_ID = 'ENTER_TEAM_CALENDAR_ID_HERE'; let GROUP_EMAIL = 'ENTER_GOOGLE_GROUP_EMAIL_HERE'; let KEYWORDS = ['vacation', 'ooo', 'out of office', 'offline']; let MONTHS_IN_ADVANCE = 3; function sync() { let today = new Date(); let maxDate = new Date(); maxDate.setMonth(maxDate.getMonth() + MONTHS_IN_ADVANCE); let lastRun = PropertiesService.getScriptProperties().getProperty('lastRun'); lastRun = lastRun ? new Date(lastRun) : null; let group = GroupsApp.getGroupByEmail(GROUP_EMAIL); let users = group.getUsers(); let count = 0; users.forEach(function(user) { let userEmail = user.getEmail(); let userName; try { let userInfo = AdminDirectory.Users.get(userEmail); userName = userInfo.name.fullName || userEmail.split('@')[0]; } catch (e) { userName = userEmail.split('@')[0]; console.warn(`无法获取${userEmail}的姓名: ${e}`); } console.log('用户姓名: %s, 邮箱: %s', userName, userEmail); // 处理休假/OOO事件 KEYWORDS.forEach(function(keyword) { let events = findEvents(user, keyword, today, maxDate, lastRun); events.forEach(function(event) { importEvent(userName, event); count++; }); }); // 处理Work Location事件 let workLocationEvents = findWorkLocationEvents(user, today, maxDate, lastRun); workLocationEvents.forEach(function(event) { event.summary = `[${userName}] 工作地点: ${event.location || '未指定'}`; importEvent(userName, event); count++; }); }); PropertiesService.getScriptProperties().setProperty('lastRun', today); console.log('共导入 %s 个事件', count); } function importEvent(userName, event) { event.summary = `[${userName}] ${event.summary}`; event.organizer = { id: TEAM_CALENDAR_ID }; event.attendees = []; console.log('导入事件: %s', event.summary); try { Calendar.Events.import(event, TEAM_CALENDAR_ID); } catch (e) { console.error('导入事件失败: %s, 跳过该事件', e.toString()); } } function findWorkLocationEvents(user, start, end, optSince) { let params = { timeMin: formatDateAsRFC3339(start), timeMax: formatDateAsRFC3339(end), showDeleted: true, singleEvents: true }; if (optSince) { params.updatedMin = formatDateAsRFC3339(optSince); } let pageToken = null; let events = []; do { params.pageToken = pageToken; let response; try { response = Calendar.Events.list(user.getEmail(), params); } catch (e) { console.error('获取用户%s的工作地点事件失败: %s', user.getEmail(), e.toString()); continue; } events = events.concat(response.items.filter(function(item) { return item.extendedProperties?.shared?.workLocation || (item.location && (item.location.toLowerCase().includes('office') || item.location.toLowerCase().includes('home'))); })); pageToken = response.nextPageToken; } while (pageToken); return events; } // 其余原有函数(findEvents、shoudImportEvent、formatDateAsRFC3339、setup)保持不变
内容的提问来源于stack exchange,提问作者Terik Holley
相关产品推荐
相关产品推荐

