Telegram Serverless antaa sinun ajaa bottisi ja Mini Appin taustajärjestelmäkoodia suoraan Telegramin infrastruktuurissa — ei palvelinten varattavaa kapasiteettia, ei käynnissä pidettäviä kontteja, ei skaalautumisen miettimistä. Kirjoitat tavallisia JavaScript-moduuleita, julkaiset ne yhdellä komennolla, ja Telegram ajaa ne nopeassa, eristetyssä V8-hiekkalaatikossa, joka sijaitsee aivan Bot API:n ja sisäänrakennetun tietokannan vieressä.

Jos olet koskaan kytkenyt botin VPS-palvelimeen, pilvifunktioon tai hallintapaneeliin vain vastataksesi /start-komentoon, tämä on se vaihe, jota sinun ei enää tarvitse tehdä.

Miksi serverless?

Telegram-botti on pohjimmiltaan ohjelma, joka reagoi päivityksiin. Perinteisesti sinun on pitänyt isännöidä ohjelmaa jossakin, mikä on aina päällä, tavoitettavissa ja suojattu — ja sen jälkeen pitää se sellaisena. Telegram Serverless poistaa tämän kerroksen kokonaan:

  • Ei infrastruktuuria. Ei konetta, jota vuokrata, päivittää tai valvoa. Koodisi ajetaan pyynnöstä ja se skaalautuu botin mukana automaattisesti.
  • Tunnukset mukana. Telegram Bot API, SQLite-pohjainen tietokanta ja lähtevä HTTP ovat jokaisen moduulin käytettävissä suoraan ilman asennuksia tai tunnistetietojen kytkemistä.
  • Nopea, eristetty suoritus. Jokainen kutsu ajetaan kevyessä V8-eristeessä, lähellä Telegramin omia järjestelmiä, joten kutsut Bot API:in ja tietokantaasi ovat nopeita ja luotettavia.
  • Aito kehittäjän työnkulku. Projekti sijaitsee koneellasi kansiossa versionhallinnan alla. Muokkaat tiedostoja, näet tarkalleen mikä muuttui, julkaiset atomisesti ja päivität tietokantaskeeman versionhallittujen migraatioiden avulla — juuri niin kuin työskentelet kaiken muunkin parissa.

Mielikuva: Miten projekti toimii

Työskentelet kolmessa paikassa, ja ne vastaavat toisiaan selkeästi:

Missä Mitä siellä on
Projektikansiosi JavaScript-moduulit — skeema, jaettu koodi, päivitysten käsittelijät, Mini Appin päätepisteet — ja Mini Appin käyttöliittymä, jos sellainen on
Pilvessä Julkaistu kopio moduuleista, bottisi tietokanta ja Mini Appin tiedostot
tgcloud CLI Silta — se näyttää erot ja synkronoi ne

Sinun ei tarvitse koskaan ottaa SSH-yhteyttä mihinkään. Muokkaat tiedostoja paikallisesti, ajat npx tgcloud push, ja alusta hoitaa loput. Bottisi liikennettä käsittelevät julkaistut moduulit; tietokantasi säilyy kutsujen välillä.

Projektissa on vain neljä koodityyppiä, kaikki yhden tgcloud/-kansion sisällä:

tgcloud/
├─ endpoints/  # funktiot, joita Mini App kutsuu
├─ handlers/   # funktiot, jotka reagoivat Telegram-päivitystyyppeihin
├─ lib/        # jaettu koodi, jota tuot mistä tahansa
└─ schema.js   # tietokantataulusi

Kun päivitys saapuu — viesti, painikkeen painallus, inline-kysely — Telegram ohjaa sen vastaavalle käsittelijälle (handlers/message.js, handlers/callback_query.js, …) ja kutsuu sen oletusvientiä (default export). Kyseinen funktio keskustelee Bot API:n ja tietokannan kanssa SDK:n kautta ja palauttaa vastauksen. Se on koko silmukka. Päivitys, jolle ei löydy käsittelijää, ohitetaan, joten lisäät vain tarvitsemasi käsittelijät.

Mini App on vapaaehtoinen. Jos botillasi on sellainen, sen käyttöliittymä sijaitsee samassa projektissa, tgcloud/-kansion vieressä. Serverless isännöi sen koontiversion (build output) ja ajaa sen kutsumat endpoints/-funktiot. Katso Mini App -käyttöliittymä .

Pikademo

Tässä on täydellinen, toimiva demobotti. Se vastaa jokaiseen viestiin ja muistaa, kuinka monta viestiä se on nähnyt kultakin keskustelulta.

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;

  // Lisää laskuri tai päivitä sitä, jos tällä keskustelulla on jo sellainen — ja palauta
  // tulosrivi samassa lauseessa .returning()-metodilla.
  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: `Hei! Olen nähnyt ${row.seen} viestiä sinulta.`,
  });
}

Julkaise se:

npx tgcloud push       # lähetä moduulit
npx tgcloud migrate    # luo `counters`-taulu

Se on toimiva botti pysyvällä tilalla ilman palvelinta. Kaikki siinä — api, db, table() DSL — on kuvattu alla olevissa osioissa.

Serverless on yleiskäyttöinen taustajärjestelmä Telegram-boteille ja Mini Appeille, ei malli yhdelle sovellustyypille. Se on ihanteellinen:

  • Keskusteleville tekoälyboteille, joiden täytyy tallentaa käyttäjäkohtaista tilaa tietokantaan.
  • Mini App -taustajärjestelmille, jotka tallentavat käyttäjätietoja ja tarjoavat dynaamista sisältöä.
  • Peleille ja työkaluille — mukaan lukien tulostaulut, visailut ja muut.
  • Automaatioille ja integraatioille, jotka kutsuvat kolmannen osapuolen HTTP-rajapintoja ja lähettävät tuloksia keskusteluihin.

Aloittaminen

Tämä läpikäynti vie sinut tyhjästä kansiosta toimivaan bottiin, joka vastaa viesteihin ja tallentaa dataa. Se olettaa, että sinulla on Node.js 18 tai uudempi asennettuna ja botti rekisteröitynä @BotFatherin kautta. Lopuksi olet käyttänyt jokaista päivittäistä komentoa: push, migrate, run ja status.

Ennen kuin teet mitään muuta, kytke Serverless päälle. Avaa @BotFatherissa bottisi → Serverless ja kytke se päälle. Tämä aktivoi ominaisuuden tälle botille ja avaa sen CLI-pääsytunnisteen, käsittelijät, kirjaston ja tietokannan.

1. Luo projekti

Nopein tapa aloittaa on projektin luontityökalu, joka luo projektirakenteen ja asentaa CLI:n siihen:

npm create @tgcloud/bot example_bot
cd example_bot

Argumentti on kohdekansio: käytä . luodaksesi nykyiseen kansioon tai anna mikä tahansa polku. Se toimii myös olemassa olevassa kansiossa eikä koskaan ylikirjoita jo olemassa olevia tiedostoja.

Tämä antaa sinulle valmiin projektin:

example_bot/
├─ tgcloud/             # kaikki mitä alusta ajaa
│  ├─ handlers/
│  │  └─ message.js     # aloittava viestinkäsittelijä (toistaa tekstin takaisin)
│  ├─ lib/              # jaetut moduulisi menevät tänne (aluksi tyhjä)
│  └─ schema.js         # tietokantataulusi
├─ docs/
│  └─ tgcloud-sdk.md    # SDK-viite (sinulle ja tekoälytyökaluillesi)
├─ AGENTS.md            # ohjeistus tekoälyavustajille
├─ package.json
└─ tgcloud.jsonc        # projektin konfiguraatio

Luodut tiedostot ovat itseohjeistavia — jokainen sisältää kommentoituja esimerkkejä siitä, mitä voit tehdä seuraavaksi.

CLI asentuu projektiin paikallisena kehityssuosituksena, joten ajat sen komennolla npx tgcloud <komento> (npx etsii kopion projektisi node_modules-kansiosta), tai npm run-pikakomentojen kautta, jotka luontityökalu lisää tiedostoon package.json (npm run deploy, npm run status). Oletuksena PATH-ympäristömuuttujassa ei ole globaalia tgcloud-komentoa.

Voit myös asentaa sen globaalisti — npm install -g @tgcloud/cli — jos haluat kirjoittaa vain tgcloud mistä tahansa. Se on kätevää tgcloud init-komennon ajamiseen missä tahansa tyhjässä kansiossa, ja se on mitä komentorivin täydennys (tab-completion) tarvitsee. Lopputulos on joka tapauksessa sama projekti.

Rakennatko Mini Appia? Aloita sen käyttöliittymästä. Luo se tavallisella työkalullasi ja aja sitten projektin luontityökalu kyseisessä kansiossa. Katso Mini App -käyttöliittymä .

2. Yhdistä bottisi

