LINE OA × Google Sheets × AI

คู่มือสร้าง “ผู้ช่วย AI ประจำทีม” ในกลุ่ม LINE

สร้างบอตที่ตอบคำถามจากข้อมูลจริง เช็กตารางเวร และแจ้งเตือนอัตโนมัติ ด้วย Google Apps Script โดยเลือกใช้ Gemini หรือ ChatGPT ได้

ระดับ: เริ่มต้น–ปานกลาง
เวลาทำต้นแบบ: ประมาณ 60–120 นาที
ตัวอย่าง: ตารางเวรประจำทีม
ปรับปรุงล่าสุด: กันยายน 2026

ระบบนี้ช่วยอะไรได้บ้าง?

ตัวอย่างในคู่มือนี้คือ “ผู้ช่วยตารางเวร” เมื่อคนในกลุ่มพิมพ์ น้องเจน สัปดาห์นี้ใครอยู่เวร? บอตจะอ่านข้อมูลล่าสุดจาก Google Sheet ส่งข้อมูลที่เกี่ยวข้องให้ AI ช่วยตีความคำถามและเรียบเรียงคำตอบ แล้วตอบกลับในกลุ่มเดิม

1. คนถามใน LINE
2. LINE ส่ง Webhook
3. Apps Script อ่าน Sheet
4. Gemini / ChatGPT ตอบ
5. LINE แสดงคำตอบ
1

LINE Official Account

เป็นตัวตนของบอต รับข้อความและส่งคำตอบในแชตส่วนตัวหรือกลุ่ม

2

Google Apps Script

เป็น Backend รับ Webhook อ่านข้อมูล เรียก AI และส่งข้อความกลับ โดยไม่ต้องเช่าเซิร์ฟเวอร์สำหรับต้นแบบ

3

Google Sheet

เป็นฐานข้อมูลเริ่มต้นที่ทีมแก้ไขได้ง่าย เช่น ตารางเวร Deadline FAQ หรือสถานะงาน

4

Gemini หรือ ChatGPT

ช่วยเข้าใจภาษาธรรมชาติ เช่น “พรุ่งนี้กะเช้ามีใคร” และสรุปข้อมูลให้อ่านง่าย

ผลลัพธ์เมื่อทำครบบอตตอบข้อมูลตารางเวรจาก Sheet, เก็บ Group ID สำหรับส่งแจ้งเตือน และส่งสรุปเวร 7 วันข้างหน้าอัตโนมัติตามเวลาที่กำหนดได้

สิ่งที่ต้องเตรียมก่อนเริ่ม

  • บัญชี LINE และสิทธิ์สร้าง LINE Official Account
  • บัญชี Google สำหรับ Google Sheets และ Apps Script
  • เลือก API อย่างน้อยหนึ่งราย: Gemini API หรือ OpenAI API
  • บัตรหรือเครดิต API หากผู้ให้บริการกำหนด แม้บางบริการอาจมีโควตาทดลอง
  • กลุ่ม LINE สำหรับทดสอบที่ยังไม่มี LINE OA ตัวอื่นอยู่
ChatGPT กับ OpenAI API ไม่ใช่แพ็กเกจเดียวกันการสมัคร ChatGPT แบบรายเดือนไม่ได้หมายความว่าจะมีเครดิต OpenAI API รวมอยู่ด้วย ส่วน API ต้องสร้าง Key และตรวจการเรียกเก็บเงินในแพลตฟอร์ม API แยกต่างหาก

Step 1กำหนดหน้าที่ของบอตให้แคบและชัด

อย่าเริ่มจากคำว่า “อยากได้บอตเก่งทุกเรื่อง” เพราะจะควบคุมคำตอบยากและมีความเสี่ยงเรื่องข้อมูล ให้เขียนขอบเขตงานหนึ่งประโยคก่อน เช่น:

ขอบเขตของต้นแบบ
บอตมีหน้าที่ตอบคำถามเกี่ยวกับตารางเวรจาก Google Sheet และแจ้งเตือนตารางเวร 7 วันข้างหน้าเท่านั้น หากไม่มีข้อมูลต้องบอกว่าไม่พบ ห้ามเดาหรือแต่งข้อมูลเพิ่ม

กำหนด Input, Data, Output

ส่วนตัวอย่างสิ่งที่ต้องตัดสินใจ
Input“น้องเจน พรุ่งนี้ใครอยู่เวรเช้า?”ต้องเรียกชื่อหรือใช้คำนำหน้าบอตหรือไม่
Dataวันที่, ช่วงเวลา, ชื่อ, หมายเหตุใครมีสิทธิ์แก้ข้อมูลต้นทาง
Outputวันที่ + กะ + รายชื่อต้องสั้นแค่ไหน และตอบเมื่อไม่พบข้อมูลอย่างไร
Actionตอบคำถาม / ส่งแจ้งเตือนงานใดทำอัตโนมัติ งานใดต้องขอยืนยัน

