AI

啟動你的 AI 工作流

請輸入「部門-名字」與通關密語進入課程指引。
提示:集團的英文名,不分大小寫。

GAMANIA COURSE GUIDE

啟動你的 AI 工作流

溝通力 × 創作力 × 效率力。這份指引整理本堂課的實作任務與可直接複製的提示詞:問不到重點練溝通、想法做不出練創作、重複一直做練效率。點上方分頁開始,每個任務做完直接送出答案。

本頁僅供 Gamania 橘子集團本課程學員課堂與課後複習使用。課堂練習請使用示範資料;未公開的營運數據、玩家個資請勿貼入 AI 工具。
★
Handbook

課程講義與重點提醒

這堂課不是逐一展示 AI 功能,而是建立正確的使用直覺:AI 怎麼運作、哪裡會出錯、怎麼問才問得好,以及怎麼把重複的事封裝起來。講義為課堂重點摘要,以課堂簡報為準。

01|思維升級:駕馭你的 AI

先分清楚:你要的是「事實」還是「靈感」

問 AI「9.11 和 9.9 誰比較大」它可能答錯,請它想五句維護公告開頭它句句有梗——同一個 AI,兩種用法。要事實,就要查證;要靈感,就大膽要。搞混這兩件事,是多數人踩坑的起點。

AI 的本質是文字接龍

AI 不是計算機,也不是搜尋引擎。它根據上下文一個字一個字推測「接下來最合理的內容」。理解這件事,你就同時理解了它的強項與弱點:很會整理、改寫、延伸;但也可能一本正經地說錯話——這就是幻覺,不是故障,是天性。

Context、Markdown 與語音輸入

  • Context(上下文):AI 回答前「看得到的所有資訊」。把背景說清楚不是禮貌,是操作技術。
  • Markdown(.md):用簡單符號標記結構的純文字格式。AI 讀它特別順——整理過的純文字,常常比華麗的檔案好用。
  • 語音輸入(如 Typeless):用說的比用打的快。把需求、背景、細節一口氣講清楚,AI 拿到的上下文反而更完整。

AI 素養四大原則

  • 容錯:AI 產出永遠是「草稿」,對外發出前必須有人負責確認。
  • 隱私個資注意:玩家個資、未公開營運數據、內部機敏文件不進 AI;練習一律用示範資料。
  • 倫理規範遵守:依公司規範使用 AI 工具;不確定能不能用時,先問,不要先傳。
  • 持續學習:AI 工具每幾個月就有大變化。今天學的是「判斷方法」,不是「某個按鈕的位置」。
02|AI 技巧:怎麼用得更好

三種交叉驗證

  • 換工具:重要的答案,丟給另一個 AI 再問一次。
  • 換問法:同一件事換個角度問,看答案是否一致。
  • 回原始來源:數據、法規、出處,最後一定回到原始資料確認。

檢索來源:不是丟什麼 AI 都讀得懂

掃描版 PDF、圖片裡的文字、格式複雜的表格,AI 可能讀不全或讀錯。丟資料前先想一下:它拿到的是「文字」還是「圖」?整理成純文字或 Markdown 最穩。

獨立對話原則

一個對話累積的上下文會互相干擾。換任務就開新對話,讓 AI 專心——這也是今天每個任務都請你「開新的對話視窗」的原因。

03|三個力:溝通力 × 創作力 × 效率力

溝通力:讓 AI 搞懂你想問什麼

  • 反問法:沒有靈感或需求模糊時,用蘇格拉底提問法讓 AI 先反問你,透過對話收斂出精準需求。
  • RODES 法則:Role(角色)→ Objective(目標)→ Details(細節)→ Example(範例)→ Sense Check(邏輯檢查)。
  • 固化常用提問:好用的問法別每次重打——從單次問答,到存成 Gem,再到平臺化的 Skill,一路把「老樣子」封裝起來。

創作力:讓想法做得出來

  • Gemini 生成 Google Slides:提案內容直接變簡報,前一關的答案餵下一關。
  • 圖片生成:用逆向工程分析喜歡的圖的風格再生成新圖,或直接參考別人的提示詞範本庫。

效率力:把重複的事自動化

  • Google 文件與試算表:Gemini 側邊欄整理、改寫、清資料、寫公式。
  • Google Apps Script:報表格式轉換這種固定流程,交給腳本自動跑。
  • NotebookLM:把多份文件變成可追問的知識庫,答案附引用來源。
  • AI 協作四個層次:聊天 → 模板化 → 自動化 → Agent。今天的目標是把你從「聊天」推進到「模板化」。
04|回顧總結:帶走四句話
  • AI 不是萬能,但比你想的能幹——事實要查證,創意可大膽。
  • 你怎麼問,AI 就怎麼答——RODES 是起點,輸入品質是關鍵。
  • 重複的事就封裝起來——好的提示詞值得做成 Gems/GPTs,別每次從零開始。
  • 從今天一件小事開始——不需要一次改變,嚐到甜頭就會停不下來。
