Telegram Serverless を使用すると、ボットおよびミニアプリのバックエンドコードを Telegram のインフラストラクチャ上で直接実行できます。サーバーのプロビジョニング、コンテナの維持、スケーリングの心配は一切不要です。プレーンな JavaScript モジュールを記述し、単一のコマンドでデプロイすれば、Telegram はそれを高速で隔離された V8 サンドボックスで実行します。このサンドボックスは、Bot API および内蔵データベースのすぐ隣に配置されています。

VPS、クラウド関数、またはホスティングパネルにボットを接続して /start に応答するようにしたことがあるなら、それはもう行う必要がなくなります。

サーバーレスとは

Telegram ボットは、本質的にはアップデートに反応するプログラムです。従来は、そのプログラムを常に稼働し、到達可能で、安全な場所にホストし、それを維持し続ける必要がありました。Telegram Serverless は、そのレイヤーを完全に排除します。

  • インフラストラクチャ不要。 借りて、パッチを適用して、監視する必要のあるマシンはありません。コードはオンデマンドで実行され、ボットに合わせて自動的にスケーリングします。
  • すぐに使える。 Telegram Bot API、SQLite データベース、およびアウトバウンド HTTP は、すべてのモジュールで標準で利用可能です。インストールは不要で、接続する認証情報もありません。
  • 高速で隔離された実行。 各呼び出しは軽量な V8 アイソレートで実行され、Telegram のシステムに近いため、Bot API およびデータベースへの呼び出しは迅速かつ信頼性があります。
  • 実際の開発ワークフロー。 プロジェクトは、バージョン管理下にあるローカルマシン上のフォルダに存在します。ファイルを編集し、変更された箇所を正確に確認し、アトミックにデプロイし、レビュー済みのマイグレーションでデータベーススキーマを更新します。これは、他のすべてを扱うのと同じ方法です。

メンタルモデル

3つの場所で作業し、それらが互いにきれいにマッピングされます。

場所 そこにあるもの
プロジェクトフォルダ JavaScript モジュール — スキーマ、共有コード、アップデートハンドラー、ミニアプリのエンドポイント — および、Mini App のフロントエンド(存在する場合)
クラウド これらのモジュールのデプロイされたコピー、ボットのデータベース、およびミニアプリのファイル
tgcloud CLI ブリッジ — 差分を表示し、同期します

SSH で何かに接続することはありません。ローカルでファイルを編集し、npx tgcloud push を実行すると、プラットフォームがそこから処理します。ボットのトラフィックは、デプロイされたモジュールによって処理され、データベースは呼び出し間で永続化されます。

プロジェクトには4種類のコードしかなく、すべて1つの tgcloud/ フォルダ内にあります。

tgcloud/
├─ endpoints/  # ミニアプリが呼び出す関数
├─ handlers/   # Telegram のアップデートタイプに反応する関数
├─ lib/        # どこからでもインポートできる共有コード
└─ schema.js   # データベーステーブル

メッセージ、ボタンのクリック、インラインクエリなどのアップデートが到着すると、Telegram はそれを一致するハンドラー(handlers/message.js、handlers/callback_query.js など)にルーティングし、そのデフォルトエクスポートを呼び出します。その関数は SDK を介して Bot API およびデータベースと通信し、返します。これがループ全体です。一致するハンドラーのないアップデートは無視されるため、必要なハンドラーのみを追加します。

ミニアプリはオプションです。ボットにミニアプリがある場合、そのフロントエンドは tgcloud/ の隣にある同じプロジェクトに配置されます。サーバーレスは、そのビルド出力と、それが呼び出す endpoints/ をホストします。 ミニアプリのフロントエンド を参照してください。

クイックデモ

以下は、完全に動作するデモボットです。すべてのメッセージに応答し、各チャットから受信したメッセージの数を記憶します。

tgcloud/schema.js

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

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

tgcloud/handlers/message.js

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

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

  // カウンターを挿入、またはそのチャットに既にカウンターがある場合はインクリメントし、
  // 同じステートメントで .returning() を介して結果の行を取得します。
  const [row] = await db.insert(counters)
    .values({ chatId, seen: 1 })
    .onConflictDoUpdate({
      target: counters.chatId,
      set: { seen: sql`${counters.seen} + 1` },
    })
    .returning()
    .run();

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

デプロイします。

npx tgcloud push       # モジュールをアップロード
npx tgcloud migrate    # `counters` テーブルを作成

これで、永続的な状態とサーバーのないライブボットが完成しました。これに含まれるすべて — api、db、table() DSL — は、以下のセクションで説明されています。

Serverless は、Telegram ボットおよびミニアプリの汎用バックエンドであり、特定の種類のアプリのテンプレートではありません。以下に最適です。

  • 会話型 AI ボット データベースにユーザーごとの状態を保存する必要があるもの。
  • ミニアプリのバックエンド ユーザーデータを保存し、動的なコンテンツを提供するもの。
  • ゲームおよびツール リーダーボード、クイズなどを含むもの。
  • 自動化および統合 サードパーティの 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 を実行するのに便利であり、シェルタブ補完が必要とするものです。どちらの方法でも同じプロジェクトが得られます。