Jokainen projekti on sidottu yhteen bottiin. Yhdistä ne login-komennolla, joka pyytää CLI-pääsytunnisteesi ( @BotFather → bottisi → Serverless → CLI Access → Access token — tämä on eri kuin botin API-tunniste ) ja tallentaa sen paikallisesti:

npx tgcloud login

Tunniste on muotoa app<id>:<secret>. CLI pitää sen .tgcloud/-kansiossa, joka on jätetty gitin ulkopuolelle, eikä koskaan tulosta salaista osaa. Kirjautumista kysytään vain kerran — katso Autentikointi nähdäksesi, miten tunnisteet ratkaistaan CI-ympäristöissä.

3. Tarkastele tilannetta

Kaksi komentoa kertovat tilanteen milloin tahansa, molemmat täysin offline-tilassa:

npx tgcloud status     # mikä on muuttunut paikallisesti verrattuna julkaistuun kopioon
npx tgcloud diff       # rivikohtaiset muutokset

Heti init-komennon jälkeen kaikki on uutta eikä mitään ole vielä julkaistu. status näyttää aloitustiedostot, jotka odottavat siirtoa.

4. Julkaise

Lähetä moduulisi pilveen:

npx tgcloud push

push lataa jokaisen muuttuneen moduulin yhdessä atomisessa erässä ja päivittää paikallisen tietosi siitä, mitä pilvessä on. Bottisi on livenä: avaa se Telegramissa ja lähetä viesti — aloittava käsittelijä toistaa sen takaisin.

Julkaisu ei koskaan koske tietokantaasi. Koodin lähettäminen ja tietokantaskeeman muuttaminen ovat tietoisesti erillisiä vaiheita, joten koodin julkaisu ei voi koskaan yllättää sinua datamigraatiolla. Sitä varten on seuraava vaihe.

5. Lisää tietokantataulu

Tehdään botista muistava. Avaa tgcloud/schema.js ja määritä taulu:

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())`) ,
});

Julkaise skeema ja sovella se tietokantaan:

npx tgcloud push       # lähettää uuden skeeman
npx tgcloud migrate    # luo `messages`-taulun

push raportoi, että skeema on epätasapainossa ja näyttää odottavan muutoksen, mutta ei tee mitään. migrate ohjaa sinut muutoksen läpi ja luo taulun vahvistuksestasi. Tämä kaksivaiheinen malli — ja se, mitä tapahtuu riskialttiimpien muutosten, kuten taulujen poistamisen, kohdalla — käsitellään kohdassa Migraatiot .

6. Tallenna ja lue dataa

Käytä nyt taulua käsittelijässäsi. Muokkaa tiedostoa tgcloud/handlers/message.js:

import { api, db } from 'sdk';
import { eq } from 'sdk/db';
import { messages } from '../schema.js';

export default async function (message) {
  // Tallenna tämä viesti.
  await db.insert(messages)
    .values({ chatId: message.chat.id, text: message.text })
    .run();

  // Laske, kuinka monta olemme tallentaneet tälle keskustelulle.
  const count = await db.$count(messages, eq(messages.chatId, message.chat.id));

  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Tallennettu. Olet lähettänyt tästä keskustelusta tähän mennessä ${count} viestiä.`,
  });
}

Julkaise päivitetty käsittelijä komennolla npx tgcloud push, lähetä botille muutama viesti ja katso laskurin kasvavan. Tietokanta säilyy kutsujen välillä — se on bottisi muisti.

7. Testaa ilman julkaisua

Sinun ei tarvitse julkaista kokeillaksesi muutosta. npx tgcloud run suorittaa käsittelijän alustalla käyttäen paikallisia tiedostojasi ilman niiden julkaisemista:

npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hello" }'

Argumentti on payload, jonka käsittelijäsi vastaanottaa — handlers/message-käsittelijälle se on Message — kirjoitettuna JSON5 -muodossa (joten voit jättää avainten lainausmerkit pois). Komento tulostaa kaiken, mitä käsittelijä on lokittanut console.*-komennoilla, paluuarvon ja suoritusajan. Tämä on nopein tapa iteroida logiikkaa — ei julkaisua, ei odottelua.

8. Pysy synkroniassa

Työskennellessäsi muutama komento pitää paikallisen projektisi ja pilven linjassa: npx tgcloud status näyttää muutokset, npx tgcloud push julkaisee, npx tgcloud pull tuo paikallisen projektisi vastaamaan pilveä, npx tgcloud fetch tarkistaa julkaistun tilan koskematta tiedostoihisi, ja npx tgcloud reset hylkää paikalliset muutokset.

Jos kaksi ihmistä (tai kaksi konetta) julkaisee samalle botille, alusta tunnistaa ristiriidan ja push pysähtyy antaakseen sinun tehdä pull-komennon ensin — et voi koskaan ylikirjoittaa kenenkään muun työtä hiljaa. Katso Pysyminen synkroniassa .

Tekoälyllä rakentaminen

Suositko tekoälyavustajan kanssa rakentamista — vai onko tiimisi ainoa koodaaja tekoäly? Voit silti julkaista botin. Olemme ottaneet ensimmäisen askeleen, jotta tekoälyagentti tuntisi olonsa kotoisaksi projektissa: jokainen uusi projekti sisältää AGENTS.md-tiedoston ja docs/tgcloud-sdk.md-viitteen, jotka tekoälytyökalut lukevat automaattisesti.

Pienen, itseään sisältävän ympäristön ansiosta — yksi SDK, ei npm-paketteja, joiden kanssa painia — avustaja saa lentävän lähdön sellaisiin konventioihin, jotka geneerinen koodigeneraattori yleensä missaa: suhteelliset tuonnit .js-päätteellä, ei vieraita avaimia, jokainen db-kutsu on asynkroninen, yksi käsittelijä päivitystyyppiä kohden ja kaksivaiheinen push/migrate-työnkulku.

Kokeile:

npm create @tgcloud/bot my-bot
cd my-bot
opencode            # tai Claude Code, Cursor, … — mikä tahansa agentti, joka lukee AGENTS.md-tiedostoa

Pyydä sitten vain selkokielellä:

Kirjoita botti, joka muistaa jokaisen henkilön tehtävälistan — lisää kohde, kun he lähettävät tekstiä, ja näytä koko lista, kun he lähettävät /list.

Avustaja muokkaa schema.js- ja käsittelijätiedostot puolestasi; tarkista, testaa muutos välittömästi npx tgcloud run-komennolla ja siirry livenä komennolla npx tgcloud push ja npx tgcloud migrate. AGENTS.md on osa projektiasi — muokkaa sitä botin kasvaessa, jotta ohjeistus pysyy tarkkana.

Liikkeellä BotFatherin kanssa

Onko käytössäsi vain puhelin? Koko projekti asuu myös @BotFatherissa — avaa bottisi → Serverless, niin saat kaiken mitä CLI hallitsee, kosketusnäytöllä:

  • Handlers — luo, muokkaa ja testaa päivitysten käsittelijöitä; BotFather pitää web-koukun synkroniassa käsittelijöiden kanssa (sama In sync / Out of sync, jonka CLI raportoi).
  • Endpoints — funktiot, joita Mini Appisi kutsuu.
  • Library — jaetut lib/-moduulisi.
  • Database — muokkaa schema.js-tiedostoa samalla Drizzle-tyylisellä syntaksilla, tarkista odottavat muutokset ja sovella ne; Save julkaisee.
  • Mini App Files — käyttöliittymä, jonka käyttäjäsi avaavat Telegramissa.
  • CLI Access — nappaa CLI-pääsytunniste tästä, kun olet takaisin näppäimistön ääressä.

Se on sama pilviprojekti, joten voit aloittaa käsittelijän puhelimellasi ja tehdä npx tgcloud pull läppärillesi myöhemmin — mikään ei ole sidottu yhteen laitteeseen. Käsittelijän ajaminen näyttää jopa sen Console-tulosteen suoraan chatissa, aivan kuten npx tgcloud run .

Projektit ja moduulit

Serverless-projekti on tavallinen kansio versionhallinnan alla. Kaikki mitä alusta ajaa, sijaitsee yhdessä kansiossa, tgcloud/; muu projekti on sinun — mini-appin käyttöliittymä, konfiguraatiot, dokumentaatio. Botille ei ole koontivaihetta (build step), ei node_modules-kansiota ajon aikana eikä palvelimen sisääntulopistettä.

Projektin anatomia

example_bot/
├─ tgcloud/             # julkaistu osa — mikään muu ei ole
│  ├─ handlers/         # päivitysten käsittelijät — tasainen, vain yksi taso
│  │  ├─ message.js
│  │  └─ callback_query.js
│  ├─ endpoints/        # funktiot, joita Mini App kutsuu — tasainen, vain yksi taso
│  │  └─ getProfile.js
│  ├─ lib/              # jaetut moduulit; alikansiot sallittu
│  │  ├─ reply.js
│  │  └─ internal/util.js
│  └─ schema.js         # tietokantaskeema — yksi tiedosto
└─ .tgcloud/            # CLI-tila, git-ohitettu