1
Communication

溝通力:讓 AI 聽懂你

先親身體驗 AI 的兩種天性——會一本正經地答錯、也會給你意想不到的靈感;再用 RODES 法則,把模糊的需求變成 AI 一次到位的精準提問。

任務一

事實測試:9.11 和 9.9 誰比較大?

1

開啟你常用的 AI(Gemini、ChatGPT 都可以),開一個新對話。

2

複製下方問題貼上,看看 AI 怎麼回答。

3

回到這裡,按下你的 AI 給出的答案。

9.11 和 9.9 誰比較大?
任務二

靈感測試:維護公告的開頭句

1

開一個新對話,複製下方提示詞貼上。

2

從 AI 給你的 5 句裡,挑一句你最喜歡的。

3

貼到下方輸入框送出——每個人的答案都不一樣,這就是靈感模式。

幫我想 5 句讓玩家心甘情願看完系統維護公告的開頭句,要有趣,不要官腔。
任務三

RODES 部門實戰:幫自己部門做一件事

想一個你部門本週真實會遇到的任務,照下面四步,把「隨口一句的需求」升級成「AI 一次到位的提問」。

沒靈感?參考你的部門:客服 → 一封得體的客訴回覆信|行銷 → 一段廣告提案話術|營運 → 一篇活動預告文案|行政 → 一封跨部門協調信。
1

先用大白話寫下你的需求——寫得遜沒關係,這就是原料(例:「幫我回一封客訴信」)。

2

開啟下方「RODES 提示詞優化器」,把你的遜版需求貼進去,拿到優化後的提示詞。

3

開一個新的對話視窗,貼上優化後的提示詞執行;不滿意就繼續調整。

4

把「優化後的提示詞」和「滿意的產出」貼回下方送出。

2
Creativity

創作力:讓想法做得出來

剛剛老師用 RODES 拿到的電玩展提案架構,現在輪到你——把它變成一份拿得出手的簡報。

任務一

把電玩展提案做成 Google 簡報

情境:你是橘子集團的員工,《新楓之谷》要參加明年的台北國際電玩展,提案方向已經確定——預算 300 萬內、主打年底新改版、帶到經典版吸引回鍋老玩家。現在要做出對內部主管提案的簡報。

1

開一個新的對話視窗(Gemini)。

2

複製下方提示詞貼上,等 Gemini 產出簡報內容並做成 Google 簡報。

3

開啟簡報,點右上角「共用」→ 改成「知道連結的使用者皆可檢視」→ 複製連結。

4

把簡報連結貼回下方送出。

請扮演資深遊戲展場策展人。我是橘子集團的員工,《新楓之谷》要參加明年的台北國際電玩展,我需要對內部主管提案攤位規劃。

提案條件:
- 預算上限 300 萬元
- 主打年底新改版內容,並帶到《新楓之谷:經典版》吸引回鍋的老玩家

請幫我製作一份 6 頁的提案簡報,頁面依序為:封面、展場核心主題與提案摘要、空間動線規劃、舞台與玩家互動活動(含經典版回鍋玩家專區)、預算分配、預期效益(KPI)。每頁有標題與 3 點以內的重點。若資訊不足,請以合理假設完成,並把假設清單放在最後一頁下方補充說明,不需反問我。

完成後,請直接把這份內容做成 Google 簡報。
貼連結前記得先改共用權限(知道連結的使用者皆可檢視),不然講師打不開你的作品。
來不及做完?直接用講師版簡報範本繼續往下走,之後再回頭補做。
任務二

修改簡報首頁圖片:把圖改簡單一點

1

開啟你的簡報(自己做的或講師範本副本都可以),到首頁。

2

點開右上角 Gemini 側邊欄,請它把首頁圖片改成更簡潔的風格,換上滿意的那張。

3

完成後回來登記完成。

任務三

增加一頁新投影片:O2O 數位互動與數據成效追蹤

1

在簡報的 Gemini 側邊欄,複製下方提示詞貼上。

2

確認新頁面有加進來、風格跟前面一致。

3

完成後回來登記完成。

請幫我在簡報中新增第八頁,主題為「O2O 數位互動與數據成效追蹤」。請列出 3 點重點,說明如何利用手機互動網頁設計虛擬獎勵與排行榜機制來串聯攤位動線、如何精準收集回鍋玩家與新玩家的數據名單,以及展後如何運用這些資料進行媒體廣告再行銷。請維持與前述簡報一致的提案風格與排版。
任務四

逆向工程生圖:分析風格,把自己變成 3D 角色

1

下載下方「大師姐」的 IP 圖片。

