# Telegram 无服务器架构

- 来源：Hacker News 热门（buzzing.cc 中文翻译）
- 作者：soheilpro
- 发布时间：2026-07-15 23:37
- AIHOT 分数：77
- AIHOT 标记：精选
- AIHOT 链接：https://aihot.virxact.com/items/cmrm9ne9101gqbiijkgrj6t9h
- 原文链接：https://core.telegram.org/bots/serverless

## 精选理由

Telegram 给机器人开发搭了一套完整的无服务器后端，内置数据库和 CLI，从写代码到部署都在一个文件夹里完成。做 Telegram 机器人的开发者可以彻底扔掉 VPS 和云函数了。

## AI 摘要

Telegram Serverless 允许开发者直接在 Telegram 基础设施上运行 Bot 和 Mini App 的后端代码，无需配置服务器或容器。开发者编写普通 JavaScript 模块，通过 `npx tgcloud push` 单命令部署，代码在靠近 Bot API 和内建数据库的轻量级 V8 隔离沙箱中执行。

## 正文

Telegram 无服务器

Telegram 无服务器让你直接在 Telegram 的基础设施上运行机器人和迷你应用的后端代码——无需配置服务器、无需保持容器运行、无需考虑扩展。你只需编写纯 JavaScript 模块，通过一条命令部署，Telegram 就会在紧邻 Bot API 和内置数据库的快速、隔离的 V8 沙箱中运行它们。

如果你曾经为了响应一个 /start 命令而把机器人连接到 VPS、云函数或托管面板，那么这部分工作你不再需要做了。

本页内容

为什么选择无服务器

快速上手

结合 AI 构建

通过 BotFather 随时操作

项目与模块

数据库

SDK

CLI 参考

为什么选择无服务器

Telegram 机器人本质上是一个响应更新的程序。传统上，你必须把该程序托管在一个始终在线、可访问且安全的地方——并一直保持这种状态。Telegram 无服务器完全移除了这一层：

无需基础设施。没有需要租用、修补或监控的机器。你的代码按需运行，并随你的机器人自动扩展。

开箱即用。Telegram Bot API、基于 SQLite 的数据库以及出站 HTTP 请求，每个模块都直接可用——无需安装任何东西，无需配置任何凭证。

快速、隔离的执行。每次调用都在轻量级的 V8 隔离环境中运行，靠近 Telegram 自身的系统，因此对 Bot API 和数据库的调用快速且可靠。

真正的开发者工作流。项目位于你机器上受版本控制的文件夹中。你编辑文件，精确查看更改内容，原子化部署，并通过经过审查的迁移来推进数据库模式——就像你处理其他所有事情一样。

心智模型

你在三个地方工作，它们之间清晰对应：

位置 那里存放什么

你的项目文件夹 JavaScript 模块——模式、共享代码、更新处理器

云端 这些模块的已部署副本，加上你机器人的数据库

tgcloud CLI 桥梁——它向你展示差异并同步它们

你永远不需要 SSH 进入任何东西。你在本地编辑文件，运行 npx tgcloud push，平台就会接手后续工作。你机器人的流量由已部署的模块处理；你的数据库在多次调用之间持久存在。

一个项目只有三种代码：

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

当有更新到达时——一条消息、一次按钮点击、一个内联查询——Telegram 会将其路由到匹配的处理程序（handlers/message.js、handlers/callback_query.js……）并调用其默认导出。该函数通过 SDK 与 Bot API 和数据库交互，然后返回。这就是整个循环。没有匹配处理程序的更新会被直接忽略，因此你只需添加自己需要的处理程序。

快速演示

下面是一个完整可运行的演示机器人。它会回复每一条消息，并记录在每个聊天中已看到多少条消息。

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

部署它：

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

这是一个具有持久状态且无需服务器的在线机器人。其中的一切——api、db、table() DSL——都在下面的章节中描述。

Serverless 是 Telegram 机器人和 Mini Apps 的通用后端，而不是某一种应用的模板。它非常适合以下场景：

需要将每个用户状态存储在数据库中的对话式 AI 机器人。

存储用户数据并提供动态内容的 Mini App 后端。

游戏和工具——包括排行榜、测验等。

调用第三方 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/ ├─ 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

