Telegram Serverless 允许您直接在 Telegram 的基础设施上运行您的机器人和 Mini App 的后端代码 — 无需配置服务器,无需保持容器运行,无需考虑扩展。您编写纯 JavaScript 模块,使用单个命令部署它们,然后 Telegram 会在紧邻 Bot API 和内置数据库的 V8 沙箱中快速、隔离地运行它们。

如果您曾将机器人连接到 VPS、云函数或托管面板,只是为了响应一个 /start 命令,那么现在这些工作您都不用再做了。

为什么选择 Serverless

Telegram 机器人本质上是一个对更新做出反应的程序。传统上,您必须将该程序托管在某个始终在线、可访问且安全的地方 — 然后保持其状态。Telegram Serverless 完全消除了这一层:

  • 无需基础设施。 没有需要租用、修补或监控的机器。您的代码按需运行,并自动随您的机器人扩展。
  • 凭据已包含。 Telegram Bot API、基于 SQLite 的数据库以及出站 HTTP 都可供每个模块开箱即用 — 无需安装,无需配置凭据。
  • 快速、隔离的执行。 每个调用都在一个轻量级的 V8 隔离环境中运行,靠近 Telegram 自己的系统,因此对 Bot API 和您的数据库的调用既快速又可靠。
  • 真正的开发工作流。 项目生活在您的机器上的一个文件夹中,并受版本控制。您编辑文件,确切地看到更改内容,进行原子化部署,并使用经过审查的迁移来推进您的数据库模式 — 这是您已经熟悉的处理其他一切的方式。

核心模型:项目如何工作

您在 三个地方 工作,它们之间可以清晰地映射:

地方 生活在那里
您的项目文件夹 JavaScript 模块 — 模式、共享代码、更新处理程序、Mini App 端点 — 以及,如果您有的话,Mini App 的前端
云端 这些模块的已部署副本,您的机器人的数据库,以及 Mini App 的文件
tgcloud CLI 桥梁 — 它显示差异并同步它们

您永远不会 SSH 到任何东西。您在本地编辑文件,运行 npx tgcloud push,然后平台会接管一切。您的机器人的流量由已部署的模块处理;您的数据库在调用之间持久存在。

一个项目只有四种代码,全部位于一个 tgcloud/ 文件夹内:

tgcloud/
├─ endpoints/  # 您的 Mini App 调用的函数
├─ handlers/   # 对 Telegram 更新类型做出反应的函数
├─ lib/        # 您从任何地方导入的共享代码
└─ schema.js   # 您的数据库表

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

Mini App 是可选的。如果您的机器人有 Mini App,其前端将位于同一项目中,紧邻 tgcloud/。Serverless 会托管其构建输出并运行它调用的 endpoints/。请参阅 Mini App 前端 。

快速演示

这是一个完整的、可工作的演示机器人。它会回复每条消息,并记住它从每个聊天中看到的消息数量。

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、table() DSL — 都将在下面的部分中进行描述。

Serverless 是 Telegram 机器人和 Mini App 的通用后端,而不是一种特定类型应用的模板。它非常适合:

  • 会话式 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/
├─ tgcloud/             # 平台运行的所有内容
│  ├─ handlers/
│  │  └─ message.js     # 一个入门的消息处理程序 (回显文本)
│  ├─ lib/              # 您的共享模块将在此处 (开始时为空)
│  └─ schema.js         # 您的数据库表
├─ docs/
│  └─ tgcloud-sdk.md    # SDK 参考 (供您和您的 AI 工具使用)
├─ AGENTS.md            # AI 编码助手的使用指南
├─ package.json
└─ tgcloud.jsonc        # 项目配置

生成的项目文件都是自文档化的 — 每个文件都包含注释示例,说明您接下来可以做什么。

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 标签补全所需要的。无论哪种方式,您都将获得相同的项目。