Step 2สร้าง Google Sheet สำหรับตารางเวร

  1. สร้าง Google Sheet ใหม่ ตั้งชื่อว่า LINE AI Team Assistant
  2. เปลี่ยนชื่อชีตแรกเป็น Schedule
  3. สร้างหัวตาราง 5 คอลัมน์ตามตัวอย่างด้านล่าง
  4. กำหนดคอลัมน์วันที่ให้เป็นชนิด Date และใช้ปี ค.ศ. เพื่อหลีกเลี่ยงความสับสน
  5. คัดลอก Spreadsheet ID จาก URL ซึ่งอยู่ระหว่าง /d/ และ /edit
DateShiftPersonNotesActive
2026-09-07เช้าสมชาย08:00–16:00TRUE
2026-09-07บ่ายสมหญิง16:00–00:00TRUE
2026-09-08เช้าอนันต์สลับเวรกับสมชายTRUE

สร้างชีตที่สองชื่อ Logs แล้วใส่หัวคอลัมน์ Timestamp, SourceType, SourceID, UserID, Message, Status ระบบจะใช้เก็บประวัติสำหรับตรวจสอบและหา Group ID

หลักข้อมูลที่ดีหนึ่งแถวควรแทนหนึ่งเวร วันที่ควรเป็นค่าชนิด Date จริง ไม่ Merge Cell และหลีกเลี่ยงการใส่หลายชื่อรวมไว้ในเซลล์เดียว

Step 3สร้าง LINE Official Account

  1. เข้า LINE Official Account Manager และเข้าสู่ระบบ
  2. เลือกสร้างบัญชีใหม่ กำหนดชื่อ รูปโปรไฟล์ และหมวดหมู่ เช่น น้องเจน – Team Assistant
  3. เปิดหน้า Settings ของบัญชี แล้วเตรียมเปิด Messaging API ใน Step ต่อไป

สำหรับระบบทีมภายใน ควรใช้ OA แยกจาก OA การตลาด เพื่อไม่ให้โควตา ข้อความ และข้อมูลผู้ใช้ปะปนกัน

Step 4เปิด Messaging API และสร้าง Token

  1. ใน LINE OA Manager ไปที่ Settings → Messaging API แล้วกด Enable
  2. เลือกหรือสร้าง Provider ซึ่งเปรียบเหมือนองค์กรที่ถือ Channel
  3. เข้า LINE Developers Console แล้วเปิด Channel ที่สร้างขึ้น
  4. แท็บ Messaging API ออก Channel Access Token สำหรับส่งข้อความกลับ
  5. แท็บ Basic settings จะมี Channel Secret แต่คู่มือต้นแบบ Apps Script นี้ไม่ได้ใช้ตรวจ Header โดยตรง ดูข้อจำกัดด้าน Security ช่วงท้าย
ห้ามวาง Token ในเอกสารหรือโพสต์สาธารณะChannel Access Token ใช้ส่งข้อความในนามบอต หากหลุดต้อง Revoke แล้วออกใหม่ทันที และไม่ควรฝังไว้ตรง ๆ ใน Source Code

Step 5เลือก Gemini หรือ ChatGPT

โค้ดชุดเดียวกันรองรับทั้งสองราย โดยเปลี่ยนค่า AI_PROVIDER เป็น gemini หรือ openai จึงเริ่มด้วยรายใดรายหนึ่งและสลับภายหลังได้

ตัวเลือกเหมาะเมื่อสิ่งที่ต้องสร้าง
Gemini APIใช้ระบบ Google อยู่แล้ว ต้องการเริ่มต้นเร็วและเชื่อม Google AI StudioGemini API Key จาก Google AI Studio
OpenAI APIต้องการใช้โมเดล OpenAI และ Responses APIOpenAI API Key พร้อม Billing/เครดิต API

ทางเลือก A — สร้าง Gemini API Key

  1. เข้า Google AI Studio – API Keys
  2. เลือกโปรเจกต์และกดสร้าง API Key
  3. คัดลอก Key เก็บไว้ชั่วคราว แล้วนำไปใส่ Script Properties ใน Step 7

ทางเลือก B — สร้าง OpenAI API Key

  1. เข้า OpenAI Platform – API Keys
  2. สร้าง Secret Key สำหรับโปรเจกต์นี้โดยเฉพาะ
  3. ตั้ง Usage limit/Budget Alert และเก็บ Key ไปใส่ Script Properties
