Telegram Serverless

Telegram Serverless lets you run backend code for your bot and Mini App directly on Telegram's infrastructure — no servers to provision, no containers to keep alive, no scaling to think about. You write plain JavaScript modules, deploy them with a single command, and Telegram runs them in a fast, isolated V8 sandbox that sits right next to the Bot API and a built‑in database.Telegram Serverless 让你直接把后端代码跑在 Telegram 的基础设施上。不需要自己准备服务器,不用管容器,也不用考虑扩容。你写普通的 JavaScript 模块,一条命令部署上去。Telegram 把它们放在一个快速的、隔离的 V8 沙箱里运行,这个沙箱就在 Bot API 和内置数据库旁边。

If you have ever wired a bot to a VPS, a cloud function, or a hosting panel just to answer a /start, this is the part you no longer have to do.以前为了响应一个 /start,要把机器人连到 VPS、云函数或者托管面板。现在不用了。

On this page 本页内容

Why serverless

A Telegram bot is, at heart, a program that reacts to updates. Traditionally you had to host that program somewhere that is always on, reachable, and secure — and then keep it that way. Telegram Serverless removes that layer entirely:Telegram 机器人本质上是一个响应更新的程序。以前你得找一个一直在线、能访问、安全的地方来托管它——还得一直维护。Telegram Serverless 把这层去掉了:

  • No infrastructure. There is no machine to rent, patch, or monitor. Your code runs on demand and scales with your bot automatically.没有基础设施。不用租机器,不用打补丁,不用监控。你的代码按需运行,机器人自动扩容。
  • Batteries included. The Telegram Bot API, an SQLite‑backed database, and outbound HTTP are available to every module out of the box — nothing to install, no credentials to wire up.开箱即用。Telegram Bot API、一个 SQLite 数据库、外部 HTTP 请求,每个模块拿来就用——不用安装,不用配置凭证。
  • Fast, isolated execution. Each invocation runs in a lightweight V8 isolate, close to Telegram's own systems, so calls to the Bot API and your database are quick and reliable.快速、隔离的执行。每次调用在一个轻量级的 V8 隔离环境里运行,离 Telegram 自己的系统很近,所以调用 Bot API 和数据库又快又可靠。
  • A real developer workflow. A project lives in a folder on your machine under version control. You edit files, see exactly what changed, deploy atomically, and roll your database schema forward with reviewed migrations — the way you already work with everything else.真正的开发者工作流。项目存在你本机的文件夹里,用版本控制。你编辑文件,看到改了什么,原子化部署,通过审核的迁移来更新数据库 schema——和你平时做其他事一样。

The mental model心智模型

You work in three places, and they map cleanly onto each other:你在三个地方工作,它们彼此清晰对应:

Where What lives there
Your project folder JavaScript modules — schema, shared code, update handlers
The cloud The deployed copy of those modules, plus your bot's database
The tgcloud CLI The bridge — it shows you differences and syncs them

You never SSH into anything. You edit files locally, run npx tgcloud push, and the platform takes it from there. Your bot's traffic is handled by the deployed modules; your database persists between invocations.你永远不会 SSH 到任何东西。你在本地编辑文件,运行 npx tgcloud push,平台就接手了。机器人的流量由已部署的模块处理;数据库在调用之间持久保存。

A project has just three kinds of code:一个项目只有三类代码:

handlers/      # entry points — one file per Telegram update type
lib/           # shared code you import from anywhere
schema.js      # your database tables

When an update arrives — a message, a button press, an inline query — Telegram routes it to the matching handler (handlers/message.js, handlers/callback_query.js, …) and calls its default export. That function talks to the Bot API and the database through the SDK, and returns. That is the whole loop. An update with no matching handler is simply ignored, so you add only the handlers you need.收到更新时——一条消息、一个按钮点击、一个内联查询——Telegram 把它路由到对应的处理程序(handlers/message.js、handlers/callback_query.js 等),并调用它的默认导出。那个函数通过 SDK 和 Bot API 及数据库交互,然后返回。整个循环就是这样。没有对应处理程序的更新会被忽略,所以你只需要添加需要的处理程序。

Quick demo快速演示

Here is a complete, working demo bot. It replies to every message and remembers how many it has seen from each chat.这是一个完整的、能运行的演示机器人。它会回复每条消息,并记住每个对话收到了多少条。

schema.jsschema.js

import { table, integer } from 'sdk/db';

export const counters = table('counters', {
  chatId: integer('chat_id').primaryKey(),
  seen:   integer('seen').notNull().default(0),
});

handlers/message.jshandlers/message.js

import { api, db } from 'sdk';
import { counters } from 'schema';
import { sql } from 'sdk/db';

export default async function (message) {
  const chatId = message.chat.id;

  // Insert the counter, or bump it if this chat already has one — and get the
  // resulting row back in the same statement via .returning().
  const [row] = await db.insert(counters)
    .values({ chatId, seen: 1 })
    .onConflictDoUpdate({
      target: counters.chatId,
      set: { seen: sql`${counters.seen} + 1` },
    })
    .returning()
    .run();

  await api.sendMessage({
    chat_id: chatId,
    text: `Hello! I've seen ${row.seen} message(s) from you.`,
  });
}

Deploy it:部署它:

npx tgcloud push       # upload the modules
npx tgcloud migrate    # create the `counters` table

That's a live bot with persistent state and no server. Everything in it — api, db, the table() DSL — is described in the sections below.这就是一个在线的机器人,有持久化状态,没有服务器。里面的所有东西——api、db、table() DSL——都在下面几节里说明。

Serverless is a general backend for Telegram bots and Mini Apps, not a template for one kind of app. It is ideal for:Serverless 是 Telegram 机器人和 Mini App 的通用后端,不是某个特定应用的模板。它适合以下场景:

  • Conversational AI Bots that need to store per‑user state in a database.对话式 AI 机器人——需要在数据库里存储每个用户的状态。
  • Mini App Backends that store user data and serve dynamic content.Mini App 后端——存储用户数据并提供动态内容。
  • Games and Tools — including leaderboards, quizzes and more.游戏和工具——包括排行榜、测验等。
  • Automations and Integrations that call third‑party HTTP APIs and push results into chats.自动化和集成——调用第三方 HTTP API 并将结果推送到的对话中。

Getting started

This walkthrough takes you from an empty folder to a live bot that answers messages and stores data. It assumes you have Node.js 18 or newer installed and a bot registered with @BotFather. By the end you will have used every command you need day to day: push, migrate, run, and status.这个教程带你从一个空文件夹开始,直到得到一个能回复消息、存储数据的在线机器人。假设你已经安装了 Node.js 18 或更新版本,并且已经在 @BotFather 注册了一个机器人。看完之后,你会用到每天都会用的所有命令:push、migrate、run 和 status。

Before anything else, switch Serverless on. In @BotFather, open your bot → Serverless and turn it on. That turns the feature on for this bot and unlocks its CLI access token, handlers, library, and database.首先,打开 Serverless。在 @BotFather 里,打开你的机器人 → Serverless 并开启。这样就为这个机器人开启了该功能,同时解锁了它的 CLI 访问令牌、处理程序、库和数据库。

1. Create a project1. 创建项目

The fastest way to start is the project creator, which scaffolds a project and installs the CLI into it:最快的方法是用项目创建工具,它会自动生成一个项目结构并安装 CLI:

npm create @tgcloud/bot example_bot
cd example_bot

The argument is the target folder: pass . to scaffold into the current folder, or any path. It works in an existing folder too and never overwrites files you already have.参数是目标文件夹:传入 . 则在当前文件夹中生成,或者传入任意路径。它也能在已有文件夹中工作,并且永远不会覆盖你已经有的文件。

This gives you a ready‑to‑edit project:你会得到一个随时可以编辑的项目:

example_bot/
├─ docs/
│  └─ tgcloud-sdk.md    # SDK reference (for you and your AI tools)
├─ handlers/
│  └─ message.js        # a starter message handler (echoes text back)
├─ lib/                 # your shared modules go here (empty to start)
├─ AGENTS.md            # orientation for AI coding assistants
├─ package.json
└─ schema.js            # your database tables

The scaffolded files are self‑documenting — each one contains commented examples of what you can do next.生成的文件是自文档的——每个文件都包含了注释示例,告诉你接下来可以做什么。

The CLI installs into the project as a local dev‑dependency, so you run it with npx tgcloud <command> (npx finds the copy in your project's node_modules), or through the npm run shortcuts the scaffold adds to package.json (npm run deploy, npm run status). By default there is no global tgcloud on your PATH.CLI 以本地开发依赖的方式安装到项目中,所以你可以用 npx tgcloud <command> 来运行它(npx 会找到项目 node_modules 中的副本),或者通过 npm run 快捷方式(scaffold 添加到 package.json 的 npm run deploy、npm run status)。默认情况下,你的 PATH 上没有全局的 tgcloud。

You can also install it globally — npm install -g @tgcloud/cli — if you'd rather type a bare tgcloud from anywhere. That's handy for running tgcloud init in any empty folder, and it's what shell tab‑completion needs. Either way you get the same project.你也可以全局安装——npm install -g @tgcloud/cli——如果想在任何地方直接输入 tgcloud。这样方便你在任意空文件夹里运行 tgcloud init,而且 shell 的 tab 补全也需要它。两种方式得到的项目是一样的。

2. Link your bot2. 关联你的机器人

Every project is tied to one bot. Connect them with login, which asks for your CLI access token (@BotFather → your bot → Serverless → CLI Access → Access token — a separate token from your bot's API token) and stores it locally:每个项目对应一个机器人。用 login 命令把它们连起来,它会要求你输入 CLI 访问令牌(@BotFather → 你的机器人 → Serverless → CLI Access → Access token——这是另一个令牌,和机器人的 API 令牌不同)并保存在本地:

npx tgcloud login

The token has the form app<id>:<secret>. The CLI keeps it in .tgcloud/, which is git‑ignored, and never prints the secret part. Login is the only time you are asked for it — see Authentication for how tokens are resolved in CI.令牌的格式是 app<id>:<secret>。CLI 把它保存在 .tgcloud/ 中,这个目录会被 git 忽略,而且永远不会打印 secret 部分。你只需要在登录时输入一次——关于 CI 环境下如何解析令牌,请看认证部分。

3. Look around3. 查看状态

Two commands tell you where things stand at any moment, both fully offline:两个命令可以随时告诉你当前的状态,都是完全离线的:

npx tgcloud status     # what has changed locally vs. the deployed copy
npx tgcloud diff       # the line‑by‑line changes

Right after init everything is new and nothing is deployed yet. status shows the starter files waiting to go up.init 之后,所有东西都是新的,还没有部署。status 会显示等待上传的文件。

4. Deploy4. 部署

Send your modules to the cloud:将你的模块发送到云端:

npx tgcloud push

push uploads every changed module in one atomic batch and updates your local record of what the cloud now holds. Your bot is live: open it in Telegram and send it a message — the starter handler echoes it back.push 会以原子批次上传所有变更的模块,并更新本地记录。你的机器人就上线了:在 Telegram 里打开它,发送一条消息——入门处理程序会原样回复。

Deploying never touches your database. Pushing code and changing your database schema are deliberately separate steps, so a code deploy can never surprise you with a data migration. That is what the next step is for.部署永远不会改动数据库。推送代码和更改数据库 schema 是特意分开的步骤,所以代码部署永远不会意外触发数据迁移。接下来这一步就是干这个的。

5. Add a database table5. 添加数据库表

Let's make the bot remember something. Open schema.js and declare a table:让机器人记住一些东西。打开 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())`),
});

Deploy the schema, then apply it to the database:部署 schema,然后应用到数据库:

npx tgcloud push       # uploads the new schema.js
npx tgcloud migrate    # creates the `messages` table

push reports that the schema is out of sync and shows you the pending change, but applies nothing. migrate walks you through the change and, on your confirmation, creates the table. This two‑step model — and what happens with riskier changes like drops — is covered in Migrations.push 会报告 schema 不同步,并显示待变更的内容,但不会应用任何东西。migrate 会引导你完成变更,在你确认后创建表。这个两步模型——以及删除等风险较高的变更如何处理——在迁移部分有详细介绍。

6. Store and read data6. 存储和读取数据

Now use the table from your handler. Edit handlers/message.js:现在在处理程序中使用这个表。编辑 handlers/message.js:

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

export default async function (message) {
  // Save this message.
  await db.insert(messages)
    .values({ chatId: message.chat.id, text: message.text })
    .run();

  // Count how many we've stored for this chat.
  const count = await db.$count(messages, eq(messages.chatId, message.chat.id));

  await api.sendMessage({
    chat_id: message.chat.id,
    text: `Saved. That's ${count} message(s) from this chat so far.`,
  });
}

Deploy the updated handler with npx tgcloud push, then send your bot a few messages and watch the count climb. The database persists between invocations — that's your bot's memory.用 npx tgcloud push 部署更新后的处理程序,然后给机器人发几条消息,看看计数增加。数据库在调用之间持续存在——这就是机器人的记忆。

7. Test without deploying7. 不用部署就能测试

You don't have to deploy to try a change. npx tgcloud run executes a handler on the platform using your local files, without publishing them:不用部署也能尝试变更。npx tgcloud run 会在平台上用你的本地文件执行一个处理程序,而不发布它们:

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

The argument is the payload your handler receives — for handlers/message, a Message — written in JSON5 (so you can skip quoting keys). The command prints anything the handler logged with console.*, the return value, and how long it took. This is the tightest loop for iterating on logic — no deploy, no waiting for a real message.参数是处理程序接收的载荷——对于 handlers/message,是一个 Message——以 JSON5 格式编写(所以可以省略键的引号)。命令会打印处理程序用 console.* 记录的所有内容、返回值以及耗时。这是迭代逻辑的最快循环——不用部署,不用等待真实消息。

8. Keep in sync8. 保持同步

As you work, a handful of commands keep your local project and the cloud aligned: npx tgcloud status shows what changed, npx tgcloud push deploys, npx tgcloud pull brings your local project in line with the cloud, npx tgcloud fetch refreshes the reference copy without touching your files, and npx tgcloud reset discards local changes. 在开发过程中,几个命令可以让你的本地项目与云端保持一致:npx tgcloud status 显示变更内容,npx tgcloud push 部署,npx tgcloud pull 将本地项目与云端对齐,npx tgcloud fetch 刷新参考副本而不触碰你的文件,npx tgcloud reset 放弃本地变更。

If two people (or two machines) deploy to the same bot, the platform detects the conflict and push stops to let you pull first — you can never silently overwrite someone else's work. See Staying in sync.如果两个人(或两台机器)部署到同一个机器人,平台会检测到冲突,push 会停下来,让你先 pull——永远不会悄无声息地覆盖别人的工作。参见保持同步。

Building with AI

Prefer to build with an AI assistant — or is the only coder on your team an AI? You can still ship a bot. We've taken a first step to make an AI agent feel at home in a project: every new one is scaffolded with an AGENTS.md and a docs/tgcloud-sdk.md reference that agentic coding tools read automatically. 倾向于用 AI 助手构建——或者团队里唯一的程序员就是 AI?你照样可以发布机器人。我们已经迈出了第一步,让 AI 代理在项目中感到自在:每个新项目都会生成一个 AGENTS.md 和 docs/tgcloud-sdk.md 参考文件,这些文件会被智能编码工具自动读取。

