Telegram Serverless 讓您直接在 Telegram 的基礎設施上執行機器人和 Mini App 的後端程式碼 — 無需配置伺服器、無需維持容器運行、無需考慮擴展。您編寫純 JavaScript 模組,只需一個命令即可部署,Telegram 會在一個快速、獨立的 V8 沙箱中執行它們,該沙箱緊鄰 Bot API 和內建資料庫。

如果您曾經將機器人連接到 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 是可選的。如果您的機器人有一個,它的前端將位於同一個專案中,位於 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/ 中(該資料夾會被 git 忽略),並且絕不會列印 secret 部分。Login 是唯一一次您被要求提供它的時候 — 請參閱 身份驗證 了解權杖如何在 CI 中解析。

3. 瀏覽

兩個命令可以隨時告訴您目前的情況,兩者都完全離線:

npx tgcloud status     # 本地與已部署副本之間的差異
npx tgcloud diff       # 行對行的變更

init 之後,一切都是新的,並且還沒有任何東西被部署。status 會顯示等待上傳的入門檔案。

4. 部署

將您的模組發送到雲端:

npx tgcloud push

push 會一次性原子化上傳所有已變更的模組,並更新您本地關於雲端目前持有內容的記錄。您的機器人已上線:在 Telegram 中打開它並發送一條訊息 — 入門處理器會將其回顯回來。

部署永遠不會觸及您的資料庫。 推送程式碼和更改資料庫結構是故意分開的步驟,因此程式碼部署永遠不會讓您對資料遷移感到意外。這就是下一步的內容。

5. 添加資料庫表格

讓我們讓機器人記住一些東西。打開 tgcloud/schema.js 並宣告一個表格:

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

