You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Google Calendar API脚本TypeError报错及功能扩展求助

问题解决:Google Apps Script日历同步脚本的两个问题

一、修复TypeError: user.getName is not a function报错

GroupsApp返回的User对象并非总能调用getName()方法——如果群组包含外部用户,或者脚本没有足够权限读取用户姓名,就会触发这个错误。要稳定获取用户全名,可借助Admin Directory API:

  1. 启用Admin Directory服务:在脚本编辑器的「服务」面板中添加「Admin Directory」,完成授权(需拥有Google Workspace管理员权限)。
  2. 替换姓名获取逻辑:
    将原代码中的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}`);
    }
    
  3. 更新事件导入函数:
    修改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事件

  1. 新增关键词(可选):扩展关键词数组兼容手动输入的情况
    let KEYWORDS = ['vacation', 'ooo', 'out of office', 'offline', 'work location', 'office', 'home'];
    
  2. 增强事件过滤逻辑:修改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属性,可直接筛选:

  1. 新增事件获取函数:
    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;
    }
    
  2. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 01:31:01