Google Apps Script 能開發什麼?深入淺出完整應用指南

Google Apps Script 能開發什麼?深入淺出完整應用指南

Google Apps Script 自動化工作流程

前言:一個改變工作效率的真實故事

小華是一名行銷專員,每週都要花費 3 小時整理客戶回饋資料:從 Gmail 收集客戶意見、整理到 Google Sheets、更新客戶資料庫、然後發送感謝信件。這個重複性工作不僅耗時,還容易出錯。

直到他發現了 Google Apps Script(GAS),一切都改變了。現在,這個過程完全自動化:當客戶回饋信件進入 Gmail 時,系統自動擷取內容、分類整理到試算表、更新客戶資料、甚至自動發送個人化的感謝信件。原本 3 小時的工作,現在只需要 5 分鐘檢查結果。

這就是 Google Apps Script 的威力 —— 讓你的 Google Workspace 變成一個強大的自動化工作站。

目錄

  1. 什麼是 Google Apps Script?
  2. 環境設置與入門
  3. 六個實用開發範例
  4. 性能優化技巧
  5. 與其他自動化工具比較
  6. 2024-2025 最新趨勢
  7. 實用資源與學習路徑
  8. 總結與未來展望

什麼是 Google Apps Script?

Google Apps Script(GAS)是 Google 提供的雲端腳本平台,基於 JavaScript 語言,讓你能夠自動化和擴展 Google Workspace 應用程式的功能。簡單來說,它就像是 Google 服務之間的「膠水」,能夠串聯 Gmail、Google Sheets、Google Drive、Google Calendar 等服務。

Google Apps Script 服務整合架構

核心特色

  • 雲端執行:無需安裝任何軟體,直接在瀏覽器中開發
  • 深度整合:與 Google Workspace 服務無縫整合
  • 自動觸發:支援時間觸發、事件觸發等多種執行方式
  • 免費使用:在合理使用範圍內完全免費
  • JavaScript 基礎:使用熟悉的 JavaScript 語法

適用場景

  • 報表自動化生成
  • 郵件批量處理
  • 資料同步與備份
  • 工作流程自動化
  • API 整合與資料擷取
  • 自訂功能擴展

環境設置與入門

開始使用 Google Apps Script

  1. 訪問 Google Apps Script

  2. 建立新專案

    • 點選「新增專案」
    • 選擇適當的專案名稱
  3. 基本介面介紹

    • 程式碼編輯器:撰寫 JavaScript 程式碼
    • 執行功能:測試和除錯程式碼
    • 觸發器管理:設定自動執行條件
    • 權限管理:管理 API 存取權限

Hello World 範例

/**
 * 第一個 Google Apps Script 程式
 * 功能:在日誌中顯示 "Hello, World!"
 */
function helloWorld() {
  // 使用 console.log 在執行日誌中顯示訊息
  console.log("Hello, World!");
  
  // 使用 Logger.log 也可以記錄訊息(舊版方法)
  Logger.log("這是我的第一個 GAS 程式!");
}

六個實用開發範例

範例 1:自動發送 Gmail 郵件

這個範例展示如何使用 GAS 自動發送個人化郵件,適合用於客戶通知、生日祝福、或定期報告發送。

/**
 * 自動發送 Gmail 郵件
 * 功能:從 Google Sheets 讀取收件人資料,發送個人化郵件
 */
function sendAutomatedEmails() {
  // 開啟指定的 Google Sheets
  const spreadsheetId = 'YOUR_SPREADSHEET_ID';
  const sheet = SpreadsheetApp.openById(spreadsheetId).getActiveSheet();
  
  // 取得資料範圍(假設 A 欄是姓名,B 欄是郵件地址,C 欄是狀態)
  const dataRange = sheet.getRange('A2:C');
  const data = dataRange.getValues();
  
  // 郵件模板設定
  const emailTemplate = {
    subject: '感謝您的支持 - 個人化通知',
    htmlBody: `
      <div style="font-family: Arial, sans-serif; max-width: 600px;">
        <h2 style="color: #4285f4;">親愛的 {{name}},</h2>
        <p>感謝您一直以來的支持!</p>
        <p>這是一封透過 Google Apps Script 自動發送的個人化郵件。</p>
        <hr>
        <p style="color: #666; font-size: 12px;">
          此郵件由系統自動發送,請勿直接回覆。
        </p>
      </div>
    `
  };
  
  // 遍歷每一列資料
  data.forEach((row, index) => {
    const [name, email, status] = row;
    
    // 檢查是否已發送(避免重複發送)
    if (status !== '已發送' && email && name) {
      try {
        // 替換郵件模板中的個人化內容
        const personalizedBody = emailTemplate.htmlBody.replace('{{name}}', name);
        
        // 發送郵件
        GmailApp.sendEmail(
          email,
          emailTemplate.subject,
          '', // 純文字內容(可為空)
          {
            htmlBody: personalizedBody,
            name: '您的名稱或公司名稱' // 寄件者顯示名稱
          }
        );
        
        // 更新狀態為已發送
        sheet.getRange(index + 2, 3).setValue('已發送');
        console.log(`郵件已發送給:${name} (${email})`);
        
        // 加入延遲避免觸發 Gmail 限制
        Utilities.sleep(1000);
        
      } catch (error) {
        console.error(`發送郵件給 ${name} 時出錯:`, error);
        sheet.getRange(index + 2, 3).setValue('發送失敗');
      }
    }
  });
  
  console.log('批量郵件發送完成!');
}

/**
 * 設定定時觸發器
 * 每天上午 9 點自動執行郵件發送
 */
function createEmailTrigger() {
  ScriptApp.newTrigger('sendAutomatedEmails')
    .timeBased()
    .everyDays(1)
    .atHour(9)
    .create();
}

使用步驟:

  1. 建立 Google Sheets,包含姓名、郵件地址、狀態三欄
  2. 替換程式碼中的 YOUR_SPREADSHEET_ID
  3. 執行 createEmailTrigger() 設定自動觸發
  4. 測試 sendAutomatedEmails() 功能

範例 2:Google Sheets 資料處理與分析

這個範例展示如何使用 GAS 進行資料清理、分析和報表生成。

/**
 * Google Sheets 資料處理與分析
 * 功能:自動清理資料、計算統計資料、生成報表
 */
function processSpreadsheetData() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const dataSheet = spreadsheet.getSheetByName('原始資料');
  const reportSheet = getOrCreateSheet(spreadsheet, '分析報表');
  
  // 取得原始資料
  const dataRange = dataSheet.getDataRange();
  const data = dataRange.getValues();
  const headers = data[0];
  const rows = data.slice(1);
  
  console.log(`開始處理 ${rows.length} 筆資料...`);
  
  // 資料清理
  const cleanedData = cleanData(rows);
  console.log(`資料清理完成,有效資料:${cleanedData.length} 筆`);
  
  // 生成分析報表
  generateReport(reportSheet, cleanedData, headers);
  
  console.log('資料處理與分析完成!');
}

/**
 * 資料清理函數
 * @param {Array} data - 原始資料陣列
 * @return {Array} - 清理後的資料
 */
function cleanData(data) {
  return data.filter(row => {
    // 移除空白列
    if (row.every(cell => !cell)) return false;
    
    // 移除無效的郵件地址(假設第二欄是郵件)
    const email = row[1];
    if (email && !isValidEmail(email)) return false;
    
    return true;
  }).map(row => {
    // 資料標準化
    return row.map(cell => {
      if (typeof cell === 'string') {
        // 移除多餘空白
        return cell.trim();
      }
      return cell;
    });
  });
}

/**
 * 郵件格式驗證
 * @param {string} email - 郵件地址
 * @return {boolean} - 是否為有效郵件
 */
function isValidEmail(email) {
  const emailRegex = /<sup>[</sup>\s@]+@[<sup>\s@]+\.[</sup>\s@]+$/;
  return emailRegex.test(email);
}

/**
 * 生成分析報表
 * @param {Sheet} sheet - 報表工作表
 * @param {Array} data - 清理後的資料
 * @param {Array} headers - 欄位標題
 */