ミニアプリを構築しますか? フロントエンドから開始してください。いつものツールで作成し、そのフォルダでプロジェクト作成ツールを実行します。 ミニアプリのフロントエンド を参照してください。

2. ボットのリンク

各プロジェクトは1つのボットに紐付けられています。login コマンドで接続します。これは、CLI アクセストークン( @BotFather → ボット → Serverless → CLI Access → Access token — ボットの API トークン とは別のトークン)を要求し、ローカルに保存します。

npx tgcloud login

トークンは app<id>:<secret> の形式です。CLI はそれを .tgcloud/ に保存し、git からは無視され、秘密の部分は決して表示しません。ログインは、トークンが要求される唯一の機会です。CI でのトークンの解決方法については、 認証 を参照してください。

3. 確認

2つのコマンドで、いつでも状況を確認できます。どちらも完全にオフラインで動作します。

npx tgcloud status     # ローカルとデプロイされたコピーとの差分
npx tgcloud diff       # 差分の行ごとの変更点

init の直後、すべてが新しく、何もデプロイされていません。status は、アップロード待ちのスターターファイルを表示します。

4. デプロイ

モジュールをクラウドに送信します。

npx tgcloud push

push は、変更されたすべてのモジュールを1つのアトミックバッチでアップロードし、クラウドが現在保持しているもののローカルレコードを更新します。ボットはライブです。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 は変更をステップバイステップで説明し、確認するとテーブルを作成します。この2段階モデル — および、よりリスクの高い変更(削除など)がどのように扱われるか — は、 マイグレーション で説明されています。

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: `Saved. That's ${count} message(s) from this chat so far.`,
  });
}

更新されたハンドラーを 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 はローカルの変更を破棄します。

2人が(または2台のマシンが)同じボットにデプロイした場合、プラットフォームは競合を検出し、push はあなたが先に pull できるように停止します — 他の人の作業をサイレントに上書きすることは決してできません。 同期を保つ を参照してください。

AI を使って構築

AI アシスタントを使って構築したいですか — または、あなたのチームの唯一のコーダーは AI ですか?それでもボットをリリースできます。AI エージェントがプロジェクトに馴染めるように、最初のステップを踏みました。新しいプロジェクトはすべて、AGENTS.md およびエージェンティックコーディングツールが自動的に読み取る docs/tgcloud-sdk.md リファレンスでスケルトン化されます。

小さな自己完結型のランタイム — 1つの SDK、管理する npm パッケージなし — と組み合わせることで、汎用的なコード生成が通常見落としがちな規約について、アシスタントが迅速に開始できます。相対インポート(.js 付き)、外部キーなし、すべての db 呼び出しは非同期、アップデートタイプごとに1つのハンドラー、そして push/migrate の2段階フローです。

試してみてください。

npm create @tgcloud/bot my-bot
cd my-bot
opencode            # または Claude Code, Cursor, … — AGENTS.md を読み取るエージェント

そして、平易な言葉で質問してください。

各ユーザーの ToDo リストを記憶するボットを書いてください。テキストが送信されたらアイテムを追加し、/list が送信されたらリスト全体を表示してください。

アシスタントは schema.js とハンドラーを編集します。あなたはレビューし、npx tgcloud run で変更を即座にテストし、npx tgcloud push と npx tgcloud migrate でライブにします。AGENTS.md はプロジェクトの一部です — ボットが成長するにつれて、ガイダンスが正確であり続けるように編集してください。

BotFather と一緒に外出先

携帯電話だけになりましたか?プロジェクト全体は @BotFather にも存在します — ボット → Serverless を開き、CLI が管理するすべてをタッチスクリーンで利用できます。

  • ハンドラー — アップデートハンドラーを作成、編集、テスト実行します。BotFather は、あなたが持っているハンドラーと Webhook を同期させ続けます(CLI が報告するのと同じ「同期中」/「同期外」)。
  • エンドポイント — ミニアプリが呼び出す関数。
  • ライブラリ — 共有の lib/ モジュール。
  • データベース — 同じ Drizzle ライクな構文で schema.js を編集し、保留中の変更をレビューし、適用します。保存でデプロイします。
  • ミニアプリファイル — ユーザーが Telegram で開くフロントエンド。
  • CLI アクセス — キーボードに戻ったときに CLI アクセストークンを取得します。

これは1つのクラウドプロジェクトなので、携帯電話でハンドラーを開始し、後でラップトップに npx tgcloud pull できます — どのクライアントにも紐付けられていません。ハンドラーを実行すると、npx tgcloud run と同じように、コンソール出力がチャットに表示されます。

プロジェクトとモジュール

サーバーレスプロジェクトは、バージョン管理下にある通常のフォルダです。プラットフォームが実行するすべては1つのフォルダ tgcloud/ に配置されます。プロジェクトの残りはあなたのものです — ミニアプリのフロントエンド、設定、ドキュメント。ボットにビルドステップはなく、実行時に node_modules はなく、サーバーのエントリポイントもありません。

