Telegram Serverless позволяет запускать серверный код для вашего бота и мини-приложения непосредственно в инфраструктуре Telegram — вам не нужно подготавливать серверы, поддерживать их в рабочем состоянии или думать о масштабировании. Вы пишете обычные JavaScript-модули, развертываете их одной командой, и Telegram запускает их в быстром, изолированном V8-песочнице, расположенной рядом с Bot API и встроенной базой данных.
Если вы когда-либо подключали бота к VPS, облачной функции или панели хостинга только для обработки команды /start, то теперь вам не придется этого делать.
Почему serverless
Telegram-бот по своей сути является программой, которая реагирует на обновления. Традиционно вам приходилось размещать эту программу где-то, где она всегда была бы включена, доступна и безопасна — и поддерживать ее в таком состоянии. Telegram Serverless полностью устраняет этот слой:
- Никакой инфраструктуры. Нет машины, которую нужно арендовать, патчить или мониторить. Ваш код выполняется по запросу и автоматически масштабируется вместе с вашим ботом.
- Учетные данные включены. Telegram Bot API, база данных на основе SQLite и исходящие HTTP-запросы доступны каждому модулю “из коробки” — ничего устанавливать не нужно, никаких учетных данных подключать не требуется.
- Быстрое, изолированное выполнение. Каждое выполнение происходит в легком V8 isolate, близком к собственным системам Telegram, поэтому вызовы Bot API и вашей базы данных быстрые и надежные.
- Настоящий рабочий процесс разработчика. Проект находится в папке на вашем компьютере под управлением версиями. Вы редактируете файлы, точно видите, что изменилось, атомарно развертываете и продвигаете схему базы данных вперед с помощью проверенных миграций — так, как вы уже работаете со всем остальным.
Модель мышления: Как работает проект
Вы работаете в трех местах, и они чисто соответствуют друг другу:
| Где | Что там находится |
|---|---|
| Папка вашего проекта | JavaScript-модули — схема, общий код, обработчики обновлений, конечные точки мини-приложения — и, если у вас есть, фронтенд мини-приложения |
| Облако | Развернутая копия этих модулей, база данных вашего бота и файлы мини-приложения |
CLI tgcloud |
Мост — он показывает вам различия и синхронизирует их |
Вы никогда не подключаетесь по SSH ни к чему. Вы редактируете файлы локально, запускаете npx tgcloud push, и платформа берет на себя все остальное. Трафик вашего бота обрабатывается развернутыми модулями; ваша база данных сохраняется между вызовами.
Проект состоит всего из четырех видов кода, все внутри одной папки tgcloud/:
tgcloud/
├─ endpoints/ # функции, которые вызывает ваше мини-приложение
├─ handlers/ # функции, реагирующие на типы обновлений Telegram
├─ lib/ # общий код, который вы импортируете откуда угодно
└─ schema.js # таблицы вашей базы данных
Когда приходит обновление — сообщение, нажатие кнопки, встроенный запрос — Telegram направляет его к соответствующему обработчику (handlers/message.js, handlers/callback_query.js, …) и вызывает его экспорт по умолчанию. Эта функция взаимодействует с Bot API и базой данных через SDK и возвращается. Это весь цикл. Обновление без соответствующего обработчика просто игнорируется, поэтому вы добавляете только те обработчики, которые вам нужны.
Мини-приложение является необязательным. Если у вашего бота есть мини-приложение, его фронтенд находится в том же проекте, рядом с tgcloud/. Serverless размещает его сборку и запускает вызываемые им endpoints/. См.
Фронтенд мини-приложения
.
Быстрая демонстрация
Вот полностью рабочий демонстрационный бот. Он отвечает на каждое сообщение и запоминает, сколько сообщений он видел от каждого чата.
tgcloud/schema.js
import { table, integer } from 'sdk/db';
export const counters = table('counters', {
chatId: integer('chat_id').primaryKey() ,
seen: integer('seen').notNull().default(0) ,
});
tgcloud/handlers/message.js
import { api, db } from 'sdk';
import { sql } from 'sdk/db';
import { counters } from '../schema.js';
export default async function (message) {
const chatId = message.chat.id;
// Вставляем счетчик или увеличиваем его, если такой чат уже есть — и получаем
// результирующую строку в том же операторе с помощью .returning().
const [row] = await db.insert(counters)
.values({ chatId, seen: 1 })
.onConflictDoUpdate({
target: counters.chatId,
set: { seen: sql`${counters.seen} + 1` },
})
.returning()
.run();
await api.sendMessage({
chat_id: chatId,
text: `Привет! Я видел ${row.seen} сообщение(ий) от вас.`,
});
}
Разверните его:
npx tgcloud push # загрузить модули
npx tgcloud migrate # создать таблицу `counters`
Это рабочий бот с сохраняемым состоянием и без сервера. Все в нем — api, db, DSL table() — описано в разделах ниже.
Serverless — это общий бэкенд для Telegram-ботов и мини-приложений, а не шаблон для одного типа приложений. Он идеально подходит для:
- Разговорные боты с ИИ, которым нужно хранить состояние для каждого пользователя в базе данных.
- Бэкенды мини-приложений, которые хранят данные пользователей и предоставляют динамический контент.
- Игры и инструменты — включая таблицы лидеров, викторины и многое другое.
- Автоматизации и интеграции, которые вызывают сторонние HTTP API и отправляют результаты в чаты.
Начало работы
Этот учебник проведет вас от пустой папки к рабочему боту, который отвечает на сообщения и хранит данные. Предполагается, что у вас установлен
Node.js
версии 18 или новее и зарегистрирован бот через
@BotFather
. К концу вы освоите каждую команду, которую будете использовать ежедневно: push, migrate, run и status.
Прежде всего, включите Serverless. В @BotFather откройте своего бота → Serverless и включите его. Это активирует функцию для этого бота и разблокирует его CLI-токен, обработчики, библиотеку и базу данных.
1. Создание проекта
Самый быстрый способ начать — использовать создатель проекта, который генерирует структуру проекта и устанавливает в него CLI:
npm create @tgcloud/bot example_bot
cd example_bot
Аргумент — это целевая папка: передайте . для создания структуры в текущей папке или любой путь. Он также работает в существующей папке и никогда не перезаписывает уже имеющиеся файлы.
Это даст вам готовый к редактированию проект:
example_bot/
├─ tgcloud/ # все, что запускается платформой
│ ├─ handlers/
│ │ └─ message.js # стартовый обработчик сообщений (отражает текст обратно)
│ ├─ lib/ # ваши общие модули здесь (пусто для начала)
│ └─ schema.js # таблицы вашей базы данных
├─ docs/
│ └─ tgcloud-sdk.md # справочник по SDK (для вас и ваших ИИ-инструментов)
├─ AGENTS.md # ориентация для ИИ-помощников по кодированию
├─ package.json
└─ tgcloud.jsonc # конфигурация проекта
Сгенерированные файлы самодокументированы — каждый из них содержит закомментированные примеры того, что можно сделать дальше.
CLI устанавливается в проект как локальная зависимость для разработки, поэтому вы запускаете его с помощью npx tgcloud <command> (npx находит копию в node_modules вашего проекта) или через ярлыки npm run, которые scaffold добавляет в package.json (npm run deploy, npm run status). По умолчанию глобальный tgcloud отсутствует в вашей PATH.
Вы также можете установить его глобально — npm install -g @tgcloud/cli — если вы предпочитаете вводить tgcloud без префикса из любого места. Это удобно для запуска tgcloud init в любой пустой папке, и это то, что нужно для автозавершения в оболочке. В любом случае вы получите один и тот же проект.
Создаете мини-приложение? Начните с его фронтенда. Создайте его с помощью обычного инструмента, затем запустите создатель проекта в этой папке. См. Фронтенд мини-приложения .
2. Привязка бота
Каждый проект привязан к одному боту. Свяжите их с помощью login, который запрашивает ваш CLI-токен доступа (
@BotFather
→ ваш бот → Serverless → CLI Access → Access token — отдельный токен от вашего
API-токена бота
) и сохраняет его локально:
npx tgcloud login
Токен имеет вид app<id>:<secret>. CLI сохраняет его в .tgcloud/, который игнорируется git, и никогда не выводит секретную часть. Login — это единственный раз, когда вас запрашивают об этом — см.
Аутентификация
для того, как токены разрешаются в CI.
3. Ознакомление
Две команды в любой момент покажут вам, каково положение дел, обе полностью офлайн:
npx tgcloud status # что изменилось локально по сравнению с развернутой копией
npx tgcloud diff # изменения построчно
Сразу после init все ново, и ничего еще не развернуто. status показывает стартовые файлы, ожидающие загрузки.
4. Развертывание
Отправьте ваши модули в облако:
npx tgcloud push
push атомарно загружает каждый измененный модуль и обновляет вашу локальную запись о том, что теперь находится в облаке. Ваш бот работает: откройте его в Telegram и отправьте ему сообщение — стартовый обработчик отразит его обратно.
Развертывание никогда не затрагивает вашу базу данных. Отправка кода и изменение схемы базы данных — это намеренно отдельные шаги, поэтому развертывание кода никогда не может удивить вас миграцией данных. Для этого и предназначен следующий шаг.
5. Добавление таблицы в базу данных
Давайте заставим бота что-то запоминать. Откройте tgcloud/schema.js и объявите таблицу:
import { table, integer, text, sql } from 'sdk/db';
export const messages = table('messages', {
id: integer('id').primaryKey({ autoIncrement: true }) ,
chatId: integer('chat_id').notNull() ,
text: text('text') ,
created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`) ,
});
Разверните схему, затем примените ее к базе данных:
npx tgcloud push # загружает новую схему
npx tgcloud migrate # создает таблицу `messages`
push сообщает, что схема не синхронизирована, и показывает вам ожидающее изменение, но ничего не применяет. migrate проведет вас через изменение и, после вашего подтверждения, создаст таблицу. Эта двухэтапная модель — и то, что происходит с более рискованными изменениями, такими как удаление — рассматривается в
Миграции
.
6. Хранение и чтение данных
Теперь используйте таблицу из вашего обработчика. Отредактируйте tgcloud/handlers/message.js:
import { api, db } from 'sdk';
import { eq } from 'sdk/db';
import { messages } from '../schema.js';
export default async function (message) {
// Сохраняем это сообщение.
await db.insert(messages)
.values({ chatId: message.chat.id, text: message.text })
.run();
// Подсчитываем, сколько мы сохранили для этого чата.
const count = await db.$count(messages, eq(messages.chatId, message.chat.id));
await api.sendMessage({
chat_id: message.chat.id,
text: `Сохранено. Это ${count} сообщение(ий) из этого чата на данный момент.`,
});
}
Разверните обновленный обработчик с помощью npx tgcloud push, затем отправьте боту несколько сообщений и наблюдайте, как счетчик растет. База данных сохраняется между вызовами — это память вашего бота.
7. Тестирование без развертывания
Вам не нужно развертывать, чтобы попробовать изменение. npx tgcloud run выполняет обработчик на платформе, используя ваши локальные файлы, не публикуя их:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hello" }'
Аргумент — это полезная нагрузка, которую получает ваш обработчик — для handlers/message это
Message
— написанный в
JSON5
(так что вы можете пропустить кавычки для ключей). Команда выводит все, что обработчик вывел с помощью console.*, возвращаемое значение и сколько времени это заняло. Это самый короткий цикл для итерации по логике — без развертывания, без ожидания реального сообщения.
8. Синхронизация
По мере работы несколько команд поддерживают ваш локальный проект и облако в синхронизированном состоянии: npx tgcloud status показывает, что изменилось, npx tgcloud push развертывает, npx tgcloud pull синхронизирует ваш локальный проект с облаком, npx tgcloud fetch проверяет, что развернуто, не трогая ваши файлы, а npx tgcloud reset отбрасывает локальные изменения.
Если два человека (или две машины) развертывают один и тот же бот, платформа обнаруживает конфликт, и
pushостанавливается, чтобы вы могли сначалаpull— вы никогда не сможете незаметно перезаписать работу другого. См. Поддержание синхронизации .
Создание с помощью ИИ
Предпочитаете создавать с помощью ИИ-помощника — или единственным разработчиком в вашей команде является ИИ? Вы все еще можете выпустить бота. Мы сделали первый шаг, чтобы ИИ-агент чувствовал себя как дома в проекте: каждый новый проект создается с файлом AGENTS.md и справочником docs/tgcloud-sdk.md, которые агентные инструменты кодирования читают автоматически.
Вместе с небольшим, самодостаточным рантаймом — один SDK, никаких npm-пакетов для управления — это дает помощнику хороший старт в отношении соглашений, которые обычно упускаются при общем кодогенерации: относительные импорты с .js, отсутствие внешних ключей, каждый вызов db является асинхронным, один обработчик на тип обновления и двухэтапный поток push/migrate.
Попробуйте:
npm create @tgcloud/bot my-bot
cd my-bot
opencode # или Claude Code, Cursor, … — любой агент, читающий AGENTS.md
Затем просто спросите обычным языком:
Напиши бота, который запоминает список дел каждого человека — добавь элемент, когда они отправляют текст, и покажи весь список, когда они отправляют /list.
Ассистент редактирует schema.js и ваши обработчики за вас; вы просматриваете, мгновенно тестируете изменение с помощью npx tgcloud run, а затем выпускаете его в продакшн с помощью npx tgcloud push и npx tgcloud migrate. AGENTS.md является частью вашего проекта — редактируйте его по мере роста бота, чтобы руководство оставалось точным.
В дороге с BotFather
Остались только с телефоном? Весь проект также живет в @BotFather — откройте своего бота → Serverless и получите все, чем управляет CLI, на сенсорном экране:
- Обработчики — создавайте, редактируйте и тестируйте обработчики обновлений; BotFather держит вебхук в синхронизации с имеющимися у вас обработчиками (такое же состояние в синхронизации / вне синхронизации, которое сообщает CLI).
- Конечные точки — функции, которые вызывает ваше мини-приложение.
- Библиотека — ваши общие модули
lib/. - База данных — редактируйте
schema.jsв том же синтаксисе, похожем на Drizzle, просматривайте ожидающие изменения и применяйте их; Сохранить развертывает. - Файлы мини-приложения — фронтенд, который ваши пользователи открывают в Telegram.
- Доступ к CLI — получите токен доступа CLI здесь, когда вернетесь к клавиатуре.
Это один и тот же облачный проект, поэтому вы можете начать обработчик на своем телефоне и позже npx tgcloud pull его на свой ноутбук — ничего не привязано к одному клиенту. Запуск обработчика даже показывает его Консольный вывод прямо в чате, как и
npx tgcloud run
.
Проекты и модули
Serverless-проект — это обычная папка под контролем версий. Все, что запускается платформой, находится в одной папке, tgcloud/; остальная часть проекта — ваша — фронтенд мини-приложения, конфигурация, документация. Для бота нет шага сборки, нет node_modules во время выполнения и нет точки входа на сервер.
Анатомия проекта
example_bot/
├─ tgcloud/ # развернутая часть — больше ничего нет
│ ├─ handlers/ # обработчики обновлений — плоские, только один уровень
│ │ ├─ message.js
│ │ └─ callback_query.js
│ ├─ endpoints/ # функции, вызываемые вашим мини-приложением — плоские, только один уровень
│ │ └─ getProfile.js
│ ├─ lib/ # общие модули; подкаталоги разрешены
│ │ ├─ reply.js
│ │ └─ internal/util.js
│ └─ schema.js # схема базы данных — один файл
└─ .tgcloud/ # состояние CLI, игнорируется git
Только файлы .js внутри tgcloud/ — schema.js, lib/, handlers/, endpoints/ — развертываются. Все остальное — Markdown, файлы конфигурации, папка .tgcloud/, все вне tgcloud/ — остается на вашей машине. Имена модулей относительны tgcloud/. Файл tgcloud/handlers/message.js — это модуль handlers/message, и это имя используется npx tgcloud run.
Обновление со старого проекта? Проекты, созданные до CLI 0.2.0, хранили
schema.js,lib/иhandlers/в корне проекта. Запуститеnpx tgcloud upgradeодин раз, чтобы переместить их вtgcloud/.
schema.js — ваша база данных. Он объявляет таблицы как именованные экспорты, используя DSL схемы, и находится в tgcloud/ как один файл. Он развертывается как любой другой модуль, но его развертывание никогда не изменяет базу данных — изменения схемы применяются отдельно с помощью npx tgcloud migrate. См.
База данных
.
lib/ — общий код, все, что вы хотите повторно использовать в обработчиках: чистые вспомогательные функции, слои доступа к базе данных, форматирование, интеграции с внешними службами. lib/ — единственная директория, которая может содержать подкаталоги (lib/internal/util.js, lib/payments/stripe.js), поэтому вы можете организовать большую кодовую базу так, как вам удобно. Модули в lib/ никогда не вызываются платформой напрямую; они существуют для импорта обработчиками и друг другом.
handlers/ — точки входа вашего бота. Каждый файл соответствует одному типу обновления Telegram, и платформа направляет каждое входящее обновление к соответствующему обработчику:
| Файл | Обрабатывает |
|---|---|
handlers/message.js |
Новые входящие сообщения |
handlers/inline_query.js |
Новые входящие встроенные запросы |
handlers/callback_query.js |
Новые входящие запросы обратного вызова |
| … | любой другой тип обновления Bot API |
handlers/ плоский — никаких подкаталогов. export default обработчика — это функция, которую платформа вызывает (см.
Обработчики
).
Тип обновления обрабатывается только если файл обработчика существует и не пуст. Если handlers/<type>.js отсутствует — или файл пуст — обновления этого типа игнорируются, и платформа ничего для них не запускает. Поэтому сохраняйте только те обработчики, которые вам действительно нужны: каждый добавленный вами обработчик — это еще один тип обновления, к которому просыпается ваш бот для обработки, а исключение остальных означает, что Telegram не запускает ваш код для обновлений, которые вы бы все равно отбросили.
Чтобы создать новый обработчик, запустите
npx tgcloud add handlers/<type>.
endpoints/ — серверная часть вашего мини-приложения. Каждый файл — это функция, которую мини-приложение вызывает по имени, поэтому endpoints/getProfile.js отвечает на POST /api/getProfile. Платформа проверяет, что вызов исходит от пользователя Telegram внутри вашего мини-приложения, прежде чем ваш код начнет выполняться, и передает вам этого пользователя. endpoints/ плоский, как и handlers/. См.
Конечные точки
.
.tgcloud/ — локальное состояние машины, полностью управляемое CLI, включая сохраненные учетные данные. Оно игнорируется git, и вам никогда не следует читать или записывать в него вручную — используйте вместо этого команды CLI.
Система модулей
Во время выполнения модуль может видеть ровно две вещи: платформенную SDK и другие модули в вашем проекте. Нет npm-пакетов, файловой системы или сети, кроме как через fetch из SDK.
Импортируйте собственные модули так же, как вы делаете это в любом JavaScript-проекте — по относительному пути, с расширением .js — а SDK по его имени:
// tgcloud/handlers/message.js
import { users } from '../schema.js'; // модуль схемы
import { addItem } from '../lib/cart.js'; // модуль lib
import { format } from '../lib/internal/fmt.js'; // вложенный модуль lib
import { db, api, fetch } from 'sdk'; // платформенная SDK
// tgcloud/lib/cart.js
import { format } from './internal/fmt.js'; // соседний модуль
import { users } from '../schema.js';
Относительный импорт должен заканчиваться на .js и не может выходить за пределы tgcloud/:
import { users } from '../schema'; // добавьте .js
import config from '../../config.js'; // вне tgcloud/
import { z } from 'zod'; // без npm-пакетов
Каждый модуль также имеет имя — его путь внутри tgcloud/ без расширения, например schema, lib/cart или handlers/message. Это имя используется npx tgcloud run и выводом CLI, и модуль может быть импортирован по нему (from 'lib/cart', без расширения). В своем коде предпочитайте относительные импорты. Они — то, что понимает ваш редактор — переход к определению, автодополнение, переименование.
Что доступно во время выполнения — это ровно две вещи: sdk и его подмодули (sdk/db, sdk/api, sdk/fetch) — вся поверхность платформы, см.
SDK
— и ваши собственные модули в tgcloud/. Это полный список. Если ваш код importит что-либо еще, он не будет разрешен. Это ограничение сохраняет модули быстрыми для загрузки и безопасными для выполнения.
Обработчики
Обработчик — это модуль в handlers/, чей экспорт по умолчанию платформа вызывает при поступлении соответствующего обновления.
// handlers/message.js
import { api } from 'sdk';
export default async function (message) {
await api.sendMessage({
chat_id: message.chat.id,
text: `Вы сказали: ${message.text ?? '(нет текста)'}`,
});
}
Обработчик получает полезную нагрузку обновления в качестве аргумента — платформа распаковывает для вас
Update
Telegram. handlers/message.js получает
Message
(т.е. update.message); handlers/callback_query.js получает
CallbackQuery
; и так далее. Второй аргумент обработчика — это объект контекста для каждого вызова, ctx. Он содержит необработанное Update как ctx.update — используйте его, когда вам нужно что-то вне полезной нагрузки, например update_id.
Для тестирования обработчика не требуется реальное обновление. npx tgcloud run выполняет его на платформе с предоставленной вами полезной нагрузкой и вашим текущим локальным кодом:
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
Аргумент — это полезная нагрузка — тот же объект, который получает ваш обработчик — в JSON5. Чтобы предоставить ctx обработчика (его второй аргумент), добавьте --ctx, например --ctx '{ update: { update_id: 1 } }'. Это выполняется против ваших локальных файлов, поэтому вы можете попробовать изменения перед их развертыванием. См.
run
.
Конечные точки
Конечная точка — это модуль в endpoints/, который вызывает ваше мини-приложение — место для всего, что фронтенд не может сделать самостоятельно, например, для чтения и записи базы данных, вызова Bot API или взаимодействия с внешними службами с вашими секретами.
// endpoints/getProfile.js
import { db, EndpointError } from 'sdk';
import { eq } from 'sdk/db';
import { profiles } from '../schema.js';
export default async function (input, ctx) {
const user = ctx.initData.user;
const profile = await db.select().from(profiles).where(eq(profiles.userId, user.id)).get();
if (!profile) throw new EndpointError('Профиль не найден', { userId: user.id });
return profile;
}
Первый аргумент, input, — это JSON-объект, отправленный мини-приложением. Второй, ctx, содержит ctx.initData —
данные инициализации
мини-приложения (user, chat, start_param, auth_date, …), уже проверенные платформой. Он имеет ту же структуру, что и Telegram.WebApp.initDataUnsafe на клиенте, но здесь вы можете ему доверять. Вызов без действительных данных инициализации отклоняется с кодом 401 до выполнения вашего кода, поэтому ctx.initData.user действительно является человеком, открывшим ваше мини-приложение. Конечные точки могут быть вызваны только таким образом — из мини-приложения вашего бота, пользователем Telegram — поэтому нет общедоступного API для защиты и нет необходимости создавать процесс входа.
Все, что возвращает конечная точка, отправляется обратно в формате JSON. Чтобы отклонить вызов, throw new EndpointError(description, parameters?). Мини-приложение получает описание и объект parameters в том виде, в котором они были переданы. Если вам нужно ветвиться в зависимости от типа ошибки на фронтенде, поместите код в parameters — { code: 'OUT_OF_STOCK' }. Любое другое исключение сообщается как общая внутренняя ошибка, а фактическое сообщение отправляется только в ваши журналы, поэтому внутренние детали не утекают на клиент.
Вызов конечной точки из мини-приложения
Мини-приложение вызывает конечную точку по имени с помощью Telegram.WebApp.Serverless.call из официального
SDK мини-приложений
, telegram-web-app.js — скрипта, который ваше мини-приложение уже включает. Он сам отправляет данные инициализации и вызывает обратно с ошибкой или результатом конечной точки:
Telegram.WebApp.Serverless.call('getProfile', { lang: 'en' }, function (err, profile) {
if (err) {
// err.message — описание; err.parameters — то, что несло ваше EndpointError
if (err.type === 'ENDPOINT_ERROR') showMessage(err.message);
return;
}
render(profile);
});
Ввод необязателен и должен быть простым объектом. Без него вызов будет Serverless.call('getProfile', callback).
err.type имеет значение 'ENDPOINT_ERROR', когда ваша конечная точка выбросила EndpointError. Любое другое значение означает, что платформа отклонила вызов или конечная точка сбойла неожиданно, например 'UNAUTHORIZED', когда данные инициализации отсутствуют или недействительны.
Имена конечных точек подобны идентификаторам JavaScript — буквы, цифры и _, не начинающиеся с цифры (getProfile, place_order). Создайте одно с помощью
npx tgcloud add endpoints/<name>
и попробуйте его без мини-приложения с помощью
run
, передав данные инициализации самостоятельно:
npx tgcloud run endpoints/getProfile '{}' --ctx '{ initData: { user: { id: 1, first_name: "Ann" } } }'
Что развертывается
Когда вы запускаете npx tgcloud push, CLI собирает каждый файл .js под schema.js, lib/, handlers/ и endpoints/, и отправляет этот точный набор как пространство модулей вашего проекта — вместе с папкой сборки мини-приложения, если tgcloud.jsonc указывает на нее (см. ниже). Все, что присутствует в облаке, но отсутствует в вашем проекте, удаляется, поэтому развернутое состояние всегда отражает вашу папку — включая удаления. Ничто вне tgcloud/ не развертывается как код — остальная часть корня проекта — ваша. Внутри tgcloud/ файл .js, который не находится в одном из этих расположений (tgcloud/util.js, tgcloud/helpers/x.js), помечается, поэтому он не будет незаметно развернут. Markdown, dotfiles и .tgcloud/ никогда не развертываются.
Фронтенд мини-приложения
Корень проекта — ваш, и это естественное место для фронтенда вашего мини-приложения — собранного с помощью React, Vue, Svelte или простого HTML, с помощью Vite или любого другого сборщика. Serverless размещает выходные данные сборки рядом с вашим ботом. Один push развертывает код и статические файлы вместе, и приложение обслуживается по адресу https://app<app_id>.tgcloud.ai/ ( push выводит точный адрес — используйте его как URL вашего мини-приложения в
@BotFather
).
Сообщение о злоупотреблениях. Мини-приложения на tgcloud.ai публикуются независимыми разработчиками. Чтобы сообщить о незаконном контенте или злоупотреблениях на субдомене
app<app_id>.tgcloud.ai, отправьте письмо по адресу [email protected] .
Чтобы начать, сначала создайте фронтенд с помощью любого удобного инструмента, а затем добавьте Serverless поверх него:
npm create vite@latest example_app # или create-vue, …
cd example_app
npm create @tgcloud/bot .
Создатель проекта никогда не перезаписывает ваши файлы. Он добавляет папку tgcloud/ рядом с фронтендом, распознает папку сборки и добавляет скрипт npm run deploy, который собирает приложение и развертывает его вместе с ботом.
Что размещается, определяется в tgcloud.jsonc, файле конфигурации проекта в корне проекта:
{
"$schema": "./node_modules/@tgcloud/cli/schema/tgcloud.json",
"static": {
"source": "dist",
"spa": true,
"immutable": ["/assets/*"],
"headers": [{ "path": "/*", "set": { "Referrer-Policy": "no-referrer" } }],
"redirects": [{ "from": "/old", "to": "/new", "status": 301 }]
}
}
Требуется только "source". Это папка сборки, относительная к корню проекта. Другие ключи "static" настраивают, как она обслуживается (ниже). init создает для вас tgcloud.jsonc. Когда он распознает фронтенд, он также заполняет "source":
| Фронтенд | init устанавливает "source" в |
|---|---|
| Vite — React, Vue, Svelte, Solid, … | dist |
| Next.js, Nuxt, SvelteKit, React Router, TanStack Start, Angular | ничего — сначала настройте статический экспорт, затем установите "source" в его папку самостоятельно |
С любой другой настройкой установите "source" в вашу папку сборки самостоятельно.
Строка $schema предоставляет вашему редактору автодополнение и проверку для каждого ключа. Файл представляет собой JSON с комментариями и разрешенными конечными запятыми. Если другой ваш инструмент должен читать его как обычный JSON, назовите его вместо этого tgcloud.json — тогда он должен быть строгим JSON.
Сборка перед push. push загружает папку точно в том виде, в каком она находится на диске — за исключением файлов и папок, имена которых начинаются с точки, которые пропускаются — и никогда не запускает вашу сборку, поэтому обычный npx tgcloud push после редактирования фронтенда развернет предыдущую сборку. Используйте npm run deploy (он запускает ваш скрипт build, затем tgcloud push) или соберите самостоятельно сначала.
Параметры обслуживания:
spa— обслуживатьindex.htmlдля путей, которые не существуют и не имеют расширения файла, поэтому клиентский маршрутизатор работает при перезагрузке. Пути с расширением по-прежнему получают404.immutable— шаблоны для файлов с хешированным содержимым (/assets/*Vite), которые браузеры могут кэшировать в течение года. Используйте его только для файлов с хешированным содержимым. Любой другой файл, соответствующий этому правилу, будет оставаться устаревшим в браузерах в течение года, и нет способа его отозвать.headers— заголовки ответов для путей, соответствующих правилу (точное совпадение пути или префикс, заканчивающийся на/*). Значениеnullудаляет заголовок.redirects— правила перенаправления, где первое совпадение выигрывает. Существующий файл всегда имеет приоритет над правилом.statusравен301, если вы его не установили."static": "dist"— сокращение для{ "source": "dist" }, когда вам ничего больше не нужно."static": false— удалить развернутый сайт при следующемpush. Вместо этого удаление ключа просто останавливает управление статическими файлами этим проектом, и то, что развернуто, остается.
Сайт обслуживается с корня своего собственного хоста, поэтому базовый URL вашего сборщика по умолчанию (/) правильный. Telegram Web открывает мини-приложения во iframe, поэтому не добавляйте X-Frame-Options или строгую политику frame-ancestors.
Тот же хост отвечает вашим
конечным точкам
по адресу /api/ — именно сюда отправляется Telegram.WebApp.Serverless.call('getProfile', …). Не помещайте в папку сборки файлы под /api/.
База данных
Каждый бот получает свою собственную базу данных — хранилище на основе SQLite, которое сохраняется между вызовами и доступно каждому модулю через db. Вы описываете свои таблицы в schema.js с помощью небольшого типизированного DSL; вы читаете и записываете их с помощью гибкого построителя запросов; и вы развиваете их с помощью проверенных миграций.
Знакомы с Drizzle ORM ? Тогда вы уже знаете, как взаимодействовать с базой данных здесь. DSL схемы и построитель запросов следуют Drizzle — построители столбцов,
select().from().where(), операторы,onConflictDoUpdate,.returning(), и тегsqlведут себя так, как вы ожидаете, поэтому чтение и запись данных — это знакомый API, который вы уже используете. Вы просто импортируете его изsdk/db, а пара специфических для платформы (в частности, отсутствие внешних ключей ) указывается там, где они встречаются.
Объявление таблиц
Таблицы — это именованные экспорты в schema.js. Вызов table() создает описание при загрузке (он не затрагивает базу данных); платформа обнаруживает экспортированные таблицы при развертывании schema.js и мигрирует базу данных, чтобы ей соответствовать.
import { table, integer, text, boolean, json, index, sql } from 'sdk/db';
export const users = table('users', {
id: integer('id').primaryKey({ autoIncrement: true }) ,
tgId: integer('tg_id').unique() ,
name: text('name').notNull() ,
lang: text('lang').default('en') ,
isAdmin: boolean('is_admin').default(false) ,
prefs: json('prefs') ,
created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`) ,
}, (t) => ({
createdIdx: index('idx_users_created').on(t.created) ,
}));
table(name, columns, extras?):
name— это имя SQL-таблицы;columns— это карта JS-свойство → определение столбца;extras— необязательный обратный вызов(t) => ({ … }), гдеtпредоставляет столбцы (t.created— это ссылка на этот столбец) — здесь объявляются индексы и ограничения на уровне таблицы.
Типы столбцов
| Фабрика | Тип SQLite | Примечания |
|---|---|---|
text() |
TEXT | |
integer() |
INTEGER | |
real() |
REAL | псевдоним float() |
numeric() |
NUMERIC | |
blob() |
BLOB | чтение/запись Uint8Array |
boolean() |
INTEGER | хранится как 0/1, читается как true/false |
json() |
TEXT | авто JSON.stringify / JSON.parse |
Аргумент имени столбца необязателен — опустите его, и будет использован JS-ключ. Параметр
modeконтролирует, как значения преобразуются между SQLite и JavaScript.
mode |
Хранится как | JS значение |
|---|---|---|
boolean |
INTEGER 0/1 | boolean |
json |
TEXT (JSON) | любой объект/массив |
timestamp |
INTEGER (секунды Unix) | Date |
timestamp_ms |
INTEGER (мс Unix) | Date |
bytes |
BLOB | Uint8Array |
boolean() и json() — это сокращения для integer(name, { mode: 'boolean' }) и text(name, { mode: 'json' }).
blob() читает и записывает Uint8Array — рантайм не имеет Node Buffer (и Buffer — это подкласс Uint8Array, так что это переносимый базовый тип). mode выше регулирует только то, как значение кодируется, независимо от типа хранения столбца: поэтому blob('col', { mode: 'json' }) — это BLOB-аналог json() — то же JSON-кодирование, но хранимое в BLOB-столбце, а не в TEXT.
Кратко:
blob()используетUint8Array.modeуправляет кодированием, а не хранением, поэтомуblob(..., { mode: 'json' })хранит JSON как BLOB, тогда какjson()хранит его как TEXT.
Модификаторы столбцов
Модификаторы столбцов цепляются к столбцу.
integer('id').primaryKey({ autoIncrement: true })
text('name').notNull()
text('tg').unique()
text('lang').default('en')
integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`)
text('slug').generatedAlwaysAs(sql`lower(name)`, { mode: 'stored' }) // или 'virtual'
text('email').deprecated('replaced by login') // помечает столбец для удаления
.default()для столбцаjson()кодирует значение за вас;sql`…`default передается как есть..deprecated()является терминальным — см. Миграции .
Индексы и ограничения
Индексы и ограничения объявляются в обратном вызове extras, где столбцы находятся в области видимости.
table('t', { /* … */ }, (t) => ({
uq: unique('uq_email').on(t.email) ,
chk: check('chk_done', sql`${t.done} in (0, 1)`) ,
idx: index('idx_name').on(t.col) ,
uidx: uniqueIndex('uidx_email').on(t.email) ,
lower: index('idx_lower').on(sql`lower(${t.email})`) , // индекс выражения
active: index('idx_active').on(t.userId).where(sql`done = 0`) , // частичный индекс
}));
Модификаторы на уровне таблицы цепляются после table(...): .strict(), .withoutRowid(), .deprecated('reason').
Нет внешних ключей
Рантайм работает с PRAGMA foreign_keys выключенным. Объявленный внешний ключ был бы беззвучно неактивен — без каскадных операций, без защиты от осиротевших записей — что хуже, чем полное их отсутствие, поэтому DSL делает это невозможным. А именно, .references() и foreignKey() на уровне таблицы выбрасывают исключение при объявлении, и схема, использующая их, не будет развернута.
Вам следует моделировать отношения с помощью обычных столбцов (userId: integer('user_id')) и обеспечивать целостность в коде вашего приложения: сначала вставляйте родительские записи, затем дочерние, корректно обрабатывайте ошибки и собирайте осиротевшие записи с помощью LEFT JOIN … WHERE parent.id IS NULL, когда вам это нужно.
Отсутствие внешних ключей — это намеренное ограничение, а не упущение. Учитывайте это на ранней стадии при планировании вашего бота.
Запросы
db — это гибкий построитель запросов. Каждый запрос асинхронен — терминальные методы (.all(), .get(), .values(), .run()) возвращают Promises, поэтому всегда используйте await.
import { db } from 'sdk';
import { eq, and, desc, asc, count, sql } from 'sdk/db';
import { users, todos } from '../schema.js';
await db.select().from(todos).all(); // все строки
await db.select().from(todos).where(eq(todos.id, 1)).get(); // первая строка или null
await db.select().from(todos).values(); // строки как массивы значений
await db.select().from(todos)
.where(and(eq(todos.userId, uid) , eq(todos.done, false)))
.orderBy(desc(todos.priority) , asc(todos.id))
.limit(10).offset(20)
.all();
// пользовательская проекция: { alias: columnRef | sqlExpr | aggregate }
await db.select({ id: todos.id, title: todos.text, n: count() })
.from(todos).groupBy(todos.userId).having(sql`count(*) > ${1}`).all();
// количество строк — помощник, а не терминал построителя:
await db.$count(todos); // все строки
await db.$count(todos, eq(todos.done, false)); // с фильтром
- Цепочечные:
.where(),.orderBy(),.limit(),.offset(),.groupBy(),.having(),.distinct(). - Терминалы:
.all(),.get(),.values(). - Подсчет строк с помощью
db.$count(table, where?)(илиcount()в проекции).
Вставка, обновление, удаление:
await db.insert(todos).values({ userId: 1, text: 'Buy milk' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run(); // пакетная вставка
await db.insert(todos).values({ text: 'X' }).returning().run(); // RETURNING *
await db.insert(users).values({ tgId: 42, name: 'Ann' })
.onConflictDoUpdate({ target: users.tgId, set: { name: 'Ann' } }).run();
await db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run();
await db.delete(todos).where(eq(todos.id, 1)).run();
Обратите внимание, что пакетная вставка — это один оператор, поэтому она ограничена пределом переменных SQLite (строки × столбцы); если вы его превысите, вставка завершится ошибкой — разбивайте ее самостоятельно.
Операторы импортируются из sdk/db:
import {
eq, ne, gt, gte, lt, lte,
like, notLike,
isNull, isNotNull, and, or, not,
between, notBetween, inArray, notInArray,
count, sum, avg, min, max,
asc, desc,
} from 'sdk/db';
.where(a, b) с несколькими аргументами эквивалентно and(a, b). Второй аргумент сравнения по умолчанию является значением, но может быть другим столбцом или sql`…` — например, eq(a.x, b.y). Агрегаты (count/sum/avg/min/max) являются SQL-фрагментами для проекции .select({ … }).
Необработанный SQL — когда построитель недостаточно гибок, переходите к необработанному SQL. Режим выбирается методом: db.run для записи, db.all для многих строк, db.get для одной строки, db.values для строк как позиционных массивов.
await db.run('UPDATE todos SET done = 1 WHERE id = :id', { ':id': 5 });
// → { rowsAffected: 3, lastInsertRowid: 0, rows: [] }
await db.all(sql`SELECT * FROM todos WHERE done = ${false}`);
await db.get(sql`SELECT count(*) AS c FROM todos`);
await db.values(sql`SELECT id, text FROM todos`); // → [[1, 'buy milk']]
Запись разрешается в результат выполнения — rowsAffected, lastInsertRowid и rows, которые содержат предложение RETURNING, если SQL имел его. Обращайтесь к db.values, когда запрос проецирует два столбца с одинаковым именем (соединение типа SELECT u.id, o.id): db.all ключует строки по имени столбца, поэтому второй id перезапишет первый.
Тег sql`…` превращает ${value} в связанный параметр, а ${table.column} — в идентификатор, и вставляет вложенные фрагменты sql. Используйте sql.raw('…') для литерала без параметров.
Необработанные запросы не связаны со столбцами, поэтому их строки возвращаются без преобразования режимов — булево значение — это
0/1, JSON-столбец — это строка, временная метка — это число. Только построитель, привязанный к таблице, преобразует значения.
Миграции
Ваша база данных изменяется по мере роста вашего бота. Платформа обеспечивает безопасность этого, разделяя развертывание кода и изменение данных, а также классифицируя каждое изменение схемы по степени его рискованности.
Развертывание никогда не затрагивает базу данных. Когда вы запускаете npx tgcloud push измененного schema.js, платформа записывает новую схему и сообщает вам, что база данных должна измениться — но ничего не применяет:
npx tgcloud push # развернуть schema.js; сообщает об ожидающих изменениях БД
npx tgcloud migrate # просмотреть и применить их
npx tgcloud migrate вычисляет разницу между вашей схемой и текущей базой данных и проводит вас через нее шаг за шагом с текущим счетчиком [N/M]:
- Краткое резюме всех ожидающих изменений.
- Безопасные изменения, применяемые вместе за один шаг после вашего подтверждения.
- Предупреждения (удаления, медленные операции) по одному, каждое подтверждается отдельно.
- Ручные изменения, показываемые с причиной и предлагаемым действием, не применяемые автоматически.
- Недокументированные объекты (в базе данных, но не в вашей схеме), показываемые для ознакомления.
Он завершается резюме: применено, пропущено, ожидает ручного исправления или отсутствует в схеме.
См.
migrateдля флагов (--dry-run,--safe,--yes,--local).
Удаление объектов
Удаление таблицы или столбца из schema.js не удаляет его — это сделало бы случайное удаление катастрофическим. Чтобы удалить что-то, пометьте это как устаревшее:
// удалить столбец
text('email').deprecated('replaced by login')
// удалить всю таблицу
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('unused');
При следующем migrate устаревший объект появится как предупреждение статуса удаления, которое вы подтверждаете индивидуально. Как только он исчезнет, удалите объявление.
Изменение типа столбца
Изменения типов — это ручные операции — SQLite не всегда может выполнить их на месте, а принудительное преобразование существующих значений — это вопрос суждения. migrate покажет изменение и его причину; выполните его самостоятельно с помощью необработанного SQL (db.run(...)), обычно путем создания нового столбца или таблицы, копирования данных и замены.
SDK
Во время выполнения модуль имеет одну библиотеку: sdk. Она объединяет три вещи, которые нужны серверной части бота — базу данных, Telegram Bot API и исходящий HTTP — без необходимости что-либо устанавливать и без необходимости настраивать учетные данные. База данных (db) рассмотрена в
База данных
; этот раздел охватывает api, fetch и глобальную переменную console.
import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk'; // вся поверхность
// или из подмодулей:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
| Импорт | Что это |
|---|---|
db |
База данных — построитель запросов и DSL схемы → База данных |
api |
Telegram Bot API — api.sendMessage(...) →
ниже
|
fetch |
Исходящий HTTP → ниже |
InputFile |
Файл (байты + имя) для загрузки — работает как с api, так и с fetch →
Файлы
|
EndpointError |
Отклонить вызов мини-приложения с сообщением, которое оно может отобразить → Конечные точки |
Импортируйте собственные модули проекта по относительному пути с расширением .js (from '../schema.js', from '../lib/cart.js'). См.
Система модулей
.
Bot API
api предоставляет вам полный
Telegram Bot API
. Вызовите любой метод как api.<method>(params). Каждый текущий — и будущий — метод Bot API работает без необходимости обновления SDK .
import { api } from 'sdk';
const me = await api.getMe(); // → распакованный результат
await api.sendMessage({ chat_id: id, text: 'Привет!' });
await api.editMessageText({ chat_id, message_id, text: 'Обновлено' });
await api.answerCallbackQuery({ callback_query_id, text: 'Готово' });
Оболочка ответа распакована. Bot API обычно оборачивает результаты в { ok: true, result: … }. api возвращает сам result — getMe() разрешается в объект пользователя, а не в оболочку. Параметры используют собственные имена Bot API в стиле snake_case (chat_id, message_id, reply_markup, …).
Сбои вызывают BotApiError. Когда Bot API возвращает { ok: false }, вызов вызывает BotApiError вместо возврата ложного значения, поэтому вы не можете случайно проигнорировать его. Ошибка содержит .code ( error_code Bot API), .description (читаемое человеком сообщение), .method (какой метод сбойил) и .parameters (дополнительные данные, такие как retry_after при 429, или migrate_to_chat_id). Перехватите ее, чтобы обработать ожидаемую ошибку и повторно выбросить остальные:
import { api, BotApiError } from 'sdk';
try {
await api.deleteMessage({ chat_id, message_id });
} catch (e) {
if (e instanceof BotApiError && e.code === 400) {
// 400 = сообщение уже удалено; здесь это нормально.
} else {
throw e;
}
}
Файлы
Работайте с байтами файлов, а не только с file_id — загружайте новые файлы и скачивайте существующие.
Загрузка — InputFile. Оберните необработанные байты в InputFile (простой контейнер: байты, имя файла и необязательный type MIME) и передайте его везде, где метод Bot API ожидает файл:
import { api, InputFile } from 'sdk';
const doc = new InputFile(bytes, 'report.pdf', { type: 'application/pdf' });
await api.sendDocument({ chat_id, document: doc });
// файлы, вложенные в параметры, также работают — например, альбом:
await api.sendMediaGroup({ chat_id, media: [
{ type: 'photo', media: new InputFile(a, 'a.jpg', { type: 'image/jpeg' }) },
{ type: 'photo', media: new InputFile(b, 'b.jpg', { type: 'image/jpeg' }) },
]});
Байты передаются в Telegram как реальная многокомпонентная загрузка — файл не буферизуется целиком в один вызов платформы — но ограничения Bot API Telegram все еще действуют: бот в настоящее время может загружать до 50 МБ на файл (и 10 МБ для фотографии).
Скачивание — по file_id. Два помощника извлекают байты файла обратно. Платформа разрешает file_id для вас (она запускает getFile и загружает содержимое за кулисами), поэтому вы никогда не прикасаетесь к токену бота или URL для скачивания:
const bytes = await api.getFileContent(file_id); // → Uint8Array (весь файл)
const { file, body } = await api.getFileStream(file_id); // потоковая передача вместо этого
file.file_unique_id; // информация getFile
for await (const chunk of body) { /* Uint8Array */ } // или буферизуйте: await stream.bytes()
Скачивания
getFileTelegram Bot API ограничены 20 МБ, поэтому это предельный размер для обоих помощников.
HTTP
fetch — это клиент, похожий на fetch, для вызова внешнего мира — сторонних API, веб-хуков, всего через HTTP.
import { fetch } from 'sdk';
const res = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Pavel' }) ,
});
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();
Ответ повторяет веб-платформу: res.status, res.statusText, res.ok (true для 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()) и чтецы тела await res.json() / await res.text().
Вы также можете читать тело инкрементально как поток —
for await (const chunk of res.body) { … }— так вы потребляете потоковые события сервера или вывод токен за токеном из AI API.
Помощники тела устанавливают соответствующий Content-Type для вас:
await fetch(url, { method: 'POST', body: fetch.body.json({ a: 1 }) }); // application/json
await fetch(url, { method: 'POST', body: fetch.body.form({ a: 1 }) }); // x-www-form-urlencoded
await fetch(url, { method: 'POST', body: fetch.body.text('hi') }); // text/plain
Тела запросов также могут содержать файлы и необработанные байты: передайте Uint8Array, InputFile или FormData (для multipart/form-data) как body, и байты будут переданы потоком — так что вы можете загружать на сторонний API так же, как отправляете файлы в Telegram:
import { fetch, FormData, InputFile } from 'sdk';
const form = new FormData();
form.append('file', new InputFile(bytes, 'photo.jpg', { type: 'image/jpeg' }));
form.append('caption', 'from my bot');
const res = await fetch('https://api.example.com/upload', { method: 'POST', body: form });
В остальном он ведет себя как стандартный fetch, который вы уже знаете, с двумя ограничениями:
- Ответ читается как текст —
await res.json()/await res.text(), или потоково по частям сres.body. - Общий ответ ограничен 30 МБ. Этот лимит покрывает весь ответ — потоковая передача с
res.bodyпозволяет обрабатывать большое тело инкрементально, но не повышает лимит.
Логирование
Стандартный глобальный console доступен — ничего импортировать не нужно, он просто есть, как в любом JavaScript. Его вывод захватывается и отображается npx tgcloud run, что делает его вашим основным инструментом отладки во время разработки.
console.log('processing', { chatId: id }); // log / debug — обычный текст
console.info('started'); // info — синий
console.warn('rate limited'); // warn — желтый
console.error(err); // error — красный, со стектрейсом
Каждая строка помечена [file:line], откуда она идет. console.error и console.trace добавляют полный стек, тогда как console.warn — нет. Когда вы запускаете npx tgcloud run из модуля, эти строки выводятся с цветным префиксом по уровню, временем с момента запуска и источником — см.
run
.
Интерфейс командной строки
tgcloud — это мост между папкой вашего проекта и облаком. Он создает структуры проектов, показывает вам, что изменилось, развертывает, запускает модули и применяет миграции базы данных. Ему нужен
Node.js
версии 18 или новее. Два способа получить его:
# Рекомендуется — создайте проект с установленным в нем CLI:
npm create @tgcloud/bot example_bot
# Или установите CLI глобально и инициализируйте пустую папку:
npm install -g @tgcloud/cli
tgcloud init
npm-пакет называется @tgcloud/cli, а команда, которую он устанавливает, — tgcloud. Каждая команда работает из любой подпапки вашего проекта.
| Команда | Назначение |
|---|---|
init
|
Создать структуру нового проекта в текущей папке |
add
|
Создать структуру нового модуля (обработчика, конечной точки или модуля lib) |
login
|
Привязать проект к боту (сохраняет токен) |
status
|
Показать, что изменилось локально по сравнению с облаком |
diff
|
Показать изменения построчно |
push
|
Развернуть измененные модули и сборку мини-приложения в облако |
migrate
|
Применить изменения схемы к базе данных |
run
|
Выполнить модуль на платформе без развертывания |
fetch
|
Проверить, что развернуто, не изменяя локальные файлы |
pull
|
Синхронизировать локальные файлы с облаком |
reset
|
Отбросить локальные изменения; восстановить из состояния облака |
webhook
|
Проверить и повторно синхронизировать вебхук, управляемый платформой |
completion
|
Напечатать скрипт автозавершения для оболочки (bash/zsh/fish) |
upgrade
|
Переместить макет проекта до 0.2.0 в tgcloud/ |
Аутентификация
Проект привязывается к одному боту по его токену, который имеет вид app<id>:<secret>. Часть app<id> является публичной и может быть напечатана; секрет никогда не появляется в логах или ошибках.
Токен разрешается в следующем порядке:
- Переменная окружения
TGCLOUD_TOKEN— для CI; никогда не записывается на диск. .tgcloud/credentials— записываетсяnpx tgcloud login.- Ни того, ни другого → ошибка, указывающая на
npx tgcloud login.
CLI никогда не запрашивает токен посреди команды. Вход всегда является явным шагом login. Если сохраненный токен становится недействительным, CLI удаляет его и просит вас снова выполнить login.
init
npx tgcloud init
Создает структуру нового проекта в текущем каталоге: папку tgcloud/ с schema.js, lib/, handlers/ и стартовым обработчиком, плюс tgcloud.jsonc, AGENTS.md, docs/ и папку состояния .tgcloud/. Запустите ее в корне существующего фронтенд-проекта — она добавляет папку tgcloud/ и документацию, и никогда не перезаписывает ваши файлы. Если она распознает фронтенд, она также указывает tgcloud.jsonc на папку сборки и добавляет скрипт deploy (сборка, затем push) в ваш package.json — только если у вас уже нет скрипта deploy. См.
Фронтенд мини-приложения
.
init отказывается создавать проект внутри другого проекта. Повторный запуск в корне самого проекта — это нормально и просто добавляет недостающее.
add
npx tgcloud add <target>
Создает структуру нового модуля, подключенного и готового к редактированию — обработчика, конечной точки или модуля lib/.
npx tgcloud add handlers/callback_query # новый обработчик обновлений
npx tgcloud add endpoints/getProfile # новая конечная точка для мини-приложения
npx tgcloud add lib/cart # новый общий модуль
Обратите внимание, что
addникогда не перезаписывает существующий файл.
<target> — это путь модуля (конечное .js необязательно). Для handlers/ имя должно быть типом обновления Telegram. Платформа рекламирует допустимый набор, поэтому недопустимое имя отклоняется заранее. handlers/ и endpoints/ плоские, тогда как lib/ может быть вложенным (lib/payments/stripe).
Имя модуля обязательно. Предоставление только каталога является ошибкой — но полезной: для handlers/ оно перечисляет типы обновлений, которых у вас еще нет, поэтому вы можете скопировать один.
$ npx tgcloud add handlers
Error: Specify a name, e.g. "handlers/callback_query". Available handlers/ types: callback_query, inline_query, chat_member, …
<Tab>автодополнение предлагает тот же набор — см.completion.
Сгенерированный файл имеет активный export default, поэтому обработчик активен сразу после развертывания — ничего не нужно раскомментировать. Разверните новый модуль с помощью
push
.
login
npx tgcloud login
Запрашивает ваш CLI-токен доступа — из
@BotFather
→ ваш бот → Serverless → CLI Access → Access token, отдельный токен от вашего
API-токена бота
— проверяет его на платформе и сохраняет в .tgcloud/credentials.
login— единственная команда, которая запрашивает токен, и ей требуется интерактивный терминал. В CI вместо этого установитеTGCLOUD_TOKEN.
status
npx tgcloud status
Показывает для каждого файла, что изменилось между вашей рабочей директорией и развернутой копией: изменено, новое, удалено, без изменений. Он работает офлайн, сравнивая с состоянием облака с момента вашего последнего push, pull или fetch. Он также предупреждает о посторонних .js файлах внутри tgcloud/, которые не будут развернуты.
Для проекта с мини-приложением status также показывает, что изменилось в его папке сборки с момента последнего развертывания.
diff
npx tgcloud diff
Как и status, но показывает фактические построчные различия для измененных модулей. Также работает офлайн.
push
npx tgcloud push [files...]
Развертывает ваш проект в облако одной атомарной партией.
Без аргументов он развертывает весь проект, и развернутое состояние точно отражает вашу папку — модули, которые вы удалили локально, удаляются в облаке.
С аргументами файлов или каталогов (npx tgcloud push tgcloud/handlers/message.js, npx tgcloud push handlers/) он развертывает только изменения в этих файлах. Целевой push никогда не удаляет другие модули. Цель может быть указана как путь к файлу (tgcloud/handlers/message.js), путь внутри tgcloud/ (handlers/message.js) или имя модуля (handlers/message) — все они означают один и тот же модуль.
Его единственный опция — --force — для пропуска проверки конкуренции и перезаписи всего, что находится в облаке. Используйте ее только тогда, когда вы уверены (см.
Поддержание синхронизации
).
Если tgcloud.jsonc указывает на папку со статическими файлами, полный push развертывает ее в той же партии — загружаются только измененные файлы — и выводит адрес сайта. Сначала соберите, потому что push никогда не запускает вашу сборку (npm run deploy делает и то, и другое). npx tgcloud push dist/ развертывает только статические файлы, а push, указывающий только модули, оставляет их без изменений.
После развертывания, если schema.js изменился и база данных не синхронизирована, push выводит сводку ожидающих изменений и предлагает npx tgcloud migrate. Он никогда не применяет их сам.
migrate
npx tgcloud migrate
Применяет изменения схемы к базе данных. Он вычисляет разницу между schema.js и текущей базой данных, а затем проводит вас через нее шаг за шагом со счетчиком [N/M]:
- Краткое резюме всех ожидающих изменений.
- Безопасные изменения, применяемые вместе за один шаг после вашего подтверждения.
- Предупреждения (удаления, медленные операции) по одному, каждое подтверждается отдельно.
- Ручные изменения, показываемые с причиной и предлагаемым действием, не применяемые автоматически.
- Недокументированные объекты (в базе данных, но не в вашей схеме), показываемые для ознакомления.
Он завершается резюме: применено, пропущено, ожидает ручного исправления или отсутствует в схеме.
См. Миграции для модели.
Параметры: --dry-run (напечатать все, применить ничего), --safe (автоматически применять безопасные изменения, пропускать предупреждения), --yes (автоматически применять безопасные изменения и все предупреждения, пропускать ручные — используйте с осторожностью), --local (сравнить с вашим локальным schema.js вместо развернутого). Без флага migrate требует терминал и выдает ошибку в неинтерактивной среде вместо угадывания.
run
npx tgcloud run <module> [args] [--ctx <json5>]
Выполняет обработчик или конечную точку на платформе без развертывания, используя ваши текущие локальные файлы. Это быстрая внутренняя петля для тестирования логики.
<module>— простое имя (ищется вhandlers/иendpoints/) или путь, напримерhandlers/message. Простое имя, существующее в обоих каталогах, должно быть указано с его каталогом.[args]— полезная нагрузка, передаваемая обработчику, написанная в JSON5 , так что вы можете пропустить кавычки для ключей. Это объект типа обновления, который получает ваш обработчик (например, Message дляhandlers/message), илиinputконечной точки, и это должен быть объект — не массив и не просто строка.--ctx <json5>— объект контекста модуля (его второй аргумент), также в JSON5. Используйте его для предоставления того, что ваш код считывает изctx— необработанного обновления для обработчика,--ctx '{ update: { update_id: 1 } }', или данных инициализации для конечной точки,--ctx '{ initData: { user: { id: 1 } } }'.runне проверяет это, поскольку вы тестируете свою логику, а не проверки платформы.
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
Платформа запускает модуль против пространства модулей, собранного из вашего локального проекта (поэтому локально измененный код lib/ также используется) и возвращает возвращаемое значение, все, что было выведено с помощью console.*, и затраченное время. Читайте большие аргументы из файла с помощью npx tgcloud run handlers/message "$(cat message.json5)".
fetch
npx tgcloud fetch
Проверяет, что в настоящее время развернуто, не затрагивая ваши рабочие файлы. После этого status и diff сравнивают с последним состоянием облака. Полезно просмотреть конфликт перед решением, как его разрешить.
pull
npx tgcloud pull
Приводит ваш локальный проект в соответствие с облаком, обновляя ваши рабочие файлы до состояния развертывания.
pull никогда не записывает папку сборки вашего мини-приложения.
reset
npx tgcloud reset
Отбрасывает ваши локальные изменения и восстанавливает модули из последнего известного состояния облака. Используйте это, чтобы избавиться от эксперимента. Папка сборки мини-приложения не затрагивается.
webhook
npx tgcloud webhook
npx tgcloud webhook sync [--drop-pending]
Telegram доставляет обновления вашему боту через веб-хук, которым платформа управляет за вас — вы никогда не указываете его вручную. npx tgcloud webhook отображает его текущее состояние: URL, список allowed_updates, количество ожидающих обновлений, последнюю ошибку доставки (если есть) и синхронизирован ли он с вашими развернутыми обработчиками.
«В синхронизации» означает, что веб-хук указывает на платформу, а его allowed_updates соответствует обработчикам, которые вы развернули — поэтому Telegram доставляет именно те типы обновлений, которые вы обрабатываете, и ничего больше. Развертывание нового обработчика (или его удаление) может оставить веб-хук вне синхронизации, пока он не будет обновлен; npx tgcloud status также помечает это.
npx tgcloud webhook sync исправляет это — он повторно указывает веб-хук на платформу и перестраивает allowed_updates из ваших развернутых обработчиков. Добавьте --drop-pending, чтобы отбросить обновления, которые Telegram уже поставил в очередь до синхронизации (в противном случае они будут доставлены после того, как веб-хук снова станет исправен).
completion
Примечание: автозавершение по нажатию Tab работает только тогда, когда в вашей
PATHприсутствует простоtgcloud— поэтому установите его глобально (npm install -g @tgcloud/cli) или поместите исполняемый файл вPATHиным образом. Оно не может подключаться кnpx.
tgcloud completion <bash|zsh|fish>
Выводит скрипт автозавершения в stdout. Включите его один раз, затем <Tab> будет автозавершать команды, флаги, каталоги модулей, типы обновлений обработчиков, которых у вас еще нет, и ваши локальные исполняемые модули.
# bash — требует пакет bash-completion:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc
# zsh — убедитесь, что `autoload -U compinit && compinit` выполняется в вашем ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc
# fish:
tgcloud completion fish > ~/.config/fish/completions/tgcloud.fish
Перезапустите оболочку (или повторно загрузите файл) после этого. Запуск tgcloud completion без указания оболочки повторно выводит эти инструкции.
upgrade
npx tgcloud upgrade [--dry-run]
Одноразовая миграция для проектов, созданных до CLI 0.2.0, которые хранили schema.js, lib/ и handlers/ в корне проекта. Она перемещает их в tgcloud/. Ваша связь с ботом сохраняется, и ничего не нужно переразвертывать. --dry-run печатает запланированные перемещения, не затрагивая ничего. Другие команды не запускаются для старого макета, пока вы не обновитесь.
Поддержание синхронизации
Каждый проект имеет монотонно увеличивающийся номер ревизии в облаке, который увеличивается при каждом развертывании. CLI помнит номер ревизии, с которым он последний раз синхронизировался, и отправляет его при каждом push. Если облако продвинулось вперед — потому что другая машина или член команды развернулись — push отклоняется, а не молча перезаписывает их работу, и CLI предлагает три пути вперед:
npx tgcloud fetch # посмотреть, что изменилось в облаке, затем перепроверить
npx tgcloud pull # внести последние изменения в ваши рабочие файлы
npx tgcloud push --force # перезаписать состояние облака (опасно)
Эта проверка оптимистичной конкуренции — причина, по которой вы можете совместно использовать бота между командой без процесса развертывания с блокировкой. Команды завершаются с ненулевым кодом при сбое — отклоненное развертывание, неудачная миграция, ошибка аутентификации, модуль, который выбросил исключение во время run — поэтому они чисто компонуются в скриптах и конвейерах CI.