这些搭建好的文件是自文档化的——每个文件都包含带注释的示例，说明你接下来可以做什么。

CLI 会作为本地开发依赖安装到项目中，因此你可以通过 `npx tgcloud <command>` 来运行它（npx 会找到项目 `node_modules` 中的副本），或者通过脚手架添加到 `package.json` 中的 npm run 快捷方式（`npm run deploy`、`npm run status`）来运行。默认情况下，你的 PATH 中不会有全局的 `tgcloud`。

你也可以全局安装它——`npm install -g @tgcloud/cli`——如果你更愿意在任何地方直接输入 `tgcloud` 的话。这对于在任何空文件夹中运行 `tgcloud init` 很方便，并且这也是 shell 选项卡补全所需要的。无论哪种方式，你都会得到相同的项目。

2. 关联你的机器人

每个项目都与一个机器人绑定。通过 `login` 命令将它们连接起来，该命令会要求你提供 CLI 访问令牌（@BotFather → 你的机器人 → Serverless → CLI Access → Access token——这是一个与你的机器人 API 令牌不同的独立令牌）并将其存储在本地：

npx tgcloud login

令牌的格式为 `app<id>:<secret>`。CLI 会将其保存在 `.tgcloud/` 目录中，该目录已被 git 忽略，并且永远不会打印出 secret 部分。你只需要在登录时提供它——关于如何在 CI 中解析令牌，请参阅身份验证部分。

3. 查看情况

有两个命令可以让你随时了解当前状态，两者都完全离线运行：

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

在 `init` 之后，一切都是全新的，尚未部署任何内容。`status` 会显示等待上传的起始文件。

4. 部署

将你的模块发送到云端：

npx tgcloud push

`push` 会以原子批处理的方式上传每个已更改的模块，并更新你本地关于云端当前内容的记录。你的机器人现已上线：在 Telegram 中打开它并发送一条消息——起始处理器会将其原样回复。

部署永远不会触及你的数据库。推送代码和更改数据库模式是刻意分开的步骤，因此代码部署永远不会因数据迁移而让你措手不及。这正是下一步要做的。

5. 添加数据库表

让机器人记住一些东西。打开 `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 # uploads the new schema.js npx tgcloud migrate # creates the `messages` table

`push` 会报告模式不同步，并显示待处理的更改，但不会应用任何内容。`migrate` 会引导你完成更改，并在你确认后创建该表。这种两步模型——以及对于删除等风险较高的更改会发生什么——在迁移部分有详细说明。

6. 存储和读取数据

现在使用你 handler 中的那张表。编辑 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.`, }); }

用 `npx tgcloud push` 部署更新后的 handler，然后给你的机器人发几条消息，观察计数增长。数据库在多次调用之间持久存在——这就是你机器人的记忆。

7. 不部署直接测试

你不需要部署就能尝试修改。`npx tgcloud run` 会使用你的本地文件在平台上执行一个 handler，而不会发布这些文件：

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

参数是你的 handler 接收到的负载——对于 handlers/message 来说，就是一个 Message——以 JSON5 格式编写（因此你可以省略键的引号）。该命令会打印 handler 通过 `console.*` 记录的任何内容、返回值以及执行耗时。这是迭代逻辑的最紧凑循环——无需部署，无需等待真实消息。

8. 保持同步

在你工作时，少数几个命令就能让你的本地项目与云端保持一致：`npx tgcloud status` 显示变更内容，`npx tgcloud push` 进行部署，`npx tgcloud pull` 让你的本地项目与云端同步，`npx tgcloud fetch` 刷新参考副本而不触碰你的文件，`npx tgcloud reset` 则丢弃本地更改。

如果两个人（或两台机器）向同一个机器人部署，平台会检测到冲突并阻止 push，让你先执行 pull——你永远不会静默覆盖他人的工作。详见“保持同步”部分。

用 AI 构建

更倾向于用 AI 助手来构建——或者你团队里唯一的程序员就是 AI？你仍然可以发布一个机器人。我们迈出了第一步，让 AI 智能体在项目中感到得心应手：每个新项目都会附带一个 `AGENTS.md` 和 `docs/tgcloud-sdk.md` 参考文件，智能体编码工具会自动读取它们。