プロジェクトの構造

example_bot/
├─ tgcloud/             # デプロイされる部分 — それ以外はデプロイされない
│  ├─ handlers/         # アップデートハンドラー — フラット、1レベルのみ
│  │  ├─ message.js
│  │  └─ callback_query.js
│  ├─ endpoints/        # ミニアプリが呼び出す関数 — フラット、1レベルのみ
│  │  └─ getProfile.js
│  ├─ lib/              # 共有モジュール。サブディレクトリ可
│  │  ├─ reply.js
│  │  └─ internal/util.js
│  └─ schema.js         # データベーススキーマ — 1ファイル
└─ .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/ — ボットのエントリポイント。各ファイルは1つの Telegram アップデートタイプに対応し、プラットフォームは各受信アップデートを一致するハンドラーにルーティングします。

ファイル 処理するアップデートタイプ
handlers/message.js 新規受信メッセージ
handlers/inline_query.js 新規受信 インライン クエリ
handlers/callback_query.js 新規受信コールバッククエリ
… その他の Bot API アップデートタイプ

handlers/ は フラット で、サブディレクトリはありません。ハンドラーの export default は、プラットフォームが呼び出す関数です( ハンドラー を参照)。

アップデートタイプは、ハンドラーファイルが存在し、空でない場合 にのみ処理されます。handlers/<type>.js がない場合 — またはファイルが空の場合 — そのタイプのアップデートは無視され、プラットフォームは何も実行しません。そのため、実際に必要なハンドラーのみを保持してください。追加する各ハンドラーは、ボットが処理のために起動する別のアップデートタイプであり、残りを省略すると、Telegram は破棄するだけのアップデートのためにコードを起動しません。

新しいハンドラーをスケルトン化するには、 npx tgcloud add handlers/<type> を実行します。

endpoints/ — ミニアプリのサーバーサイド。各ファイルは、ミニアプリが名前で呼び出す関数です。たとえば、endpoints/getProfile.js は POST /api/getProfile に応答します。プラットフォームは、コードが実行される前に、呼び出しがミニアプリ内の Telegram ユーザーから行われたことを確認し、そのユーザーを渡します。endpoints/ は handlers/ と同様に フラット です。 エンドポイント を参照してください。

.tgcloud/ — CLI によって完全に管理されるマシンローカルの状態。保存された認証情報も含まれます。git からは無視され、手動で読み書きしないでください — CLI コマンドを使用してください。

モジュールシステム

実行時には、モジュールは プラットフォーム SDK と プロジェクト内の他のモジュール の2つだけを見ることができます。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 の出力で使用されるものであり、モジュールはその名前でインポートすることもできます('lib/cart' から、拡張子なし)。コードでは相対インポートを優先してください。それらはエディタが理解するものであり、定義へのジャンプ、オートコンプリート、名前変更などが可能です。

実行時に利用可能なのは、sdk とそのサブモジュール(sdk/db、sdk/api、sdk/fetch) — プラットフォームの全表面、 SDK を参照 — と、独自のモジュール tgcloud/ の2つです。これが完全なリストです。コードが他のものを import しても解決しません。この制約により、モジュールのロードが高速で、実行が安全になります。

ハンドラー

ハンドラーは 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)'}`,
  });
}

