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 push zatrzymuje 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.js w 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/ i handlers/ w głównym folderze projektu. Uruchom npx tgcloud upgrade raz, aby przenieść je do tgcloud/.

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 — serwuj index.html dla ś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ść null usuwa nagłówek.
  • redirects — reguły przekierowań, gdzie pierwsze dopasowanie wygrywa. Istniejący plik zawsze wygrywa z regułą. status to 301, 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 tag sql zachowują się tak, jak oczekujesz, więc czytanie i pisanie danych to znane API, którego już używasz. Po prostu importujesz je z sdk/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?):

  • name to nazwa tabeli SQL;
  • columns to mapa właściwości JS → definicja kolumny;
  • extras to opcjonalny callback (t) => ({ … }), gdzie t eksponuje kolumny (t.created to 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 mode kontroluje, 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żywa Uint8Array. mode kontroluje kodowanie, nie przechowywanie, więc blob(..., { mode: 'json' }) przechowuje JSON jako BLOB, podczas gdy json() 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 kolumnie json() 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?) (lub count() 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 migrate dla 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 getFile do 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.body pozwala 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:

  1. TGCLOUD_TOKEN zmienna środowiskowa — dla CI; nigdy nie zapisywana na dysk.
  2. .tgcloud/credentials — zapisane przez npx tgcloud login .
  3. 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 add nigdy 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 — zobacz completion .

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.

login to jedyna komenda, która prosi o token i wymaga interaktywnego terminala. W CI ustaw zamiast tego TGCLOUD_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 w handlers/ i endpoints/) lub ścieżka typu handlers/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 dla handlers/message) lub input endpointu, 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 z ctx — surową aktualizację dla handlera, --ctx '{ update: { update_id: 1 } }', lub dane init dla endpointu, --ctx '{ initData: { user: { id: 1 } } }'. run tego 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 tgcloud jest w PATH — więc zainstaluj globalnie (npm install -g @tgcloud/cli) lub w inny sposób umieść binarkę w PATH. Nie może podpiąć się pod npx.

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.