再加上一个小巧、自包含的运行时——一个 SDK，无需处理 npm 包——这能让助手快速上手那些通用代码生成往往遗漏的约定：通过裸名称导入、无外键、每个数据库调用都是异步的、每个更新类型对应一个 handler、以及两步式的 push/migrate 流程。

试试看：

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

然后直接用自然语言提出要求：

编写一个能记住每个人待办事项列表的机器人——当用户发送文本时添加一个项目，当用户发送 /list 时显示整个列表。

智能助手会为你编辑 schema.js 和你的处理程序；你只需审查代码，用 `npx tgcloud run` 即时测试变更，然后通过 `npx tgcloud push` 和 `npx tgcloud migrate` 上线发布。AGENTS.md 是你项目的一部分——随着机器人功能增长，你可以编辑该文件，确保指导说明始终准确。

通过 BotFather 随时随地进行管理

身边只有手机？整个项目同样托管在 @BotFather 中——打开你的机器人 → 选择 Serverless，你就能在触屏上获得 CLI 管理的全部功能：

处理程序（Handlers）——创建、编辑和测试运行更新处理程序；BotFather 会保持 webhook 与你当前拥有的处理程序同步（即 CLI 所报告的"同步中/不同步"状态）。

库（Library）——你的共享 lib/ 模块。

数据库（Database）——使用与 Drizzle 类似的语法编辑 schema.js，审查待处理的变更，然后应用这些变更；支持部署。

CLI 访问（CLI Access）——当你回到键盘前时，可在此处获取 CLI 访问令牌。

这是同一个云项目，因此你可以在手机上创建一个处理程序，稍后在笔记本电脑上用 `npx tgcloud pull` 拉取它——所有操作都不绑定到单一客户端。运行处理程序时，甚至能直接在聊天中显示其控制台输出，就像 `npx tgcloud run` 一样。

项目与模块

一个无服务器项目就是一个受版本控制的普通文件夹。它只包含 JavaScript 模块和少量本地状态——无需构建步骤，运行时没有 node_modules，也没有服务器入口点。

项目结构解析

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)

只有 schema.js 以及 lib/ 和 handlers/ 目录下的 .js 文件会被部署。其他所有内容——Markdown 文件、配置文件、.tgcloud/ 文件夹——都保留在你的本地机器上。

schema.js——你的数据库。它使用 schema DSL 以命名导出的方式声明数据表，作为单个文件位于项目根目录。它像其他模块一样被部署，但部署它永远不会改变数据库——schema 变更需要通过 `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>。

.tgcloud/ — 完全由 CLI 管理的机器本地状态：你保存的凭据、用于离线差异比较的已部署代码镜像，以及一个小型缓存。它被 git 忽略，你绝不应手动读取或写入它——请改用 CLI 命令。

模块系统

在运行时，一个模块只能看到两样东西：平台 SDK 和你项目中的其他模块。没有 npm 包，没有文件系统，除了通过 SDK 的 fetch 之外也没有网络。

模块通过其名称来寻址——即从项目根目录开始的路径，不带 .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

相对路径和文件扩展名不起作用——平台在其模块空间中解析名称，而不是目录中的文件。

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

运行时能访问的东西只有两样：sdk 及其子模块（sdk/db、sdk/api、sdk/fetch）——也就是整个平台的能力面，详见《SDK》——以及你自己在 schema、lib/、handlers/ 下的模块。这就是完整清单。如果你的代码导入了其他任何东西，它都不会被解析。这个约束保证了模块加载快速且运行安全。

处理器

处理器是 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)'}`, }); }

处理器接收更新的载荷作为其参数——平台会为你解包 Telegram Update。handlers/message.js 接收 Message（即 update.message）；handlers/callback_query.js 接收 CallbackQuery；以此类推。处理器的第二个参数是一个每次调用都会创建的上下文对象 ctx。它携带原始 Update 作为 ctx.update——当你需要载荷之外的内容（如 update_id）时可以访问它。

处理器可以是异步的（通常如此），并且可以返回一个值。它通过 SDK 访问 Bot API、数据库和出站 HTTP。

你不需要真实的更新来测试处理器。npx tgcloud run 会在平台上用你提供的载荷和你当前的本地代码来执行它：

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