Vain .js-tiedostot tgcloud/-kansion sisällä — schema.js, lib/, handlers/, endpoints/ — julkaistaan. Kaikki muu — Markdown, konfiguraatiotiedostot, .tgcloud/-kansio, kaikki tgcloud/-kansion ulkopuolella — pysyy koneellasi. Moduulien nimet ovat suhteessa tgcloud/-kansioon. Tiedosto tgcloud/handlers/message.js on moduuli handlers/message, ja se on nimi, jota npx tgcloud run käyttää.

Päivitätkö vanhempaa projektia? Ennen CLI 0.2.0 luodut projektit pitivät schema.js-, lib/- ja handlers/-tiedostot projektin juuressa. Aja npx tgcloud upgrade kerran siirtääksesi ne tgcloud/-kansioon.

schema.js — tietokantasi. Se ilmoittaa taulut nimetyillä viennillä (named exports) käyttäen skeema-DSL:ää ja elää tgcloud/-kansiossa yhtenä tiedostona. Se julkaistaan kuten mikä tahansa moduuli, mutta sen julkaiseminen ei koskaan muuta tietokantaa — skeeman muutokset sovelletaan erikseen komennolla npx tgcloud migrate. Katso Tietokanta .

lib/ — jaettu koodi, mitä tahansa haluat käyttää uudelleen käsittelijöiden välillä: puhtaat apuohjelmat, tietokannan pääsykerrokset, muotoilu, integraatiot ulkoisiin palveluihin. lib/ on ainoa hakemisto, joka voi sisältää alikansioita (lib/internal/util.js, lib/payments/stripe.js), joten voit järjestellä suuremman koodikannan haluamallasi tavalla. Alustan ei kutsuta lib/-kansion moduuleita suoraan; ne ovat olemassa, jotta käsittelijät ja toiset moduulit voivat tuoda niitä.

handlers/ — bottisi sisääntulopisteet. Jokainen tiedosto vastaa yhtä Telegram-päivitystyyppiä, ja alusta ohjaa jokaisen saapuvan päivityksen vastaavalle käsittelijälle:

Tiedosto Käsittelee
handlers/message.js Uudet saapuvat viestit
handlers/inline_query.js Uudet saapuvat inline -kyselyt
handlers/callback_query.js Uudet saapuvat callback-kyselyt
… mikä tahansa muu Bot API -päivitystyyppi

handlers/-kansio on tasainen — ei alikansioita. Käsittelijän export default on funktio, jota alusta kutsuu (katso Käsittelijät ).

Päivitystyyppi käsitellään vain, jos sen käsittelijätiedosto on olemassa eikä se ole tyhjä. Jos handlers/<type>.js-tiedostoa ei ole — tai se on tyhjä — kyseisen tyypin päivitykset ohitetaan, eikä alusta aja mitään niiden käsittelemiseksi. Joten pidä vain ne käsittelijät, joita todella tarvitset: jokainen lisäämäsi käsittelijä on toinen päivitystyyppi, jonka botisi herää käsittelemään, ja muiden jättäminen tarkoittaa, ettei Telegram käynnistä koodiasi päivityksille, jotka kuitenkin hylkäisit.

Luo uusi käsittelijä ajamalla npx tgcloud add handlers/<type> .

endpoints/ — Mini Appin taustajärjestelmä. Jokainen tiedosto on funktio, jota Mini App kutsuu nimellä, joten endpoints/getProfile.js vastaa POST /api/getProfile-kutsuun. Alusta tarkistaa, että kutsu tulee Telegram-käyttäjältä Mini Appin sisältä ennen kuin koodisi ajetaan, ja antaa sinulle kyseisen käyttäjän tiedot. endpoints/-kansio on tasainen, kuten handlers/. Katso Päätepisteet .

.tgcloud/ — konekohtainen tila, jota CLI hallitsee kokonaan, mukaan lukien tallennetut tunnisteet. Se on git-ohitettu, eikä sinun pitäisi koskaan lukea tai kirjoittaa siihen käsin — käytä sen sijaan CLI-komentoja.

Moduulijärjestelmä

Ajon aikana moduuli näkee tasan kaksi asiaa: alustan SDK:n ja projektisi muut moduulit. Ei npm-paketteja, ei tiedostojärjestelmää eikä verkkoa muuten kuin SDK:n fetch-kutsun kautta.

Tuo omat moduulisi samalla tavalla kuin missä tahansa JavaScript-projektissa — suhteellisella polulla ja .js-päätteellä — ja SDK sen nimellä:

// tgcloud/handlers/message.js
import { users } from '../schema.js';              //  skeemamoduuli
import { addItem } from '../lib/cart.js';          //  lib-moduuli
import { format } from '../lib/internal/fmt.js';  //  sisäkkäinen lib-moduuli
import { db, api, fetch } from 'sdk';              //  alustan SDK
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js';        //  naapuri
import { users } from '../schema.js';

Suhteellisen tuonnin on päätyttävä .js-päätteeseen eikä se voi johtaa tgcloud/-kansion ulkopuolelle:

import { users } from '../schema';       //  lisää .js
import config from '../../config.js';    //  tgcloud/ ulkopuolella
import { z } from 'zod';                 //  ei npm-paketteja

Jokaisella moduulilla on myös nimi — sen polku tgcloud/-kansion sisällä ilman päätettä, kuten schema, lib/cart tai handlers/message. Kyseistä nimeä npx tgcloud run ja CLI:n tuloste käyttävät, ja moduulin voi tuoda myös nimellä (from 'lib/cart', ei päätettä). Suosi koodissasi suhteellisia tuonteja. Ne ovat niitä, joita editorisi ymmärtää — hyppää määritelmään, automaattinen täydennys, uudelleennimeäminen.

Ajon aikana käytettävissä on tasan kaksi asiaa: sdk ja sen alimoduulit (sdk/db, sdk/api, sdk/fetch) — koko alustan rajapinta, katso SDK — sekä omat moduulisi tgcloud/-kansiossa. Se on koko lista. Jos koodisi importtaa jotain muuta, se ei ratkea. Tämä rajoitus pitää moduulit nopeina ladata ja turvallisina ajaa.

Käsittelijät

Käsittelijä on moduuli handlers/-kansiossa, jonka oletusvientiä alusta kutsuu, kun vastaava päivitys saapuu.

// handlers/message.js
import { api } from 'sdk';