2

開一個新的對話視窗,上傳範例圖,貼上提示詞 ①,讓 Gemini 分析出風格描述。

3

接著上傳自己的照片(或任何一張人像照),貼上提示詞 ②,依剛剛的風格生成新圖。

4

把生成的圖片下載下來,回到這裡上傳。

提示詞 ①(分析風格):

請分析這張圖片的視覺風格,包括:材質與質感、光線、色調配色、人物頭身比例與五官特徵、整體氛圍。整理成一段之後可以直接用來生成同風格圖片的「風格描述」。

提示詞 ②(上傳人像照後生成):

這是一張人像照片,請依照剛剛整理的風格描述,把照片中的人物重新生成為同樣風格的 3D 角色圖,保留人物的髮型、服裝特色與姿勢。
3
Efficiency

效率力:把重複的事自動化

兩條路線:先在 Google 文件裡讓 Gemini 幫你讀寫改,再用 Gemini Notebook 把多份文件變成可追問的知識庫。

任務一

Google 文件:大師姐新人手冊實戰

1

點「使用範本」,把文件建立到自己的雲端硬碟。

2

打開右側 Gemini 側邊欄,輸入下方提示詞,把第三條「部門與職掌」的表格變成條列,然後點「插入」取代,即完成。

請把文件中第三條「部門與職掌」的表格改寫成條列式清單:每個部門一個標題,底下條列該部門的職掌重點,方便新人快速閱讀。
任務二

Google 試算表:電玩展報名資料整理

故事收尾:電玩展提案過了、簡報做了,活動預約開跑——50 筆玩家報名資料進來了(教學用模擬資料)。資料裡刻意藏了重複報名、Email 缺漏、日期格式不一、活動寫法亂七八糟的問題,交給 Gemini 幫你收拾。
1

開啟範本(或下載 CSV 上傳到自己的 Google 試算表)。

2

點開右上角 Gemini 側邊欄,依序試下面三張卡。

3

做完回來登記完成。

任務 01找問題

檢查資料品質

請檢查這張報名表的資料品質問題:重複報名(同一 Email 報了兩次以上)、Email 缺漏或格式錯誤、日期格式不一致,條列告訴我哪幾列需要處理。
任務 02標準化

活動分類與玩家類型

請新增一欄「活動分類」,把「想參加的活動」歸類成:舞台活動/新作試玩/經典版懷舊區/未明確;再新增一欄「玩家類型」,依「是否玩過橘子的遊戲」分成老玩家/新玩家。最後統計各活動分類的人數與老玩家比例。
任務 03摘要

給主管的報名摘要

請用 150 字寫一段給主管的報名狀況摘要:目前有效報名人數(扣掉重複)、最熱門的活動、資料需要補齊的地方,以及建議下一步。
GAS 萬能產線:打造資料清洗到匯報的自動化工廠

一鍵清洗髒資料、自建多平台 CSV 上傳中心、再到自動寄信匯報,把每天重複的報表苦工變成一條自動化產線。

任務一

資料整理:體驗一鍵做好資料清洗,逐一選取 B 欄位,於自訂選單選取體驗格式轉換

延伸挑戰:修改模板,以既有腳本參考,將【拆分多個電子郵件】轉換為直向呈現

任務二 之一

多活動報表彙整系統

1
分析三份活動報表的格式:先下載素材包裡的三份 CSV(Google 表單報名資料、Accupass 報名資料、廠商自有系統報名資料),丟給 AI,請 AI 分析每份報表各有哪些欄位。這樣 AI 才能理解不同來源的資料結構——例如 Google 表單有「回覆編號、姓名、服務單位、電子郵件地址」,Accupass 有「票券編號、參加人姓名、公司名稱、參加人Email」,廠商系統有「廠商流水號、聯絡人、單位名稱、聯絡信箱」。
2
匯整至目標範本:打開上方的「使用範本」建立一份副本,告訴 AI 這就是目標格式,共有五個統一欄位:來源平台、報名編號、姓名、單位、Email。請 AI 根據剛才分析出的三份報表結構,設計一個 Google Apps Script 腳本,把不同格式的資料自動對應到這五個欄位。
3
設定自訂選單與上傳視窗:提醒 AI 在腳本中加入自訂選單——在試算表上方新增一個「資料匯整工具」選單,點選後會彈出上傳視窗,可以直接把三份 CSV 檔案選取上傳,系統就會自動辨識來源、對應欄位、匯入範本,不用手動複製貼上。
const DATA_START_ROW = 2;
const TARGET_COLUMNS = 5;

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('資料匯整工具')
    .addItem('上傳 CSV', 'showUploadDialog')
    .addItem('清空資料', 'clearImportedData')
    .addToUi();
}

