Telegram Serverless ช่วยให้คุณรันโค้ดแบ็คเอนด์สำหรับบอทและ Mini App ได้โดยตรงบนโครงสร้างพื้นฐานของ Telegram โดยไม่ต้องจัดเตรียมเซิร์ฟเวอร์ ไม่ต้องคอยดูแลคอนเทนเนอร์ และไม่ต้องกังวลเรื่องการขยายระบบ (scaling) คุณเพียงแค่เขียน JavaScript โมดูลทั่วไปแล้ว deploy ด้วยคำสั่งเดียว จากนั้น Telegram จะรันโค้ดเหล่านั้นใน V8 sandbox ที่รวดเร็วและแยกส่วน ซึ่งทำงานอยู่ใกล้กับ Bot API และฐานข้อมูลในตัว
หากคุณเคยต้องเชื่อมต่อบอทเข้ากับ VPS, Cloud Function หรือ Hosting Panel เพียงเพื่อตอบรับคำสั่ง /start สิ่งนี้คือสิ่งที่คุณไม่จำเป็นต้องทำอีกต่อไป
ทำไมต้องใช้ Serverless
โดยพื้นฐานแล้ว Telegram Bot คือโปรแกรมที่ตอบสนองต่ออัปเดตต่างๆ ตามปกติคุณจะต้องโฮสต์โปรแกรมนั้นไว้ในที่ที่เปิดใช้งานอยู่ตลอดเวลา เข้าถึงได้ และปลอดภัย แต่ Telegram Serverless จะลบขั้นตอนเหล่านั้นออกไปทั้งหมด:
- ไม่มีโครงสร้างพื้นฐาน: ไม่มีเครื่องให้เช่า ไม่มีระบบที่ต้องคอยอัปเดตหรือตรวจสอบ โค้ดของคุณจะทำงานเมื่อมีการเรียกใช้และปรับขนาดตามบอทของคุณโดยอัตโนมัติ
- มี Credentials ให้พร้อม: Telegram Bot API, ฐานข้อมูล SQLite และ outbound HTTP พร้อมใช้งานสำหรับทุกโมดูลทันที ไม่ต้องติดตั้งอะไรเพิ่ม และไม่ต้องตั้งค่า Credentials
- ประมวลผลเร็วและปลอดภัย: การเรียกใช้แต่ละครั้งจะรันใน V8 isolate ขนาดเบา ซึ่งอยู่ใกล้กับระบบของ Telegram ทำให้การเรียกใช้งาน Bot API และฐานข้อมูลของคุณรวดเร็วและเชื่อถือได้
- Workflow ของนักพัฒนาที่แท้จริง: โปรเจกต์จะอยู่ในโฟลเดอร์บนเครื่องของคุณภายใต้ระบบ Version Control คุณสามารถแก้ไขไฟล์ ดูการเปลี่ยนแปลงที่แน่นอน deploy แบบ atomic และอัปเดต Schema ของฐานข้อมูลพร้อมกับประวัติการแก้ไขได้ เช่นเดียวกับงานพัฒนาอื่นๆ ที่คุณทำอยู่
แนวคิดการทำงาน: โปรเจกต์ทำงานอย่างไร
คุณจะทำงานใน สามส่วน ซึ่งเชื่อมโยงกันอย่างชัดเจน:
| ตำแหน่ง | สิ่งที่อยู่ภายใน |
|---|---|
| โฟลเดอร์โปรเจกต์ | JavaScript โมดูล — Schema, โค้ดที่ใช้ร่วมกัน, ตัวจัดการอัปเดต, Endpoint ของ Mini App และส่วนหน้าของ Mini App (ถ้ามี) |
| ระบบคลาวด์ | สำเนาโมดูลที่ deploy แล้ว, ฐานข้อมูลของบอท และไฟล์ของ Mini App |
tgcloud CLI |
ตัวเชื่อมประสาน — ทำหน้าที่แสดงความแตกต่างและซิงค์ข้อมูลเข้าด้วยกัน |
คุณไม่จำเป็นต้อง SSH เข้าไปที่ไหน คุณเพียงแก้ไขไฟล์ในเครื่อง รันคำสั่ง npx tgcloud push แล้วแพลตฟอร์มจะจัดการที่เหลือให้เอง ทราฟฟิกของบอทจะถูกจัดการโดยโมดูลที่ deploy ไว้ และฐานข้อมูลจะถูกเก็บรักษาไว้ระหว่างการเรียกใช้งาน
โปรเจกต์จะมีโค้ดเพียงสี่ประเภท ทั้งหมดอยู่ภายในโฟลเดอร์ tgcloud/ เดียว:
tgcloud/
├─ endpoints/ # ฟังก์ชันที่ Mini App ของคุณเรียกใช้
├─ handlers/ # ฟังก์ชันที่ตอบสนองต่อประเภทของอัปเดตใน Telegram
├─ lib/ # โค้ดส่วนกลางที่คุณ import ไปใช้ที่ไหนก็ได้
└─ schema.js # ตารางฐานข้อมูลของคุณ
เมื่อมีอัปเดตเข้ามา เช่น ข้อความ, การกดปุ่ม, หรือ Inline Query — Telegram จะส่งอัปเดตนั้นไปยัง Handler ที่ตรงกัน (handlers/message.js, handlers/callback_query.js, …) และเรียกใช้ฟังก์ชัน export เริ่มต้นของมัน ฟังก์ชันนั้นจะสื่อสารกับ Bot API และฐานข้อมูลผ่าน SDK แล้วส่งผลลัพธ์กลับไป นั่นคือวงจรการทำงานทั้งหมด อัปเดตใดที่ไม่มี Handler ที่ตรงกันจะถูกละเว้น ดังนั้นคุณเพียงแค่เพิ่มเฉพาะ Handler ที่ต้องการใช้งานเท่านั้น
การทำ Mini App เป็นทางเลือกเสริม หากบอทของคุณมี Mini App ส่วนหน้าของมันจะอยู่ในโปรเจกต์เดียวกัน ข้างๆ โฟลเดอร์ tgcloud/ โดย Serverless จะโฮสต์ไฟล์ที่ build แล้วและรัน endpoints/ ที่มันเรียกใช้ ดูรายละเอียดที่
Mini App front‑end
เดโมฉบับย่อ
นี่คือเดโมบอทที่ทำงานได้จริง มันจะตอบกลับทุกข้อความและจดจำจำนวนข้อความที่ได้รับจากแต่ละแชท
tgcloud/schema.js
import { table, integer } from 'sdk/db';
export const counters = table('counters', {
chatId: integer('chat_id').primaryKey() ,
seen: integer('seen').notNull().default(0) ,
});
tgcloud/handlers/message.js
import { api, db } from 'sdk';
import { sql } from 'sdk/db';
import { counters } from '../schema.js';
export default async function (message) {
const chatId = message.chat.id;
// แทรกเคาน์เตอร์ หรือเพิ่มค่าหากแชทนี้มีอยู่แล้ว — และรับแถวข้อมูลกลับมาในคำสั่งเดียวผ่าน .returning()
const [row] = await db.insert(counters)
.values({ chatId, seen: 1 })
.onConflictDoUpdate({
target: counters.chatId,
set: { seen: sql`${counters.seen} + 1` },
})
.returning()
.run();
await api.sendMessage({
chat_id: chatId,
text: `สวัสดี! ฉันเห็นข้อความจากคุณแล้ว ${row.seen} ข้อความ`,
});
}
Deploy:
npx tgcloud push # อัปโหลดโมดูล
npx tgcloud migrate # สร้างตาราง `counters`
นี่คือบอทที่ใช้งานได้จริง มีสถานะที่บันทึกไว้ และไม่ต้องใช้เซิร์ฟเวอร์ ทุกอย่างในนี้ — api, db, table() DSL — ได้อธิบายไว้ในหัวข้อด้านล่าง
Serverless เป็นแบ็คเอนด์ทั่วไปสำหรับ Telegram Bots และ Mini Apps ไม่ใช่เทมเพลตสำหรับแอปประเภทใดประเภทหนึ่ง จึงเหมาะอย่างยิ่งสำหรับ:
- Conversational AI Bots ที่ต้องการจัดเก็บสถานะของผู้ใช้แต่ละคนในฐานข้อมูล
- Mini App Backends ที่จัดเก็บข้อมูลผู้ใช้และแสดงผลคอนเทนต์แบบไดนามิก
- เกมและเครื่องมือต่างๆ — รวมถึงลีดเดอร์บอร์ด, แบบทดสอบ และอื่นๆ
- ระบบอัตโนมัติและการเชื่อมต่อ ที่เรียกใช้ HTTP API ภายนอกและส่งผลลัพธ์กลับไปยังแชท
เริ่มต้นใช้งาน
คู่มือนี้จะพาคุณจากโฟลเดอร์ว่างๆ ไปสู่บอทที่ทำงานได้จริง สามารถตอบกลับข้อความและจัดเก็บข้อมูลได้ โดยสมมติว่าคุณได้ติดตั้ง
Node.js
เวอร์ชัน 18 ขึ้นไปแล้ว และมีบอทที่ลงทะเบียนกับ
@BotFather
เรียบร้อยแล้ว เมื่อจบขั้นตอนคุณจะรู้วิธีใช้คำสั่งที่จำเป็นในแต่ละวัน ได้แก่: push, migrate, run, และ status
ก่อนอื่น เปิดใช้งาน Serverless ก่อน ใน @BotFather ให้เปิดบอทของคุณไปที่ Serverless แล้วกดเปิดใช้งาน สิ่งนี้จะปลดล็อกการเข้าถึง CLI token, handlers, library และฐานข้อมูลสำหรับบอทนี้
1. สร้างโปรเจกต์
วิธีที่เร็วที่สุดคือใช้ตัวสร้างโปรเจกต์ (Project Creator) ซึ่งจะสร้างโครงสร้างโปรเจกต์และติดตั้ง CLI ให้โดยอัตโนมัติ:
npm create @tgcloud/bot example_bot
cd example_bot
อาร์กิวเมนต์คือชื่อโฟลเดอร์เป้าหมาย: คุณสามารถใช้ . เพื่อสร้างโครงสร้างในโฟลเดอร์ปัจจุบัน หรือใช้พาธอื่นก็ได้ มันสามารถทำงานในโฟลเดอร์ที่มีอยู่แล้วได้โดยไม่เขียนทับไฟล์เดิมของคุณ
คุณจะได้โปรเจกต์ที่พร้อมแก้ไข:
example_bot/
├─ tgcloud/ # ส่วนที่แพลตฟอร์มรัน
│ ├─ handlers/
│ │ └─ message.js # ตัวอย่าง message handler (ตอบกลับข้อความ)
│ ├─ lib/ # โมดูลที่แชร์กันไว้ที่นี่ (เริ่มแรกจะว่าง)
│ └─ schema.js # ตารางฐานข้อมูล
├─ docs/
│ └─ tgcloud-sdk.md # ข้อมูลอ้างอิง SDK (สำหรับคุณและ AI ของคุณ)
├─ AGENTS.md # คู่มือสำหรับ AI coding assistants
├─ package.json
└─ tgcloud.jsonc # การตั้งค่าโปรเจกต์
ไฟล์ที่สร้างขึ้นมีคำอธิบายในตัว — แต่ละไฟล์จะมีตัวอย่างพร้อมคำแนะนำว่าคุณสามารถทำอะไรต่อไปได้บ้าง
CLI จะติดตั้งลงในโปรเจกต์เป็น dev-dependency ดังนั้นคุณจะรันด้วย npx tgcloud <คำสั่ง> (npx จะหาไฟล์ใน node_modules ของโปรเจกต์) หรือผ่านทาง npm run shortcut ที่สร้างไว้ใน package.json (npm run deploy, npm run status) โดยปกติจะไม่มีคำสั่ง tgcloud ทั่วโลกใน PATH ของคุณ
คุณสามารถติดตั้งแบบ Global ได้ด้วยคำสั่ง npm install -g @tgcloud/cli หากคุณต้องการพิมพ์คำสั่ง tgcloud จากที่ไหนก็ได้ ซึ่งสะดวกสำหรับการรัน tgcloud init ในโฟลเดอร์เปล่าๆ ไม่ว่าจะใช้วิธีไหนคุณก็จะได้โปรเจกต์ที่เหมือนกัน
สร้าง Mini App อยู่หรือเปล่า? เริ่มต้นจากส่วนหน้า (front-end) ของมันก่อน สร้างมันด้วยเครื่องมือที่คุณใช้ปกติ จากนั้นค่อยรันตัวสร้างโปรเจกต์ในโฟลเดอร์นั้น ดูที่ Mini App front‑end
2. เชื่อมต่อบอทของคุณ
ทุกโปรเจกต์ผูกกับบอทตัวเดียว เชื่อมต่อพวกมันด้วยคำสั่ง login ซึ่งจะถามหา CLI access token ของคุณ (
@BotFather
→ บอทของคุณ → Serverless → CLI Access → Access token — ซึ่งเป็นคนละโทเคนกับ
bot’s API token
) และจัดเก็บไว้ในเครื่อง:
npx tgcloud login
โทเคนจะมีรูปแบบเป็น app<id>:<secret> CLI จะเก็บไว้ใน .tgcloud/ ซึ่งถูก git-ignored ไว้ และจะไม่แสดงส่วนที่เป็นความลับออกมา การ Login จะถามแค่ครั้งเดียว — ดูที่
Authentication
สำหรับการตั้งค่าโทเคนใน CI
3. ตรวจสอบสถานะ
คำสั่งสองคำสั่งจะช่วยให้คุณทราบสถานะของโปรเจกต์ได้ตลอดเวลาโดยไม่ต้องออนไลน์:
npx tgcloud status # ดูสิ่งที่เปลี่ยนแปลงในเครื่องเทียบกับที่ deploy ไว้
npx tgcloud diff # ดูความแตกต่างทีละบรรทัด
หลังจาก init ทุกอย่างจะเป็นของใหม่และยังไม่ได้ deploy status จะแสดงไฟล์เริ่มต้นที่รอการอัปโหลดขึ้นไป
4. Deploy
ส่งโมดูลของคุณขึ้นไปยังคลาวด์:
npx tgcloud push
push จะอัปโหลดทุกโมดูลที่มีการเปลี่ยนแปลงในคราวเดียวและอัปเดตบันทึกในเครื่องว่าคลาวด์มีข้อมูลใดอยู่ บอทของคุณจะออนไลน์ทันที: ลองเปิดบอทใน Telegram แล้วส่งข้อความไปดู ตัวอย่าง handler จะตอบกลับข้อความนั้น
การ Deploy จะไม่ไปยุ่งกับฐานข้อมูลของคุณ การส่งโค้ดกับการเปลี่ยน Schema ของฐานข้อมูลเป็นคนละขั้นตอนกัน ดังนั้นการ Deploy โค้ดจะไม่ทำให้ข้อมูลของคุณเสียหายโดยไม่ได้ตั้งใจ
5. เพิ่มตารางฐานข้อมูล
มาทำให้บอทจำอะไรบางอย่างได้บ้าง เปิดไฟล์ tgcloud/schema.js และประกาศตาราง:
import { table, integer, text, sql } from 'sdk/db';
export const messages = table('messages', {
id: integer('id').primaryKey({ autoIncrement: true }) ,
chatId: integer('chat_id').notNull() ,
text: text('text') ,
created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`) ,
});
Deploy schema แล้วนำไปใช้กับฐานข้อมูล:
npx tgcloud push # อัปโหลด schema ใหม่
npx tgcloud migrate # สร้างตาราง `messages`
push จะรายงานว่า schema ไม่ตรงกันและแสดงการเปลี่ยนแปลงที่รอดำเนินการ แต่จะยังไม่ทำอะไร migrate จะพาคุณไปทีละขั้นตอนและเมื่อคุณยืนยัน มันจะสร้างตารางให้ โมเดลสองขั้นตอนแบบนี้ — และสิ่งที่เกิดขึ้นกับการเปลี่ยนแปลงที่มีความเสี่ยงเช่นการลบตาราง — ถูกอธิบายไว้ใน
Migrations
6. จัดเก็บและอ่านข้อมูล
ตอนนี้ใช้ตารางจาก Handler ของคุณ แก้ไขไฟล์ tgcloud/handlers/message.js:
import { api, db } from 'sdk';
import { eq } from 'sdk/db';
import { messages } from '../schema.js';
export default async function (message) {
// บันทึกข้อความนี้
await db.insert(messages)
.values({ chatId: message.chat.id, text: message.text })
.run();
// นับจำนวนข้อความที่เก็บไว้สำหรับแชทนี้
const count = await db.$count(messages, eq(messages.chatId, message.chat.id));
await api.sendMessage({
chat_id: message.chat.id,
text: `บันทึกแล้ว นี่คือข้อความที่ ${count} จากแชทนี้จนถึงตอนนี้`,
});
}
Deploy Handler ที่อัปเดตแล้วด้วย npx tgcloud push จากนั้นลองส่งข้อความให้บอทหลายๆ ครั้งแล้วดูตัวเลขที่เพิ่มขึ้น ฐานข้อมูลจะถูกบันทึกไว้อย่างถาวรระหว่างการรัน นั่นคือความจำของบอทของคุณ
7. ทดสอบโดยไม่ต้อง Deploy
คุณไม่จำเป็นต้อง Deploy เพื่อทดสอบการเปลี่ยนแปลง npx tgcloud run จะรัน Handler บนแพลตฟอร์มโดยใช้ไฟล์ ในเครื่อง ของคุณ โดยไม่ต้องเผยแพร่จริง:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hello" }'
อาร์กิวเมนต์คือเพย์โหลดที่ Handler ของคุณได้รับ — สำหรับ handlers/message คือ
Message
— เขียนในรูปแบบ
JSON5
(คุณสามารถละการใส่เครื่องหมายคำพูดที่คีย์ได้) คำสั่งนี้จะแสดงผลทุกอย่างที่ Handler ได้บันทึกไว้ด้วย console.*, ค่าที่ return ออกมา และเวลาที่ใช้ไป นี่คือวิธีการวนลูปที่เร็วที่สุดในการทดสอบตรรกะ — ไม่ต้อง Deploy ไม่ต้องรอข้อความจริง
8. รักษาสถานะให้ซิงค์กัน
ในขณะที่คุณทำงาน คำสั่งกลุ่มหนึ่งจะช่วยให้โปรเจกต์ในเครื่องและบนคลาวด์ตรงกันเสมอ: npx tgcloud status แสดงสิ่งที่เปลี่ยนไป, npx tgcloud push สำหรับการ Deploy, npx tgcloud pull เพื่อดึงโปรเจกต์ในเครื่องให้ตรงกับคลาวด์, npx tgcloud fetch ตรวจสอบสิ่งที่ Deploy ไว้โดยไม่แตะต้องไฟล์ของคุณ และ npx tgcloud reset ทิ้งการเปลี่ยนแปลงในเครื่อง
หากสองคน (หรือสองเครื่อง) Deploy ไปที่บอทเดียวกัน แพลตฟอร์มจะตรวจพบความขัดแย้งและ
pushจะหยุดรอให้คุณpullก่อน คุณไม่มีทางเผลอเขียนทับงานของคนอื่นได้โดยไม่รู้ตัว ดูที่ Staying in sync
การสร้างด้วย AI
หากคุณชอบทำงานกับผู้ช่วย AI หรือหากนักเขียนโค้ดในทีมคุณคือ AI คุณยังสามารถสร้างบอทได้ เราได้เริ่มต้นก้าวแรกเพื่อให้ AI Agent รู้สึกเหมือนอยู่บ้านในโปรเจกต์: ทุกโปรเจกต์ใหม่จะมีไฟล์ AGENTS.md และเอกสารอ้างอิง docs/tgcloud-sdk.md ที่เครื่องมือเขียนโค้ดของ AI สามารถอ่านได้โดยอัตโนมัติ
ด้วย Runtime ที่มีขนาดกะทัดรัดและครบถ้วนในตัว — SDK ตัวเดียว ไม่มี npm packages ให้วุ่นวาย — ทำให้ Assistant มีจุดเริ่มต้นที่ดีในธรรมเนียมปฏิบัติที่ Generic CodeGen มักจะพลาด เช่น Relative imports ที่มี .js, ไม่มี Foreign keys, ทุกการเรียก db ต้องเป็น Async, หนึ่ง Handler ต่อหนึ่งประเภทอัปเดต และขั้นตอนการทำงานแบบ push/migrate สองขั้นตอน
ลองดูได้เลย:
npm create @tgcloud/bot my-bot
cd my-bot
opencode # หรือ Claude Code, Cursor, … — AI Agent ใดๆ ที่อ่านไฟล์ AGENTS.md ได้
จากนั้นเพียงแค่ถามเป็นภาษาง่ายๆ:
เขียนบอทที่จดจำรายการ To-do ของแต่ละคน — เพิ่มรายการเมื่อส่งข้อความ และแสดงรายการทั้งหมดเมื่อส่ง /list
Assistant จะแก้ไข schema.js และ Handler ให้คุณ คุณเพียงแค่ตรวจสอบ ทดสอบการเปลี่ยนแปลงทันทีด้วย npx tgcloud run แล้วเผยแพร่จริงด้วย npx tgcloud push และ npx tgcloud migrate ไฟล์ AGENTS.md เป็นส่วนหนึ่งของโปรเจกต์ของคุณ — แก้ไขมันเมื่อบอทของคุณเติบโตขึ้นเพื่อให้คำแนะนำยังคงถูกต้องแม่นยำ
จัดการได้ทุกที่ด้วย BotFather
มีเพียงมือถือเครื่องเดียวก็ใช้งานได้? โปรเจกต์ทั้งหมดอยู่ใน @BotFather เช่นกัน — เปิดบอทของคุณ → Serverless แล้วคุณจะได้รับทุกอย่างที่ CLI จัดการได้บนหน้าจอสัมผัส:
- Handlers — สร้าง, แก้ไข และทดสอบรัน Handler ของอัปเดต; BotFather จะคอยซิงค์ Webhook ให้ตรงกับ Handler ที่คุณมี (สถานะเดียวกับ In sync / Out of sync ที่ CLI รายงาน)
- Endpoints — ฟังก์ชันที่ Mini App ของคุณเรียกใช้
- Library — โมดูลส่วนกลาง
lib/ของคุณ - Database — แก้ไข
schema.jsด้วย Syntax แบบ Drizzle ตรวจสอบการเปลี่ยนแปลงที่รอดำเนินการ และกดใช้; การกด Save คือการ Deploy - Mini App Files — ส่วนหน้า (front-end) ที่ผู้ใช้ของคุณเปิดใน Telegram
- CLI Access — ดึง CLI access token ที่นี่เมื่อคุณกลับมาใช้งานคีย์บอร์ด
มันคือโปรเจกต์บนคลาวด์เดียวกัน ดังนั้นคุณสามารถเริ่มเขียน Handler บนมือถือแล้วใช้ npx tgcloud pull มาที่แล็ปท็อปทีหลังได้ ไม่มีอะไรผูกติดกับไคลเอ็นต์เดียว การรัน Handler ยังแสดงผลลัพธ์จาก Console ในแชทได้ทันที เช่นเดียวกับ
npx tgcloud run
โปรเจกต์และโมดูล
โปรเจกต์แบบ Serverless คือโฟลเดอร์ทั่วไปภายใต้ Version Control ทุกอย่างที่แพลตฟอร์มรันจะอยู่ในโฟลเดอร์ tgcloud/ ส่วนที่เหลือของโปรเจกต์เป็นของคุณ — ส่วนหน้าของ Mini-app, การตั้งค่า, เอกสาร ไม่มีขั้นตอนการ Build สำหรับบอท, ไม่มี node_modules ขณะรันไทม์ และไม่มีจุดเริ่มต้นของเซิร์ฟเวอร์
โครงสร้างของโปรเจกต์
example_bot/
├─ tgcloud/ # ส่วนที่ถูก Deploy — ไม่มีอย่างอื่น
│ ├─ handlers/ # Update handlers — แบบแบน ระดับเดียวเท่านั้น
│ │ ├─ message.js
│ │ └─ callback_query.js
│ ├─ endpoints/ # ฟังก์ชันที่ Mini App เรียกใช้ — แบบแบน ระดับเดียวเท่านั้น
│ │ └─ getProfile.js
│ ├─ lib/ # โมดูลส่วนกลาง อนุญาตให้มีโฟลเดอร์ย่อยได้
│ │ ├─ reply.js
│ │ └─ internal/util.js
│ └─ schema.js # Schema ฐานข้อมูล — ไฟล์เดียว
└─ .tgcloud/ # สถานะ CLI, ถูก git-ignored
เฉพาะไฟล์ .js ภายใน tgcloud/ — schema.js, lib/, handlers/, endpoints/ — เท่านั้นที่จะ ถูก Deploy ทุกอย่างที่เหลือ — Markdown, ไฟล์คอนฟิก, โฟลเดอร์ .tgcloud/, อะไรก็ตามนอก tgcloud/ — จะค้างอยู่ในเครื่องของคุณ ชื่อโมดูลจะอ้างอิงจาก tgcloud/ ไฟล์ tgcloud/handlers/message.js คือโมดูล handlers/message และนั่นคือชื่อที่ใช้ในคำสั่ง npx tgcloud run
อัปเกรดจากโปรเจกต์เก่า? โปรเจกต์ที่สร้างก่อน CLI 0.2.0 เก็บไฟล์
schema.js,lib/และhandlers/ไว้ที่รูทของโปรเจกต์ ให้รันnpx tgcloud upgradeหนึ่งครั้งเพื่อย้ายไฟล์เหล่านั้นเข้าไปในtgcloud/
schema.js — ฐานข้อมูลของคุณ มันประกาศตารางเป็น named exports โดยใช้ Schema DSL และอยู่ใน tgcloud/ เป็นไฟล์เดียว มันจะถูก Deploy เหมือนโมดูลอื่นๆ แต่การ Deploy จะไม่ไปเปลี่ยนฐานข้อมูล — การเปลี่ยนแปลง Schema จะทำแยกต่างหากด้วย npx tgcloud migrate ดูที่
The database
lib/ — โค้ดส่วนกลาง สิ่งที่คุณต้องการใช้ซ้ำในหลาย Handler: ตัวช่วย (Helpers), ชั้นเข้าถึงฐานข้อมูล, การจัดรูปแบบ, การเชื่อมต่อกับบริการภายนอก lib/ เป็นไดเรกทอรีเดียวที่อนุญาตให้มี โฟลเดอร์ย่อย (lib/internal/util.js, lib/payments/stripe.js) เพื่อให้คุณจัดระเบียบโค้ดขนาดใหญ่ได้ตามต้องการ โมดูลใน lib/ จะไม่ถูกเรียกโดยตรงจากแพลตฟอร์ม แต่จะมีไว้เพื่อ import โดย Handler และโมดูลอื่นๆ
handlers/ — จุดเริ่มต้นของบอทของคุณ แต่ละไฟล์สอดคล้องกับหนึ่ง Telegram update type และแพลตฟอร์มจะส่งอัปเดตที่เข้ามาไปยัง Handler ที่ตรงกัน:
| ไฟล์ | จัดการ |
|---|---|
handlers/message.js |
ข้อความที่เข้ามาใหม่ |
handlers/inline_query.js |
Inline queries ใหม่ |
handlers/callback_query.js |
Callback queries ใหม่ |
| … | อัปเดตประเภทอื่นๆ ของ Bot API |
handlers/ เป็นแบบ แบน — ไม่มีโฟลเดอร์ย่อย ฟังก์ชันที่ Handler export default คือฟังก์ชันที่แพลตฟอร์มจะเรียกใช้ (ดูที่
Handlers
)
ประเภทของอัปเดตจะถูกจัดการ ก็ต่อเมื่อไฟล์ Handler นั้นมีอยู่และไม่ว่างเปล่า หากไม่มี handlers/<type>.js — หรือไฟล์ว่างเปล่า — อัปเดตประเภทนั้นจะถูกละเว้น และแพลตฟอร์มจะไม่รันอะไรให้มัน ดังนั้นให้เก็บไว้เฉพาะ Handler ที่คุณต้องการจริงๆ ยิ่งเพิ่มมากเท่าไหร่ ก็ยิ่งมีประเภทอัปเดตที่คุณต้องประมวลผลมากขึ้นเท่านั้น และการทิ้งส่วนที่เหลือไว้จะช่วยให้ Telegram ไม่ต้องรันโค้ดของคุณสำหรับอัปเดตที่คุณไม่ได้สนใจอยู่แล้ว
เพื่อสร้าง Handler ใหม่ ให้รัน
npx tgcloud add handlers/<type>
endpoints/ — ฝั่งเซิร์ฟเวอร์ของ Mini App ของคุณ แต่ละไฟล์เป็นฟังก์ชันที่ Mini App เรียกใช้ตามชื่อ ดังนั้น endpoints/getProfile.js จะตอบรับคำขอ POST /api/getProfile แพลตฟอร์มจะตรวจสอบว่าคำขอนั้นมาจากผู้ใช้ Telegram ภายใน Mini App ของคุณก่อนที่โค้ดจะรัน และจะส่งข้อมูลผู้ใช้นั้นมาให้ endpoints/ เป็นแบบ แบน เหมือน handlers/ ดูที่
Endpoints
.tgcloud/ — สถานะภายในเครื่องที่ CLI จัดการทั้งหมด รวมถึง Credentials ที่บันทึกไว้ มันถูก git-ignored ไว้และคุณไม่ควรแก้ไขหรืออ่านไฟล์ในนี้ด้วยตนเอง — ให้ใช้คำสั่ง CLI แทน
ระบบโมดูล
ในขณะรันไทม์ โมดูลจะเห็นเพียงสองอย่างเท่านั้น: Platform SDK และ โมดูลอื่นๆ ในโปรเจกต์ของคุณ ไม่มี npm packages, ไม่มีระบบไฟล์ และไม่มีเครือข่ายยกเว้นผ่าน fetch ของ SDK
Import โมดูลของคุณเองในแบบที่คุณทำในโปรเจกต์ JavaScript ทั่วไป — โดย Relative path พร้อมนามสกุล .js — และ import SDK โดยใช้ชื่อของมัน:
// tgcloud/handlers/message.js
import { users } from '../schema.js'; // โมดูล schema
import { addItem } from '../lib/cart.js'; // โมดูล lib
import { format } from '../lib/internal/fmt.js'; // โมดูล lib ในโฟลเดอร์ย่อย
import { db, api, fetch } from 'sdk'; // Platform SDK
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js'; // โมดูลใกล้เคียง
import { users } from '../schema.js';
Relative import ต้องลงท้ายด้วย .js และไม่สามารถออกนอก tgcloud/:
import { users } from '../schema'; // ต้องเพิ่ม .js
import config from '../../config.js'; // ออกนอก tgcloud/ ไม่ได้
import { z } from 'zod'; // ไม่มี npm packages
ทุกโมดูลยังมี ชื่อ ของมันเอง — คือพาธภายใน tgcloud/ โดยไม่มีนามสกุล เช่น schema, lib/cart หรือ handlers/message นั่นคือชื่อที่ npx tgcloud run และเอาต์พุตของ CLI ใช้ และโมดูลก็สามารถ import ด้วยชื่อนี้ได้เช่นกัน (from 'lib/cart', ไม่ต้องใส่นามสกุล) ควรใช้ Relative imports ในโค้ดของคุณ เพราะเป็นวิธีที่ Editor ของคุณเข้าใจได้ง่าย — ทั้ง Jump-to-definition, Autocomplete และการ Rename
สิ่งที่ใช้งานได้ใน Runtime คือสองอย่างเท่านั้น: sdk และ Submodules ของมัน (sdk/db, sdk/api, sdk/fetch) — คือทั้งหมดของ Platform Surface, ดูที่
The SDK
— และ โมดูลของคุณเอง ใน tgcloud/ นั่นคือรายการทั้งหมด หากโค้ดของคุณ import อย่างอื่น มันจะไม่สามารถ Resolve ได้ ข้อจำกัดนี้ช่วยให้โมดูลโหลดได้เร็วและปลอดภัยต่อการรัน
Handlers
Handler คือโมดูลใน handlers/ ที่มี Default export ซึ่งแพลตฟอร์มจะเรียกใช้เมื่ออัปเดตที่ตรงกันเข้ามา
// handlers/message.js
import { api } from 'sdk';
export default async function (message) {
await api.sendMessage({
chat_id: message.chat.id,
text: `คุณพูดว่า: ${message.text ?? '(ไม่มีข้อความ)'}`,
});
}
Handler จะได้รับ Payload ของอัปเดตเป็นอาร์กิวเมนต์ — แพลตฟอร์มจะแกะ
Update
ของ Telegram ให้คุณ handlers/message.js จะได้รับ
Message
(เช่น update.message); handlers/callback_query.js จะได้รับ
CallbackQuery
และอื่นๆ อาร์กิวเมนต์ ตัวที่สอง ของ Handler คือ Context Object ต่อการรันแต่ละครั้ง ctx ซึ่งมี Raw Update อยู่ใน ctx.update — ให้ใช้มันเมื่อคุณต้องการข้อมูลนอกเหนือจาก Payload เช่น update_id
Handler สามารถเป็น async (โดยปกติจะเป็น) และส่งค่ากลับได้ มันสื่อสารกับ Bot API, ฐานข้อมูล และ HTTP ภายนอกผ่าน SDK
คุณไม่จำเป็นต้องมีอัปเดตจริงเพื่อทดสอบ Handler npx tgcloud run จะรันมันบนแพลตฟอร์มด้วย Payload ที่คุณจัดเตรียมให้และโค้ดในเครื่องปัจจุบันของคุณ:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
อาร์กิวเมนต์คือ Payload — Object เดียวกับที่ Handler ของคุณได้รับ — ในรูปแบบ JSON5 ในการระบุ ctx ของ Handler (อาร์กิวเมนต์ตัวที่สอง) ให้เพิ่ม --ctx, เช่น --ctx '{ update: { update_id: 1 } }' วิธีนี้จะรันกับไฟล์ ในเครื่อง ของคุณ ดังนั้นคุณสามารถลองเปลี่ยนโค้ดก่อน Deploy ได้ ดูที่
run
Endpoints
Endpoint คือโมดูลใน endpoints/ ที่ Mini App ของคุณเรียกใช้ — เป็นจุดสำหรับสิ่งใดก็ตามที่ Front-end ไม่สามารถทำเองได้ เช่น การอ่านและเขียนฐานข้อมูล, การเรียกใช้ Bot API, หรือการคุยกับบริการภายนอกด้วย Secrets ของคุณ
// endpoints/getProfile.js
import { db, EndpointError } from 'sdk';
import { eq } from 'sdk/db';
import { profiles } from '../schema.js';
export default async function (input, ctx) {
const user = ctx.initData.user;
const profile = await db.select().from(profiles).where(eq(profiles.userId, user.id)).get();
if (!profile) throw new EndpointError('ไม่พบโปรไฟล์', { userId: user.id });
return profile;
}
อาร์กิวเมนต์ตัวแรก input คือ JSON object ที่ Mini App ส่งมา อาร์กิวเมนต์ตัวที่สอง ctx จะมี ctx.initData —
Init data
ของ Mini App (user, chat, start_param, auth_date, …) ซึ่ง ตรวจสอบโดยแพลตฟอร์มแล้ว มันมีรูปร่างเหมือน Telegram.WebApp.initDataUnsafe บนไคลเอ็นต์ แต่ที่นี่คุณเชื่อถือมันได้ คำขอที่ไม่มี Init data ที่ถูกต้องจะถูกปฏิเสธด้วย 401 ก่อนที่โค้ดของคุณจะรัน ดังนั้น ctx.initData.user คือผู้ที่เปิด Mini App ของคุณจริงๆ Endpoints จะถูกเรียกผ่านวิธีนี้เท่านั้น — จาก Mini App ของบอทโดยผู้ใช้ Telegram — ดังนั้นจึงไม่มี Public API ที่ต้องป้องกันและไม่มีระบบ Login ที่ต้องสร้าง
ค่าใดก็ตามที่ Endpoint ส่งกลับมาจะถูกส่งกลับเป็น JSON เพื่อปฏิเสธคำขอ throw new EndpointError(description, parameters?) Mini App จะได้รับคำอธิบายและ Object parameters ตามนั้น หากคุณต้องการแยกสาขา Error ที่ Front-end ให้ใส่โค้ดใน parameters — { code: 'OUT_OF_STOCK' } ข้อยกเว้นอื่นๆ จะถูกรายงานเป็น Internal Error ทั่วไป และข้อความจริงจะไปอยู่ใน Logs เท่านั้น ดังนั้นข้อมูลภายในจะไม่หลุดไปถึงไคลเอ็นต์
การเรียก Endpoint จาก Mini App
Mini App จะเรียก Endpoint ตามชื่อด้วย Telegram.WebApp.Serverless.call จาก
Mini Apps SDK
ทางการ — telegram-web-app.js — สคริปต์ที่ Mini App ของคุณรวมไว้อยู่แล้ว มันจะส่ง Init data ให้คุณและ Callback กลับมาพร้อมกับ Error หรือผลลัพธ์ของ Endpoint:
Telegram.WebApp.Serverless.call('getProfile', { lang: 'th' }, function (err, profile) {
if (err) {
// err.message — คำอธิบาย; err.parameters — สิ่งที่ EndpointError ส่งมา
if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
return;
}
render(profile);
});
Input เป็นทางเลือกและต้องเป็น Object ทั่วไป หากไม่มีก็เรียก Serverless.call('getProfile', callback)
err.type จะเป็น 'ENDPOINT_ERROR' เมื่อ Endpoint ของคุณ Throw EndpointError ออกมา ค่าอื่นใดหมายความว่าแพลตฟอร์มปฏิเสธคำขอ หรือ Endpoint ล้มเหลวโดยไม่คาดคิด เช่น 'UNAUTHORIZED' เมื่อ Init data หายไปหรือไม่ถูกต้อง
ชื่อของ Endpoint เหมือนกับ JavaScript Identifiers — ตัวอักษร, ตัวเลข และ _ โดยต้องไม่ขึ้นต้นด้วยตัวเลข (getProfile, place_order) สร้างมันด้วย
npx tgcloud add endpoints/<name>
และลองทดสอบโดยไม่ต้องมี Mini App ด้วย
run
โดยใส่ Init data ด้วยตัวคุณเอง:
npx tgcloud run endpoints/getProfile '{}' --ctx '{ initData: { user: { id: 1, first_name: "Ann" } } }'
สิ่งที่จะถูก Deploy
เมื่อคุณ npx tgcloud push CLI จะรวบรวมทุกไฟล์ .js ภายใต้ schema.js, lib/, handlers/, และ endpoints/ และส่งชุดข้อมูลนั้นไปเป็นพื้นที่โมดูลของโปรเจกต์ของคุณ — พร้อมกับโฟลเดอร์ Build ของ Mini App หาก tgcloud.jsonc ระบุไว้ (ดูด้านล่าง) สิ่งใดที่มีอยู่ในคลาวด์แต่ไม่มีในโปรเจกต์ของคุณจะถูกลบออก ดังนั้นสถานะที่ Deploy จะสะท้อนถึงโฟลเดอร์ของคุณเสมอ — รวมถึงการลบด้วย ไม่มีอะไรภายนอก tgcloud/ ที่จะถูก Deploy เป็นโค้ด — ส่วนที่เหลือของโปรเจกต์เป็นของคุณ ภายใน tgcloud/ ไฟล์ .js ใดที่ไม่ใช่หนึ่งในตำแหน่งเหล่านี้ (tgcloud/util.js, tgcloud/helpers/x.js) จะถูกทำเครื่องหมายไว้ เพื่อไม่ให้มัน Deploy ไปโดยไม่ตั้งใจ Markdown, Dotfiles และ .tgcloud/ จะไม่ถูก Deploy
Mini App front-end
รูทของโปรเจกต์เป็นของคุณ และเป็นที่ที่เหมาะสำหรับ Front-end ของ Mini App — ที่สร้างด้วย React, Vue, Svelte หรือ HTML ธรรมดา โดย Vite หรือ Bundler อื่นๆ Serverless จะโฮสต์ Build output ของมันไว้ข้างๆ บอทของคุณ การ push หนึ่งครั้งจะ Deploy โค้ดและไฟล์ Static ไปพร้อมกัน และแอปจะถูกให้บริการที่ https://app<app_id>.tgcloud.ai/ (push จะแสดงที่อยู่เป๊ะๆ ออกมา — ใช้มันเป็น URL สำหรับ Mini App ใน
@BotFather
)
การรายงานการละเมิด Mini Apps บน tgcloud.ai เผยแพร่โดยนักพัฒนาอิสระ เพื่อรายงานเนื้อหาผิดกฎหมายหรือการละเมิดบน subdomain
app<app_id>.tgcloud.aiให้ส่งอีเมลไปที่ [email protected]
ในการเริ่มต้น ให้สร้าง Front-end ก่อนด้วยเครื่องมือที่คุณชอบ แล้วค่อยเพิ่ม Serverless เข้าไป:
npm create vite@latest example_app # หรือ create-vue, …
cd example_app
npm create @tgcloud/bot .
ตัวสร้างโปรเจกต์จะไม่เขียนทับไฟล์ของคุณ มันจะเพิ่มโฟลเดอร์ tgcloud/ ไว้ข้างๆ Front-end, รู้จักโฟลเดอร์ Build และเพิ่มสคริปต์ npm run deploy ที่จะ Build แอปแล้ว Deploy ไปพร้อมกับบอท
สิ่งที่โฮสต์จะถูกกำหนดค่าใน tgcloud.jsonc ซึ่งเป็นไฟล์คอนฟิกที่รูทของโปรเจกต์:
{
"$schema": "./node_modules/@tgcloud/cli/schema/tgcloud.json",
"static": {
"source": "dist",
"spa": true,
"immutable": ["/assets/*"],
"headers": [{ "path": "/*", "set": { "Referrer-Policy": "no-referrer" } }],
"redirects": [{ "from": "/old", "to": "/new", "status": 301 }]
}
}
ต้องระบุ "source" เสมอ ซึ่งเป็นโฟลเดอร์ Build ที่อ้างอิงจากรูทของโปรเจกต์ Key อื่นๆ ของ "static" จะกำหนดวิธีการให้บริการ (ด้านล่าง) init จะสร้าง tgcloud.jsonc ให้คุณ เมื่อมันพบ Front-end มันจะกรอก "source" ให้ด้วย:
| Front‑end | init ตั้ง "source" เป็น |
|---|---|
| Vite — React, Vue, Svelte, Solid, … | dist |
| Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular | ไม่มี — ต้องตั้งค่า Static export ก่อน แล้วค่อยระบุ "source" เป็นโฟลเดอร์ของมันเอง |
สำหรับการตั้งค่าอื่นๆ ให้ระบุ "source" เป็นโฟลเดอร์ Build ด้วยตัวเอง
บรรทัด $schema ช่วยให้ Editor ของคุณมี Autocomplete และตรวจสอบความถูกต้องของ Key ต่างๆ ได้ ไฟล์นี้เป็น JSON ที่อนุญาตให้ใส่ Comment และเครื่องหมายคอมม่าปิดท้ายได้ หากเครื่องมืออื่นต้องอ่านเป็น JSON ปกติ ให้ตั้งชื่อเป็น tgcloud.json — ซึ่งต้องเป็น JSON ที่เข้มงวด
Build ก่อน Push push จะอัปโหลดโฟลเดอร์ตามที่มีอยู่บนดิสก์ — ยกเว้นไฟล์และโฟลเดอร์ที่ชื่อขึ้นต้นด้วยจุดซึ่งจะถูกข้าม — และไม่เคยรัน Build ของคุณ ดังนั้นการรัน npx tgcloud push เปล่าๆ หลังจากแก้ไข Front-end จะเป็นการ Deploy Build เดิม ให้ใช้ npm run deploy (มันจะรันสคริปต์ build ของคุณ แล้วค่อย tgcloud push) หรือ Build เองก่อน
ตัวเลือกการให้บริการ:
spa— ให้บริการindex.htmlสำหรับพาธที่ไม่มีอยู่จริงและไม่มีนามสกุลไฟล์ เพื่อให้ Client-side router ทำงานได้เมื่อโหลดใหม่ พาธที่มีนามสกุลจะยังคงได้404immutable— รูปแบบสำหรับไฟล์ที่มี Content-hashed (เช่น/assets/*ของ Vite) ซึ่งเบราว์เซอร์อาจแคชไว้หนึ่งปี ใช้สำหรับไฟล์ที่มี Content-hashed เท่านั้น ไฟล์อื่นที่ตรงกับกฎนี้จะค้างอยู่ในเบราว์เซอร์หนึ่งปีและไม่มีวิธีดึงกลับheaders— Response headers สำหรับพาธที่กฎตรงกัน (พาธเป๊ะๆ หรือ Prefix ที่ลงท้ายด้วย/*) ค่าnullจะเป็นการลบ Header นั้นredirects— กฎการเปลี่ยนเส้นทาง (Redirect) โดยอันแรกที่ตรงจะชนะ ไฟล์ที่มีอยู่จะชนะกฎเสมอstatusจะเป็น301ยกเว้นคุณจะตั้งค่าเป็นอย่างอื่น"static": "dist"— รูปแบบย่อสำหรับ{ "source": "dist" }เมื่อไม่ต้องตั้งค่าอย่างอื่น"static": false— ลบไซต์ที่ Deploy ไว้ออกในการ Push ครั้งถัดไป การลบ Key นี้ทิ้งเพียงแค่หยุดการจัดการไฟล์ Static ของโปรเจกต์นี้ และสิ่งที่ Deploy ไว้จะยังคงอยู่
ไซต์จะถูกให้บริการจากรูทของ Host นั้น ดังนั้น base (/) เริ่มต้นของ Bundler ของคุณจึงถูกต้องแล้ว Telegram Web จะเปิด Mini Apps ใน iframe ดังนั้นอย่าใส่ X-Frame-Options หรือนโยบาย frame-ancestors ที่เข้มงวดเกินไป
Host เดียวกันนี้จะตอบรับ
Endpoints
ของคุณภายใต้ /api/ — นั่นคือที่ที่ Telegram.WebApp.Serverless.call('getProfile', …) วิ่งไป อย่าใส่ไฟล์ของคุณไว้ใต้ /api/ ในโฟลเดอร์ Build
ฐานข้อมูล
บอททุกตัวมีฐานข้อมูลของตัวเอง — Store ที่ใช้ SQLite ซึ่งบันทึกไว้อย่างถาวรระหว่างการรันและใช้งานได้สำหรับทุกโมดูลผ่าน db คุณอธิบายตารางใน schema.js ด้วย DSL แบบระบุชนิดข้อมูล (Typed DSL) คุณอ่านและเขียนด้วย Query Builder และวิวัฒนาการมันด้วย Migrations ที่ตรวจสอบแล้ว
รู้จัก Drizzle ORM ไหม? ถ้าใช่ คุณรู้วิธีคุยกับฐานข้อมูลที่นี่แล้ว Schema DSL และ Query Builder ทำตาม Drizzle — Column builders,
select().from().where(), Operators,onConflictDoUpdate,.returning(), และsqltag ทั้งหมดทำงานตามที่คุณคาดหวัง ดังนั้นการอ่านเขียนข้อมูลจึงเป็น API ที่คุ้นเคยอยู่แล้ว คุณเพียงแค่ Import จากsdk/dbและสิ่งที่เฉพาะตัวของแพลตฟอร์ม (โดยเฉพาะ ไม่มี Foreign keys ) จะถูกเน้นย้ำไว้
การประกาศตาราง
ตารางเป็น Named exports ใน schema.js การเรียก table() จะสร้างคำอธิบายในช่วงโหลด (มันไม่ไปแตะฐานข้อมูล) แพลตฟอร์มจะค้นหาตารางที่ Export ออกมาเมื่อคุณ Deploy schema.js และ Migrate ฐานข้อมูลให้ตรงกัน
import { table, integer, text, boolean, json, index, sql } from 'sdk/db';
export const users = table('users', {
id: integer('id').primaryKey({ autoIncrement: true }) ,
tgId: integer('tg_id').unique() ,
name: text('name').notNull() ,
lang: text('lang').default('en') ,
isAdmin: boolean('is_admin').default(false) ,
prefs: json('prefs') ,
created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`) ,
}, (t) => ({
createdIdx: index('idx_users_created').on(t.created) ,
}));
table(name, columns, extras?):
nameคือชื่อตารางใน SQLcolumnsคือ Map ของ JS property → นิยามของ Columnextrasคือ Callback ทางเลือก(t) => ({ … })ซึ่งtคือการเปิดเผย Column (t.createdคือการอ้างอิงถึง Column นั้น) — ประกาศ Indexes และ Constraints ระดับตารางที่นี่
ประเภทของ Column
| Factory | ประเภท SQLite | หมายเหตุ |
|---|---|---|
text() |
TEXT | |
integer() |
INTEGER | |
real() |
REAL | alias float() |
numeric() |
NUMERIC | |
blob() |
BLOB | อ่าน/เขียน Uint8Array |
boolean() |
INTEGER | เก็บเป็น 0/1, อ่านเป็น true/false |
json() |
TEXT | auto JSON.stringify / JSON.parse |
อาร์กิวเมนต์ชื่อ Column เป็นทางเลือก — หากละไว้จะใช้ JS key แทน ตัวเลือก
modeจะควบคุมวิธีการแปลงค่าระหว่าง SQLite และ JavaScript
mode |
เก็บเป็น | ค่าใน JS |
|---|---|---|
boolean |
INTEGER 0/1 | boolean |
json |
TEXT (JSON) | object/array ใดก็ได้ |
timestamp |
INTEGER (วินาที unix) | Date |
timestamp_ms |
INTEGER (ms unix) | Date |
bytes |
BLOB | Uint8Array |
boolean() และ json() เป็นรูปแบบย่อของ integer(name, { mode: 'boolean' }) และ text(name, { mode: 'json' })
blob() อ่านและเขียน Uint8Array — Runtime ไม่มี Node Buffer (และ Buffer เป็น Subclass ของ Uint8Array ดังนั้นนี่คือ Type พื้นฐาน) mode ข้างต้นควบคุมเฉพาะวิธีที่ค่า ถูกเข้ารหัส โดยอิสระจากประเภทที่จัดเก็บของ Column: ดังนั้น blob('col', { mode: 'json' }) จึงเป็นคู่ขนานของ json() ในรูปของ BLOB — คือการเข้ารหัส JSON เดียวกัน แต่เก็บใน Column BLOB แทน TEXT
สรุป:
blob()ใช้Uint8Arraymodeควบคุมการเข้ารหัส ไม่ใช่การจัดเก็บ ดังนั้นblob(..., { mode: 'json' })เก็บ JSON เป็น BLOB ในขณะที่json()เก็บเป็น TEXT
Column modifiers
Column modifiers เชื่อมต่อต่อท้าย Column
integer('id').primaryKey({ autoIncrement: true })
text('name').notNull()
text('tg').unique()
text('lang').default('en')
integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`)
text('slug').generatedAlwaysAs(sql`lower(name)`, { mode: 'stored' }) // หรือ 'virtual'
text('email').deprecated('ถูกแทนที่โดย login') // ทำเครื่องหมายเพื่อลบในอนาคต
.default()บน Columnjson()จะเข้ารหัสค่าให้คุณ; ค่า default แบบsql`…`จะถูกส่งผ่านไปตรงๆ.deprecated()คือจุดสิ้นสุด — ดูที่ Migrations
Indexes และ Constraints
Indexes และ Constraints ประกาศใน extras callback ซึ่ง Column ต่างๆ อยู่ใน Scope
table('t', { /* … */ }, (t) => ({
uq: unique('uq_email').on(t.email) ,
chk: check('chk_done', sql`${t.done} in (0, 1)`) ,
idx: index('idx_name').on(t.col) ,
uidx: uniqueIndex('uidx_email').on(t.email) ,
lower: index('idx_lower').on(sql`lower(${t.email})`) , // Expression index
active: index('idx_active').on(t.userId).where(sql`done = 0`) , // Partial index
}));
Table‑level modifiers ต่อท้าย table(...): .strict(), .withoutRowid(), .deprecated('เหตุผล')
ไม่มี Foreign keys
Runtime ทำงานโดยปิด PRAGMA foreign_keys เอาไว้ Foreign key ที่ประกาศไว้จะไม่มีผลโดยสิ้นเชิง — ไม่มี Cascades, ไม่มีการป้องกัน Orphan ซึ่งแย่กว่าการไม่มีเลย ดังนั้น DSL จึงทำให้เป็นไปไม่ได้ โดยเฉพาะ .references() และ foreignKey() ระดับตาราง จะ Throw Error เมื่อประกาศ และ Schema ที่ใช้งานมันจะไม่ Deploy
คุณควรสร้างความสัมพันธ์ด้วย Column ธรรมดา (userId: integer('user_id')) และบังคับใช้ Integrity ในโค้ดแอปพลิเคชันของคุณ: ใส่ข้อมูล Parent ก่อน Child, ลบ Child ก่อน Parent, จัดการ Error ให้ถูกต้อง และกวาด Orphan ด้วย LEFT JOIN … WHERE parent.id IS NULL เมื่อจำเป็น
การไม่มี Foreign keys เป็นข้อจำกัดที่ตั้งใจ ไม่ใช่มองข้าม วางแผนรองรับเรื่องนี้ตั้งแต่ตอนออกแบบบอท
การทำ Query
db คือ Fluent query builder ทุก Query เป็นแบบ Asynchronous — เมธอดสุดท้าย (.all(), .get(), .values(), .run()) จะคืนค่าเป็น Promises ดังนั้น ต้อง await เสมอ
import { db } from 'sdk';
import { eq, and, desc, asc, count, sql } from 'sdk/db';
import { users, todos } from '../schema.js';
await db.select().from(todos).all(); // ทุกแถว
await db.select().from(todos).where(eq(todos.id, 1)).get(); // แถวแรก หรือ null
await db.select().from(todos).values(); // แถวข้อมูลในรูปแบบอาเรย์
await db.select().from(todos)
.where(and(eq(todos.userId, uid) , eq(todos.done, false)))
.orderBy(desc(todos.priority) , asc(todos.id))
.limit(10).offset(20)
.all();
// Custom projection: { alias: columnRef | sqlExpr | aggregate }
await db.select({ id: todos.id, title: todos.text, n: count() })
.from(todos).groupBy(todos.userId).having(sql`count(*) > ${1}`).all();
// นับแถว — เป็น Helper ไม่ใช่ builder terminal:
await db.$count(todos); // ทุกแถว
await db.$count(todos, eq(todos.done, false)); // พร้อมฟิลเตอร์
- Chainable:
.where(),.orderBy(),.limit(),.offset(),.groupBy(),.having(),.distinct() - Terminals:
.all(),.get(),.values() - นับแถวด้วย
db.$count(table, where?)(หรือcount()ใน projection)
Insert, update, delete:
await db.insert(todos).values({ userId: 1, text: 'ซื้อนม' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run(); // Batch
await db.insert(todos).values({ text: 'X' }).returning().run(); // RETURNING *
await db.insert(users).values({ tgId: 42, name: 'Ann' })
.onConflictDoUpdate({ target: users.tgId, set: { name: 'Ann' } }).run();
await db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run();
await db.delete(todos).where(eq(todos.id, 1)).run();
โปรดทราบว่า Batch insert คือคำสั่งเดียว จึงถูกจำกัดโดยตัวแปรของ SQLite (
แถว × คอลัมน์); หากเกินขีดจำกัด การ Insert จะล้มเหลว — ให้แบ่งข้อมูลเป็นส่วนๆ เอง
Operators ถูก Import จาก sdk/db:
import {
eq, ne, gt, gte, lt, lte,
like, notLike,
isNull, isNotNull, and, or, not,
between, notBetween, inArray, notInArray,
count, sum, avg, min, max,
asc, desc,
} from 'sdk/db';
.where(a, b) ที่มีอาร์กิวเมนต์หลายตัวจะเหมือนกับ and(a, b) อาร์กิวเมนต์ตัวที่สองของการเปรียบเทียบจะเป็นค่าตามปกติ แต่อาจเป็นอีก Column หรือ sql`…` ก็ได้ — เช่น eq(a.x, b.y) Aggregates (count/sum/avg/min/max) คือ SQL Fragments สำหรับ Projection แบบ .select({ … })
Raw SQL — เมื่อตัวสร้างไม่เพียงพอ ให้ใช้ Raw SQL โหมดจะถูกเลือกโดยเมธอด: db.run สำหรับเขียน, db.all สำหรับหลายแถว, db.get สำหรับแถวเดียว, db.values สำหรับแถวในรูปแบบ Positional arrays
await db.run('UPDATE todos SET done = 1 WHERE id = :id', { ':id': 5 });
// → { rowsAffected: 3, lastInsertRowid: 0, rows: [] }
await db.all(sql`SELECT * FROM todos WHERE done = ${false}`);
await db.get(sql`SELECT count(*) AS c FROM todos`);
await db.values(sql`SELECT id, text FROM todos`); // → [[1, 'ซื้อนม']]
คำสั่งเขียนจะคืนค่า Run result — rowsAffected, lastInsertRowid, และ rows ซึ่งมี Clause RETURNING หาก SQL นั้นมีมาให้ ใช้ db.values เมื่อ Query โปรเจกต์ข้อมูลสอง Column ที่มีชื่อเหมือนกัน (Join เช่น SELECT u.id, o.id): db.all จะใช้ชื่อ Column เป็นคีย์ ดังนั้น id ตัวที่สองจะเขียนทับตัวแรก
แท็ก sql`…` จะเปลี่ยน ${value} เป็น Bound parameter และ ${table.column} เป็น Identifier และเชื่อม Fragment ของ sql ที่ซ้อนกันได้ ใช้ sql.raw('…') สำหรับค่า Literal ที่ไม่มี Parameter
Raw queries ไม่ได้ผูกกับตาราง ดังนั้นแถวข้อมูลจะกลับมาโดยไม่มีการแปลงโหมด — Boolean จะเป็น
0/1, Column JSON จะเป็น String, Timestamp จะเป็นตัวเลข เฉพาะ Builder ที่ผูกกับตารางเท่านั้นที่จะแปลงค่าให้
Migrations
ฐานข้อมูลของคุณเปลี่ยนแปลงไปตามการเติบโตของบอท แพลตฟอร์มช่วยให้เรื่องนี้ปลอดภัยโดยการแยก การ Deploy โค้ด ออกจาก การเปลี่ยนข้อมูล และจัดประเภทการเปลี่ยนแปลง Schema แต่ละอย่างตามความเสี่ยง
การ Deploy จะไม่ไปแตะต้องฐานข้อมูล เมื่อคุณ npx tgcloud push ไฟล์ schema.js ที่เปลี่ยนไป แพลตฟอร์มจะบันทึก Schema ใหม่และแจ้งให้คุณทราบว่าฐานข้อมูล จะ เปลี่ยนแปลงอย่างไร — แต่จะไม่มีการนำไปใช้:
npx tgcloud push # deploy schema.js; รายงานการเปลี่ยนแปลง DB ที่รออยู่
npx tgcloud migrate # ตรวจสอบและนำไปใช้
npx tgcloud migrate จะคำนวณความแตกต่างระหว่าง Schema ของคุณกับฐานข้อมูลจริง และพาคุณไปทีละขั้นตอน คุณจะถูกถามเสมอก่อนที่จะมีการนำไปใช้จริง หมายความว่าการ Deploy โค้ดตามปกติจะไม่ทำให้เกิดการ Migrate ข้อมูลโดยไม่ตั้งใจ
การเปลี่ยนแปลงที่รอดำเนินการแต่ละอย่างจะมีสถานะที่กำหนดว่า migrate จะจัดการอย่างไร:
| สถานะ | ความหมาย | ใน migrate |
|---|---|---|
| safe | เพิ่มเติมและไม่ปิดกั้น — ตาราง, คอลัมน์ หรือ Index ใหม่ | นำไปใช้ร่วมกันในขั้นตอนเดียว เมื่อกดยืนยัน |
| warning | อาจทำลายข้อมูลหรือช้า — การลบบางอย่าง หรือ Index บนตารางขนาดใหญ่ | แสดงทีละอย่าง แต่ละอย่างต้องกดยืนยันแยกกัน |
| manual | ทำโดยอัตโนมัติไม่ได้ — เช่น เปลี่ยนชนิดของคอลัมน์ | แสดงคำแนะนำ; คุณต้องทำด้วยตัวเอง |
| undocumented | มีอยู่ในฐานข้อมูลแต่ไม่มีใน Schema ของคุณ | แสดงเพื่อรับทราบ; ไม่นำไปใช้ |
Safe changes จะรวดเร็วและย้อนกลับได้ในเชิงแนวคิด ดังนั้นจึงผ่านไปพร้อมกัน Warning แต่ละอย่างต้องได้รับการยืนยันแยกกัน — ไม่มีการ “กดยอมรับทั้งหมด” สำหรับการเปลี่ยนแปลงที่อาจทำลายข้อมูล
Manual changes มาพร้อมกับเหตุผลและวิธีการที่แนะนำ migrate จะจบลงด้วยสรุป: จำนวนการเปลี่ยนแปลงที่นำไปใช้, ข้าม, รอการแก้ไขด้วยตนเอง, หรือไม่มีใน Schema ของคุณ
ดู
migrateสำหรับ Flag ต่างๆ (--dry-run,--safe,--yes,--local)
การลบสิ่งต่างๆ
การลบตารางหรือคอลัมน์จาก schema.js จะไม่ ลบข้อมูลจริง — เพราะนั่นอาจทำให้เกิดการลบโดยไม่ตั้งใจที่รุนแรง ในการลบบางอย่าง ให้ทำเครื่องหมายว่า Deprecated:
// ลบคอลัมน์
text('email').deprecated('ถูกแทนที่โดย login')
// ลบทั้งตาราง
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('ไม่ได้ใช้งาน');
ในการ migrate ครั้งถัดไป วัตถุที่ Deprecated จะปรากฏเป็นสถานะ warning ให้คุณยืนยันการลบทีละรายการ เมื่อลบแล้วค่อยเอาประกาศนั้นออก
การเปลี่ยนชนิดของคอลัมน์
การเปลี่ยนชนิดข้อมูลเป็น Manual — SQLite ไม่สามารถทำโดยตรงได้เสมอไป และการแปลงค่าที่มีอยู่เป็นเรื่องของการตัดสินใจ migrate จะแสดงการเปลี่ยนแปลงและเหตุผล; ให้ทำด้วยตัวเองด้วย Raw SQL (db.run(...)) โดยทั่วไปคือการสร้างคอลัมน์หรือตารางใหม่, คัดลอกข้อมูล, แล้วสลับกัน
SDK
ใน Runtime โมดูลจะมี Library เดียวคือ sdk มันรวบรวมสามสิ่งที่แบ็คเอนด์บอทต้องการ — ฐานข้อมูล, Telegram Bot API, และ HTTP ภายนอก — โดยไม่ต้องติดตั้งอะไรและไม่ต้องตั้งค่า Credentials ฐานข้อมูล (db) ได้ครอบคลุมใน
The database
ส่วนหัวข้อนี้จะครอบคลุม api, fetch และ Global console
import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk'; // ทั้งหมด
// หรือจาก Submodules:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
| Import | มันคืออะไร |
|---|---|
db |
ฐานข้อมูล — Query builder และ Schema DSL → The database |
api |
Telegram Bot API — api.sendMessage(...) →
ด้านล่าง
|
fetch |
Outbound HTTP → ด้านล่าง |
InputFile |
ไฟล์ (bytes + name) สำหรับอัปโหลด — ทำงานได้กับ api และ fetch →
Files
|
EndpointError |
ปฏิเสธคำขอจาก Mini App ด้วยข้อความที่แสดงได้ → Endpoints |
Import โมดูลโปรเจกต์ของคุณเองด้วย Relative path พร้อมนามสกุล .js (from '../schema.js', from '../lib/cart.js') ดูที่
The module system
The Bot API
api ให้คุณเข้าถึง
Telegram Bot API
ได้ทั้งหมด เรียกใช้เมธอดใดก็ได้ด้วย api.<method>(params) ทุกเมธอด Bot API ปัจจุบัน — และในอนาคต — ใช้งานได้โดย ไม่ต้องอัปเดต SDK
import { api } from 'sdk';
const me = await api.getMe(); // → ผลลัพธ์ที่แกะออกมาแล้ว
await api.sendMessage({ chat_id: id, text: 'สวัสดี!' });
await api.editMessageText({ chat_id, message_id, text: 'อัปเดตแล้ว' });
await api.answerCallbackQuery({ callback_query_id, text: 'เรียบร้อย' });
Response envelope จะถูกแกะออกมา ปกติ Bot API จะห่อผลลัพธ์ใน { ok: true, result: … } api จะส่งคืน result โดยตรง — getMe() จะได้ Object ผู้ใช้ ไม่ใช่ตัวห่อหุ้ม พารามิเตอร์ใช้ชื่อแบบ snake_case ตาม Bot API (chat_id, message_id, reply_markup, …)
ความล้มเหลวจะ Throw BotApiError เมื่อ Bot API ส่งคืน { ok: false } การเรียกจะ Throw BotApiError แทนที่จะส่งค่าที่เป็น Falsy เพื่อป้องกันไม่ให้คุณเผลอละเลยมัน Error จะมี .code (Bot API error_code), .description (ข้อความอ่านง่าย), .method (เมธอดที่ล้มเหลว) และ .parameters (ข้อมูลเสริม เช่น retry_after ใน 429, หรือ migrate_to_chat_id) ให้ Catch เพื่อจัดการความล้มเหลวที่คาดหวังและ Rethrow ที่เหลือ:
import { api, BotApiError } from 'sdk';
try {
await api.deleteMessage({ chat_id, message_id });
} catch (e) {
if (e instanceof BotApiError && e.code === 400) {
// 400 = ข้อความถูกลบไปแล้ว; ในกรณีนี้ถือว่าปกติ
} else {
throw e;
}
}
Files
จัดการไฟล์ด้วย Bytes ไม่ใช่แค่ file_id — อัปโหลดไฟล์ใหม่และดาวน์โหลดไฟล์ที่มีอยู่
อัปโหลด — InputFile ห่อหุ้ม Bytes ไว้ใน InputFile (ตัวเก็บข้อมูล: bytes, ชื่อไฟล์ และ MIME type ทางเลือก) แล้วส่งไปที่เมธอดของ Bot API ที่ต้องการไฟล์:
import { api, InputFile } from 'sdk';
const doc = new InputFile(bytes, 'report.pdf', { type: 'application/pdf' });
await api.sendDocument({ chat_id, document: doc });
// ไฟล์ที่ซ้อนใน Params ก็ใช้ได้ — เช่น Album:
await api.sendMediaGroup({ chat_id, media: [
{ type: 'photo', media: new InputFile(a, 'a.jpg', { type: 'image/jpeg' }) },
{ type: 'photo', media: new InputFile(b, 'b.jpg', { type: 'image/jpeg' }) },
]});
Bytes จะถูก Stream ไปที่ Telegram เป็น Multipart upload จริง — ไฟล์ไม่ได้ถูกบัฟเฟอร์จนเต็มก่อนส่ง — แต่ขีดจำกัดของ Telegram Bot API ยังคงใช้ได้: บอทปัจจุบันอัปโหลดได้สูงสุด 50 MB ต่อไฟล์ (และ 10 MB สำหรับรูปภาพ)
ดาวน์โหลด — ด้วย file_id สองตัวช่วยจะดึงข้อมูล Bytes ของไฟล์กลับมา แพลตฟอร์มจะ Resolve file_id ให้คุณ (มันรัน getFile และดึงเนื้อหาเบื้องหลังให้) ดังนั้นคุณไม่ต้องแตะ Bot Token หรือ Download URL:
const bytes = await api.getFileContent(file_id); // → Uint8Array (ไฟล์ทั้งหมด)
const { file, body } = await api.getFileStream(file_id); // สตรีมแทน
file.file_unique_id; // ข้อมูลของ getFile
for await (const chunk of body) { /* Uint8Array */ } // หรือบัฟเฟอร์มัน: await stream.bytes()
Bot API ของ Telegram จำกัดการดาวน์โหลด
getFileไว้ที่ 20 MB นั่นคือขีดจำกัดสำหรับทั้งสองตัวช่วย
HTTP
fetch คือไคลเอ็นต์แบบ fetch สำหรับเรียกใช้โลกภายนอก — Third-party APIs, Webhooks, อะไรก็ตามผ่าน HTTP
import { fetch } from 'sdk';
const res = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Pavel' }) ,
});
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();
Response จะสะท้อนถึง Web Platform: res.status, res.statusText, res.ok (จริงสำหรับ 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()) และตัวอ่าน Body await res.json() / await res.text()
คุณสามารถอ่าน Body ทีละส่วนเป็น Stream ได้ —
for await (const chunk of res.body) { … }— ซึ่งเป็นวิธีที่คุณใช้รับ Server-sent events หรือเอาต์พุตแบบทีละ Token จาก AI APIs
ตัวช่วย Body จะตั้ง Content-Type ที่ตรงกันให้คุณ:
await fetch(url, { method: 'POST', body: fetch.body.json({ a: 1 }) }); // application/json
await fetch(url, { method: 'POST', body: fetch.body.form({ a: 1 }) }); // x-www-form-urlencoded
await fetch(url, { method: 'POST', body: fetch.body.text('hi') }); // text/plain
Request bodies ยังสามารถบรรจุ ไฟล์และ Bytes ดิบ: ส่ง Uint8Array, InputFile, หรือ FormData (สำหรับ multipart/form-data) เป็น body และ Bytes จะถูก Stream ออกไป — ดังนั้นคุณสามารถอัปโหลดไปยัง API ภายนอกได้เช่นเดียวกับที่ส่งไฟล์ให้ Telegram:
import { fetch, FormData, InputFile } from 'sdk';
const form = new FormData();
form.append('file', new InputFile(bytes, 'photo.jpg', { type: 'image/jpeg' }));
form.append('caption', 'จากบอทของฉัน');
const res = await fetch('https://api.example.com/upload', { method: 'POST', body: form });
มันทำงานเหมือน fetch มาตรฐานที่คุณรู้จัก โดยมี ข้อจำกัดสองประการ:
- Response อ่านเป็นข้อความ —
await res.json()/await res.text()หรือสตรีมทีละส่วนด้วยres.body - Response รวมจำกัดที่ 30 MB ขีดจำกัดนี้ครอบคลุม Response ทั้งหมด — การสตรีมด้วย
res.bodyช่วยให้คุณประมวลผล Body ขนาดใหญ่ทีละส่วนได้ แต่มันไม่ได้เพิ่มขีดจำกัด
การ Logging
Global console มาตรฐานมีให้ใช้งาน — ไม่ต้อง Import มันอยู่ที่นั่นเหมือนใน JavaScript ทั่วไป เอาต์พุตของมันถูกจับและแสดงโดย npx tgcloud run ซึ่งทำให้เป็นเครื่องมือ Debug หลักของคุณระหว่างพัฒนา
console.log('กำลังประมวลผล', { chatId: id }); // log / debug — ธรรมดา
console.info('เริ่มทำงาน'); // info — สีฟ้า
console.warn('จำกัดอัตรา'); // warn — สีเหลือง
console.error(err); // error — สีแดง, พร้อม Stack trace
แต่ละบรรทัดจะถูกแท็กด้วย [file:line] ที่มันมาจาก console.error และ console.trace จะเพิ่ม Stack เต็มๆ ในขณะที่ console.warn จะไม่เพิ่ม เมื่อคุณ npx tgcloud run โมดูล บรรทัดเหล่านี้จะพิมพ์ด้วย Prefix สีตามระดับ, เวลาตั้งแต่เริ่มรัน และจุดกำเนิด — ดู
run
Command-line interface
tgcloud คือสะพานเชื่อมระหว่างโฟลเดอร์โปรเจกต์ของคุณและคลาวด์ มันใช้สร้างโปรเจกต์, แสดงสิ่งที่เปลี่ยน, Deploy, รันโมดูล และ Migration ฐานข้อมูล มันต้องการ
Node.js
18 ขึ้นไป มีสองวิธีในการติดตั้ง:
# แนะนำ — สร้างโปรเจกต์พร้อม CLI ที่ติดตั้งในนั้น:
npm create @tgcloud/bot example_bot
# หรือติดตั้ง CLI ทั่วโลกและ Init โฟลเดอร์เปล่า:
npm install -g @tgcloud/cli
tgcloud init
แพกเกจ npm คือ @tgcloud/cli และคำสั่งที่ติดตั้งคือ tgcloud ทุกคำสั่งทำงานได้จาก Subfolder ของโปรเจกต์ของคุณ
| คำสั่ง | จุดประสงค์ |
|---|---|
init
|
สร้างโครงสร้างโปรเจกต์ใหม่ในโฟลเดอร์ปัจจุบัน |
add
|
สร้างโมดูลใหม่ (Handler, Endpoint หรือ Lib โมดูล) |
login
|
เชื่อมต่อโปรเจกต์กับบอท (บันทึกโทเคน) |
status
|
แสดงสิ่งที่เปลี่ยนในเครื่องเทียบกับคลาวด์ |
diff
|
แสดงการเปลี่ยนแปลงทีละบรรทัด |
push
|
Deploy โมดูลและ Build ของ Mini App ไปยังคลาวด์ |
migrate
|
นำการเปลี่ยนแปลง Schema ไปใช้กับฐานข้อมูล |
run
|
รันโมดูลบนแพลตฟอร์มโดยไม่ Deploy |
fetch
|
ตรวจสอบสิ่งที่ Deploy ไว้ โดยไม่เปลี่ยนไฟล์ในเครื่อง |
pull
|
ดึงไฟล์ในเครื่องให้ตรงกับคลาวด์ |
reset
|
ทิ้งการเปลี่ยนแปลงในเครื่อง; กู้คืนจากสถานะคลาวด์ |
webhook
|
ตรวจสอบและซิงค์ Webhook ที่แพลตฟอร์มจัดการใหม่ |
completion
|
พิมพ์ Shell completion สคริปต์ (bash/zsh/fish) |
upgrade
|
ย้ายโครงสร้างโปรเจกต์ก่อน 0.2.0 เข้าไปใน tgcloud/ |
Authentication
โปรเจกต์ผูกกับบอทตัวเดียวด้วยโทเคน ซึ่งมีรูปแบบ app<id>:<secret> ส่วน app<id> เป็นสาธารณะและแสดงได้ ส่วน Secret จะไม่ปรากฏใน Log หรือ Error
โทเคนจะถูก Resolve ตามลำดับนี้:
TGCLOUD_TOKENEnvironment variable — สำหรับ CI; ไม่เขียนลงดิสก์.tgcloud/credentials— เขียนโดยnpx tgcloud login- ไม่มีทั้งคู่ → Error บอกให้คุณ
npx tgcloud login
CLI ไม่เคยถามโทเคนระหว่างรันคำสั่ง การ Login คือขั้นตอนที่ต้องทำเอง หากโทเคนบันทึกไว้ใช้งานไม่ได้ CLI จะลบออกและขอให้คุณ login ใหม่อีกครั้ง
init
npx tgcloud init
สร้างโครงสร้างโปรเจกต์ใหม่ในไดเรกทอรีปัจจุบัน: โฟลเดอร์ tgcloud/ พร้อม schema.js, lib/, handlers/ และ Handler เริ่มต้น รวมถึง tgcloud.jsonc, AGENTS.md, docs/ และโฟลเดอร์สถานะ .tgcloud/ รันในรูทของโปรเจกต์ Front-end ที่มีอยู่แล้วได้ด้วย — มันจะเพิ่มโฟลเดอร์ tgcloud/ และเอกสารให้ โดยไม่เขียนทับไฟล์เดิมของคุณ หากมันรู้จัก Front-end มันจะชี้ tgcloud.jsonc ไปที่โฟลเดอร์ Build และเพิ่มสคริปต์ deploy (Build แล้ว Push) ให้ใน package.json — ถ้าคุณยังไม่มีสคริปต์ deploy อยู่แล้ว ดูที่
Mini App front‑end
init จะปฏิเสธการสร้างโปรเจกต์ ซ้อนในโปรเจกต์อื่น การรันซ้ำในรูทของโปรเจกต์เดิมทำได้และเป็นการเพิ่มสิ่งที่ขาดไปเท่านั้น
add
npx tgcloud add <target>
สร้างโมดูลใหม่ตัวเดียว พร้อมใช้งานและแก้ไขได้ทันที — Handler, Endpoint หรือโมดูล lib/
npx tgcloud add handlers/callback_query # Handler อัปเดตใหม่
npx tgcloud add endpoints/getProfile # Endpoint ใหม่สำหรับ Mini App
npx tgcloud add lib/cart # โมดูลแชร์ใหม่
หมายเหตุ
addจะไม่เขียนทับไฟล์ที่มีอยู่
<target> คือพาธของโมดูล (นามสกุล .js ไม่ต้องใส่ก็ได้) สำหรับ handlers/ ชื่อต้องเป็นประเภทอัปเดตของ Telegram แพลตฟอร์มจะแจ้งชื่อที่ใช้ได้ออกมา ดังนั้นชื่อที่ไม่ถูกต้องจะถูกปฏิเสธทันที handlers/ และ endpoints/ เป็นแบบแบน ในขณะที่ lib/ สามารถซ้อนได้ (lib/payments/stripe)
ชื่อโมดูลจำเป็นต้องระบุ การให้ แค่ไดเรกทอรี จะเป็น Error — แต่เป็น Error ที่มีประโยชน์: สำหรับ handlers/ มันจะแสดงรายการประเภทอัปเดตที่คุณยังไม่มี เพื่อให้คุณก๊อปปี้ไปใช้ได้
$ npx tgcloud add handlers
Error: ระบุชื่อ เช่น "handlers/callback_query" ประเภท handlers/ ที่ใช้ได้: callback_query, inline_query, chat_member, …
<Tab>Completion จะเสนอรายการเดียวกัน — ดูcompletion
ไฟล์ที่สร้างขึ้นจะมี export default ที่ใช้งานได้เลย ดังนั้น Handler จะทำงานทันทีที่คุณ Deploy — ไม่มีอะไรต้อง Uncomment Deploy โมดูลใหม่ด้วย
push
login
npx tgcloud login
ถามหา CLI access token — จาก
@BotFather
→ บอทของคุณ → Serverless → CLI Access → Access token ซึ่งเป็นโทเคนคนละตัวกับ
bot’s API token
— ตรวจสอบกับแพลตฟอร์ม และบันทึกลงใน .tgcloud/credentials
loginเป็นคำสั่ง เดียว ที่ถามหาโทเคน และต้องใช้ Interactive terminal ใน CI ให้ตั้งค่าTGCLOUD_TOKENแทน
status
npx tgcloud status
แสดงสถานะรายไฟล์ว่าอะไรเปลี่ยนไปบ้างระหว่าง Working directory ของคุณกับที่ Deploy ไว้: แก้ไข, ใหม่, ลบ, ไม่เปลี่ยน มันทำงานแบบออฟไลน์ เทียบกับสถานะคลาวด์ล่าสุดจากการ push, pull หรือ fetch ครั้งสุดท้ายของคุณ มันยังเตือนเกี่ยวกับไฟล์ .js แปลกปลอมใน tgcloud/ ที่จะไม่ถูก Deploy ด้วย
สำหรับโปรเจกต์ที่มี Mini App status จะแสดงสิ่งที่เปลี่ยนในโฟลเดอร์ Build ตั้งแต่ Deploy ครั้งสุดท้ายด้วย
diff
npx tgcloud diff
เหมือน status แต่แสดงความแตกต่างทีละบรรทัดสำหรับโมดูลที่เปลี่ยนไป ทำงานแบบออฟไลน์เช่นกัน
push
npx tgcloud push [files...]
Deploy โปรเจกต์ของคุณไปที่คลาวด์ในคราวเดียว
หาก ไม่มีอาร์กิวเมนต์ มันจะ Deploy ทั้งโปรเจกต์ และสถานะบนคลาวด์จะถูกทำให้ตรงกับโฟลเดอร์ของคุณเป๊ะๆ — โมดูลที่คุณลบในเครื่องจะถูกลบในคลาวด์ด้วย
หากมี อาร์กิวเมนต์ไฟล์หรือไดเรกทอรี (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/) มันจะ Deploy เฉพาะการเปลี่ยนแปลงในไฟล์เหล่านั้น Targeted push จะไม่ลบโมดูลอื่นเป้าหมายอาจระบุเป็นพาธไฟล์ (tgcloud/handlers/message.js), พาธภายใน tgcloud/ (handlers/message.js) หรือชื่อโมดูล (handlers/message) — ทั้งหมดหมายถึงโมดูลเดียวกัน
ตัวเลือกเดียวคือ --force — เพื่อข้ามการตรวจสอบความขัดแย้งและเขียนทับสิ่งที่อยู่บนคลาวด์ ใช้เมื่อคุณมั่นใจเท่านั้น (ดู
Staying in sync
)
หาก tgcloud.jsonc ระบุโฟลเดอร์ Static ไว้ Full push จะ Deploy มันในรอบเดียวกัน — เฉพาะไฟล์ที่เปลี่ยนเท่านั้นที่จะถูกอัปโหลด — และพิมพ์ที่อยู่ไซต์ออกมา Build ก่อนเสมอ เพราะ push ไม่รัน Build ของคุณ (npm run deploy ทำทั้งสองอย่าง) npx tgcloud push dist/ จะ Deploy เฉพาะไฟล์ Static และการ Push ที่ระบุแค่โมดูลจะไม่มีการแตะต้องไฟล์เหล่านั้น
หลัง Deploy หาก schema.js เปลี่ยนและฐานข้อมูลไม่ซิงค์ push จะพิมพ์สรุปการเปลี่ยนแปลงที่รอดำเนินการและแนะนำให้ npx tgcloud migrate มันไม่เคยนำไปใช้โดยตัวเอง
migrate
npx tgcloud migrate
นำการเปลี่ยนแปลง Schema ไปใช้กับฐานข้อมูล มันคำนวณความแตกต่างระหว่าง schema.js กับฐานข้อมูลจริง แล้วพาคุณไปทีละขั้นตอนด้วยตัวนับ [N/M]:
- สรุปการเปลี่ยนแปลงที่รอดำเนินการ
- Safe changes นำไปใช้ในขั้นตอนเดียวเมื่อกดยืนยัน
- Warnings (ลบตาราง, ดำเนินการช้า) ทีละรายการ แต่ละรายการต้องกดยืนยันแยกกัน
- Manual changes แสดงเหตุผลและวิธีการ ไม่นำไปใช้โดยอัตโนมัติ
- Undocumented objects (มีในฐานข้อมูลแต่ไม่มีใน Schema) แสดงเพื่อรับทราบ
จบลงด้วยสรุป: นำไปใช้, ข้าม, รอแก้ไขด้วยตนเอง, ไม่มีใน Schema
ดู Migrations สำหรับโมเดล
ตัวเลือก: --dry-run (พิมพ์ทุกอย่าง, ไม่ทำจริง), --safe (Auto-apply safe changes, ข้าม Warning), --yes (Auto-apply safe changes และ Warning ทุกอย่าง, ข้าม Manual — ใช้ด้วยความระมัดระวัง), --local (Diff กับ schema.js ในเครื่องแทนคลาวด์) โดยไม่มี Flag migrate ต้องการ Terminal และจะ Error ใน Environment ที่ไม่ใช่ Interactive แทนที่จะเดาสุ่ม
run
npx tgcloud run <module> [args] [--ctx <json5>]
รัน Handler หรือ Endpoint บนแพลตฟอร์ม โดยไม่ต้อง Deploy โดยใช้ไฟล์ในเครื่องปัจจุบัน นี่คือการทดสอบตรรกะแบบเร็ว
<module>— ชื่อตรงๆ (หาในhandlers/และendpoints/) หรือพาธเช่นhandlers/messageชื่อที่ซ้ำกันในสองไดเรกทอรีต้องระบุไดเรกทอรีให้ชัดเจน[args]— Payload ที่ส่งให้ Handler ในรูปแบบ JSON5 ดังนั้นไม่ต้องใส่เครื่องหมายคำพูดที่คีย์ มันคือ Object อัปเดตที่ Handler ได้รับ หรือinputของ Endpoint และต้องเป็น Object — ไม่ใช่ Array หรือ String--ctx <json5>— Context object ของโมดูล (อาร์กิวเมนต์ตัวที่สอง) ในรูปแบบ JSON5 ใช้เพื่อส่งสิ่งที่โค้ดคุณอ่านจากctx— เช่น Raw update สำหรับ Handler,--ctx '{ update: { update_id: 1 } }', หรือ Init data สำหรับ Endpoint,--ctx '{ initData: { user: { id: 1 } } }'runไม่ตรวจสอบความถูกต้อง เพราะคุณกำลังทดสอบตรรกะของคุณ ไม่ใช่การตรวจสอบของแพลตฟอร์ม
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
แพลตฟอร์มรันโมดูลกับโมดูลที่รวบรวมจากโปรเจกต์ ในเครื่อง ของคุณ (รวมถึงไฟล์ lib/ ที่แก้ในเครื่อง) แล้วคืนค่า Return value, สิ่งที่ Log ด้วย console.* และเวลาที่ใช้ อ่านอาร์กิวเมนต์ยาวๆ จากไฟล์ด้วย npx tgcloud run handlers/message "$(cat message.json5)"
fetch
npx tgcloud fetch
ตรวจสอบสิ่งที่ Deploy ไว้ โดยไม่แตะต้องไฟล์ในเครื่อง หลังจากนั้น status และ diff จะเทียบกับสถานะคลาวด์ล่าสุด มีประโยชน์ในการดูความขัดแย้งก่อนตัดสินใจแก้ไข
pull
npx tgcloud pull
ดึงโปรเจกต์ในเครื่องให้ตรงกับคลาวด์โดยอัปเดตไฟล์ในเครื่องให้เป็นสถานะที่ Deploy ไว้
pull ไม่เคยเขียนทับโฟลเดอร์ Build ของ Mini App ของคุณ
reset
npx tgcloud reset
ทิ้งการเปลี่ยนแปลงในเครื่องและกู้คืนโมดูลจากสถานะคลาวด์ล่าสุด ใช้เพื่อทิ้งการทดลองของคุณ โฟลเดอร์ Build ของ Mini App จะไม่ถูกแตะต้อง
webhook
npx tgcloud webhook
npx tgcloud webhook sync [--drop-pending]
Telegram ส่งอัปเดตให้บอทผ่าน Webhook ซึ่งแพลตฟอร์มจัดการให้คุณ — คุณไม่ต้องชี้ที่ไหนเอง npx tgcloud webhook แสดงสถานะปัจจุบัน: URL, รายการ allowed_updates, จำนวนอัปเดตที่ค้าง, Error ล่าสุด (ถ้ามี) และ In sync กับ Handler ที่ Deploy ไว้หรือไม่
“In sync” หมายถึง Webhook ชี้ที่แพลตฟอร์มและ allowed_updates ตรงกับ Handler ที่คุณ Deploy ไว้ ดังนั้น Telegram จะส่งเฉพาะอัปเดตที่คุณจัดการเท่านั้น การ Deploy Handler ใหม่ (หรือลบออก) อาจทำให้ Webhook หลุด Sync จนกว่าจะ Refresh; npx tgcloud status จะเตือนเรื่องนี้ด้วย
npx tgcloud webhook sync จะแก้ไขมัน — ชี้ Webhook ไปที่แพลตฟอร์มและสร้าง allowed_updates ใหม่จาก Handler ที่ Deploy เพิ่ม --drop-pending เพื่อทิ้งอัปเดตที่ Telegram คิวไว้ก่อน Sync (ไม่อย่างนั้นมันจะถูกส่งมาเมื่อ Webhook ปกติแล้ว)
completion
หมายเหตุ: Tab-completion ทำงานเฉพาะเมื่อ
tgcloudอยู่บนPATH— ดังนั้นติดตั้งทั่วโลก (npm install -g @tgcloud/cli) หรือนำ Binary ไว้บนPATHของคุณ มัน Hook กับnpxไม่ได้
tgcloud completion <bash|zsh|fish>
พิมพ์ Shell completion สคริปต์ไปที่ Stdout เปิดใช้งานครั้งเดียว แล้ว <Tab> จะช่วยเติมคำสั่ง, Flag, ไดเรกทอรีโมดูล, ประเภทอัปเดต Handler ที่คุณยังไม่มี และโมดูลที่รันได้ในเครื่อง
# bash — ต้องมี bash-completion:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc
# zsh — ตรวจสอบว่า `autoload -U compinit && compinit` ทำงานใน ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc
# fish:
tgcloud completion fish > ~/.config/fish/completions/tgcloud.fish
รีสตาร์ท Shell หลังจากนั้น การรัน tgcloud completion เปล่าๆ จะแสดงคำแนะนำอีกครั้ง
upgrade
npx tgcloud upgrade [--dry-run]
Migration ครั้งเดียวสำหรับโปรเจกต์ที่สร้างก่อน CLI 0.2.0 ซึ่งเก็บ schema.js, lib/ และ handlers/ ไว้ที่รูทโปรเจกต์ มันจะย้ายพวกมันเข้าไปใน tgcloud/ โทเคนบอทของคุณยังคงเดิมและไม่ต้อง Deploy อะไรใหม่ --dry-run จะพิมพ์การย้ายที่วางแผนไว้โดยไม่เปลี่ยนไฟล์ คำสั่งอื่นจะไม่รันบนโครงสร้างเก่าจนกว่าคุณจะ Upgrade
Staying in sync
ทุกโปรเจกต์มี Revision บนคลาวด์ที่เพิ่มขึ้นเรื่อยๆ ซึ่งจะเพิ่มขึ้นเมื่อ Deploy CLI จำ Revision ล่าสุดที่ซิงค์ไว้และส่งไปในทุก push หากคลาวด์ล้ำหน้าไป — เพราะเครื่องอื่นหรือเพื่อนร่วมทีม Deploy — การ Push จะถูก ปฏิเสธ แทนที่จะเขียนทับงานคนอื่นโดยไม่รู้ตัว และ CLI จะเสนอสามทางเลือก:
npx tgcloud fetch # ดูสิ่งที่เปลี่ยนในคลาวด์ แล้วตรวจซ้ำ
npx tgcloud pull # ดึงสิ่งที่ล่าสุดมาที่ไฟล์ในเครื่องของคุณ
npx tgcloud push --force # เขียนทับสถานะคลาวด์ (อันตราย)
การตรวจสอบ Optimistic-concurrency นี้คือเหตุผลที่คุณสามารถแชร์บอทกับทีมได้โดยไม่ต้องมีการ Deploy แบบล๊อคขั้นตอน คำสั่งจะ Exit non-zero เมื่อล้มเหลว — Push ปฏิเสธ, Migrate ล้มเหลว, Auth Error, โมดูล Error ตอน run — ดังนั้นมันจึงทำงานร่วมกับสคริปต์และ CI Pipelines ได้อย่างราบรื่น