export default async function (message) {
  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Sanoit: ${message.text ?? '(ei tekstiä)'}`,
  });
}

Käsittelijä vastaanottaa päivityksen payloadin argumenttina — alusta purkaa Telegram Update -objektin puolestasi. handlers/message.js saa Message -objektin (eli update.message); handlers/callback_query.js saa CallbackQuery -objektin; ja niin edelleen. Käsittelijän toinen argumentti on kutsujen välinen kontekstiobjekti, ctx. Se sisältää raa’an Update-objektin kohdassa ctx.update — käytä sitä, kun tarvitset jotain payloadin ulkopuolelta, kuten update_id.

Käsittelijä voi olla async (yleensä on) ja se voi palauttaa arvon. Se saavuttaa Bot API:n, tietokannan ja lähtevän HTTP-liikenteen SDK:n kautta.

Et tarvitse oikeaa päivitystä testataksesi käsittelijää. npx tgcloud run suorittaa sen alustalla antamallasi payloadilla ja nykyisellä paikallisella koodillasi:

npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'

Argumentti on payload — sama objekti, jonka käsittelijäsi vastaanottaa — JSON5-muodossa. Antaaksesi käsittelijän ctx-kontekstin (sen toisen argumentin), lisää --ctx, esim. --ctx '{ update: { update_id: 1 } }'. Tämä ajetaan paikallisia tiedostojasi vasten, joten voit kokeilla muutoksia ennen niiden julkaisemista. Katso run .

Päätepisteet

Päätepiste on moduuli endpoints/-kansiossa, jota Mini App kutsuu — paikka kaikelle sille, mitä käyttöliittymä ei voi tehdä itse, kuten tietokannan lukeminen ja kirjoittaminen, Bot API:n kutsuminen tai ulkopuolisten palveluiden keskustelu salaisuuksiesi avulla.

// 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('Profiilia ei löytynyt', { userId: user.id });
  return profile;
}

Ensimmäinen argumentti, input, on JSON-objekti, jonka Mini App lähetti. Toinen, ctx, sisältää ctx.initData-tiedot — Mini Appin init-data (user, chat, start_param, auth_date, …), jotka alusta on jo tarkistanut. Sillä on sama muoto kuin Telegram.WebApp.initDataUnsafe-objektilla asiakaspuolella, mutta täällä voit luottaa siihen. Kutsu ilman pätevää init-dataa hylätään 401-virheellä ennen kuin koodisi ajetaan, joten ctx.initData.user on todella se henkilö, joka avasi Mini Appin. Päätepisteitä voi kutsua vain tällä tavalla — botin Mini Appista, Telegram-käyttäjän toimesta — joten suojattavaa julkista rajapintaa ei ole eikä kirjautumista tarvitse rakentaa.

Se, minkä päätepiste palauttaa, lähetetään takaisin JSON-muodossa. Kieltäytyäksesi kutsusta, käytä throw new EndpointError(kuvaus, parametrit?). Mini App saa kuvauksen ja parametrit-objektin sellaisenaan. Jos sinun täytyy haarautua virheen tyypin mukaan käyttöliittymässä, laita koodi parametrit-objektiin — { code: 'OUT_OF_STOCK' }. Kaikki muut poikkeukset raportoidaan yleisenä sisäisenä virheenä, ja varsinainen viesti menee vain lokiisi, joten sisäiset asiat eivät ikinä vuoda asiakkaalle.

Päätepisteen kutsuminen Mini Appista

Mini App kutsuu päätepistettä nimellä käyttämällä komentoa Telegram.WebApp.Serverless.call virallisesta Mini Apps SDK -kirjastosta (telegram-web-app.js) — skripti, jonka Mini Appisi jo sisältää. Se lähettää init-datan puolestasi ja kutsuu takaisin virheellä tai päätepisteen tuloksella:

Telegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {
  if (err) {
    // err.message — kuvaus; err.parameters — mitä EndpointError sisälsi
    if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
    return;
  }
  render(profile);
});

Input on vapaaehtoinen ja sen on oltava tavallinen objekti. Ilman sitä kutsu on Serverless.call('getProfile', callback).

err.type on 'ENDPOINT_ERROR', kun päätepisteesi heitti EndpointError-virheen. Mikä tahansa muu arvo tarkoittaa, että alusta hylkäsi kutsun tai päätepiste epäonnistui odottamattomasti, esimerkiksi 'UNAUTHORIZED', kun init-data puuttuu tai on virheellinen.

Päätepisteiden nimet ovat kuin JavaScript-tunnisteet — kirjaimia, numeroita ja _, eivätkä ne ala numerolla (getProfile, place_order). Luo sellainen komennolla npx tgcloud add endpoints/<nimi> ja kokeile sitä ilman Mini Appia komennolla run , välittäen init-data itse:

npx tgcloud run endpoints/getProfile '{}' --ctx '{ initData: { user: { id: 1, first_name: "Ann" } } }'

Mitä julkaistaan

Kun ajat npx tgcloud push, CLI kerää jokaisen .js-tiedoston schema.js-, lib/-, handlers/- ja endpoints/-kansioista ja lähettää ne projektisi moduuliavaruutena — yhdessä Mini Appin koontikansion kanssa, jos tgcloud.jsonc nimeää sellaisen (katso alla). Kaikki, mikä on pilvessä mutta puuttuu projektistasi, poistetaan, joten julkaistu tila vastaa aina kansiotasi — poistot mukaan lukien. Mitään tgcloud/-kansion ulkopuolelta ei julkaista koodina — muu projektin juuri on sinun. tgcloud/-kansion sisällä oleva .js-tiedosto, joka ei ole yhdessä noista paikoista (tgcloud/util.js, tgcloud/helpers/x.js), merkitään, joten se ei jää vahingossa julkaisematta. Markdownia, pistetiedostoja ja .tgcloud/-kansiota ei koskaan julkaista.

Mini App -käyttöliittymä

Projektin juuri on sinun, ja se on luonnollinen paikka Mini Appin käyttöliittymälle — rakennettu Reactilla, Vuella, Sveltellä tai tavallisella HTML:llä, Vite-työkalulla tai millä tahansa muulla niputtajalla. Serverless isännöi sen koontiversion botin vieressä. Yksi push julkaisee koodin ja staattiset tiedostot yhdessä, ja sovellus tarjoillaan osoitteessa https://app<app_id>.tgcloud.ai/ (push tulostaa tarkan osoitteen — käytä sitä bottisi Mini App -URL-osoitteena @BotFatherissa ).

Väärinkäytösten raportointi. tgcloud.ai-palvelussa olevat Mini Appit ovat riippumattomien kehittäjien julkaisemia. Raportoidaksesi laittomasta sisällöstä tai väärinkäytöksistä app<app_id>.tgcloud.ai-aliverkkotunnuksella, lähetä sähköpostia osoitteeseen [email protected] .

Aloita luomalla käyttöliittymä ensin millä tahansa haluamallasi työkalulla, ja lisää sitten Serverless sen päälle:

npm create vite@latest example_app   # tai create-vue, …
cd example_app
npm create @tgcloud/bot .

Projektin luontityökalu ei koskaan ylikirjoita tiedostojasi. Se lisää tgcloud/-kansion käyttöliittymän viereen, tunnistaa koontikansion ja lisää npm run deploy-skriptin, joka kokoaa sovelluksen ja julkaisee sen yhdessä botin kanssa.

Se, mitä isännöidään, asetetaan tiedostossa tgcloud.jsonc, projektin konfiguraatiotiedostossa projektin juuressa:

{
  "$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 }]
  }
}

Vain "source" on vaadittu. Se on koontikansio suhteessa projektin juureen. Muut "static"-avaimet konfiguroivat, miten se tarjoillaan (alla). init luo tgcloud.jsonc-tiedoston puolestasi. Kun se tunnistaa käyttöliittymän, se täyttää myös "source"-kentän:

Käyttöliittymä init asettaa "source"-arvoksi
Vite — React, Vue, Svelte, Solid, … dist
Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular ei mitään — konfiguroi staattinen vienti ensin, aseta sitten "source" kansioon itse

Muilla asetuksilla, aseta "source" koontikansioosi itse.

$schema-rivi antaa editorillesi automaattisen täydennyksen ja validoinnin jokaiselle avaimelle. Tiedosto on JSON-muotoinen kommenteilla ja sallituilla loppupilkuilla. Jos jokin muu työkalusi täytyy lukea se tavallisena JSON-tiedostona, nimeä se tgcloud.json-muotoon — silloin sen on oltava tiukkaa JSONia.

Kokoa ennen julkaisua. push lataa kansion täsmälleen sellaisena kuin se on levyllä — lukuun ottamatta tiedostoja ja kansioita, joiden nimi alkaa pisteellä, jotka ohitetaan — eikä koskaan aja koontia, joten tavallinen npx tgcloud push käyttöliittymän muokkaamisen jälkeen julkaisisi edellisen koontiversion. Käytä npm run deploy (se ajaa build-skriptisi, sitten tgcloud push), tai kokoa itse ensin.

Tarjoiluvaihtoehdot:

  • spa — tarjoile index.html poluille, joita ei ole olemassa eikä niillä ole tiedostopäätettä, jotta asiakaspuolen reititin toimii uudelleenlatauksessa. Päätteelliset polut saavat edelleen 404.
  • immutable — mallit sisällön hash-nimisille tiedostoille (Viten /assets/*), joita selaimet voivat välimuistittaa vuoden ajan. Käytä tätä vain hash-nimisille tiedostoille. Mikä tahansa muu tässä täsmäävä tiedosto pysyisi selaimissa vanhentuneena vuoden, eikä sitä voi kutsua takaisin.
  • headers — vastausotsikot poluille, joihin sääntö täsmää (tarkka polku tai /*-päätteinen etuliite). null-arvo poistaa otsikon.
  • redirects — uudelleenohjaussäännöt, joissa ensimmäinen osuma voittaa. Olemassa oleva tiedosto voittaa aina säännön. status on 301, ellet aseta sitä.
  • "static": "dist" — lyhenne { "source": "dist" } silloin, kun et tarvitse mitään muuta.
  • "static": false — poista julkaistu sivusto seuraavassa julkaisussa. Avaimen poistaminen vain lopettaa tämän projektin staattisten tiedostojen hallinnan, ja julkaistu sivusto pysyy.

Sivusto tarjoillaan sen oman isännän juuresta, joten niputtajasi oletusarvoinen base (/) on oikein. Telegram Web avaa Mini Appit iframessa, joten älä lisää X-Frame-Options- tai rajoittavaa frame-ancestors-käytäntöä.

Sama isäntä vastaa päätepisteisiisi /api/-polun alla — sinne Telegram.WebApp.Serverless.call('getProfile', …) ohjataan. Älä laita omia tiedostojasi /api/-kansion alle koontikansiossa.

Tietokanta

Jokainen botti saa oman tietokannan — SQLite-pohjaisen tallennustilan, joka säilyy kutsujen välillä ja on jokaisen moduulin saatavilla db-kautta. Kuvaat taulusi schema.js-tiedostossa pienellä, tyypitetyllä DSL:llä; luet ja kirjoitat niitä sujuvalla kyselynrakentajalla; ja kehität niitä tarkastetuilla migraatioilla.

Tunnetko Drizzle ORM:n ? Silloin tiedät jo kuinka keskustella tietokannan kanssa tässä. Skeeman DSL ja kyselynrakentaja seuraavat Drizzleä — sarakerakentajat, select().from().where(), operaattorit, onConflictDoUpdate, .returning() ja sql-tagi käyttäytyvät kaikki odotetulla tavalla, joten datan lukeminen ja kirjoittaminen on tuttua rajapintaa. Tuot ne vain sdk/db-kansiosta, ja muutama alustakohtainen erityispiirre (erityisesti ei vieraita avaimia ) osoitetaan, kun niitä tulee vastaan.

Taulujen ilmoittaminen

Taulut ovat nimettyjä vientejä schema.js-tiedostossa. table()-funktion kutsuminen rakentaa kuvauksen lataushetkellä (se ei koske tietokantaan); alusta löytää viedyt taulut, kun julkaiset schema.js-tiedoston, ja migroi tietokannan vastaamaan sitä.

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 on SQL-taulun nimi;
  • columns on kartta JS-ominaisuus → sarakekuvaus;
  • extras on vapaaehtoinen takaisinkutsu (t) => ({ … }), jossa t paljastaa sarakkeet (t.created on viite kyseiseen sarakkeeseen) — ilmoita indeksit ja taulutason rajoitteet tässä.

Sarake-tyypit

Tehdas SQLite-tyyppi Huomautukset
text() TEXT
integer() INTEGER
real() REAL alias float()
numeric() NUMERIC
blob() BLOB lukee/kirjoittaa Uint8Array
boolean() INTEGER tallennettu 0/1, luettu true/false
json() TEXT auto JSON.stringify / JSON.parse

Sarakenimen argumentti on vapaaehtoinen — jätä se pois, niin JS-avainta käytetään. mode-valinta ohjaa, miten arvot muuntuvat SQliten ja JavaScriptin välillä.

mode Tallennettu muodossa JS-arvo
boolean INTEGER 0/1 boolean
json TEXT (JSON) mikä tahansa objekti/taulukko
timestamp INTEGER (unix sekuntia) Date
timestamp_ms INTEGER (unix ms) Date
bytes BLOB Uint8Array

boolean() ja json() ovat lyhenteitä integer(name, { mode: 'boolean' }) ja text(name, { mode: 'json' }).

blob() lukee ja kirjoittaa Uint8Array-tyyppiä — suoritusympäristössä ei ole Noden Buffer-tyyppiä (ja Buffer on Uint8Array-alaluokka, joten tämä on kannettava kantatyyppi). Yllä oleva mode hallitsee vain sitä, miten arvo enkoodataan, riippumatta sarakkeen tallennustyypistä: joten blob('col', { mode: 'json' }) on BLOB-vastine json()-tyypille — sama JSON-enkoodaus, pidetty BLOB-sarakkeessa tekstin sijaan.

TLDR: blob() käyttää Uint8Array-tyyppiä. mode ohjaa enkoodausta, ei tallennusta, joten blob(..., { mode: 'json' }) tallentaa JSONin BLOB-muodossa, kun taas json() tallentaa sen TEXT-muodossa.

Sarakemuokkaimet

Sarakemuokkaimet ketjutetaan sarakkeen perään.

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' })  // tai 'virtual'
text('email').deprecated('replaced by login')   // merkitsee sarakkeen poistettavaksi

.default() json()-sarakkeessa enkoodaa arvon puolestasi; sql`…` -default välitetään sellaisenaan. .deprecated() on päätepiste — katso Migraatiot .

Indeksit ja rajoitteet

Indeksit ja rajoitteet ilmoitetaan extras-takaisinkutsussa, jossa sarakkeet ovat näkyvissä.

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})`) ,          // lausekeindeksi
  active: index('idx_active').on(t.userId).where(sql`done = 0`) ,  // osittainen indeksi
}));