function generateReport(sheet, data, headers) {
  // 清除現有內容
  sheet.clear();
  
  // 設定報表標題
  sheet.getRange('A1').setValue('資料分析報表');
  sheet.getRange('A1').setFontSize(16).setFontWeight('bold');
  
  // 基本統計資訊
  const stats = [
    ['生成時間', new Date()],
    ['總資料筆數', data.length],
    ['資料完整性', `${(data.length / (data.length + 100)) * 100}%`] // 假設統計
  ];
  
  // 寫入統計資訊
  let currentRow = 3;
  stats.forEach(([label, value]) => {
    sheet.getRange(currentRow, 1).setValue(label);
    sheet.getRange(currentRow, 2).setValue(value);
    currentRow++;
  });
  
  // 如果資料包含數值欄位,進行進階分析
  if (data.length > 0) {
    currentRow += 2;
    sheet.getRange(currentRow, 1).setValue('詳細分析');
    sheet.getRange(currentRow, 1).setFontWeight('bold');
    currentRow++;
    
    // 假設第三欄是數值資料進行分析
    const numericData = data
      .map(row => parseFloat(row[2]))
      .filter(num => !isNaN(num));
    
    if (numericData.length > 0) {
      const average = numericData.reduce((sum, num) => sum + num, 0) / numericData.length;
      const max = Math.max(...numericData);
      const min = Math.min(...numericData);
      
      const analyticsData = [
        ['平均值', average.toFixed(2)],
        ['最大值', max],
        ['最小值', min],
        ['資料範圍', `${min} - ${max}`]
      ];
      
      analyticsData.forEach(([label, value]) => {
        sheet.getRange(currentRow, 1).setValue(label);
        sheet.getRange(currentRow, 2).setValue(value);
        currentRow++;
      });
    }
  }
  
  // 美化報表格式
  formatReport(sheet, currentRow);
}

/**
 * 報表格式美化
 * @param {Sheet} sheet - 工作表
 * @param {number} lastRow - 最後一列
 */
function formatReport(sheet, lastRow) {
  // 設定欄寬
  sheet.autoResizeColumns(1, 2);
  
  // 設定邊框
  const range = sheet.getRange(1, 1, lastRow, 2);
  range.setBorder(true, true, true, true, true, true);
  
  // 設定交替列顏色
  for (let i = 3; i <= lastRow; i += 2) {
    sheet.getRange(i, 1, 1, 2).setBackground('#f8f9fa');
  }
}

/**
 * 取得或建立工作表
 * @param {Spreadsheet} spreadsheet - 試算表物件
 * @param {string} sheetName - 工作表名稱
 * @return {Sheet} - 工作表物件
 */
function getOrCreateSheet(spreadsheet, sheetName) {
  let sheet = spreadsheet.getSheetByName(sheetName);
  if (!sheet) {
    sheet = spreadsheet.insertSheet(sheetName);
  }
  return sheet;
}

/**
 * 設定定期資料處理觸發器
 */
function setupDataProcessingTrigger() {
  // 每週一上午 8 點執行
  ScriptApp.newTrigger('processSpreadsheetData')
    .timeBased()
    .onWeekDay(ScriptApp.WeekDay.MONDAY)
    .atHour(8)
    .create();
}

範例 3:Google Calendar 自動化排程

這個範例展示如何自動建立行事曆事件、管理會議室預約、發送提醒等功能。

/**
 * Google Calendar 自動化排程
 * 功能:從 Sheets 讀取會議資料,自動建立行事曆事件
 */
function createCalendarEvents() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = spreadsheet.getSheetByName('會議安排');
  
  // 取得會議資料(假設格式:標題、開始時間、結束時間、參與者、地點、說明)
  const dataRange = sheet.getRange('A2:G');
  const meetings = dataRange.getValues();
  
  // 取得預設行事曆
  const calendar = CalendarApp.getDefaultCalendar();
  
  meetings.forEach((meeting, index) => {
    const [title, startTime, endTime, attendees, location, description, status] = meeting;
    
    // 跳過空白列或已處理的會議
    if (!title || status === '已建立') return;
    
    try {
      // 建立行事曆事件
      const event = calendar.createEvent(
        title,
        new Date(startTime),
        new Date(endTime),
        {
          description: description || '',
          location: location || '',
          guests: attendees || '',
          sendInvites: true // 自動發送邀請
        }
      );
      
      // 設定提醒
      event.addEmailReminder(30); // 30 分鐘前郵件提醒
      event.addPopupReminder(15);  // 15 分鐘前彈出提醒
      
      // 更新狀態
      sheet.getRange(index + 2, 7).setValue('已建立');
      
      console.log(`已建立會議:${title}`);
      
    } catch (error) {
      console.error(`建立會議 "${title}" 時出錯:`, error);
      sheet.getRange(index + 2, 7).setValue('建立失敗');
    }
  });
}

/**
 * 會議室可用性檢查
 * @param {string} roomEmail - 會議室郵件地址
 * @param {Date} startTime - 開始時間
 * @param {Date} endTime - 結束時間
 * @return {boolean} - 是否可用
 */
function checkRoomAvailability(roomEmail, startTime, endTime) {
  try {
    const calendar = CalendarApp.getCalendarById(roomEmail);
    if (!calendar) return false;
    
    const events = calendar.getEvents(startTime, endTime);
    return events.length === 0; // 沒有事件表示可用
    
  } catch (error) {
    console.error(`檢查會議室 ${roomEmail} 可用性時出錯:`, error);
    return false;
  }
}

/**
 * 智能會議室預約
 * 功能:自動尋找可用會議室並預約
 */
function smartRoomBooking() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const dataRange = sheet.getRange('A2:H');
  const meetings = dataRange.getValues();
  
  // 會議室清單(實際使用時請替換為真實的會議室郵件)
  const meetingRooms = [
    { name: '小會議室A', email: 'room-a@company.com', capacity: 6 },
    { name: '大會議室B', email: 'room-b@company.com', capacity: 12 },
    { name: '視訊會議室C', email: 'room-c@company.com', capacity: 8 }
  ];
  
  meetings.forEach((meeting, index) => {
    const [title, startTime, endTime, attendees, requiredCapacity, , , status] = meeting;
    
    if (!title || status === '已預約') return;
    
    const participantCount = attendees ? attendees.split(',').length : 1;
    const capacity = requiredCapacity || participantCount;
    
    // 尋找合適的會議室
    const availableRoom = findAvailableRoom(
      meetingRooms,
      new Date(startTime),
      new Date(endTime),
      capacity
    );
    
    if (availableRoom) {
      try {
        // 預約會議室
        const roomCalendar = CalendarApp.getCalendarById(availableRoom.email);
        roomCalendar.createEvent(
          `[預約] ${title}`,
          new Date(startTime),
          new Date(endTime),
          {
            description: `會議:${title}\n參與人數:${participantCount}`,
            guests: attendees || ''
          }
        );
        
        // 更新會議室資訊
        sheet.getRange(index + 2, 6).setValue(availableRoom.name);
        sheet.getRange(index + 2, 8).setValue('已預約');
        
        console.log(`已為 "${title}" 預約 ${availableRoom.name}`);
        
      } catch (error) {
        console.error(`預約會議室失敗:`, error);
        sheet.getRange(index + 2, 8).setValue('預約失敗');
      }
    } else {
      sheet.getRange(index + 2, 8).setValue('無可用會議室');
    }
  });
}

/**
 * 尋找可用會議室
 * @param {Array} rooms - 會議室清單
 * @param {Date} startTime - 開始時間
 * @param {Date} endTime - 結束時間
 * @param {number} requiredCapacity - 所需容量
 * @return {Object|null} - 可用的會議室或 null
 */
function findAvailableRoom(rooms, startTime, endTime, requiredCapacity) {
  // 按容量排序,優先使用剛好符合需求的會議室
  const suitableRooms = rooms
    .filter(room => room.capacity >= requiredCapacity)
    .sort((a, b) => a.capacity - b.capacity);
  
  for (const room of suitableRooms) {
    if (checkRoomAvailability(room.email, startTime, endTime)) {
      return room;
    }
  }
  
  return null;
}

/**
 * 會議提醒系統
 * 功能:在會議前一天發送提醒郵件
 */
