telegram
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTelegram 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 → JoelKey files:
- — channel adapter (inbound + outbound)
packages/gateway/src/channels/telegram.ts - — streaming UX (progressive text updates)
packages/gateway/src/telegram-stream.ts - — response routing
packages/gateway/src/outbound/router.ts - —
packages/gateway/src/channels/types.tsinterfaceChannel
SDK: — Bot instance at module scope, exposed via .
grammy@1.40.0getBot()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 ; non-owners stay passive/send-only and retry lease acquisition with backoff.
getUpdates
Conflict guard still applies for non-cooperative pollers: (with metadata) + + .
telegram.channel.start_failedconflicttelegram.channel.retry_scheduledtelegram.channel.polling_recoveredJoel (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 - —— 流式传输UX(渐进式文本更新)
packages/gateway/src/telegram-stream.ts - —— 响应路由
packages/gateway/src/outbound/router.ts - ——
packages/gateway/src/channels/types.ts接口定义Channel
SDK: —— Bot实例位于模块作用域,通过对外暴露。
grammy@1.40.0getBot()多实例轮询所有权(2026-03-05更新):Telegram长轮询现在对每个Bot令牌哈希使用Redis租约。
- 所有者键:
joelclaw:gateway:telegram:poll-owner:<tokenHash> - 状态键:
joelclaw:gateway:telegram:poll-status:<tokenHash> - 仅持有租约的所有者会轮询接口;非所有者保持被动/仅发送状态,并通过退避机制重试获取租约。
getUpdates
针对非协作轮询器仍有冲突防护机制:触发(附带元数据) + + 事件。
telegram.channel.start_failedconflicttelegram.channel.retry_scheduledtelegram.channel.polling_recoveredCapabilities
能力说明
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: for HTML-aware splitting,
TelegramConverter.chunk()for raw textchunkMessage() - Format: markdown→HTML via , with plain text fallback on validation failure
TelegramConverter.convert() - Buttons: →
InlineButton[][]reply markupinline_keyboard
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限制)
- 消息分片:使用实现HTML感知的内容拆分,
TelegramConverter.chunk()用于纯文本拆分chunkMessage() - 格式:通过将Markdown转换为HTML,校验失败时降级为纯文本发送
TelegramConverter.convert() - 按钮:转换为
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 at the start of a response. The outbound router strips it and calls before sending text.
<<react:EMOJI>>setMessageReactiontypescript
// grammy API
await bot.api.setMessageReaction(chatId, messageId, [
{ type: "emoji", emoji: "👍" }
]);Telegram支持固定的emoji反应集合,常用的有:👍 👎 ❤️ 🔥 🎉 🤔 👀 ✅ ❌ 🤯 💯
Agent约定: 在响应开头添加标记,出站路由器会先剥离该标记并调用接口,再发送文本内容。
<<react:EMOJI>>setMessageReactionReplies
消息回复
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 . The agent uses directive.
RichSendOptions.replyTo<<reply:MSG_ID>>typescript
// grammy API — reply to a specific message
await bot.api.sendMessage(chatId, text, {
reply_parameters: { message_id: targetMessageId }
});适配器已通过支持该能力,Agent使用指令触发回复。
RichSendOptions.replyTo<<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 processingFile 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 — final edit with
finish()parse_mode: "HTML" - Throttled edits: 800ms minimum between API calls
- Cursor: appended during streaming, removed on finish
▌ - awaited in
initialSendPromiseto prevent race conditionsfinish()
带光标效果的渐进式文本更新:
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);- 流式传输过程中使用纯文本(无)——避免部分内容导致的HTML损坏
parse_mode - 仅在调用时应用HTML格式:使用
finish()完成最终编辑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 buttonsCallback data max: 64 bytes. Format: .
action:contexttypescript
// 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:contextCommands
指令
- — abort current turn without killing the daemon.
/stop - — alias for
/esc./stop - — hard stop: disables launchd service + kills process. Emergency use only.
/kill
- —— 中止当前轮次,不杀死守护进程
/stop - ——
/esc的别名/stop - —— 强制停止:禁用launchd服务 + 杀死进程,仅紧急情况使用
/kill
Configuration
配置
Currently via environment variables (migrating to per ADR-0162):
~/.joelclaw/channels.toml| Env Var | Purpose |
|---|---|
| Grammy bot token |
| Joel's Telegram user ID (only authorized user) |
当前通过环境变量配置(根据ADR-0162正在迁移到):
~/.joelclaw/channels.toml| 环境变量 | 用途 |
|---|---|
| Grammy bot令牌 |
| Joel的Telegram用户ID(唯一授权用户) |
Security
安全
- Single-user lockdown — middleware drops all messages from users other than
TELEGRAM_USER_ID - No token in config — references
channels.tomlkey names, not raw tokensagent-secrets - Media downloads to with UUID filenames (no path traversal)
/tmp/joelclaw-media/
- 单用户锁定 —— 中间件会丢弃所有非用户发送的消息
TELEGRAM_USER_ID - 配置中无明文令牌 —— 引用
channels.toml的键名,而非原始令牌agent-secrets - 媒体下载 到目录,使用UUID作为文件名(避免路径遍历风险)
/tmp/joelclaw-media/
Troubleshooting
问题排查
Bot not receiving messages
Bot无法接收消息
- Check gateway is running:
cat /tmp/joelclaw/gateway.pid && ps aux | grep daemon.ts - Check Telegram polling started:
grep "telegram.*started" /tmp/joelclaw/gateway.log - Verify token:
curl https://api.telegram.org/bot<TOKEN>/getMe - Check polling errors in stderr:
rg "telegram.channel.start_failed|failed to start polling|getUpdates" /tmp/joelclaw/gateway.err - Check ownership lifecycle telemetry:
joelclaw otel search "telegram.channel.poll_owner" --hours 1joelclaw 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 contention.
getUpdates- 检查网关是否正在运行:
cat /tmp/joelclaw/gateway.pid && ps aux | grep daemon.ts - 检查Telegram轮询是否已启动:
grep "telegram.*started" /tmp/joelclaw/gateway.log - 验证令牌有效性:
curl https://api.telegram.org/bot<TOKEN>/getMe - 检查stderr中的轮询错误:
rg "telegram.channel.start_failed|failed to start polling|getUpdates" /tmp/joelclaw/gateway.err - 检查所有权生命周期遥测数据:
joelclaw otel search "telegram.channel.poll_owner" --hours 1joelclaw otel search "telegram.channel.retry_scheduled" --hours 1
如果看到重复的409冲突,说明有另一个Bot进程正在轮询相同的令牌。Telegram手机/桌面应用不属于Bot API轮询器,不会导致资源竞争。
getUpdatesMessages arriving but no response
消息已送达但无响应
- Check command queue:
grep "command-queue\|enqueue" /tmp/joelclaw/gateway.log | tail -10 - Check pi session health:
grep "session\|prompt" /tmp/joelclaw/gateway.log | tail -10 - Check outbound routing:
grep "outbound\|response ready" /tmp/joelclaw/gateway.log | tail -10
- 检查指令队列:
grep "command-queue\|enqueue" /tmp/joelclaw/gateway.log | tail -10 - 检查pi会话健康状态:
grep "session\|prompt" /tmp/joelclaw/gateway.log | tail -10 - 检查出站路由:
grep "outbound\|response ready" /tmp/joelclaw/gateway.log | tail -10
Streaming not working
流式传输不工作
- Verify events:
text_deltagrep "text_delta" /tmp/joelclaw/gateway.log | tail -5 - Check lifecycle:
telegram-streamgrep "telegram-stream" /tmp/joelclaw/gateway.log | tail -10 - Common issue: model does tool calls before text → no deltas until after tools complete
- Race condition fix: in
initialSendPromise(commit 175c6ca)finish()
- 验证事件:
text_deltagrep "text_delta" /tmp/joelclaw/gateway.log | tail -5 - 检查生命周期:
telegram-streamgrep "telegram-stream" /tmp/joelclaw/gateway.log | tail -10 - 常见问题:模型在输出文本前先调用工具 → 工具完成前不会生成delta
- 竞态条件修复:中的
finish()(提交记录175c6ca)initialSendPromise
HTML formatting broken
HTML格式损坏
- Check converter output: +
TelegramConverter.convert(text).validate(result) - Fallback: adapter auto-strips HTML and sends plain text if validation fails
- Streaming path sends plain text (no parse_mode), only adds HTML
finish()
- 检查转换器输出:+
TelegramConverter.convert(text).validate(result) - 降级逻辑:如果校验失败,适配器会自动剥离HTML发送纯文本
- 流式传输路径发送纯文本(无),仅
parse_mode调用时添加HTML格式finish()
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 —— 消息反应、回复和频道配置