内容
="center">
<img src="https://raw.githubusercontent.com/Omnith/trunkline/main/assets/banner.png" alt="trunkline" width="500">
</p>
<p align="center">
<strong>☎️ 允许不同机器上的代理通过 HTTP 或 MCP 相互通信。</strong>
</p>
<p align="center">
<a href="https://github.com/Omnith/trunkline/actions"><img src="https://github.com/Omnith/trunkline/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="https://github.com/Omnith/trunkline/releases"><img src="https://img.shields.io/github/v/release/Omnith/trunkline" alt="Release"></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields/License-MIT-blue.svg="License: MIT"></</p>
<p align=" <a href="trunkline.omnith.com">网站</a> •
<a href="#安装</a> •
<a href="https://trunkline.omnith/troubleshooting">故障排除</a>
</p>
---
遇到一个有趣的问题:我有两个代理,一个生产 CI 运行器堆栈,另一个为项目消耗它,我手动协调消息来回,以实现消费者报告的问题发现所做的。
标签选择,笔记本电脑 `arm64` 与台式机 `x64`,需要避免信息流和访问的竞争条件,因为运行器提交修复,消费者尝试并行工作并注意到这些修复,但除非我告诉它我正在中继信息(然后回复它),否则它不会认为自己的更改已得到解决…… 经典的两个自主代理之间的协调,对吧?
这让我停下来思考:
> 两个自主编码代理(Claude Code 会话,CI 机器人可以运行 CLI 的东西)在问题的两端工作,不应该需要人类在它们之间中继消息。
因此,`tr` 应运而生小型服务器,两个代理入:代理 **注册** 到一个电话簿,**呼 彼此,**发送** 消息,并在另一方不监听时留下 **语音邮件**。交付至少一次进行,确保消息在代理之间传递时不会丢失。
核心是一个服务器,提供两个接口,一个 JSON HTTP API 和一个 MCP 端点;以及一个为后台和 MCP 连接而构建的客户端 CLI,用于与服务器通信。
## 安装
###一次,在一台机器上
**Docker(推荐):**
```bash
docker run -d --name trunkline -p :4747 -v trunkline:/data \
ghcr.io/omnith/trunkline:latest
```
或者使用 docker-compose:`docker compose up -d`(参见 `docker-compose.yml`)。状态(SQLite + 事件日志)存储在 `trunkline` 卷中;`-p` 映射是唯一需要配置的网络暴露。
或者,从克隆仓库构建:`docker compose up -d --build`(在同一镜像名称下标记本地构建)从源码:**
```bash
corepack enable
pnpm install; pnpm run build
export TRUNKLINE_BIND="0.0.0.0" # 暴露到 localhost 之例如,在您的 mesh/VPN 上)
node dist/trunkline.js serve # 默认监听 :4747
```
为每个允许加入的代理生成一次性邀请:
```bash
# docker: docker exec trunkline node dist/trunkline.js admin invite --name alice
# from source: nodeunkline.js adminname alice
```
`` / `admin revoke <name>` 管理电话簿。配置是故意仅限本地的 - 没有远程管理界面。从源码编译后,使用与服务器相同的 `TRUNKLINE_DB` 运行 `admin`(它直接编辑数据库);在 docker 中,共享卷自动满足此要求。优先使用上述命名卷 - 主机绑定到 `/data` 必须由 uid 1000 预先拥有,否则非 root 服务器无法写入。
### 客户端:每个代理/机器
(上面的服务器块是 PowerShell bash。在 Windows PowerShell 上,使用 `$env:NAME = "value"` 代替 `export`。)
```bash
npm i -g trunkline # Node >= 22.12; 首次安装获取预编译的 sqlite,需要几秒钟
export TRUNKLINE_URL=http://<服务器-ip>:4747
trunkline register --nameinvite tl-invite-XXXX 打印您的 token 一次
export TRUNKLINE_TOKEN=tl_... # 保持安全和秘密;token身份
```
<details>
<summary>从源码(开发)</summary>
```bash
git clone https://github.com/Omnith/trunkline.git && cd trunkline
corepack enable # 随 Node 22 捆绑 - 使 p用
# 开发工具(eslint 10)还需要 Node 22.13+ 在 22.x 线上
pnpm install && pnpm run build && pnpm link --global # 将 `trunkline` 放入您的 PATH
# (如果 pnpm 抱怨全局 bin 目录,请运行 `pnpm setup` 一次并重新打开 shell)
```
</details>
完成上述操作。 可选地添加 MCP 接口(相同的 token):
```bash
claude mcp add --transport http trunkline $TRUNKLINE_URL/mcp \
--header "Authorization: Bearer $TRUNKLINE_TOKEN"
```
## 使用电话(代理备忘录)
呼叫需要对等方:每台机器都注册其 **自己的** 名称,而您拨打 **其他** 代理的名称(如下:您是 `alice`,您的对等方是 `bob`)。
驱动电话有两种方法:
- 在一个回合内或回合之间使用 **MCP 工具**(相同的 10 个动词)例如查看活动会话 `history` 以查找新消息
- 使用 **CLI** 进行在回合之间唤醒您的后台铃声(MCP `listen` 工具在 60 秒后超时并且无法后台运行)
`listen`/`inbox` 打印确切的 `trunkline ack --through <id>` 行,以便您在处理一批后运行。使用 `listen --ack` 在交付时自动确认,并使用 `ack --all` 清除整个收件```bash
trunkline phonebook # 已注册正在监听
trunkline call bob --subject "构建 42" -m "新构建已上线"
trunkline listen --wait 3600 # 铃声 - 见下文
trunkline inbox 偷窥语音邮件(从不消耗)
tr ack --through 7 # 消息在您确认之前会重新发送
trunkline send --to bob -m "..." # 回复到您的通话(重新打开已结束的通话)
git diff | trunkline send --to bob # 将任何内容作为消息体进行管道trunkline threads # 列出您的通话及其 #id
tr history 3 重新阅读通话
trunkline hangup 3 --note "完成,谢谢" # 结束通注释作为 [系统] 到达
```
**铃声使基于回合的代理可行的模式:以 **后台任务** 运行 `trunkline listen --wait 3600`。进程在有事情发生时立即退出,这会用消息唤醒代理程序:
```
exit 0 消息已 在 stdout 上,准备处理,然后确认
exit 2 窗口超时,空 - 只需再次监听
exit 1 暂时性网络/服务器)- 回退,重新监听
```
未确认的消息是持久的,并且无论如何都会重新传递。
**保持快速。** 您做的每个工具调用都会使您付出整个思考-行动周期;电话本身是毫秒级的。 四条规则:
1. **铃声已经传递了消息** - `listen` 在退出时打印它们。 从其输出中处理它们; 不要使用 `inbox`/`history` 重新获取。
2. **在单个 shell 调用中批量动词** (`inbox; ack --all; send ...` 链接)动词一次调用。
3. **在一个回合中回复:** `trunkline send --thread 3-through 7 -m "..."`。
4. **在前台运行 `listen`,也从不管道它** (`| tail` 会吃掉退出代码 - 退出 0 已传递 / 2 空 / 1 错误是信号)。
### 工作原理
- **身份验证 = 身份。** 每个代理的 bearer token(服务器仅存储哈希),通过一次性邀请生成。无开放注册。
- **至少一次传递。** 每个代理的光标仅在显式 `ack` 时才会前进;其后的一切都会在每次 `listen`/`inbox` 时重新传递 - 重新启动(SQLite)后。
- **通线程,而不是会话。** `hangup` 是建议性的;向已结束的通话发送消息会重新打开它,而空闲通话会在 24 小时后过期。无脆弱状态机。
- **可观察。** 每个操作都会将一个宽 JSON 事件发出到 `.jsonl` 文件,因此您可以 `grep`/`jq` 穿过任何会话以进行法医分析。
### 配置(环境变量)
| 变量 | 默认值 | |
|---|---|---|
| `TRUNKLINE_PORT` | `4747` | 服务器端口 |
| `TRUNKLINE_BIND` | `127.0.1` | 设置 `0.0.0` 以公开 |
| `TRUNKLINE_DB `./trunkline.db` | SQLite 文件 |
| `TRUNKLINE_EVENTS` | `./trunkline.events.jsonl`规范事件日志 |
| `TRUNKLINE_THREAD_TTL_HOURS` | `24` | 空闲通窗口 |
| `TRUNKLINE_URL` / `LINE_TOKEN` | — | 客户端必填 |
### 开发
```bash
pnpm test # vitest,内存 SQLite
pnpm run typecheck && pnpm run lint
pnpm run build # tsup -> dist/trunkline.js
```
节点 ≥。CI 在 Windows 和 macOS 上运行完整的编译检查。