telegram

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Telegram Channel Skill

Telegram频道Skill

Operate the joelclaw Telegram channel — the primary mobile interface between Joel and the gateway agent. Built on grammy (Bot API wrapper), supports text, media, reactions, replies, inline buttons, callbacks, and streaming.
运营joelclaw Telegram频道——Joel与网关Agent之间的主要移动端交互接口。基于grammy(Bot API封装库)构建,支持文本、媒体、消息反应、回复、内联按钮、回调和流式传输。

Architecture

架构

Joel (Telegram app)
  → Bot API (long polling via grammy)
    → telegram.ts channel adapter
      → enrichPromptWithVaultContext()
        → command-queue → pi session
          → outbound router → telegram.ts send → Bot API → Joel
Key files:
  • packages/gateway/src/channels/telegram.ts
    — channel adapter (inbound + outbound)
  • packages/gateway/src/telegram-stream.ts
    — streaming UX (progressive text updates)
  • packages/gateway/src/outbound/router.ts
    — response routing
  • packages/gateway/src/channels/types.ts
    Channel
    interface
SDK:
grammy@1.40.0
— Bot instance at module scope, exposed via
getBot()
.
Multi-instance poll ownership (2026-03-05): Telegram long polling now uses a Redis lease per bot token hash.
  • Owner key:
    joelclaw:gateway:telegram:poll-owner:<tokenHash>
  • Status key:
    joelclaw:gateway:telegram:poll-status:<tokenHash>
  • Only owner polls
    getUpdates
    ; non-owners stay passive/send-only and retry lease acquisition with backoff.
Conflict guard still applies for non-cooperative pollers:
telegram.channel.start_failed
(with
conflict
metadata) +
telegram.channel.retry_scheduled
+
telegram.channel.polling_recovered
.
Joel (Telegram app)
  → Bot API (long polling via grammy)
    → telegram.ts channel adapter
      → enrichPromptWithVaultContext()
        → command-queue → pi session
          → outbound router → telegram.ts send → Bot API → Joel
核心文件:
  • packages/gateway/src/channels/telegram.ts
    —— 频道适配器(入站+出站)
  • packages/gateway/src/telegram-stream.ts
    —— 流式传输UX(渐进式文本更新)
  • packages/gateway/src/outbound/router.ts
    —— 响应路由
  • packages/gateway/src/channels/types.ts
    ——
    Channel
    接口定义
SDK:
grammy@1.40.0
—— Bot实例位于模块作用域,通过
getBot()
对外暴露。
多实例轮询所有权(2026-03-05更新):Telegram长轮询现在对每个Bot令牌哈希使用Redis租约。
  • 所有者键:
    joelclaw:gateway:telegram:poll-owner:<tokenHash>
  • 状态键:
    joelclaw:gateway:telegram:poll-status:<tokenHash>
  • 仅持有租约的所有者会轮询
    getUpdates
    接口;非所有者保持被动/仅发送状态,并通过退避机制重试获取租约。
针对非协作轮询器仍有冲突防护机制:触发
telegram.channel.start_failed
(附带
conflict
元数据) +
telegram.channel.retry_scheduled
+
telegram.channel.polling_recovered
事件。

Capabilities

能力说明

Sending Messages

发送消息

typescript
// Via channel adapter
await telegramChannel.send("telegram:7718912466", "Hello", { format: "html" });

// Direct grammy API (from telegram-stream or daemon)
const bot = getBot();
await bot.api.sendMessage(chatId, text, { parse_mode: "HTML" });
  • Max message length: 4096 chars (Telegram API limit)
  • Chunking:
    TelegramConverter.chunk()
    for HTML-aware splitting,
    chunkMessage()
    for raw text
  • Format: markdown→HTML via
    TelegramConverter.convert()
    , with plain text fallback on validation failure
  • Buttons:
    InlineButton[][]
    inline_keyboard
    reply markup