function sendMeetingReminders() {
  const calendar = CalendarApp.getDefaultCalendar();
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);
  tomorrow.setHours(0, 0, 0, 0);
  
  const dayAfterTomorrow = new Date(tomorrow);
  dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 1);
  
  // 取得明天的會議
  const events = calendar.getEvents(tomorrow, dayAfterTomorrow);
  
  events.forEach(event => {
    const attendees = event.getGuestList();
    if (attendees.length === 0) return;
    
    const reminderEmail = {
      subject: `會議提醒:${event.getTitle()}`,
      htmlBody: `
        <div style="font-family: Arial, sans-serif;">
          <h2>會議提醒</h2>
          <p><strong>會議主題:</strong>${event.getTitle()}</p>
          <p><strong>時間:</strong>${event.getStartTime().toLocaleDateString()} 
             ${event.getStartTime().toLocaleTimeString()} - 
             ${event.getEndTime().toLocaleTimeString()}</p>
          <p><strong>地點:</strong>${event.getLocation() || '未指定'}</p>
          <p><strong>說明:</strong></p>
          <p>${event.getDescription() || '無'}</p>
          <hr>
          <p style="color: #666; font-size: 12px;">
            此提醒由系統自動發送
          </p>
        </div>
      `
    };
    
    // 發送給所有參與者
    attendees.forEach(guest => {
      try {
        GmailApp.sendEmail(
          guest.getEmail(),
          reminderEmail.subject,
          '',
          { htmlBody: reminderEmail.htmlBody }
        );
      } catch (error) {
        console.error(`發送提醒給 ${guest.getEmail()} 失敗:`, error);
      }
    });
  });
  
  console.log(`已發送 ${events.length} 個會議的提醒`);
}

範例 4:多服務串聯自動化

這個範例展示如何整合多個 Google 服務,建立完整的工作流程自動化。

/**
 * 多服務串聯自動化:客戶回饋處理系統
 * 整合 Gmail、Sheets、Drive、Calendar 的完整工作流程
 */

/**
 * 主要工作流程
 * 1. 監控 Gmail 新郵件
 * 2. 提取客戶回饋內容
 * 3. 分類並儲存到 Sheets
 * 4. 生成回覆郵件
 * 5. 安排後續追蹤行程
 * 6. 備份到 Drive
 */
function processCustomerFeedbackWorkflow() {
  console.log('開始執行客戶回饋處理工作流程...');
  
  // 步驟 1: 監控並處理新的客戶回饋郵件
  const newFeedback = monitorGmailFeedback();
  
  if (newFeedback.length === 0) {
    console.log('沒有新的客戶回饋郵件');
    return;
  }
  
  // 步驟 2: 處理每個回饋
  newFeedback.forEach(feedback => {
    try {
      // 步驟 3: 儲存到試算表
      const recordId = saveFeedbackToSheet(feedback);
      
      // 步驟 4: 分析情感並分類
      const analysis = analyzeFeedbackSentiment(feedback.body);
      updateFeedbackAnalysis(recordId, analysis);
      
      // 步驟 5: 生成並發送回覆
      if (analysis.requiresResponse) {
        sendAutoReply(feedback, analysis);
      }
      
      // 步驟 6: 安排後續追蹤
      if (analysis.priority === 'high') {
        scheduleFollowUp(feedback, analysis);
      }
      
      // 步驟 7: 標記郵件為已處理
      markEmailAsProcessed(feedback.thread);
      
      console.log(`成功處理來自 ${feedback.sender} 的回饋`);
      
    } catch (error) {
      console.error(`處理回饋時出錯:`, error);
    }
  });
  
  // 步驟 8: 生成日報表
  generateDailyReport();
  
  console.log('客戶回饋處理工作流程完成');
}

/**
 * 監控 Gmail 中的新客戶回饋
 * @return {Array} 新的回饋郵件陣列
 */
function monitorGmailFeedback() {
  // 搜尋特定標籤或主題的未讀郵件
  const searchQuery = 'label:customer-feedback is:unread';
  const threads = GmailApp.search(searchQuery, 0, 50);
  
  const feedbackEmails = [];
  
  threads.forEach(thread => {
    const messages = thread.getMessages();
    messages.forEach(message => {
      if (message.isUnread()) {
        feedbackEmails.push({
          thread: thread,
          message: message,
          sender: message.getFrom(),
          subject: message.getSubject(),
          body: message.getPlainBody(),
          receivedTime: message.getDate(),
          attachments: message.getAttachments()
        });
      }
    });
  });
  
  return feedbackEmails;
}

/**
 * 儲存回饋到 Google Sheets
 * @param {Object} feedback - 回饋物件
 * @return {string} 記錄 ID
 */
function saveFeedbackToSheet(feedback) {
  const sheet = getOrCreateFeedbackSheet();
  const recordId = Utilities.getUuid();
  
  const rowData = [
    recordId,
    feedback.receivedTime,
    feedback.sender,
    feedback.subject,
    feedback.body,
    '', // 情感分析結果
    '', // 優先級
    '', // 處理狀態
    '', // 回覆時間
    '', // 備註
  ];
  
  sheet.appendRow(rowData);
  return recordId;
}

/**
 * 取得或建立客戶回饋工作表
 * @return {Sheet} 工作表物件
 */
function getOrCreateFeedbackSheet() {
  const spreadsheet = SpreadsheetApp.openById('YOUR_FEEDBACK_SPREADSHEET_ID');
  let sheet = spreadsheet.getSheetByName('客戶回饋');
  
  if (!sheet) {
    sheet = spreadsheet.insertSheet('客戶回饋');
    // 設定標題列
    const headers = [
      '記錄ID', '接收時間', '寄件者', '主題', '內容',
      '情感分析', '優先級', '處理狀態', '回覆時間', '備註'
    ];
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
  }
  
  return sheet;
}

/**
 * 分析回饋情感和優先級
 * @param {string} feedbackText - 回饋文本
 * @return {Object} 分析結果
 */
function analyzeFeedbackSentiment(feedbackText) {
  // 簡單的關鍵字分析(實際應用中可整合 AI API)
  const positiveKeywords = ['滿意', '好', '棒', '感謝', '讚', '優秀'];
  const negativeKeywords = ['不滿', '差', '爛', '問題', '錯誤', '投訴'];
  const urgentKeywords = ['緊急', '立即', '急', '重要', '嚴重'];
  
  const text = feedbackText.toLowerCase();
  
  let sentiment = 'neutral';
  let priority = 'normal';
  let requiresResponse = false;
  
  // 情感分析
  const positiveScore = positiveKeywords.filter(keyword => text.includes(keyword)).length;
  const negativeScore = negativeKeywords.filter(keyword => text.includes(keyword)).length;
  
  if (negativeScore > positiveScore) {
    sentiment = 'negative';
    requiresResponse = true;
  } else if (positiveScore > negativeScore) {
    sentiment = 'positive';
  }
  
  // 優先級判斷
  if (urgentKeywords.some(keyword => text.includes(keyword)) || sentiment === 'negative') {
    priority = 'high';
    requiresResponse = true;
  }
  
  return {
    sentiment,
    priority,
    requiresResponse,
    analysis: `情感:${sentiment}, 優先級:${priority}`
  };
}

/**
 * 更新回饋分析結果
 * @param {string} recordId - 記錄 ID
 * @param {Object} analysis - 分析結果
 */
function updateFeedbackAnalysis(recordId, analysis) {
  const sheet = getOrCreateFeedbackSheet();
  const data = sheet.getDataRange().getValues();
  
  // 尋找對應的記錄
  for (let i = 0; i < data.length; i++) {
    if (data[i][0] === recordId) {
      sheet.getRange(i + 1, 6).setValue(analysis.sentiment);
      sheet.getRange(i + 1, 7).setValue(analysis.priority);
      sheet.getRange(i + 1, 8).setValue('已分析');
      break;
    }
  }
}

/**
 * 發送自動回覆
 * @param {Object} feedback - 原始回饋
 * @param {Object} analysis - 分析結果
 */
function sendAutoReply(feedback, analysis) {
  let replyTemplate;
  
  if (analysis.sentiment === 'positive') {
    replyTemplate = `
      <div style="font-family: Arial, sans-serif;">
        <p>親愛的客戶,您好:</p>
        <p>感謝您給予我們正面的回饋!您的支持是我們持續改進的動力。</p>
        <p>如果您有任何其他建議,歡迎隨時與我們聯繫。</p>
        <br>
        <p>祝您順心!</p>
        <p>客服團隊</p>
      </div>
    `;
  } else if (analysis.sentiment === 'negative') {
    replyTemplate = `
      <div style="font-family: Arial, sans-serif;">
        <p>親愛的客戶,您好:</p>
        <p>非常感謝您的回饋,我們對造成您的不便深感抱歉。</p>
        <p>我們已將您的意見轉交給相關部門處理,會在 24 小時內有專人與您聯繫。</p>
        <p>再次為您帶來的困擾致歉,我們會持續改善服務品質。</p>
        <br>
        <p>祝您順心!</p>
        <p>客服團隊</p>
      </div>
    `;
  }
  
  if (replyTemplate) {
    feedback.thread.reply('', {
      htmlBody: replyTemplate,
      subject: `Re: ${feedback.subject}`
    });
    
    console.log(`已發送自動回覆給:${feedback.sender}`);
  }
}

