ระบบนี้ช่วยอะไรได้บ้าง?
ตัวอย่างในคู่มือนี้คือ “ผู้ช่วยตารางเวร” เมื่อคนในกลุ่มพิมพ์ น้องเจน สัปดาห์นี้ใครอยู่เวร? บอตจะอ่านข้อมูลล่าสุดจาก Google Sheet ส่งข้อมูลที่เกี่ยวข้องให้ AI ช่วยตีความคำถามและเรียบเรียงคำตอบ แล้วตอบกลับในกลุ่มเดิม
LINE Official Account
เป็นตัวตนของบอต รับข้อความและส่งคำตอบในแชตส่วนตัวหรือกลุ่ม
Google Apps Script
เป็น Backend รับ Webhook อ่านข้อมูล เรียก AI และส่งข้อความกลับ โดยไม่ต้องเช่าเซิร์ฟเวอร์สำหรับต้นแบบ
Google Sheet
เป็นฐานข้อมูลเริ่มต้นที่ทีมแก้ไขได้ง่าย เช่น ตารางเวร Deadline FAQ หรือสถานะงาน
Gemini หรือ ChatGPT
ช่วยเข้าใจภาษาธรรมชาติ เช่น “พรุ่งนี้กะเช้ามีใคร” และสรุปข้อมูลให้อ่านง่าย
สิ่งที่ต้องเตรียมก่อนเริ่ม
- บัญชี LINE และสิทธิ์สร้าง LINE Official Account
- บัญชี Google สำหรับ Google Sheets และ Apps Script
- เลือก API อย่างน้อยหนึ่งราย: Gemini API หรือ OpenAI API
- บัตรหรือเครดิต API หากผู้ให้บริการกำหนด แม้บางบริการอาจมีโควตาทดลอง
- กลุ่ม LINE สำหรับทดสอบที่ยังไม่มี LINE OA ตัวอื่นอยู่
Step 1กำหนดหน้าที่ของบอตให้แคบและชัด
อย่าเริ่มจากคำว่า “อยากได้บอตเก่งทุกเรื่อง” เพราะจะควบคุมคำตอบยากและมีความเสี่ยงเรื่องข้อมูล ให้เขียนขอบเขตงานหนึ่งประโยคก่อน เช่น:
บอตมีหน้าที่ตอบคำถามเกี่ยวกับตารางเวรจาก Google Sheet และแจ้งเตือนตารางเวร 7 วันข้างหน้าเท่านั้น หากไม่มีข้อมูลต้องบอกว่าไม่พบ ห้ามเดาหรือแต่งข้อมูลเพิ่ม
กำหนด Input, Data, Output
| ส่วน | ตัวอย่าง | สิ่งที่ต้องตัดสินใจ |
|---|---|---|
| Input | “น้องเจน พรุ่งนี้ใครอยู่เวรเช้า?” | ต้องเรียกชื่อหรือใช้คำนำหน้าบอตหรือไม่ |
| Data | วันที่, ช่วงเวลา, ชื่อ, หมายเหตุ | ใครมีสิทธิ์แก้ข้อมูลต้นทาง |
| Output | วันที่ + กะ + รายชื่อ | ต้องสั้นแค่ไหน และตอบเมื่อไม่พบข้อมูลอย่างไร |
| Action | ตอบคำถาม / ส่งแจ้งเตือน | งานใดทำอัตโนมัติ งานใดต้องขอยืนยัน |
Step 2สร้าง Google Sheet สำหรับตารางเวร
- สร้าง Google Sheet ใหม่ ตั้งชื่อว่า LINE AI Team Assistant
- เปลี่ยนชื่อชีตแรกเป็น
Schedule - สร้างหัวตาราง 5 คอลัมน์ตามตัวอย่างด้านล่าง
- กำหนดคอลัมน์วันที่ให้เป็นชนิด Date และใช้ปี ค.ศ. เพื่อหลีกเลี่ยงความสับสน
- คัดลอก Spreadsheet ID จาก URL ซึ่งอยู่ระหว่าง
/d/และ/edit
| Date | Shift | Person | Notes | Active |
|---|---|---|---|---|
| 2026-09-07 | เช้า | สมชาย | 08:00–16:00 | TRUE |
| 2026-09-07 | บ่าย | สมหญิง | 16:00–00:00 | TRUE |
| 2026-09-08 | เช้า | อนันต์ | สลับเวรกับสมชาย | TRUE |
สร้างชีตที่สองชื่อ Logs แล้วใส่หัวคอลัมน์ Timestamp, SourceType, SourceID, UserID, Message, Status ระบบจะใช้เก็บประวัติสำหรับตรวจสอบและหา Group ID
Step 3สร้าง LINE Official Account
- เข้า LINE Official Account Manager และเข้าสู่ระบบ
- เลือกสร้างบัญชีใหม่ กำหนดชื่อ รูปโปรไฟล์ และหมวดหมู่ เช่น น้องเจน – Team Assistant
- เปิดหน้า Settings ของบัญชี แล้วเตรียมเปิด Messaging API ใน Step ต่อไป
สำหรับระบบทีมภายใน ควรใช้ OA แยกจาก OA การตลาด เพื่อไม่ให้โควตา ข้อความ และข้อมูลผู้ใช้ปะปนกัน
Step 4เปิด Messaging API และสร้าง Token
- ใน LINE OA Manager ไปที่ Settings → Messaging API แล้วกด Enable
- เลือกหรือสร้าง Provider ซึ่งเปรียบเหมือนองค์กรที่ถือ Channel
- เข้า LINE Developers Console แล้วเปิด Channel ที่สร้างขึ้น
- แท็บ Messaging API ออก Channel Access Token สำหรับส่งข้อความกลับ
- แท็บ Basic settings จะมี Channel Secret แต่คู่มือต้นแบบ Apps Script นี้ไม่ได้ใช้ตรวจ Header โดยตรง ดูข้อจำกัดด้าน Security ช่วงท้าย
Step 5เลือก Gemini หรือ ChatGPT
โค้ดชุดเดียวกันรองรับทั้งสองราย โดยเปลี่ยนค่า AI_PROVIDER เป็น gemini หรือ openai จึงเริ่มด้วยรายใดรายหนึ่งและสลับภายหลังได้
| ตัวเลือก | เหมาะเมื่อ | สิ่งที่ต้องสร้าง |
|---|---|---|
| Gemini API | ใช้ระบบ Google อยู่แล้ว ต้องการเริ่มต้นเร็วและเชื่อม Google AI Studio | Gemini API Key จาก Google AI Studio |
| OpenAI API | ต้องการใช้โมเดล OpenAI และ Responses API | OpenAI API Key พร้อม Billing/เครดิต API |
ทางเลือก A — สร้าง Gemini API Key
- เข้า Google AI Studio – API Keys
- เลือกโปรเจกต์และกดสร้าง API Key
- คัดลอก Key เก็บไว้ชั่วคราว แล้วนำไปใส่ Script Properties ใน Step 7
ทางเลือก B — สร้าง OpenAI API Key
- เข้า OpenAI Platform – API Keys
- สร้าง Secret Key สำหรับโปรเจกต์นี้โดยเฉพาะ
- ตั้ง Usage limit/Budget Alert และเก็บ Key ไปใส่ Script Properties
Step 6สร้าง Google Apps Script และวางโค้ด
- เปิด Google Sheet → Extensions → Apps Script
- ตั้งชื่อโปรเจกต์ว่า LINE AI Team Assistant
- ลบโค้ดเดิมใน
Code.gsแล้ววางโค้ดด้านล่างทั้งหมด - กด 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);
}Step 7ตั้งค่า Script Properties
- ใน Apps Script กด Project Settings รูปเฟือง
- เลื่อนไปที่ Script Properties แล้วเพิ่มค่าตามตาราง
- สร้าง
WEBHOOK_SECRETเป็นข้อความสุ่มยาวอย่างน้อย 32 ตัวอักษร ไม่ใช้รหัสผ่านส่วนตัว
| Property | ค่าตัวอย่าง | จำเป็นเมื่อ |
|---|---|---|
LINE_CHANNEL_ACCESS_TOKEN | Token จาก LINE | ทุกกรณี |
SHEET_ID | 1AbC...xyz | ทุกกรณี |
SHEET_NAME | Schedule | ทุกกรณี |
WEBHOOK_SECRET | ข้อความสุ่ม 32+ ตัว | ทุกกรณี |
AI_PROVIDER | gemini หรือ openai | ทุกกรณี |
GEMINI_API_KEY | Key จาก AI Studio | เลือก Gemini |
GEMINI_MODEL | gemini-3.7-flash | เลือก Gemini |
OPENAI_API_KEY | Key จาก OpenAI | เลือก OpenAI |
OPENAI_MODEL | gpt-5-mini | เลือก OpenAI |
GROUP_ID | ใส่หลังบอตเข้ากลุ่ม | ต้องการ Push แจ้งเตือน |
AI_PROVIDER ต้องเป็นตัวพิมพ์เล็กตามตัวอย่างStep 8Deploy Apps Script เป็น Web App
- กด Deploy → New deployment
- Select type เลือก Web app
- Execute as เลือก Me เพื่อให้สคริปต์อ่าน Sheet ด้วยสิทธิ์เจ้าของ
- Who has access เลือกตัวเลือกที่อนุญาตให้ระบบภายนอกเรียก URL ได้ เช่น Anyone ตามตัวเลือกที่บัญชีของคุณแสดง
- กด Deploy และอนุญาตสิทธิ์ที่จำเป็น
- คัดลอก URL ที่ลงท้ายด้วย
/execห้ามใช้ URL ทดสอบที่ลงท้าย/dev
ทดสอบเปิด URL /exec ในเบราว์เซอร์ ควรเห็น JSON คล้าย {"ok":true,"service":"LINE AI Team Assistant"}
Step 9เชื่อม Webhook URL กับ LINE
นำ URL จาก Step 8 มาต่อท้ายด้วย Secret แบบนี้:
https://script.google.com/macros/s/DEPLOYMENT_ID/exec?key=ค่าจาก_WEBHOOK_SECRET- เปิด LINE Developers Console → Channel → แท็บ Messaging API
- วาง URL ข้างต้นในช่อง Webhook URL
- กด Verify ควรขึ้น Success
- เปิดสวิตช์ Use webhook
- ใน LINE OA Manager ปิดข้อความตอบกลับอัตโนมัติที่ชนกับบอต เช่น Greeting/Auto-response ตามรูปแบบใช้งาน
ตอน LINE กด Verify ระบบอาจส่ง Event ว่างเข้ามา โค้ดจึงต้องตอบสถานะสำเร็จได้แม้ไม่มีข้อความ
Step 10ทดสอบแบบส่วนตัวก่อนเข้ากลุ่ม
- เพิ่ม LINE OA เป็นเพื่อนด้วย QR Code ในหน้า Messaging API
- ส่งข้อความ พรุ่งนี้ใครอยู่เวร? ในแชตส่วนตัว
- ตรวจว่าบอตตอบจากข้อมูลใน Sheet และไม่สร้างชื่อที่ไม่มีอยู่
- เปิด Apps Script → Executions เพื่อตรวจสถานะ หากล้มเหลวให้เปิดรายละเอียด Error
- เปิดชีต
Logsตรวจว่ามีรายการ RECEIVED และ REPLIED
| กรณีทดสอบ | คำถาม | ผลลัพธ์ที่ควรได้ |
|---|---|---|
| มีข้อมูล | พรุ่งนี้ใครเวรเช้า? | ตอบชื่อจาก Sheet ตรงวันและกะ |
| ไม่มีข้อมูล | วันที่ที่ไม่มีใน Sheet ใครอยู่เวร? | บอกว่าไม่พบ ไม่เดา |
| คำถามนอกขอบเขต | ช่วยวิเคราะห์หุ้นให้หน่อย | ปฏิเสธหรือบอกขอบเขตหน้าที่ |
| ข้อมูลถูกปิด | ถามถึงแถวที่ Active = FALSE | ไม่ควรนำแถวนั้นมาตอบ |
Step 11เปิดให้บอตเข้ากลุ่มและหา Group ID
- LINE Developers Console → Messaging API เปิด Allow bot to join group chats ซึ่งปกติปิดไว้
- เชิญ LINE OA เข้ากลุ่มทดสอบ โปรดทราบว่าในหนึ่งกลุ่มมี LINE OA ได้เพียงหนึ่งบัญชีในเวลาเดียวกัน
- เมื่อบอตเข้ากลุ่ม ระบบจะบันทึก Event ลง
Logs - พิมพ์ น้องเจน สัปดาห์นี้ใครอยู่เวร?
- คัดลอกค่า SourceID ที่ขึ้นต้นลักษณะ
C...จาก Logs ไปใส่ Script Property ชื่อGROUP_ID
Step 12ตั้งแจ้งเตือนตารางเวรอัตโนมัติ
- ตรวจว่าใส่
GROUP_IDใน Script Properties แล้ว - ใน Apps Script เลือกฟังก์ชัน
sendWeeklyDutyReminderแล้วกด Run หนึ่งครั้ง - ตรวจว่ากลุ่มได้รับข้อความ “ตารางเวร 7 วันข้างหน้า”
- ไปที่เมนู Triggers รูปนาฬิกา → Add Trigger
- Choose function เลือก
sendWeeklyDutyReminder - Event source เลือก Time-driven, เลือก Weekly และกำหนดวัน/ช่วงเวลา
Time-driven trigger ของ Apps Script มักทำงานภายในช่วงเวลาที่เลือก ไม่รับประกันนาทีตรงเป๊ะ หากงานต้องตรงเวลาและมี SLA ควรใช้ Cloud Scheduler ร่วมกับ Backend ที่เหมาะกับ Production
Security และข้อจำกัดที่ต้องรู้ก่อนใช้จริง
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 จาก LINE | Token ผิด หมดอายุ หรือถูก Revoke | ออก Token ใหม่และอัปเดต Script Property |
| 401/403 จาก AI | API 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 version | Manage deployments → Edit → New version |
| Push แจ้งเตือนไม่เข้า | GROUP_ID ผิดหรือบอตออกจากกลุ่มแล้ว | คัดลอก SourceID ใหม่จาก Logs |
Prompt สำหรับส่ง Error ให้ AI ช่วยแก้
อาการ: [อธิบายสิ่งที่เกิด]
สิ่งที่คาดหวัง: [ผลลัพธ์ที่ต้องการ]
Error จาก Apps Script Executions: [วางข้อความ Error โดยลบ Token/API Key ออกแล้ว]
ฟังก์ชันที่เกี่ยวข้อง: [วางเฉพาะโค้ดส่วนที่เกี่ยวข้อง]
ช่วยวิเคราะห์ Root Cause, บอกจุดที่ต้องแก้ และส่งโค้ดฟังก์ชันฉบับแก้ไขกลับมา ห้ามขอหรือแสดง Secret
ต่อยอดจากตารางเวรไปเป็นผู้ช่วยแบบอื่น
Deadline Assistant
อ่าน Content Tracker แล้วตอบว่าวันนี้มีงานอะไรต้องส่ง หรือแจ้งเฉพาะงานที่ใกล้เกิน SLA
Meeting Assistant
เชื่อม Google Calendar เพื่อสรุปประชุมประจำวันและแจ้งเตือนก่อนประชุม
Team Knowledge Bot
ค้น FAQ, SOP และลิงก์เอกสารที่ทีมถามซ้ำ โดยต้องมีสิทธิ์เข้าถึงตามบทบาท
Performance Alert
ดึงข้อมูลจาก Ads API หรือ Dashboard แล้วแจ้งเมื่อ CPA, Budget หรือ Conversion ผิดเกณฑ์
ลำดับการพัฒนาที่แนะนำ
- Phase 1 — Read only: ให้บอตอ่านและตอบข้อมูลอย่างเดียว
- Phase 2 — Alerts: เพิ่มการแจ้งเตือนเมื่อถึงเวลา/เข้าเงื่อนไข
- Phase 3 — Write with confirmation: เพิ่มงานหรือแก้ข้อมูลหลังผู้ใช้กดยืนยัน
- Phase 4 — Production: ย้าย Backend, ตรวจ Signature, ทำสิทธิ์, Queue, Monitoring และ Backup
เริ่มจากงานที่ทีม “ถามซ้ำ เช็กซ้ำ เตือนซ้ำ และค้นซ้ำ” หนึ่งงาน ทำให้ใช้ได้จริง แล้วค่อยขยาย ไม่จำเป็นต้องเริ่มจาก AI Agent ที่ทำได้ทุกอย่าง
แหล่งอ้างอิงทางการ
- LINE: เริ่มต้นใช้งาน Messaging API
- LINE: Channel Access Token และ Webhook URL
- LINE: การใช้งานบอตใน Group Chat
- LINE: Reply, Push และการนับข้อความ
- LINE: การตรวจสอบ Webhook Signature
- Google: Deploy Apps Script เป็น Web App
- Google: Properties Service
- Google: การสร้างและจัดการ Gemini API Key
- Google: Gemini generateContent API
- OpenAI: Text generation ด้วย Responses API