tinyotb = Tiny Opencode-telegram Bridge——opencode server 的 Telegram 远程控制桥。常驻 daemon 把 opencode 会话事件推到 Telegram,并把你的回复 / 审批转回 opencode。
项目原始来源:codex-telegram-bridge (该项目的模块划分、渲染、daemon 结构可对照参考;它是 WebSocket/codex 后端,本项目的 opencode HTTP+SSE 部分为全新实现)。 Rust 实现,纯同步(ureq),无 async runtime,单二进制。
本项目由 AI 模型 Kimi K3 与 DeepSeek V4 Flash 协作实现。
/away开启远程模式(先确认 opencode server 可达,只推之后的事件);/back关闭;- 会话完成 / 出错时推送通知(附回答原文摘要);
- opencode 请求权限时推送通知,附「允许一次 / 总是允许 / 拒绝」内联按钮,点按钮即完成审批;
- 用 Telegram 的「回复(Reply)」回复某条通知,内容转发回对应会话;
/sessions [n]浏览最近会话、/new [提示词]新建会话、/status查看状态、/help;/project [id]查看 / 切换已注册项目(仅限 config 里预置的目录,远程不接受任意路径);- systemd 用户服务安装(
tinyotb daemon install)。
cargo build --release
install -m755 target/release/tinyotb ~/.local/bin/tinyotb# 1. 创建 Telegram bot(找 @BotFather),拿到 token;
# 把 bot 加进你的聊天,拿到 chat_id(可用 @userinfobot 之类查询)。
# 2. 配置:
tinyotb setup --bot-token 123456:ABC... --chat-id 123456789 \
--allowed-user-id 123456789 --directory /path/to/project
# 3. 检查:
tinyotb doctor
# 4. 启动 daemon(或安装为 systemd 用户服务):
tinyotb daemon install # 推荐:开机自启 + 崩溃重启
# 手动方式:
tinyotb daemon run然后到 Telegram 里发 /away,之后的事件就会推过来。
| 命令 | 说明 |
|---|---|
/away |
开启远程模式(先检查 opencode server 可达) |
/back |
关闭远程模式 |
/status |
away 状态 + server 健康 + 待审批数 + 会话数 |
/sessions [n] |
最近 n 个会话(默认 5,每条一条消息,回复即切换) |
/new [提示词] |
新建会话,可选附带首条提示词 |
/project |
列出已注册项目(● = 当前) |
/project <id> |
切换到已注册项目(daemon 热重载,无需重启) |
/help |
帮助 |
本地 CLI:
| 命令 | 说明 |
|---|---|
tinyotb setup |
写 ~/.tinyotb/config.json(0600);--add-project <路径> 可重复,注册可切换项目 |
tinyotb doctor |
检查 opencode 二进制 / server / config / Telegram token / daemon 状态 |
tinyotb status |
away 状态 + server 健康 + state 概况 |
tinyotb daemon run [--once] |
前台运行 daemon;--once 只跑一轮(调试) |
tinyotb daemon start |
启动 daemon:已装 systemd 服务走 systemctl;未装则在后台拉起(日志写 ~/.tinyotb/daemon.log) |
tinyotb daemon stop |
停止 daemon(systemd 服务或裸跑进程,按 lock 文件里的 pid) |
tinyotb daemon install / uninstall |
systemd 用户服务(~/.config/systemd/user/tinyotb.service) |
所有 CLI 输出为 JSON envelope:{ok: true, data: ...} 或 {ok: false, error: {code, message}}。daemon 的 stdout 为 JSONL 日志。
tinyotb daemon install 会在 ~/.config/systemd/user/tinyotb.service 写入 unit(开机自启 + 崩溃自动重启,Restart=on-failure),日常控制直接用 systemctl --user:
systemctl --user start tinyotb # 启动(等价:tinyotb daemon start)
systemctl --user stop tinyotb # 停止(等价:tinyotb daemon stop)
systemctl --user restart tinyotb # 重启(改完 config 后必须重启才生效)
systemctl --user status tinyotb # 查看运行状态
systemctl --user enable tinyotb # 开机自启(install 已自动做)
systemctl --user disable tinyotb # 取消开机自启
journalctl --user -u tinyotb -f # 实时查看日志注意:
- 开机自启的前提是用户会话在登录前就存在:执行一次
loginctl enable-linger $USER,让未登录时服务也能跑(远程模式正是要人不在时工作,建议开启); - 改
~/.tinyotb/config.json后需systemctl --user restart tinyotb才生效(唯一例外:/project切项目是热重载,不用重启); - opencode server 由 daemon 自动拉起(
autostart: true且不可达时),平时无需手动维护; - 卸载:
tinyotb daemon uninstall(停止 + 禁用 + 删除 unit)。
~/.tinyotb/config.json(0600;TINYOTB_STATE_DIR 可覆盖目录):
{
"telegram": { "bot_token": "...", "chat_id": 123, "allowed_user_id": 456 },
"opencode": {
"base_url": "http://127.0.0.1:4096",
"username": null,
"password": null,
"autostart": true,
"command": "opencode",
"directory": "/path/to/project"
},
"projects": [
{ "id": "tinyotb", "label": "tinyotb", "cwd": "/path/to/project" },
{ "id": "dex", "label": "dex", "cwd": "/path/to/other" }
],
"poll_interval_ms": 1000
}opencode.username/password:opencode server 的 HTTP Basic 认证(OPENCODE_SERVER_USERNAME/PASSWORD),不设则不需要;autostart:daemon 发现 server 不可达时自动opencode serve --port <base_url 端口>;base_url强制 loopback(127.0.0.1/localhost/::1),远程 URL 拒绝;allowed_user_id校验所有入站消息 / callback;不设则不做用户校验(不推荐);projects:/project的可切换注册表(轻量版多项目)。opencode.directory是当前项目;切换 = 把某个注册项的cwd设为当前目录,daemon 热重载(重建 REST 客户端与 SSE 流,opencode server 无需重启)。切换后旧的回复路由 / 待审批 / 最近会话清空。
Telegram Bot API ←→ telegram.rs ←→ daemon.rs 主循环 ←→ opencode.rs (REST)
↑ |
| ↓
sse.rs reader 线程 ←→ opencode /event (SSE)
- 纯同步:
ureq 3做 HTTP 与 SSE;SSE 按行解析,跑在专用 reader 线程,经mpsc送回主循环;断线退避重连(1s/2s/5s/10s); - 纯逻辑与 IO 分离:
sse.rs行解析、events.rs事件映射、render.rs渲染全部无 IO,可单测; - 幂等:事件经
seen_event_ids去重;会话完成用「busy/retry → idle 状态迁移」判定,session.status与session.idle双事件不会重复通知; - 重连对账:SSE 重连成功后
GET /session/status与内存状态比对,补出断线期间的完成事件; - 状态持久化:
state.json原子写(tmp + rename),读时容错;路由表 / 已见事件各保留最近 500 条; - 单实例:
daemon.lock(fs2 排他锁),防止双 daemon 造成 getUpdates 冲突。
已知限制(与 opencode 1.18.x 实测):
- 断线期间的
permission.asked不会在重连后重放(服务端不缓存),只能等用户手动处理;session.idle会重放,配合对账可补完成事件; - 事件帧无
id字段,去重不依赖服务端 id; - 权限回复 body 用
{response: "once"|"always"|"reject"},remember字段服务端未实现。
cargo test # 单元测试 + 二进制冒烟 + 端到端(mock opencode + mock Telegram)
cargo clippy --all-targetstests/e2e.rs 用 TcpListener::bind("127.0.0.1:0") 起最小 HTTP/SSE mock,spawn 真实 daemon 二进制,验证
/away → 权限按钮 → 完成通知 → 回复路由 → 回调审批 全链路。
opencode serve(或autostart: true让 daemon 拉起);- 测试 bot 发
/away→ 确认「远程模式已开启」; - 在 opencode 里发起一个会话 → 收到完成通知(含回答摘要);
- 回复该通知 → 收到「已发送」确认;
- 触发权限请求(如
"*": "ask")→ 收到带按钮通知 → 点「允许一次」→ 按钮变为「✅ 已允许一次」; /status、/sessions、/new、/back各过一遍。
MIT