构建 Mini App? 从其前端开始。使用您常用的工具创建它,然后在该文件夹中运行项目创建器。请参阅 Mini App 前端 。

2. 关联您的机器人

每个项目都绑定到一个机器人。使用 login 命令进行关联,它会要求您提供 CLI 访问令牌 ( @BotFather → 您的机器人 → Serverless → CLI Access → Access token — 一个独立于您 机器人 API 令牌 的令牌) 并将其本地保存:

npx tgcloud login

令牌的格式为 app<id>:<secret>。CLI 会将其保存在 .tgcloud/ 中 (这是一个 .gitignore 文件) ,并且永远不会打印 secret 部分。Login 是唯一一次要求您提供它的地方 — 请参阅 Authentication 以了解令牌如何在 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 会引导您完成更改,并在您确认后创建表。此两步模型 — 以及风险更高的更改 (如下 drops) 会发生什么 — 在 Migrations 中进行了介绍。

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 — 您永远不会无声地覆盖别人的工作。请参阅 Staying in sync 。

使用 AI 构建

更喜欢与 AI 助手一起构建 — 或者您团队中唯一的编码员是 AI?您仍然可以发布一个机器人。我们已迈出第一步,让 AI 代理在一个项目中感觉宾至如归:每个新项目都带有 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 管理的所有内容:

  • Handlers — 创建、编辑和测试运行更新处理程序;BotFather 会使 webhook 与您拥有的处理程序保持同步 (与 CLI 报告的“In sync”/“Out of sync”相同)。
  • Endpoints — 您的 Mini App 调用的函数。
  • Library — 您的共享 lib/ 模块。
  • Database — 使用相同的 Drizzle 风格语法编辑 schema.js,审查待定更改,并应用它们;Save 会部署。
  • Mini App Files — 用户在 Telegram 中打开的前端。
  • CLI Access — 当您回到键盘旁时,在此处获取 CLI 访问令牌。

这是一个相同的云项目,因此您可以从手机上开始一个处理程序,然后稍后在笔记本电脑上使用 npx tgcloud pull — 没有任何东西绑定到单个客户端。运行处理程序甚至会直接在聊天中显示其控制台输出,就像 npx tgcloud run 一样。

项目和模块

Serverless 项目是一个普通的受版本控制的文件夹。平台运行的所有内容都位于一个文件夹 tgcloud/ 中;项目的其余部分是您的 — Mini App 前端、配置、文档。机器人没有构建步骤,运行时没有 node_modules,也没有服务器入口点。

项目结构

example_bot/
├─ tgcloud/             # 已部署的部分 — 其他任何内容都不会
│  ├─ handlers/         # 更新处理程序 — 平坦的,只有一层
│  │  ├─ message.js
│  │  └─ callback_query.js
│  ├─ endpoints/        # 您的 Mini App 调用的函数 — 平坦的,只有一层
│  │  └─ getProfile.js
│  ├─ lib/              # 共享模块;允许子目录
│  │  ├─ reply.js
│  │  └─ internal/util.js
│  └─ schema.js         # 数据库模式 — 单个文件
└─ .tgcloud/            # CLI 状态,已 git-ignore

只有 tgcloud/ 内的 .js 文件 — 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 单独应用。请参阅 The database 。

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 )。

一个更新类型只有在其处理程序文件存在且非空时才会被处理。如果不存在 handlers/<type>.js — 或者文件是空的 — 该类型的更新将被忽略,平台也不会为它们运行任何代码。因此,只保留您实际需要的处理程序:您添加的每个处理程序都是您的机器人会唤醒处理的又一个更新类型,而忽略其余的则意味着 Telegram 不会为那些您只会丢弃的更新启动您的代码。

要生成新的处理程序,请运行 npx tgcloud add handlers/<type> 。