参数是载荷——也就是你的处理器接收到的同一个对象——采用 JSON5 格式。要提供处理器的 ctx（它的第二个参数），添加 --ctx，例如 --ctx '{ update: { update_id: 1 } }'。这会针对你的本地文件运行，因此你可以在部署之前尝试修改。参见 run。

部署的内容

当你执行 npx tgcloud push 时，CLI 会收集 schema.js、lib/ 和 handlers/ 下的所有 .js 文件，并将这组文件作为你项目的模块空间发送。云端存在但项目中缺失的任何内容都会被移除，因此部署状态始终与你的文件夹保持一致——包括删除操作。这些位置之外的文件会被忽略。项目根目录下多余的 .js 文件（非配置文件）会被标记出来，以免被无声忽略，因为项目根目录只应存放无服务器内容。Markdown 文件、点文件和 .tgcloud/ 目录永远不会被部署。

数据库

每个机器人都有自己的数据库——一个基于 SQLite 的存储，在多次调用之间持久存在，并且每个模块都可以通过 `db` 访问。你在 `schema.js` 中使用一个小型、带类型的 DSL 描述数据表；通过流畅的查询构建器进行读写；并通过经过审查的迁移来演进数据表。

了解 Drizzle ORM？那你已经知道如何在这里与数据库交互了。Schema 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 之间的转换方式。

模式 存储为 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 的子类，因此这是可移植的基础类型）。上述模式仅控制值的编码方式，与列的存储类型无关：因此 `blob('col', { mode: 'json' })` 是 `json()` 的 BLOB 对应版本——采用相同的 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' }) // or 'virtual' text('email').deprecated('replaced by login') // marks the column for removal

在 `json()` 列上使用 `.default()` 会为你自动编码该值；而 `sql…`` 形式的默认值则会原样传递。`.deprecated()` 是终结性修饰符——参见迁移（Migrations）部分。

索引与约束

索引和约束在 `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(...)` 之后链式调用：`.strict()`、`.withoutRowid()`、`.deprecated('reason')`。

无外键

运行时以 `PRAGMA foreign_keys=off` 运行。声明的外键将静默失效——无级联操作，无孤儿保护——这比完全没有外键更糟糕，因此 DSL 使其无法实现。具体来说，`.references()` 和表级 `foreignKey()` 在声明时会抛出错误，使用它们的 schema 将无法部署。

你应该使用普通列来建模关系（例如 `userId: integer('user_id')`），并在应用程序代码中强制完整性：先插入父记录再插入子记录，先删除子记录再删除父记录，妥善处理错误，并在需要时使用 `LEFT JOIN … WHERE parent.id IS NULL` 来清理孤儿记录。

缺少外键是一个刻意的约束，而非疏忽。在规划你的机器人时，请尽早考虑到这一点。

查询

`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

可链式调用的方法：`.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(); // 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();

请注意，批量插入是一条语句，因此受 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）是用于 .select({ … }) 投影的 sql 片段。

原生 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`);

sql…`` 标签将 ${value} 转换为绑定参数，将 ${table.column} 转换为标识符，并拼接嵌套的 sql 片段。使用 sql.raw('…') 可以传入不带参数的纯文本。

原生查询不绑定到特定表，因此返回的行不会经过模式转换——布尔值显示为 0/1，JSON 列显示为字符串，时间戳显示为数字。只有绑定表的构建器才会转换值。

数据库迁移

随着你的机器人不断成长，数据库也会发生变化。平台通过将部署代码与更改数据分离，并根据风险等级对每个模式变更进行分类，来确保这一过程的安全。

部署操作从不触及数据库。当你运行 npx tgcloud push 推送一个已修改的 schema.js 时，平台会记录新的模式，并告知你数据库将发生哪些变化——但不会应用任何更改：

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

npx tgcloud migrate 会计算你的模式与实时数据库之间的差异，并引导你逐步完成迁移。在应用任何更改之前，系统都会向你确认。这意味着常规的代码部署绝不会意外触发数据迁移。

每个待处理的变更都带有一个状态，该状态决定了 migrate 如何处理它：

状态 含义 在迁移中的处理方式

安全 新增且非阻塞——例如新表、新列或新索引 确认后一次性全部应用

