| name | telegram-agents |
| description | Configure and manage Telegram-connected Claude agents with heartbeat scheduling on macOS. Use when the user wants to "set up Telegram agent", "add heartbeat", "configure Telegram channel", "管理 Telegram agent", "配置心跳", "添加定时任务", "Telegram 多 agent", "配置 Telegram", "添加 Telegram agent", "给 Telegram bot 创建身份", "设置定时任务", "新增定时任务", "每天X点执行Y", "schedule a task", "run this automatically", "set up a cron job", or encounters Telegram polling conflicts, 409 Conflict, file upload failures through proxy, or needs to add recurring/periodic automated task execution for Claude agents. |
Telegram Agents
让多个 Claude agent 各自绑定独立 Telegram bot,通过 tmux 常驻,配合 launchd 定时发送心跳消息。
架构
agents.yaml(统一配置)
│
├── tmux session: channels
│ ├── window: sage ──── 官方 telegram@claude-plugins-official(channel server,轮询 getUpdates)
│ ├── window: herald 各自独立 bot token + 状态目录
│ ├── window: ... 一个 session 管理所有 agent
│ └── [每个 window 一个 agent 身份文件]
│
└── launchd(每分钟触发)
│
dispatcher.py
│ 匹配 cron 表达式
└── 心跳消息 → Telegram Bot API → channel session 接收处理
核心原则:
- 双插件分工(最易踩错,先看这条):双向 channel 由官方
telegram@claude-plugins-official 提供——只有它内置 channel server(轮询 getUpdates 把消息注入会话),所以 --channels 永远指向它。本仓库(agent-steroids)的 telegram 插件只负责管理层(本 skill、dispatcher、tg-* 命令)+ send-only 的 telegram-notify MCP(供普通 session 发通知),它没有 channel server,绝不能用作 --channels 目标。⚠️ 别因为本 skill 住在 agent-steroids 就把 --channels 改成 telegram@agent-steroids——那样 poller 不会启动、消息完全不注入。
- 收发分离:channel session(官方插件)负责双向对话;其他普通 session 用
telegram-notify MCP 仅发通知。
- Bot 隔离:每个 agent 独立 bot token + 独立状态目录,天然无 409 冲突。
- 调度解耦:dispatcher.py 不知道 Claude 存在,只做匹配时间 + 发 Telegram 消息。
配置
~/.config/telegram-agents/agents.yaml 是唯一配置源:
tmux_session: channels
restart_schedule: "0 4 * * *"
agents:
sage:
state_dir: telegram-sage
agent: sage
dir: ~/projects/my-project
heartbeats:
- schedule: "0 9 * * *"
prompt: 早报摘要
- schedule: "0 20 * * *"
prompt: 晚间回顾
customer-bot:
enabled: false
state_dir: telegram-customer
agent: customer-bot
dir: ~/projects/customer
user_session: customer-user
heartbeats:
- schedule: "0 9 * * *"
prompt: 推送每日内容
default:
state_dir: telegram
agent: my-assistant
dir: ~
常用 cron 表达式:
| 表达式 | 含义 |
|---|
0 9 * * * | 每天 9:00 |
0 */6 * * * | 每 6 小时 |
0 9 * * 1-5 | 工作日 9:00 |
*/30 * * * * | 每 30 分钟 |
0 0 1 * * | 每月 1 号 0:00 |
定时重启
常驻的 channel session 在启动时加载插件(skill 列表、hook、MCP server)。插件更新后,已运行的 session 不会自动感知结构性变更。restart_schedule 通过定时重建 tmux window 解决这个问题。
dispatcher 匹配到 restart_schedule 时,依次对每个 agent 执行:从 tmux pane 状态栏捕获 session ID → kill tmux window → 用 --resume <session-id> 重建 window(与 /tg-restart 命令相同的逻辑)。这样重启后 agent 保持之前的会话上下文。重启当分钟跳过心跳发送,避免向正在启动的 agent 发消息。
心跳机制
dispatcher.py 每分钟被 launchd 触发,读取 agents.yaml,找到当前时间命中的 heartbeat,通过 Telegram Bot API 的 sendMessage 向对应的 channel session 发送消息。
消息格式:[定时任务 YYYY-MM-DD HH:MM] <prompt>
- 时间戳:防止消息被去重忽略,保证每次触发都是独立事件
[定时任务] 前缀:作为协议标记,让 agent 身份文件可以包含针对此前缀的特定处理指令
Agent 身份文件的系统提示词中应包含心跳处理说明,例如:
Agent 身份
每个 channel session 绑定一个 agent 身份文件,赋予 bot 独立人格和能力范围。
文件位置:~/.claude/agents/<name>.md(全局)或 .claude/agents/<name>.md(项目内)
格式:
---
name: sage
description: |
<触发条件,支持 example 块>
model: inherit
color: purple
memory: user
---
<系统提示词:角色定义、能力范围、交流风格、心跳处理规则>
注意事项:
description 多行内容必须用 | block scalar,否则解析失败、agent 无法加载
name 只能用小写字母、数字和连字符(3-50 字符)
color 只接受 8 种值:red, blue, green, yellow, purple, orange, pink, cyan
memory: user 用于全局 agent(跨项目记忆),memory: project 用于项目 agent
回复排版:Telegram reply 默认传 format: "markdownv2" 以及完整转义规则属于 Telegram 通用约定,写在用户全局 CLAUDE.md 的「Telegram Channel 交互」章节,所有接入 Telegram 的会话自动加载,不需要在每个 agent 身份文件里重复。
首次安装
不配置心跳的场景:如果只需要 Telegram channel session(双向对话),不需要定时心跳,可以跳过 dispatcher 和 launchd 的安装。只需创建 agents.yaml、状态目录和 agent 身份文件,然后启动 tmux session。
配置心跳的完整安装:
- 创建
~/.config/telegram-agents/,复制 ${SKILL_PATH}/scripts/dispatcher.py 和 auth.py 进去
- 安装依赖:
pip3 install pyyaml telethon 'python-socks[asyncio]'
- 运行
python3 ~/.config/telegram-agents/auth.py 完成默认用户的 Telethon 认证(一次性,输入手机号 + Telegram App 内验证码)。这使 dispatcher 能以用户身份发送消息给 bot,bot 的 getUpdates 才能收到
- 创建
~/Library/LaunchAgents/com.$USER.telegram-agents.plist,每分钟触发,StartInterval: 60
- dispatcher 通过
cat dispatcher.py | python3 执行(绕过 macOS provenance 限制)
为什么需要 Telethon:Telegram Bot API 的 sendMessage 发出的消息不会出现在 bot 自己的 getUpdates 中——这是 Telegram 的设计限制。Dispatcher 使用 Telethon(User API)以用户身份发送心跳消息,bot 的 channel session 才能正常接收。使用 Telegram Desktop 的公开 API 凭据,安全性等同于官方客户端。
多用户认证
当 bot 面向的不是 bot 拥有者本人,而是其他 Telegram 用户时,心跳需要以该用户的身份发送,否则 bot 的 access control(allowFrom)会拒绝消息。
架构:每个需要独立用户身份的 agent 在 agents.yaml 中指定 user_session 字段,指向一个独立的 Telethon session 文件。不指定则使用默认的 user.session。
添加新用户认证:
- 在 agents.yaml 中为 agent 添加
user_session: <session-name>
- 运行
python3 ~/.config/telegram-agents/auth.py <session-name>,以该用户的手机号完成认证
- 生成
~/.config/telegram-agents/<session-name>.session
auth.py 支持命名参数:不带参数创建默认 user.session,带参数创建命名 session。
dispatcher 行为:发送心跳时按 session 分组,同一 session 的心跳共用一个 TelegramClient 连接。缺失 session 文件时跳过对应 agent 并记录错误日志,不影响其他 agent。
关键陷阱:
| 陷阱 | 原因 | 解决 |
|---|
AbandonProcessGroup: true | launchd 主进程退出后会杀进程组,后台任务被终止 | plist 中加此键 |
| PATH 极简 | launchd 只有 /usr/bin:/bin,找不到 python3/pip 等 | plist 的 PATH 中补充 Homebrew、~/.local/bin、~/.bun/bin 等 |
| macOS provenance | Claude 创建的文件带 com.apple.provenance,launchd 拒绝直接执行 | cat | python3 管道执行 |
| MTProto 被墙 | 中国大陆直连 Telegram MTProto 服务器会失败 | dispatcher 从 all_proxy/http_proxy 环境变量检测代理,但 launchd 环境没有这些变量——必须在 plist 的 bash 脚本中显式 export,否则 Telethon 会直连超时 |
添加 Agent
方向:BotFather 建 bot → 创建状态目录 → 写 agent 身份 → 更新 agents.yaml → 在 tmux session 中添加 window。
状态目录结构:
~/.claude/channels/telegram-<name>/
├── .env # TELEGRAM_BOT_TOKEN=...(chmod 600)
└── access.json # 权限控制(见下方格式)
access.json 必须预填 bot 拥有者的 Telegram user ID,否则 bot 启动后进入 pairing 模式(要求用户发配对码),增加不必要的操作步骤。从已有 agent 的 access.json 中复制 allowFrom 列表即可:
{
"dmPolicy": "allowlist",
"allowFrom": ["<owner_telegram_user_id>"],
"groups": {},
"pending": {}
}
查找自己的 user ID:cat ~/.claude/channels/telegram/access.json | grep allowFrom
Channel session 管理:
所有 agent 共用一个 tmux session(名称在 agents.yaml 的 tmux_session 中定义),每个 agent 一个 window。
tmux new-session -d -s <tmux_session> -n <name> -c <dir> "<command>"
tmux new-window -t <tmux_session> -n <name> -c <dir> "<command>"
其中 command 为:
TELEGRAM_STATE_DIR=~/.claude/channels/<state_dir> \
claude --channels 'plugin:telegram@claude-plugins-official' \
--agent <agent> --dangerously-skip-permissions \
--settings '{"enabledPlugins": {"telegram@claude-plugins-official": true}}'
默认 agent(state_dir=telegram)不需要设置 TELEGRAM_STATE_DIR。
启动前检查 409 冲突:ps eww 查找已有 bun 进程使用同一 token,有则先 kill。
心跳端到端测试
首个配置心跳的 agent 必须经过此验证。确认整条链路通畅:dispatcher → Telethon → Telegram → bot getUpdates → channel session 处理。
方向:
- 确认用户已完成 Telethon 认证(
~/.config/telegram-agents/user.session 存在)。未认证则先引导运行 auth.py
- 在 agents.yaml 中为目标 agent 添加一条临时心跳,schedule 设为 1-2 分钟后触发
- 等待 launchd 触发 dispatcher
- 检查
/tmp/telegram-agents.log 确认出现 "Sent heartbeat for agent=..."
- 检查 agent 的 tmux window 确认收到消息并开始处理(
tmux capture-pane 搜索 定时任务)
- 验证通过后删除临时心跳
常见问题排查:
- 日志显示 "Session file not found" → 运行
auth.py
- 日志显示 "Session not authorized" → 重新运行
auth.py
- 日志显示网络错误 → 检查代理配置(launchd 环境需要在 plist 中设置 proxy 环境变量)
- 日志无任何输出 → 检查
launchctl list | grep telegram-agents,确认 plist 已加载
- bot 端无反应 → 确认 bot 的 channel session 在 tmux 中运行
- agent 在线但收不到消息 → 用
--debug-file 启动检查是否有 spawn bun ENOENT,确认 bun 在 PATH 中(launchd 重启的 agent 继承精简 PATH)
全局 Telegram 配置
- 从
~/.claude/settings.json 的 enabledPlugins 中删除 telegram@claude-plugins-official(禁止普通 session 轮询),不要卸载插件本身
- 在
~/.claude.json 的 mcpServers 中注册 send-only 的 telegram-notify MCP,token 读取顺序:$TELEGRAM_BOT_TOKEN 环境变量 → ~/.claude/channels/telegram/.env
Bun Proxy 文件上传问题
仅影响使用 HTTP 代理的环境,无代理可跳过。
现象:通过 Telegram plugin 发送文件时报 Network request for 'sendDocument' failed!
根因:grammy 用 ReadableStream 构造 multipart body,Bun 1.3.x 的 fetch 在通过 proxy 发送 ReadableStream body 时有 TLS 记录排序 bug(oven-sh/bun#17434)。文本消息不受影响。
Workaround:在 Telegram plugin 的 server.ts 中,用 undici 的 fetch + ProxyAgent 替代文件上传请求。不改 grammy 源码,plugin 更新后需重新应用。
验证 Bun 是否已修复:运行 ${SKILL_PATH}/scripts/test-bun-proxy-stream.sh <chat_id>。输出 FIXED 则可移除 undici patch。
陷阱
- 409 Conflict:同一 bot token 多个 session 轮询会冲突。多 agent 方案中每个 agent 独立 token,无此问题。单 agent 场景中,普通 session 全局禁用插件可防止误启轮询
- AbandonProcessGroup:launchd plist 必须加
AbandonProcessGroup: true,否则 dispatcher 后台化任务后主进程退出,后台任务被立即终止
- macOS provenance:Claude Code 创建的脚本文件带
com.apple.provenance 扩展属性,launchd 无法直接执行。用 cat | python3(dispatcher)和 echo | bash(shell 任务)绕过
- launchd PATH:launchd 环境 PATH 极简,plist 中必须显式补充 Homebrew 路径、
~/.local/bin 和 ~/.bun/bin。Telegram 插件的 MCP server 用 bun 启动(.mcp.json 中 "command": "bun"),缺少 bun 路径会导致 spawn bun ENOENT,agent 看似正常运行但完全收不到消息
- launchd dispatcher 只能在 GUI 会话里加载:SSH 登录是 Background 域(
launchctl managername 返回 Background),launchctl bootstrap gui/$(id -u) <plist> 会报 125: Domain does not support specified action。必须在 Mac 本机 Terminal(Aqua 域)执行加载,或等下次登录由 ~/Library/LaunchAgents 自动加载