Together with a small, self‑contained runtime — one SDK, no npm packages to wrangle — that gives the assistant a running start on the conventions generic codegen tends to miss here: import by bare name, no foreign keys, every db call is async, one handler per update type, and the two‑step push/migrate flow.配合一个小而自包含的运行时——一个 SDK,不用处理 npm 包——助手就能快速了解这里通用代码生成容易遗漏的约定:按裸名称导入,没有外键,每个数据库调用都是异步的,每种更新类型一个处理程序,以及两步的 push/migrate 流程。

Try it:试试看:

npm create @tgcloud/bot my-bot
cd my-bot
opencode            # or Claude Code, Cursor, … — any agent that reads AGENTS.md

Then just ask, in plain language:然后直接问,用白话:

Write a bot that remembers each person's to‑do list — add an item when they send text, and show the whole list when they send /list.写一个机器人,记住每个人的待办事项列表——发文字就添加一项,发 /list 就显示整个列表。

The assistant edits schema.js and your handlers for you; you review, test a change instantly with npx tgcloud run, then go live with npx tgcloud push and npx tgcloud migrate. AGENTS.md is part of your project — edit it as the bot grows so the guidance stays accurate.助手会为你编辑 schema.js 和处理程序;你审查一下,用 npx tgcloud run 快速测试变更,然后通过 npx tgcloud push 和 npx tgcloud migrate 上线。AGENTS.md 是你项目的一部分——随着机器人的成长修改它,让指导保持准确。

On the go with BotFather

Down to just your phone? The whole project lives in @BotFather too — open your bot → Serverless and you get everything the CLI manages, on a touchscreen:手边只有手机?整个项目也都在 @BotFather 里——打开你的机器人 → Serverless,就能看到 CLI 管理的一切,触摸屏操作:

  • Handlers — create, edit, and test‑run update handlers; BotFather keeps the webhook in sync with the handlers you have (the same In sync / Out of sync the CLI reports).处理程序——创建、编辑和测试运行更新处理程序;BotFather 会保持 webhook 与你拥有的处理程序同步(与 CLI 报告的“同步/不同步”相同)。
  • Library — your shared lib/ modules.库——你共享的 lib/ 模块。
  • Database — edit schema.js in the same Drizzle‑like syntax, review pending changes, and apply them; Save deploys.数据库——用同样的 Drizzle 风格语法编辑 schema.js,查看待变更的内容并应用;保存即部署。
  • CLI Access — grab the CLI access token here when you're back at a keyboard.CLI 访问——回到键盘时,在这里获取 CLI 访问令牌。

It's one and the same cloud project, so you can start a handler on your phone and npx tgcloud pull it to your laptop later — nothing is tied to a single client. Running a handler even shows its Console output right in the chat, just like npx tgcloud run.这是同一个云端项目,所以你可以先在手机上写一个处理程序,之后再到笔记本上用 npx tgcloud pull 拉下来——不与任何单一客户端绑定。运行处理程序时,甚至会在对话中直接显示控制台输出,就像 npx tgcloud run 一样。

Projects and modules项目和模块

A serverless project is an ordinary folder under version control. It holds nothing but JavaScript modules and a little local state — there is no build step, no node_modules at runtime, and no server entry point.Serverless 项目就是一个普通的文件夹,用版本控制管理。里面只有 JavaScript 模块和一些本地状态——没有构建步骤,运行时没有 node_modules,也没有服务器入口。

Anatomy of a project项目结构

example_bot/
├─ handlers/            # update handlers — flat, one level only
│  ├─ message.js
│  └─ callback_query.js
├─ lib/                 # shared modules; subdirectories allowed
│  ├─ reply.js
│  └─ internal/util.js
├─ schema.js            # database schema — one file, at the root
└─ .tgcloud/            # CLI state — credentials, snapshot, cache (git‑ignored)

Only schema.js and .js files under lib/ and handlers/ are deployed. Everything else — Markdown, config files, the .tgcloud/ folder — stays on your machine.只有 schema.js 以及 lib/ 和 handlers/ 下的 .js 文件会被部署。其他所有东西——Markdown、配置文件、.tgcloud/ 文件夹——都留在你的机器上。

schema.js — your database. It declares tables as named exports using the schema DSL and lives at the project root as a single file. It is deployed like any other module, but deploying it never changes the database — schema changes are applied separately with npx tgcloud migrate. See The database.schema.js——你的数据库。它用 schema DSL 以命名导出的方式声明表,作为一个文件放在项目根目录。它和其他模块一样被部署,但部署它不会改变数据库——Schema 变更需要另外用 npx tgcloud migrate 来应用。参见数据库部分。

lib/ — shared code, anything you want to reuse across handlers: pure helpers, database access layers, formatting, integrations with outside services. lib/ is the only directory that may contain subdirectories (lib/internal/util.js, lib/payments/stripe.js), so you can organize a larger codebase however you like. Modules in lib/ are never invoked directly by the platform; they exist to be imported by handlers and by each other.lib/——共享代码,你想在处理程序之间复用的任何东西:纯工具函数、数据库访问层、格式化、外部服务集成。lib/ 是唯一可以包含子目录的目录(lib/internal/util.js、lib/payments/stripe.js),所以你可以按需组织较大的代码库。lib/ 中的模块不会直接被平台调用;它们被处理程序和其他 lib/ 模块导入。

handlers/ — the entry points of your bot. Each file corresponds to one Telegram update type, and the platform routes each incoming update to the matching handler:handlers/——机器人的入口。每个文件对应一种 Telegram 更新类型,平台将每个进入的更新路由到匹配的处理程序:

File Handles
handlers/message.js New incoming messages
handlers/inline_query.js New incoming inline queries
handlers/callback_query.js New incoming callback queries
any other Bot API update type

handlers/ is flat — no subdirectories. A handler's export default is the function the platform calls (see Handlers).handlers/ 是扁平的——没有子目录。处理程序的 export default 是平台调用的函数(参见处理程序)。

An update type is handled only if its handler file exists and is non‑empty. If there is no handlers/<type>.js — or the file is empty — updates of that type are ignored, and the platform runs nothing for them. So keep only the handlers you actually need: each one you add is another update type your bot wakes up to process, and leaving out the rest means Telegram doesn't spin up your code for updates you'd only discard anyway. 只有处理程序文件存在且非空时,才会处理该更新类型。如果没有 handlers/<type>.js——或者文件为空——这类更新会被忽略,平台不会为其运行任何代码。所以只保留你真正需要的处理程序:每增加一个,你的机器人就会在处理该更新类型时被唤醒;去掉不需要的,Telegram 就不会为那些你只会丢弃的更新而启动你的代码。

To scaffold a new handler, run npx tgcloud add handlers/<type>.要生成一个新的处理程序,运行 npx tgcloud add handlers/<type>。

.tgcloud/ — machine‑local state managed entirely by the CLI: your saved credentials, a mirror of the deployed code used for offline diffs, and a small cache. It is git‑ignored, and you should never read from or write to it by hand — use the CLI commands instead..tgcloud/——由 CLI 管理的本地状态:你保存的凭证、用于离线 diff 的已部署代码镜像、以及一个小缓存。它被 git 忽略,你不应该手动读写它——改用 CLI 命令。

The module system模块系统

At runtime, a module can see exactly two things: the platform SDK and the other modules in your project. There are no npm packages, no filesystem, and no network except through the SDK's fetch.运行时,一个模块只能看到两样东西:平台 SDK 和你项目中的其他模块。没有 npm 包,没有文件系统,除了通过 SDK 的 fetch 之外没有网络。

Modules are addressed by their name — the path from the project root, without the .js extension — not by their location on disk. Always import by that bare name:模块通过它的名称来引用——从项目根目录开始的路径,不带 .js 扩展名——而不是它在磁盘上的位置。始终用这个裸名称导入:

import { users } from 'schema';            //  the schema module
import { addItem } from 'lib/cart';        //  a lib module
import { format } from 'lib/internal/fmt'; //  nested lib module
import { db, api, fetch } from 'sdk';      //  the platform SDK