Taulutason muokkaimet ketjutetaan table(...)-funktion jälkeen: .strict(), .withoutRowid(), .deprecated('syy').

Ei vieraita avaimia

Suoritusympäristö toimii PRAGMA foreign_keys pois päältä -tilassa. Ilmoitettu vieras avain olisi hiljaisesti inertti — ei kaskadeja, ei orpovarauksia — mikä on huonompi kuin se, ettei niitä ole lainkaan, joten DSL tekee siitä mahdottoman. Nimittäin .references() ja taulutason foreignKey() heittävät virheen ilmoitettaessa, eikä skeema, joka käyttää niitä, julkaista.

Sinun tulisi mallintaa suhteet tavallisilla sarakkeilla (userId: integer('user_id')) ja valvoa eheyttä sovelluskoodissasi: lisää vanhemmat ennen lapsia, poista lapset ennen vanhempia, käsittele virheet oikein ja siivoa orvot LEFT JOIN … WHERE parent.id IS NULL-kyselyllä, kun tarvitset.

Vieraiden avaimien puuttuminen on tietoinen rajoitus, ei huolimattomuutta. Ota tämä huomioon varhain, kun suunnittelet bottiasi.

Kyselyt

db on sujuva kyselynrakentaja. Jokainen kysely on asynkroninen — päätemetodit (.all(), .get(), .values(), .run()) palauttavat Promise-objekteja, joten aina käytä 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();                          // kaikki rivit
await db.select().from(todos).where(eq(todos.id, 1)).get();   // ensimmäinen rivi tai null
await db.select().from(todos).values();                       // rivit arvotaulukoina

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();

// mukautettu projektio: { 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();

// rivien määrä — apufunktio, ei rakentajan pääte:
await db.$count(todos);                        // kaikki rivit
await db.$count(todos, eq(todos.done, false)); // suodattimella
  • Ketjutettavissa: .where(), .orderBy(), .limit(), .offset(), .groupBy(), .having(), .distinct().
  • Päätteet: .all(), .get(), .values().
  • Laske rivit db.$count(table, where?)-funktiolla (tai count()-funktiolla projektiossa).

Lisääminen, päivittäminen, poistaminen:

await db.insert(todos).values({ userId: 1, text: 'Buy milk' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run();   // erä
await db.insert(todos).values({ text: 'X' }).returning().run();        // RETURNING *

await db.insert(users).values({ tgId: 42, name: 'Ann' })
  .onConflictDoUpdate({ target: users.tgId, set: { name: 'Ann' } }).run();

await db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run();
await db.delete(todos).where(eq(todos.id, 1)).run();

Huomaa, että erälisäys on yksi lauseke, joten se on rajoitettu SQliten muuttujarajoituksella (rivien määrä × sarakkeiden määrä); jos ylität sen, lisäys epäonnistuu — pilko se itse.

Operaattorit tuodaan sdk/db-kansiosta:

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) useammalla argumentilla on sama kuin and(a, b). Vertailun toinen argumentti on oletusarvoisesti arvo, mutta se voi olla toinen sarake tai sql`…` -lauseke — esim. eq(a.x, b.y). Aggregaatit (count/sum/avg/min/max) ovat SQL-fragmentteja .select({ … })-projektioon.

Raaka SQL — kun rakentaja ei riitä, käytä raakaa SQL:ää. Tila valitaan metodilla: db.run kirjoituksille, db.all monille riveille, db.get yhdelle riville, db.values riveille paikkakohtaisina taulukoina.

await db.run('UPDATE todos SET done = 1 WHERE id = :id', { ':id': 5 });
// → { rowsAffected: 3, lastInsertRowid: 0, rows: [] }

await db.all(sql`SELECT * FROM todos WHERE done = ${false}`);
await db.get(sql`SELECT count(*) AS c FROM todos`);
await db.values(sql`SELECT id, text FROM todos`);   // → [[1, 'buy milk']]

Kirjoitus ratkeaa ajotulokseen — rowsAffected, lastInsertRowid ja rows, jotka sisältävät RETURNING-lausekkeen tulokset, jos SQL:ssä oli sellainen. Käytä db.values-metodia, kun kysely projisoi kaksi saman nimistä saraketta (join kuten SELECT u.id, o.id): db.all avaintaa rivit sarakenimen mukaan, joten toinen id ylikirjoittaisi ensimmäisen.

sql`…` -tagi muuttaa ${value}-arvon sidotuksi parametriksi ja ${table.column}-arvon tunnisteeksi, ja yhdistää sisäkkäiset sql-fragmentit. Käytä sql.raw('…')-metodia literaalille ilman parametreja.

Raakakyselyt eivät ole sidottuja tauluun, joten niiden rivit tulevat takaisin ilman tilamuunnosta (mode conversion) — boolean on 0/1, JSON-sarake on merkkijono, aikaleima on numero. Vain tauluun sidottu rakentaja muuntaa arvot.

Migraatiot

Tietokantasi muuttuu botin kasvaessa. Alusta pitää sen turvallisena erottamalla koodin julkaisemisen ja datan muuttamisen toisistaan, sekä luokittelemalla jokaisen skeeman muutoksen sen riskialttiuden mukaan.

Julkaisu ei koskaan koske tietokantaa. Kun teet npx tgcloud push-komennon muutetulle schema.js-tiedostolle, alusta tallentaa uuden skeeman ja kertoo sinulle, mitä tietokanta muuttaisi — mutta ei sovella mitään:

npx tgcloud push       # julkaise schema.js; raportoi odottavat DB-muutokset
npx tgcloud migrate    # tarkista ja sovella ne

npx tgcloud migrate laskee eron skeemasi ja elävän tietokannan välillä ja ohjaa sinut sen läpi. Sinulta kysytään aina vahvistus ennen kuin mitään sovelletaan. Tämä tarkoittaa, ettei rutiininomainen koodin julkaisu voi koskaan vahingossa laukaista datamigraatiota.

Jokainen odottava muutos sisältää tilan, joka määrittää, miten migrate käsittelee sen:

Tila Merkitys migrate-kohdassa
safe Additiivinen ja ei-estävä — uusi taulu, sarake tai indeksi Sovelletaan yhdessä yhdessä vaiheessa, vahvistuksella
warning Mahdollisesti tuhoisa tai hidas — poistaminen tai indeksi valtavassa taulussa Esitetään yksi kerrallaan, jokainen vahvistetaan erikseen
manual Ei voida tehdä automaattisesti — esim. sarakkeen tyypin muuttaminen Näytetään ohjeistuksella; suoritat sen käsin
undocumented Olemassa tietokannassa mutta ei skeemassasi Näytetään tiedoksi; ei sovelleta

Turvalliset muutokset ovat nopeita ja hengeltään palautettavissa, joten ne menevät läpi yhdessä. Jokainen varoitus on harkittu, yksittäinen vahvistus — tuhoisille muutoksille ei ole “sovella kaikki” -vaihtoehtoa.

Manuaaliset muutokset sisältävät syyn ja ehdotetun toiminnon. migrate päättyy yhteenvetoon: kuinka monta muutosta sovellettiin, ohitettiin, odottaa manuaalista korjausta tai ei ole skeemassasi.

Katso migrate valitsimille (--dry-run, --safe, --yes, --local).

Asioiden poistaminen

Taulun tai sarakkeen poistaminen schema.js-tiedostosta ei pudota sitä — se tekisi vahingossa tapahtuvasta poistosta katastrofaalisen. Poistaaksesi jotain, merkitse se vanhentuneeksi:

// pudota sarake
text('email').deprecated('replaced by login')

// pudota koko taulu
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('unused');

Seuraavassa migrate-ajossa vanhentunut objekti näkyy warning-tilan poistona, jonka vahvistat erikseen. Kun se on poissa, poista ilmoitus.

Sarakkeen tyypin muuttaminen

Tyyppimuutokset ovat manuaalisia — SQLite ei aina voi tehdä niitä paikan päällä, ja olemassa olevien arvojen pakottaminen on harkinnanvaraista. migrate näyttää muutoksen ja sen perustelut; suorita se itse raa’alla SQL:llä (db.run(...)), yleensä luomalla uusi sarake tai taulu, kopioimalla data ja vaihtamalla.

SDK

Ajon aikana moduulilla on yksi kirjasto: sdk. Se niputtaa kolme asiaa, joita bottitaustajärjestelmä tarvitsee — tietokannan, Telegram Bot API:n ja lähtevän HTTP-liikenteen — ilman mitään asennettavaa ja ilman konfiguroitavia tunnisteita. Tietokanta (db) on käsitelty kohdassa Tietokanta ; tämä osio käsittelee api:n, fetch:n ja globaalin console-objektin.

import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk';   // koko pinta
// tai alimoduuleista:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
Tuonti Mikä se on
db Tietokanta — kyselynrakentaja ja skeeman DSL → Tietokanta
api Telegram Bot API — api.sendMessage(...) → alla
fetch Lähtevä HTTP → alla
InputFile Tiedosto (tavut + nimi) ladattavaksi — toimii sekä api:n että fetch:n kanssa → Tiedostot
EndpointError Kieltäydy Mini Appin kutsusta viestillä, jonka se voi näyttää → Päätepisteet

Tuo omat projektimoduulisi suhteellisella polulla ja .js-päätteellä (from '../schema.js', from '../lib/cart.js'). Katso Moduulijärjestelmä .

Bot API

api antaa sinulle koko Telegram Bot API:n . Kutsu mitä tahansa metodia muodossa api.<metodi>(parametrit). Jokainen nykyinen — ja tuleva — Bot API -metodi toimii ilman SDK-päivitystä.

import { api } from 'sdk';

const me = await api.getMe();                            // → purettu tulos
await api.sendMessage({ chat_id: id, text: 'Hello!' });
await api.editMessageText({ chat_id, message_id, text: 'Updated' });
await api.answerCallbackQuery({ callback_query_id, text: 'Done' });

Vastauskuori on purettu. Bot API käärii tulokset normaalisti { ok: true, result: … } -muotoon. api palauttaa result-tuloksen suoraan — getMe() ratkeaa käyttäjäobjektiksi, ei kuoreksi. Parametrit käyttävät Bot API:n omia snake_case-nimiä (chat_id, message_id, reply_markup, …).

Virheet heittävät BotApiError-poikkeuksen. Kun Bot API palauttaa { ok: false }, kutsu heittää BotApiError-poikkeuksen sen sijaan, että palauttaisi falsy-arvon, joten et voi vahingossa ohittaa sitä. Virhe sisältää .code (Bot API error_code), .description (ihmisen luettava viesti), .method (mikä metodi epäonnistui) ja .parameters (lisädata kuten retry_after 429-virheessä tai migrate_to_chat_id). Nappaa se käsitelläksesi odotettu virhe ja heitä muut uudelleen:

import { api, BotApiError } from 'sdk';

try {
  await api.deleteMessage({ chat_id, message_id });
} catch (e) {
  if (e instanceof BotApiError && e.code === 400) {
    // 400 = viesti on jo poissa; se on fine tässä.
  } else {
    throw e;
  }
}

Tiedostot

Työskentele tiedostojen tavuilla, älä vain file_id-tunnuksilla — lataa uusia tiedostoja ja lataa olemassa olevia.

Lataus — InputFile. Kääri raa’at tavut InputFile-objektiin (tavallinen säiliö: tavut, tiedostonimi ja valinnainen MIME-tyyppi) ja välitä se minne tahansa, missä Bot API -metodi odottaa tiedostoa:

import { api, InputFile } from 'sdk';

const doc = new InputFile(bytes, 'report.pdf', { type: 'application/pdf' });
await api.sendDocument({ chat_id, document: doc });

// parametrien sisään kätketyt tiedostot toimivat myös — esim. albumi:
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' }) },
]});

Tavut suoratoistetaan Telegramiin aitona multipart-lähetyksenä — tiedostoa ei puskuroida kokonaan yhteen kutsuun alustalle — mutta Telegramin Bot API -rajoitukset ovat silti voimassa: botti voi tällä hetkellä ladata jopa 50 MB tiedostoa kohden (ja 10 MB kuvaa kohden).

Lataaminen — file_id-tunnuksella. Kaksi apufunktiota hakevat tiedoston tavut takaisin. Alusta ratkaisee file_id-tunnuksen puolestasi (se ajaa getFile-kutsun ja hakee sisällön taustalla), joten et koskaan koske bottitunnisteeseen tai lataus-URL-osoitteeseen:

const bytes = await api.getFileContent(file_id);          // → Uint8Array (koko tiedosto)

const { file, body } = await api.getFileStream(file_id);  // suoratoista sen sijaan
file.file_unique_id;                                      // getFile-tiedot
for await (const chunk of body) { /* Uint8Array */ }      // tai puskuroida: await stream.bytes()

Telegramin Bot API rajoittaa getFile-lataukset 20 MB-kokoon, joten se on molempien apufunktioiden yläraja.

HTTP

fetch on fetch-tyyppinen asiakas ulkomaailman kutsumiseen — kolmannen osapuolen rajapinnat, web-koukut, mikä tahansa HTTP-protokollan yli.

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();

Vastaus vastaa web-alustaa: res.status, res.statusText, res.ok (true arvoille 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()), ja rungon lukijat await res.json() / await res.text().

Voit myös lukea rungon inkrementaalisesti virtana — for await (const chunk of res.body) { … } — jolla kulutat server-sent events -tapahtumia tai merkki-merkiltä tuotettua tulostetta tekoälyrajapinnoista.

Runkoapufunktiot asettavat vastaavan Content-Type-otsikon puolestasi:

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

Pyyntöjen rungot voivat kantaa myös tiedostoja ja raakoja tavuja: välitä Uint8Array, InputFile tai FormData (multipart/form-data-muodolle) body-argumenttina, ja tavut suoratoistetaan ulos — joten voit ladata kolmannen osapuolen rajapintaan samalla tavalla kuin lähetät tiedostoja Telegramiin:

import { fetch, FormData, InputFile } from 'sdk';

const form = new FormData();
form.append('file', new InputFile(bytes, 'photo.jpg', { type: 'image/jpeg' }));
form.append('caption', 'from my bot');
const res = await fetch('https://api.example.com/upload', { method: 'POST', body: form });

Se käyttäytyy muuten kuin standardi fetch, jonka jo tunnet, kahdella rajoituksella:

  • Vastaus luetaan tekstinä — await res.json() / await res.text(), tai suoratoistetaan pala kerrallaan res.body-objektilla.
  • Kokonaisvastaus on rajoitettu 30 MB-kokoon. Tuo raja kattaa koko vastauksen — suoratoisto res.body-objektilla antaa sinun käsitellä suuren rungon inkrementaalisesti, mutta se ei nosta rajaa.

Lokitus

Standardi globaali console on käytettävissä — ei tuotavaa, se on vain siinä, kuten missä tahansa JavaScriptissä. Sen tuloste kaapataan ja näytetään npx tgcloud run-komennolla, mikä tekee siitä ensisijaisen virheenjäljitystyökalusi kehityksen aikana.

console.log('processing', { chatId: id });   // loki / debug — tavallinen
console.info('started');                     // info  — sininen
console.warn('rate limited');                // varoitus  — keltainen
console.error(err);                          // virhe — punainen, pinon jäljityksellä

Jokainen rivi on merkitty [file:line]-tiedolla, josta se tuli. console.error ja console.trace lisäävät koko pinon, kun taas console.warn ei. Kun ajat npx tgcloud run moduulille, nämä rivit tulostetaan värillisellä etuliitteellä tason mukaan, ajoajalla ja alkuperällä — katso run .

Komentorivityökalu

tgcloud on silta projektikansiosi ja pilven välillä. Se luo projekteja, näyttää mikä muuttui, julkaisee, ajaa moduuleja ja sovellat tietokantamigraatioita. Se vaatii Node.js 18 tai uudemman. Kaksi tapaa saada se:

# Suositeltu — luo projekti CLI:llä asennettuna siihen:
npm create @tgcloud/bot example_bot

# Tai asenna CLI globaalisti ja alusta tyhjä kansio:
npm install -g @tgcloud/cli
tgcloud init

npm-paketti on @tgcloud/cli, ja sen asentama komento on tgcloud. Jokainen komento toimii mistä tahansa projektisi alikansiosta.

Komento Tarkoitus
init Luo uusi projekti nykyiseen kansioon
add Luo uusi moduuli (käsittelijä, päätepiste tai lib-moduuli)
login Yhdistä projekti bottiin (tallentaa tunnisteen)
status Näytä mikä muuttui paikallisesti vs. pilvessä
diff Näytä rivikohtaiset muutokset
push Julkaise muuttuneet moduulit ja Mini Appin koonti pilveen
migrate Sovella skeeman muutokset tietokantaan
run Suorita moduuli alustalla julkaisematta
fetch Tarkista mikä on julkaistu koskematta paikallisiin tiedostoihin
pull Tuo paikalliset tiedostot vastaamaan pilveä
reset Hylkää paikalliset muutokset; palauta pilven tilasta
webhook Tarkasta ja synkronoi alustan hallitsema web-koukku
completion Tulosta komentorivin täydennysskripti (bash/zsh/fish)
upgrade Siirrä pre-0.2.0 projektirakenne tgcloud/-kansioon

Autentikointi

Projekti on sidottu yhteen bottiin tunnisteellaan, joka on muodossa app<id>:<secret>. app<id>-osa on julkinen ja voidaan tulostaa; salaisuus ei koskaan näy lokeissa tai virheissä.

Tunniste ratkaistaan tässä järjestyksessä:

  1. TGCLOUD_TOKEN-ympäristömuuttuja — CI-ympäristöjä varten; ei koskaan kirjoiteta levylle.
  2. .tgcloud/credentials — kirjoitettu komennolla npx tgcloud login .
  3. Ei kumpaakaan → virhe, joka ohjaa komennolle npx tgcloud login.

CLI ei koskaan pyydä tunnistetta kesken komennon. Kirjautuminen on aina eksplisiittinen login-vaihe. Jos tallennettu tunniste muuttuu virheelliseksi, CLI poistaa sen ja pyytää sinua kirjautumaan uudelleen.

init

npx tgcloud init

Luo uuden projektin nykyiseen hakemistoon: tgcloud/-kansion, jossa on schema.js, lib/, handlers/ ja aloittava käsittelijä, plus tgcloud.jsonc, AGENTS.md, docs/ ja .tgcloud/-tila. Aja se myös olemassa olevan käyttöliittymäprojektin juuressa — se lisää tgcloud/-kansion ja dokumentaation eikä koskaan ylikirjoita tiedostojasi. Jos se tunnistaa käyttöliittymän, se osoittaa tgcloud.jsonc-tiedoston koontikansioon ja lisää deploy-skriptin (build, sitten push) tiedostoon package.json — vain jos sinulla ei vielä ole deploy-skriptiä. Katso Mini App -käyttöliittymä .

init kieltäytyy luomasta projektia toisen projektin sisälle. Uudelleenajaminen projektin omassa juuressa on kunnossa ja lisää vain puuttuvan.

add

npx tgcloud add <kohde>

Luo yhden uuden moduulin, kytkettynä ja valmiina muokattavaksi — käsittelijä, päätepiste tai lib/-moduuli.

npx tgcloud add handlers/callback_query   # uusi päivitysten käsittelijä
npx tgcloud add endpoints/getProfile      # uusi päätepiste Mini Appille
npx tgcloud add lib/cart                  # uusi jaettu moduuli

Huomaa, että add ei koskaan ylikirjoita olemassa olevaa tiedostoa.

<kohde> on moduulin polku (päätteellinen .js on vapaaehtoinen). handlers/-kansiossa nimen on oltava Telegram-päivitystyyppi. Alusta mainostaa päteviä tyyppejä, joten virheellinen nimi hylätään heti. handlers/- ja endpoints/-kansiot ovat tasaisia, kun taas lib/ voi olla sisäkkäinen (lib/payments/stripe).

Moduulin nimi on vaadittu. Vain hakemiston antaminen on virhe — mutta avulias sellainen: handlers/-kansion kohdalla se listaa päivitystyypit, joita sinulla ei vielä ole, jotta voit kopioida yhden.

$ npx tgcloud add handlers
Virhe: Määritä nimi, esim. "handlers/callback_query". Saatavilla olevat handlers/-tyypit: callback_query, inline_query, chat_member, …

<Tab>-täydennys tarjoaa saman setin — katso completion .

Luodussa tiedostossa on toimiva export default, joten käsittelijä on aktiivinen heti kun julkaiset sen — ei tarvitse poistaa kommentteja. Julkaise uusi moduuli komennolla push .

login

npx tgcloud login

Pyytää CLI-pääsytunnistettasi — @BotFather → bottisi → Serverless → CLI Access → Access token, eri kuin botin API-tunniste — validoi sen alustaa vasten ja tallentaa sen tiedostoon .tgcloud/credentials.

login on ainoa komento, joka pyytää tunnistetta, ja se vaatii interaktiivisen komentorivin. CI-ympäristöissä aseta sen sijaan TGCLOUD_TOKEN.

status

npx tgcloud status

Näyttää tiedostokohtaisesti, mikä on muuttunut työhakemistosi ja julkaistun kopion välillä: muokattu, uusi, poistettu, ei muutoksia. Se toimii offline-tilassa, verraten viimeisimmän push-, pull- tai fetch-toiminnon pilven tilaan. Se varoittaa myös harhaanjohtavista .js-tiedostoista tgcloud/-kansion sisällä, joita ei julkaista.

Projektille, jossa on Mini App, status näyttää myös, mikä muuttui koontikansiossa viimeisimmän julkaisun jälkeen.

diff

npx tgcloud diff

Kuten status, mutta näyttää todelliset rivikohtaiset erot muuttuneille moduuleille. Myös offline-tilassa.

push

npx tgcloud push [tiedostot...]

Julkaisee projektisi pilveen yhtenä atomisena eränä.

Ilman argumentteja se julkaisee koko projektin, ja julkaistu tila vastaa täsmälleen kansiotasi — paikallisesti poistetut moduulit poistetaan pilvestä.

Tiedosto- tai kansioargumenteilla (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/), se julkaisee vain kyseisten tiedostojen muutokset. Kohdennettu push ei koskaan poista muita moduuleita. Kohteen voi kirjoittaa tiedostopolkuna (tgcloud/handlers/message.js), polkuna tgcloud/-kansion sisällä (handlers/message.js) tai moduulin nimenä (handlers/message) — ne kaikki tarkoittavat samaa moduulia.