/**
 * 安排後續追蹤行程
 * @param {Object} feedback - 回饋物件
 * @param {Object} analysis - 分析結果
 */
function scheduleFollowUp(feedback, analysis) {
  const calendar = CalendarApp.getDefaultCalendar();
  const followUpDate = new Date();
  followUpDate.setHours(followUpDate.getHours() + 24); // 24 小時後追蹤
  
  const endTime = new Date(followUpDate);
  endTime.setHours(endTime.getHours() + 1);
  
  calendar.createEvent(
    `追蹤客戶回饋:${feedback.sender}`,
    followUpDate,
    endTime,
    {
      description: `
        客戶:${feedback.sender}
        原始主題:${feedback.subject}
        優先級:${analysis.priority}
        情感:${analysis.sentiment}
        
        回饋內容:
        ${feedback.body.substring(0, 200)}...
      `,
      reminders: [
        { method: 'email', minutes: 60 },
        { method: 'popup', minutes: 15 }
      ]
    }
  );
  
  console.log(`已安排追蹤行程:${feedback.sender}`);
}

/**
 * 標記郵件為已處理
 * @param {GmailThread} thread - Gmail 對話串
 */
function markEmailAsProcessed(thread) {
  // 加上「已處理」標籤
  const processedLabel = GmailApp.getUserLabelByName('已處理') || 
                        GmailApp.createLabel('已處理');
  thread.addLabel(processedLabel);
  
  // 移除「customer-feedback」標籤以避免重複處理
  const feedbackLabel = GmailApp.getUserLabelByName('customer-feedback');
  if (feedbackLabel) {
    thread.removeLabel(feedbackLabel);
  }
  
  // 標記為已讀
  thread.markRead();
}

/**
 * 生成每日報表
 */
function generateDailyReport() {
  const sheet = getOrCreateFeedbackSheet();
  const today = new Date();
  const todayStr = Utilities.formatDate(today, Session.getScriptTimeZone(), 'yyyy-MM-dd');
  
  // 統計今日處理的回饋
  const data = sheet.getDataRange().getValues();
  const todayFeedback = data.filter(row => {
    if (row[1] instanceof Date) {
      const dateStr = Utilities.formatDate(row[1], Session.getScriptTimeZone(), 'yyyy-MM-dd');
      return dateStr === todayStr;
    }
    return false;
  });
  
  const stats = {
    total: todayFeedback.length,
    positive: todayFeedback.filter(row => row[5] === 'positive').length,
    negative: todayFeedback.filter(row => row[5] === 'negative').length,
    high_priority: todayFeedback.filter(row => row[6] === 'high').length
  };
  
  // 發送報表給管理者
  const reportEmail = `
    <div style="font-family: Arial, sans-serif;">
      <h2>客戶回饋日報表 - ${todayStr}</h2>
      <table border="1" style="border-collapse: collapse;">
        <tr><td>總計</td><td>${stats.total}</td></tr>
        <tr><td>正面回饋</td><td>${stats.positive}</td></tr>
        <tr><td>負面回饋</td><td>${stats.negative}</td></tr>
        <tr><td>高優先級</td><td>${stats.high_priority}</td></tr>
      </table>
      <p>詳細資料請查看 <a href="${sheet.getParent().getUrl()}">客戶回饋試算表</a></p>
    </div>
  `;
  
  GmailApp.sendEmail(
    'manager@company.com', // 請替換為實際的管理者郵件
    `客戶回饋日報表 - ${todayStr}`,
    '',
    { htmlBody: reportEmail }
  );
}

/**
 * 設定工作流程觸發器
 */
function setupWorkflowTriggers() {
  // 每 30 分鐘檢查一次新郵件
  ScriptApp.newTrigger('processCustomerFeedbackWorkflow')
    .timeBased()
    .everyMinutes(30)
    .create();
  
  // 每天下午 6 點生成日報表
  ScriptApp.newTrigger('generateDailyReport')
    .timeBased()
    .everyDays(1)
    .atHour(18)
    .create();
}

範例 5:網頁爬蟲與資料蒐集

這個範例展示如何使用 GAS 進行網頁資料擷取和 API 整合。

/**
 * 網頁爬蟲與資料蒐集
 * 功能:自動擷取網站資料、API 整合、資料清理與儲存
 */

/**
 * 主要資料蒐集工作流程
 */
function dataCollectionWorkflow() {
  console.log('開始執行資料蒐集工作流程...');
  
  try {
    // 1. 蒐集新聞資料
    const newsData = collectNewsData();
    
    // 2. 蒐集股價資料
    const stockData = collectStockData();
    
    // 3. 蒐集天氣資料
    const weatherData = collectWeatherData();
    
    // 4. 整合資料並儲存
    const consolidatedData = {
      timestamp: new Date(),
      news: newsData,
      stocks: stockData,
      weather: weatherData
    };
    
    saveDataToSheet(consolidatedData);
    
    // 5. 生成分析報告
    generateAnalysisReport(consolidatedData);
    
    console.log('資料蒐集工作流程完成');
    
  } catch (error) {
    console.error('資料蒐集過程中發生錯誤:', error);
    sendErrorNotification(error);
  }
}

/**
 * 蒐集新聞資料
 * @return {Array} 新聞資料陣列
 */
function collectNewsData() {
  const newsUrls = [
    'https://jsonplaceholder.typicode.com/posts', // 示例 API
    // 實際使用時請替換為真實的新聞 API
  ];
  
  const allNewsData = [];
  
  newsUrls.forEach(url => {
    try {
      const response = UrlFetchApp.fetch(url, {
        method: 'GET',
        headers: {
          'User-Agent': 'Google Apps Script Data Collector',
          'Accept': 'application/json'
        },
        muteHttpExceptions: true
      });
      
      if (response.getResponseCode() === 200) {
        const data = JSON.parse(response.getContentText());
        
        // 資料處理和清理
        const processedData = data.slice(0, 5).map(item => ({
          id: item.id,
          title: item.title,
          content: item.body,
          source: 'JSON Placeholder',
          timestamp: new Date()
        }));
        
        allNewsData.push(...processedData);
      } else {
        console.error(`取得新聞資料失敗,HTTP 狀態碼:${response.getResponseCode()}`);
      }
      
    } catch (error) {
      console.error('蒐集新聞資料時出錯:', error);
    }
    
    // 避免過於頻繁的請求
    Utilities.sleep(1000);
  });
  
  console.log(`成功蒐集 ${allNewsData.length} 筆新聞資料`);
  return allNewsData;
}

/**
 * 蒐集股價資料
 * @return {Array} 股價資料陣列
 */
function collectStockData() {
  // 使用 Google Finance 函數或外部 API
  const stockSymbols = ['AAPL', 'GOOGL', 'MSFT', 'TSLA'];
  const stockData = [];
  
  stockSymbols.forEach(symbol => {
    try {
      // 這裡使用模擬資料,實際使用時可整合真實的股價 API
      const simulatedPrice = Math.random() * 1000 + 100;
      const simulatedChange = (Math.random() - 0.5) * 20;
      
      stockData.push({
        symbol: symbol,
        price: simulatedPrice.toFixed(2),
        change: simulatedChange.toFixed(2),
        changePercent: ((simulatedChange / simulatedPrice) * 100).toFixed(2),
        timestamp: new Date()
      });
      
      // 實際的股價 API 呼叫範例:
      /*
      const apiKey = 'YOUR_API_KEY';
      const url = `https://api.example.com/stock/${symbol}?apikey=${apiKey}`;
      const response = UrlFetchApp.fetch(url);
      const data = JSON.parse(response.getContentText());
      // 處理回傳的資料...
      */
      
    } catch (error) {
      console.error(`取得 ${symbol} 股價資料時出錯:`, error);
    }
  });
  
  console.log(`成功蒐集 ${stockData.length} 檔股票資料`);
  return stockData;
}