function showUploadDialog() {
  const html = HtmlService.createHtmlOutputFromFile('UploadDialog')
    .setWidth(520)
    .setHeight(360);

  SpreadsheetApp.getUi().showModalDialog(html, '上傳 CSV 檔');
}

function clearImportedData() {
  clearImportedData_();
  SpreadsheetApp.getUi().alert('已清空資料。');
}

function importCsvFiles(files) {
  if (!files || files.length === 0) {
    throw new Error('請至少上傳一份 CSV 檔。');
  }

  const sheet = getTargetSheet_();
  clearImportedData_();

  const allRows = [];

  files.forEach(function (file) {
    const fileName = file.name || '';
    const csvText = file.content || '';
    const sourcePlatform = detectSourcePlatform_(fileName, csvText);
    const rows = parseCsv_(csvText);

    if (rows.length < 2) return;

    const headers = rows[0].map(normalizeHeader_);
    const dataRows = rows.slice(1);

    dataRows.forEach(function (row) {
      if (isEmptyRow_(row)) return;
      allRows.push(mapRowToTarget_(sourcePlatform, headers, row));
    });
  });

  if (allRows.length === 0) {
    throw new Error('沒有讀到可匯入的資料,請確認 CSV 內容。');
  }

  sheet.getRange(DATA_START_ROW, 1, allRows.length, TARGET_COLUMNS).setValues(allRows);

  return `匯入完成,共 ${allRows.length} 筆資料。`;
}

function getTargetSheet_() {
  return SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
}

function clearImportedData_() {
  const sheet = getTargetSheet_();
  const lastRow = sheet.getLastRow();

  if (lastRow < DATA_START_ROW) return;

  sheet
    .getRange(DATA_START_ROW, 1, lastRow - DATA_START_ROW + 1, TARGET_COLUMNS)
    .clearContent();
}

function detectSourcePlatform_(fileName, csvText) {
  const lowerName = fileName.toLowerCase();
  const firstLine = csvText.split(/\r?\n/)[0] || '';

  if (
    lowerName.includes('google') ||
    lowerName.includes('form') ||
    firstLine.includes('電子郵件地址')
  ) {
    return 'Google Form';
  }

  if (
    lowerName.includes('accupass') ||
    firstLine.includes('票券編號')
  ) {
    return 'Accupass';
  }

  if (
    lowerName.includes('vendor') ||
    lowerName.includes('廠商') ||
    lowerName.includes('system') ||
    firstLine.includes('廠商流水號')
  ) {
    return '廠商自有系統';
  }

  return '未知來源';
}

function mapRowToTarget_(sourcePlatform, headers, row) {
  if (sourcePlatform === 'Google Form') {
    return [
      sourcePlatform,
      getCellByHeader_(headers, row, '回覆編號'),
      getCellByHeader_(headers, row, '姓名'),
      getCellByHeader_(headers, row, '服務單位'),
      getCellByHeader_(headers, row, '電子郵件地址')
    ];
  }

  if (sourcePlatform === 'Accupass') {
    return [
      sourcePlatform,
      getCellByHeader_(headers, row, '票券編號'),
      getCellByHeader_(headers, row, '參加人姓名'),
      getCellByHeader_(headers, row, '公司名稱'),
      getCellByHeader_(headers, row, '參加人Email')
    ];
  }

  if (sourcePlatform === '廠商自有系統') {
    return [
      sourcePlatform,
      getCellByHeader_(headers, row, '廠商流水號'),
      getCellByHeader_(headers, row, '聯絡人'),
      getCellByHeader_(headers, row, '單位名稱'),
      getCellByHeader_(headers, row, '聯絡信箱')
    ];
  }

  return [sourcePlatform, '', '', '', ''];
}

function getCellByHeader_(headers, row, headerName) {
  const index = headers.indexOf(normalizeHeader_(headerName));
  if (index === -1) return '';
  return row[index] || '';
}

function normalizeHeader_(header) {
  return String(header || '')
    .trim()
    .replace(/\s+/g, '')
    .toLowerCase();
}

function isEmptyRow_(row) {
  return row.every(function (cell) {
    return String(cell || '').trim() === '';
  });
}