Relative paths and file extensions do not work — the platform resolves names in its module space, not files in a directory:相对路径和文件扩展名不起作用——平台在模块空间中解析名称,而不是目录中的文件:

import { users } from './schema';    //  won't compile
import { users } from '../schema';   //  won't compile
import x from 'lib/cart.js';         //  drop the .js

What's available at runtime is exactly two things: sdk and its submodules (sdk/db, sdk/api, sdk/fetch) — the whole platform surface, see The SDK — and your own modules under schema, lib/, handlers/. That's the complete list. If your code imports anything else, it won't resolve. This constraint is what keeps modules fast to load and safe to run.运行时能用的东西只有两样:sdk 及其子模块(sdk/db、sdk/api、sdk/fetch)——整个平台接口,参见 SDK——以及你自己的模块(schema、lib/、handlers/)。就这些。如果你的代码导入其他东西,它不会解析。这个约束让模块加载快、运行安全。

Handlers处理程序

A handler is a module in handlers/ whose default export the platform invokes when a matching update arrives.处理程序是 handlers/ 中的一个模块,当匹配的更新到达时,平台调用它的默认导出。

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

export default async function (message) {
  await api.sendMessage({
    chat_id: message.chat.id,
    text: `You said: ${message.text ?? '(no text)'}`,
  });
}

A handler receives the update's payload as its argument — the platform unwraps the Telegram Update for you. handlers/message.js gets the Message (i.e. update.message); handlers/callback_query.js gets the CallbackQuery; and so on. The handler's second argument is a per‑invocation context object, ctx. It carries the raw Update as ctx.update — reach for it when you need something outside the payload, like update_id.处理程序接收更新的载荷作为参数——平台为你解开了 Telegram Update。handlers/message.js 得到 Message(即 update.message);handlers/callback_query.js 得到 CallbackQuery;以此类推。处理程序的第二个参数是每次调用的上下文对象 ctx。它包含原始的 Update 作为 ctx.update——当你需要 update_id 之类的东西时就用它。

A handler can be async (usually is) and may return a value. It reaches the Bot API, the database, and outbound HTTP through the SDK.处理程序可以是异步的(通常如此),也可以返回一个值。它通过 SDK 访问 Bot API、数据库和外部 HTTP。

You don't need a real update to test a handler. npx tgcloud run executes it on the platform with the payload you supply and your current local code:不需要真实的更新就能测试处理程序。npx tgcloud run 在平台上用你提供的载荷和你当前的本地代码执行它:

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

The argument is the payload — the same object your handler receives — in JSON5. To supply the handler's ctx (its second argument), add --ctx, e.g. --ctx '{ update: { update_id: 1 } }'. This runs against your local files, so you can try changes before deploying them. See run.参数是载荷——处理程序接收的同一个对象——用 JSON5 格式。要提供处理程序的 ctx(第二个参数),加上 --ctx,例如 --ctx '{ update: { update_id: 1 } }'。这会在你的本地文件上运行,所以可以先尝试变更再部署。参见 run 命令。

What gets deployed哪些会被部署

When you npx tgcloud push, the CLI gathers every .js file under schema.js, lib/, and handlers/, and sends that exact set as your project's module space. Anything present in the cloud but absent from your project is removed, so the deployed state always mirrors your folder — deletions included. Files outside those locations are ignored. A stray .js at the project root (not a config file) is flagged so it doesn't silently go unnoticed, because the project root is meant to hold only serverless content. Markdown, dotfiles, and .tgcloud/ are never deployed.当你运行 npx tgcloud push 时,CLI 会收集 schema.js、lib/ 和 handlers/ 下的所有 .js 文件,然后将其作为项目的模块空间发送出去。如果云端有而项目中不存在的文件会被移除,所以部署状态始终与你的文件夹镜像一致——包括删除。这些位置之外的文件会被忽略。项目根目录中的无关 .js 文件(不是配置文件)会被标记出来,以免被默默忽视,因为项目根目录只应该放 serverless 内容。Markdown、点文件和 .tgcloud/ 永远不会被部署。

The database

Every bot gets its own database — an SQLite‑backed store that persists between invocations and is available to every module through db. You describe your tables in schema.js with a small, typed DSL; you read and write them with a fluent query builder; and you evolve them with reviewed migrations.每个机器人都有自己的数据库——一个基于 SQLite 的存储,在调用之间持久保存,所有模块都可以通过 db 访问。你在 schema.js 中用一个小型的类型化 DSL 描述表;用一个流畅的查询构建器读写它们;通过经过审核的迁移来演进。

Know Drizzle ORM? Then you already know how to talk to the database here. The schema DSL and query builder follow Drizzle — the column builders, select().from().where(), the operators, onConflictDoUpdate, .returning(), and the sql tag all behave the way you'd expect, so reading and writing data is the familiar API you already use. You just import it from sdk/db, and a couple of platform specifics (most notably no foreign keys) are pointed out where they come up.了解 Drizzle ORM?那你已经知道如何操作这里的数据库了。Schema DSL 和查询构建器遵循 Drizzle——列构建器、select().from().where()、运算符、onConflictDoUpdate、.returning() 和 sql 标签的行为都和你期望的一样,所以读写数据就是你熟悉的 API。你只需从 sdk/db 导入,一些平台特有的东西(最明显的是没有外键)会在出现时说明。

Declaring tables声明表

Tables are named exports in schema.js. Calling table() builds a description at load time (it does not touch the database); the platform discovers the exported tables when you deploy schema.js and migrates the database to match.表是 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?): table(name, columns, extras?):

  • name is the SQL table name; name 是 SQL 表名;
  • columns is a map of JS property → column definition; columns 是一个 JS 属性名到列定义的映射;
  • extras is an optional callback (t) => ({ … }) where t exposes the columns (t.created is a reference to that column) — declare indexes and table‑level constraints here.extras 是一个可选的回调 (t) => ({ … }),t 暴露了列(t.created 是对该列的引用)——在这里声明索引和表级约束。
Column types列类型
Factory SQLite type Notes
text() TEXT
integer() INTEGER
real() REAL alias float()
numeric() NUMERIC
blob() BLOB reads/writes Uint8Array
boolean() INTEGER stored as 0/1, read as true/false
json() TEXT auto JSON.stringify / JSON.parse

The column name argument is optional — omit it and the JS key is used. The mode option controls how values convert between SQLite and JavaScript.列名参数是可选的——省略时使用 JS 键。mode 选项控制值如何在 SQLite 和 JavaScript 之间转换。

mode Stored as JS value
boolean INTEGER 0/1 boolean
json TEXT (JSON) any object/array
timestamp INTEGER (unix seconds) Date
timestamp_ms INTEGER (unix ms) Date
bytes BLOB Uint8Array

boolean() and json() are shorthands for integer(name, { mode: 'boolean' }) and text(name, { mode: 'json' }).boolean() 和 json() 分别是 integer(name, { mode: 'boolean' }) 和 text(name, { mode: 'json' }) 的简写。

A blob() reads and writes a Uint8Array — the runtime has no Node Buffer (and a Buffer is a Uint8Array subclass, so this is the portable base type). The mode above governs only how a value is encoded, independent of the column's storage type: so blob('col', { mode: 'json' }) is the BLOB counterpart of json() — the same JSON encoding, kept in a BLOB column rather than TEXT.blob() 读写 Uint8Array——运行时没有 Node Buffer(但 Buffer 是 Uint8Array 的子类,所以这是可移植的基础类型)。上面的 mode 只控制值的编码方式,与列的存储类型无关:所以 blob('col', { mode: 'json' }) 是 json() 的 BLOB 对应物——同样的 JSON 编码,只是存储在 BLOB 列而不是 TEXT 中。

TLDR: blob() uses Uint8Array. mode controls encoding, not storage, so blob(..., { mode: 'json' }) stores JSON as a BLOB, while json() stores it as TEXT.简而言之:blob() 使用 Uint8Array。mode 控制编码,而不是存储,所以 blob(..., { mode: 'json' }) 将 JSON 存储为 BLOB,而 json() 将其存储为 TEXT。