警告 可能具有破坏性或速度较慢——例如删除某些内容，或在大型表上创建索引 逐个呈现，每个变更单独确认

手动 无法自动完成——例如更改列的数据类型 附带指导说明显示；你需要手动执行

未记录 存在于数据库中，但不在你的模式中 为提醒而显示；不会被应用

安全变更在本质上快速且可逆，因此会一并执行。每条警告都需要经过深思熟虑的单独确认——破坏性变更不存在“全部应用”的选项。

手动变更附带原因说明和建议操作。migrate 命令执行完毕后会显示摘要：有多少变更已应用、已跳过、等待手动修复，或不在你的数据库模式中。

关于 migrate 命令的标志（--dry-run、--safe、--yes、--local），请参见相关文档。

删除操作

从 schema.js 中删除表或列并不会实际删除它们——否则意外删除将造成灾难性后果。要删除某个对象，请将其标记为已弃用：

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

// drop a whole table 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, 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';

导入 定义

db 数据库——查询构建器和模式 DSL → 参见《数据库》章节

api Telegram Bot API——api.sendMessage(...) → 参见下文

fetch 出站 HTTP → 参见下文

通过裸名称（from 'schema'、from 'lib/cart'）导入你自己的项目模块——切勿使用相对路径或 .js 扩展名。参见《模块系统》章节。

Bot API

api 为你提供完整的 Telegram Bot API。以 api.<方法>(参数) 的形式调用任意方法。所有当前及未来的 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' });

响应信封已被解包。Bot API 通常将结果包装在 { ok: true, result: … } 中。api 直接返回结果——getMe() 解析为用户对象，而非包装对象。参数使用 Bot API 自身的 snake_case 命名（chat_id、message_id、reply_markup……）。

失败时会抛出 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_id 处理 Telegram 服务器上已有的文件——发送、转发或重复使用它们——但尚不支持从处理程序中下载文件的字节（getFile 加上获取内容）或上传新文件。

你可以通过传递 file_id 而非原始字节来轻松绕开这个临时限制。

HTTP

fetch 是一个类似 fetch 的客户端，用于调用外部世界——第三方 API、webhook，以及任何基于 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();

响应镜像了 Web 平台：res.status、res.statusText、res.ok（200–299 时为 true）、res.url、res.headers（.get()、.has()、.keys()、.entries()），以及正文读取器 await res.json() / await res.text()。

你还可以以流的形式增量读取正文——`for await (const chunk of res.body) { … }`——这就是你消费服务器发送事件或 AI API 逐 token 输出的方式。

正文辅助函数会为你设置匹配的 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

除此之外，它的行为与你已经熟悉的标准 fetch 相同，但有两条限制：

响应内容是文本形式的（不支持二进制负载）。

总响应大小上限为 32 MB。该上限涵盖整个响应——使用 res.body 进行流式处理可以让你增量处理大型正文，但不会提高这个限制。

日志记录

标准全局 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

每一行都带有来源的 [文件:行号] 标签。console.error 和 console.trace 会附加完整的堆栈信息，而 console.warn 则不会。当你使用 npx tgcloud run 运行一个模块时，这些行会按级别打印带有彩色前缀、自运行开始以来的时间以及来源——请参阅 run。

命令行界面

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

npm 包名为 @tgcloud/cli；安装后得到的命令是 tgcloud。该 CLI 通过从当前目录向上查找最近的 .tgcloud/ 目录来定位你的项目，因此所有命令都可以在任何子文件夹中运行。

命令 用途

init 在当前文件夹中搭建一个新项目

add 搭建一个新模块（一个 handler 或 lib 模块）

login 将项目关联到一个机器人（保存 token）

status 显示本地与云端之间的变更内容

diff 显示逐行变更

push 将已变更的模块部署到云端

migrate 将 schema 变更应用到数据库

run 在平台上执行一个模块，无需部署

fetch 从云端刷新本地参考副本

pull 使本地文件与云端保持一致

reset 放弃本地变更；从云端状态恢复

webhook 检查并重新同步平台管理的 webhook

completion 打印 shell 补全脚本（bash/zsh/fish）

身份验证

一个项目通过其 token 与一个机器人绑定，token 的格式为 app<id>:<secret>。app<id> 部分是公开的，可以打印；secret 永远不会出现在日志或错误信息中。