typescript
// Via channel adapter
await telegramChannel.send("telegram:7718912466", "Hello", { format: "html" });

// Direct grammy API (from telegram-stream or daemon)
const bot = getBot();
await bot.api.sendMessage(chatId, text, { parse_mode: "HTML" });
  • 最大消息长度:4096字符(Telegram API限制)
  • 消息分片:使用
    TelegramConverter.chunk()
    实现HTML感知的内容拆分,
    chunkMessage()
    用于纯文本拆分
  • 格式:通过
    TelegramConverter.convert()
    将Markdown转换为HTML,校验失败时降级为纯文本发送
  • 按钮:
    InlineButton[][]
    转换为
    inline_keyboard
    回复标记

Reactions (ADR-0162)

消息反应(ADR-0162)

typescript
// grammy API
await bot.api.setMessageReaction(chatId, messageId, [
  { type: "emoji", emoji: "👍" }
]);
Telegram supports a fixed set of emoji reactions. Common ones: 👍 👎 ❤️ 🔥 🎉 🤔 👀 ✅ ❌ 🤯 💯
Agent convention: Include
<<react:EMOJI>>
at the start of a response. The outbound router strips it and calls
setMessageReaction
before sending text.
typescript
// grammy API
await bot.api.setMessageReaction(chatId, messageId, [
  { type: "emoji", emoji: "👍" }
]);
Telegram支持固定的emoji反应集合,常用的有:👍 👎 ❤️ 🔥 🎉 🤔 👀 ✅ ❌ 🤯 💯
Agent约定: 在响应开头添加
<<react:EMOJI>>
标记,出站路由器会先剥离该标记并调用
setMessageReaction
接口,再发送文本内容。

Replies

消息回复

typescript
// grammy API — reply to a specific message
await bot.api.sendMessage(chatId, text, {
  reply_parameters: { message_id: targetMessageId }
});
Already wired in the adapter via
RichSendOptions.replyTo
. The agent uses
<<reply:MSG_ID>>
directive.
typescript
// grammy API — reply to a specific message
await bot.api.sendMessage(chatId, text, {
  reply_parameters: { message_id: targetMessageId }
});
适配器已通过
RichSendOptions.replyTo
支持该能力,Agent使用
<<reply:MSG_ID>>
指令触发回复。

Media

媒体处理

Supports photo, video, audio, voice, and document sending/receiving:
typescript
// Send
await telegramChannel.sendMedia(chatId, "/path/to/file.jpg", { caption: "Look at this" });

// Receive — handled by bot.on("message:photo") etc.
// Downloads via Bot API getFile → local /tmp/joelclaw-media/
// Emits media/received Inngest event for pipeline processing
File size limit: 20MB download via Bot API (larger files need direct Telegram API).
支持图片、视频、音频、语音和文档的收发:
typescript
// Send
await telegramChannel.sendMedia(chatId, "/path/to/file.jpg", { caption: "Look at this" });

// Receive — handled by bot.on("message:photo") etc.
// Downloads via Bot API getFile → local /tmp/joelclaw-media/
// Emits media/received Inngest event for pipeline processing
文件大小限制:通过Bot API下载最大支持20MB(更大的文件需要调用直接Telegram API)。

Streaming (ADR-0160)

流式传输(ADR-0160)

Progressive text updates with cursor:
typescript
import { begin, pushDelta, finish, abort } from "./telegram-stream";

// On prompt dispatch
begin({ chatId, bot, replyTo });

// On each text_delta event
pushDelta(delta);

// On message_end
await finish(fullText);
  • Plain text during streaming (no parse_mode) — avoids broken HTML on partial content
  • HTML formatting only on
    finish()
    — final edit with
    parse_mode: "HTML"
  • Throttled edits: 800ms minimum between API calls
  • Cursor:
    appended during streaming, removed on finish
  • initialSendPromise
    awaited in
    finish()
    to prevent race conditions
带光标效果的渐进式文本更新:
typescript
import { begin, pushDelta, finish, abort } from "./telegram-stream";

