Telegram Serverless pozwala na uruchamianie kodu backendowego dla Twojego bota i aplikacji Mini App bezpośrednio na infrastrukturze Telegrama — bez potrzeby konfigurowania serwerów, utrzymywania kontenerów czy martwienia się o skalowanie. Piszesz zwykłe moduły JavaScript, wdrażasz je jednym poleceniem, a Telegram uruchamia je w szybkim, izolowanym środowisku V8, które znajduje się tuż obok Bot API i wbudowanej bazy danych.
Jeśli kiedykolwiek musiałeś podłączać bota do VPS, funkcji chmurowej lub panelu hostingowego tylko po to, by obsłużyć komendę /start, to jest to etap, który możesz teraz pominąć.
Dlaczego serverless
Bot Telegrama to w gruncie rzeczy program reagujący na aktualizacje. Tradycyjnie trzeba było hostować go w miejscu, które jest zawsze włączone, dostępne i bezpieczne — a następnie utrzymywać je w takim stanie. Telegram Serverless całkowicie usuwa tę warstwę:
- Brak infrastruktury. Nie ma maszyn do wynajęcia, łatania czy monitorowania. Twój kod działa na żądanie i automatycznie skaluje się wraz z Twoim botem.
- Wbudowane dane uwierzytelniające. Telegram Bot API, baza danych oparta na SQLite oraz wychodzący ruch HTTP są dostępne dla każdego modułu “od zaraz” — nic nie trzeba instalować ani konfigurować.
- Szybka, izolowana egzekucja. Każde wywołanie działa w lekkim izolacie V8, blisko systemów Telegrama, dzięki czemu wywołania Bot API i bazy danych są szybkie i niezawodne.
- Prawdziwy przepływ pracy programisty. Projekt znajduje się w folderze na Twoim komputerze pod kontrolą wersji. Edytujesz pliki, widzisz dokładnie, co się zmieniło, wdrażasz atomowo i aktualizujesz schemat bazy danych za pomocą sprawdzonych migracji — tak, jak pracujesz ze wszystkim innym.
Model myślowy: Jak działa projekt
Pracujesz w trzech miejscach, które płynnie się ze sobą łączą:
| Miejsce | Co się tam znajduje |
|---|---|
| Folder projektu | Moduły JavaScript — schemat, współdzielony kod, handlery aktualizacji, punkty końcowe Mini App — oraz, jeśli posiadasz, frontend Mini App |
| Chmura | Wdrożona kopia tych modułów, baza danych Twojego bota oraz pliki Mini App |
CLI tgcloud |
Pomost — pokazuje różnice i synchronizuje je |
Nigdy nie logujesz się przez SSH. Edytujesz pliki lokalnie, uruchamiasz npx tgcloud push, a platforma zajmuje się resztą. Ruch Twojego bota jest obsługiwany przez wdrożone moduły; Twoja baza danych jest trwała pomiędzy wywołaniami.
Projekt składa się z zaledwie czterech rodzajów kodu, wewnątrz jednego folderu tgcloud/:
tgcloud/
├─ endpoints/ # funkcje wywoływane przez Twoją Mini App
├─ handlers/ # funkcje reagujące na typy aktualizacji Telegrama
├─ lib/ # współdzielony kod, który importujesz skądkolwiek
└─ schema.js # Twoje tabele bazy danych
Gdy nadejdzie aktualizacja — wiadomość, naciśnięcie przycisku, inline query — Telegram kieruje ją do odpowiedniego handlera (handlers/message.js, handlers/callback_query.js, …) i wywołuje jego domyślny eksport. Ta funkcja komunikuje się z Bot API i bazą danych poprzez SDK, a następnie kończy działanie. To cała pętla. Aktualizacja bez pasującego handlera jest po prostu ignorowana, więc dodajesz tylko te handlery, których potrzebujesz.
Mini App jest opcjonalna. Jeśli Twój bot ją posiada, jej frontend znajduje się w tym samym projekcie, obok tgcloud/. Serverless hostuje wynik kompilacji i uruchamia wywoływane przez nią endpoints/. Zobacz
Frontend Mini App
.
Szybkie demo
Oto kompletny, działający bot demonstracyjny. Odpowiada na każdą wiadomość i pamięta, ile ich otrzymał od każdego czatu.
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;
// Wstaw licznik lub zwiększ go, jeśli ten czat już go posiada — i pobierz
// wynikowy wiersz w tym samym poleceniu poprzez .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: `Cześć! Widziałem od Ciebie ${row.seen} wiadomość(i).`,
});
}
Wdróż go:
npx tgcloud push # prześlij moduły
npx tgcloud migrate # utwórz tabelę `counters`
To żywy bot z trwałym stanem i bez serwera. Wszystko w nim — api, db, DSL table() — jest opisane w poniższych sekcjach.
Serverless to ogólny backend dla botów Telegrama i Mini Apps, a nie szablon dla jednego typu aplikacji. Jest idealny dla:
- Konwersacyjnych botów AI, które muszą przechowywać stan użytkownika w bazie danych.
- Backendów Mini App, które przechowują dane użytkowników i serwują dynamiczną zawartość.
- Gier i narzędzi — w tym rankingów, quizów i innych.
- Automatyzacji i integracji, które wywołują zewnętrzne API HTTP i przesyłają wyniki na czaty.
Pierwsze kroki
Ten przewodnik poprowadzi Cię od pustego folderu do działającego bota, który odpowiada na wiadomości i przechowuje dane. Zakłada, że masz zainstalowane
Node.js
w wersji 18 lub nowszej oraz bota zarejestrowanego w
@BotFather
. Na końcu będziesz znać każdą komendę potrzebną w codziennej pracy: push, migrate, run oraz status.
Zanim przejdziesz dalej, włącz Serverless. W @BotFather otwórz swojego bota → Serverless i włącz tę funkcję. To aktywuje tę funkcjonalność dla tego bota i odblokowuje token dostępu CLI, handlery, bibliotekę oraz bazę danych.
1. Stwórz projekt
Najszybszym sposobem na start jest kreator projektu, który tworzy szkielet projektu i instaluje w nim CLI:
npm create @tgcloud/bot example_bot
cd example_bot
Argument to folder docelowy: przekaż . aby utworzyć projekt w obecnym folderze, lub dowolną ścieżkę. Działa to również w istniejącym folderze i nigdy nie nadpisuje plików, które już posiadasz.
Otrzymujesz gotowy do edycji projekt:
example_bot/
├─ tgcloud/ # wszystko, co uruchamia platforma
│ ├─ handlers/
│ │ └─ message.js # startowy handler wiadomości (odsyła tekst)
│ ├─ lib/ # tutaj trafiają współdzielone moduły (na start puste)
│ └─ schema.js # Twoje tabele bazy danych
├─ docs/
│ └─ tgcloud-sdk.md # referencja SDK (dla Ciebie i narzędzi AI)
├─ AGENTS.md # instrukcja dla asystentów AI
├─ package.json
└─ tgcloud.jsonc # konfiguracja projektu
Wygenerowane pliki są samokomentujące się — każdy zawiera przykłady tego, co możesz zrobić dalej.
CLI instaluje się w projekcie jako lokalna zależność programistyczna, więc uruchamiasz je przez npx tgcloud <command> (npx znajduje kopię w node_modules projektu), lub przez skróty npm run, które kreator dodaje do package.json (npm run deploy, npm run status). Domyślnie w PATH nie ma globalnego polecenia tgcloud.
Możesz też zainstalować je globalnie — npm install -g @tgcloud/cli — jeśli wolisz wpisywać krótkie tgcloud z dowolnego miejsca. Jest to przydatne do uruchamiania tgcloud init w pustym folderze. W obu przypadkach otrzymasz ten sam projekt.
Budujesz Mini App? Zacznij od frontendu. Stwórz go swoim standardowym narzędziem, a następnie uruchom kreator projektu w tym folderze. Zobacz Frontend Mini App .
2. Połącz swojego bota
Każdy projekt jest przypisany do jednego bota. Połącz je za pomocą login, który zapyta o Twój token dostępu CLI (
@BotFather
→ Twój bot → Serverless → CLI Access → Access token — to osobny token od
tokena API bota
) i zapisze go lokalnie:
npx tgcloud login
Token ma postać app<id>:<secret>. CLI przechowuje go w .tgcloud/ (folder jest ignorowany przez git) i nigdy nie wyświetla części sekretnej. Login to jedyny moment, w którym będziesz o niego pytany — zobacz
Uwierzytelnianie
, aby dowiedzieć się, jak tokeny są obsługiwane w CI.
3. Rozejrzyj się
Dwie komendy powiedzą Ci, na czym stoisz w dowolnym momencie, obie działają offline:
npx tgcloud status # co zmieniło się lokalnie w stosunku do wdrożonej kopii
npx tgcloud diff # zmiany linijka po linijce
Zaraz po init wszystko jest nowe i jeszcze nic nie zostało wdrożone. status pokazuje pliki startowe czekające na przesłanie.
4. Wdróż
Prześlij swoje moduły do chmury:
npx tgcloud push
push przesyła każdy zmieniony moduł w jednej atomowej paczce i aktualizuje lokalny zapis tego, co znajduje się w chmurze. Twój bot jest aktywny: otwórz go w Telegramie i wyślij wiadomość — startowy handler odeśle ją z powrotem.
Wdrażanie nigdy nie dotyka bazy danych. Przesyłanie kodu i zmiana schematu bazy danych to celowo rozdzielone kroki, dzięki czemu wdrożenie kodu nigdy nie zaskoczy Cię migracją danych. Do tego służy kolejny krok.
5. Dodaj tabelę w bazie danych
Sprawmy, by bot coś zapamiętał. Otwórz tgcloud/schema.js i zadeklaruj tabelę:
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())`) ,
});
Wdróż schemat, a następnie zastosuj go do bazy danych:
npx tgcloud push # przesyła nowy schemat
npx tgcloud migrate # tworzy tabelę `messages`
push raportuje, że schemat nie jest zsynchronizowany i pokazuje oczekującą zmianę, ale niczego nie stosuje. migrate przeprowadzi Cię przez zmianę i po potwierdzeniu utworzy tabelę. Ten dwuetapowy model — oraz co dzieje się przy ryzykowniejszych zmianach, takich jak usuwanie — jest opisany w
Migracjach
.
6. Zapisuj i czytaj dane
Teraz użyj tabeli w swoim handlerze. Edytuj tgcloud/handlers/message.js:
import { api, db } from 'sdk';
import { eq } from 'sdk/db';
import { messages } from '../schema.js';
export default async function (message) {
// Zapisz tę wiadomość.
await db.insert(messages)
.values({ chatId: message.chat.id, text: message.text })
.run();
// Policz, ile zapisaliśmy dla tego czatu.
const count = await db.$count(messages, eq(messages.chatId, message.chat.id));
await api.sendMessage({
chat_id: message.chat.id,
text: `Zapisano. To już ${count} wiadomość(i) z tego czatu.`,
});
}
Wdróż zaktualizowany handler za pomocą npx tgcloud push, a następnie wyślij do bota kilka wiadomości i obserwuj, jak licznik rośnie. Baza danych jest trwała pomiędzy wywołaniami — to pamięć Twojego bota.
7. Testuj bez wdrażania
Nie musisz wdrażać, aby wypróbować zmiany. npx tgcloud run wykonuje handler na platformie przy użyciu Twoich lokalnych plików, bez ich publikowania:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hello" }'
Argument to payload, który otrzymuje Twój handler — dla handlers/message jest to
Message
— zapisany w
JSON5
(dzięki czemu możesz pominąć cudzysłowy w kluczach). Komenda drukuje wszystko, co handler wypisał za pomocą console.*, wartość zwrotną oraz czas trwania. To najszybsza pętla do iterowania logiki — bez wdrażania, bez czekania na prawdziwą wiadomość.
8. Synchronizacja
W trakcie pracy kilka komend utrzymuje Twój lokalny projekt i chmurę w zgodzie: npx tgcloud status pokazuje zmiany, npx tgcloud push wdraża, npx tgcloud pull dostosowuje Twój projekt lokalny do chmury, npx tgcloud fetch sprawdza, co jest wdrożone bez dotykania Twoich plików, a npx tgcloud reset odrzuca zmiany lokalne.
Jeśli dwie osoby (lub dwa komputery) wdrażają zmiany dla tego samego bota, platforma wykrywa konflikt, a
pushzatrzymuje się, pozwalając Ci najpierw wykonaćpull— nigdy nie nadpiszesz czyjejś pracy w sposób niezauważony. Zobacz Staying in sync .
Budowanie z AI
Wolisz budować z asystentem AI — czy jedynym koderem w Twoim zespole jest AI? Nadal możesz stworzyć bota. Zrobiliśmy pierwszy krok, aby agent AI czuł się jak w domu w projekcie: każdy nowy projekt zawiera AGENTS.md oraz referencję docs/tgcloud-sdk.md, które narzędzia programistyczne AI odczytują automatycznie.
W połączeniu z małym, samowystarczalnym runtime’em — jedno SDK, brak paczek npm do zarządzania — daje to asystentowi dobry start w kwestii konwencji, których ogólne generatory kodu często nie wyłapują: importy relatywne z .js, brak kluczy obcych, każde wywołanie db jest asynchroniczne, jeden handler na typ aktualizacji oraz dwuetapowy proces push/migrate.
Wypróbuj to:
npm create @tgcloud/bot my-bot
cd my-bot
opencode # lub Claude Code, Cursor, … — dowolny agent, który czyta AGENTS.md
Następnie po prostu zapytaj, zwykłym językiem:
Napisz bota, który pamięta listę rzeczy do zrobienia dla każdej osoby — dodaj element, gdy wyślą tekst, i pokaż całą listę, gdy wyślą /list.
Asystent edytuje schema.js i Twoje handlery; Ty recenzujesz, testujesz zmianę natychmiast za pomocą npx tgcloud run, a następnie wchodzisz na żywo za pomocą npx tgcloud push i npx tgcloud migrate. AGENTS.md jest częścią Twojego projektu — edytuj go wraz z rozwojem bota, aby wskazówki pozostały dokładne.
W podróży z BotFather
Zostałeś tylko z telefonem? Cały projekt żyje również w @BotFather — otwórz bota → Serverless i uzyskaj dostęp do wszystkiego, czym zarządza CLI, na ekranie dotykowym:
- Handlers — twórz, edytuj i testuj handlery aktualizacji; BotFather utrzymuje webhook w synchronizacji z handlerami, które masz (to samo In sync / Out of sync, co w CLI).
- Endpoints — funkcje wywoływane przez Twoją Mini App.
- Library — Twoje współdzielone moduły
lib/. - Database — edytuj
schema.jsw tej samej składni typu Drizzle, recenzuj oczekujące zmiany i stosuj je; Save wdraża zmiany. - Mini App Files — frontend, który Twoi użytkownicy otwierają w Telegramie.
- CLI Access — pobierz token dostępu CLI tutaj, gdy wrócisz do klawiatury.
To ten sam projekt w chmurze, więc możesz zacząć pisać handler na telefonie, a później pobrać go na laptopa za pomocą npx tgcloud pull — nic nie jest przywiązane do jednego urządzenia. Uruchomienie handlera pokazuje nawet wyjście Console bezpośrednio na czacie, tak jak
npx tgcloud run
.
Projekty i moduły
Projekt serverless to zwykły folder pod kontrolą wersji. Wszystko, co uruchamia platforma, znajduje się w jednym folderze, tgcloud/; reszta projektu należy do Ciebie — frontend mini-aplikacji, konfiguracja, dokumentacja. Nie ma kroku budowania dla bota, brak node_modules w czasie wykonywania i brak punktu wejścia serwera.
Anatomia projektu
example_bot/
├─ tgcloud/ # część wdrożona — nic więcej
│ ├─ handlers/ # handlery aktualizacji — płaskie, tylko jeden poziom
│ │ ├─ message.js
│ │ └─ callback_query.js
│ ├─ endpoints/ # funkcje wywoływane przez Mini App — płaskie, tylko jeden poziom
│ │ └─ getProfile.js
│ ├─ lib/ # współdzielone moduły; dozwolone podkatalogi
│ │ ├─ reply.js
│ │ └─ internal/util.js
│ └─ schema.js # schemat bazy danych — jeden plik
└─ .tgcloud/ # stan CLI, ignorowany przez git
Tylko pliki .js wewnątrz tgcloud/ — schema.js, lib/, handlers/, endpoints/ — są wdrażane. Wszystko inne — Markdown, pliki konfiguracyjne, folder .tgcloud/, cokolwiek poza tgcloud/ — zostaje na Twoim komputerze. Nazwy modułów są relatywne do tgcloud/. Plik tgcloud/handlers/message.js to moduł handlers/message i właśnie pod tą nazwą używa go npx tgcloud run.
Aktualizujesz starszy projekt? Projekty stworzone przed CLI 0.2.0 trzymały
schema.js,lib/ihandlers/w głównym folderze projektu. Uruchomnpx tgcloud upgraderaz, aby przenieść je dotgcloud/.
schema.js — Twoja baza danych. Deklaruje tabele jako nazwane eksporty przy użyciu schematu DSL i znajduje się w tgcloud/ jako pojedynczy plik. Jest wdrażana jak każdy inny moduł, ale samo wdrożenie nigdy nie zmienia bazy danych — zmiany schematu są stosowane osobno przez npx tgcloud migrate. Zobacz
Baza danych
.
lib/ — współdzielony kod, wszystko co chcesz reużyć w handlerach: pomocnicy, warstwy dostępu do danych, formatowanie, integracje z zewnętrznymi usługami. lib/ to jedyny katalog, który może zawierać podkatalogi (lib/internal/util.js, lib/payments/stripe.js), dzięki czemu możesz organizować większą bazę kodu, jak chcesz. Moduły w lib/ nigdy nie są wywoływane bezpośrednio przez platformę; istnieją po to, by być importowane przez handlery i siebie nawzajem.
handlers/ — punkty wejścia Twojego bota. Każdy plik odpowiada za jeden typ aktualizacji Telegrama, a platforma kieruje każdą przychodzącą aktualizację do pasującego handlera:
| Plik | Obsługuje |
|---|---|
handlers/message.js |
Nowe przychodzące wiadomości |
handlers/inline_query.js |
Nowe przychodzące zapytania inline |
handlers/callback_query.js |
Nowe przychodzące callback queries |
| … | każdy inny typ aktualizacji Bot API |
handlers/ jest płaski — brak podkatalogów. Domyślny eksport handlera (export default) to funkcja, którą wywołuje platforma (zobacz
Handlery
).
Typ aktualizacji jest obsługiwany tylko jeśli plik handlera istnieje i nie jest pusty. Jeśli nie ma handlers/<type>.js — lub plik jest pusty — aktualizacje tego typu są ignorowane, a platforma nic dla nich nie uruchamia. Trzymaj więc tylko handlery, których faktycznie potrzebujesz: każdy dodany to kolejny typ aktualizacji, dla którego bot się budzi, a pominięcie reszty oznacza, że Telegram nie uruchamia Twojego kodu dla aktualizacji, które i tak byś odrzucił.
Aby stworzyć szkielet nowego handlera, uruchom
npx tgcloud add handlers/<type>.
endpoints/ — strona serwerowa Twojej Mini App. Każdy plik to funkcja wywoływana przez Mini App po nazwie, więc endpoints/getProfile.js odpowiada na POST /api/getProfile. Platforma sprawdza, czy wywołanie pochodzi od użytkownika Telegrama wewnątrz Twojej Mini App przed uruchomieniem kodu i przekazuje Ci tego użytkownika. endpoints/ jest płaski, jak handlers/. Zobacz
Endpoints
.
.tgcloud/ — stan lokalny urządzenia zarządzany w całości przez CLI, w tym zapisane dane uwierzytelniające. Jest ignorowany przez git i nigdy nie powinieneś czytać z niego ani pisać do niego ręcznie — zamiast tego użyj komend CLI.
System modułów
W czasie wykonywania moduł widzi dokładnie dwie rzeczy: SDK platformy oraz inne moduły w Twoim projekcie. Brak paczek npm, brak systemu plików i brak sieci poza fetch z SDK.
Importuj własne moduły tak, jak w każdym projekcie JavaScript — przez relatywną ścieżkę z rozszerzeniem .js — a SDK po jego nazwie:
// tgcloud/handlers/message.js
import { users } from '../schema.js'; // moduł schematu
import { addItem } from '../lib/cart.js'; // moduł lib
import { format } from '../lib/internal/fmt.js'; // zagnieżdżony moduł lib
import { db, api, fetch } from 'sdk'; // SDK platformy
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js'; // sąsiad
import { users } from '../schema.js';
Import relatywny musi kończyć się na .js i nie może wychodzić poza tgcloud/:
import { users } from '../schema'; // dodaj .js
import config from '../../config.js'; // poza tgcloud/
import { z } from 'zod'; // brak paczek npm
Każdy moduł ma również nazwę — jego ścieżkę wewnątrz tgcloud/ bez rozszerzenia, np. schema, lib/cart lub handlers/message. Ta nazwa jest używana przez npx tgcloud run i wyjście CLI, a moduł może być również importowany po niej (from 'lib/cart', bez rozszerzenia). Preferuj importy relatywne w swoim kodzie. Są one zrozumiałe dla edytora — jump-to-definition, autouzupełnianie, zmiana nazwy.
Dostępne w czasie wykonywania są dokładnie dwie rzeczy: sdk i jego podmoduły (sdk/db, sdk/api, sdk/fetch) — cała powierzchnia platformy, zobacz
SDK
— oraz Twoje własne moduły w tgcloud/. To kompletna lista. Jeśli Twój kod importuje cokolwiek innego, nie zostanie to rozwiązane. To ograniczenie sprawia, że moduły ładują się szybko i są bezpieczne w uruchomieniu.
Handlery
Handler to moduł w handlers/, którego domyślny eksport platforma wywołuje, gdy nadejdzie pasująca aktualizacja.
// handlers/message.js
import { api } from 'sdk';
export default async function (message) {
await api.sendMessage({
chat_id: message.chat.id,
text: `Napisałeś: ${message.text ?? '(brak tekstu)'}`,
});
}
Handler otrzymuje payload aktualizacji jako swój argument — platforma rozpakowuje
Update
Telegrama za Ciebie. handlers/message.js otrzymuje
Message
(tzn. update.message); handlers/callback_query.js otrzymuje
CallbackQuery
; i tak dalej. Drugi argument handlera to obiekt kontekstu ctx. Przenosi on surowy Update jako ctx.update — sięgnij po niego, gdy potrzebujesz czegoś spoza payloadu, np. update_id.
Handler może być async (zazwyczaj jest) i może zwracać wartość. Łączy się z Bot API, bazą danych i zewnętrznym HTTP poprzez SDK.
Nie potrzebujesz prawdziwej aktualizacji, aby przetestować handler. npx tgcloud run wykonuje go na platformie z podanym payloadem i Twoim obecnym lokalnym kodem:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
Argument to payload — ten sam obiekt, który otrzymuje handler — w formacie JSON5. Aby dostarczyć ctx handlera (jego drugi argument), dodaj --ctx, np. --ctx '{ update: { update_id: 1 } }'. To uruchamia test na Twoich lokalnych plikach, więc możesz sprawdzić zmiany przed ich wdrożeniem. Zobacz
run
.
Endpoints
Endpoint to moduł w endpoints/, który wywołuje Twoja Mini App — miejsce na wszystko, czego frontend nie może zrobić samodzielnie, jak odczytywanie i zapisywanie bazy danych, wywoływanie Bot API czy komunikacja z usługami zewnętrznymi przy użyciu Twoich sekretów.
// 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('Profil nie znaleziony', { userId: user.id });
return profile;
}
Pierwszy argument, input, to obiekt JSON wysłany przez Mini App. Drugi, ctx, przenosi ctx.initData —
dane inicjalizacyjne
Mini App (user, chat, start_param, auth_date, …), już zweryfikowane przez platformę. Mają one taką samą strukturę jak Telegram.WebApp.initDataUnsafe po stronie klienta, ale tutaj możesz im zaufać. Wywołanie bez ważnych danych init jest odrzucane z kodem 401 przed uruchomieniem Twojego kodu, więc ctx.initData.user to faktycznie osoba, która otworzyła Twoją Mini App. Endpoints mogą być wywoływane tylko w ten sposób — z poziomu Mini App Twojego bota, przez użytkownika Telegrama — więc nie ma publicznego API do ochrony ani logowania do budowania.
Cokolwiek zwróci endpoint, jest wysyłane z powrotem jako JSON. Aby odmówić wywołania, throw new EndpointError(description, parameters?). Mini App otrzymuje opis i obiekt parameters w takiej postaci, w jakiej są. Jeśli musisz rozgałęzić logikę błędów we frontendzie, umieść kod w parameters — { code: 'OUT_OF_STOCK' }. Każdy inny wyjątek jest raportowany jako generyczny błąd wewnętrzny, a prawdziwa wiadomość trafia tylko do Twoich logów, więc dane wewnętrzne nigdy nie wyciekają do klienta.
Wywoływanie endpointu z Mini App
Mini App wywołuje endpoint po nazwie za pomocą Telegram.WebApp.Serverless.call z oficjalnego
Mini Apps SDK
, telegram-web-app.js — skryptu, który Twoja Mini App już zawiera. Wysyła on dane inicjalizacyjne za Ciebie i wywołuje callback z błędem lub wynikiem endpointu:
Telegram.WebApp.Serverless.call('getProfile', { lang: 'pl' }, function (err, profile) {
if (err) {
// err.message — opis; err.parameters — co przenosił Twój EndpointError
if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
return;
}
render(profile);
});
Input jest opcjonalny i musi być zwykłym obiektem. Bez niego wywołanie to Serverless.call('getProfile', callback).
err.type to 'ENDPOINT_ERROR', gdy Twój endpoint wyrzucił EndpointError. Każda inna wartość oznacza, że platforma odrzuciła wywołanie lub endpoint zawiódł niespodziewanie, na przykład 'UNAUTHORIZED', gdy brakuje danych init lub są nieprawidłowe.
Nazwy endpointów są jak identyfikatory JavaScript — litery, cyfry i _, nie zaczynające się od cyfry (getProfile, place_order). Stwórz szkielet za pomocą
npx tgcloud add endpoints/<name>
i przetestuj go bez Mini App za pomocą
run
, przekazując dane init samodzielnie:
npx tgcloud run endpoints/getProfile '{}' --ctx '{ initData: { user: { id: 1, first_name: "Ania" } } }'
Co jest wdrażane
Gdy wykonasz npx tgcloud push, CLI zbiera każdy plik .js z schema.js, lib/, handlers/ oraz endpoints/ i wysyła ten dokładny zestaw jako przestrzeń modułów Twojego projektu — wraz z folderem budowania Mini App, jeśli tgcloud.jsonc go wskazuje (zobacz poniżej). Cokolwiek obecnego w chmurze, a nieobecnego w Twoim projekcie, jest usuwane, więc wdrożony stan zawsze odzwierciedla Twój folder — włącznie z usunięciami. Nic poza tgcloud/ nie jest wdrażane jako kod — reszta głównego folderu projektu należy do Ciebie. Wewnątrz tgcloud/, plik .js, którego nie ma w jednej z tych lokalizacji (tgcloud/util.js, tgcloud/helpers/x.js), jest oflagowany, więc nie zostanie wdrażany w sposób nieumyślny. Markdown, pliki dotfiles oraz .tgcloud/ nigdy nie są wdrażane.
Frontend Mini App
Główny folder projektu należy do Ciebie i jest naturalnym miejscem dla frontendu Twojej Mini App — zbudowanego w React, Vue, Svelte lub zwykłym HTML, przez Vite lub dowolny inny bundler. Serverless hostuje jego wynik budowania obok Twojego bota. Jeden push wdraża kod i pliki statyczne razem, a aplikacja jest serwowana pod adresem https://app<app_id>.tgcloud.ai/ (push wypisuje dokładny adres — użyj go jako URL Twojej Mini App w
@BotFather
).
Raportowanie nadużyć. Mini Apps na tgcloud.ai są publikowane przez niezależnych programistów. Aby zgłosić nielegalne treści lub nadużycia w subdomenie
app<app_id>.tgcloud.ai, wyślij e-mail na adres [email protected] .
Aby zacząć, stwórz najpierw frontend, jakimkolwiek narzędziem chcesz, a następnie dodaj Serverless na jego wierzch:
npm create vite@latest example_app # lub create-vue, …
cd example_app
npm create @tgcloud/bot .
Kreator projektu nigdy nie nadpisuje Twoich plików. Dodaje folder tgcloud/ obok frontendu, rozpoznaje folder budowania i dodaje skrypt npm run deploy, który buduje aplikację i wdraża ją razem z botem.
To, co jest hostowane, jest ustawione w tgcloud.jsonc, pliku konfiguracyjnym w głównym folderze projektu:
{
"$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 }]
}
}
Tylko "source" jest wymagane. To folder budowania, relatywny do głównego folderu projektu. Inne klucze "static" konfigurują, jak jest serwowany (poniżej). init tworzy tgcloud.jsonc za Ciebie. Gdy rozpozna frontend, wypełnia również "source":
| Frontend | init ustawia "source" na |
|---|---|
| Vite — React, Vue, Svelte, Solid, … | dist |
| Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular | nic — skonfiguruj najpierw eksport statyczny, a potem sam ustaw "source" na jego folder |
Przy jakiejkolwiek innej konfiguracji ustaw "source" samodzielnie na swój folder budowania.
Linia $schema daje edytorowi autouzupełnianie i walidację dla każdego klucza. Plik jest plikiem JSON, w którym dozwolone są komentarze i przecinki końcowe. Jeśli inne Twoje narzędzie musi czytać go jako zwykły JSON, nazwij go tgcloud.json — wtedy musi być ścisłym JSONem.
Buduj przed pushowaniem. push przesyła folder dokładnie tak, jak jest na dysku — z wyjątkiem plików i folderów zaczynających się od kropki, które są pomijane — i nigdy nie uruchamia budowania, więc zwykły npx tgcloud push po edycji frontendu wdrożyłby poprzednią wersję. Użyj npm run deploy (uruchamia skrypt build, a następnie tgcloud push) lub buduj samodzielnie.
Opcje serwowania:
spa— serwujindex.htmldla ścieżek, które nie istnieją i nie mają rozszerzenia pliku, dzięki czemu router po stronie klienta działa przy odświeżeniu. Ścieżki z rozszerzeniem nadal otrzymują404.immutable— wzorce dla plików z hashami w nazwie (Vite’a/assets/*), które przeglądarki mogą cache’ować przez rok. Używaj tylko dla plików z hashami. Każdy inny plik tutaj utknąłby w przeglądarkach na rok i nie ma możliwości jego wymuszonego odświeżenia.headers— nagłówki odpowiedzi dla ścieżek pasujących do reguły (dokładna ścieżka lub prefiks kończący się na/*). Wartośćnullusuwa nagłówek.redirects— reguły przekierowań, gdzie pierwsze dopasowanie wygrywa. Istniejący plik zawsze wygrywa z regułą.statusto301, chyba że ustawisz inaczej."static": "dist"— skrót dla{ "source": "dist" }, gdy nic więcej nie jest potrzebne."static": false— usuń wdrożoną stronę przy następnym pushu. Usunięcie klucza powoduje jedynie zaprzestanie zarządzania plikami statycznymi przez ten projekt, a to, co jest wdrożone, zostaje.
Strona jest serwowana z głównego folderu swojego hosta, więc domyślny base (/) Twojego bundlera jest poprawny. Telegram Web otwiera Mini Apps w ramce iframe, więc nie dodawaj X-Frame-Options ani restrykcyjnej polityki frame-ancestors.
Ten sam host odpowiada na Twoje
endpoints
pod /api/ — to tam kieruje Telegram.WebApp.Serverless.call('getProfile', …). Nie umieszczaj własnych plików pod /api/ w folderze budowania.
Baza danych
Każdy bot dostaje własną bazę danych — magazyn oparty na SQLite, który jest trwały pomiędzy wywołaniami i dostępny dla każdego modułu przez db. Opisujesz tabele w schema.js za pomocą małego, typowanego DSL; czytasz i piszesz do nich za pomocą płynnego budowniczego zapytań (query builder); i ewoluujesz je za pomocą recenzowanych migracji.
Znasz Drizzle ORM ? W takim razie już wiesz, jak rozmawiać z bazą danych tutaj. Schemat DSL i query builder podążają za Drizzle — budowniczowie kolumn,
select().from().where(), operatory,onConflictDoUpdate,.returning()oraz tagsqlzachowują się tak, jak oczekujesz, więc czytanie i pisanie danych to znane API, którego już używasz. Po prostu importujesz je zsdk/db, a kilka specyficznych dla platformy kwestii (przede wszystkim brak kluczy obcych ) jest wskazanych tam, gdzie się pojawiają.
Deklarowanie tabel
Tabele to nazwane eksporty w schema.js. Wywołanie table() buduje opis w czasie ładowania (nie dotyka bazy danych); platforma odkrywa wyeksportowane tabele, gdy wdrażasz schema.js i migruje bazę danych, aby pasowała.
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?):
nameto nazwa tabeli SQL;columnsto mapa właściwości JS → definicja kolumny;extrasto opcjonalny callback(t) => ({ … }), gdzieteksponuje kolumny (t.createdto odniesienie do tej kolumny) — deklaruj tutaj indeksy i ograniczenia na poziomie tabeli.
Typy kolumn
| Fabryka | Typ SQLite | Notatki |
|---|---|---|
text() |
TEXT | |
integer() |
INTEGER | |
real() |
REAL | alias float() |
numeric() |
NUMERIC | |
blob() |
BLOB | czyta/pisze Uint8Array |
boolean() |
INTEGER | przechowywane jako 0/1, czytane jako true/false |
json() |
TEXT | auto JSON.stringify / JSON.parse |
Argument nazwy kolumny jest opcjonalny — pomiń go, a zostanie użyty klucz JS. Opcja
modekontroluje, jak wartości konwertują się między SQLite a JavaScript.
mode |
Przechowywane jako | Wartość JS |
|---|---|---|
boolean |
INTEGER 0/1 | boolean |
json |
TEXT (JSON) | dowolny obiekt/tablica |
timestamp |
INTEGER (sekundy unix) | Date |
timestamp_ms |
INTEGER (ms unix) | Date |
bytes |
BLOB | Uint8Array |
boolean() i json() to skróty dla integer(name, { mode: 'boolean' }) i text(name, { mode: 'json' }).
blob() czyta i pisze Uint8Array — runtime nie ma Buffer z Node (a Buffer jest podklasą Uint8Array, więc to przenośny typ bazowy). Powyższy mode rządzi tylko tym, jak wartość jest kodowana, niezależnie od typu przechowywania kolumny: więc blob('col', { mode: 'json' }) jest odpowiednikiem blob dla json() — to samo kodowanie JSON, trzymane w kolumnie BLOB zamiast TEXT.
TLDR:
blob()używaUint8Array.modekontroluje kodowanie, nie przechowywanie, więcblob(..., { mode: 'json' })przechowuje JSON jako BLOB, podczas gdyjson()przechowuje go jako TEXT.
Modyfikatory kolumn
Modyfikatory kolumn łańcuchują się do kolumny.
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' }) // lub 'virtual'
text('email').deprecated('zastąpione przez login') // oznacza kolumnę do usunięcia
.default()w kolumniejson()koduje wartość za Ciebie;sql`…`default jest przekazywany w oryginale..deprecated()jest terminalne — zobacz Migracje .
Indeksy i ograniczenia
Indeksy i ograniczenia są deklarowane w callbacku extras, gdzie kolumny są w zasięgu.
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})`) , // indeks wyrażenia
active: index('idx_active').on(t.userId).where(sql`done = 0`) , // indeks częściowy
}));
Modyfikatory poziomu tabeli łańcuchują się po table(...): .strict(), .withoutRowid(), .deprecated('powód').
Brak kluczy obcych
Runtime działa z wyłączonym PRAGMA foreign_keys. Zadeklarowany klucz obcy byłby cicho nieaktywny — brak kaskad, brak ochrony osieroconych rekordów — co jest gorsze niż posiadanie żadnych, więc DSL sprawia, że jest to niemożliwe. Mianowicie, .references() i foreignKey() poziomu tabeli wyrzucają błąd przy deklaracji, a schemat, który ich używa, nie zostanie wdrożony.
Powinieneś modelować relacje za pomocą zwykłych kolumn (userId: integer('user_id')) i wymuszać integralność w kodzie aplikacji: wstawiaj rodziców przed dziećmi, usuwaj dzieci przed rodzicami, poprawnie obsługuj błędy i czyść osierocone rekordy za pomocą LEFT JOIN … WHERE parent.id IS NULL, gdy potrzebujesz.
Brak kluczy obcych to świadome ograniczenie, nie niedopatrzenie. Uwzględnij to wcześnie przy planowaniu bota.
Zapytania
db to płynny query builder. Każde zapytanie jest asynchroniczne — metody terminalne (.all(), .get(), .values(), .run()) zwracają Promises, więc zawsze 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(); // wszystkie wiersze
await db.select().from(todos).where(eq(todos.id, 1)).get(); // pierwszy wiersz lub null
await db.select().from(todos).values(); // wiersze jako tablice wartości
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();
// własna projekcja: { 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();
// liczenie wierszy — pomocnik, nie terminal budowniczego:
await db.$count(todos); // wszystkie wiersze
await db.$count(todos, eq(todos.done, false)); // z filtrem
- Łańcuchowanie:
.where(),.orderBy(),.limit(),.offset(),.groupBy(),.having(),.distinct(). - Terminale:
.all(),.get(),.values(). - Licz wiersze za pomocą
db.$count(table, where?)(lubcount()w projekcji).
Wstawianie, aktualizacja, usuwanie:
await db.insert(todos).values({ userId: 1, text: 'Kup mleko' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run(); // partia
await db.insert(todos).values({ text: 'X' }).returning().run(); // RETURNING *
await db.insert(users).values({ tgId: 42, name: 'Ania' })
.onConflictDoUpdate({ target: users.tgId, set: { name: 'Ania' } }).run();
await db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run();
await db.delete(todos).where(eq(todos.id, 1)).run();
Zauważ, że wstawianie partii to jedno polecenie, więc jest ograniczone limitem zmiennych SQLite (
wiersze × kolumny); jeśli go przekroczysz, wstawianie wyrzuci błąd — podziel dane samodzielnie.
Operatory są importowane z 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) z wieloma argumentami jest tym samym co and(a, b). Drugi argument porównania to domyślnie wartość, ale może być inną kolumną lub sql`…` — np. eq(a.x, b.y). Agregaty (count/sum/avg/min/max) to fragmenty sql dla projekcji .select({ … }).
Raw SQL — gdy budowniczy nie wystarcza, przejdź do surowego SQL. Tryb wybiera się metodą: db.run dla zapisów, db.all dla wielu wierszy, db.get dla jednego wiersza, db.values dla wierszy jako tablic pozycyjnych.
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, 'kup mleko']]
Zapis resolve’uje do wyniku wykonania — rowsAffected, lastInsertRowid oraz rows, które przenoszą klauzulę RETURNING, jeśli SQL taką posiadał. Sięgnij po db.values, gdy zapytanie projektuje dwie kolumny o tej samej nazwie (join typu SELECT u.id, o.id): db.all kluczy wiersze nazwą kolumny, więc drugie id nadpisałoby pierwsze.
Tag sql`…` zamienia ${value} w parametr wiązany, a ${table.column} w identyfikator i scala zagnieżdżone fragmenty sql. Użyj sql.raw('…') dla literału bez parametrów.
Zapytania surowe nie są powiązane z tabelą, więc ich wiersze wracają bez konwersji trybu — boolean to
0/1, kolumna JSON to string, timestamp to liczba. Tylko budowniczy powiązany z tabelą konwertuje wartości.
Migracje
Twoja baza danych zmienia się wraz z rozwojem bota. Platforma utrzymuje to bezpieczeństwo, oddzielając wdrażanie kodu od zmiany danych i klasyfikując każdą zmianę schematu według ryzyka.
Wdrażanie nigdy nie dotyka bazy danych. Gdy wykonasz npx tgcloud push ze zmienionym schema.js, platforma zapisuje nowy schemat i mówi Ci, co baza danych by zmieniła — ale nic nie stosuje:
npx tgcloud push # wdróż schema.js; raportuje oczekujące zmiany DB
npx tgcloud migrate # przejrzyj i zastosuj je
npx tgcloud migrate oblicza różnicę między Twoim schematem a żywą bazą danych i przeprowadza Cię przez nią. Zawsze jesteś pytany, zanim cokolwiek zostanie zastosowane. Oznacza to, że rutynowe wdrożenie kodu nigdy nie może przypadkowo wywołać migracji danych.
Każda oczekująca zmiana posiada status, który określa, jak traktuje ją migrate:
| Status | Znaczenie | W migrate |
|---|---|---|
| safe | Addytywna i nieblokująca — nowa tabela, kolumna lub indeks | Stosowane razem w jednym kroku, po potwierdzeniu |
| warning | Potencjalnie destrukcyjna lub wolna — usuwanie czegoś lub indeks na ogromnej tabeli | Prezentowane jedno po drugim, każde potwierdzane osobno |
| manual | Nie może być wykonane automatycznie — np. zmiana typu kolumny | Pokazane z wytycznymi; wykonujesz ręcznie |
| undocumented | Istnieje w bazie danych, ale nie w Twoim schemacie | Pokazane dla świadomości; nie stosowane |
Bezpieczne zmiany są szybkie i odwracalne w zamyśle, więc przechodzą razem. Każde ostrzeżenie to celowe, indywidualne potwierdzenie — brak opcji “zastosuj wszystko” dla zmian destrukcyjnych.
Zmiany ręczne pojawiają się z powodem i sugerowaną akcją. migrate kończy się podsumowaniem: ile zmian zastosowano, pominięto, oczekuje ręcznej poprawki lub nie ma w schemacie.
Zobacz
migratedla flag (--dry-run,--safe,--yes,--local).
Usuwanie rzeczy
Usunięcie tabeli lub kolumny z schema.js nie usuwa jej fizycznie — uczyniłoby to przypadkowe usunięcie katastrofalnym. Aby coś usunąć, oznacz to jako przestarzałe (deprecated):
// usuń kolumnę
text('email').deprecated('zastąpione przez login')
// usuń całą tabelę
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('nieużywane');
Przy następnym migrate, przestarzały obiekt pojawia się jako usunięcie ze statusem warning, które potwierdzasz indywidualnie. Gdy już zniknie, usuń deklarację.
Zmiana typu kolumny
Zmiany typu są manualne — SQLite nie zawsze może je wykonać w miejscu, a rzutowanie istniejących wartości to kwestia oceny. migrate pokaże zmianę i jej uzasadnienie; wykonaj ją samodzielnie za pomocą surowego SQL (db.run(...)), zazwyczaj poprzez utworzenie nowej kolumny lub tabeli, kopiowanie danych i zamianę.
SDK
W czasie wykonywania moduł ma jedną bibliotekę: sdk. Łączy ona trzy rzeczy, których potrzebuje backend bota — bazę danych, Telegram Bot API oraz zewnętrzny HTTP — bez instalowania czegokolwiek i konfigurowania danych uwierzytelniających. Baza danych (db) jest opisana w
Baza danych
; ta sekcja opisuje api, fetch oraz globalny console.
import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk'; // cała powierzchnia
// lub z podmodułów:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
| Import | Co to jest |
|---|---|
db |
Baza danych — query builder i schemat DSL → Baza danych |
api |
Telegram Bot API — api.sendMessage(...) →
poniżej
|
fetch |
Wychodzący HTTP → poniżej |
InputFile |
Plik (bajty + nazwa) do przesłania — działa z api i fetch →
Pliki
|
EndpointError |
Odmowa wywołania Mini App z wiadomością, którą może pokazać → Endpoints |
Importuj własne moduły projektu przez relatywną ścieżkę z rozszerzeniem .js (from '../schema.js', from '../lib/cart.js'). Zobacz
System modułów
.
Bot API
api daje Ci pełne
Telegram Bot API
. Wywołaj dowolną metodę jako api.<method>(params). Każda obecna — i przyszła — metoda Bot API działa bez potrzeby aktualizacji SDK.
import { api } from 'sdk';
const me = await api.getMe(); // → rozpakowany wynik
await api.sendMessage({ chat_id: id, text: 'Cześć!' });
await api.editMessageText({ chat_id, message_id, text: 'Zaktualizowano' });
await api.answerCallbackQuery({ callback_query_id, text: 'Gotowe' });
Koperta odpowiedzi jest rozpakowana. Bot API zazwyczaj zawija wyniki w { ok: true, result: … }. api zwraca result bezpośrednio — getMe() zwraca obiekt użytkownika, a nie obiekt opakowujący. Parametry używają własnych nazw snake_case Bot API (chat_id, message_id, reply_markup, …).
Błędy wyrzucają BotApiError. Gdy Bot API zwraca { ok: false }, wywołanie wyrzuca BotApiError zamiast zwracać wartość falsy, więc nie możesz przypadkowo tego zignorować. Błąd przenosi .code (kod błędu Bot API), .description (wiadomość dla człowieka), .method (która metoda zawiodła) oraz .parameters (dodatkowe dane, np. retry_after przy 429 lub migrate_to_chat_id). Złap go, aby obsłużyć oczekiwaną awarię, a resztę wyrzuć dalej:
import { api, BotApiError } from 'sdk';
try {
await api.deleteMessage({ chat_id, message_id });
} catch (e) {
if (e instanceof BotApiError && e.code === 400) {
// 400 = wiadomość już nie istnieje; tutaj to w porządku.
} else {
throw e;
}
}
Pliki
Pracuj z bajtami plików, nie tylko file_id — przesyłaj nowe pliki i pobieraj istniejące.
Upload — InputFile. Zawiń surowe bajty w InputFile (zwykły kontener: bajty, nazwa pliku i opcjonalny MIME type) i przekaż tam, gdzie metoda Bot API oczekuje pliku:
import { api, InputFile } from 'sdk';
const doc = new InputFile(bytes, 'raport.pdf', { type: 'application/pdf' });
await api.sendDocument({ chat_id, document: doc });
// pliki zagnieżdżone w parametrach też działają — np. 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' }) },
]});
Bajty są przesyłane do Telegrama jako prawdziwy multipart upload — plik nie jest buforowany w całości w jednym wywołaniu do platformy — ale limity Bot API Telegrama nadal obowiązują: bot obecnie może przesłać do 50 MB na plik (i 10 MB dla zdjęcia).
Download — przez file_id. Dwa pomocniki pobierają bajty pliku. Platforma rozwiązuje file_id za Ciebie (uruchamia getFile i pobiera zawartość w tle), więc nigdy nie dotykasz tokena bota ani URL pobierania:
const bytes = await api.getFileContent(file_id); // → Uint8Array (cały plik)
const { file, body } = await api.getFileStream(file_id); // streamuj zamiast tego
file.file_unique_id; // informacje getFile
for await (const chunk of body) { /* Uint8Array */ } // lub zbuforuj: await stream.bytes()
Bot API Telegrama ogranicza pobieranie
getFiledo 20 MB, więc to jest limit dla obu pomocników.
HTTP
fetch to klient typu fetch do wywoływania świata zewnętrznego — zewnętrznych API, webhooków, czegokolwiek przez 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();
Odpowiedź odzwierciedla platformę webową: res.status, res.statusText, res.ok (true dla 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()) oraz czytniki ciała await res.json() / await res.text().
Możesz również czytać ciało przyrostowo jako stream —
for await (const chunk of res.body) { … }— co jest sposobem na konsumowanie zdarzeń serwera (server-sent events) lub wyjścia token-po-tokenie z API AI.
Pomocnicy ciała ustawiają pasujący Content-Type za Ciebie:
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
Ciała żądań mogą również przenosić pliki i surowe bajty: przekaż Uint8Array, InputFile lub FormData (dla multipart/form-data) jako body, a bajty są streamowane na zewnątrz — więc możesz przesłać plik do zewnętrznego API tak samo, jak wysyłasz pliki do Telegrama:
import { fetch, FormData, InputFile } from 'sdk';
const form = new FormData();
form.append('file', new InputFile(bytes, 'zdjecie.jpg', { type: 'image/jpeg' }));
form.append('caption', 'od mojego bota');
const res = await fetch('https://api.example.com/upload', { method: 'POST', body: form });
Inaczej zachowuje się jak standardowy fetch, który już znasz, z dwoma ograniczeniami:
- Odpowiedź jest czytana jako tekst —
await res.json()/await res.text()lub streamowana chunk-po-chunku za pomocąres.body. - Całkowita odpowiedź jest ograniczona do 30 MB. Ten limit obejmuje całą odpowiedź — streamowanie za pomocą
res.bodypozwala przetwarzać duże ciało przyrostowo, ale nie podnosi limitu.
Logowanie
Standardowy globalny console jest dostępny — nic nie trzeba importować, po prostu jest, jak w każdym JavaScript. Jego wyjście jest przechwytywane i pokazywane przez npx tgcloud run, co czyni go głównym narzędziem do debugowania podczas programowania.
console.log('przetwarzanie', { chatId: id }); // log / debug — zwykły
console.info('rozpoczęto'); // info — niebieski
console.warn('limit szybkości'); // warn — żółty
console.error(err); // error — czerwony, ze śladem stosu
Każda linia jest oznaczona [plik:linia], z której pochodzi. console.error i console.trace dodają pełny stack trace, podczas gdy console.warn nie. Gdy uruchamiasz npx tgcloud run dla modułu, te linie są drukowane z kolorowym prefiksem według poziomu, czasem od startu uruchomienia i źródłem — zobacz
run
.
Interfejs linii komend (CLI)
tgcloud to pomost między Twoim folderem projektu a chmurą. Tworzy projekty, pokazuje zmiany, wdraża, uruchamia moduły i stosuje migracje bazy danych. Wymaga
Node.js
18 lub nowszego. Dwa sposoby, aby go zdobyć:
# Zalecane — stwórz projekt z zainstalowanym CLI:
npm create @tgcloud/bot example_bot
# Lub zainstaluj CLI globalnie i zainicjuj pusty folder:
npm install -g @tgcloud/cli
tgcloud init
Paczka npm to @tgcloud/cli, a polecenie, które instaluje, to tgcloud. Każda komenda działa z dowolnego podfolderu Twojego projektu.
| Komenda | Cel |
|---|---|
init
|
Stwórz nowy projekt w bieżącym folderze |
add
|
Stwórz nowy moduł (handler, endpoint lub moduł lib) |
login
|
Połącz projekt z botem (zapisuje token) |
status
|
Pokaż zmiany lokalne vs. chmura |
diff
|
Pokaż zmiany linijka po linijce |
push
|
Wdróż zmienione moduły i budowanie Mini App do chmury |
migrate
|
Zastosuj zmiany schematu do bazy danych |
run
|
Wykonaj moduł na platformie bez wdrażania |
fetch
|
Sprawdź, co jest wdrożone, bez zmiany plików lokalnych |
pull
|
Dostosuj pliki lokalne do chmury |
reset
|
Odrzuć zmiany lokalne; przywróć ze stanu chmury |
webhook
|
Inspekcja i ponowna synchronizacja webhooka zarządzanego przez platformę |
completion
|
Drukuj skrypt uzupełniania powłoki (bash/zsh/fish) |
upgrade
|
Przenieś układ projektu sprzed 0.2.0 do tgcloud/ |
Uwierzytelnianie
Projekt jest przypisany do jednego bota za pomocą tokena, który ma postać app<id>:<secret>. Część app<id> jest publiczna; sekret nigdy nie pojawia się w logach ani błędach.
Token jest rozwiązywany w tej kolejności:
TGCLOUD_TOKENzmienna środowiskowa — dla CI; nigdy nie zapisywana na dysk..tgcloud/credentials— zapisane przeznpx tgcloud login.- Brak powyższych → błąd kierujący do
npx tgcloud login.
CLI nigdy nie prosi o token w trakcie komendy. Logowanie to zawsze jawny krok login. Jeśli zapisany token stanie się nieważny, CLI usuwa go i prosi o ponowne zalogowanie.
init
npx tgcloud init
Tworzy szkielet nowego projektu w bieżącym katalogu: folder tgcloud/ z schema.js, lib/, handlers/ i startowym handlerem, plus tgcloud.jsonc, AGENTS.md, docs/ oraz folder stanu .tgcloud/. Uruchom go również w głównym folderze istniejącego projektu frontendu — dodaje folder tgcloud/ i dokumentację, nigdy nie nadpisując Twoich plików. Jeśli rozpozna frontend, wskazuje tgcloud.jsonc na folder budowania i dodaje skrypt deploy (budowanie, potem push) do Twojego package.json — tylko jeśli nie masz już skryptu deploy. Zobacz
Frontend Mini App
.
init odmawia stworzenia projektu wewnątrz innego projektu. Ponowne uruchomienie w głównym folderze projektu jest w porządku i dodaje tylko brakujące elementy.
add
npx tgcloud add <target>
Tworzy szkielet pojedynczego nowego modułu, podłączonego i gotowego do edycji — handler, endpoint lub moduł lib/.
npx tgcloud add handlers/callback_query # nowy handler aktualizacji
npx tgcloud add endpoints/getProfile # nowy endpoint dla Mini App
npx tgcloud add lib/cart # nowy współdzielony moduł
Zauważ, że
addnigdy nie nadpisuje istniejącego pliku.
<target> to ścieżka modułu (końcowe .js jest opcjonalne). Dla handlers/ nazwa musi być typem aktualizacji Telegrama. Platforma reklamuje poprawny zestaw, więc błędna nazwa jest odrzucana na wstępie. handlers/ i endpoints/ są płaskie, podczas gdy lib/ może być zagnieżdżony (lib/payments/stripe).
Nazwa modułu jest wymagana. Podanie tylko katalogu jest błędem — ale pomocnym: dla handlers/ wypisuje typy aktualizacji, których jeszcze nie masz, więc możesz jeden skopiować.
$ npx tgcloud add handlers
Błąd: Podaj nazwę, np. "handlers/callback_query". Dostępne typy handlers/: callback_query, inline_query, chat_member, …
Uzupełnianie
<Tab>oferuje ten sam zestaw — zobaczcompletion.
Wygenerowany plik ma aktywny export default, więc handler jest aktywny zaraz po wdrożeniu — nie ma nic do odkomentowania. Wdróż nowy moduł za pomocą
push
.
login
npx tgcloud login
Prosi o token dostępu CLI — z
@BotFather
→ Twój bot → Serverless → CLI Access → Access token, osobny token od
tokena API bota
— weryfikuje go z platformą i zapisuje w .tgcloud/credentials.
loginto jedyna komenda, która prosi o token i wymaga interaktywnego terminala. W CI ustaw zamiast tegoTGCLOUD_TOKEN.
status
npx tgcloud status
Pokazuje, plik po pliku, co zmieniło się między Twoim folderem roboczym a wdrożoną kopią: zmodyfikowane, nowe, usunięte, niezmienione. Działa offline, porównując ze stanem chmury z Twojego ostatniego push, pull lub fetch. Ostrzega również o zbłąkanych plikach .js wewnątrz tgcloud/, które nie będą wdrożone.
Dla projektu z Mini App, status pokazuje również, co zmieniło się w folderze budowania od ostatniego wdrożenia.
diff
npx tgcloud diff
Jak status, ale pokazuje rzeczywiste różnice linijka po linijce dla zmienionych modułów. Również offline.
push
npx tgcloud push [files...]
Wdraża Twój projekt do chmury w jednej atomowej paczce.
Z brakiem argumentów wdraża cały projekt, a wdrożony stan jest dopasowywany dokładnie do Twojego folderu — moduły, które usunąłeś lokalnie, są usuwane w chmurze.
Z argumentami plików lub katalogów (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/), wdraża tylko zmiany w tych plikach. Docelowy push nigdy nie usuwa innych modułów. Cel może być zapisany jako ścieżka pliku (tgcloud/handlers/message.js), ścieżka wewnątrz tgcloud/ (handlers/message.js) lub nazwa modułu (handlers/message) — wszystkie oznaczają ten sam moduł.
Opcją jest --force — aby pominąć sprawdzanie współbieżności i nadpisać to, co jest w chmurze. Używaj tylko, gdy jesteś pewien (zobacz
Staying in sync
).
Jeśli tgcloud.jsonc wskazuje folder statyczny, pełny push wdraża go w tej samej paczce — przesyłane są tylko zmienione pliki — i wypisuje adres strony. Buduj najpierw, ponieważ push nigdy nie uruchamia budowania (npm run deploy robi oba). npx tgcloud push dist/ wdraża same pliki statyczne, a push wskazujący tylko moduły zostawia je nietknięte.
Po wdrożeniu, jeśli schema.js zmienił się, a baza danych nie jest zsynchronizowana, push wypisuje podsumowanie oczekujących zmian i sugeruje npx tgcloud migrate. Nigdy nie stosuje ich samodzielnie.
migrate
npx tgcloud migrate
Stosuje zmiany schematu do bazy danych. Oblicza różnicę między schema.js a żywą bazą danych, a następnie przeprowadza Cię przez nią krok po kroku z licznikiem [N/M]:
- Krótkie podsumowanie wszystkich oczekujących zmian.
- Bezpieczne zmiany, stosowane razem w jednym kroku po Twoim potwierdzeniu.
- Ostrzeżenia (usunięcia, wolne operacje), jedno po drugim, każde potwierdzane osobno.
- Zmiany manualne, pokazane z powodem i sugerowaną akcją, nie stosowane automatycznie.
- Obiekty nieudokumentowane (w bazie danych, ale nie w Twoim schemacie), pokazane dla świadomości.
Kończy się podsumowaniem: zastosowano, pominięto, oczekuje naprawy ręcznej, nie ma w schemacie.
Zobacz Migracje dla modelu.
Opcje: --dry-run (drukuj wszystko, nie stosuj), --safe (automatycznie stosuj bezpieczne zmiany, pomiń ostrzeżenia), --yes (automatycznie stosuj bezpieczne zmiany i każde ostrzeżenie, pomiń ręczne — używaj ostrożnie), --local (porównaj ze schematem lokalnym zamiast wdrożonego). Bez flagi migrate wymaga terminala i wyrzuca błąd w środowisku nieinteraktywnym zamiast zgadywać.
run
npx tgcloud run <module> [args] [--ctx <json5>]
Wykonuje handler lub endpoint na platformie bez wdrażania, używając Twoich obecnych plików lokalnych. To szybka pętla wewnętrzna do testowania logiki.
<module>— sama nazwa (szukana whandlers/iendpoints/) lub ścieżka typuhandlers/message. Sama nazwa, która istnieje w obu katalogach, musi być podana z katalogiem.[args]— payload przekazany do handlera, zapisany w JSON5 , dzięki czemu możesz pominąć cudzysłowy w kluczach. To obiekt typu aktualizacji, który otrzymuje Twój handler (np. Message dlahandlers/message) lubinputendpointu, i musi być obiektem — nie tablicą ani pojedynczym ciągiem znaków.--ctx <json5>— obiekt kontekstu modułu (jego drugi argument), również JSON5. Użyj go, aby dostarczyć to, co Twój kod czyta zctx— surową aktualizację dla handlera,--ctx '{ update: { update_id: 1 } }', lub dane init dla endpointu,--ctx '{ initData: { user: { id: 1 } } }'.runtego nie weryfikuje, ponieważ testujesz logikę, a nie sprawdzenia platformy.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
Platforma uruchamia moduł względem przestrzeni modułów złożonej z Twojego lokalnego projektu (więc używany jest też lokalnie zmieniony kod lib/) i zwraca wynik, wszystko zapisane przez console.* oraz czas wykonania. Czytaj duże argumenty z pliku: npx tgcloud run handlers/message "$(cat message.json5)".
fetch
npx tgcloud fetch
Sprawdza, co jest obecnie wdrożone, bez dotykania Twoich plików roboczych. Po tym, status i diff porównują z najnowszym stanem chmury. Przydatne do podejrzenia konfliktu przed decyzją, jak go rozwiązać.
pull
npx tgcloud pull
Dostosowuje projekt lokalny do chmury przez aktualizację plików roboczych do wdrożonego stanu.
pull nigdy nie nadpisuje folderu budowania Mini App.
reset
npx tgcloud reset
Odrzuca lokalne zmiany i przywraca moduły z ostatniego znanego stanu chmury. Użyj do wyrzucenia eksperymentu. Folder budowania Mini App nie jest ruszany.
webhook
npx tgcloud webhook
npx tgcloud webhook sync [--drop-pending]
Telegram dostarcza aktualizacje do Twojego bota przez webhook, którym zarządza platforma — nigdy nie kierujesz go nigdzie ręcznie. npx tgcloud webhook pokazuje bieżący stan: URL, listę allowed_updates, ile aktualizacji oczekuje, ostatni błąd dostarczania (jeśli jest) i czy jest zsynchronizowany z Twoimi wdrożonymi handlerami.
“Zsynchronizowany” oznacza, że webhook wskazuje na platformę, a jego allowed_updates pasują do wdrożonych handlerów — więc Telegram dostarcza dokładnie te typy aktualizacji, które obsługujesz, i nic więcej. Wdrożenie nowego handlera (lub usunięcie starego) może zostawić webhook niesynchronizowany do czasu odświeżenia; npx tgcloud status również to oflagowuje.
npx tgcloud webhook sync naprawia to — ponownie kieruje webhook na platformę i przebudowuje allowed_updates z Twoich wdrożonych handlerów. Dodaj --drop-pending, aby odrzucić aktualizacje, które Telegram już zakolejkował przed synchronizacją (w przeciwnym razie zostaną dostarczone, gdy webhook będzie zdrowy).
completion
Uwaga: tab-completion działa tylko wtedy, gdy
tgcloudjest wPATH— więc zainstaluj globalnie (npm install -g @tgcloud/cli) lub w inny sposób umieść binarkę wPATH. Nie może podpiąć się podnpx.
tgcloud completion <bash|zsh|fish>
Wypisuje skrypt uzupełniania powłoki na stdout. Włącz raz, a potem <Tab> uzupełnia komendy, flagi, katalogi modułów, typy aktualizacji handlerów, których jeszcze nie masz, oraz lokalne moduły uruchomieniowe.
# bash — wymaga pakietu bash-completion:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc
# zsh — upewnij się, że `autoload -U compinit && compinit` działa w Twoim ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc
# fish:
tgcloud completion fish > ~/.config/fish/completions/tgcloud.fish
Zrestartuj powłokę (lub przeładuj plik) po tym. Uruchomienie tgcloud completion bez powłoki wypisuje te instrukcje ponownie.
upgrade
npx tgcloud upgrade [--dry-run]
Jednorazowa migracja dla projektów sprzed CLI 0.2.0, które trzymały schema.js, lib/ i handlers/ w głównym katalogu projektu. Przenosi je do tgcloud/. Połączenie z botem zostaje zachowane, nic nie trzeba wdrażać ponownie. --dry-run drukuje planowane ruchy bez dotykania plików. Inne komendy nie działają na starym układzie, dopóki nie zaktualizujesz.
Staying in sync
Każdy projekt ma monotonicznie rosnącą wersję (revision) w chmurze, zwiększaną przy każdym wdrożeniu. CLI pamięta wersję, z którą ostatnio się synchronizowało i wysyła ją przy każdym push. Jeśli chmura poszła do przodu — ponieważ inny komputer lub zespół wdrożył zmiany — push jest odrzucany zamiast cichego nadpisywania ich pracy, a CLI oferuje trzy drogi naprzód:
npx tgcloud fetch # zobacz, co zmieniło się w chmurze, potem sprawdź ponownie
npx tgcloud pull # pobierz najnowsze zmiany do swoich plików roboczych
npx tgcloud push --force # nadpisz stan chmury (niebezpieczne)
To sprawdzanie optymistycznej współbieżności jest powodem, dla którego możesz dzielić bota w zespole bez rygorystycznego procesu wdrażania krok po kroku. Komendy kończą się kodem błędu przy awarii — odrzucony push, nieudana migracja, błąd autoryzacji, moduł, który wyrzucił błąd w run — więc komponują się czysto w skryptach i potokach CI.