function parseCsv_(text) {
  const rows = [];
  let row = [];
  let cell = '';
  let inQuotes = false;

  for (let i = 0; i < text.length; i++) {
    const char = text[i];
    const nextChar = text[i + 1];

    if (char === '"' && inQuotes && nextChar === '"') {
      cell += '"';
      i++;
      continue;
    }

    if (char === '"') {
      inQuotes = !inQuotes;
      continue;
    }

    if (char === ',' && !inQuotes) {
      row.push(cell);
      cell = '';
      continue;
    }

    if ((char === '\n' || char === '\r') && !inQuotes) {
      if (char === '\r' && nextChar === '\n') i++;

      row.push(cell);
      rows.push(row);
      row = [];
      cell = '';
      continue;
    }

    cell += char;
  }

  if (cell !== '' || row.length > 0) {
    row.push(cell);
    rows.push(row);
  }

  return rows;
}
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      body {
        font-family: Arial, "Noto Sans TC", sans-serif;
        padding: 20px;
        color: #222;
      }

      h2 {
        margin: 0 0 12px;
        font-size: 20px;
      }

      p {
        margin: 0 0 16px;
        line-height: 1.6;
        color: #555;
      }

      .box {
        border: 1px solid #ddd;
        border-radius: 8px;
        padding: 16px;
        background: #fafafa;
      }

      input {
        width: 100%;
        margin: 8px 0 16px;
      }

      button {
        border: none;
        border-radius: 6px;
        padding: 10px 16px;
        cursor: pointer;
        font-size: 14px;
      }

      .primary {
        background: #5b21b6;
        color: white;
      }

      .secondary {
        background: #e5e7eb;
        color: #111;
        margin-left: 8px;
      }

      #status {
        margin-top: 16px;
        color: #444;
        line-height: 1.5;
      }
    </style>
  </head>

  <body>
    <h2>上傳 CSV 檔</h2>
    <p>請選擇三份 CSV 檔,系統會整理成:來源平台、報名編號、姓名、單位、Email。</p>

    <div class="box">
      <input id="csvFiles" type="file" accept=".csv,text/csv" multiple>

      <button class="primary" onclick="uploadFiles()">開始匯入</button>
      <button class="secondary" onclick="google.script.host.close()">取消</button>

      <div id="status"></div>
    </div>

    <script>
      function uploadFiles() {
        const input = document.getElementById('csvFiles');
        const status = document.getElementById('status');
        const files = Array.from(input.files || []);

        if (files.length === 0) {
          status.textContent = '請先選擇 CSV 檔。';
          return;
        }

        status.textContent = '讀取檔案中...';

        Promise.all(files.map(readFileAsText))
          .then(function (filePayloads) {
            status.textContent = '匯入中,請稍候...';

            google.script.run
              .withSuccessHandler(function (message) {
                status.textContent = message;
              })
              .withFailureHandler(function (error) {
                status.textContent = '匯入失敗:' + error.message;
              })
              .importCsvFiles(filePayloads);
          })
          .catch(function (error) {
            status.textContent = '讀取檔案失敗:' + error.message;
          });
      }

      function readFileAsText(file) {
        return new Promise(function (resolve, reject) {
          const reader = new FileReader();

          reader.onload = function (event) {
            resolve({
              name: file.name,
              content: event.target.result
            });
          };

          reader.onerror = function () {
            reject(new Error(file.name + ' 讀取失敗'));
          };

          reader.readAsText(file, 'UTF-8');
        });
      }
    </script>
  </body>
</html>
任務二 之二

自動寄信

const DATA_START_ROW = 2;
const TARGET_COLUMNS = 5;

const MAIL_NAME_COLUMN = 3;
const MAIL_EMAIL_COLUMN = 5;
const MAIL_STATUS_COLUMN = 6;

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('資料匯整工具')
    .addItem('上傳 CSV', 'showUploadDialog')
    .addItem('清空資料', 'clearImportedData')
    .addSeparator()
    .addItem('寄送通知信', 'sendNotificationEmails')
    .addItem('寄送信件', '寄送信件')
    .addToUi();
}

function showUploadDialog() {
  const html = HtmlService.createHtmlOutputFromFile('UploadDialog')
    .setWidth(520)
    .setHeight(360);

  SpreadsheetApp.getUi().showModalDialog(html, '上傳 CSV 檔');
}

function clearImportedData() {
  clearImportedData_();
  SpreadsheetApp.getUi().alert('已清空資料。');
}

function importCsvFiles(files) {
  if (!files || files.length === 0) {
    throw new Error('請至少上傳一份 CSV 檔。');
  }

  const sheet = getTargetSheet_();
  clearImportedData_();

  const allRows = [];

  files.forEach(function (file) {
    const fileName = file.name || '';
    const csvText = file.content || '';
    const sourcePlatform = detectSourcePlatform_(fileName, csvText);
    const rows = parseCsv_(csvText);

    if (rows.length < 2) return;

    const headers = rows[0].map(normalizeHeader_);
    const dataRows = rows.slice(1);

    dataRows.forEach(function (row) {
      if (isEmptyRow_(row)) return;
      allRows.push(mapRowToTarget_(sourcePlatform, headers, row));
    });
  });

  if (allRows.length === 0) {
    throw new Error('沒有讀到可匯入的資料,請確認 CSV 內容。');
  }

  sheet.getRange(DATA_START_ROW, 1, allRows.length, TARGET_COLUMNS).setValues(allRows);

  return `匯入完成,共 ${allRows.length} 筆資料。`;
}