/**
 * 蒐集天氣資料
 * @return {Object} 天氣資料物件
 */
function collectWeatherData() {
  const cities = ['台北', '台中', '高雄'];
  const weatherData = {};
  
  cities.forEach(city => {
    try {
      // 這裡使用模擬資料,實際使用時可整合氣象 API
      weatherData[city] = {
        temperature: Math.floor(Math.random() * 15) + 20, // 20-35度
        humidity: Math.floor(Math.random() * 30) + 60,    // 60-90%
        condition: ['晴天', '多雲', '雨天'][Math.floor(Math.random() * 3)],
        timestamp: new Date()
      };
      
      // 實際的天氣 API 呼叫範例:
      /*
      const apiKey = 'YOUR_WEATHER_API_KEY';
      const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}`;
      const response = UrlFetchApp.fetch(url);
      const data = JSON.parse(response.getContentText());
      // 處理回傳的資料...
      */
      
    } catch (error) {
      console.error(`取得 ${city} 天氣資料時出錯:`, error);
    }
  });
  
  console.log(`成功蒐集 ${Object.keys(weatherData).length} 個城市的天氣資料`);
  return weatherData;
}

/**
 * 網頁內容擷取器
 * @param {string} url - 目標網址
 * @return {string} 擷取的內容
 */
function scrapeWebContent(url) {
  try {
    const response = UrlFetchApp.fetch(url, {
      method: 'GET',
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; Google Apps Script)'
      },
      followRedirects: true,
      muteHttpExceptions: true
    });
    
    if (response.getResponseCode() !== 200) {
      throw new Error(`HTTP 錯誤:${response.getResponseCode()}`);
    }
    
    const htmlContent = response.getContentText();
    
    // 簡單的 HTML 內容清理(移除標籤)
    const cleanContent = htmlContent
      .replace(/<script\b[<sup><]*(?:(?!<\/script>)<[</sup><]*)*<\/script>/gi, '') // 移除 script
      .replace(/<style\b[<sup><]*(?:(?!<\/style>)<[</sup><]*)*<\/style>/gi, '')   // 移除 style
      .replace(/<<sup>[>]</sup>*>/g, '')                                            // 移除 HTML 標籤
      .replace(/\s+/g, ' ')                                               // 合併空白字元
      .trim();
    
    return cleanContent;
    
  } catch (error) {
    console.error(`擷取網頁內容失敗(${url}):`, error);
    return null;
  }
}

/**
 * RSS Feed 解析器
 * @param {string} rssUrl - RSS Feed URL
 * @return {Array} 解析後的文章陣列
 */
function parseRSSFeed(rssUrl) {
  try {
    const response = UrlFetchApp.fetch(rssUrl);
    const xmlContent = response.getContentText();
    
    // 使用正規表達式解析 RSS(簡化版本)
    const items = [];
    const itemRegex = /<item>(.*?)<\/item>/gs;
    const titleRegex = /<title><!\[CDATA\[(.*?)\]\]><\/title>|<title>(.*?)<\/title>/;
    const linkRegex = /<link>(.*?)<\/link>/;
    const descRegex = /<description><!\[CDATA\[(.*?)\]\]><\/description>|<description>(.*?)<\/description>/;
    const pubDateRegex = /<pubDate>(.*?)<\/pubDate>/;
    
    let match;
    while ((match = itemRegex.exec(xmlContent)) !== null) {
      const itemContent = match[1];
      
      const titleMatch = titleRegex.exec(itemContent);
      const linkMatch = linkRegex.exec(itemContent);
      const descMatch = descRegex.exec(itemContent);
      const dateMatch = pubDateRegex.exec(itemContent);
      
      if (titleMatch && linkMatch) {
        items.push({
          title: titleMatch[1] || titleMatch[2] || '',
          link: linkMatch[1] || '',
          description: descMatch ? (descMatch[1] || descMatch[2] || '') : '',
          pubDate: dateMatch ? new Date(dateMatch[1]) : new Date(),
          source: rssUrl
        });
      }
    }
    
    return items;
    
  } catch (error) {
    console.error(`解析 RSS Feed 失敗(${rssUrl}):`, error);
    return [];
  }
}

/**
 * 儲存資料到 Google Sheets
 * @param {Object} data - 要儲存的資料物件
 */
function saveDataToSheet(data) {
  const spreadsheet = SpreadsheetApp.openById('YOUR_DATA_SPREADSHEET_ID');
  
  // 儲存新聞資料
  if (data.news && data.news.length > 0) {
    const newsSheet = getOrCreateSheet(spreadsheet, '新聞資料');
    data.news.forEach(news => {
      newsSheet.appendRow([
        news.timestamp,
        news.source,
        news.title,
        news.content,
        news.id
      ]);
    });
  }
  
  // 儲存股價資料
  if (data.stocks && data.stocks.length > 0) {
    const stockSheet = getOrCreateSheet(spreadsheet, '股價資料');
    data.stocks.forEach(stock => {
      stockSheet.appendRow([
        stock.timestamp,
        stock.symbol,
        stock.price,
        stock.change,
        stock.changePercent
      ]);
    });
  }
  
  // 儲存天氣資料
  if (data.weather) {
    const weatherSheet = getOrCreateSheet(spreadsheet, '天氣資料');
    Object.keys(data.weather).forEach(city => {
      const weather = data.weather[city];
      weatherSheet.appendRow([
        weather.timestamp,
        city,
        weather.temperature,
        weather.humidity,
        weather.condition
      ]);
    });
  }
  
  console.log('資料已成功儲存到試算表');
}

/**
 * 生成分析報告
 * @param {Object} data - 資料物件
 */
function generateAnalysisReport(data) {
  const reportContent = `
    <div style="font-family: Arial, sans-serif;">
      <h2>每日資料蒐集報告</h2>
      <p><strong>報告時間:</strong>${data.timestamp.toLocaleString()}</p>
      
      <h3>資料統計</h3>
      <ul>
        <li>新聞資料:${data.news ? data.news.length : 0} 筆</li>
        <li>股價資料:${data.stocks ? data.stocks.length : 0} 檔</li>
        <li>天氣資料:${data.weather ? Object.keys(data.weather).length : 0} 個城市</li>
      </ul>
      
      <h3>重點摘要</h3>
      ${generateSummary(data)}
      
      <hr>
      <p style="color: #666; font-size: 12px;">
        此報告由 Google Apps Script 自動生成
      </p>
    </div>
  `;
  
  // 發送報告郵件
  GmailApp.sendEmail(
    'data-team@company.com', // 請替換為實際郵件地址
    `每日資料蒐集報告 - ${Utilities.formatDate(data.timestamp, Session.getScriptTimeZone(), 'yyyy-MM-dd')}`,
    '',
    { htmlBody: reportContent }
  );
}

/**
 * 生成資料摘要
 * @param {Object} data - 資料物件
 * @return {string} HTML 格式的摘要
 */
function generateSummary(data) {
  let summary = '';
  
  // 股價摘要
  if (data.stocks && data.stocks.length > 0) {
    const gainers = data.stocks.filter(stock => parseFloat(stock.change) > 0);
    const losers = data.stocks.filter(stock => parseFloat(stock.change) < 0);
    
    summary += `
      <h4>股市概況</h4>
      <p>上漲:${gainers.length} 檔,下跌:${losers.length} 檔</p>
    `;
  }
  
  // 天氣摘要
  if (data.weather) {
    const temperatures = Object.values(data.weather).map(w => w.temperature);
    const avgTemp = temperatures.reduce((sum, temp) => sum + temp, 0) / temperatures.length;
    
    summary += `
      <h4>天氣概況</h4>
      <p>平均溫度:${avgTemp.toFixed(1)}°C</p>
    `;
  }
  
  return summary;
}

/**
 * 取得或建立工作表
 * @param {Spreadsheet} spreadsheet - 試算表物件
 * @param {string} sheetName - 工作表名稱
 * @return {Sheet} 工作表物件
 */