endpoints/ — 您的 Mini App 的服务器端。每个文件都是 Mini App 按名称调用的函数,因此 endpoints/getProfile.js 响应 POST /api/getProfile。平台会在您的代码运行前检查该调用是否来自您的 Mini App 中的 Telegram 用户,并将该用户传递给您。endpoints/ 是平坦的,就像 handlers/ 一样。请参阅 Endpoints 。

.tgcloud/ — 由 CLI 完全管理的本地机器状态,包括您保存的凭据。它已 git-ignore,您不应该手动读取或写入它 — 请改用 CLI 命令。

模块系统

在运行时,一个模块可以看到平台 SDK 和项目中的其他模块。没有 npm 包,没有文件系统,除了通过 SDK 的 fetch 之外没有网络。

像在任何 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) — 整个平台表面,请参阅 The SDK — 以及您自己的模块在 tgcloud/ 中。这是完整的列表。如果您的代码 import 了其他任何东西,它将无法解析。此限制确保了模块的加载速度快且运行安全。

处理程序 (Handlers)

处理程序是 handlers/ 目录中的一个模块,当匹配的更新到达时,平台会调用其默认导出。

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

export default async function (message) {
  await api.sendMessage({
    chat_id: message.chat.id,
    text: `你说:${message.text ?? '(没有文本)'}`,
  });
}

处理程序接收更新的有效负载作为其参数 — 平台会为您解包 Telegram Update 。handlers/message.js 接收 Message (即 update.message);handlers/callback_query.js 接收 CallbackQuery ;以此类推。处理程序的第二个参数是一个每个调用上下文对象 ctx。它将原始 Update 作为 ctx.update — 当您需要有效负载之外的内容时,例如 update_id。

处理程序可以 async (通常是) 并且可以返回值。它通过 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 。

端点 (Endpoints)

端点是 endpoints/ 目录中的一个模块,供您的Mini App 调用 — 这是前端自身无法完成的任何事情的地方,例如读取和写入数据库,调用 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 是 Mini App 发送的 JSON 对象。第二个参数 ctx 包含**ctx.initData** — Mini App 的 init data (user、chat、start_param、auth_date 等) ,已由平台验证。它的形状与客户端上的 Telegram.WebApp.initDataUnsafe 相同,但在这里您可以信任它。没有有效 init data 的调用会在您的代码运行前被拒绝 (返回 401) ,因此 ctx.initData.user 确实是打开了您的 Mini App 的用户。端点只能通过这种方式调用 — 从您的机器人的 Mini App 调用,由 Telegram 用户发起 — 因此没有公开 API 需要保护,也没有登录需要构建。

端点返回的任何内容都将作为 JSON 返回。要拒绝调用,请 throw new EndpointError(description, parameters?)。Mini App 将获得描述和 parameters 对象。如果您需要在前端基于错误类型进行分支,请在 parameters 中放置一个代码 — { code: 'OUT_OF_STOCK' }。任何其他异常都将报告为通用的内部错误,实际消息仅发送到您的日志,因此内部错误不会泄露给客户端。

从 Mini App 调用端点

Mini App 使用官方 Mini Apps SDK 中的 Telegram.WebApp.Serverless.call 从官方 Mini Apps SDK 调用端点,telegram-web-app.js — 这是您的 Mini App 已经包含的脚本。它会为您发送 init data,并根据错误或端点结果进行回调:

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' 当 init data 缺失或无效时。

端点名称类似于 JavaScript 标识符 — 字母、数字和 _,不以数字开头 (getProfile、place_order)。使用 npx tgcloud add endpoints/<name> 生成一个,并使用 run 在没有 Mini App 的情况下进行测试,自己传递 init data:

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

部署内容