// On prompt dispatch
begin({ chatId, bot, replyTo });

// On each text_delta event
pushDelta(delta);

// On message_end
await finish(fullText);
  • 流式传输过程中使用纯文本(无
    parse_mode
    )——避免部分内容导致的HTML损坏
  • 仅在
    finish()
    调用时应用HTML格式:使用
    parse_mode: "HTML"
    完成最终编辑
  • 编辑限流:API调用间隔最少800ms
  • 光标:流式传输过程中末尾追加
    ,传输完成后移除
  • finish()
    中会等待
    initialSendPromise
    执行完成,避免竞态条件

Inline Buttons & Callbacks (ADR-0070)

内联按钮与回调(ADR-0070)

typescript
// Send message with buttons
await sendTelegramMessage(chatId, "Choose:", {
  buttons: [
    [{ text: "✅ Approve", action: "approve:item123" }],
    [{ text: "❌ Reject", action: "reject:item123" }],
  ]
});

// Callback handler fires telegram/callback.received Inngest event
// Then edits message to show action taken + removes buttons
Callback data max: 64 bytes. Format:
action:context
.
typescript
// Send message with buttons
await sendTelegramMessage(chatId, "Choose:", {
  buttons: [
    [{ text: "✅ Approve", action: "approve:item123" }],
    [{ text: "❌ Reject", action: "reject:item123" }],
  ]
});

// Callback handler fires telegram/callback.received Inngest event
// Then edits message to show action taken + removes buttons
回调数据最大长度:64字节,格式为:
action:context

Commands

指令

  • /stop
    — abort current turn without killing the daemon.
  • /esc
    — alias for
    /stop
    .
  • /kill
    — hard stop: disables launchd service + kills process. Emergency use only.
  • /stop
    —— 中止当前轮次,不杀死守护进程
  • /esc
    ——
    /stop
    的别名
  • /kill
    —— 强制停止:禁用launchd服务 + 杀死进程,仅紧急情况使用

Configuration

配置

Currently via environment variables (migrating to
~/.joelclaw/channels.toml
per ADR-0162):
Env VarPurpose
TELEGRAM_BOT_TOKEN
Grammy bot token
TELEGRAM_USER_ID
Joel's Telegram user ID (only authorized user)
当前通过环境变量配置(根据ADR-0162正在迁移到
~/.joelclaw/channels.toml
):
环境变量用途
TELEGRAM_BOT_TOKEN
Grammy bot令牌
TELEGRAM_USER_ID
Joel的Telegram用户ID(唯一授权用户)

Security

安全

  • Single-user lockdown — middleware drops all messages from users other than
    TELEGRAM_USER_ID
  • No token in config
    channels.toml
    references
    agent-secrets
    key names, not raw tokens
  • Media downloads to
    /tmp/joelclaw-media/
    with UUID filenames (no path traversal)
  • 单用户锁定 —— 中间件会丢弃所有非
    TELEGRAM_USER_ID
    用户发送的消息
  • 配置中无明文令牌 ——
    channels.toml
    引用
    agent-secrets
    的键名,而非原始令牌
  • 媒体下载
    /tmp/joelclaw-media/
    目录,使用UUID作为文件名(避免路径遍历风险)

Troubleshooting

问题排查

Bot not receiving messages

Bot无法接收消息

  1. Check gateway is running:
    cat /tmp/joelclaw/gateway.pid && ps aux | grep daemon.ts
  2. Check Telegram polling started:
    grep "telegram.*started" /tmp/joelclaw/gateway.log
  3. Verify token:
    curl https://api.telegram.org/bot<TOKEN>/getMe
  4. Check polling errors in stderr:
    rg "telegram.channel.start_failed|failed to start polling|getUpdates" /tmp/joelclaw/gateway.err
  5. Check ownership lifecycle telemetry:
    • joelclaw otel search "telegram.channel.poll_owner" --hours 1
    • joelclaw otel search "telegram.channel.retry_scheduled" --hours 1