Sen yksi valitsin on --force — ohittaa samanaikaisuuden tarkistuksen ja ylikirjoittaa pilvessä olevan. Käytä sitä vain, kun olet varma (katso Pysyminen synkroniassa ).

Jos tgcloud.jsonc nimeää staattisen kansion, täysi push julkaisee sen samassa erässä — vain muuttuneet tiedostot ladataan — ja tulostaa sivuston osoitteen. Kokoa ensin, koska push ei koskaan aja koontia (npm run deploy tekee molemmat). npx tgcloud push dist/ julkaisee staattiset tiedostot yksin, ja push, joka nimeää vain moduuleita, jättää ne koskematta.

Julkaisun jälkeen, jos schema.js muuttui ja tietokanta on epätasapainossa, push tulostaa yhteenvedon odottavista muutoksista ja ehdottaa npx tgcloud migrate. Se ei koskaan sovella niitä itse.

migrate

npx tgcloud migrate

Soveltaa skeeman muutokset tietokantaan. Se laskee eron schema.js-tiedoston ja elävän tietokannan välillä, ja ohjaa sinut sen läpi askel kerrallaan [N/M]-laskurilla:

  • Lyhyt yhteenveto kaikista odottavista muutoksista.
  • Turvalliset muutokset, sovelletaan yhdessä vahvistuksellasi.
  • Varoitukset (poistot, hitaat operaatiot), yksi kerrallaan, jokainen vahvistetaan erikseen.
  • Manuaaliset muutokset, näytetään syyllä ja ehdotetulla toiminnolla, ei sovelleta automaattisesti.
  • Dokumentoimattomat objektit (tietokannassa mutta ei skeemassasi), näytetään tiedoksi.

Se päättyy yhteenvetoon: sovellettu, ohitettu, odottaa manuaalista korjausta, ei skeemassa.

Katso Migraatiot mallia varten.

Valitsimet: --dry-run (tulosta kaikki, älä sovella mitään), --safe (sovella turvalliset muutokset automaattisesti, ohita varoitukset), --yes (sovella turvalliset muutokset ja jokainen varoitus automaattisesti, ohita manuaaliset — käytä varoen), --local (vertaa paikalliseen schema.js-tiedostoon julkaistun sijaan). Ilman valitsinta migrate vaatii interaktiivisen komentorivin eikä arvaile.

run

npx tgcloud run <moduuli> [argumentit] [--ctx <json5>]

Suorittaa käsittelijän tai päätepisteen alustalla ilman julkaisua, käyttäen nykyisiä paikallisia tiedostojasi. Tämä on nopein tapa testata logiikkaa.

  • <moduuli> — nimi (etsitään handlers/ ja endpoints/ alta) tai polku kuten handlers/message. Nimi, joka löytyy molemmista kansioista, on kirjoitettava kansiopolun kanssa.
  • [argumentit] — payload, joka välitetään käsittelijälle, JSON5-muodossa. Se on päivitystyyppiobjekti, jonka käsittelijäsi vastaanottaa (esim. Message handlers/message-käsittelijälle), tai päätepisteen input, ja sen on oltava objekti — ei taulukko tai pelkkä merkkijono.
  • --ctx <json5> — moduulin kontekstiobjekti (sen toinen argumentti), myös JSON5. Käytä sitä syöttämään koodisi lukemat ctx-tiedot — raaka päivitys käsittelijälle, --ctx '{ update: { update_id: 1 } }', tai init-data päätepisteelle, --ctx '{ initData: { user: { id: 1 } } }'. run ei validoi tätä, koska testaat logiikkaasi, et alustan tarkistuksia.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'

Alusta ajaa moduulia paikallisesta projektistasi kootusta moduuliavaruudesta (joten paikallisesti muutettu lib/-koodi käytetään myös) ja palauttaa paluuarvon, kaiken lokitetun console.*-komennoilla, ja kuluneen ajan. Lue suuret argumentit tiedostosta npx tgcloud run handlers/message "$(cat message.json5)".

fetch

npx tgcloud fetch

Tarkistaa mikä on tällä hetkellä julkaistu, koskematta työtiedostoihisi. Tämän jälkeen status ja diff vertaavat viimeisintä pilven tilaa. Hyödyllinen ristiriidan tarkasteluun ennen päätöstä sen ratkaisemisesta.

pull

npx tgcloud pull

Tuu projektisi vastaamaan pilveä päivittämällä työtiedostosi julkaistuun tilaan.

pull ei koskaan kirjoita Mini Appin koontikansiota.

reset

npx tgcloud reset

Hylkää paikalliset muutoksesi ja palauttaa moduulit viimeisimmästä tunnetusta pilven tilasta. Käytä kokeilun hylkäämiseen. Mini Appin koontikansioon ei kosketa.

webhook

npx tgcloud webhook
npx tgcloud webhook sync [--drop-pending]

Telegram toimittaa päivitykset botillesi web-koukun kautta, jota alusta hallitsee puolestasi — et koskaan osoita sitä mihinkään käsin. npx tgcloud webhook näyttää sen nykyisen tilan: URL, allowed_updates-lista, kuinka monta päivitystä on jonossa, viimeisin toimitusvirhe (jos on) ja onko se synkroniassa julkaistujen käsittelijöiden kanssa.

“Synkroniassa” tarkoittaa, että web-koukku osoittaa alustalle ja sen allowed_updates vastaa julkaistuja käsittelijöitäsi — joten Telegram toimittaa täsmälleen ne päivitystyypit, joita käsittelet, eikä mitään muuta. Uuden käsittelijän julkaisu (tai poistaminen) voi jättää web-koukun epätasapainoon, kunnes se päivitetään; npx tgcloud status merkitsee myös tämän.

npx tgcloud webhook sync korjaa sen — se osoittaa web-koukun uudelleen alustalle ja rakentaa allowed_updates-listan julkaistuista käsittelijöistäsi. Lisää --drop-pending hylätäksesi päivitykset, jotka Telegram oli jo jonottanut ennen synkronointia (muuten ne toimitetaan heti kun web-koukku on terve).

completion

Huom: tab-täydennys toimii vain kun paljas tgcloud on PATH-ympäristömuuttujassasi — joten asenna se globaalisti (npm install -g @tgcloud/cli) tai lisää binääri PATH-muuttujaasi muuten. Se ei voi koukkua npx-komentoon.

tgcloud completion <bash|zsh|fish>

Tulostaa komentorivin täydennysskriptin stdout-tulosteeseen. Ota se käyttöön kerran, niin <Tab> täydentää komennot, valitsimet, moduulikansiot, päivitystyypit joita sinulla ei vielä ole, ja paikalliset ajettavat moduulit.

# bash — tarvitsee bash-completion-paketin:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc

# zsh — varmista että `autoload -U compinit && compinit` ajetaan tiedostossa ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc

# fish:
tgcloud completion fish > ~/.config/fish/completions/tgcloud.fish

Käynnistä komentorivi (tai lataa tiedosto uudelleen) tämän jälkeen. tgcloud completion-komennon ajaminen ilman kuorta tulostaa nämä ohjeet uudelleen.

upgrade

npx tgcloud upgrade [--dry-run]

Kertaluontoinen migraatio projekteille, jotka on luotu ennen CLI 0.2.0-versiota, jotka pitivät schema.js-, lib/- ja handlers/-tiedostot projektin juuressa. Se siirtää ne tgcloud/-kansioon. Linkki bottiin säilyy, eikä mitään tarvitse julkaista uudelleen. --dry-run tulostaa suunnitellut siirrot koskematta mihinkään. Muut komennot eivät toimi vanhalla rakenteella ennen kuin päivität.

Pysyminen synkroniassa

Jokaisella projektilla on pilvessä monotonisesti kasvava revisio, joka päivittyy jokaisella julkaisulla. CLI muistaa revision, jonka kanssa se viimeksi synkronoi, ja lähettää sen jokaisella push-kerralla. Jos pilvi on liikkunut eteenpäin — koska toinen kone tai tiimin jäsen julkaisi — push hylätään sen sijaan, että se ylikirjoittaisi hiljaa heidän työtään, ja CLI tarjoaa kolme tapaa eteenpäin:

npx tgcloud fetch           # katso mitä pilvessä muuttui, tarkista uudelleen
npx tgcloud pull            # tuo uusin tila työtiedostoihisi
npx tgcloud push --force    # ylikirjoita pilven tila (vaarallinen)

Tämä optimistinen samanaikaisuuden tarkistus on syy siihen, miksi voit jakaa botin tiimin kesken ilman lukittua julkaisuprosessia. Komentojen paluuarvo on nollasta poikkeava virheen sattuessa — hylätty julkaisu, epäonnistunut migraatio, autentikointivirhe, moduuli joka heitti poikkeuksen run-kerralla — joten ne toimivat puhtaasti skripteissä ja CI-putkissa.