function getTargetSheet_() {
  return SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
}

function clearImportedData_() {
  const sheet = getTargetSheet_();
  const lastRow = sheet.getLastRow();

  if (lastRow < DATA_START_ROW) return;

  sheet
    .getRange(DATA_START_ROW, 1, lastRow - DATA_START_ROW + 1, TARGET_COLUMNS)
    .clearContent();
}

function detectSourcePlatform_(fileName, csvText) {
  const lowerName = fileName.toLowerCase();
  const firstLine = csvText.split(/\r?\n/)[0] || '';

  if (
    lowerName.includes('google') ||
    lowerName.includes('form') ||
    firstLine.includes('電子郵件地址')
  ) {
    return 'Google Form';
  }

  if (lowerName.includes('accupass') || firstLine.includes('票券編號')) {
    return 'Accupass';
  }

  if (
    lowerName.includes('vendor') ||
    lowerName.includes('廠商') ||
    lowerName.includes('system') ||
    firstLine.includes('廠商流水號')
  ) {
    return '廠商自有系統';
  }

  return '未知來源';
}

function mapRowToTarget_(sourcePlatform, headers, row) {
  if (sourcePlatform === 'Google Form') {
    return [
      sourcePlatform,
      getCellByHeader_(headers, row, '回覆編號'),
      getCellByHeader_(headers, row, '姓名'),
      getCellByHeader_(headers, row, '服務單位'),
      getCellByHeader_(headers, row, '電子郵件地址')
    ];
  }

  if (sourcePlatform === 'Accupass') {
    return [
      sourcePlatform,
      getCellByHeader_(headers, row, '票券編號'),
      getCellByHeader_(headers, row, '參加人姓名'),
      getCellByHeader_(headers, row, '公司名稱'),
      getCellByHeader_(headers, row, '參加人Email')
    ];
  }

  if (sourcePlatform === '廠商自有系統') {
    return [
      sourcePlatform,
      getCellByHeader_(headers, row, '廠商流水號'),
      getCellByHeader_(headers, row, '聯絡人'),
      getCellByHeader_(headers, row, '單位名稱'),
      getCellByHeader_(headers, row, '聯絡信箱')
    ];
  }

  return [sourcePlatform, '', '', '', ''];
}

function getCellByHeader_(headers, row, headerName) {
  const index = headers.indexOf(normalizeHeader_(headerName));
  if (index === -1) return '';
  return row[index] || '';
}

function normalizeHeader_(header) {
  return String(header || '')
    .trim()
    .replace(/\s+/g, '')
    .toLowerCase();
}

function isEmptyRow_(row) {
  return row.every(function (cell) {
    return String(cell || '').trim() === '';
  });
}

function parseCsv_(text) {
  const rows = [];
  let row = [];
  let cell = '';
  let inQuotes = false;

  for (let i = 0; i < text.length; i++) {
    const char = text[i];
    const nextChar = text[i + 1];

    if (char === '"' && inQuotes && nextChar === '"') {
      cell += '"';
      i++;
      continue;
    }

    if (char === '"') {
      inQuotes = !inQuotes;
      continue;
    }

    if (char === ',' && !inQuotes) {
      row.push(cell);
      cell = '';
      continue;
    }

    if ((char === '\n' || char === '\r') && !inQuotes) {
      if (char === '\r' && nextChar === '\n') i++;

      row.push(cell);
      rows.push(row);
      row = [];
      cell = '';
      continue;
    }

    cell += char;
  }

  if (cell !== '' || row.length > 0) {
    row.push(cell);
    rows.push(row);
  }

  return rows;
}

function sendNotificationEmails() {
  const ui = SpreadsheetApp.getUi();
  const sheet = getTargetSheet_();
  const lastRow = sheet.getLastRow();

  if (lastRow < DATA_START_ROW) {
    ui.alert('目前沒有可寄送的資料。');
    return;
  }

  const rowCount = lastRow - DATA_START_ROW + 1;
  const values = sheet
    .getRange(DATA_START_ROW, 1, rowCount, TARGET_COLUMNS)
    .getValues();

  let sentCount = 0;
  let skippedCount = 0;

  for (let i = 0; i < values.length; i++) {
    const rowNumber = DATA_START_ROW + i;
    const name = values[i][MAIL_NAME_COLUMN - 1];
    const email = values[i][MAIL_EMAIL_COLUMN - 1];
    const status = values[i][MAIL_STATUS_COLUMN - 1];

    if (status && String(status).trim() !== '') {
      skippedCount++;
      continue;
    }

    if (!email || String(email).trim() === '') {
      skippedCount++;
      continue;
    }

    const subject = '【報名確認】我們已收到你的報名';
    const body =
      (name ? name : '您好') + ',\n\n' +
      '我們已經收到你的報名資料,活動相關細節將另行通知。\n\n' +
      '謝謝你的參與!';

    try {
      MailApp.sendEmail({
        to: String(email).trim(),
        subject: subject,
        body: body
      });

      sheet.getRange(rowNumber, MAIL_STATUS_COLUMN).setValue('已寄送');
      sentCount++;
    } catch (err) {
      sheet.getRange(rowNumber, MAIL_STATUS_COLUMN).setValue('寄送失敗:' + err.message);
    }
  }

  ui.alert('寄送完成:成功 ' + sentCount + ' 筆,略過 ' + skippedCount + ' 筆。');
}

