Telegram Serverless מאפשר לך להריץ קוד צד-שרת (backend) עבור הבוט וה-Mini App שלך ישירות על התשתית של Telegram — ללא צורך להקצות שרתים, ללא קונטיינרים שיש להשאיר דולקים, וללא מחשבה על שינויי קנה מידה (scaling). אתה כותב מודולי JavaScript פשוטים, פורס אותם בפקודה אחת, ו-Telegram מריצה אותם בתוך סביבת V8 מבודדת ומהירה שיושבת ממש ליד ה-Bot API ומסד נתונים מובנה.
אם אי פעם חיברת בוט ל-VPS, ל-Cloud Function, או ללוח ניהול אירוח רק כדי לענות על /start, זה החלק שאתה כבר לא צריך לעשות.
למה Serverless
בוט של Telegram הוא, בבסיסו, תוכנית שמגיבה לעדכונים. באופן מסורתי היית צריך לארח את התוכנית הזו במקום שפועל תמיד, נגיש ומאובטח — ואז לשמור על כך. Telegram Serverless מסירה את השכבה הזו לחלוטין:
- ללא תשתית. אין מכונה לשכור, לעדכן או לנטר. הקוד שלך רץ לפי דרישה וגדל אוטומטית יחד עם הבוט שלך.
- פרטי התחברות כלולים. ה-Telegram Bot API, מסד נתונים מבוסס SQLite, וגישת HTTP יוצאת זמינים לכל מודול ישר מהקופסה — אין מה להתקין, אין פרטי התחברות לחבר.
- הרצה מהירה ומבודדת. כל קריאה רצה בתוך V8 isolate קל משקל, קרוב למערכות של Telegram עצמה, כך שהקריאות ל-Bot API ולמסד הנתונים שלך מהירות ואמינות.
- תהליך עבודה אמיתי של מפתחים. פרויקט חי בתוך תיקייה במחשב שלך תחת בקרת גרסאות. אתה עורך קבצים, רואה בדיוק מה השתנה, פורס באופן אטומי, ומקדם את הסכימה של מסד הנתונים שלך עם הגירות (migrations) שעברו בדיקה — הדרך שבה אתה כבר עובד עם כל השאר.
המודל המנטלי: איך הפרויקט עובד
אתה עובד ב-שלושה מקומות, והם ממופים בצורה ברורה אחד לשני:
| איפה | מה חי שם |
|---|---|
| תיקיית הפרויקט שלך | מודולי JavaScript — סכימה, קוד משותף, מטפלי עדכונים, נקודות קצה של Mini App — ואם יש לך, ה-Front-end של ה-Mini App |
| הענן | העותק הפרוס של המודולים האלה, מסד הנתונים של הבוט שלך, והקבצים של ה-Mini App |
ה-CLI tgcloud |
הגשר — הוא מראה לך הבדלים ומסנכרן אותם |
לעולם אינך צריך להתחבר ב-SSH לאף מקום. אתה עורך קבצים מקומית, מריץ npx tgcloud push, והפלטפורמה דואגת לכל השאר. התעבורה של הבוט שלך מטופלת על ידי המודולים הפרוסים; מסד הנתונים שלך נשמר בין הרצות.
לפרויקט יש רק ארבעה סוגי קוד, כולם בתוך תיקיית tgcloud/ אחת:
tgcloud/
├─ endpoints/ # פונקציות שה-Mini App שלך קורא להן
├─ handlers/ # פונקציות שמגיבות לסוגי עדכונים של Telegram
├─ lib/ # קוד משותף שאתה מייבא מכל מקום
└─ schema.js # טבלאות מסד הנתונים שלך
כאשר מגיע עדכון — הודעה, לחיצת כפתור, שאילתא מוטמעת (inline query) — Telegram מנתבת אותו למטפל (handler) המתאים (handlers/message.js, handlers/callback_query.js, …) וקוראת ל-default export שלו. הפונקציה הזו מדברת עם ה-Bot API ומסד הנתונים דרך ה-SDK, ומחזירה תשובה. זהו כל הלופ. עדכון ללא מטפל תואם פשוט מתעלם, לכן אתה מוסיף רק את המטפלים שאתה צריך.
Mini App הוא אופציונלי. אם לבוט שלך יש אחד, ה-Front-end שלו חי באותו פרויקט, ליד tgcloud/. ה-Serverless מארח את תוצרי הבנייה שלו ומריץ את ה-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: `Hello! I've seen ${row.seen} message(s) from you.`,
});
}
פרוס את זה:
npx tgcloud push # העלה את המודולים
npx tgcloud migrate # צור את טבלת `counters`
זהו בוט חי עם מצב (state) נשמר וללא שרת. הכל בו — api, db, ה-table() DSL — מתואר בסעיפים למטה.
Serverless הוא Backend כללי לבוטים של Telegram ול-Mini Apps, לא תבנית לסוג אחד של אפליקציה. הוא אידיאלי עבור:
- בוטי AI שיחתיים שצריכים לשמור מצב לכל משתמש במסד נתונים.
- Backend ל-Mini Apps ששומר נתוני משתמש ומגיש תוכן דינמי.
- משחקים וכלים — כולל טבלאות מובילים, חידונים ועוד.
- אוטומציות ואינטגרציות שקוראות ל-HTTP APIs של צד שלישי ודוחפות תוצאות לצ’אטים.
תחילת עבודה
מדריך זה לוקח אותך מתיקייה ריקה לבוט חי שעונה להודעות ושומר נתונים. הוא מניח שיש לך
Node.js
18 ומעלה מותקן ובוט רשום ב-
@BotFather
. בסיום, תשתמש בכל פקודה שאתה צריך ביום-יום: push, migrate, run, ו-status.
לפני הכל, הפעל את Serverless. ב- @BotFather , פתח את הבוט שלך → Serverless והפעל אותו. זה מפעיל את התכונה עבור בוט זה ופותח את טוקן הגישה ל-CLI, המטפלים, הספרייה ומסד הנתונים שלו.
1. צור פרויקט
הדרך המהירה ביותר להתחיל היא יוצר הפרויקטים, שמכין תבנית פרויקט ומתקין לתוכו את ה-CLI:
npm create @tgcloud/bot example_bot
cd example_bot
הארגומנט הוא תיקיית היעד: העבר . כדי ליצור בתיקייה הנוכחית, או כל נתיב אחר. זה עובד גם בתיקייה קיימת ולעולם לא דורס קבצים שכבר יש לך.
זה נותן לך פרויקט מוכן לעריכה:
example_bot/
├─ tgcloud/ # כל מה שהפלטפורמה מריצה
│ ├─ handlers/
│ │ └─ message.js # מטפל הודעות התחלתי (מחזיר את ההודעה חזרה)
│ ├─ lib/ # המודולים המשותפים שלך הולכים לכאן (ריק בהתחלה)
│ └─ schema.js # טבלאות מסד הנתונים שלך
├─ docs/
│ └─ tgcloud-sdk.md # הפניה ל-SDK (עבורך ועבור כלי ה-AI שלך)
├─ AGENTS.md # הכוונה לעוזרי קידוד AI
├─ package.json
└─ tgcloud.jsonc # הגדרות פרויקט
הקבצים שנוצרו מתעדים את עצמם — כל אחד מכיל דוגמאות מוערות למה שאתה יכול לעשות הלאה.
ה-CLI מותקן לתוך הפרויקט כ-dev-dependency מקומי, לכן אתה מריץ אותו עם npx tgcloud <command> (npx מוצא את העותק ב-node_modules של הפרויקט שלך), או דרך קיצורי ה-npm run שהתבנית מוסיפה ל-package.json (npm run deploy, npm run status). כברירת מחדל אין tgcloud גלובלי ב-PATH שלך.
אתה יכול גם להתקין אותו גלובלית — npm install -g @tgcloud/cli — אם אתה מעדיף להקליד tgcloud פשוט מכל מקום. זה נוח להרצת tgcloud init בכל תיקייה ריקה, וזה מה שהשלמה אוטומטית בטרמינל (tab-completion) צריכה. כך או כך, אתה מקבל את אותו פרויקט.
בונה Mini App? התחל מה-Front-end שלו קודם. צור אותו עם הכלי הרגיל שלך, ואז הרץ את יוצר הפרויקט בתיקייה ההיא. ראה Mini App front-end .
2. קשר את הבוט שלך
כל פרויקט קשור לבוט אחד. חבר אותם עם login, שמבקש את טוקן הגישה ל-CLI שלך (
@BotFather
→ הבוט שלך → Serverless → CLI Access → טוקן גישה — טוקן נפרד מ-
טוקן ה-API של הבוט שלך
) ושומר אותו מקומית:
npx tgcloud login
לטוקן יש את הפורמט app<id>:<secret>. ה-CLI שומר אותו ב-.tgcloud/, שהוא כלול ב-git-ignore, ולעולם לא מדפיס את חלק הסוד. ההתחברות היא הפעם היחידה שתתבקש להזין אותו — ראה
אימות
לאופן שבו טוקנים מפוענחים ב-CI.
3. הסתכל מסביב
שתי פקודות אומרות לך איפה הדברים עומדים בכל רגע, שתיהן לא מקוונות לחלוטין:
npx tgcloud status # מה השתנה מקומית לעומת העותק הפרוס
npx tgcloud diff # השינויים שורה-אחר-שורה
מיד לאחר init הכל חדש ושום דבר עדיין לא פרוס. status מראה את קבצי ההתחלה שמחכים לעלות.
4. פרוס
שלח את המודולים שלך לענן:
npx tgcloud push
push מעלה כל מודול שהשתנה באצווה אטומית אחת ומעדכן את הרישום המקומי שלך לגבי מה שנמצא כעת בענן. הבוט שלך חי: פתח אותו ב-Telegram ושלח לו הודעה — מטפל ההתחלה מהדהד אותה חזרה.
פריסה לעולם לא נוגעת במסד הנתונים שלך. העלאת קוד ושינוי סכימת מסד הנתונים הם שלבים נפרדים בכוונה, כך שפריסת קוד לעולם לא יכולה להפתיע אותך עם הגירת נתונים. לזה נועד השלב הבא.
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())`) ,
});
פרוס את הסכימה, ואז החל אותה על מסד הנתונים:
npx tgcloud push # מעלה את הסכימה החדשה
npx tgcloud migrate # יוצר את טבלת `messages`
push מדווח שהסכימה לא מסונכרנת ומראה לך את השינוי הממתין, אך לא מחיל כלום. migrate מדריך אותך דרך השינוי, ועם אישורך, יוצר את הטבלה. מודל דו-שלבי זה — ומה קורה עם שינויים מסוכנים יותר כמו מחיקות — מכוסה ב-
הגירות
.
6. שמור וקרא נתונים
עכשיו השתמש בטבלה מהמטפל שלך. ערוך את 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: `Saved. That's ${count} message(s) from this chat so far.`,
});
}
פרוס את המטפל המעודכן עם npx tgcloud push, אז שלח לבוט כמה הודעות וראה את המונה עולה. מסד הנתונים נשמר בין הרצות — זה הזיכרון של הבוט שלך.
7. בדוק ללא פריסה
אתה לא חייב לפרוס כדי לנסות שינוי. npx tgcloud run מריץ מטפל על הפלטפורמה באמצעות הקבצים המקומיים שלך, מבלי לפרסם אותם:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hello" }'
הארגומנט הוא ה-payload שהמטפל שלך מקבל — עבור handlers/message,
Message
— כתוב ב-
JSON5
(כך שניתן לדלג על הוספת מרכאות למפתחות). הפקודה מדפיסה כל מה שהמטפל תיעד עם console.*, את ערך ההחזרה, וכמה זמן זה לקח. זהו הלופ המהיר ביותר לאיטרציה על לוגיקה — בלי פריסה, בלי לחכות להודעה אמיתית.
8. שמור על סנכרון
בזמן שאתה עובד, קומץ פקודות שומרות על הפרויקט המקומי והענן מסונכרנים: npx tgcloud status מראה מה השתנה, npx tgcloud push פורס, npx tgcloud pull מביא את הפרויקט המקומי שלך לקו אחד עם הענן, npx tgcloud fetch בודק מה פרוס בלי לגעת בקבצים שלך, ו-npx tgcloud reset זורק שינויים מקומיים.
אם שני אנשים (או שתי מכונות) פורסים לאותו בוט, הפלטפורמה מזהה את הקונפליקט ו-
pushנעצר כדי לאפשר לך לעשותpullקודם — לעולם לא תוכל לדרוס בשקט עבודה של מישהו אחר. ראה שמירה על סנכרון .
בנייה עם AI
מעדיף לבנות עם עוזר AI — או שהמתכנת היחיד בצוות שלך הוא AI? אתה עדיין יכול לשלוח בוט. עשינו צעד ראשון כדי לגרום לסוכן AI להרגיש בבית בפרויקט: כל פרויקט חדש נוצר עם AGENTS.md והפניה ל-docs/tgcloud-sdk.md שכלי קידוד אוטונומיים קוראים אוטומטית.
יחד עם סביבת הרצה קטנה ומכילה את עצמה — SDK אחד, אין חבילות npm להתעסק איתן — זה נותן לעוזר התחלה טובה על מוסכמות שקוד גנרטיבי נוטה לפספס כאן: ייבוא יחסי עם .js, ללא מפתחות זרים (foreign keys), כל קריאת db היא אסינכרונית, מטפל אחד לכל סוג עדכון, וזרימת ה-push/migrate הדו-שלבית.
נסה את זה:
npm create @tgcloud/bot my-bot
cd my-bot
opencode # או Claude Code, Cursor, … — כל סוכן שקורא AGENTS.md
ואז פשוט שאל, בשפה פשוטה:
כתוב בוט שזוכר את רשימת המטלות של כל אדם — הוסף פריט כשהם שולחים טקסט, והצג את כל הרשימה כשהם שולחים /list.
העוזר עורך את schema.js והמטפלים שלך עבורך; אתה בודק, בוחן שינוי מיידית עם npx tgcloud run, ואז הופך לחי עם npx tgcloud push ו-npx tgcloud migrate. AGENTS.md הוא חלק מהפרויקט שלך — ערוך אותו ככל שהבוט גדל כדי שההכוונה תישאר מדויקת.
בדרכים עם BotFather
נשארת רק עם הטלפון שלך? כל הפרויקט חי ב- @BotFather גם כן — פתח את הבוט שלך → Serverless ותקבל את כל מה שה-CLI מנהל, על מסך מגע:
- Handlers — צור, ערוך ובחן ריצה של מטפלי עדכונים; BotFather שומר על ה-webhook מסונכרן עם המטפלים שיש לך (אותו In sync / Out of sync שה-CLI מדווח).
- Endpoints — הפונקציות שה-Mini App שלך קורא להן.
- Library — מודולי ה-
lib/המשותפים שלך. - Database — ערוך את
schema.jsבאותו תחביר דמוי Drizzle, סקור שינויים ממתינים, והחל אותם; Save מבצע פריסה. - Mini App Files — ה-Front-end שהמשתמשים שלך פותחים ב-Telegram.
- CLI Access — תפוס את טוקן הגישה ל-CLI כאן כשאתה חוזר למקלדת.
זהו אותו פרויקט ענן, כך שאתה יכול להתחיל מטפל בטלפון שלך לעשות לו npx tgcloud pull למחשב הנייד מאוחר יותר — שום דבר לא קשור ללקוח אחד בלבד. הרצת מטפל אפילו מציגה את פלט ה-Console שלו ישירות בצ’אט, בדיוק כמו
npx tgcloud run
.
פרויקטים ומודולים
פרויקט serverless הוא תיקייה רגילה תחת בקרת גרסאות. כל מה שהפלטפורמה מריצה יושב בתיקייה אחת, tgcloud/; שאר הפרויקט הוא שלך — front-end של mini-app, הגדרות, מסמכים. אין שלב בנייה עבור הבוט, אין node_modules בזמן ריצה, ואין נקודת כניסה לשרת.
אנטומיה של פרויקט
example_bot/
├─ tgcloud/ # החלק הפרוס — שום דבר אחר לא
│ ├─ handlers/ # מטפלי עדכונים — שטוח, רמה אחת בלבד
│ │ ├─ message.js
│ │ └─ callback_query.js
│ ├─ endpoints/ # פונקציות שה-Mini App שלך קורא להן — שטוח, רמה אחת בלבד
│ │ └─ getProfile.js
│ ├─ lib/ # מודולים משותפים; תת-תיקיות מותרות
│ │ ├─ reply.js
│ │ └─ internal/util.js
│ └─ schema.js # סכימת מסד הנתונים — קובץ אחד
└─ .tgcloud/ # מצב CLI, ב-git-ignore
רק קבצי .js בתוך tgcloud/ — schema.js, lib/, handlers/, endpoints/ — הם פרוסים. כל השאר — 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/ כקובץ יחיד. הוא פרוס כמו כל מודול אחר, אך פריסתו לעולם לא משנה את מסד הנתונים — שינויי סכימה מוחלים בנפרד עם npx tgcloud migrate. ראה
מסד הנתונים
.
lib/ — קוד משותף, כל דבר שאתה רוצה לעשות בו שימוש חוזר בין מטפלים: עזרי עזר טהורים, שכבות גישה למסד נתונים, עיצוב, אינטגרציות עם שירותים חיצוניים. lib/ היא התיקייה היחידה שיכולה להכיל תת-תיקיות (lib/internal/util.js, lib/payments/stripe.js), כך שאתה יכול לארגן בסיס קוד גדול יותר איך שאתה אוהב. מודולים ב-lib/ לעולם לא נקראים ישירות על ידי הפלטפורמה; הם קיימים כדי להיות מיובאים על ידי מטפלים ועל ידי זה את זה.
handlers/ — נקודות הכניסה של הבוט שלך. כל קובץ תואם ל-סוג עדכון של Telegram, והפלטפורמה מנתבת כל עדכון נכנס למטפל התואם:
| קובץ | מטפל ב- |
|---|---|
handlers/message.js |
הודעות נכנסות חדשות |
handlers/inline_query.js |
שאילתות inline חדשות |
handlers/callback_query.js |
שאילתות callback נכנסות חדשות |
| … | כל סוג עדכון אחר של Bot API |
handlers/ הוא שטוח — אין תת-תיקיות. ה-export default של המטפל הוא הפונקציה שהפלטפורמה קוראת לה (ראה
Handlers
).
סוג עדכון מטופל רק אם קובץ המטפל שלו קיים ואינו ריק. אם אין handlers/<type>.js — או שהקובץ ריק — עדכונים מסוג זה מתעלמים, והפלטפורמה לא מריצה כלום עבורם. אז שמור רק את המטפלים שאתה באמת צריך: כל אחד שאתה מוסיף הוא עוד סוג עדכון שהבוט שלך מתעורר לעבד, ולהשאיר את השאר אומר ש-Telegram לא מפעילה את הקוד שלך עבור עדכונים שבכל מקרה היית זורק.
כדי ליצור מטפל חדש, הרץ
npx tgcloud add handlers/<type>.
endpoints/ — צד השרת של ה-Mini App שלך. כל קובץ הוא פונקציה שה-Mini App קורא לה בשם, כך ש-endpoints/getProfile.js עונה ל-POST /api/getProfile. הפלטפורמה בודקת שהקריאה מגיעה ממשתמש Telegram בתוך ה-Mini App שלך לפני שהקוד שלך רץ, ומוסרת לך את המשתמש ההוא. endpoints/ הוא שטוח, כמו handlers/. ראה
Endpoints
.
.tgcloud/ — מצב מקומי למכונה שמנוהל במלואו על ידי ה-CLI, כולל פרטי ההתחברות השמורים שלך. הוא ב-git-ignore, ואסור לך לעולם לקרוא ממנו או לכתוב אליו ידנית — השתמש בפקודות CLI במקום.
מערכת המודולים
בזמן ריצה, מודול יכול לראות בדיוק שני דברים: ה-SDK של הפלטפורמה ו-המודולים האחרים בפרויקט שלך. אין חבילות npm, אין מערכת קבצים, ואין רשת מלבד דרך ה-fetch של ה-SDK.
ייבא מודולים משלך בדרך שבה היית עושה בכל פרויקט JavaScript — לפי נתיב יחסי, עם הסיומת .js — ואת ה-SDK לפי השם שלו:
// tgcloud/handlers/message.js
import { users } from '../schema.js'; // מודול הסכימה
import { addItem } from '../lib/cart.js'; // מודול lib
import { format } from '../lib/internal/fmt.js'; // מודול lib מקונן
import { db, api, fetch } from 'sdk'; // ה-SDK של הפלטפורמה
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js'; // שכן
import { users } from '../schema.js';
ייבוא יחסי חייב להסתיים ב-.js ולא יכול להוביל מחוץ ל-tgcloud/:
import { users } from '../schema'; // הוסף את ה-.js
import config from '../../config.js'; // מחוץ ל-tgcloud/
import { z } from 'zod'; // אין חבילות npm
לכל מודול יש גם שם — הנתיב שלו בתוך tgcloud/ ללא הסיומת, כגון schema, lib/cart או handlers/message. השם הזה הוא מה ש-npx tgcloud run והפלט של ה-CLI משתמשים בו, וניתן לייבא מודול גם על פיו (from 'lib/cart', ללא סיומת). העדף ייבוא יחסי בקוד שלך. זה מה שהעורך שלך מבין — קפיצה להגדרה, השלמה אוטומטית, שינוי שם.
מה שזמין בזמן ריצה הוא בדיוק שני דברים: sdk ותתי-המודולים שלו (sdk/db, sdk/api, sdk/fetch) — כל משטח הפלטפורמה, ראה
ה-SDK
— ו-המודולים שלך ב-tgcloud/. זו הרשימה המלאה. אם הקוד שלך מייבא (import) כל דבר אחר, הוא לא יפענח. אילוץ זה הוא מה ששומר על מודולים מהירים לטעינה ובטוחים להרצה.
Handlers
מטפל הוא מודול ב-handlers/ שהפלטפורמה מפעילה את ה-default export שלו כשמגיע עדכון תואם.
// handlers/message.js
import { api } from 'sdk';
export default async function (message) {
await api.sendMessage({
chat_id: message.chat.id,
text: `You said: ${message.text ?? '(no text)'}`,
});
}
מטפל מקבל את ה-payload של העדכון כארגומנט שלו — הפלטפורמה פותחת את ה-
Update
של Telegram עבורך. handlers/message.js מקבל את ה-
Message
(כלומר update.message); handlers/callback_query.js מקבל את ה-
CallbackQuery
; וכן הלאה. ה-ארגומנט השני של המטפל הוא אובייקט הקשר (context) לכל הרצה, ctx. הוא נושא את ה-Update הגולמי בתור ctx.update — השתמש בו כשאתה צריך משהו מחוץ ל-payload, כמו update_id.
מטפל יכול להיות async (בדרך כלל הוא כזה) ועשוי להחזיר ערך. הוא מגיע ל-Bot API, למסד הנתונים, ול-HTTP יוצא דרך ה-SDK.
אתה לא צריך עדכון אמיתי כדי לבדוק מטפל. npx tgcloud run מריץ אותו על הפלטפורמה עם ה-payload שאתה מספק והקוד המקומי הנוכחי שלך:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
הארגומנט הוא ה-payload — אותו אובייקט שהמטפל שלך מקבל — ב-JSON5. כדי לספק את ה-ctx של המטפל (הארגומנט השני שלו), הוסף --ctx, למשל --ctx '{ update: { update_id: 1 } }'. זה רץ מול הקבצים המקומיים שלך, כך שאתה יכול לנסות שינויים לפני פריסתם. ראה
run
.
Endpoints
נקודת קצה היא מודול ב-endpoints/ שה-Mini App שלך קורא לו — המקום לכל דבר שה-Front-end לא יכול לעשות לבדו, כמו קריאה וכתיבה למסד הנתונים, קריאה ל-Bot API, או דיבור עם שירותים חיצוניים עם הסודות שלך.
// 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('Profile not found', { userId: user.id });
return profile;
}
הארגומנט הראשון, input, הוא אובייקט ה-JSON שה-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 — כך שאין API ציבורי להגן עליו ואין התחברות לבנות.
כל מה שנקודת הקצה מחזירה נשלח בחזרה כ-JSON. כדי לסרב לקריאה, throw new EndpointError(description, parameters?). ה-Mini App מקבל את התיאור ואת אובייקט ה-parameters כפי שהם. אם אתה צריך להסתעף על סוג השגיאה ב-Front-end, שים קוד ב-parameters — { code: 'OUT_OF_STOCK' }. כל חריגה אחרת מדווחת כשגיאה פנימית גנרית, וההודעה האמיתית הולכת רק ליומנים שלך, כך שפרטים פנימיים לעולם לא דולפים ללקוח.
קריאה לנקודת קצה מה-Mini App
ה-Mini App קורא לנקודת קצה לפי שם עם Telegram.WebApp.Serverless.call מה-
Mini Apps SDK
הרשמי, telegram-web-app.js — הסקריפט שה-Mini App שלך כבר כולל. הוא שולח את ה-init data עבורך וחוזר עם שגיאה או עם התוצאה של נקודת הקצה:
Telegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {
if (err) {
// err.message — התיאור; err.parameters — מה ש-EndpointError נשא
if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
return;
}
render(profile);
});
ה-input הוא אופציונלי וחייב להיות אובייקט פשוט. בלעדיו הקריאה היא Serverless.call('getProfile', callback).
err.type הוא 'ENDPOINT_ERROR' כאשר נקודת הקצה שלך זרקה EndpointError. כל ערך אחר אומר שהפלטפורמה סירבה לקריאה או שנקודת הקצה נכשלה באופן בלתי צפוי, למשל 'UNAUTHORIZED' כאשר ה-init data חסר או לא תקין.
שמות נקודות קצה הם כמו מזהי JavaScript — אותיות, ספרות ו-_, שלא מתחילים בספרה (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" } } }'
מה נפרס
כשאתה מריץ npx tgcloud push, ה-CLI אוסף כל קובץ .js תחת schema.js, lib/, handlers/, ו-endpoints/, ושולח את הסט המדויק הזה כמרחב המודולים של הפרויקט שלך — יחד עם תיקיית הבנייה של ה-Mini App, אם tgcloud.jsonc מציין אחת (ראה להלן). כל מה שנמצא בענן אך חסר בפרויקט שלך מוסר, כך שהמצב הפרוס תמיד משקף את התיקייה שלך — כולל מחיקות. שום דבר מחוץ ל-tgcloud/ לא נפרס כקוד — שאר שורש הפרויקט הוא שלך. בתוך tgcloud/, קובץ .js שלא נמצא באחד המיקומים האלה (tgcloud/util.js, tgcloud/helpers/x.js) מסומן, כך שהוא לא נשאר ללא פריסה בשקט. Markdown, קבצי dotfiles ו-.tgcloud/ לעולם לא נפרסים.
Mini App front-end
שורש הפרויקט הוא שלך, והוא המקום הטבעי עבור ה-Front-end של ה-Mini App שלך — בנוי עם React, Vue, Svelte או HTML פשוט, על ידי Vite או כל bundler אחר. Serverless מארח את תוצרי הבנייה שלו ליד הבוט שלך. push אחד פורס את הקוד והקבצים הסטטיים יחד, והאפליקציה מוגשת ב-https://app<app_id>.tgcloud.ai/ (push מדפיס את הכתובת המדויקת — השתמש בה ככתובת ה-URL של ה-Mini App שלך ב-
@BotFather
).
דיווח על שימוש לרעה. Mini Apps ב-tgcloud.ai מתפרסמים על ידי מפתחים עצמאיים. כדי לדווח על תוכן לא חוקי או שימוש לרעה בתת-דומיין של
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, מזהה את תיקיית הבנייה, ומוסיף סקריפט npm run 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" נדרש. זו תיקיית הבנייה, יחסית לשורש הפרויקט. שאר המפתחות של "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 | כלום — הגדר ייצוא סטטי קודם, ואז הגדר את "source" לתיקייה שלו בעצמך |
עם כל הגדרה אחרת, הגדר את "source" לתיקיית הבנייה שלך בעצמך.
שורת ה-$schema נותנת לעורך שלך השלמה אוטומטית ואימות לכל מפתח. הקובץ הוא JSON עם הערות ופסיקים בסוף מותרים. אם כלי אחר שלך צריך לקרוא אותו כ-JSON פשוט, תן לו את השם tgcloud.json במקום — אז הוא חייב להיות JSON קפדני.
בנה לפני שאתה פורס. push מעלה את התיקייה בדיוק כפי שהיא על הדיסק — למעט קבצים ותיקיות ששמם מתחיל בנקודה, שמדולגים — ולעולם לא מריץ את הבנייה שלך, לכן npx tgcloud push פשוט לאחר עריכת ה-Front-end יפרוס את הבנייה הקודמת. השתמש ב-npm run deploy (הוא מריץ את סקריפט ה-build שלך, ואז tgcloud push), או בנה בעצמך קודם.
אפשרויות ההגשה:
spa— הגשindex.htmlעבור נתיבים שלא קיימים ואין להם סיומת קובץ, כך שנתב צד-לקוח עובד בטעינה מחדש. נתיבים עם סיומת עדיין יקבלו404.immutable— תבניות לקבצים עם גיבוב תוכן (ה-/assets/*של Vite) שדפדפנים עשויים לשמור במטמון לשנה. השתמש בזה רק לקבצים עם גיבוב תוכן. כל קובץ אחר שמותאם כאן יישאר ישן בדפדפנים לשנה, ואין דרך להחזיר אותו.headers— כותרות תגובה לנתיבים שחוק מתאים להם (נתיב מדויק, או תחילית שמסתיימת ב-/*). ערךnullמסיר כותרת.redirects— חוקי הפניה, שבהם ההתאמה הראשונה מנצחת. קובץ קיים תמיד מנצח חוק.statusהוא301אלא אם כן תגדיר אחרת."static": "dist"— קיצור דרך ל-{ “source”: “dist” } כשאתה לא צריך כלום."static": false— הסר את האתר הפרוס בפריסה הבאה. מחיקת המפתח במקום זאת פשוט מפסיקה את ניהול הקבצים הסטטיים על ידי הפרויקט הזה, ומה שפרוס נשאר.
האתר מוגש משורש המארח שלו, כך שה-base (/) המוגדר כברירת מחדל של ה-bundler שלך הוא נכון. Telegram Web פותח Mini Apps בתוך iframe, אז אל תוסיף X-Frame-Options או מדיניות frame-ancestors מגבילה.
אותו מארח עונה ל-
endpoints
שלך תחת /api/ — זה המקום אליו Telegram.WebApp.Serverless.call('getProfile', …) הולך. אל תשים קבצים משלך תחת /api/ בתיקיית הבנייה.
מסד הנתונים
כל בוט מקבל מסד נתונים משלו — חנות מבוססת SQLite שנשמרת בין הרצות וזמינה לכל מודול דרך db. אתה מתאר את הטבלאות שלך ב-schema.js עם DSL קטן וטיפוסי; אתה קורא וכותב אותן עם בונה שאילתות שוטף; ואתה מפתח אותן עם הגירות (migrations) שעברו בדיקה.
מכיר את Drizzle ORM ? אז אתה כבר יודע איך לדבר עם מסד הנתונים כאן. ה-schema DSL ובונה השאילתות עוקבים אחרי Drizzle — בוני העמודות,
select().from().where(), האופרטורים,onConflictDoUpdate,.returning(), ותגית ה-sqlכולם מתנהגים כפי שהיית מצפה, כך שקריאת וכתיבת נתונים היא ה-API המוכר שאתה כבר משתמש בו. אתה רק מייבא אותו מ-sdk/db, ומספר מאפיינים ספציפיים לפלטפורמה (בעיקר ללא מפתחות זרים ) מצוינים איפה שהם עולים.
הצהרה על טבלאות
טבלאות הן named exports ב-schema.js. קריאה ל-table() בונה תיאור בזמן טעינה (זה לא נוגע במסד הנתונים); הפלטפורמה מגלה את הטבלאות המיוצאות כשאתה פורס את schema.js ומבצעת הגירה של מסד הנתונים כדי להתאים.
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הוא שם טבלת ה-SQL;columnsהוא מפה של JS property → הגדרת עמודה;extrasהוא callback אופציונלי(t) => ({ … })שבוtחושף את העמודות (t.createdהיא הפניה לעמודה ההיא) — הצהרת על אינדקסים ואילוצים ברמת הטבלה כאן.
סוגי עמודות
| 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 |
ארגומנט שם העמודה הוא אופציונלי — השמט אותו והמפתח של ה-JS ישמש. האופציה
modeשולטת איך ערכים מומרים בין SQLite ל-JavaScript.
mode |
נשמר כ | ערך JS |
|---|---|---|
boolean |
INTEGER 0/1 | boolean |
json |
TEXT (JSON) | כל אובייקט/מערך |
timestamp |
INTEGER (שניות יוניקס) | Date |
timestamp_ms |
INTEGER (מילישניות יוניקס) | Date |
bytes |
BLOB | Uint8Array |
boolean() ו-json() הם קיצורי דרך ל-integer(name, { mode: 'boolean' }) ו-text(name, { mode: 'json' }).
blob() קורא וכותב Uint8Array — לסביבת הריצה אין Buffer של Node (ו-Buffer הוא תת-מחלקה של Uint8Array, לכן זהו סוג הבסיס הנייד). ה-mode שלמעלה שולט רק איך ערך מקודד, ללא קשר לסוג האחסון של העמודה: כך ש-blob('col', { mode: 'json' }) הוא המקביל ל-BLOB של json() — אותו קידוד JSON, מוחזק בעמודת BLOB במקום TEXT.
TLDR:
blob()משתמש ב-Uint8Array.modeשולט בקידוד, לא באחסון, לכןblob(..., { mode: 'json' })שומר JSON כ-BLOB, בעוד ש-json()שומר אותו כ-TEXT.
מגדירי עמודות
מגדירי עמודות משתשרים על עמודה.
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('replaced by login') // מסמן את העמודה להסרה
.default()על עמודתjson()מקודד את הערך עבורך; ברירת מחדל שלsql`…`מועברת כפי שהיא..deprecated()היא סופית — ראה הגירות .
אינדקסים ואילוצים
אינדקסים ואילוצים מוצהרים ב-callback ה-extras, שבו העמודות נמצאות בטווח.
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})`) , // אינדקס ביטוי
active: index('idx_active').on(t.userId).where(sql`done = 0`) , // אינדקס חלקי
}));
מגדירים ברמת הטבלה משתשרים אחרי table(...): .strict(), .withoutRowid(), .deprecated('reason').
ללא מפתחות זרים (foreign keys)
סביבת הריצה רצה עם PRAGMA foreign_keys כבוי. מפתח זר מוצהר יהיה חסר תועלת בשקט — ללא cascaded, ללא הגנה על יתומים — מה שגרוע יותר מאי-קיום בכלל, לכן ה-DSL הופך את זה לבלתי אפשרי. ליתר דיוק, .references() ו-foreignKey() ברמת הטבלה זורקים שגיאה כשמוצהרים, וסכימה שמשתמשת בהם לא תיפרס.
עליך למדל מערכות יחסים עם עמודות רגילות (userId: integer('user_id')) ולאכוף שלמות בקוד האפליקציה שלך: הכנס הורים לפני ילדים, מחק ילדים לפני הורים, טפל בשגיאות כראוי וטאטא יתומים עם LEFT JOIN … WHERE parent.id IS NULL כשאתה צריך.
היעדר מפתחות זרים הוא אילוץ מכוון, לא פיספוס. קח זאת בחשבון מוקדם כשאתה מתכנן את הבוט שלך.
שאילתות
db הוא בונה שאילתות שוטף. כל שאילתה היא אסינכרונית — השיטות הסופיות (.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();
// הקרנה מותאמת אישית: { 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();
// ספירת שורות — עוזר, לא סופית של בונה:
await db.$count(todos); // כל השורות
await db.$count(todos, eq(todos.done, false)); // עם פילטר
- ניתנות לשרשור:
.where(),.orderBy(),.limit(),.offset(),.groupBy(),.having(),.distinct(). - סופיות:
.all(),.get(),.values(). - ספר שורות עם
db.$count(table, where?)(אוcount()בהקרנה).
הכנסה, עדכון, מחיקה:
await db.insert(todos).values({ userId: 1, text: 'Buy milk' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run(); // אצווה
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();
שים לב שהכנסת אצווה היא הצהרה אחת, לכן היא מוגבלת על ידי מגבלת המשתנים של SQLite (
שורות × עמודות); אם תעבור אותה, ההכנסה נכשלת — פרק אותה בעצמך.
אופרטורים מיובאים מ-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). הארגומנט השני של השוואה הוא ערך כברירת מחדל, אך עשוי להיות עמודה אחרת או sql`…` — למשל eq(a.x, b.y). אגרגטורים (count/sum/avg/min/max) הם מקטעי sql עבור הקרנת .select({ … }).
Raw SQL — כשבונה השאילתות לא מספיק, רד ל-raw SQL. המצב נבחר לפי שיטה: db.run לכתיבות, db.all להרבה שורות, db.get לשורה אחת, db.values לשורות כמערכים מיקומיים.
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, 'buy milk']]
כתיבה מתפענחת ל-run result — rowsAffected, lastInsertRowid, ו-rows, שנושאים סעיף RETURNING אם ה-SQL הכיל אחד כזה. השתמש ב-db.values כשהשאילתה מקרינה שתי עמודות עם אותו שם (join כמו SELECT u.id, o.id): db.all ממפתח שורות לפי שם עמודה, לכן ה-id השני ידרוס את הראשון.
תגית ה-sql`…` הופכת ${value} לפרמטר קשור ו-${table.column} למזהה, ומחברת מקטעי sql מקוננים. השתמש ב-sql.raw('…') עבור literal ללא פרמטרים.
שאילתות raw אינן קשורות לטבלה, לכן השורות שלהן חוזרות ללא המרת מצב (mode conversion) — בוליאני הוא
0/1, עמודת JSON היא מחרוזת, timestamp הוא מספר. רק הבונה הקשור לטבלה ממיר ערכים.
הגירות (Migrations)
מסד הנתונים שלך משתנה ככל שהבוט שלך גדל. הפלטפורמה שומרת על זה בטוח על ידי הפרדת פריסת קוד מ-שינוי נתונים, ועל ידי סיווג כל שינוי סכימה לפי כמה הוא מסוכן.
פריסה לעולם לא נוגעת במסד הנתונים. כשאתה מריץ npx tgcloud push על schema.js שונה, הפלטפורמה מתעדת את הסכימה החדשה ואומרת לך מה מסד הנתונים היה משנה — אך לא מחילה כלום:
npx tgcloud push # פרוס schema.js; מדווח על שינויי DB ממתינים
npx tgcloud migrate # סקור והחל אותם
npx tgcloud migrate מחשב את ההבדל בין הסכימה שלך למסד הנתונים החי ומדריך אותך דרכו. אתה תמיד נשאל לפני שכל דבר מוחל. זה אומר שפריסת קוד שגרתית לעולם לא יכולה להפעיל הגירת נתונים בטעות.
כל שינוי ממתין נושא סטטוס שקובע איך migrate מתייחס אליו:
| סטטוס | משמעות | ב-migrate |
|---|---|---|
| safe | תוספתי ולא חוסם — טבלה, עמודה או אינדקס חדש | מוחלים יחד בצעד אחד, עם אישור |
| warning | עלול להרוס או איטי — מחיקת משהו, או אינדקס על טבלה ענקית | מוצג אחד בכל פעם, כל אחד מאושר בנפרד |
| manual | לא ניתן לביצוע אוטומטית — למשל שינוי סוג עמודה | מוצג עם הכוונה; אתה מבצע זאת ידנית |
| undocumented | קיים במסד הנתונים אך לא בסכימה שלך | מוצג למודעות; לא מוחל |
שינויים בטוחים הם מהירים והפיכים ברוחם, לכן הם עוברים יחד. כל אזהרה היא אישור מכוון ואישי — אין “החל הכל” לשינויים הרסניים.
שינויים ידניים מגיעים עם סיבה ופעולה מוצעת. migrate מסתיים בסיכום: כמה שינויים הוחלו, דולגו, ממתינים לתיקון ידני, או אינם בסכימה שלך.
ראה
migrateעבור הדגלים (--dry-run,--safe,--yes,--local).
הסרת דברים
מחיקת טבלה או עמודה מ-schema.js לא מוחקת אותן — זה יהיה הופך מחיקה מקרית לקטסטרופלית. כדי להסיר משהו, סמן אותו כ-deprecated:
// מחק עמודה
text('email').deprecated('replaced by login')
// מחק טבלה שלמה
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('unused');
בהגירה הבאה, האובייקט ה-deprecated מופיע כמחיקה בסטטוס אזהרה שאתה מאשר בנפרד. ברגע שהוא נעלם, הסר את ההצהרה.
שינוי סוג עמודה
שינויי סוג הם ידניים — SQLite לא תמיד יכול לעשות אותם במקום, ואילוץ ערכים קיימים הוא עניין של שיקול דעת. migrate יראה את השינוי והסיבה שלו; בצע אותו בעצמך עם raw SQL (db.run(...)), בדרך כלל על ידי יצירת עמודה או טבלה חדשה, העתקת נתונים, והחלפה.
ה-SDK
בזמן ריצה למודול יש ספרייה אחת: sdk. היא מאגדת את שלושת הדברים ש-backend לבוט צריך — מסד נתונים, ה-Telegram Bot API, ו-HTTP יוצא — ללא שום דבר להתקנה וללא פרטי התחברות להגדרה. מסד הנתונים (db) מכוסה ב-
מסד הנתונים
; סעיף זה מכסה את api, fetch, ואת ה-console הגלובלי.
import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk'; // כל המשטח
// או מתתי-מודולים:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
| ייבוא | מה זה |
|---|---|
db |
מסד נתונים — בונה שאילתות ו-schema DSL → מסד הנתונים |
api |
Telegram Bot API — api.sendMessage(...) →
למטה
|
fetch |
HTTP יוצא → למטה |
InputFile |
קובץ (בתים + שם) להעלאה — עובד עם api ו-fetch →
קבצים
|
EndpointError |
סרב לקריאה של Mini App עם הודעה שהוא יכול להציג → Endpoints |
ייבא את מודולי הפרויקט שלך לפי נתיב יחסי עם הסיומת .js (from '../schema.js', from '../lib/cart.js'). ראה
מערכת המודולים
.
ה-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: 'Hello!' });
await api.editMessageText({ chat_id, message_id, text: 'Updated' });
await api.answerCallbackQuery({ callback_query_id, text: 'Done' });
מעטפת התגובה נפתחת. ה-Bot API בדרך כלל עוטף תוצאות ב-{ ok: true, result: … }. api מחזיר את ה-result ישירות — getMe() מתפענח לאובייקט המשתמש, לא למעטפת. פרמטרים משתמשים בשמות ה-snake_case של ה-Bot API עצמו (chat_id, message_id, reply_markup, …).
כישלונות זורקים BotApiError. כאשר ה-Bot API מחזיר { ok: false }, הקריאה זורקת BotApiError במקום להחזיר ערך falsy, כך שלא תוכל להתעלם מזה בטעות. השגיאה נושאת .code (ה-error_code של ה-Bot API), .description (הודעה קריאה לאדם), .method (איזו שיטה נכשלה), ו-.parameters (נתונים נוספים כגון retry_after ב-429, או migrate_to_chat_id). תפוס אותה כדי לטפל בכישלון צפוי וזרוק את השאר מחדש:
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;
}
}
קבצים
עבוד עם בתים של קבצים, לא רק file_ids — העלה קבצים חדשים והורד קיימים.
העלאה — InputFile. עטוף בתים גולמיים ב-InputFile (מחזיק פשוט: בתים, שם קובץ, ו-type MIME אופציונלי) והעבר אותו לכל מקום ששיטת Bot API מצפה לקובץ:
import { api, InputFile } from 'sdk';
const doc = new InputFile(bytes, 'report.pdf', { type: 'application/pdf' });
await api.sendDocument({ chat_id, document: doc });
// קבצים מקוננים בתוך פרמטרים עובדים גם כן — למשל אלבום:
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' }) },
]});
הבתים מוזרמים ל-Telegram כהעלאה multipart אמיתית — הקובץ לא מבוסס (buffered) כולו לתוך קריאה אחת לפלטפורמה — אך מגבלות ה-Bot API של Telegram עדיין חלות: בוט יכול כרגע להעלות עד 50 MB לקובץ (ו-10 MB עבור תמונה).
הורדה — לפי file_id. שני עוזרים מושכים את הבתים של קובץ חזרה. הפלטפורמה מפוענחת את ה-file_id עבורך (היא מריצה getFile ומביאה את התוכן מאחורי הקלעים), כך שלעולם לא תיגע בטוקן הבוט או ב-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 */ } // או בצע לו buffer: await stream.bytes()
ה-Bot API של Telegram מגביל הורדות
getFileל-20 MB, אז זהו התקרה עבור שני העוזרים.
HTTP
fetch הוא לקוח דמוי fetch לקריאה לעולם החיצון — 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();
התגובה משקפת את פלטפורמת האינטרנט: res.status, res.statusText, res.ok (נכון עבור 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()), וקוראי גוף await res.json() / await res.text().
אתה יכול גם לקרוא את הגוף באופן הדרגתי כ-stream —
for await (const chunk of res.body) { … }— שזו הדרך בה אתה צורך אירועים שנשלחו מהשרת (server-sent events) או פלט token-by-token מ-AI APIs.
עזרי גוף מגדירים את ה-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
גופי בקשה יכולים גם לשאת קבצים ובתים גולמיים: העבר Uint8Array, InputFile, או FormData (עבור multipart/form-data) כ-body, והבתים מוזרמים החוצה — כך שאתה יכול להעלות ל-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', 'from my bot');
const res = await fetch('https://api.example.com/upload', { method: 'POST', body: form });
הוא מתנהג אחרת כמו ה-fetch הסטנדרטי שאתה כבר מכיר, עם שני אילוצים:
- ה-תגובה נקראת כטקסט —
await res.json()/await res.text(), או מוזרמת chunk-by-chunk עםres.body. - ה-תגובה הכוללת מוגבלת ל-30 MB. המגבלה הזו מכסה את כל התגובה — הזרמה עם
res.bodyמאפשרת לך לעבד גוף גדול בהדרגה, אך היא לא מעלה את המגבלה.
תיעוד (Logging)
ה-console הגלובלי הסטנדרטי זמין — אין מה לייבא, הוא פשוט שם כפי שבכל JavaScript. הפלט שלו נתפס ומוצג על ידי npx tgcloud run, מה שהופך אותו לכלי הניפוי (debugging) העיקרי שלך במהלך הפיתוח.
console.log('processing', { chatId: id }); // log / debug — פשוט
console.info('started'); // info — כחול
console.warn('rate limited'); // warn — צהוב
console.error(err); // error — אדום, עם stack trace
כל שורה מתויגת עם ה-[file:line] שממנו היא הגיעה. console.error ו-console.trace מצרפים stack מלא, בעוד console.warn לא. כשאתה מריץ npx tgcloud run על מודול, השורות האלה מודפסות עם תחילית צבעונית לפי רמה, הזמן מאז שהריצה התחילה, והמקור — ראה
run
.
ממשק שורת פקודה (CLI)
tgcloud הוא הגשר בין תיקיית הפרויקט שלך לענן. הוא מכין תבניות פרויקטים, מראה לך מה השתנה, פורס, מריץ מודולים, ומחיל הגירות מסד נתונים. הוא דורש
Node.js
18 ומעלה. שתי דרכים להשיג אותו:
# מומלץ — צור פרויקט עם ה-CLI מותקן לתוכו:
npm create @tgcloud/bot example_bot
# או התקן את ה-CLI גלובלית ואתחל תיקייה ריקה:
npm install -g @tgcloud/cli
tgcloud init
חבילת ה-npm היא @tgcloud/cli, והפקודה שהיא מתקינה היא tgcloud. כל פקודה עובדת מכל תת-תיקייה של הפרויקט שלך.
| פקודה | מטרה |
|---|---|
init
|
צור תבנית פרויקט חדש בתיקייה הנוכחית |
add
|
צור תבנית מודול חדש (מטפל, נקודת קצה או מודול lib) |
login
|
קשר את הפרויקט לבוט (שומר את הטוקן) |
status
|
הצג מה השתנה מקומית לעומת הענן |
diff
|
הצג את השינויים שורה-אחר-שורה |
push
|
פרוס מודולים ששונו ותוצרי הבנייה של ה-Mini App לענן |
migrate
|
החל שינויי סכימה על מסד הנתונים |
run
|
הרץ מודול על הפלטפורמה ללא פריסה |
fetch
|
בדוק מה פרוס, ללא שינוי קבצים מקומיים |
pull
|
הבא קבצים מקומיים לקו אחד עם הענן |
reset
|
זרוק שינויים מקומיים; שחזר ממצב הענן |
webhook
|
בדוק וסנכרן מחדש את ה-webhook שמנוהל על ידי הפלטפורמה |
completion
|
הדפס סקריפט השלמה לשורה (bash/zsh/fish) |
upgrade
|
העבר פריסת פרויקט לפני 0.2.0 ל-tgcloud/ |
אימות (Authentication)
פרויקט קשור לבוט אחד על ידי הטוקן שלו, שיש לו את הפורמט app<id>:<secret>. החלק של app<id> הוא ציבורי ועשוי להיות מודפס; הסוד לעולם לא מופיע ביומנים או בשגיאות.
הטוקן מפוענח בסדר הזה:
TGCLOUD_TOKENמשתנה סביבה — עבור CI; לעולם לא נכתב לדיסק..tgcloud/credentials— נכתב על ידיnpx tgcloud login.- לא זה ולא זה → שגיאה שמכוונת אותך ל-
npx tgcloud login.
ה-CLI לעולם לא מבקש טוקן באמצע פקודה. התחברות היא תמיד צעד ה-login המפורש. אם טוקן שמור הופך ללא תקין, ה-CLI מסיר אותו ומבקש ממך לעשות login שוב.
init
npx tgcloud init
יוצר תבנית פרויקט חדש בתיקייה הנוכחית: תיקיית tgcloud/ עם schema.js, lib/, handlers/ ומטפל התחלתי, פלוס tgcloud.jsonc, AGENTS.md, docs/, ותיקיית המצב .tgcloud/. הרץ אותו בשורש של פרויקט front-end קיים גם כן — הוא מוסיף את תיקיית tgcloud/ והמסמכים, ולעולם לא דורס את הקבצים שלך. אם הוא מזהה את ה-front-end, הוא גם מצביע ב-tgcloud.jsonc על תיקיית הבנייה ומוסיף סקריפט deploy (בנייה, אז push) ל-package.json שלך — רק אם אין לך סקריפט deploy כבר. ראה
Mini App front-end
.
init מסרב ליצור פרויקט בתוך פרויקט אחר. הרצה חוזרת שלו בשורש של פרויקט קיים זה בסדר ופשוט מוסיפה את מה שחסר.
add
npx tgcloud add <target>
יוצר תבנית מודול חדש בודד, מחובר ומוכן לעריכה — מטפל, נקודת קצה או מודול lib/.
npx tgcloud add handlers/callback_query # מטפל עדכון חדש
npx tgcloud add endpoints/getProfile # נקודת קצה חדשה עבור ה-Mini App
npx tgcloud add lib/cart # מודול משותף חדש
שים לב ש-
addלעולם לא דורס קובץ קיים.
ה-<target> הוא נתיב המודול (סיומת .js בסוף היא אופציונלית). עבור handlers/, השם חייב להיות סוג עדכון של Telegram. הפלטפורמה מפרסמת את הסט התקין, לכן שם לא תקין נדחה מראש. handlers/ ו-endpoints/ הם שטוחים, בעוד lib/ עשוי להיות מקונן (lib/payments/stripe).
שם המודול נדרש. מתן רק התיקייה היא שגיאה — אך מועילה: עבור handlers/ היא מפרטת את סוגי העדכונים שאין לך עדיין, כך שתוכל להעתיק אחד.
$ npx tgcloud add handlers
Error: Specify a name, e.g. "handlers/callback_query". Available handlers/ types: callback_query, inline_query, chat_member, …
השלמת
<Tab>מציעה את אותו סט — ראהcompletion.
לקובץ שנוצר יש export default פעיל, כך שהמטפל פעיל ברגע שאתה פורס — אין מה לבטל הערה. פרוס את המודול החדש עם
push
.
login
npx tgcloud login
מבקש את טוקן הגישה ל-CLI שלך — מ-
@BotFather
→ הבוט שלך → Serverless → CLI Access → טוקן גישה, טוקן נפרד מ-
טוקן ה-API של הבוט שלך
— מאמת אותו מול הפלטפורמה, ושומר אותו ב-.tgcloud/credentials.
loginהיא הפקודה ה-יחידה שמבקשת טוקן, והיא דורשת טרמינל אינטראקטיבי. ב-CI, הגדרTGCLOUD_TOKENבמקום.
status
npx tgcloud status
מראה, לפי קובץ, מה השתנה בין תיקיית העבודה שלך לבין העותק הפרוס: שונה, חדש, נמחק, לא השתנה. זה עובד לא מקוון, בהשוואה למצב הענן נכון ל-push, pull או fetch האחרון שלך. הוא גם מזהיר לגבי קבצי .js תועים בתוך tgcloud/ שלא ייפרסו.
עבור פרויקט עם Mini App, status גם מראה מה השתנה בתיקיית הבנייה שלו מאז הפריסה האחרונה.
diff
npx tgcloud diff
כמו status, אך מראה את ההבדלים בפועל שורה-אחר-שורה עבור מודולים ששונו. גם לא מקוון.
push
npx tgcloud push [files...]
פורס את הפרויקט שלך לענן באצווה אטומית אחת.
ללא ארגומנטים, הוא פורס את כל הפרויקט, והמצב הפרוס מותאם לשקף את התיקייה שלך במדויק — מודולים שמחקת מקומית מוסרים בענן.
עם ארגומנטים של קבצים או תיקיות (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/), הוא פורס רק את השינויים בקבצים האלה. push ממוקד לעולם לא מוחק מודולים אחרים. ניתן לציין מטרה כנתיב קובץ (tgcloud/handlers/message.js), הנתיב בתוך tgcloud/ (handlers/message.js) או שם המודול (handlers/message) — כולם אומרים את אותו המודול.
האופציה היחידה שלו היא --force — לדלג על בדיקת העקביות ולדרוס כל מה שנמצא בענן. השתמש בזה רק כשאתה בטוח (ראה
שמירה על סנכרון
).
אם tgcloud.jsonc מציין תיקייה סטטית, push מלא פורס אותה באותה אצווה — רק הקבצים ששונו מועלים — ומדפיס את כתובת האתר. בנה קודם, כי push לעולם לא מריץ את הבנייה שלך (npm run deploy עושה את שניהם). npx tgcloud push dist/ פורס את הקבצים הסטטיים לבדם, ו-push שמציין רק מודולים משאיר אותם ללא שינוי.
לאחר פריסה, אם schema.js השתנה ומסד הנתונים לא מסונכרן, push מדפיס סיכום של השינויים הממתינים ומציע npx tgcloud migrate. הוא לעולם לא מחיל אותם בעצמו.
migrate
npx tgcloud migrate
מחילה את שינויי הסכימה שלך על מסד הנתונים. היא מחשבת את ההבדל בין schema.js למסד הנתונים החי, ואז מדריכה אותך דרכו צעד אחד בכל פעם עם מונה [N/M] רץ:
- סיכום קצר של כל השינויים הממתינים.
- שינויים בטוחים, מוחלים יחד בצעד יחיד עם אישורך.
- אזהרות (מחיקות, פעולות איטיות), אחת בכל פעם, כל אחת מאושרת בנפרד.
- שינויים ידניים, מוצגים עם סיבה ופעולה מוצעת, לא מוחלים אוטומטית.
- אובייקטים לא מתועדים (במסד הנתונים אך לא בסכימה שלך), מוצגים למודעות.
זה מסתיים בסיכום: הוחלו, דולגו, ממתינים לתיקון ידני, לא בסכימה.
ראה הגירות עבור המודל.
אופציות: --dry-run (הדפס הכל, אל תחיל כלום), --safe (החל אוטומטית שינויים בטוחים, דלג על אזהרות), --yes (החל אוטומטית שינויים בטוחים וכל אזהרה, דלג על ידני — השתמש בזהירות), --local (השוואה מול schema.js המקומי שלך במקום הפרוס). ללא דגל, migrate דורש טרמינל ושוגה בסביבה לא אינטראקטיבית במקום לנחש.
run
npx tgcloud run <module> [args] [--ctx <json5>]
מריץ מטפל או נקודת קצה על הפלטפורמה ללא פריסה, תוך שימוש בקבצים המקומיים הנוכחיים שלך. זהו הלופ הפנימי המהיר לבדיקת לוגיקה.
<module>— שם פשוט (חיפוש תחתhandlers/ו-endpoints/) או נתיב כמוhandlers/message. שם פשוט שקיים בשתי התיקיות חייב להיות מצוין עם התיקייה שלו.[args]— ה-payload שעובר למטפל, כתוב ב- JSON5 כך שתוכל לדלג על מרכאות למפתחות. זה אובייקט סוג העדכון שהמטפל שלך מקבל (למשל הודעה עבורhandlers/message), או ה-inputשל נקודת קצה, והוא חייב להיות אובייקט — לא מערך או מחרוזת פשוטה.--ctx <json5>— אובייקט ההקשר של המודול (הארגומנט השני שלו), גם JSON5. השתמש בו כדי לספק את מה שהקוד שלך קורא מ-ctx— העדכון הגולמי עבור מטפל,--ctx '{ update: { update_id: 1 } }', או ה-init data עבור נקודת קצה,--ctx '{ initData: { user: { id: 1 } } }'.runלא מאמת את זה, מכיוון שאתה בודק את הלוגיקה שלך, לא את הבדיקות של הפלטפורמה.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
הפלטפורמה מריצה את המודול מול מרחב המודולים שהורכב מהפרויקט המקומי שלך (כך שגם קוד lib/ ששונה מקומית משמש) ומחזירה את ערך ההחזרה, כל מה שתועד עם console.*, והזמן שחלף. קרא ארגומנטים גדולים מקובץ עם npx tgcloud run handlers/message "$(cat message.json5)".
fetch
npx tgcloud fetch
בודק מה פרוס כעת, מבלי לגעת בקבצי העבודה שלך. אחריו, status ו-diff משווים מול מצב הענן האחרון. שימושי להסתכל על קונפליקט לפני החלטה איך לפתור אותו.
pull
npx tgcloud pull
מביא את הפרויקט המקומי שלך לקו אחד עם הענן על ידי עדכון קבצי העבודה שלך למצב הפרוס.
pull לעולם לא כותב את תיקיית הבנייה של ה-Mini App שלך.
reset
npx tgcloud reset
זורק את השינויים המקומיים שלך ומשחזר את המודולים ממצב הענן האחרון הידוע. השתמש בזה כדי לזרוק ניסוי. תיקיית הבנייה של ה-Mini App לא נוגעת.
webhook
npx tgcloud webhook
npx tgcloud webhook sync [--drop-pending]
Telegram מעביר עדכונים לבוט שלך דרך webhook, שהפלטפורמה מנהלת עבורך — לעולם לא תצביע עליו ידנית. npx tgcloud webhook מראה את מצבו הנוכחי: ה-URL, רשימת ה-allowed_updates, כמה עדכונים ממתינים, שגיאת העברה אחרונה (אם יש), והאם הוא מסונכרן עם המטפלים הפרוסים שלך.
“מסונכרן” אומר שה-webhook מצביע על הפלטפורמה ורשימת ה-allowed_updates שלו תואמת למטפלים שפרסת — כך ש-Telegram מעבירה בדיוק את סוגי העדכונים שאתה מטפל בהם, ולא שום דבר אחר. פריסת מטפל חדש (או הסרת אחד) יכולה להשאיר את ה-webhook לא מסונכרן עד שהוא יתרענן; npx tgcloud status מסמן זאת גם כן.
npx tgcloud webhook sync מתקן זאת — הוא מצביע מחדש את ה-webhook על הפלטפורמה ובונה מחדש את ה-allowed_updates מהמטפלים הפרוסים שלך. הוסף --drop-pending כדי לזרוק עדכונים ש-Telegram כבר תורנה לפני הסנכרון (אחרת הם יועברו ברגע שה-webhook יהיה תקין שוב).
completion
הערה: השלמה אוטומטית (tab-completion) עובדת רק כש-
tgcloudפשוט נמצא ב-PATHשלך — אז התקן אותו גלובלית (npm install -g @tgcloud/cli) או שים את ה-binary ב-PATH. זה לא יכול להתחבר ל-npx.
tgcloud completion <bash|zsh|fish>
מדפיס סקריפט השלמה לשורה ל-stdout. הפעל אותו פעם אחת, אז <Tab> משלים פקודות, דגלים, תיקיות מודולים, סוגי עדכוני המטפלים שאין לך עדיין, והמודולים המקומיים שאפשר להריץ.
# 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
הפעל מחדש את הטרמינל (או טען מחדש את הקובץ) לאחר מכן. הרצת tgcloud completion ללא shell מדפיסה את ההוראות האלה שוב.
upgrade
npx tgcloud upgrade [--dry-run]
הגירה חד-פעמית עבור פרויקטים שנוצרו לפני CLI 0.2.0, ששמרו את schema.js, lib/ ו-handlers/ בשורש הפרויקט. היא מעבירה אותם ל-tgcloud/. הקישור שלך לבוט נשמר, ושום דבר לא צריך פריסה מחדש. --dry-run מדפיס את המעברים המתוכננים ללא נגיעה בכלום. פקודות אחרות לא רצות על הפריסה הישנה עד שתשדרג.
שמירה על סנכרון (Staying in sync)
לכל פרויקט יש גרסה (revision) שעולה באופן מונוטוני בענן, שקופצת בכל פריסה. ה-CLI זוכר את הגרסה האחרונה שהוא סנכרן איתה ושולח אותה בכל push. אם הענן התקדם — בגלל שמכונה אחרת או חבר צוות פרס — ה-push נדחה במקום לדרוס בשקט את עבודתם, וה-CLI מציע שלוש דרכים קדימה:
npx tgcloud fetch # ראה מה השתנה בענן, אז בדוק שוב
npx tgcloud pull # הבא את העדכון האחרון לקבצי העבודה שלך
npx tgcloud push --force # דרוס את מצב הענן (מסוכן)
בדיקת העקביות האופטימית הזו היא הסיבה שאתה יכול לחלוק בוט עם צוות ללא תהליך פריסה נעול. פקודות יוצאות בערך לא-אפס בכישלון — פריסה נדחתה, הגירה נכשלה, שגיאת אימות, מודול שזרק שגיאה במהלך run — כך שהן מתחברות בצורה נקייה בסקריפטים ובצינורות CI.