function getOrCreateSheet(spreadsheet, sheetName) {
  let sheet = spreadsheet.getSheetByName(sheetName);
  if (!sheet) {
    sheet = spreadsheet.insertSheet(sheetName);
    
    // 根據不同類型設定標題列
    let headers = [];
    switch (sheetName) {
      case '新聞資料':
        headers = ['時間戳記', '來源', '標題', '內容', 'ID'];
        break;
      case '股價資料':
        headers = ['時間戳記', '股票代碼', '價格', '變動', '變動%'];
        break;
      case '天氣資料':
        headers = ['時間戳記', '城市', '溫度', '濕度', '天氣狀況'];
        break;
    }
    
    if (headers.length > 0) {
      sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
      sheet.getRange(1, 1, 1, headers.length).setFontWeight('bold');
    }
  }
  
  return sheet;
}

/**
 * 錯誤通知
 * @param {Error} error - 錯誤物件
 */
function sendErrorNotification(error) {
  const errorReport = `
    <div style="font-family: Arial, sans-serif;">
      <h2 style="color: red;">資料蒐集錯誤通知</h2>
      <p><strong>錯誤時間:</strong>${new Date().toLocaleString()}</p>
      <p><strong>錯誤訊息:</strong>${error.message}</p>
      <p><strong>錯誤堆疊:</strong></p>
      <pre style="background: #f5f5f5; padding: 10px;">${error.stack}</pre>
    </div>
  `;
  
  GmailApp.sendEmail(
    'admin@company.com', // 請替換為實際管理者郵件
    '資料蒐集系統錯誤通知',
    '',
    { htmlBody: errorReport }
  );
}

/**
 * 設定資料蒐集觸發器
 */
function setupDataCollectionTriggers() {
  // 每小時執行一次資料蒐集
  ScriptApp.newTrigger('dataCollectionWorkflow')
    .timeBased()
    .everyHours(1)
    .create();
}

範例 6:AI 服務整合(Gemini API)

Google Apps Script 開發場景

這個範例展示如何整合最新的 Gemini AI 服務,實現智能內容生成和分析功能。

/**
 * Google Apps Script 整合 Gemini AI
 * 功能:智能內容生成、文本分析、自動化決策
 */

// Gemini API 設定
const GEMINI_API_KEY = 'YOUR_GEMINI_API_KEY'; // 請到 Google AI Studio 取得 API Key
const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent';

/**
 * 智能郵件回覆系統
 * 使用 Gemini AI 分析郵件內容並生成個人化回覆
 */
function intelligentEmailReplySystem() {
  console.log('開始執行智能郵件回覆系統...');
  
  // 取得需要回覆的郵件
  const threads = GmailApp.search('label:needs-reply is:unread', 0, 10);
  
  threads.forEach(thread => {
    const messages = thread.getMessages();
    const latestMessage = messages[messages.length - 1];
    
    if (latestMessage.isUnread()) {
      try {
        // 分析郵件內容
        const emailAnalysis = analyzeEmailWithGemini(latestMessage);
        
        // 生成回覆
        const reply = generateReplyWithGemini(latestMessage, emailAnalysis);
        
        // 發送回覆(可選擇手動審核)
        if (emailAnalysis.confidence > 0.8) {
          thread.reply(reply.content, {
            htmlBody: reply.htmlContent
          });
          
          // 加上已回覆標籤
          const repliedLabel = GmailApp.getUserLabelByName('AI-已回覆') || 
                              GmailApp.createLabel('AI-已回覆');
          thread.addLabel(repliedLabel);
          
          console.log(`已自動回覆:${latestMessage.getSubject()}`);
        } else {
          // 信心度不足,標記為需要人工審核
          const reviewLabel = GmailApp.getUserLabelByName('需要審核') || 
                             GmailApp.createLabel('需要審核');
          thread.addLabel(reviewLabel);
          
          // 將建議回覆儲存到草稿
          saveSuggestedReplyToDraft(thread, reply);
          
          console.log(`已準備草稿回覆:${latestMessage.getSubject()}`);
        }
        
      } catch (error) {
        console.error(`處理郵件回覆時出錯:`, error);
      }
    }
  });
}

/**
 * 使用 Gemini AI 分析郵件內容
 * @param {GmailMessage} message - Gmail 訊息物件
 * @return {Object} 分析結果
 */
function analyzeEmailWithGemini(message) {
  const prompt = `
    請分析以下郵件內容,並回傳 JSON 格式的分析結果:
    
    寄件者:${message.getFrom()}
    主旨:${message.getSubject()}
    內容:${message.getPlainBody()}
    
    請分析:
    1. 郵件類型(inquiry、complaint、praise、request、other)
    2. 緊急程度(1-5,5為最緊急)
    3. 情感極性(positive、neutral、negative)
    4. 主要議題
    5. 建議的回覆語調(formal、friendly、apologetic、enthusiastic)
    6. AI回覆信心度(0-1,1為最有信心)
    
    請回傳格式如下的 JSON:
    {
      "type": "inquiry",
      "urgency": 3,
      "sentiment": "neutral",
      "topics": ["產品詢問", "價格"],
      "tone": "friendly",
      "confidence": 0.85
    }
  `;
  
  try {
    const response = callGeminiAPI(prompt);
    const analysisText = response.candidates[0].content.parts[0].text;
    
    // 提取 JSON 內容
    const jsonMatch = analysisText.match(/\{[\s\S]*\}/);
    if (jsonMatch) {
      return JSON.parse(jsonMatch[0]);
    } else {
      throw new Error('無法解析 AI 回應');
    }
    
  } catch (error) {
    console.error('郵件分析失敗:', error);
    return {
      type: 'other',
      urgency: 3,
      sentiment: 'neutral',
      topics: [],
      tone: 'friendly',
      confidence: 0.5
    };
  }
}

/**
 * 使用 Gemini AI 生成回覆內容
 * @param {GmailMessage} message - 原始郵件
 * @param {Object} analysis - 分析結果
 * @return {Object} 回覆內容
 */
function generateReplyWithGemini(message, analysis) {
  const prompt = `
    基於以下郵件和分析結果,生成一個專業的回覆:
    
    原始郵件:
    寄件者:${message.getFrom()}
    主旨:${message.getSubject()}
    內容:${message.getPlainBody()}
    
    分析結果:
    類型:${analysis.type}
    情感:${analysis.sentiment}
    語調:${analysis.tone}
    主要議題:${analysis.topics.join(', ')}
    
    請生成一個${analysis.tone}語調的回覆,內容應該:
    1. 回應所有提到的議題
    2. 保持專業但友善的態度
    3. 如果是投訴,要表達歉意並提供解決方案
    4. 如果是詢問,要提供有用的資訊
    5. 包含適當的結尾和簽名
    
    請同時提供純文字版本和 HTML 版本。
  `;
  
  try {
    const response = callGeminiAPI(prompt);
    const replyText = response.candidates[0].content.parts[0].text;
    
    // 簡單的 HTML 格式化
    const htmlContent = replyText
      .replace(/\n/g, '<br>')
      .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
    
    return {
      content: replyText,
      htmlContent: `<div style="font-family: Arial, sans-serif;">${htmlContent}</div>`
    };
    
  } catch (error) {
    console.error('生成回覆失敗:', error);
    return {
      content: '感謝您的來信,我們會儘快回覆您。',
      htmlContent: '<div style="font-family: Arial, sans-serif;">感謝您的來信,我們會儘快回覆您。</div>'
    };
  }
}

/**
 * 智能內容摘要工具
 * 自動摘要長篇文章或報告
 */
function intelligentContentSummarizer() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = spreadsheet.getSheetByName('待摘要內容');
  
  if (!sheet) {
    console.log('找不到"待摘要內容"工作表');
    return;
  }
  
  const data = sheet.getDataRange().getValues();
  const headers = data[0];
  const rows = data.slice(1);
  
  rows.forEach((row, index) => {
    const [title, content, summary, status] = row;
    
    // 跳過已處理的內容
    if (status === '已摘要' || !content) return;
    
    try {
      console.log(`正在摘要:${title}`);
      
      const prompt = `
        請為以下內容生成一個簡潔的摘要(150-200字):
        
        標題:${title}
        內容:${content}
        
        摘要應該:
        1. 包含主要重點
        2. 保持客觀中立
        3. 使用繁體中文
        4. 結構清晰
      `;
      
      const response = callGeminiAPI(prompt);
      const summary = response.candidates[0].content.parts[0].text;
      
      // 更新摘要到試算表
      sheet.getRange(index + 2, 3).setValue(summary);
      sheet.getRange(index + 2, 4).setValue('已摘要');
      
      console.log(`完成摘要:${title}`);
      
      // 避免 API 限制
      Utilities.sleep(1000);
      
    } catch (error) {
      console.error(`摘要"${title}"時出錯:`, error);
      sheet.getRange(index + 2, 4).setValue('摘要失敗');
    }
  });
}