当您运行 npx tgcloud push 时,CLI 会收集 schema.js、lib/、handlers/ 和 endpoints/ 下的所有 .js 文件,并将该精确集合作为您项目的模块空间 — 以及 Mini App 的构建文件夹 (如果 tgcloud.jsonc 中指定了名称)。在云端存在的但不在您项目中的任何内容都会被删除,因此已部署状态始终反映您的文件夹 — 包括删除的内容。tgcloud/ 之外的任何内容都不会被部署为代码 — 项目根目录的其余部分是您的。在 tgcloud/ 内部,不在这些位置的 .js 文件 (tgcloud/util.js、tgcloud/helpers/x.js) 会被标记,因此它们不会被无声地漏掉。Markdown、点文件和 .tgcloud/ 永远不会被部署。

Mini App 前端

项目根目录是您的,也是您 Mini App 前端的自然位置 — 使用 React、Vue、Svelte 或纯 HTML 构建,使用 Vite 或任何其他打包器。Serverless 会在其旁边托管其构建输出。一次 push 会同时部署代码和静态文件,并在 https://app<app_id>.tgcloud.ai/ 上提供该应用程序 (push 会打印确切地址 — 将其用作您在 @BotFather 中的 Mini App URL)。

举报滥用。 tgcloud.ai 上的 Mini App 由独立开发者发布。要举报 app<app_id>.tgcloud.ai 子域上的非法内容或滥用行为,请发送电子邮件至 [email protected] 。

要开始,请先创建前端,使用您喜欢的任何工具,然后在其之上添加 Serverless:

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

项目创建器从不覆盖您的文件。它会在前端旁边添加 tgcloud/ 文件夹,识别构建文件夹,并在 package.json 中添加一个 deploy 脚本 (构建,然后推送) — 仅当您还没有 deploy 脚本时。请参阅 Mini App 前端 。

init 拒绝在另一个项目内部创建项目。在项目自己的根目录中重新运行它也没问题,只会添加缺失的内容。

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 会上传磁盘上完全相同的文件夹 — 除了以点开头的文件和文件夹会被跳过 — 并且永远不会运行您的构建,因此在编辑前端后运行 npx tgcloud push 会部署上一个构建。使用 npm run deploy (它会运行您的 build 脚本,然后是 tgcloud push) ,或自行构建。