export const messages = table('messages', {
  id:      integer('id').primaryKey({ autoIncrement: true }) ,
  chatId:  integer('chat_id').notNull() ,
  text:    text('text') ,
  created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`) ,
});

部署結構,然後將其應用到資料庫:

npx tgcloud push       # 上傳新的結構
npx tgcloud migrate    # 創建 `messages` 表

push 會報告結構不同步並向您顯示待處理的變更,但不會應用任何內容。migrate 會引導您完成變更,在您確認後創建表格。這個兩步模型 — 以及發生更危險的變更(如刪除)時會發生什麼 — 在 遷移 中涵蓋。

6. 儲存和讀取資料

現在從您的處理器中使用該表格。編輯 tgcloud/handlers/message.js:

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

export default async function (message) {
  // 儲存此訊息。
  await db.insert(messages)
    .values({ chatId: message.chat.id, text: message.text })
    .run();

  // 計算此聊天已儲存了多少條訊息。
  const count = await db.$count(messages, eq(messages.chatId, message.chat.id));

  await api.sendMessage({
    chat_id: message.chat.id,
    text: `已儲存。此聊天到目前為止共有 ${count} 條訊息。`,
  });
}

使用 npx tgcloud push 部署更新後的處理器,然後向您的機器人發送幾條訊息,觀察計數器的攀升。資料庫在調用之間持久化 — 這就是您機器人的記憶。

7. 無需部署即可測試

您不必部署即可嘗試變更。npx tgcloud run 在平台上使用您的本地檔案執行處理器,而無需發布它們:

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

引數是您的處理器收到的負載 — 對於 handlers/message,是一個 Message — 以 JSON5 編寫(因此您可以省略鍵的引號)。該命令會列印處理器通過 console.* 記錄的任何內容、返回值以及耗時。這是迭代邏輯的最緊密循環 — 無需部署,無需等待真實訊息。

8. 保持同步

在您工作時,有幾個命令可以讓您的本地專案和雲端保持一致:npx tgcloud status 顯示變更內容,npx tgcloud push 部署,npx tgcloud pull 將您的本地專案與雲端對齊,npx tgcloud fetch 在不修改您的檔案的情況下檢查已部署內容,以及 npx tgcloud reset 丟棄本地變更。

如果兩個人(或兩台機器)部署到同一個機器人,平台會檢測到衝突,push 會停止,讓您先 pull — 您永遠不會無聲地覆蓋別人的工作。請參閱 保持同步 。

使用 AI 進行建置

偏好使用 AI 助理進行建置 — 或者您團隊中唯一的程式設計師是 AI?您仍然可以發布機器人。我們已經採取了第一步,讓 AI 代理在專案中感到賓至如歸:每個新專案都生成了 AGENTS.md 和 docs/tgcloud-sdk.md 參考,AI 編碼工具會自動讀取它們。

結合一個小型、自給自足的運行環境 — 一個 SDK,沒有需要處理的 npm 套件 — 這為助理提供了一個良好的開端,解決了通用程式碼生成器在這裡經常忽略的約定:相對匯入,使用 .js,沒有外部鍵,每個 db 調用都是異步的,每個更新類型一個處理器,以及兩步 push/migrate 流程。

嘗試一下:

npm create @tgcloud/bot my-bot
cd my-bot
opencode            # 或 Claude Code, Cursor, … — 任何讀取 AGENTS.md 的代理

然後只需用簡單的語言提問:

編寫一個記住每個人待辦事項清單的機器人 — 當他們發送文字時添加項目,當他們發送 /list 時顯示整個清單。

助理會為您編輯 schema.js 和處理器;您進行審查,使用 npx tgcloud run 即時測試變更,然後使用 npx tgcloud push 和 npx tgcloud migrate 上線。AGENTS.md 是您專案的一部分 — 在機器人成長時編輯它,以便指導保持準確。

與 BotFather 一同行動

只剩下一部手機?整個專案也存在於 @BotFather 中 — 打開您的機器人 → Serverless,您將獲得 CLI 管理的一切,透過觸控螢幕:

  • 處理器 — 創建、編輯和測試運行更新處理器;BotFather 會將 webhook 與您擁有的處理器保持同步(與 CLI 報告的相同 同步 / 不同步)。
  • 端點 — 您的 Mini App 調用的函數。
  • 函式庫 — 您的共享 lib/ 模組。
  • 資料庫 — 以相同的 Drizzle 式語法編輯 schema.js,審查待處理的變更,並應用它們;儲存 會部署。
  • Mini App 檔案 — 您的用戶在 Telegram 中打開的前端。
  • CLI 訪問權杖 — 當您回到鍵盤時,在此處獲取 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 忽略

僅 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 分開應用。請參閱 資料庫 。

lib/ — 共享程式碼,任何您想在處理器之間重用的內容:純輔助函數、資料庫訪問層、格式化、與外部服務的整合。lib/ 是唯一可以包含子目錄 (lib/internal/util.js、lib/payments/stripe.js) 的目錄,因此您可以隨意組織大型程式碼庫。lib/ 中的模組從未被平台直接調用;它們的存在是為了被處理器和彼此匯入。

handlers/ — 您機器人的進入點。每個檔案對應一個Telegram 更新類型,平台將每個傳入的更新路由到匹配的處理器:

檔案 處理
handlers/message.js 新進訊息
handlers/inline_query.js 新進 內聯 查詢
handlers/callback_query.js 新進回調查詢
…… 任何其他 Bot API 更新類型

handlers/ 是扁平的 — 沒有子目錄。處理器的 export default 是平台調用的函數(請參閱 處理器 )。

只有當其處理器檔案存在且非空時,才會處理某種更新類型。如果沒有 handlers/<type>.js — 或者檔案為空 — 該類型的更新將被忽略,平台也不會為它們運行任何程式碼。因此,請僅保留您實際需要的處理器:您添加的每一個都是您的機器人會喚醒處理的另一個更新類型,而省略其餘的意味著 Telegram 不會為您將僅捨棄的更新啟動您的程式碼。

要生成新的處理器,請執行 npx tgcloud add handlers/<type> 。

endpoints/ — 您的 Mini App 的伺服器端。每個檔案都是 Mini App 按名稱調用的函數,因此 endpoints/getProfile.js 回應 POST /api/getProfile。平台會在您的程式碼運行前檢查該調用是否來自您的 Mini App 中的 Telegram 使用者,並將該使用者交給您。endpoints/ 是扁平的,就像 handlers/ 一樣。請參閱 端點 。

.tgcloud/ — 由 CLI 完全管理的機器本地狀態,包括您保存的憑證。它被 git 忽略,您不應該手動讀寫它 — 請使用 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) — 整個平台介面,請參閱 SDK — 以及您自己的模組在 tgcloud/ 中。這就是完整列表。如果您的程式碼 import 了其他任何東西,它將無法解析。此限制是讓模組載入快速且安全執行的原因。

處理器

處理器是 handlers/ 中的一個模組,當匹配的更新到達時,平台會調用其預設匯出。

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

export default async function (message) {
  await api.sendMessage({
    chat_id: message.chat.id,
    text: `您說了:${message.text ?? '(無文字)'}`,
  });
}

處理器接收更新的負載作為其參數 — 平台為您解開 Telegram Update 。handlers/message.js 接收 Message (即 update.message);handlers/callback_query.js 接收 [CallbackQuery]( https://core.telegram.org/bots/api#callbackquery );依此類推。處理器的第二個參數是一個每次調用的上下文對象 ctx。它將原始 Update 作為 ctx.update 攜帶 — 當您需要負載之外的內容時(例如 update_id)可以訪問它。

您無需真實更新即可測試處理器。npx tgcloud run 在平台上執行它,並使用您提供的負載和您當前的本地檔案:

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

引數是負載 — 與您的處理器收到的對象相同 — 以 JSON5 格式。要提供處理器的 ctx(其第二個參數),請添加 --ctx,例如 --ctx '{ update: { update_id: 1 } }'。這會針對您的本地檔案運行,因此您可以在部署之前嘗試變更。請參閱 run 。

端點

端點是 endpoints/ 中的一個模組,由您的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 調用按名稱命名的端點,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 在您的端點拋出 EndpointError 時為 'ENDPOINT_ERROR'。任何其他值表示平台拒絕了調用或端點意外失敗,例如 '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 或任何其他 bundler 建置。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 前端 。

託管的內容設定在**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 — 在下一次推送時移除已部署的網站。刪除該鍵而不是僅僅停止此專案管理靜態檔案,任何已部署的內容將保留。

該網站從其自己的主機根目錄提供服務,因此您的 bundler 的預設 base(/)是正確的。Telegram Web 會將 Mini App 開啟在 iframe 中,因此請勿添加 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 秒) Date
timestamp_ms INTEGER (unix 毫秒) Date
bytes BLOB Uint8Array

boolean() 和 json() 分別是 integer(name, { mode: 'boolean' }) 和 text(name, { mode: 'json' }) 的簡寫。

blob() 讀寫 Uint8Array — 運行時沒有 Node Buffer(Buffer 是 Uint8Array 的子類,所以這是可移植的基礎類型)。上面的 mode 僅控制值如何編碼,與欄位的儲存類型無關:因此 blob('col', { mode: 'json' }) 是 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' })  // 或 'virtual'
text('email').deprecated('replaced by login')   // 標記欄位為待刪除

json() 欄位上的 .default() 會為您編碼值;sql… 的預設值會按原樣傳遞。.deprecated() 是終結的 — 請參閱 遷移 。

索引和約束

索引和約束在 extras 回調中聲明,其中欄位在範圍內。

table('t', { /* … */ }, (t) => ({
  uq:     unique('uq_email').on(t.email) ,
  chk:    check('chk_done', sql`${t.done} in (0, 1)`) ,
  idx:    index('idx_name').on(t.col) ,
  uidx:   uniqueIndex('uidx_email').on(t.email) ,
  lower:  index('idx_lower').on(sql`lower(${t.email})`) ,          // 表達式索引
  active: index('idx_active').on(t.userId).where(sql`done = 0`) ,  // 部分索引
}));

表級修飾符鏈接到 table(...) 之後:.strict()、.withoutRowid()、.deprecated('reason')。

無外部鍵

運行時 PRAGMA foreign_keys 關閉。聲明的外部鍵將被靜默無效 — 沒有級聯,沒有孤兒保護 — 這比沒有還糟,因此 DSL 不允許這樣做。也就是說,.references() 和表級 foreignKey() 在聲明時會拋出,使用它們的結構將無法部署。

您應該使用普通欄位(userId: integer('user_id'))來模擬關係,並在您的應用程式程式碼中強制執行完整性:先插入父項再插入子項,先刪除子項再刪除父項,妥善處理錯誤,並在需要時使用 LEFT JOIN … WHERE parent.id IS NULL 來清理孤兒。

lack of foreign keys 是一個刻意的限制,而不是疏忽。在規劃您的機器人時,請儘早考慮到這一點。

查詢

db 是一個流暢的查詢建構器。每個查詢都是異步的 — 終端方法(.all()、.get()、.values()、.run())返回 Promises,因此始終使用 await。

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

await db.select().from(todos).all();                          // 所有行
await db.select().from(todos).where(eq(todos.id, 1)).get();   // 第一行或 null
await db.select().from(todos).values();                       // 行作為值陣列

await db.select().from(todos)
  .where(and(eq(todos.userId, uid) , eq(todos.done, false)))
  .orderBy(desc(todos.priority) , asc(todos.id))
  .limit(10).offset(20)
  .all();

// 自定義投影:{ 别名: 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 子句,則 rows 將攜帶它。當查詢投影兩個同名欄位時(如 SELECT u.id, o.id 的連接),請使用 db.values:db.all 按欄位名稱鍵入行,因此第二個 id 會覆蓋第一個。

sql`…` 標籤將 ${value} 轉換為綁定參數,將 ${table.column} 轉換為標識符,並拼接嵌套的 sql 片段。使用 sql.raw('…') 創建一個沒有參數的字面量。

原始查詢不綁定到表格,因此它們的行返回時沒有模式轉換 — 布林值是 0/1,JSON 欄位是字串,時間戳記是數字。只有基於表格的建構器才會轉換值。

遷移

隨著您的機器人成長,您的資料庫也會發生變化。平台通過將部署程式碼與更改資料分開,並根據其危險程度對每個結構變更進行分類,來確保其安全。

部署永遠不會觸及資料庫。 當您 npx tgcloud push 已更改的 schema.js 時,平台會記錄新的結構並告知您資料庫將如何更改 — 但不會應用任何內容:

npx tgcloud push       # 部署 schema.js;報告待處理的資料庫變更
npx tgcloud migrate    # 審查並應用它們

npx tgcloud migrate 會計算您的結構與實時資料庫之間的差異,然後逐步引導您完成,每次一個步驟,帶有運行中的 [N/M] 計數器:

  • 所有待處理變更的簡要摘要。
  • 安全變更,在您確認後一起應用。
  • 警告(刪除、慢速操作),一次一個,每個單獨確認。
  • 手動變更,顯示原因和建議操作,不會自動應用。
  • 未記錄的對象(存在於資料庫但不在您的結構中),顯示以供了解。

它以摘要結束:已應用、已跳過、等待手動修復或不在結構中。

請參閱 migrate 了解標誌(--dry-run、--safe、--yes、--local)。

刪除項目

從 schema.js 中刪除表格或欄位不會刪除它們 — 這會使意外刪除變得災難性。要刪除某個內容,請將其標記為已棄用:

// 刪除欄位
text('email').deprecated('replaced by login')

// 刪除整個表格
export const oldSessions = table('old_sessions', { /* … */ }).deprecated('unused');

在下一次 migrate 時,已棄用的對象將顯示為一個警告狀態的刪除,您需要單獨確認。一旦它消失,請移除聲明。

更改欄位類型

類型更改是手動的 — SQLite 並非總是能就地完成,並且將現有值強制轉換是一個判斷問題。migrate 將顯示變更及其原因;您可以通過原始 SQL (db.run(...)) 手動執行,通常是創建一個新欄位或表格,複製數據,然後交換。

SDK

在運行時,一個模組有一個函式庫:sdk。它捆綁了機器人後端所需的三个東西 — 資料庫、Telegram Bot API 和出站 HTTP — 無需安裝,無需配置憑證。資料庫 (db) 在 資料庫 中介紹;本節介紹 api、fetch 和 console 全域。

import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk';   // 整個介面
// 或從子模組匯入:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
匯入 它是什麼
db 資料庫 — 查詢建構器和結構 DSL → 資料庫
api Telegram Bot API — api.sendMessage(...) → 下方
fetch 出站 HTTP → 下方
InputFile 檔案(位元組 + 名稱)用於上傳 — 同時適用於 api 和 fetch → 檔案
EndpointError 拒絕 Mini App 的調用,並附帶一個它可以顯示的訊息 → 端點

通過相對路徑並帶有 .js 擴展名匯入您自己的專案模組(from '../schema.js'、from '../lib/cart.js')。請參閱 模組系統 。

Bot API

api 為您提供了完整的 Telegram Bot API 。將任何方法調用為 api.<method>(params)。所有當前 — 以及未來 — 的 Bot API 方法都可使用,無需更新 SDK。

import { api } from 'sdk';

const me = await api.getMe();                            // → 未包裝的結果
await api.sendMessage({ chat_id: id, text: 'Hello!' });
await api.editMessageText({ chat_id, message_id, text: 'Updated' });
await api.answerCallbackQuery({ callback_query_id, text: 'Done' });

響應信封已解開。 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' }) },
]});

位元組將作為實際的多部分上傳流式傳輸到 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 風格的客戶端,用於調用外部世界 — 第三方 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 的逐令牌輸出。

主體輔助函數會為您設置匹配的 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 。

生成的檔案有一個實時的 export default,因此處理器在您部署後立即激活 — 沒有什麼需要取消註解的。使用 push 部署新模組。

login

npx tgcloud login

提示輸入您的 CLI 訪問權杖 — 從 @BotFather → 您的機器人 → Serverless → CLI Access → Access token,這是一個獨立於您 機器人 API 權杖 的權杖 — 驗證它與平台,並將其儲存到 .tgcloud/credentials。

login 是唯一一個要求權杖的命令,並且它需要一個交互式終端。在 CI 中,請改為設置 TGCLOUD_TOKEN。

status

npx tgcloud status

顯示每個檔案在您的工作目錄與已部署副本之間的變更:已修改、新增、刪除、未變更。它離線工作,與您上次 push、pull 或 fetch 以來的雲端狀態進行比較。它還會警告 tgcloud/ 內部未部署的臨時 .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] 計數器:

  • 所有待處理變更的簡要摘要。
  • 安全變更,在您確認後一次性應用。
  • 警告(刪除、慢速操作),一次一個,每個單獨確認。
  • 手動變更,顯示原因和建議操作,不會自動應用。
  • 未記錄的對象(存在於資料庫但不在您的結構中),顯示以供了解。

它以摘要結束:已應用、已跳過、等待手動修復、不在結構中。

請參閱 遷移 了解模型。

選項:--dry-run(列印所有內容,不應用任何內容)、--safe(自動應用安全變更,跳過警告)、--yes(自動應用安全變更和所有警告,跳過手動 — 小心使用)、--local(與您的本地 schema.js 進行差異比較,而不是已部署的)。如果沒有標誌,migrate 需要一個終端,並在非交互式環境中出錯,而不是猜測。

run

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

在平台上執行處理器或端點,無需部署,使用您當前的本地檔案。這是測試邏輯的快速內部循環。

  • <module> — 一個裸名稱(在 handlers/ 和 endpoints/ 下搜尋)或一個路徑,如 handlers/message。同時存在於兩個目錄中的裸名稱必須拼寫其目錄。
  • [args] — 傳遞給處理器的負載,以 JSON5 編寫,因此您可以省略鍵的引號。它是您的處理器收到的更新類型對象(例如,handlers/message 的 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 恢復健康,它們將被傳遞)。

completion

注意: tab 完成僅在 bare 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(或重新載入該檔案)。運行 tgcloud completion 而不帶 shell 會再次列印這些說明。

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 管道中。