/**
 * 智能數據分析助手
 * 分析試算表數據並生成洞察報告
 */
function intelligentDataAnalyst() {
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const dataSheet = spreadsheet.getSheetByName('分析數據');
  
  if (!dataSheet) {
    console.log('找不到"分析數據"工作表');
    return;
  }
  
  // 取得數據
  const data = dataSheet.getDataRange().getValues();
  const headers = data[0];
  const rows = data.slice(1);
  
  // 準備數據摘要
  const dataStats = generateDataStatistics(data);
  
  const prompt = `
    請分析以下數據並生成洞察報告:
    
    數據欄位:${headers.join(', ')}
    數據筆數:${rows.length}
    
    統計摘要:
    ${JSON.stringify(dataStats, null, 2)}
    
    請提供:
    1. 數據概覽
    2. 主要趨勢和模式
    3. 異常值或有趣的發現  
    4. 商業洞察和建議
    5. 後續分析建議
    
    報告應該專業但易懂,適合商業決策參考。
  `;
  
  try {
    const response = callGeminiAPI(prompt);
    const analysisReport = response.candidates[0].content.parts[0].text;
    
    // 建立分析報告工作表
    const reportSheet = getOrCreateSheet(spreadsheet, 'AI分析報告');
    reportSheet.clear();
    
    // 寫入報告
    reportSheet.getRange('A1').setValue('AI 數據分析報告');
    reportSheet.getRange('A1').setFontSize(16).setFontWeight('bold');
    reportSheet.getRange('A3').setValue(analysisReport);
    reportSheet.getRange('A3').setWrap(true);
    
    // 美化格式
    reportSheet.autoResizeColumns(1, 1);
    reportSheet.setColumnWidth(1, 800);
    
    console.log('AI 數據分析報告已生成');
    
  } catch (error) {
    console.error('生成數據分析報告時出錯:', error);
  }
}

/**
 * 生成數據統計摘要
 * @param {Array} data - 數據陣列
 * @return {Object} 統計摘要
 */
function generateDataStatistics(data) {
  const headers = data[0];
  const rows = data.slice(1);
  const stats = {};
  
  headers.forEach((header, colIndex) => {
    const values = rows.map(row => row[colIndex]).filter(val => val !== '');
    
    if (values.length === 0) {
      stats[header] = { type: 'empty' };
      return;
    }
    
    // 檢查是否為數值欄位
    const numericValues = values.map(val => parseFloat(val)).filter(val => !isNaN(val));
    
    if (numericValues.length > values.length * 0.8) {
      // 數值欄位統計
      stats[header] = {
        type: 'numeric',
        count: numericValues.length,
        min: Math.min(...numericValues),
        max: Math.max(...numericValues),
        average: numericValues.reduce((sum, val) => sum + val, 0) / numericValues.length,
        median: getMedian(numericValues)
      };
    } else {
      // 文字欄位統計
      const uniqueValues = [...new Set(values)];
      const valueCounts = {};
      values.forEach(val => {
        valueCounts[val] = (valueCounts[val] || 0) + 1;
      });
      
      stats[header] = {
        type: 'text',
        count: values.length,
        unique: uniqueValues.length,
        mostCommon: Object.keys(valueCounts).reduce((a, b) => 
          valueCounts[a] > valueCounts[b] ? a : b
        )
      };
    }
  });
  
  return stats;
}

/**
 * 計算中位數
 * @param {Array} numbers - 數值陣列
 * @return {number} 中位數
 */
function getMedian(numbers) {
  const sorted = numbers.sort((a, b) => a - b);
  const middle = Math.floor(sorted.length / 2);
  
  if (sorted.length % 2 === 0) {
    return (sorted[middle - 1] + sorted[middle]) / 2;
  } else {
    return sorted[middle];
  }
}

/**
 * 呼叫 Gemini API
 * @param {string} prompt - 提示內容
 * @return {Object} API 回應
 */
function callGeminiAPI(prompt) {
  const payload = {
    contents: [{
      parts: [{
        text: prompt
      }]
    }],
    generationConfig: {
      temperature: 0.7,
      topK: 40,
      topP: 0.95,
      maxOutputTokens: 2048,
    }
  };
  
  const options = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };
  
  const response = UrlFetchApp.fetch(`${GEMINI_API_URL}?key=${GEMINI_API_KEY}`, options);
  const responseCode = response.getResponseCode();
  
  if (responseCode !== 200) {
    throw new Error(`Gemini API 錯誤:${responseCode} - ${response.getContentText()}`);
  }
  
  return JSON.parse(response.getContentText());
}

/**
 * 將建議回覆儲存為草稿
 * @param {GmailThread} thread - Gmail 對話串
 * @param {Object} reply - 回覆內容
 */
function saveSuggestedReplyToDraft(thread, reply) {
  const draft = thread.createDraftReply(reply.content, {
    htmlBody: reply.htmlContent
  });
  
  console.log(`已建立草稿回覆:${thread.getFirstMessageSubject()}`);
}

/**
 * 取得或建立工作表
 * @param {Spreadsheet} spreadsheet - 試算表物件
 * @param {string} sheetName - 工作表名稱
 * @return {Sheet} 工作表物件
 */
function getOrCreateSheet(spreadsheet, sheetName) {
  let sheet = spreadsheet.getSheetByName(sheetName);
  if (!sheet) {
    sheet = spreadsheet.insertSheet(sheetName);
  }
  return sheet;
}

/**
 * 設定 AI 功能觸發器
 */
function setupAITriggers() {
  // 每 2 小時檢查需要回覆的郵件
  ScriptApp.newTrigger('intelligentEmailReplySystem')
    .timeBased()
    .everyHours(2)
    .create();
  
  // 每天凌晨 2 點執行內容摘要
  ScriptApp.newTrigger('intelligentContentSummarizer')
    .timeBased()
    .everyDays(1)
    .atHour(2)
    .create();
}

性能優化技巧

在開發 Google Apps Script 應用時,性能優化是非常重要的考量。以下是一些實用的優化技巧:

1. 批量操作優化

/**
 * 錯誤示範:逐一操作(效率低下)
 */
function inefficientMethod() {
  const sheet = SpreadsheetApp.getActiveSheet();
  
  // 每次操作都會觸發一次 API 呼叫
  for (let i = 1; i <= 100; i++) {
    sheet.getRange(i, 1).setValue(`資料 ${i}`);
    const value = sheet.getRange(i, 2).getValue();
    sheet.getRange(i, 3).setValue(value * 2);
  }
}

/**
 * 正確示範:批量操作(高效率)
 */
function efficientMethod() {
  const sheet = SpreadsheetApp.getActiveSheet();
  
  // 一次取得所有需要的資料
  const range = sheet.getRange(1, 1, 100, 3);
  const values = range.getValues();
  
  // 在記憶體中處理資料
  for (let i = 0; i < values.length; i++) {
    values[i][0] = `資料 ${i + 1}`;
    values[i][2] = values[i][1] * 2;
  }
  
  // 一次寫入所有資料
  range.setValues(values);
}

2. 快取機制

/**
 * 使用快取減少重複計算和 API 呼叫
 */
function useCache() {
  const cache = CacheService.getScriptCache();
  const cacheKey = 'expensive_calculation_result';
  
  // 嘗試從快取取得結果
  let result = cache.get(cacheKey);
  
  if (result === null) {
    // 快取中沒有資料,執行計算
    console.log('從快取中未找到資料,執行計算...');
    result = performExpensiveCalculation();
    
    // 將結果儲存到快取(有效時間 10 分鐘)
    cache.put(cacheKey, result, 600);
  } else {
    console.log('從快取中取得資料');
  }
  
  return result;
}

function performExpensiveCalculation() {
  // 模擬耗時的計算
  Utilities.sleep(2000);
  return Math.random().toString();
}

3. 錯誤處理與重試機制

/**
 * 強化的錯誤處理和重試機制
 */