token 按以下顺序解析：

TGCLOUD_TOKEN 环境变量——用于 CI；永远不会写入磁盘。

.tgcloud/credentials——由 npx tgcloud login 写入。

两者都不存在 → 报错，提示你执行 npx tgcloud login。

CLI 永远不会在命令执行中途提示输入 token——意外的提示会导致脚本和 CI 挂起。登录始终是一个明确的步骤，如果保存的 token 失效（401/403），CLI 会清除它并要求你重新登录，而不是在原地再次提示。

init

npx tgcloud init

在当前目录中搭建一个新项目：schema.js、lib/、handlers/、一个 starter handler、AGENTS.md、docs/ 以及 .tgcloud/ 状态文件夹。它创建的文件集合由平台提供，因此无需升级 CLI 即可出现新的 starter 文件和目录。离线时，它会回退到内置副本，因此 init 始终可用。

init 命令拒绝嵌套在另一个项目内部——即上级目录中如果已存在 `.tgcloud/` 文件夹，则无法执行——这样你就不会意外覆盖已有项目；在项目自身根目录重新运行 init 是没问题的，只会补全缺失的内容。

add

npx tgcloud add <target>

搭建一个全新的模块，连接好即可编辑——可以是 handler 或 lib/ 模块。

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

注意，add 命令从不覆盖已有文件。

<target> 是模块的路径（末尾的 .js 是可选的）。对于 handlers/，名称必须是 Telegram 更新类型；平台会告知有效的类型集合，因此无效名称会提前被拒绝。handlers/ 是扁平结构；lib/ 可以嵌套（如 lib/payments/stripe）。

模块名称是必填项。仅提供目录名会报错——但错误提示很友好：对于 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。

生成的文件包含一个实时的 export default，因此 handler 在部署后立即生效——无需取消任何注释。使用 push 部署新模块。

npx tgcloud login

提示输入你的 CLI 访问令牌——来自 @BotFather → 你的 bot → Serverless → CLI Access → Access token，这是一个与你的 bot API 令牌不同的独立令牌——它会向平台验证该令牌，并将其保存到 .tgcloud/credentials。

login 是唯一需要输入令牌的命令。它需要真实的终端环境，没有终端就无法运行，因此绝不会在 CI 中卡住。

status

npx tgcloud status

按文件显示工作目录与已部署副本之间的差异：已修改、新增、已删除、未变更。完全离线运行——它会与 .tgcloud/ 中的本地参考副本进行比较。完整运行时还会警告项目根目录下多余的 .js 文件。

diff

npx tgcloud diff

与 status 类似，但会显示已变更模块的实际逐行差异。同样离线运行。

push

npx tgcloud push [files...]

以一次原子批量操作将你的项目部署到云端。

不带参数时，它会部署整个项目，并使已部署状态与你的文件夹完全一致——你在本地删除的模块也会在云端被移除。

如果指定了文件或目录参数（如 `npx tgcloud push handlers/message.js`、`npx tgcloud push handlers/`），则会缩小发送变更的范围，但仍会发送完整的清单文件，因此定向推送不会删除未触及的模块。

它的一个选项是 `--force`——用于跳过并发检查并覆盖云端的任何内容。仅在你确定时使用（参见“保持同步”部分）。

部署后，如果 `schema.js` 发生了更改且数据库不同步，`push` 会打印出待处理更改的摘要，并建议执行 `npx tgcloud migrate`。它本身从不应用这些更改。

migrate

npx tgcloud migrate

将你的 schema 更改应用到数据库。它会计算 `schema.js` 与实时数据库之间的差异，然后通过一个实时更新的 [N/M] 计数器，引导你逐步完成操作：

所有待处理更改的简要摘要。

安全更改，在你确认后一次性全部应用。

警告（删除操作、慢速操作），逐条确认，每条单独确认。

手动更改，会显示原因和建议操作，不会自动应用。

未记录的对象（存在于数据库中但不在你的 schema 中），会显示出来以供了解。

最后会给出一个摘要：已应用、已跳过、等待手动修复、不在 schema 中。

参见该模型的迁移文档。

