把 CodeBuddy CLI 部署在云服务器上、通过 Telegram 远程指挥它干活,是”随身 AI 编程助手”的经典玩法。但真实跑起来后你会发现:通道正常 ≠ 好用。本文记录一次完整的故障排查与改造过程——从”发消息石沉大海”到”每一步思考实时推送到手机”,期间踩过的坑(伪终端注入误区、硬编码审批弹窗、多层 SSH 引号)都是通用经验,适用于任何类似的 CLI-agent + 消息桥接架构。
一、架构:消息到底是怎么流动的
用 --channels server:telegram 启动 CodeBuddy 后,链路是这样的:
1 | 你在 Telegram 发消息 |
三个容易想当然的点:
- 桥接消息不走终端输入,走的是 MCP 通知管道。往 TUI 的 pty 写按键是另一条路,两者互不相通。
- 过程不可见是协议设计的:只有模型显式调用
reply工具,内容才会到 Telegram。中间的思考、工具调用、卡住,用户一概看不见。 - Telegram Bot API 的
getUpdates只允许一个消费者,但sendMessage不受限——这给后面”另开一路进度推送”留了口子。
二、事故复盘:三次”没反应”的真相
第 1 次:回车被界面重绘吞了
上一个任务刚结束的瞬间(TUI 正在做收尾重绘),我发出的消息其实已经送达、已经躺进了输入框——但提交它的回车在竞态中丢了。几秒后下一次重绘把输入框草稿清掉,消息彻底消失。
症状特征:日志里能看到停在输入框里的消息文本和 ↵ send 提示,之后零活动。
第 2 次:硬编码的高危审批弹窗(Telegram 永远看不到)
补发消息后,模型开始干活,但一条含 rm -rf 的命令触发了 CodeBuddy 内置的高危操作确认框:
- 它是硬编码的:正则清单写死在二进制里,
bypassPermissions权限模式也压不住,没有任何配置项能关。 - 它只在服务器终端的 TUI 里弹出。通道的 Telegram 权限按钮走的是另一套 MCP 通知,覆盖不到这种 TUI 弹窗。
- 无人值守时,弹窗 = 整条通道死锁。你盯着 Telegram,它盯着终端,谁也等不到谁。
反编译确认后死心:配置层无解。治本思路是让模型的工具调用里根本不出现危险命令文本——见第四节。
第 3 次:按键注入的”安慰剂”
我尝试替用户按键:往进程的 /proc/<pid>/fd/3(一个指向 /dev/ptmx 的符号链接)写字节。写入”成功”了,界面上甚至能看到回显——但会话转录文件纹丝不动,弹窗岿然不动。
折腾了三轮 Esc、数字键、回车之后才意识到根本错误:
/dev/ptmx每次被 open 都会分配一个全新的伪终端。 我写的所有字节都进了那个无人读取的幽灵终端。所谓”进程 stdin 卡死”是假象,进程活得好好的,只是我的输入从未到达它。
三、两个关键技术
1. 往别人的终端打字:TIOCSTI
正确姿势是打开 pty 的 slave 端(/dev/pts/N),用 ioctl(TIOCSTI) 逐字节塞进终端输入队列(需要 root):
1 | import fcntl, os, time |
注入是否生效,不要看终端回显(它会骗你),要看会话转录文件的 mtime 是否前进。
2. 判断真相:转录文件 > TUI
CLI 会把每一次思考(reasoning)和工具调用实时写入会话转录(JSONL):
- mtime 在动 = turn 活着;mtime 冻结 = 挂起
- 最后一条事件的
reasoning字段直接告诉你模型在想什么 - TUI 每 60 秒定时重绘一遍界面,看起来”有输出”,但不代表有进展
排查通道问题时,永远先看转录,别信终端画面。
四、治本:rm → 回收站 shim
高危弹窗的死结在于:模型删除文件天然要用 rm,而命令文本含 rm 就触发弹窗。破局方式是改变语义——在 PATH 最前面放一个 shim,让 rm 变成”挪进回收站”:
1 |
|
配套两件事:
- 给 agent 写持久记忆(CodeBuddy 的 memory 机制):”所有删除一律走 shim/
mv到~/.trash,绝不使用/bin/rm;验证删除结果用ls/cat读 journal——命令文本里含 rm 也会触发弹窗”。 - 误删可恢复(回收站里有时间戳和原路径),真要硬删永远保留
/bin/rm这条路。
这个模式值得推广:凡是”agent 发出的命令会触发人肉审批”的场景,都可以用语义替换(shim)+ 持久记忆来消除死锁,而不是想办法远程替人按按钮。
五、体感升级:过程流式推送到 Telegram
“没反应”最磨人的地方是不知道它是在干活还是死了。既然每次思考/工具调用都会落进转录文件,那就 tail 它:
1 | 独立守护进程(systemd 常驻) |
要点:
- 发送直接走 Bot API 的
sendMessage,与通道的getUpdates轮询互不冲突(API 只限制后者单消费者) - token 从桥接的 MCP 配置里读,白名单取
access.json的allowFrom——不引入新凭据 - 关键细节:attach 时要从文件尾部回读一次、初始化”最后事件时间”,否则服务在事故中途上线时,卡住告警永远不会触发(我们的第一版就栽在这)
- 每条消息间隔 ≥3 秒,规避 Telegram 频控
上线后的体感:📩 收到消息 → 🤔 思考中 → ⚙️ 正在执行 xx → ✅ 完成 →(最终回复)。终于”看得见它在干活”,卡住也会在 3 分钟内收到 ⏸ 告警。
六、通用避坑清单
- 多层 SSH 引号必剥变量:
ssh host 'sudo -i bash -c "… $VAR …"'里$VAR会在本地被剥成空串。正确做法是本地写好脚本,ssh host 'sudo -i bash -s' < script.sh管道进去。 - 写
/proc/*/fd/*里的 ptmx 符号链接 = 开新终端,不是往原终端写。要注入用 TIOCSTI 打 slave 端。 - 无人值守 + 人肉审批 = 死局。用 shim 语义替换 + agent 持久记忆消灭危险命令文本,或换成可远程应答的审批通道(如 MCP permission 通知 + inline 按钮)。
- turn 收尾瞬间是消息丢失高发期。重要指令避开”刚干完活”的时点,或靠进度推送器的 ⏸ 告警兜底。
- 给桥接加 ack 反馈:收到消息立刻回一个 emoji reaction / “typing…” 状态,用户至少知道消息进来了。
- 回执、进度、告警三路分离:最终答案走 reply(触发通知),过程流走推送器,卡死告警独立触发——任何一路静默都还有另外两路。
七、结语
这次改造后,远程 AI 编程助手的工作流变成了:Telegram 下指令 → 手机上看实时过程流 → 干完收到正式回复 → 卡住 3 分钟内收到告警 → 误删可从回收站捞回。核心经验一句话:先弄清消息真实流动的管道,再谈可靠性;能用语义替换消灭的审批死锁,就不要建造更精巧的”远程按键”机器。