Column modifiers列修饰符

Column modifiers chain onto a column.列修饰符可以链式调用。

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' })  // or 'virtual'
text('email').deprecated('replaced by login')   // marks the column for removal

.default() on a json() column encodes the value for you; a sql`…` default is passed through verbatim. .deprecated() is terminal — see Migrations..default() 在 json() 列上会为你编码值;sql`…` 默认值会原样传递。.deprecated() 是终端方法——参见迁移部分。

Indexes and constraints索引和约束

Indexes and constraints are declared in the extras callback, where the columns are in scope.索引和约束在 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})`),          // expression index
  active: index('idx_active').on(t.userId).where(sql`done = 0`),  // partial index
}));

Table‑level modifiers chain after table(...): .strict(), .withoutRowid(), .deprecated('reason').表级修饰符可以链式调用在 table(...) 后面:.strict()、.withoutRowid()、.deprecated('reason')。

No foreign keys没有外键

The runtime runs with PRAGMA foreign_keys off. A declared foreign key would be silently inert — no cascades, no orphan protection — which is worse than having none at all, so the DSL makes it impossible. Namely, .references() and table‑level foreignKey() throw when declared, and a schema that uses them will not deploy.运行时以 PRAGMA foreign_keys off 运行。声明的外键会静默无效——没有级联,没有孤儿保护——这比没有更糟,所以 DSL 让它不可能发生。具体来说,.references() 和表级 foreignKey() 在声明时会抛出错误,使用它们的 schema 无法部署。

You should model relationships with plain columns (userId: integer('user_id')) and enforce integrity in your application code: insert parents before children, delete children before parents, handle errors properly and sweep orphans with a LEFT JOIN … WHERE parent.id IS NULL when you need to.你应该用普通列(userId: integer('user_id'))来建模关系,并在应用代码中强制完整性:先插入父记录再插入子记录,先删除子记录再删除父记录,正确处理错误,在需要时用 LEFT JOIN … WHERE parent.id IS NULL 清理孤儿。

The absence of foreign keys is a deliberate constraint, not an oversight. Account for it early on when planning your bot.缺少外键是一个刻意的约束,不是疏忽。在规划机器人时尽早考虑这一点。

Querying查询

db is a fluent query builder. Every query is asynchronous — the terminal methods (.all(), .get(), .values(), .run()) return Promises, so always await.db 是一个流畅的查询构建器。每个查询都是异步的——终端方法(.all()、.get()、.values()、.run())返回 Promise,所以始终 await。

import { db } from 'sdk';
import { users, todos } from 'schema';
import { eq, and, desc, asc, count, sql } from 'sdk/db';

await db.select().from(todos).all();                          // all rows
await db.select().from(todos).where(eq(todos.id, 1)).get();   // first row or null
await db.select().from(todos).values();                       // rows as value arrays

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

// custom projection: { 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();

// row count — a helper, not a builder terminal:
await db.$count(todos);                        // all rows
await db.$count(todos, eq(todos.done, false)); // with a filter
  • Chainable: .where(), .orderBy(), .limit(), .offset(), .groupBy(), .having(), .distinct(). 可链式调用:.where()、.orderBy()、.limit()、.offset()、.groupBy()、.having()、.distinct()。
  • Terminals: .all(), .get(), .values(). 终端方法:.all()、.get()、.values()。
  • Count rows with db.$count(table, where?) (or count() in a projection).用 db.$count(table, where?)(或在投影中使用 count())来统计行数。

Insert, update, delete:插入、更新、删除:

await db.insert(todos).values({ userId: 1, text: 'Buy milk' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run();   // batch
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();

Note that a batch insert is one statement, so it's capped by SQLite's variable limit (rows × columns); if you go over it, the insert errors out — chunk it yourself.注意,批量插入是一条语句,所以受限于 SQLite 的变量限制(行数 × 列数);如果超出,插入会报错——需要自己分块。

Operators are imported from sdk/db:运算符从 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) with multiple arguments is the same as and(a, b). A comparison's second argument is a value by default, but may be another column or sql`…` — e.g. eq(a.x, b.y). Aggregates (count/sum/avg/min/max) are sql fragments for a .select({ … }) projection..where(a, b) 带多个参数等同于 and(a, b)。比较的第二个参数默认是一个值,但也可以是另一个列或 sql`…`——例如 eq(a.x, b.y)。聚合函数(count/sum/avg/min/max)是用于 .select({ … }) 投影的 sql 片段。

Raw SQL — when the builder isn't enough, drop to raw SQL. The mode is chosen by method: db.run for writes, db.all for many rows, db.get for one row.原始 SQL——当构建器不够用时,可以降级到原始 SQL。通过方法选择模式:db.run 用于写操作,db.all 用于多行,db.get 用于单行。

await db.run('UPDATE todos SET done = 1 WHERE id = :id', { ':id': 5 });
await db.all(sql`SELECT * FROM todos WHERE done = ${false}`);
await db.get(sql`SELECT count(*) AS c FROM todos`);

The sql`…` tag turns ${value} into a bound parameter and ${table.column} into an identifier, and splices nested sql fragments. Use sql.raw('…') for a literal with no parameters.sql`…` 标签将 ${value} 转换为绑定参数,将 ${table.column} 转换为标识符,并拼接嵌套的 sql 片段。使用 sql.raw('…') 表示没有参数的字面量。

Raw queries are not bound to a table, so their rows come back without mode conversion — a boolean is 0/1, a JSON column is a string, a timestamp is a number. Only the table‑bound builder converts values.原始查询没有绑定到表,所以返回的行不经过模式转换——布尔值是 0/1,JSON 列是字符串,时间戳是数字。只有表绑定的构建器会转换值。

Migrations迁移

Your database changes as your bot grows. The platform keeps that safe by separating deploying code from changing data, and by classifying every schema change by how risky it is.随着机器人的成长,数据库会变化。平台通过分离部署代码和更改数据,以及按风险程度对每个 schema 变更进行分类,来确保安全。

Deploying never touches the database. When you npx tgcloud push a changed schema.js, the platform records the new schema and tells you what the database would change — but applies nothing:部署永远不会触及数据库。当你用 npx tgcloud push 推送修改后的 schema.js 时,平台会记录新的 schema 并告诉你数据库会发生什么变化——但不会应用任何东西:

npx tgcloud push       # deploy schema.js; reports pending DB changes
npx tgcloud migrate    # review and apply them

npx tgcloud migrate computes the difference between your schema and the live database and walks you through it. You are always asked before anything is applied. This means a routine code deploy can never trigger a data migration by accident.npx tgcloud migrate 计算你的 schema 和在线数据库之间的差异,并引导你完成。在应用任何内容之前,你总是会被询问。这意味着常规的代码部署永远不会意外触发数据迁移。

Each pending change carries a status that determines how migrate treats it:每个待变更都有一个状态,决定了 migrate 如何处理它:

Status Meaning In migrate
safe Additive and non‑blocking — a new table, column, or index Applied together in one step, on confirmation
warning Potentially destructive or slow — dropping something, or an index on a huge table Presented one at a time, each confirmed separately
manual Can't be done automatically — e.g. changing a column's type Shown with guidance; you perform it by hand
undocumented Exists in the database but not in your schema Shown for awareness; not applied

Safe changes are quick and reversible in spirit, so they go through together. Each warning is a deliberate, individual confirmation — there is no “apply all” for destructive changes. 安全变更是快速的,本质上是可逆的,所以它们会一起执行。每个警告都需要单独确认——破坏性变更没有“全部应用”选项。

Manual changes come with a reason and a suggested action. migrate ends with a summary: how many changes were applied, skipped, awaiting a manual fix, or not in your schema. 手动变更带有原因和建议操作。migrate 最后会总结:应用了多少变更,跳过了多少,等待手动修复的,以及不在你的 schema 中的。

See migrate for the flags (--dry-run, --safe, --yes, --local).参见 migrate 命令的选项(--dry-run、--safe、--yes、--local)。

Removing things删除东西

Deleting a table or column from schema.js does not drop it — that would make an accidental deletion catastrophic. To remove something, mark it deprecated:从 schema.js 中删除表或列不会实际删除它们——否则意外删除会带来灾难。要删除某个东西,先标记为已弃用:

// drop a column
text('email').deprecated('replaced by login')

// drop a whole table
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('unused');

On the next migrate, the deprecated object shows up as a warning‑status drop that you confirm individually. Once it's gone, remove the declaration.下次运行 migrate 时,已弃用的对象会以警告状态显示为删除操作,需要你单独确认。一旦删除完成,再从声明中移除。

Changing a column's type更改列的类型

Type changes are manual — SQLite can't always do them in place, and coercing existing values is a judgment call. migrate will show the change and its reasoning; perform it yourself with raw SQL (db.run(...)), typically by creating a new column or table, copying data, and swapping.类型变更是手动的——SQLite 并不总能原地修改类型,而且强制转换现有值需要判断。migrate 会显示变更及其理由;你需要自己用原始 SQL(db.run(...))来执行,通常的做法是创建新列或新表、复制数据、然后切换。

The SDK

At runtime a module has one library: sdk. It bundles the three things a bot backend needs — a database, the Telegram Bot API, and outbound HTTP — with nothing to install and no credentials to configure. The database (db) is covered in The database; this section covers api, fetch, and the console global.运行时,模块只有一个库:sdk。它集成了机器人后端需要的三样东西——数据库、Telegram Bot API 和外部 HTTP——无需安装,无需配置凭证。数据库(db)在数据库部分介绍;本节介绍 api、fetch 和 console 全局对象。

import { db, api, fetch, BotApiError } from 'sdk';   // the whole surface
// or from submodules:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
Import What it is
db Database — query builder and schema DSL → The database
api Telegram Bot API — api.sendMessage(...)below
fetch Outbound HTTP → below

Import your own project modules by their bare name (from 'schema', from 'lib/cart') — never a relative path or a .js extension. See The module system.用裸名称导入你自己的项目模块(from 'schema'、from 'lib/cart')——永远不要用相对路径或 .js 扩展名。参见模块系统。

The Bot APIBot API

api gives you the entire Telegram Bot API. Call any method as api.<method>(params). Every current — and future — Bot API method works with no SDK update required.api 提供了完整的 Telegram Bot API。以 api.<method>(params) 的方式调用任何方法。所有当前和未来的 Bot API 方法都可以使用,无需更新 SDK。

import { api } from 'sdk';

const me = await api.getMe();                            // → the unwrapped result
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' });

The response envelope is unwrapped. The Bot API normally wraps results in { ok: true, result: … }. api returns the result directly — getMe() resolves to the user object, not to a wrapper. Parameters use the Bot API's own snake_case names (chat_id, message_id, reply_markup, …).响应信封已经被解开。Bot API 通常将结果包装在 { ok: true, result: … } 中。api 直接返回 result——getMe() 解析为用户对象,而不是包装对象。参数使用 Bot API 自己的 snake_case 名称(chat_id、message_id、reply_markup 等)。

Failures throw BotApiError. When the Bot API returns { ok: false }, the call throws a BotApiError instead of returning a falsy value, so you can't accidentally ignore it. The error carries .code (the Bot API error_code), .description (the human‑readable message), .method (which method failed), and .parameters (extra data such as retry_after on a 429, or migrate_to_chat_id). Catch it to handle an expected failure and rethrow the rest:失败会抛出 BotApiError。当 Bot API 返回 { ok: false } 时,调用会抛出 BotApiError 而不是返回一个假值,这样你就不会不小心忽略它。错误对象包含 .code(Bot API error_code)、.description(可读消息)、.method(哪个方法失败)和 .parameters(额外数据,如 429 时的 retry_after,或 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 = the message is already gone; that's fine here.
  } else {
    throw e;
  }
}
File Limitations文件限制

You can work with files already on Telegram's servers by their file_id — send, forward, or reuse them — but downloading a file's bytes (getFile plus fetching the content) or uploading a new file from a handler isn't supported yet.你可以使用已经在 Telegram 服务器上的文件,通过 file_id 发送、转发或复用——但从处理程序下载文件的字节(getFile 加上获取内容)或上传新文件目前还不支持。

You can easily design around this temporary limitation by passing file_ids rather than raw bytes.你可以通过传递 file_id 而不是原始字节来轻松绕过这个临时限制。

HTTP

fetch is a fetch‑like client for calling the outside world — third‑party APIs, webhooks, anything over HTTP.fetch 是一个类似 fetch 的客户端,用于调用外部世界——第三方 API、webhooks,任何 HTTP 请求。

import { fetch } from 'sdk';

const res = await fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Pavel' }),
});
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();

The response mirrors the web platform: res.status, res.statusText, res.ok (true for 200–299), res.url, res.headers (.get(), .has(), .keys(), .entries()), and body readers await res.json() / await res.text(). 响应与 Web 平台一致:res.status、res.statusText、res.ok(200–299 为 true)、res.url、res.headers(.get()、.has()、.keys()、.entries())以及 body 读取器 await res.json() / await res.text()。

You can also read the body incrementally as a streamfor await (const chunk of res.body) { … } — which is how you consume server‑sent events or token‑by‑token output from AI APIs.你也可以以流的形式增量读取 body——for await (const chunk of res.body) { … }——这在消费服务器发送事件或 AI API 的逐 token 输出时很有用。

Body helpers set the matching Content-Type for you:Body 辅助方法会为你设置匹配的 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

It otherwise behaves like the standard fetch you already know, with two constraints: 除此之外,它的行为和你已经熟悉的标准 fetch 一样,有两个限制:

  • Response content is textual (binary payloads aren't supported).响应内容是文本(不支持二进制载荷)。
  • The total response is capped at 32 MB. That cap covers the whole response — streaming with res.body lets you process a large body incrementally, but it does not raise the limit.总响应限制为 32 MB。这个限制覆盖整个响应——用 res.body 流式处理可以增量消费大型 body,但不会提高限制。

Logging日志

The standard global console is available — nothing to import, it's just there as in any JavaScript. Its output is captured and shown by npx tgcloud run, which makes it your primary debugging tool during development.标准的全局 console 是可用的——无需导入,就像在任何 JavaScript 中一样。它的输出会被 npx tgcloud run 捕获并显示,这是开发期间主要的调试工具。

console.log('processing', { chatId: id });   // log / debug — plain
console.info('started');                     // info  — blue
console.warn('rate limited');                // warn  — yellow
console.error(err);                          // error — red, with a stack trace

Each line is tagged with the [file:line] it came from. console.error and console.trace append a full stack, while console.warn does not. When you npx tgcloud run a module, these lines are printed with a colored prefix per level, the time since the run started, and the origin — see run.每行都会标记来源的 [file:line]。console.error 和 console.trace 会附加完整的堆栈,而 console.warn 不会。当你用 npx tgcloud run 运行模块时,这些行会按级别显示带颜色的前缀、运行开始后的耗时和来源——参见 run 命令。

Command-line interface命令行界面

tgcloud is the bridge between your project folder and the cloud. It scaffolds projects, shows you what changed, deploys, runs modules, and applies database migrations. It needs Node.js 18 or newer. Two ways to get it:tgcloud 是你的项目文件夹和云端之间的桥梁。它生成项目、显示变更、部署、运行模块、应用数据库迁移。需要 Node.js 18 或更新版本。有两种安装方式:

# Recommended — create a project with the CLI installed into it:
npm create @tgcloud/bot example_bot

# Or install the CLI globally and init an empty folder:
npm install -g @tgcloud/cli
tgcloud init

The npm package is @tgcloud/cli; the command it installs is tgcloud. The CLI finds your project by walking up from the current directory to the nearest .tgcloud/, so every command works from any subfolder.npm 包是 @tgcloud/cli;安装的命令是 tgcloud。CLI 会从当前目录向上查找最近的 .tgcloud/ 来定位项目,所以每个命令都可以在任何子文件夹中运行。

Command Purpose
init Scaffold a new project in the current folder
add Scaffold a new module (a handler or a lib module)
login Link the project to a bot (saves the token)
status Show what changed locally vs. the cloud
diff Show the line‑by‑line changes
push Deploy changed modules to the cloud
migrate Apply schema changes to the database
run Execute a module on the platform without deploying
fetch Refresh the local reference copy from the cloud
pull Bring local files in line with the cloud
reset Discard local changes; restore from the cloud state
webhook Inspect and re‑sync the platform‑managed webhook
completion Print a shell completion script (bash/zsh/fish)

Authentication认证

A project is tied to one bot by its token, which has the form app<id>:<secret>. The app<id> part is public and may be printed; the secret never appears in logs or errors. 一个项目通过令牌与一个机器人绑定,令牌格式为 app<id>:<secret>。app<id> 部分是公开的,可以打印;secret 永远不会出现在日志或错误中。

The token is resolved in this order:令牌按以下顺序解析:

  1. TGCLOUD_TOKEN environment variable — for CI; never written to disk.TGCLOUD_TOKEN 环境变量——用于 CI;不会写入磁盘。
  2. .tgcloud/credentials — written by npx tgcloud login..tgcloud/credentials——由 npx tgcloud login 写入。
  3. Neither → an error pointing you at npx tgcloud login.两者都没有 → 出现错误,提示你运行 npx tgcloud login。

The CLI never prompts for a token mid‑command — a surprise prompt would hang scripts and CI. Logging in is always the explicit login step, and if a saved token becomes invalid (401/403), the CLI clears it and asks you to login again rather than re‑prompting in place. CLI 不会在命令执行中提示输入令牌——意外的提示会导致脚本和 CI 挂起。登录始终是一个明确的步骤,如果保存的令牌失效(401/403),CLI 会清除它并要求你重新登录,而不是原地再次提示。

initinit

npx tgcloud init

Scaffolds a new project in the current directory: schema.js, lib/, handlers/, a starter handler, AGENTS.md, docs/, and the .tgcloud/ state folder. The set of files it creates is provided by the platform, so new starter files and directories can appear without upgrading the CLI. Offline, it falls back to a built‑in copy, so init always works.在当前目录中生成一个新项目:schema.js、lib/、handlers/、一个入门处理程序、AGENTS.md、docs/ 和 .tgcloud/ 状态目录。它创建的文件集由平台提供,所以无需升级 CLI 就可以出现新的入门文件和目录。离线时,它会回退到内置副本,所以 init 始终有效。

init refuses to nest inside another project — an ancestor directory that already has a .tgcloud/ — so you can't accidentally shadow one; re‑running init in a project's own root is fine and just fills in anything missing. init 拒绝嵌套在另一个项目内部——即祖先目录中已经有 .tgcloud/——这样你就不会意外覆盖一个项目;在项目根目录中重新运行 init 是安全的,只会补充缺失的文件。

addadd

npx tgcloud add <target>

Scaffolds a single new module, wired up and ready to edit — a handler or a lib/ module.生成一个单独的模块,开箱即用,可以编辑——一个处理程序或 lib/ 模块。

npx tgcloud add handlers/callback_query   # a new update handler
npx tgcloud add lib/cart                  # a new shared module

Note that add never overwrites an existing file. 注意,add 命令不会覆盖已有的文件。

The <target> is the module's path (a trailing .js is optional). For handlers/, the name must be a Telegram update type; the platform advertises the valid set, so an invalid name is rejected up front. handlers/ is flat; lib/ may be nested (lib/payments/stripe).<target> 是模块的路径(末尾的 .js 可选)。handlers/ 下的名称,必须是 Telegram 的更新类型。平台会公布哪些类型有效,名称不对,一开始就拒绝。handlers/ 是扁平的。lib/ 可以嵌套,比如 lib/payments/stripe。

The module name is required. Giving just the directory is an error — but a helpful one: for handlers/ it lists the update types you don't already have, so you can copy one.模块名必填。光给目录不行,会报错。但错误提示有用:handlers/ 下,它列出你没有的更新类型,你可以照着复制一个。

$ npx tgcloud add handlers
Error: Specify a name, e.g. "npx tgcloud add handlers/callback_query".
Available handlers/ types: callback_query, inline_query, chat_member, …

<Tab> completion offers the same set — see completion. Tab 补全会给出同样的集合——见 completion。

The generated file has a live export default, so the handler is active as soon as you deploy — there's nothing to uncomment. Deploy the new module with push.生成的文件带一个活的 export default。部署上去,处理器就能用,不用去掉什么注释。用 push 部署新模块。

loginlogin

npx tgcloud login

Prompts for your CLI access token — from @BotFather → your bot → Serverless → CLI Access → Access token, a separate token from your bot's API token — validates it against the platform, and saves it to .tgcloud/credentials. 程序会提示输入 CLI 访问令牌。这个令牌从 @BotFather 拿:your bot → Serverless → CLI Access → Access token,跟机器人的 API 令牌不是同一个。它向平台验证,验证通过,存到 .tgcloud/credentials。

login is the only command that asks for a token. It requires a real terminal and will not run without one, so it never hangs in CI.只有 login 命令会问令牌。它需要真正的终端,没有就不运行。所以在 CI 里不会卡住。

statusstatus

npx tgcloud status

Shows, per file, what has changed between your working directory and the deployed copy: modified, new, deleted, unchanged. Fully offline — it compares against the local reference copy in .tgcloud/. A full run also warns about stray .js files at the project root.status 按文件显示变化:修改、新增、删除、未变。完全离线,跟 .tgcloud/ 里的本地参考副本比较。项目根目录下有多余的 .js 文件,也会警告。

diffdiff

npx tgcloud diff

Like status, but shows the actual line‑by‑line differences for changed modules. Also offline.跟 status 类似,但把改过的模块一行一行列出来。也是离线的。

pushpush

npx tgcloud push [files...]

Deploys your project to the cloud in one atomic batch.一次原子批处理,把项目部署到云上。

With no arguments, it deploys the whole project, and the deployed state is made to mirror your folder exactly — modules you deleted locally are removed in the cloud.无参数时,部署整个项目,部署后的状态跟你的文件夹完全一样——本地删了的模块,云上也删掉。

With file or directory arguments (npx tgcloud push handlers/message.js, npx tgcloud push handlers/), it narrows which changes are sent, but still sends the full manifest, so a targeted push never deletes untouched modules.给文件或目录参数,只发这些变化,但清单还是全量的。所以定向推送不会删掉没动过的模块。

Its one option is --force — to skip the concurrency check and overwrite whatever is in the cloud. Only use it when you're sure (see Staying in sync). 它只有一个选项 --force,跳过并发检查,直接覆盖云上的内容。只在确定的时候用(见 Staying in sync)。

After a deploy, if schema.js changed and the database is out of sync, push prints a summary of the pending changes and suggests npx tgcloud migrate. It never applies them itself.部署之后,如果 schema.js 变了,数据库不同步,push 会打印待修改的总结,建议运行 npx tgcloud migrate。它自己不会执行迁移。

migratemigrate

npx tgcloud migrate

Applies your schema changes to the database. It computes the difference between schema.js and the live database, then walks you through it one step at a time with a running [N/M] counter:migrate 把 schema 改动应用到数据库。它计算 schema.js 跟线上数据库的差异,然后一步一步带着你走,每一步显示 [N/M] 进度。

  • A brief summary of all pending changes.先列出所有待改动的简要总结。
  • Safe changes, applied together in a single step on your confirmation.安全的改动,你确认后一次全部应用。
  • Warnings (drops, slow operations), one at a time, each confirmed separately.有警告的(删除、慢操作),每次一个,逐个确认。
  • Manual changes, shown with a reason and suggested action, not applied automatically.需要手动改的,显示原因和操作建议,不会自动应用。
  • Undocumented objects (in the database but not your schema), shown for awareness. 数据库中有的但 schema 中没有的对象,显示出来让你知道。

It ends with a summary: applied, skipped, awaiting manual fix, not in schema.最后显示总结:已应用、已跳过、等待手动修复、不在 schema 中。

See Migrations for the model.模型参考 Migrations。

Options: --dry-run (print everything, apply nothing), --safe (auto‑apply safe changes, skip warnings), --yes (auto‑apply safe changes and every warning, skip manual — use with care), --local (diff against your local schema.js instead of the deployed one). Without a flag, migrate requires a terminal and errors in a non‑interactive environment rather than guessing.选项:--dry-run(只打印,不执行)、--safe(自动应用安全改动,跳过警告)、--yes(自动应用安全改动和所有警告,跳过手动——小心使用)、--local(跟本地 schema.js 比较,而不是线上)。不加标志时,migrate 需要终端,在非交互环境会报错,不会自行猜测。

runrun

npx tgcloud run <module> [args] [--ctx <json5>]

Executes a handler on the platform without deploying, using your current local files. This is the fast inner loop for testing logic.run 在平台上执行一个处理器,但不部署,用你当前的本地文件。这是快速测试逻辑的内部循环。

  • <module> — a bare name (searched under handlers/) or a path like handlers/message.<module>:裸名称(在 handlers/ 下查找)或路径,比如 handlers/message。
  • [args] — the payload passed to the handler, written in JSON5 so you can skip quoting keys. It's the update‑type object your handler receives (e.g. a Message for handlers/message).[args]:传给处理器的载荷,用 JSON5 写,所以键可以不加引号。就是处理器收到的更新类型对象(比如 handlers/message 的 Message 对象)。
  • --ctx <json5> — the handler's context object (its second argument), also JSON5. Use it to supply what your handler reads off ctx — e.g. the raw update: --ctx '{ update: { update_id: 1 } }'.--ctx <json5>:处理器的上下文对象(第二个参数),也是 JSON5。你可以用它提供处理器从 ctx 中读取的内容,比如原始更新:--ctx '{ update: { update_id: 1 } }'。
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'

The platform runs the module against the module space assembled from your local project (so locally‑changed lib/ code is used too) and returns the return value, anything logged with console.*, and the elapsed time. Read big arguments from a file with npx tgcloud run handlers/message "$(cat message.json5)".平台会从本地项目组装模块空间(所以本地改过的 lib/ 代码也会用到),然后运行模块,返回返回值、console.* 记录的内容、以及运行耗时。大的参数可以从文件读取:npx tgcloud run handlers/message "$(cat message.json5)"。

fetchfetch

npx tgcloud fetch

Refreshes the local reference copy of the deployed state without touching your working files. Useful to re‑check a conflict before deciding how to resolve it.fetch 刷新部署状态的本地参考副本,不动你的工作文件。在决定怎么解决冲突前,可以用来重新检查。

pullpull

npx tgcloud pull

Brings your local project in line with the cloud — updates both the reference copy and your working files to the deployed state.pull 使本地项目跟云上一致——参考副本和工作文件都更新到部署状态。

resetreset

npx tgcloud reset

Discards your local changes and restores the working directory from the last known cloud state. Use it to throw away an experiment.reset 丢弃本地改动,从上次已知的云状态恢复工作目录。用来扔掉实验。

webhookwebhook

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

Telegram delivers updates to your bot through a webhook, which the platform manages for you — you never point it anywhere by hand. npx tgcloud webhook shows its current state: the URL, the allowed_updates list, how many updates are pending, the last delivery error (if any), and whether it is in sync with your deployed handlers.Telegram 通过 webhook 把更新送到你的机器人,平台替你看管——你不需要手动指向哪里。npx tgcloud webhook 显示当前状态:URL、allowed_updates 列表、待处理更新数量、上次投递错误(如果有)、以及是否和已部署的处理器同步。

“In sync” means the webhook points at the platform and its allowed_updates match the handlers you've deployed — so Telegram delivers exactly the update types you handle, and nothing else. Deploying a new handler (or removing one) can leave the webhook out of sync until it's refreshed; npx tgcloud status flags this too."同步"指 webhook 指向平台,且 allowed_updates 跟已部署的处理器匹配——这样 Telegram 只会投递你处理的那种更新类型,不会多。部署新处理器(或删除一个)后,webhook 会不同步,直到刷新;npx tgcloud status 也会标记这一点。

npx tgcloud webhook sync fixes it — it re‑points the webhook at the platform and rebuilds allowed_updates from your deployed handlers. Add --drop-pending to discard updates Telegram had already queued before the sync (otherwise they're delivered once the webhook is healthy again).npx tgcloud webhook sync 可以修复——它把 webhook 重新指向平台,并根据已部署的处理器重建 allowed_updates。加上 --drop-pending 可以丢弃 Telegram 在同步前已经排队的更新(否则 webhook 恢复后还会投递)。

completioncompletion

Note: tab‑completion works only when a bare tgcloud is on your PATH — so install it globally (npm install -g @tgcloud/cli) or otherwise put the binary on your PATH. It can't hook into npx.注意:Tab 补全只在 PATH 上有裸 tgcloud 时才有效——所以全局安装(npm install -g @tgcloud/cli)或者把二进制文件放到 PATH 里。它不能挂到 npx 上。

tgcloud completion <bash|zsh|fish>

Prints a shell completion script to stdout. Enable it once, then <Tab> completes commands, flags, module directories, the handler update‑types you don't have yet, and your local runnable modules — the suggestions are computed live, so they reflect the current project and the platform's advertised update‑types.completion 向标准输出打印一个 shell 补全脚本。启用一次后,Tab 可以补全命令、标志、模块目录、你还未拥有的处理器更新类型、以及本地的可运行模块——这些建议是实时计算的,能反映当前项目和平台公布的更新类型。

# bash — needs the bash-completion package:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc

# zsh — ensure `autoload -U compinit && compinit` runs in your ~/.zshrc:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc

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

Restart your shell (or re‑source the file) afterwards. Running tgcloud completion with no shell prints these instructions again.之后重启 shell(或重新 source 该文件)。如果运行 tgcloud completion 时不指定 shell,它会再次打印这些说明。

Staying in sync保持同步

Every project has a monotonically increasing revision in the cloud, bumped on each deploy. The CLI remembers the revision it last synced with and sends it on each push. If the cloud has moved ahead — because another machine or teammate deployed — the push is rejected instead of silently overwriting their work, and the CLI offers three ways forward:每个项目在云上有一个单调递增的版本号,每次部署递增。CLI 记得上次同步的版本号,每次 push 时发送。如果云上的版本更新了——因为其他机器或同事部署过——push 会被拒绝,不会默默覆盖他们的工作。CLI 提供三种处理方法:

npx tgcloud fetch           # pull the latest into the reference copy, then re-check
npx tgcloud pull            # pull the latest into both reference and working files
npx tgcloud push --force    # overwrite the cloud state (dangerous)

This optimistic‑concurrency check is why you can share a bot across a team without a lockstep deploy process. Commands exit non‑zero on failure — a rejected deploy, a failed migration, an authentication error, a module that threw during run — so they compose cleanly in scripts and CI pipelines.有了这个乐观并发检查,团队共享一个机器人就不需要严格的部署流程。失败时命令退出码非零——部署被拒绝、迁移失败、认证错误、运行中模块抛异常——所以在脚本和 CI 管道中可以干净地组合。

Go up