function 寄送信件() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const lastRow = sheet.getLastRow();

  // 沒有資料列(只有標題)就提醒並結束
  if (lastRow < 2) {
    SpreadsheetApp.getUi().alert('沒有可寄送的資料。');
    return;
  }

  // 從第 2 列開始(跳過標題),讀取 B、C、D 三欄
  // B=2, C=3, D=4,共 3 欄
  const data = sheet.getRange(2, 2, lastRow - 1, 3).getValues();

  let 成功 = 0;
  let 略過 = 0;
  const 錯誤清單 = [];

  data.forEach((row, i) => {
    const 列號 = i + 2; // 對應實際試算表列號
    const 收件人 = String(row[0]).trim(); // B 欄
    const 主旨   = String(row[1]).trim(); // C 欄
    const 內容   = String(row[2]);        // D 欄

    // B 欄沒填 email 就略過
    if (!收件人) {
      略過++;
      return;
    }

    // 簡單驗證 email 格式
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(收件人)) {
      錯誤清單.push(`第 ${列號} 列:「${收件人}」不是有效的信箱`);
      return;
    }

    try {
      MailApp.sendEmail({
        to: 收件人,
        subject: 主旨,
        body: 內容
      });
      成功++;
    } catch (e) {
      錯誤清單.push(`第 ${列號} 列:${e.message}`);
    }
  });

  // 結果回報
  let 訊息 = `寄送完成!\n成功:${成功} 封\n略過(B 欄空白):${略過} 列`;
  if (錯誤清單.length > 0) {
    訊息 += `\n\n失敗 ${錯誤清單.length} 筆:\n` + 錯誤清單.join('\n');
  }
  SpreadsheetApp.getUi().alert(訊息);
}
💡
記得把「寄送信件」掛到選單:單純貼上 寄送信件() 函式還不夠,要在 onOpen() 的選單裡加上 .addItem('寄送信件', '寄送信件') 這一行(上面示範碼已加好),選單「資料匯整工具」才會出現「寄送信件」可以點。少了這一行,函式雖然存在卻無法從選單觸發。
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      body {
        font-family: Arial, "Noto Sans TC", sans-serif;
        padding: 20px;
        color: #222;
      }

      h2 {
        margin: 0 0 12px;
        font-size: 20px;
      }

      p {
        margin: 0 0 16px;
        line-height: 1.6;
        color: #555;
      }

      .box {
        border: 1px solid #ddd;
        border-radius: 8px;
        padding: 16px;
        background: #fafafa;
      }

      input {
        width: 100%;
        margin: 8px 0 16px;
      }

      button {
        border: none;
        border-radius: 6px;
        padding: 10px 16px;
        cursor: pointer;
        font-size: 14px;
      }

      .primary {
        background: #5b21b6;
        color: white;
      }

      .secondary {
        background: #e5e7eb;
        color: #111;
        margin-left: 8px;
      }

      #status {
        margin-top: 16px;
        color: #444;
        line-height: 1.5;
      }
    </style>
  </head>

  <body>
    <h2>上傳 CSV 檔</h2>
    <p>請選擇三份 CSV 檔,系統會整理成:來源平台、報名編號、姓名、單位、Email。</p>

    <div class="box">
      <input id="csvFiles" type="file" accept=".csv,text/csv" multiple>

      <button class="primary" onclick="uploadFiles()">開始匯入</button>
      <button class="secondary" onclick="google.script.host.close()">取消</button>

      <div id="status"></div>
    </div>

    <script>
      function uploadFiles() {
        const input = document.getElementById('csvFiles');
        const status = document.getElementById('status');
        const files = Array.from(input.files || []);

        if (files.length === 0) {
          status.textContent = '請先選擇 CSV 檔。';
          return;
        }

        status.textContent = '讀取檔案中...';

        Promise.all(files.map(readFileAsText))
          .then(function (filePayloads) {
            status.textContent = '匯入中,請稍候...';

            google.script.run
              .withSuccessHandler(function (message) {
                status.textContent = message;
              })
              .withFailureHandler(function (error) {
                status.textContent = '匯入失敗:' + error.message;
              })
              .importCsvFiles(filePayloads);
          })
          .catch(function (error) {
            status.textContent = '讀取檔案失敗:' + error.message;
          });
      }

      function readFileAsText(file) {
        return new Promise(function (resolve, reject) {
          const reader = new FileReader();

          reader.onload = function (event) {
            resolve({
              name: file.name,
              content: event.target.result
            });
          };

          reader.onerror = function () {
            reject(new Error(file.name + ' 讀取失敗'));
          };

          reader.readAsText(file, 'UTF-8');
        });
      }
    </script>
  </body>