If you see repeated 409 conflicts, another bot process is polling the same token. Telegram phone/desktop apps are not Bot API pollers and do not cause
getUpdates
contention.
  1. 检查网关是否正在运行:
    cat /tmp/joelclaw/gateway.pid && ps aux | grep daemon.ts
  2. 检查Telegram轮询是否已启动:
    grep "telegram.*started" /tmp/joelclaw/gateway.log
  3. 验证令牌有效性:
    curl https://api.telegram.org/bot<TOKEN>/getMe
  4. 检查stderr中的轮询错误:
    rg "telegram.channel.start_failed|failed to start polling|getUpdates" /tmp/joelclaw/gateway.err
  5. 检查所有权生命周期遥测数据:
    • joelclaw otel search "telegram.channel.poll_owner" --hours 1
    • joelclaw otel search "telegram.channel.retry_scheduled" --hours 1
如果看到重复的409冲突,说明有另一个Bot进程正在轮询相同的令牌。Telegram手机/桌面应用不属于Bot API轮询器,不会导致
getUpdates
资源竞争。

Messages arriving but no response

消息已送达但无响应

  1. Check command queue:
    grep "command-queue\|enqueue" /tmp/joelclaw/gateway.log | tail -10
  2. Check pi session health:
    grep "session\|prompt" /tmp/joelclaw/gateway.log | tail -10
  3. Check outbound routing:
    grep "outbound\|response ready" /tmp/joelclaw/gateway.log | tail -10
  1. 检查指令队列:
    grep "command-queue\|enqueue" /tmp/joelclaw/gateway.log | tail -10
  2. 检查pi会话健康状态:
    grep "session\|prompt" /tmp/joelclaw/gateway.log | tail -10
  3. 检查出站路由:
    grep "outbound\|response ready" /tmp/joelclaw/gateway.log | tail -10

Streaming not working

流式传输不工作

  1. Verify
    text_delta
    events:
    grep "text_delta" /tmp/joelclaw/gateway.log | tail -5
  2. Check
    telegram-stream
    lifecycle:
    grep "telegram-stream" /tmp/joelclaw/gateway.log | tail -10
  3. Common issue: model does tool calls before text → no deltas until after tools complete
  4. Race condition fix:
    initialSendPromise
    in
    finish()
    (commit 175c6ca)
  1. 验证
    text_delta
    事件:
    grep "text_delta" /tmp/joelclaw/gateway.log | tail -5
  2. 检查
    telegram-stream
    生命周期:
    grep "telegram-stream" /tmp/joelclaw/gateway.log | tail -10
  3. 常见问题:模型在输出文本前先调用工具 → 工具完成前不会生成delta
  4. 竞态条件修复:
    finish()
    中的
    initialSendPromise
    (提交记录175c6ca)

HTML formatting broken

HTML格式损坏

  1. Check converter output:
    TelegramConverter.convert(text)
    +
    .validate(result)
  2. Fallback: adapter auto-strips HTML and sends plain text if validation fails
  3. Streaming path sends plain text (no parse_mode), only
    finish()
    adds HTML
  1. 检查转换器输出:
    TelegramConverter.convert(text)
    +
    .validate(result)
  2. 降级逻辑:如果校验失败,适配器会自动剥离HTML发送纯文本
  3. 流式传输路径发送纯文本(无
    parse_mode
    ),仅
    finish()
    调用时添加HTML格式

Related ADRs

相关ADR

  • ADR-0042 — Media download pipeline
  • ADR-0070 — Inline buttons and callbacks
  • ADR-0160 — Telegram streaming UX
  • ADR-0162 — Reactions, replies, and channel configuration
  • ADR-0042 —— 媒体下载管道
  • ADR-0070 —— 内联按钮与回调
  • ADR-0160 —— Telegram流式传输UX
  • ADR-0162 —— 消息反应、回复和频道配置