ชื่อโมเดลเปลี่ยนได้ตามเวลาตัวอย่างกำหนดชื่อโมเดลไว้ใน Script Properties เพื่อให้แก้ได้โดยไม่แตะโค้ด หาก API แจ้งว่า model not found ให้ตรวจรายชื่อโมเดลใน Console ของผู้ให้บริการแล้วอัปเดตค่านี้

Step 6สร้าง Google Apps Script และวางโค้ด

  1. เปิด Google Sheet → Extensions → Apps Script
  2. ตั้งชื่อโปรเจกต์ว่า LINE AI Team Assistant
  3. ลบโค้ดเดิมใน Code.gs แล้ววางโค้ดด้านล่างทั้งหมด
  4. กด Save
const APP = {
  timezone: 'Asia/Bangkok',
  replyPrefixes: ['น้องเจน', '@น้องเจน', '/assistant'],
  maxRowsForAI: 200
};

function doGet() {
  return jsonOutput_({ ok: true, service: 'LINE AI Team Assistant' });
}

function doPost(e) {
  try {
    const cfg = getConfig_();

    // ตัวป้องกันพื้นฐานสำหรับต้นแบบ ไม่ใช่ตัวแทนการตรวจ X-Line-Signature
    if (!e || !e.parameter || e.parameter.key !== cfg.webhookSecret) {
      return jsonOutput_({ ok: false, error: 'unauthorized' });
    }

    const body = JSON.parse(e.postData.contents || '{}');
    const events = body.events || [];

    events.forEach(function(event) {
      try {
        processEvent_(event, cfg);
      } catch (err) {
        logEvent_(event, '', 'ERROR: ' + err.message);
      }
    });

    return jsonOutput_({ ok: true });
  } catch (err) {
    console.error(err.stack || err);
    return jsonOutput_({ ok: false, error: err.message });
  }
}

function processEvent_(event, cfg) {
  const source = event.source || {};
  const sourceId = source.groupId || source.roomId || source.userId || '';

  if (event.type === 'join') {
    logEvent_(event, '[BOT JOINED]', 'OK');
    if (event.replyToken) {
      replyLine_(event.replyToken,
        'สวัสดีค่ะ ฉันคือผู้ช่วยตารางเวร\nเรียกฉันด้วย “น้องเจน” แล้วตามด้วยคำถามได้เลย', cfg);
    }
    return;
  }

  if (event.type !== 'message' || !event.message || event.message.type !== 'text') {
    return;
  }

  const originalText = String(event.message.text || '').trim();
  logEvent_(event, originalText, 'RECEIVED');

  // ในกลุ่มให้ตอบเฉพาะเมื่อถูกเรียก ป้องกันบอตตอบทุกข้อความ
  if (source.type === 'group' || source.type === 'room') {
    if (!shouldRespond_(originalText)) return;
  }

  const question = cleanPrefix_(originalText);
  if (!question) {
    replyLine_(event.replyToken,
      'ลองถามว่า “น้องเจน พรุ่งนี้ใครอยู่เวรเช้า?”', cfg);
    return;
  }

  const schedule = readSchedule_();
  const context = scheduleToText_(schedule.slice(0, APP.maxRowsForAI));
  const prompt = buildPrompt_(question, context);

  let answer;
  if (cfg.aiProvider === 'openai') {
    answer = callOpenAI_(prompt, cfg);
  } else {
    answer = callGemini_(prompt, cfg);
  }

  replyLine_(event.replyToken, answer, cfg);
  logEvent_(event, originalText, 'REPLIED via ' + cfg.aiProvider);

  // sourceId ที่บันทึกใน Logs คือ Group ID สำหรับตั้งแจ้งเตือนอัตโนมัติ
  console.log('Source ID: ' + sourceId);
}

function shouldRespond_(text) {
  const normalized = String(text || '').trim().toLowerCase();
  return APP.replyPrefixes.some(function(prefix) {
    return normalized.indexOf(prefix.toLowerCase()) === 0;
  });
}

function cleanPrefix_(text) {
  let result = String(text || '').trim();
  APP.replyPrefixes.forEach(function(prefix) {
    if (result.toLowerCase().indexOf(prefix.toLowerCase()) === 0) {
      result = result.substring(prefix.length).trim();
    }
  });
  return result;
}