服务选项:

  • spa — 为不存在且没有文件扩展名的路径提供 index.html,以便客户端路由可以在重新加载时工作。具有扩展名的路径仍然会得到 404。
  • immutable — 用于内容哈希文件的模式 (Vite 的 /assets/*),浏览器可以缓存一年。仅用于内容哈希文件。匹配此处的任何其他文件将导致浏览器缓存过时一年,并且无法撤回。
  • headers — 匹配规则的路径的响应头 (精确路径,或以 /* 结尾的前缀)。null 值会移除一个头。
  • redirects — 重定向规则,第一个匹配获胜。现有文件始终优先于规则。status 为 301,除非您另行设置。
  • "static": "dist" — 当您不需要其他东西时,{ "source": "dist" } 的简写。
  • "static": false — 在下一次推送时移除已部署的站点。删除该键只会停止此项目管理静态文件,并且已部署的内容将保留。

该站点从其自己的主机根目录提供服务,因此您的打包器的默认 base (/) 是正确的。Telegram Web 会在 iframe 中打开 Mini App,因此请勿添加 X-Frame-Options 或限制性的 frame-ancestors 策略。

同一主机在 /api/ 下响应您的 端点 — 这是 Telegram.WebApp.Serverless.call('getProfile', …) 发送的位置。不要在构建文件夹的 /api/ 下放置您自己的文件。

数据库

每个机器人都有自己的数据库 — 一个 SQLite 支持的存储,它在调用之间持久存在,并且可以通过 db 供所有模块访问。您使用一个小的、类型化的 DSL 在 schema.js 中描述您的表;您使用一个流畅的查询生成器来读取和写入它们;并且您通过审查过的迁移来演进它们。

了解 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 seconds) Date
timestamp_ms INTEGER (unix ms) 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' }) 是 json() 的 BLOB 等价物 — 相同的 JSON 编码,存储在 BLOB 列中而不是 TEXT 中。

TLDR: 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')   // 标记列以供移除

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})`) ,          // 表达式索引
  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。

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

// 自定义投影: { 别名: 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) 是用于 .select({ … }) 投影的 SQL 片段。

原始 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,如果 SQL 有 RETURNING 子句,则会包含该子句。当查询投影两列同名时 (如 SELECT u.id, o.id 的连接):db.all 按列名键入行,第二个 id 将覆盖第一个。使用 db.values。

sql`…` 标签将 ${value} 转换为绑定参数,将 ${table.column} 转换为标识符,并拼接嵌套的 sql 片段。使用 sql.raw('…') 来创建无参数的字面量。

原始查询不绑定到表,因此它们的行会按原样返回,没有模式转换 — 布尔值为 0/1,JSON 列为字符串,时间戳为数字。只有绑定到表的生成器才会转换值。

迁移

您的数据库会随着您的机器人成长而变化。平台通过分离部署代码和更改数据,以及根据风险程度对每个模式更改进行分类,来确保安全。

部署永远不会触及数据库。 当您 npx tgcloud push 一个更改的 schema.js 时,平台会记录新模式并告诉您数据库将如何更改 — 但不应用任何内容:

npx tgcloud push       # 部署 schema.js;报告待定 DB 更改
npx tgcloud migrate    # 审查并应用它们

npx tgcloud migrate 计算 schema.js 与实时数据库之间的差异,然后一步一步地引导您完成,其中包含一个运行中的 [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) 在 The database 中介绍;本节介绍 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 → The database
api Telegram Bot API — api.sendMessage(...) → 下方
fetch 出站 HTTP → 下方
InputFile 文件 (字节 + 名称) 上传 — 适用于 api 和 fetch → Files
EndpointError 使用 Mini App 可以显示的描述来拒绝其调用 → Endpoints

按相对路径使用 .js 扩展名导入您自己的项目模块 (from '../schema.js', from '../lib/cart.js')。请参阅 The module system 。

Bot API

api 使您能够使用整个 Telegram Bot API 。将任何方法调用为 api.<method>(params)。所有当前 — 和未来的 — Bot API 方法都可以无需更新 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 (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 = 消息已不存在;这里没关系。
  } else {
    throw e;
  }
}

文件

使用字节而不是仅使用 file_id — 上传新文件并下载现有文件。

上传 — InputFile。 使用 InputFile (一个简单的容器:字节、文件名和可选的 MIME type) 包装原始字节,并将其传递给 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' }) },
]});

字节会以真正的 multipart 上传方式流式传输到 Telegram — 文件不会整个缓冲到对平台的单个调用中 — 但 Telegram 的 Bot API 限制仍然适用:目前一个机器人可以上传50 MB 的文件 (照片为10 MB)。

下载 — 按 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()

Telegram 的 Bot API 将 getFile 下载限制为 20 MB,因此这是两个助手的上限。

HTTP

fetch 是一个 fetch-like 客户端,用于调用外部世界 — 第三方 API、Webhooks、任何通过 HTTP 的东西。

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()) 和 body 阅读器 await res.json() / await res.text()。

您也可以将 body 逐步读取为流 — for await (const chunk of res.body) { … } — 这是您如何处理大型事件或从AI API 接收逐个 token 的输出。

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

请求体也可以携带文件和原始字节:将 Uint8Array、InputFile 或 FormData (用于 multipart/form-data) 作为 body 传递,字节将被流式传输 — 因此您可以通过与发送文件到 Telegram 相同的方式上传到第三方 API:

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 MB。该限制涵盖整个响应 — 使用 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 将更改的模块和 Mini App 的构建部署到云端
migrate 将模式更改应用到数据库
run 在不部署的情况下在平台上执行模块
fetch 检查已部署的内容,而不更改本地文件
pull 将本地文件与云端同步
reset 丢弃本地更改;从云端状态恢复
webhook 检查和重新同步平台管理的 webhook
completion 打印 shell 补全脚本 (bash/zsh/fish)
upgrade 将 0.2.0 之前的项目布局移至 tgcloud/

身份验证

项目通过其令牌与一个机器人关联,令牌格式为 app<id>:<secret>。app<id> 部分是公开的,可能会显示;secret 永远不会出现在日志或错误中。

令牌的解析顺序如下:

  1. TGCLOUD_TOKEN 环境变量 — 用于 CI;永不写入磁盘。
  2. .tgcloud/credentials — 由 npx tgcloud login 写入。
  3. 以上两者都不是 → 错误提示您执行 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 指向构建文件夹,并在您的 package.json 中添加一个 deploy 脚本 (构建,然后推送) — 仅当您尚无 deploy 脚本时。请参阅 Mini App 前端 。

init 拒绝在另一个项目内部创建项目。在项目自己的根目录中重新运行它也没问题,只会添加缺失的内容。

add

npx tgcloud add <target>

生成单个新模块,已设置好并可编辑 — 处理程序、端点或 lib/ 模块。

npx tgcloud add handlers/callback_query   # 一个新的更新处理程序
npx tgcloud add endpoints/getProfile      # 一个新的 Mini App 端点
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 。

生成的文件的默认导出是活动的,因此一旦部署,处理程序就会激活 — 无需取消注释。使用 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 时的云端状态进行比较。它还会警告 tgcloud/ 内部的 stray .js 文件,这些文件不会被部署。

对于带有 Mini App 的项目,status 还会显示其构建文件夹自上次部署以来发生的变化。

diff

npx tgcloud diff

与 status 类似,但显示已更改模块的实际逐行差异。同样是离线操作。

push

npx tgcloud push [files...]

将您的项目原子化地部署到云端。

不带参数时,它会部署整个项目,已部署状态将精确反映您的文件夹 — 本地删除的模块将在云端被移除。

使用文件或目录参数 (npx tgcloud push tgcloud/handlers/message.js、npx tgcloud push handlers/) 时,它仅部署这些文件中的更改。目标推送永远不会删除其他模块。目标可以按文件路径 (tgcloud/handlers/message.js)、tgcloud/ 内的路径 (handlers/message.js) 或模块名称 (handlers/message) 拼写 — 它们都表示同一个模块。

其唯一选项是 --force — 跳过并发检查并覆盖云端的所有内容。仅在您确定时使用 (请参阅 Staying in sync )。

如果 tgcloud.jsonc 指定了静态文件夹,则完整的 push 会在同一批次中部署它 — 仅上传已更改的文件 — 并打印站点的地址。先构建,因为 push 永远不会运行您的构建 (npm run deploy 会同时执行两者)。npx tgcloud push dist/ 仅部署静态文件,而命名仅为模块的推送则不会触及它们。

部署后,如果 schema.js 已更改且数据库不同步,push 会打印待定更改的摘要,并建议 npx tgcloud migrate。它永远不会自行应用它们。

migrate

npx tgcloud migrate

将您的模式更改应用到数据库。它计算 schema.js 与实时数据库之间的差异,然后一步一步地引导您完成,其中包含运行中的 [N/M] 计数器:

  • 所有待定更改的简要摘要。
  • 安全更改,在您确认后一次性应用。
  • 警告 (删除、慢操作),一次一个,每次单独确认。
  • 手动更改,显示原因和建议的操作,不自动应用。
  • 未记录的对象 (在数据库中但不在您的模式中),显示供参考。

它以摘要结束:已应用、跳过、等待手动修复或不在模式中。

请参阅 Migrations 以了解模型。

选项:--dry-run (打印所有内容,不应用任何内容)、--safe (自动应用安全更改,跳过警告)、--yes (自动应用安全更改和所有警告,跳过手动 — 谨慎使用)、--local (与本地 schema.js 进行 diff 而不是已部署的)。如果没有标志,migrate 需要一个终端并在非交互式环境中出错,而不是猜测。

run

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

在不部署的情况下,使用您当前本地文件在平台上执行处理程序或端点。这是测试逻辑的快速内部循环。

  • <module> — 一个裸名称 (在 handlers/ 和 endpoints/ 下搜索) 或一个路径,如 handlers/message。在两个目录中都存在的裸名称必须使用其目录拼写。
  • [args] — 传递给处理程序的有效负载,以 JSON5 格式编写,因此您可以省略键的引号。它是您的处理程序接收的更新类型对象 (例如,handlers/message 的消息) ,或者是端点的 input,并且它必须是一个对象 — 不是数组或裸字符串。
  • --ctx <json5> — 模块的上下文对象 (其第二个参数) ,也是 JSON5 格式。使用它来提供您的代码从 ctx 读取的内容 — 处理程序的原始更新,--ctx '{ update: { update_id: 1 } }',或者是端点的 init data,--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 从不写入您的 Mini App 的构建文件夹。

reset

npx tgcloud reset

丢弃您的本地更改,并将模块从上次已知的云端状态恢复。使用它来丢弃一个实验。Mini App 的构建文件夹不会被触及。

webhook

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

Telegram 通过 webhook 将更新传递到您的机器人,该 webhook 由平台为您管理 — 您永远不需要手动指向它。npx tgcloud webhook 会显示其当前状态:URL、allowed_updates 列表、待处理更新的数量、最后一个传递错误 (如果有) ,以及它是否与您已部署的处理程序同步。

“同步”意味着 webhook 指向平台,并且其 allowed_updates 与您已部署的处理程序匹配 — 因此 Telegram 只传递您处理的更新类型,而没有其他。部署新处理程序 (或删除一个) 可能会导致 webhook 不同步,直到它被刷新;npx tgcloud status 也会标记这一点。

npx tgcloud webhook sync 会修复它 — 它会将 webhook 指向平台,并根据您已部署的处理程序重建 allowed_updates。添加 --drop-pending 以丢弃 Telegram 在 webhook 恢复正常后已排队的更新 (否则它们会在 webhook 恢复正常后立即传递)。

completion

注意: 标签补全仅在裸 tgcloud 位于您的 PATH 时才有效 — 因此请全局安装它 (npm install -g @tgcloud/cli) 或以其他方式将二进制文件放在您的 PATH 上。它无法与 npx 集成。

tgcloud completion <bash|zsh|fish>

将 shell 补全脚本打印到 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

之后重新启动 shell (或重新加载文件)。不带 shell 参数运行 tgcloud completion 会再次打印这些说明。

upgrade

npx tgcloud upgrade [--dry-run]

一次性迁移适用于 CLI 0.2.0 之前的项目,这些项目将 schema.js、lib/ 和 handlers/ 保留在项目根目录。它会将它们移动到 tgcloud/ 中。您与机器人的连接将保留,并且无需重新部署任何内容。--dry-run 会在不触碰任何内容的情况下打印计划的移动。其他命令在您升级之前不会在旧布局上运行。

Staying in sync

每个项目在云端都有一个单调递增的修订号,每次部署都会更新。CLI 会记住它上次同步的修订号,并在每次 push 时发送。如果云端已更新 — 因为另一台机器或同事进行了部署 — 则 push 将被拒绝,而不是无声地覆盖他们的工作,CLI 会提供三种前进方式:

npx tgcloud fetch           # 查看云端发生了什么变化,然后重新检查
npx tgcloud pull            # 将最新内容同步到您的工作文件中
npx tgcloud push --force    # 覆盖云端状态 (危险)

这种乐观并发检查使您可以在没有锁定部署流程的情况下在团队之间共享机器人。命令在失败时 (被拒绝的部署、失败的迁移、身份验证错误、run 期间抛出的模块) 以非零退出码退出,因此它们可以干净地组合到脚本和 CI 管道中。