function robustOperation() {
  const maxRetries = 3;
  let retryCount = 0;
  
  while (retryCount < maxRetries) {
    try {
      // 執行可能失敗的操作
      const result = riskyOperation();
      return result;
      
    } catch (error) {
      retryCount++;
      console.error(`操作失敗(第 ${retryCount} 次嘗試):`, error.message);
      
      if (retryCount >= maxRetries) {
        // 達到最大重試次數,記錄錯誤並處理
        console.error('操作最終失敗,已達到最大重試次數');
        handleFinalError(error);
        throw error;
      }
      
      // 等待後重試(指數退避)
      const waitTime = Math.pow(2, retryCount) * 1000;
      console.log(`等待 ${waitTime}ms 後重試...`);
      Utilities.sleep(waitTime);
    }
  }
}

function riskyOperation() {
  // 模擬可能失敗的操作
  if (Math.random() < 0.3) {
    throw new Error('隨機失敗');
  }
  return '成功';
}

function handleFinalError(error) {
  // 發送錯誤通知
  GmailApp.sendEmail(
    'admin@company.com',
    '系統錯誤通知',
    `操作失敗:${error.message}\n時間:${new Date()}`
  );
}

4. 執行時間管理

/**
 * 管理執行時間,避免觸發 6 分鐘限制
 */
function longRunningTask() {
  const startTime = new Date().getTime();
  const maxRunTime = 5 * 60 * 1000; // 5 分鐘限制
  
  const tasks = generateTasks(); // 假設有很多任務要執行
  const processingQueue = PropertiesService.getScriptProperties();
  
  let currentIndex = parseInt(processingQueue.getProperty('currentIndex') || '0');
  
  while (currentIndex < tasks.length) {
    // 檢查執行時間
    if (new Date().getTime() - startTime > maxRunTime) {
      console.log('接近執行時間限制,儲存進度並安排下次執行');
      processingQueue.setProperty('currentIndex', currentIndex.toString());
      
      // 安排觸發器繼續執行
      ScriptApp.newTrigger('longRunningTask')
        .timeBased()
        .after(60000) // 1 分鐘後繼續
        .create();
      
      return;
    }
    
    // 處理目前任務
    processTask(tasks[currentIndex]);
    currentIndex++;
  }
  
  // 所有任務完成,清除進度
  processingQueue.deleteProperty('currentIndex');
  console.log('所有任務完成');
}

function generateTasks() {
  return Array.from({ length: 1000 }, (_, i) => `任務 ${i + 1}`);
}

function processTask(task) {
  console.log(`處理:${task}`);
  Utilities.sleep(100); // 模擬處理時間
}

與其他自動化工具比較

Google Apps Script vs. 其他自動化平台

特性 Google Apps Script Zapier n8n Power Automate
成本 免費(有限制) 付費(免費版功能少) 開源免費 付費
Google 整合 原生深度整合 良好 良好 一般
程式彈性 高(JavaScript) 低(GUI操作) 高(視覺化+程式碼) 中(GUI操作)
學習曲線 中等 簡單 中等 簡單
自訂能力 非常高
執行限制 6分鐘/日配額 依方案 無限制 依方案

選擇建議

選擇 Google Apps Script 當:

  • 主要使用 Google Workspace 服務
  • 需要深度自訂功能
  • 預算有限
  • 團隊有程式開發能力

選擇其他工具當:

  • 需要整合大量第三方服務
  • 團隊缺乏程式開發經驗
  • 需要複雜的工作流程視覺化
  • 有充足的軟體預算

2024-2025 最新趨勢

1. Gemini AI 整合

Google 正在將 Gemini AI 深度整合到各種服務中,包括:

  • Gemini CLI:命令列工具,可與 Apps Script 配合使用
  • AI-powered 自動化:智能郵件回覆、內容生成
  • 多模態處理:處理文字、圖片、影片的綜合自動化
// 2025 年趨勢:智能工作流程
function futureWorkflow() {
  // 1. AI 驅動的郵件分類
  classifyEmailsWithAI();
  
  // 2. 自動生成報告
  generateReportsWithAI();
  
  // 3. 智能資料洞察
  getDataInsightsWithAI();
  
  // 4. 多語言自動翻譯
  translateContentWithAI();
}

2. 低程式碼/無程式碼趨勢

  • Google Workspace 外掛程式更加視覺化
  • Apps Script 編輯器增強的自動完成和建議功能
  • 模板市場提供更多預製解決方案

3. 安全性增強

// 新的安全性最佳實踐(2025)
function secureScriptPractices() {
  // 1. 使用 PropertiesService 儲存敏感資料
  const apiKey = PropertiesService.getScriptProperties().getProperty('API_KEY');
  
  // 2. 實作權限檢查
  if (!hasPermission(Session.getActiveUser().getEmail())) {
    throw new Error('權限不足');
  }
  
  // 3. 資料加密處理
  const encryptedData = encryptSensitiveData(data);
  
  // 4. 審計日誌
  logActivity('data_access', Session.getActiveUser().getEmail());
}

4. 雲端整合深化

  • BigQuery 整合:直接查詢大數據
  • Cloud Functions 互動:更強大的後端處理
  • Firebase 整合:即時資料庫和認證

實用資源與學習路徑

官方資源

  1. Google Apps Script 官方文件

  2. Google AI Studio

  3. Google Workspace 開發者中心

學習路徑建議

初學者(0-3個月)

  1. JavaScript 基礎語法
  2. Google Apps Script 環境熟悉
  3. 基本的 Sheets 和 Gmail 操作
  4. 簡單的自動化腳本

中級(3-6個月)

  1. 進階 API 使用(Calendar、Drive、Forms)
  2. 錯誤處理和除錯技巧
  3. 觸發器和工作流程設計
  4. 第三方 API 整合

高級(6個月以上)

  1. 性能優化技巧
  2. 大型專案架構設計
  3. AI 服務整合
  4. 企業級解決方案開發

社群資源

  1. Stack Overflow

    • 標籤:google-apps-script
    • 豐富的問題解答
  2. Reddit 社群

    • r/GoogleAppsScript
    • 經驗分享和討論
  3. GitHub 專案

    • 開源的 GAS 專案和程式庫
    • 學習最佳實踐

推薦工具

  1. clasp:命令列工具,本地開發 GAS
  2. Apps Script Dashboard:專案管理和監控
  3. Google Cloud Console:進階功能和配置

總結與未來展望

Google Apps Script 作為一個強大的雲端自動化平台,正在經歷快速的發展和進化。從基本的工作表操作到複雜的 AI 整合,GAS 為個人和企業提供了無限的可能性。

核心價值

  1. 無縫整合:與 Google Workspace 的深度整合無人能及
  2. 成本效益:免費使用額度對中小型應用足夠
  3. 靈活性:JavaScript 基礎提供無限自訂可能
  4. 易於部署:雲端執行,無需伺服器管理

未來發展方向

  1. AI 驅動自動化:Gemini 等 AI 服務的深度整合
  2. 低程式碼發展:更多視覺化開發工具
  3. 企業級功能:增強的安全性和管理功能
  4. 生態系統擴展:更多第三方整合和模板

建議行動

對於想要提升工作效率的人:

  • 從小型自動化專案開始
  • 逐步學習 JavaScript 和 GAS API
  • 關注最新功能和最佳實踐
  • 加入開發者社群交流經驗

對於企業組織:

  • 評估現有工作流程的自動化潛力
  • 投資員工的 GAS 技能培訓
  • 建立內部知識庫和最佳實踐
  • 考慮與專業開發者合作

最後的話

Google Apps Script 不僅僅是一個工具,它是連接想法與實現之間的橋樑。無論你是想要節省日常工作時間的上班族,還是要建立複雜自動化系統的開發者,GAS 都能提供你所需的能力。

隨著 AI 技術的發展,我們正站在自動化新時代的門檻上。現在學習和掌握 Google Apps Script,就是為未來的智能工作方式做準備。

記住:最好的自動化不是替代人類的判斷,而是釋放人類的創造力。讓 Google Apps Script 處理重複性工作,你則專注於更有價值的創新和策略思考。


相關標籤: #GoogleAppsScript #GAS #自動化 #GoogleWorkspace #JavaScript #工作流程 #生產力工具 #程式開發

更新日期: 2025年1月

作者簡介: 專業的技術內容創作者,專注於雲端自動化和工作效率提升解決方案。


本文最初發布於 HackMD @BASHCAT

留言

這個網誌中的熱門文章

Arduino 課本可能沒教的事(1)

SI4432 搭配Arduino

燒錄 Arduino mini Pro 燒錄