function readSchedule_() {
  const cfg = getConfig_();
  const sheet = SpreadsheetApp.openById(cfg.sheetId).getSheetByName(cfg.sheetName);
  if (!sheet) throw new Error('ไม่พบชีตชื่อ ' + cfg.sheetName);

  const values = sheet.getDataRange().getValues();
  if (values.length < 2) return [];

  return values.slice(1).filter(function(row) {
    const active = row[4];
    return row[0] && active !== false && String(active).toUpperCase() !== 'FALSE';
  }).map(function(row) {
    return {
      date: formatDateValue_(row[0]),
      shift: String(row[1] || ''),
      person: String(row[2] || ''),
      notes: String(row[3] || '')
    };
  });
}

function formatDateValue_(value) {
  if (value instanceof Date) {
    return Utilities.formatDate(value, APP.timezone, 'yyyy-MM-dd');
  }
  return String(value || '').trim();
}

function scheduleToText_(rows) {
  if (!rows.length) return 'ไม่พบข้อมูลตารางเวร';
  return rows.map(function(r) {
    return [r.date, r.shift, r.person, r.notes].join(' | ');
  }).join('\n');
}

function buildPrompt_(question, context) {
  const now = Utilities.formatDate(new Date(), APP.timezone, 'yyyy-MM-dd HH:mm');
  return [
    'คุณคือผู้ช่วยตารางเวรของทีม ตอบภาษาไทยแบบสั้น ชัด และสุภาพ',
    'เวลาปัจจุบันในประเทศไทย: ' + now,
    'กติกา:',
    '1) ใช้เฉพาะข้อมูลตารางเวรที่ให้มาเท่านั้น',
    '2) ห้ามเดาชื่อ วันที่ หรือกะที่ไม่มีในข้อมูล',
    '3) หากไม่พบ ให้ตอบว่า “ไม่พบข้อมูลในตารางเวร กรุณาตรวจสอบกับผู้ดูแล”',
    '4) แปลงคำว่า วันนี้/พรุ่งนี้/สัปดาห์นี้ จากเวลาปัจจุบันข้างต้น',
    '5) ไม่ต้องบอกว่าคุณเป็น AI และไม่ต้องอธิบายวิธีคิด',
    '',
    'คำถาม: ' + question,
    '',
    'ข้อมูลตารางเวร (Date | Shift | Person | Notes):',
    context
  ].join('\n');
}

function callGemini_(prompt, cfg) {
  const url = 'https://generativelanguage.googleapis.com/v1beta/models/' +
    encodeURIComponent(cfg.geminiModel) + ':generateContent';

  const response = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-goog-api-key': cfg.geminiApiKey },
    payload: JSON.stringify({
      contents: [{ role: 'user', parts: [{ text: prompt }] }],
      generationConfig: { temperature: 0.1, maxOutputTokens: 500 }
    }),
    muteHttpExceptions: true
  });

  ensureSuccess_(response, 'Gemini');
  const data = JSON.parse(response.getContentText());
  const parts = (((data.candidates || [])[0] || {}).content || {}).parts || [];
  const text = parts.map(function(p) { return p.text || ''; }).join('').trim();
  if (!text) throw new Error('Gemini ไม่ส่งข้อความกลับ');
  return text;
}

function callOpenAI_(prompt, cfg) {
  const response = UrlFetchApp.fetch('https://api.openai.com/v1/responses', {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + cfg.openaiApiKey },
    payload: JSON.stringify({
      model: cfg.openaiModel,
      instructions: 'ตอบจากข้อมูลที่ได้รับเท่านั้น ห้ามแต่งข้อมูล',
      input: prompt,
      max_output_tokens: 500
    }),
    muteHttpExceptions: true
  });

  ensureSuccess_(response, 'OpenAI');
  const data = JSON.parse(response.getContentText());
  if (data.output_text) return String(data.output_text).trim();

  const texts = [];
  (data.output || []).forEach(function(item) {
    (item.content || []).forEach(function(part) {
      if (part.type === 'output_text' && part.text) texts.push(part.text);
    });
  });
  if (!texts.length) throw new Error('OpenAI ไม่ส่งข้อความกลับ');
  return texts.join('\n').trim();
}

function ensureSuccess_(response, serviceName) {
  const code = response.getResponseCode();
  if (code < 200 || code >= 300) {
    throw new Error(serviceName + ' API ' + code + ': ' +
      response.getContentText().substring(0, 500));
  }
}

function replyLine_(replyToken, text, cfg) {
  if (!replyToken) return;
  const safeText = String(text || 'ไม่พบคำตอบ').substring(0, 4500);
  const response = UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + cfg.lineToken },
    payload: JSON.stringify({
      replyToken: replyToken,
      messages: [{ type: 'text', text: safeText }]
    }),
    muteHttpExceptions: true
  });
  ensureSuccess_(response, 'LINE Reply');
}