ハンドラーは、プラットフォームがあなたのために Update をアンラップした ペイロード を引数として受け取ります。handlers/message.js は Message (つまり update.message)を受け取ります。handlers/callback_query.js は [CallbackQuery]( https://core.telegram.org/bots/api#callbackquery )を受け取ります。など。ハンドラーの 2番目の引数 は、呼び出しごとのコンテキストオブジェクト ctx です。ペイロード以外のものが必要な場合(たとえば update_id)は、それを ctx.update として保持します。

ハンドラーは async (通常はそうです)にでき、値を返すこともできます。SDK を介して Bot API、データベース、およびアウトバウンド HTTP にアクセスします。

ハンドラーをテストするために実際のアップデートは必要ありません。npx tgcloud run は、提供されたペイロードと現在のローカルコードを使用してプラットフォーム上でそれを実行します。

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

引数はペイロード(ハンドラーが受け取るのと同じオブジェクト)で、JSON5 形式です。ハンドラーの ctx(2番目の引数)を供給するには、--ctx を追加します。例: --ctx '{ update: { update_id: 1 } }'。これは ローカル ファイルに対して実行されるため、デプロイする前に変更を試すことができます。 run を参照してください。

エンドポイント

エンドポイントは endpoints/ ディレクトリ内のモジュールで、ミニアプリ が呼び出します。これは、フロントエンドだけではできないこと(データベースの読み書き、Bot API の呼び出し、秘密情報を使った外部サービスとの通信など)を行う場所です。

// endpoints/getProfile.js
import { db, EndpointError } from 'sdk';
import { eq } from 'sdk/db';
import { profiles } from '../schema.js';

export default async function (input, ctx) {
  const user = ctx.initData.user;
  const profile = await db.select().from(profiles).where(eq(profiles.userId, user.id)).get();
  if (!profile) throw new EndpointError('Profile not found', { userId: user.id });
  return profile;
}

最初の引数 input は、ミニアプリが送信した JSON オブジェクトです。2番目の引数 ctx には ctx.initData が含まれています — ミニアプリの init data (user、chat、start_param、auth_date など)で、プラットフォームによって検証済みです。クライアントの Telegram.WebApp.initDataUnsafe と同じ形状ですが、ここでは信頼できます。有効な init データがない呼び出しは、コードが実行される前に 401 で拒否されるため、ctx.initData.user は実際にミニアプリを開いたユーザーです。エンドポイントは、Telegram ユーザーがミニアプリから呼び出した場合にのみ呼び出されるため、保護すべき公開 API はなく、構築すべきログインもありません。

エンドポイントが返すものはすべて JSON として返されます。呼び出しを拒否するには、throw new EndpointError(description, parameters?) を使用します。ミニアプリは説明と parameters オブジェクトをそのまま受け取ります。フロントエンドでエラーの種類に応じて分岐する必要がある場合は、parameters にコードを入れます — { code: 'OUT_OF_STOCK' }。それ以外の例外は一般的な内部エラーとして報告され、実際のメッセージはログにのみ記録されるため、内部情報がクライアントに漏洩することはありません。

ミニアプリからのエンドポイントの呼び出し

ミニアプリは、公式の ミニアプリSDK telegram-web-app.js の Telegram.WebApp.Serverless.call を使用してエンドポイントを名前で呼び出します。これは、ミニアプリが既に含んでいるスクリプトです。これは、init データを自動的に送信し、エラーまたはエンドポイントの結果でコールバックします。

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' になります。それ以外の値は、プラットフォームが呼び出しを拒否したか、エンドポイントが予期せず失敗したことを意味します。たとえば、init データがないか無効な場合の 'UNAUTHORIZED' です。

エンドポイント名は JavaScript の識別子に似ています — 文字、数字、_ で、数字で始まらないもの(getProfile、place_order)です。 npx tgcloud add endpoints/<name> でスケルトン化し、ミニアプリなしで run を使って、init データを自分で渡してテストします。

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

デプロイされるもの

npx tgcloud push を実行すると、CLI は schema.js、lib/、handlers/、endpoints/ の下のすべての .js ファイルを収集し、その正確なセットをプロジェクトのモジュールスペースとして送信します — ミニアプリのビルドフォルダと並んで(tgcloud.jsonc が名前を指定している場合、下記参照)。クラウドに存在するがプロジェクトに存在しないものは削除されるため、デプロイされた状態は常にフォルダをミラーリングします — 削除も含まれます。tgcloud/ 外のものはコードとしてデプロイされません — プロジェクトルートの残りはあなたのものです。tgcloud/ 内の、それらの場所のいずれにもない .js ファイル(tgcloud/util.js、tgcloud/helpers/x.js)はフラグが立てられ、サイレントにデプロイされないようになります。Markdown、ドットファイル、および .tgcloud/ は決してデプロイされません。

ミニアプリのフロントエンド

プロジェクトルートはあなたのものです。ミニアプリのフロントエンドを配置するのに自然な場所です — React、Vue、Svelte、またはプレーンな HTML で、Vite またはその他のバンドラーでビルドされたものです。Serverless は、ボットの隣にそのビルド出力をホストします。1回の push でコードと静的ファイルが一緒にデプロイされ、アプリは https://app<app_id>.tgcloud.ai/ で提供されます(push は正確なアドレスを表示します — ミニアプリの URL として @BotFather で使用してください)。

不正利用の報告。 tgcloud.ai 上のミニアプリは、独立した開発者によって公開されています。app<app_id>.tgcloud.ai サブドメイン上の違法コンテンツまたは不正利用を報告するには、 [email protected] にメールしてください。

開始するには、まずフロントエンドを任意のツールで作成し、その上に Serverless を追加します。

npm create vite@latest example_app   # または create-vue、…
cd example_app
npm create @tgcloud/bot .

プロジェクト作成ツールはファイルを上書きしません。tgcloud/ フォルダをフロントエンドの隣に追加し、ビルドフォルダを認識して、package.json に npm run deploy スクリプトを追加します(ビルドしてからプッシュ)。

ホストされるものは、プロジェクトルートにあるプロジェクト設定ファイル**tgcloud.jsonc**で設定されます。

{
  "$schema": "./node_modules/@tgcloud/cli/schema/tgcloud.json",
  "static": {
    "source": "dist",
    "spa": true,
    "immutable": ["/assets/*"],
    "headers":   [{ "path": "/*", "set": { "Referrer-Policy": "no-referrer" } }],
    "redirects": [{ "from": "/old", "to": "/new", "status": 301 }]
  }
}

"source" のみが必要です。これはプロジェクトルートからのビルドフォルダです。他のキーは、それがどのように提供されるかを構成します(下記参照)。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 を使用します(ビルドスクリプトを実行してから tgcloud push を実行します)。または、まず自分でビルドします。

提供オプション:

  • spa — 存在しないパスや拡張子がないパスに対して index.html を提供し、クライアントサイドルーターがリロード時に機能するようにします。拡張子を持つパスは依然として 404 を返します。
  • immutable — ブラウザが1年間キャッシュできるコンテンツハッシュファイル(Vite の /assets/*)のパターン。コンテンツハッシュファイルにのみ使用してください。ここで一致する他のファイルは1年間ブラウザにキャッシュされ、リコールする方法はありません。
  • headers — ルールが一致するパス(完全なパス、または /* で終わるプレフィックス)に対するレスポンスヘッダー。null 値はヘッダーを削除します。
  • redirects — リダイレクトルール。最初のマッチが優先されます。既存のファイルは常にルールよりも優先されます。status は 301 です。
  • "static": "dist" — 他に必要なものがない場合の { "source": "dist" } のショートカット。
  • "static": false — 次のプッシュでデプロイされたサイトを削除します。キーを削除するだけでは、このプロジェクトが静的ファイルを管理するのを停止するだけで、デプロイされたものはそのまま残ります。

サイトは独自のホストのルートから提供されるため、バンドラーのデフォルト base(/)で正しいです。Telegram Web はミニアプリを iframe で開くため、X-Frame-Options や制限的な frame-ancestors ポリシーを追加しないでください。

同じホストが エンドポイント を /api/ の下に回答します — Telegram.WebApp.Serverless.call('getProfile', …) がそこにアクセスします。ビルドフォルダの /api/ の下に独自のファイルを置かないでください。

データベース

すべてのボットには独自のデータベースがあります — 呼び出し間で永続化され、db を介してすべてのモジュールで利用可能な SQLite バックエンドストアです。テーブルは、小さな型付き 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 を使用します。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… ``default はそのまま渡されます。.deprecated()` は終端です — マイグレーション を参照してください。

インデックスと制約

インデックスと制約は extras コールバックで宣言され、カラムがスコープ内にあります。

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

テーブルレベルの修飾子は table(...) の後にチェーンされます: .strict()、.withoutRowid()、.deprecated('reason')。

外部キーなし

実行環境では PRAGMA foreign_keys が オフ で実行されます。宣言された外部キーはサイレントに無効になるため、カスケードや孤児保護はありません。これは、ないよりも悪いことです。そのため、DSL はそれを不可能にします。具体的には、.references() およびテーブルレベルの foreignKey() は 宣言時にスローされ、これらを使用するスキーマはデプロイされません。

プレーンなカラム(userId: integer('user_id'))で関係をモデリングし、整合性をアプリケーションコードで強制する必要があります。親を子より先に挿入し、子を親より先に削除し、エラーを適切に処理し、必要に応じて LEFT JOIN … WHERE parent.id IS NULL で孤児を掃き集めてください。

外部キーがないことは、見落としではなく意図的な制約です。ボットの計画を立てる際には、これを早期に考慮してください。

クエリ

db は連鎖的なクエリビルダーです。すべてのクエリは非同期です — 終端メソッド(.all()、.get()、.values()、.run())は Promise を返すため、常に await してください。

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

await db.select().from(todos).all();                          // すべての行
await db.select().from(todos).where(eq(todos.id, 1)).get();   // 最初の行または null
await db.select().from(todos).values();                       // 値の配列としての行

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

// カスタムプロジェクション: { alias: columnRef | sqlExpr | aggregate }
await db.select({ id: todos.id, title: todos.text, n: count() })
  .from(todos).groupBy(todos.userId).having(sql`count(*) > ${1}`).all();

// 行数 — ヘルパーであり、ビルダーの終端ではありません:
await db.$count(todos);                        // すべての行
await db.$count(todos, eq(todos.done, false)); // フィルタ付き
  • 連鎖可能: .where()、.orderBy()、.limit()、.offset()、.groupBy()、.having()、.distinct()。
  • 終端: .all()、.get()、.values()。
  • 行数をカウント: db.$count(table, where?)(またはプロジェクション内の count())。

挿入、更新、削除:

await db.insert(todos).values({ userId: 1, text: 'Buy milk' }).run();
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run();   // バッチ
await db.insert(todos).values({ text: 'X' }).returning().run();        // RETURNING *

await db.insert(users).values({ tgId: 42, name: 'Ann' })
  .onConflictDoUpdate({ target: users.tgId, set: { name: 'Ann' } }).run();

await db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run();
await db.delete(todos).where(eq(todos.id, 1)).run();

バッチ挿入は単一のステートメントであるため、SQLite の変数制限(行数 × カラム数)によって制限されます。それを超えると、挿入はエラーになります — ご自身でチャンク化してください。

演算子は sdk/db からインポートします。

import {
  eq, ne, gt, gte, lt, lte,
  like, notLike,
  isNull, isNotNull, and, or, not,
  between, notBetween, inArray, notInArray,
  count, sum, avg, min, max,
  asc, desc,
} from 'sdk/db';

.where(a, b) の複数の引数は and(a, b) と同じです。比較の2番目の引数はデフォルトで値ですが、別のカラムまたは sql`…` にすることもできます — 例: eq(a.x, b.y)。集計(count/sum/avg/min/max)は .select({ … }) プロジェクションのための SQL フラグメントです。

生の SQL — ビルダーが十分でない場合は、生の SQL にドロップします。モードはメソッドによって選択されます: 書き込みには db.run、多数の行には db.all、1行には 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、および RETURNING 句がある場合にそれを運ぶ rows。クエリが同じ名前の2つのカラムを投影する場合(SELECT u.id, o.id のような結合)に db.values を使用します。db.all はカラム名で行をキーイングするため、2番目の id は最初の id を上書きします。

sql`…` タグは ${value} をバインドパラメータに、${table.column} を識別子に変換し、ネストされた sql フラグメントをスプライスします。パラメータなしの文字通りの文字列には sql.raw('…') を使用します。

生のクエリは テーブルにバインドされていません。そのため、行はモード変換なしで返されます — boolean は 0/1、JSON カラムは文字列、タイムスタンプは数値です。テーブルにバインドされたビルダーのみが値を変換します。

マイグレーション

ボットが成長するにつれて、データベースは変化します。プラットフォームは、コードのデプロイとデータの変更を分離し、すべてのスキーマ変更をリスクの度合いによって分類することで、それを安全に保ちます。

デプロイはデータベースに触れません。 変更された schema.js を npx tgcloud push すると、プラットフォームは新しいスキーマを記録し、データベースが どのように 変更されるかを通知しますが、何も適用しません。

npx tgcloud push       # schema.js をデプロイ; 保留中の DB 変更を報告
npx tgcloud migrate    # レビューして適用

npx tgcloud migrate は、スキーマとライブデータベースの違いを計算し、ステップバイステップで進めます。すべてが適用される前に、必ず確認を求められます。これにより、通常のコードデプロイでデータマイグレーションが誤ってトリガーされることはありません。

各保留中の変更には、migrate がそれをどのように扱うかを決定するステータスがあります。

ステータス 意味 migrate での処理
safe 追加的で非ブロッキング — 新しいテーブル、カラム、またはインデックス 確認後、1つのステップでまとめて適用
warning 破壊的または遅い可能性 — 何かを削除、または巨大なテーブルへのインデックス 個別に確認、それぞれ個別に確認
manual 自動的に完了できない — 例:カラムの型の変更 ガイダンスと共に表示。手動で実行
undocumented データベースに存在するが、スキーマに存在しない 認識のために表示。適用されない

安全な変更は迅速で、概念的には元に戻せるため、まとめて処理されます。各警告は意図的な個別の確認です — 破壊的な変更のための「すべて適用」はありません。

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

実行時には、モジュールには1つのライブラリがあります: sdk。これは、データベース、Telegram Bot API、およびアウトバウンド HTTP — インストール不要で、構成不要な認証情報 — というボットバックエンドが必要とする3つのものをバンドルします。データベース(db)は データベース で説明されています。このセクションでは、api、fetch、および console グローバルについて説明します。

import { db, api, fetch, InputFile, BotApiError, EndpointError } from 'sdk';   // 全体
// またはサブモジュールから:
import { table, integer, text, eq, sql } from 'sdk/db';
import { api } from 'sdk/api';
import { fetch } from 'sdk/fetch';
インポート 何か
db データベース — クエリビルダーとスキーマ DSL → データベース
api Telegram Bot API — api.sendMessage(...) → 下記
fetch アウトバウンド HTTP → 下記
InputFile アップロードするファイル(バイト + 名前) — api および fetch で機能 → ファイル
EndpointError ミニアプリの呼び出しをメッセージで拒否。表示可能 → エンドポイント

自身のプロジェクトモジュールは、.js 拡張子付きの相対パスでインポートします(from '../schema.js', from '../lib/cart.js')。 モジュールシステム を参照してください。

Bot API

api は、完全な Telegram Bot API を提供します。任意のメソッドを api.<method>(params) として呼び出します。現在および将来のすべての Bot API メソッドは、SDK の更新なしで機能します。

import { api } from 'sdk';

const me = await api.getMe();                            // → アンラップされた結果
await api.sendMessage({ chat_id: id, text: '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 固有のスネークケース名(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 で。 2つのヘルパーがファイルのバイトをプルバックします。プラットフォームは 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 は、外部世界 — サードパーティ API、Webhooks、HTTP 経由のすべて — を呼び出すための fetch ライクなクライアントです。

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 と同様に動作します。ただし、2つの制約があります。

  • レスポンスはテキストとして読み取られます — await res.json() / await res.text()、または res.body でチャンクごとにストリーミングします。
  • レスポンス全体は 30 MB に制限されています。その制限はレスポンス全体をカバーします — res.body でストリーミングすると、大きなボディを段階的に処理できますが、制限は上がりません。

ログ

標準のグローバル console が利用可能です — インポートする必要はありません。JavaScript のように利用できます。その出力はキャプチャされ、npx tgcloud run によって表示されるため、開発中の主要なデバッグツールになります。

console.log('processing', { chatId: id });   // ログ / デバッグ — プレーン
console.info('started');                     // 情報 — 青
console.warn('rate limited');                // 警告 — 黄色
console.error(err);                          // エラー — 赤、スタックトレース付き

各行は、発生した [file:line] でタグ付けされます。console.error および console.trace は完全なスタックを追加しますが、console.warn は追加しません。モジュールを npx tgcloud run すると、これらの行は、レベルごとの色付きプレフィックス、実行開始からの時間、およびソースとともに表示されます — run を参照してください。

コマンドラインインターフェース

tgcloud は、プロジェクトフォルダとクラウド間のブリッジです。プロジェクトをスケルトン化し、変更されたものを表示し、デプロイし、モジュールを実行し、データベースマイグレーションを適用します。 Node.js 18 以降が必要です。2つの取得方法があります。

# 推奨 — CLI がインストールされたプロジェクトを作成します:
npm create @tgcloud/bot example_bot

# または CLI をグローバルにインストールし、空のフォルダを初期化します:
npm install -g @tgcloud/cli
tgcloud init

npm パッケージは @tgcloud/cli で、インストールされるコマンドは tgcloud です。すべてのコマンドは、プロジェクトの任意のサブフォルダから機能します。

コマンド 目的
init 現在のフォルダに新しいプロジェクトをスケルトン化する
add 新しいモジュール(ハンドラー、エンドポイント、libモジュール)をスケルトン化する
login プロジェクトをボットにリンクする(トークンを保存)
status ローカルとクラウドの変更を表示する
diff 差分の行ごとの変更を表示する
push 変更されたモジュールとミニアプリのビルドをクラウドにデプロイする
migrate スキーマ変更をデータベースに適用する
run デプロイせずにプラットフォームでモジュールを実行する
fetch デプロイされているものを、ローカルファイルを変更せずにチェックする
pull ローカルファイルをクラウドに同期させる
reset ローカルの変更を破棄し、クラウドの状態から復元する
webhook プラットフォーム管理の Webhook を検査し、再同期する
completion シェル補完スクリプト(bash/zsh/fish)を表示する
upgrade 0.2.0 より前のプロジェクトレイアウトを tgcloud/ に移動する

認証

プロジェクトは、トークンによって1つのボットに紐付けられます。トークンは app<id>:<secret> の形式です。app<id> 部分は公開可能で表示されることがありますが、シークレットはログやエラーに決して表示されません。

トークンは以下の順序で解決されます。

  1. TGCLOUD_TOKEN 環境変数 — CI 用。ディスクには書き込まれません。
  2. .tgcloud/credentials — npx tgcloud login によって書き込まれます。
  3. どちらでもない場合 → npx tgcloud login を指示するエラー。

CLI はコマンドの途中でトークンをプロンプトしません。ログインは常に明示的な login ステップです。保存されたトークンが無効になった場合、CLI はそれを削除し、再度 login を要求します。

init

npx tgcloud init

現在のディレクトリに新しいプロジェクトをスケルトン化します:schema.js、lib/、handlers/ とスターターハンドラーを含む tgcloud/ フォルダ、さらに tgcloud.jsonc、AGENTS.md、docs/、および .tgcloud/ 状態フォルダ。既存のフロントエンドプロジェクトのルートで実行しても、tgcloud/ フォルダとドキュメントを追加するだけで、ファイルを上書きすることはありません。フロントエンドを認識した場合、tgcloud.jsonc をビルドフォルダにポイントし、package.json に deploy スクリプト(ビルドしてからプッシュ)を追加します — 既に deploy スクリプトがある場合を除く。 ミニアプリのフロントエンド を参照してください。

init は、別のプロジェクト内にプロジェクトを作成することを拒否します。プロジェクト自体のルートで再実行しても問題なく、不足しているものを追加するだけです。

add

npx tgcloud add <target>

1つの新しいモジュールをスケルトン化し、配線して編集可能にします — ハンドラー、エンドポイント、または lib/ モジュール。

npx tgcloud add handlers/callback_query   # 新しいアップデートハンドラー
npx tgcloud add endpoints/getProfile      # ミニアプリ用の新しいエンドポイント
npx tgcloud add lib/cart                  # 新しい共有モジュール

注意:add は既存のファイルを上書きしません。

<target> はモジュールのパスです(末尾の .js はオプション)。handlers/ の場合、名前は Telegram のアップデートタイプである必要があります。プラットフォームは有効なセットを公開するため、無効な名前は upfront で拒否されます。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 ファイルについても警告します。

ミニアプリを持つプロジェクトの場合、status はビルドフォルダの最終デプロイからの変更も表示します。

diff

npx tgcloud diff

status と同様ですが、変更されたモジュールの実際の行ごとの差分を表示します。オフラインでも動作します。

push

npx tgcloud push [files...]

プロジェクトを1つのアトミックバッチでクラウドにデプロイします。

引数なしの場合、プロジェクト全体をデプロイし、デプロイされた状態はフォルダを正確にミラーリングします — ローカルで削除したモジュールはクラウドから削除されます。

ファイルまたはディレクトリ引数(npx tgcloud push tgcloud/handlers/message.js、npx tgcloud push handlers/)の場合、それらのファイルでの変更のみをデプロイします。ターゲットプッシュは他のモジュールを削除しません。ターゲットはファイルパス(tgcloud/handlers/message.js)、tgcloud/ 内のパス(handlers/message.js)、またはモジュール名(handlers/message)として記述できます — すべて同じモジュールを意味します。

オプションは --force です — 並行性チェックをスキップして、クラウドにあるものを上書きします。確信がある場合のみ使用してください( 同期を保つ を参照)。

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] カウンターでステップバイステップで案内します。

  • 保留中のすべての変更の簡単な概要。
  • 安全な変更、確認後にまとめて1つのステップで適用。
  • 警告(削除、遅い操作)、1つずつ、それぞれ個別に確認。
  • 手動変更、理由と推奨アクションと共に表示、自動的に適用されない。
  • 文書化されていないオブジェクト(データベースに存在するがスキーマに存在しない)、認識のために表示。

最後に概要が表示されます:適用されたもの、スキップされたもの、手動修正待ちのもの、スキーマにないもの。

モデルについては、 マイグレーション を参照してください。

オプション:--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> — モジュールのコンテキストオブジェクト(2番目の引数)、これも JSON5 形式です。コードが ctx から読み取るものを供給するために使用します — ハンドラーの生のアップデート、--ctx '{ update: { update_id: 1 } }'、またはエンドポイントの init データ、--ctx '{ initData: { user: { id: 1 } } }'。run はテストしているのがロジックであるため、プラットフォームのチェックではなく、それを検証しません。
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'

プラットフォームは、ローカルプロジェクトから組み立てられたモジュールスペース(したがって、ローカルで変更された lib/ コードも使用されます)に対してモジュールを実行し、戻り値、console.* でログに記録されたすべて、および経過時間を返します。大きな引数はファイルから読み取ります: npx tgcloud run handlers/message "$(cat message.json5)"。

fetch

npx tgcloud fetch

作業ファイルを変更せずに、現在デプロイされているものをチェックします。その後、status および diff は最新のクラウド状態と比較されます。競合を解決する方法を決定する前に、それを確認するのに役立ちます。

pull

npx tgcloud pull

ローカルプロジェクトをクラウドに同期させるために、作業ファイルをデプロイされた状態に更新します。

pull はミニアプリのビルドフォルダには書き込みません。

reset

npx tgcloud reset

ローカルの変更を破棄し、最後に知られたクラウド状態からモジュールを復元します。実験を破棄するために使用します。ミニアプリのビルドフォルダは変更されません。

webhook

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

Telegram は 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

注意: タブ補完は、ベア tgcloud が PATH にある場合にのみ機能します — グローバルにインストールしてください(npm install -g @tgcloud/cli)またはバイナリを PATH に配置してください。npx にフックすることはできません。

tgcloud completion <bash|zsh|fish>

標準出力にシェル補完スクリプトを表示します。一度有効にすると、<Tab> はコマンド、フラグ、モジュールディレクトリ、まだ持っていないハンドラーのアップデートタイプ、およびローカルで実行可能なモジュールを補完します。

# bash — bash-completion パッケージが必要です:
echo 'eval "$(tgcloud completion bash)"' >> ~/.bashrc

# zsh — ~/.zshrc で `autoload -U compinit && compinit` が実行されることを確認してください:
echo 'eval "$(tgcloud completion zsh)"' >> ~/.zshrc

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

その後、シェルを再起動してください(またはファイルを再ソースしてください)。シェルなしで tgcloud completion を実行すると、これらの指示が再度表示されます。

upgrade

npx tgcloud upgrade [--dry-run]

CLI 0.2.0 より前に作成されたプロジェクトのための、一度限りの移行。これは schema.js、lib/、handlers/ をプロジェクトルートに保持していました。それを tgcloud/ に移動します。ボットへのリンクは維持され、再デプロイは不要です。--dry-run は、何も変更せずに計画された移動を表示します。他のコマンドは、アップグレードするまで古いレイアウトでは実行されません。

同期を保つ

各プロジェクトは、クラウドで単調増加するリビジョンを持ち、各デプロイでインクリメントされます。CLI は最後に同期したリビジョンを記憶し、各 push でそれを送信します。クラウドが先に進んでいる場合 — 別のマシンやチームメイトがデプロイしたため — プッシュはサイレントに上書きするのではなく拒否され、CLI は3つの方法を提示します。

npx tgcloud fetch           # クラウドで変更されたものを表示し、再チェック
npx tgcloud pull            # 最新をローカルファイルに同期
npx tgcloud push --force    # クラウドの状態を上書き(危険)

この楽観的並行性チェックにより、ロックステップのデプロイプロセスなしで、チーム間でボットを共有できます。コマンドは、拒否されたデプロイ、失敗したマイグレーション、認証エラー、run 中にスローされたモジュールなど、失敗時に非ゼロで終了するため、スクリプトや CI パイプラインでクリーンに構成できます。