</html>
任務三

Gemini Notebook:新人文件交叉比對

1

開啟 Gemini Notebook,建立新的 notebook。

2

上傳兩份模擬內部文件。

3

先請它整理重點,再追問差異、規則、待確認事項。

4

看到答案後,回到引用來源確認內容。

三個層次的示範提問

第一題:先把新人到職事項整理成清單

目標是先讓 Gemini Notebook 從單一文件中整理出可行動的 checklist。

我是今天報到的新人,請根據《新人到職說明》幫我整理今天必須完成的事項,分成「一定要做」「如果來得及可以做」「需要主管協助」三類,並整理成 checklist。

第二題:交叉兩份文件,處理真實卡點

目標是讓 AI 把新人到職說明與財務差旅規定串起來,看見跨文件流程。

我是到職第一週的新人,主管臨時安排我明天出差或去外部會議,但我的差勤系統和財務系統可能都還沒開通。請根據《新人到職說明》和《財務核銷與差旅規定》,整理我出發前、會議或出差中、回來後各自要注意什麼,並列出可能卡住的地方與建議處理方式。

第三題:請 AI 檢查文件之間的重複、矛盾與缺口

目標是把 Gemini Notebook 從查詢工具升級成文件改善助理。

請交叉比對《新人到職說明》和《財務核銷與差旅規定》這兩份文件,找出內容中互相補充、重複、可能矛盾或容易讓新人誤解的地方。請用表格整理,欄位包含「問題類型」「相關文件或段落」「可能造成的困惑」「建議補充或修改方式」。
★
Take Away

提示詞包

把今天的方法帶回工作現場:六個常見情境的提示詞直接複製就能用。提醒:貼任何內容進 AI 之前,先把玩家個資與未公開營運數據拿掉。

情境 01會議

會議記錄轉行動清單

以下是會議逐字稿或筆記,請整理成:①三句話摘要 ②決議事項 ③行動清單(每項標負責人與期限,沒講到的標【待確認】)④待追蹤問題。

【貼上會議記錄】
情境 02客服

客訴回覆潤稿

以下是一則玩家反映的問題(已去除個資)與我想回覆的重點。請幫我寫成一封得體的客服回覆:先同理對方的感受,再說明處理方式與預計時程,語氣專業友善、不官腔,200 字內。

玩家反映:【貼上】
我想回覆的重點:【貼上】
情境 03公告

玩家活動公告生成

請幫我寫一篇玩家活動公告。活動資訊:【活動名稱/時間/參加方式/獎勵內容】。要求:標題吸睛、開頭有梗不官腔、內文條列清楚、結尾提醒注意事項,300 字內。
情境 04回報

週報產生器

以下是我這週做的事(流水帳)。請整理成給主管的週報,分四區:本週成果、進行中、下週計畫、需要協助,每區最多 3 點,用詞精簡。

【貼上流水帳】
情境 05資料

散亂資料變表格

以下是一段散亂的資料(訊息、信件或筆記混在一起)。請整理成表格,欄位由你依內容判斷,缺漏的欄位標【缺】,最後告訴我哪些資料可能有問題需要人工確認。

【貼上散亂資料】
情境 06研究

競品與輿情快查(Perplexity 用)

請幫我整理【某款遊戲/某個產品】近一個月的玩家口碑與討論熱點:主要好評、主要抱怨、討論聲量的變化,每點附上來源連結,用表格呈現。
★
Resources

課後資源

課後想繼續玩,從這四個入口出發。

Perplexity

附來源的 AI 搜尋引擎:查產業趨勢、競品輿情、事實查證都好用,每個答案都附引用連結,點回原始出處確認。

Gemini Nano Banana 提示詞參考

GitHub 上的 Nano Banana(Gemini 生圖)案例集:大量生圖範例附完整提示詞與成品對照,看到喜歡的直接抄回家改。

GPT Image 2 提示詞參考

依用途分類的 GPT Image 2 提示詞庫:海報、插畫、App 與網頁視覺都有現成範例,找靈感、學寫法都方便。

Poseidon AI

把今天學的「模板化」再往前推一步:可以建立自己的 Skill(常用任務範本)與知識庫,讓 AI 帶著你的工作方法與資料回答問題,個人或團隊都容易上手的 AI 工作平臺。