function pushLine_(to, text, cfg) {
  const response = UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + cfg.lineToken },
    payload: JSON.stringify({
      to: to,
      messages: [{ type: 'text', text: String(text).substring(0, 4500) }]
    }),
    muteHttpExceptions: true
  });
  ensureSuccess_(response, 'LINE Push');
}

function sendWeeklyDutyReminder() {
  const cfg = getConfig_();
  if (!cfg.groupId) throw new Error('ยังไม่ได้ตั้ง GROUP_ID ใน Script Properties');

  const today = startOfDay_(new Date());
  const end = new Date(today.getTime() + 6 * 24 * 60 * 60 * 1000);
  const rows = readSchedule_().filter(function(r) {
    const d = parseIsoDate_(r.date);
    return d && d >= today && d <= end;
  });

  const title = '📅 ตารางเวร 7 วันข้างหน้า';
  const detail = rows.length ? rows.map(function(r) {
    return '• ' + r.date + ' | ' + r.shift + ' | ' + r.person +
      (r.notes ? ' (' + r.notes + ')' : '');
  }).join('\n') : 'ไม่พบข้อมูลตารางเวรในช่วงนี้';

  pushLine_(cfg.groupId, title + '\n' + detail, cfg);
}

function startOfDay_(date) {
  const iso = Utilities.formatDate(date, APP.timezone, 'yyyy-MM-dd');
  return parseIsoDate_(iso);
}

function parseIsoDate_(iso) {
  const m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (!m) return null;
  return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
}

function logEvent_(event, message, status) {
  try {
    const cfg = getConfig_();
    const ss = SpreadsheetApp.openById(cfg.sheetId);
    let sheet = ss.getSheetByName('Logs');
    if (!sheet) {
      sheet = ss.insertSheet('Logs');
      sheet.appendRow(['Timestamp','SourceType','SourceID','UserID','Message','Status']);
    }
    const source = event.source || {};
    sheet.appendRow([
      new Date(), source.type || '',
      source.groupId || source.roomId || source.userId || '',
      source.userId || '', message || '', status || ''
    ]);
  } catch (err) {
    console.error('Log error: ' + err.message);
  }
}

function getConfig_() {
  const p = PropertiesService.getScriptProperties().getProperties();
  const required = ['LINE_CHANNEL_ACCESS_TOKEN','SHEET_ID','WEBHOOK_SECRET','AI_PROVIDER'];
  required.forEach(function(key) {
    if (!p[key]) throw new Error('ขาด Script Property: ' + key);
  });

  const provider = String(p.AI_PROVIDER).toLowerCase();
  if (provider === 'gemini' && !p.GEMINI_API_KEY) {
    throw new Error('ขาด Script Property: GEMINI_API_KEY');
  }
  if (provider === 'openai' && !p.OPENAI_API_KEY) {
    throw new Error('ขาด Script Property: OPENAI_API_KEY');
  }

  return {
    lineToken: p.LINE_CHANNEL_ACCESS_TOKEN,
    sheetId: p.SHEET_ID,
    sheetName: p.SHEET_NAME || 'Schedule',
    webhookSecret: p.WEBHOOK_SECRET,
    aiProvider: provider,
    geminiApiKey: p.GEMINI_API_KEY || '',
    geminiModel: p.GEMINI_MODEL || 'gemini-3.7-flash',
    openaiApiKey: p.OPENAI_API_KEY || '',
    openaiModel: p.OPENAI_MODEL || 'gpt-5-mini',
    groupId: p.GROUP_ID || ''
  };
}

function jsonOutput_(data) {
  return ContentService.createTextOutput(JSON.stringify(data))
    .setMimeType(ContentService.MimeType.JSON);
}
ทำไมให้ AI อ่านข้อมูลแทนการตอบจากความจำ?เพราะข้อมูลตารางเวรเปลี่ยนได้ ระบบจึงอ่าน Sheet ใหม่ทุกครั้งและสั่ง AI ให้ใช้เฉพาะ Context ที่ส่งไป วิธีนี้ลดการตอบจากข้อมูลเก่าหรือแต่งคำตอบเอง

Step 7ตั้งค่า Script Properties

  1. ใน Apps Script กด Project Settings รูปเฟือง
  2. เลื่อนไปที่ Script Properties แล้วเพิ่มค่าตามตาราง
  3. สร้าง WEBHOOK_SECRET เป็นข้อความสุ่มยาวอย่างน้อย 32 ตัวอักษร ไม่ใช้รหัสผ่านส่วนตัว