选项：`--dry-run`（仅打印所有内容，不执行任何操作）、`--safe`（自动应用安全更改，跳过警告）、`--yes`（自动应用安全更改及所有警告，跳过手动操作——请谨慎使用）、`--local`（与你本地的 `schema.js` 进行差异对比，而非已部署的版本）。不带标志时，`migrate` 需要终端环境，在非交互式环境中会报错，而非自行猜测。

run

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

使用你当前的本地文件，在平台上执行一个处理器（handler）而无需部署。这是用于测试逻辑的快速内循环。

`<module>` —— 一个裸名称（在 `handlers/` 目录下查找）或一个路径，如 `handlers/message`。

`[args]` —— 传递给处理器的负载（payload），以 JSON5 格式编写，因此你可以省略键的引号。这是你的处理器接收到的更新类型对象（例如，对于 `handlers/message` 来说是一个 Message 对象）。

`--ctx <json5>` —— 处理器的上下文对象（其第二个参数），同样使用 JSON5 格式。用它来提供你的处理器从 `ctx` 中读取的内容——例如原始更新：`--ctx '{ update: { update_id: 1 } }'`。

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

该平台会从你的本地项目中组装出模块空间，并在其上运行该模块（因此本地修改过的 lib/ 代码也会被使用），然后返回返回值、所有通过 console.* 记录的内容以及运行耗时。使用 `npx tgcloud run handlers/message "$(cat message.json5)"` 可从文件中读取较大的参数。

fetch

npx tgcloud fetch

刷新已部署状态的本地参考副本，但不会触碰你的工作文件。在决定如何解决冲突之前，可用于重新检查冲突情况。

pull

npx tgcloud pull

使你的本地项目与云端保持一致——将参考副本和工作文件都更新为已部署状态。

reset

npx tgcloud reset

丢弃你的本地更改，并从最后一个已知的云端状态恢复工作目录。用于放弃一次实验。

webhook

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

Telegram 通过 webhook 向你的机器人传递更新，该平台会为你管理这个 webhook——你永远不需要手动指向任何地方。`npx tgcloud webhook` 会显示其当前状态：URL、allowed_updates 列表、待处理的更新数量、最后一次投递错误（如果有的话），以及它是否与你已部署的 handlers 保持同步。

“同步”意味着 webhook 指向该平台，并且其 allowed_updates 与你已部署的 handlers 相匹配——因此 Telegram 只会投递你处理的那几种更新类型，而不会投递其他类型。部署新的 handler（或移除一个）可能会导致 webhook 不同步，直到它被刷新；`npx tgcloud status` 也会标记这种情况。

`npx tgcloud webhook sync` 可以修复这个问题——它会将 webhook 重新指向该平台，并根据你已部署的 handlers 重建 allowed_updates。加上 `--drop-pending` 可以丢弃同步前 Telegram 已排队的更新（否则，一旦 webhook 恢复正常，这些更新仍会被投递）。

completion

注意：Tab 补全仅在裸 `tgcloud` 命令位于你的 PATH 中时才有效——因此请全局安装它（`npm install -g @tgcloud/cli`）或以其他方式将二进制文件放入你的 PATH。它无法接入 npx。

tgcloud completion <bash|zsh|fish>

将 shell 补全脚本输出到标准输出。启用一次后，按 <Tab> 键即可补全命令、标志、模块目录、你尚未拥有的 handler 更新类型以及你的本地可运行模块——这些建议是实时计算的，因此它们会反映当前项目和该平台公布的更新类型。

# 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

之后重启你的 shell（或重新加载该文件）。在不带任何参数的情况下运行 `tgcloud completion` 会再次打印这些说明。

保持同步

云端的每个项目都有一个单调递增的修订版本号，每次部署时都会递增。CLI 会记住它上次同步的修订版本号，并在每次推送时将其发送出去。如果云端版本已经更新——因为另一台机器或团队成员进行了部署——推送会被拒绝，而不是静默覆盖他们的工作，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)

这种乐观并发检查机制，正是你可以在无需严格同步部署流程的情况下，与团队共享一个 bot 的原因。命令在失败时返回非零退出码——无论是部署被拒、迁移失败、身份验证错误，还是运行时抛出异常的模块——因此它们可以干净地集成到脚本和 CI 流水线中。