Propertyค่าตัวอย่างจำเป็นเมื่อ
LINE_CHANNEL_ACCESS_TOKENToken จาก LINEทุกกรณี
SHEET_ID1AbC...xyzทุกกรณี
SHEET_NAMEScheduleทุกกรณี
WEBHOOK_SECRETข้อความสุ่ม 32+ ตัวทุกกรณี
AI_PROVIDERgemini หรือ openaiทุกกรณี
GEMINI_API_KEYKey จาก AI Studioเลือก Gemini
GEMINI_MODELgemini-3.7-flashเลือก Gemini
OPENAI_API_KEYKey จาก OpenAIเลือก OpenAI
OPENAI_MODELgpt-5-miniเลือก OpenAI
GROUP_IDใส่หลังบอตเข้ากลุ่มต้องการ Push แจ้งเตือน
จุดตรวจสอบProperties ต้องไม่มีเครื่องหมายคำพูดครอบค่า ไม่มีช่องว่างนำหน้า/ท้าย และค่า AI_PROVIDER ต้องเป็นตัวพิมพ์เล็กตามตัวอย่าง

Step 8Deploy Apps Script เป็น Web App

  1. กด Deploy → New deployment
  2. Select type เลือก Web app
  3. Execute as เลือก Me เพื่อให้สคริปต์อ่าน Sheet ด้วยสิทธิ์เจ้าของ
  4. Who has access เลือกตัวเลือกที่อนุญาตให้ระบบภายนอกเรียก URL ได้ เช่น Anyone ตามตัวเลือกที่บัญชีของคุณแสดง
  5. กด Deploy และอนุญาตสิทธิ์ที่จำเป็น
  6. คัดลอก URL ที่ลงท้ายด้วย /exec ห้ามใช้ URL ทดสอบที่ลงท้าย /dev

ทดสอบเปิด URL /exec ในเบราว์เซอร์ ควรเห็น JSON คล้าย {"ok":true,"service":"LINE AI Team Assistant"}

เมื่อแก้โค้ดภายหลังไปที่ Deploy → Manage deployments → Edit → เลือก New version แล้ว Deploy อีกครั้ง มิฉะนั้น LINE อาจยังเรียกโค้ดเวอร์ชันเก่า

Step 9เชื่อม Webhook URL กับ LINE

นำ URL จาก Step 8 มาต่อท้ายด้วย Secret แบบนี้:

https://script.google.com/macros/s/DEPLOYMENT_ID/exec?key=ค่าจาก_WEBHOOK_SECRET
  1. เปิด LINE Developers Console → Channel → แท็บ Messaging API
  2. วาง URL ข้างต้นในช่อง Webhook URL
  3. กด Verify ควรขึ้น Success
  4. เปิดสวิตช์ Use webhook
  5. ใน LINE OA Manager ปิดข้อความตอบกลับอัตโนมัติที่ชนกับบอต เช่น Greeting/Auto-response ตามรูปแบบใช้งาน

ตอน LINE กด Verify ระบบอาจส่ง Event ว่างเข้ามา โค้ดจึงต้องตอบสถานะสำเร็จได้แม้ไม่มีข้อความ

Step 10ทดสอบแบบส่วนตัวก่อนเข้ากลุ่ม

  1. เพิ่ม LINE OA เป็นเพื่อนด้วย QR Code ในหน้า Messaging API
  2. ส่งข้อความ พรุ่งนี้ใครอยู่เวร? ในแชตส่วนตัว
  3. ตรวจว่าบอตตอบจากข้อมูลใน Sheet และไม่สร้างชื่อที่ไม่มีอยู่
  4. เปิด Apps Script → Executions เพื่อตรวจสถานะ หากล้มเหลวให้เปิดรายละเอียด Error
  5. เปิดชีต Logs ตรวจว่ามีรายการ RECEIVED และ REPLIED
กรณีทดสอบคำถามผลลัพธ์ที่ควรได้
มีข้อมูลพรุ่งนี้ใครเวรเช้า?ตอบชื่อจาก Sheet ตรงวันและกะ
ไม่มีข้อมูลวันที่ที่ไม่มีใน Sheet ใครอยู่เวร?บอกว่าไม่พบ ไม่เดา
คำถามนอกขอบเขตช่วยวิเคราะห์หุ้นให้หน่อยปฏิเสธหรือบอกขอบเขตหน้าที่
ข้อมูลถูกปิดถามถึงแถวที่ Active = FALSEไม่ควรนำแถวนั้นมาตอบ

Step 11เปิดให้บอตเข้ากลุ่มและหา Group ID

  1. LINE Developers Console → Messaging API เปิด Allow bot to join group chats ซึ่งปกติปิดไว้
  2. เชิญ LINE OA เข้ากลุ่มทดสอบ โปรดทราบว่าในหนึ่งกลุ่มมี LINE OA ได้เพียงหนึ่งบัญชีในเวลาเดียวกัน
  3. เมื่อบอตเข้ากลุ่ม ระบบจะบันทึก Event ลง Logs
  4. พิมพ์ น้องเจน สัปดาห์นี้ใครอยู่เวร?
  5. คัดลอกค่า SourceID ที่ขึ้นต้นลักษณะ C... จาก Logs ไปใส่ Script Property ชื่อ GROUP_ID
ควบคุมไม่ให้บอตรบกวนกลุ่มใน Group Chat โค้ดจะตอบเฉพาะข้อความที่เริ่มด้วย “น้องเจน”, “@น้องเจน” หรือ “/assistant” แต่ในแชตส่วนตัวสามารถถามตรง ๆ ได้

Step 12ตั้งแจ้งเตือนตารางเวรอัตโนมัติ

  1. ตรวจว่าใส่ GROUP_ID ใน Script Properties แล้ว
  2. ใน Apps Script เลือกฟังก์ชัน sendWeeklyDutyReminder แล้วกด Run หนึ่งครั้ง
  3. ตรวจว่ากลุ่มได้รับข้อความ “ตารางเวร 7 วันข้างหน้า”
  4. ไปที่เมนู Triggers รูปนาฬิกา → Add Trigger
  5. Choose function เลือก sendWeeklyDutyReminder
  6. Event source เลือก Time-driven, เลือก Weekly และกำหนดวัน/ช่วงเวลา

Time-driven trigger ของ Apps Script มักทำงานภายในช่วงเวลาที่เลือก ไม่รับประกันนาทีตรงเป๊ะ หากงานต้องตรงเวลาและมี SLA ควรใช้ Cloud Scheduler ร่วมกับ Backend ที่เหมาะกับ Production

วางแผนโควตา LINEPush Message เข้า Group นับตามจำนวนผู้รับ ไม่ได้คิดตามจำนวนกล่องข้อความ เช่น ส่งหนึ่งครั้งเข้ากลุ่ม 20 คน อาจนับเป็น 20 ข้อความ จึงควรแจ้งเฉพาะเรื่องสำคัญและให้บอตตอบเมื่อถูกเรียกเป็นหลัก

Security และข้อจำกัดที่ต้องรู้ก่อนใช้จริง

ข้อจำกัดสำคัญของ Apps Script Web AppLINE แนะนำให้ตรวจ x-line-signature ทุก Webhook เพื่อยืนยันว่าคำขอมาจาก LINE แต่ Event ของ doPost(e) ใน Apps Script แบบพื้นฐานไม่ได้เปิด arbitrary request headers ตามอินเทอร์เฟซที่ Google ระบุ จึงตรวจ Header นี้โดยตรงในสถาปัตยกรรมตัวอย่างไม่ได้ ค่า Secret ใน URL เป็นเพียงเกราะเสริมสำหรับต้นแบบ ไม่ใช่การตรวจลายเซ็นตามมาตรฐาน LINE

ใช้เมื่อใด?

สถานการณ์คำแนะนำ
ต้นแบบในทีมเล็ก ข้อมูลไม่อ่อนไหวApps Script + Secret URL ใช้ทดลองแนวคิดได้ พร้อมจำกัดผู้เข้าถึง Sheet
ข้อมูลลูกค้า พนักงาน การเงิน หรือสุขภาพย้าย Webhook ไป Cloud Run / Cloud Functions หรือ Server ที่อ่าน Header และตรวจ LINE Signature ได้
บอตมีสิทธิ์แก้ข้อมูลหรือสั่งงานทำ Allowlist, Role/Permission, Confirmation และ Audit Log ก่อนเปิดใช้
มีผู้ใช้จำนวนมากหรือมี SLAใช้ Queue, Retry, Monitoring, Database และ Backend ที่รองรับโหลดจริง

รายการควบคุมขั้นต่ำ

  • เก็บ Token และ API Key ใน Script Properties ไม่วางใน Sheet หรือโค้ด
  • จำกัดสิทธิ์แก้ Sheet เฉพาะผู้ดูแล
  • หมุนเวียน Key/Token เมื่อสงสัยว่ารั่ว
  • อย่าส่งข้อมูลส่วนบุคคลที่ไม่จำเป็นไปยัง AI API
  • กำหนดอายุ Logs และลบข้อความเก่าตามนโยบายองค์กร
  • งานที่แก้ข้อมูล ลบข้อมูล หรือส่งข้อความภายนอกควรให้มนุษย์ยืนยัน

คู่มือแก้ปัญหาที่พบบ่อย

อาการสาเหตุที่เป็นไปได้วิธีตรวจ
Webhook Verify ไม่ผ่านใช้ URL /dev, ลืม Secret, สิทธิ์ Web App ไม่เปิดเปิด URL /exec ก่อน แล้วตรวจ ?key=... ให้ตรง Property
บอตไม่ตอบ แต่ Webhook ผ่านUse webhook ยังปิด, Auto-response ชน, Prefix ไม่ตรงดู Apps Script Executions และ Logs
401 จาก LINEToken ผิด หมดอายุ หรือถูก Revokeออก Token ใหม่และอัปเดต Script Property
401/403 จาก AIAPI Key ผิด, Billing/สิทธิ์โปรเจกต์มีปัญหาตรวจ Key, Project และ API usage console
404 model not foundชื่อโมเดลไม่เปิดใช้หรือเปลี่ยนแล้วเปลี่ยน GEMINI_MODEL/OPENAI_MODEL ให้ตรง Console
อ่าน Sheet ไม่ได้SHEET_ID ผิด, ชื่อ Sheet ผิด, Deployment ใช้สิทธิ์ไม่ถูกตรวจ ID, Schedule, และ Execute as Me
ตอบข้อมูลผิดวันวันที่เป็นข้อความหรือ Timezone ผิดแปลงคอลัมน์เป็น Date และตั้ง Project timezone เป็น Asia/Bangkok
แก้โค้ดแล้วไม่เปลี่ยนยังไม่ได้ Deploy New versionManage deployments → Edit → New version
Push แจ้งเตือนไม่เข้าGROUP_ID ผิดหรือบอตออกจากกลุ่มแล้วคัดลอก SourceID ใหม่จาก Logs

Prompt สำหรับส่ง Error ให้ AI ช่วยแก้

ฉันกำลังสร้าง LINE Bot ด้วย Google Apps Script ตามโครงสร้าง LINE Webhook → Apps Script → Google Sheet → [Gemini/OpenAI] → LINE Reply

อาการ: [อธิบายสิ่งที่เกิด]
สิ่งที่คาดหวัง: [ผลลัพธ์ที่ต้องการ]
Error จาก Apps Script Executions: [วางข้อความ Error โดยลบ Token/API Key ออกแล้ว]
ฟังก์ชันที่เกี่ยวข้อง: [วางเฉพาะโค้ดส่วนที่เกี่ยวข้อง]

ช่วยวิเคราะห์ Root Cause, บอกจุดที่ต้องแก้ และส่งโค้ดฟังก์ชันฉบับแก้ไขกลับมา ห้ามขอหรือแสดง Secret

ต่อยอดจากตารางเวรไปเป็นผู้ช่วยแบบอื่น

CONTENT

Deadline Assistant

อ่าน Content Tracker แล้วตอบว่าวันนี้มีงานอะไรต้องส่ง หรือแจ้งเฉพาะงานที่ใกล้เกิน SLA

MEETING

Meeting Assistant

เชื่อม Google Calendar เพื่อสรุปประชุมประจำวันและแจ้งเตือนก่อนประชุม

KNOWLEDGE

Team Knowledge Bot

ค้น FAQ, SOP และลิงก์เอกสารที่ทีมถามซ้ำ โดยต้องมีสิทธิ์เข้าถึงตามบทบาท

MARKETING

Performance Alert

ดึงข้อมูลจาก Ads API หรือ Dashboard แล้วแจ้งเมื่อ CPA, Budget หรือ Conversion ผิดเกณฑ์

ลำดับการพัฒนาที่แนะนำ

  1. Phase 1 — Read only: ให้บอตอ่านและตอบข้อมูลอย่างเดียว
  2. Phase 2 — Alerts: เพิ่มการแจ้งเตือนเมื่อถึงเวลา/เข้าเงื่อนไข
  3. Phase 3 — Write with confirmation: เพิ่มงานหรือแก้ข้อมูลหลังผู้ใช้กดยืนยัน
  4. Phase 4 — Production: ย้าย Backend, ตรวจ Signature, ทำสิทธิ์, Queue, Monitoring และ Backup
หลักคิดสุดท้าย
เริ่มจากงานที่ทีม “ถามซ้ำ เช็กซ้ำ เตือนซ้ำ และค้นซ้ำ” หนึ่งงาน ทำให้ใช้ได้จริง แล้วค่อยขยาย ไม่จำเป็นต้องเริ่มจาก AI Agent ที่ทำได้ทุกอย่าง

แหล่งอ้างอิงทางการ

Skill360