npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bubbolink/cli

v2026.6.4

Published

Relay-first CLI for pairing and bridging OpenClaw gateway traffic

Downloads

1,660

Readme

@bubbolink/cli

把本地 AI runtime(OpenClaw 或 Hermes)接入 Bubbolink relay 服务的独立 CLI。

手机端 App 通过 relay 跟本地 runtime 对话,CLI 负责:

  • 配对:把手机 App 账号和本地 runtime 绑定到同一个 relay gateway
  • 桥接:长时间运行一个中继 worker,把 relay 队列里的请求拉下来、喂给本地 runtime、再把事件发回 relay
  • 守护:用系统服务(launchd / systemd / Task Scheduler)让桥接在后台常驻

深入文档

  • docs/PAIR_FLOW.mdbubbolink pair 完整流程:每一步做了什么、改了哪些文件、--runtime all 模式遇到 runtime 缺失怎么处理、对 OpenClaw 配置改动的安全性分析、故障排查 checklist
  • docs/EXTERNAL_AGENTS.md — claude / codex 等外部 agent 接入指南
  • docs/LOGGING.md — 日志格式与排查
  • docs/PLATFORM.md — 跨平台细节(launchd / systemd / Windows)
  • bubbolink-server/TOOL_EVENTS.md — 工具调用链事件 schema(OpenClaw plugin / Hermes / Codex / Claude → server session.tool 字段映射 + 真实样例)

1. 整体架构

┌────────────────┐    HTTPS / SSE    ┌───────────────────────┐
│   Mobile App   │◀─────────────────▶│   Relay Server (Go)   │
│  (nexus_flutter)│                  │  go-relay-test.cece.com   │
└────────────────┘                   │                       │
                                     │  MySQL + Redis        │
                                     └───────────┬───────────┘
                                                 │
                                 long-poll  +   POST events
                                                 │
                                     ┌───────────▼───────────┐
                                     │   bubbolink CLI (Node) │
                                     │   bin/bubbolink        │
                                     │                       │
                                     │  ┌─────────────────┐  │
                                     │  │ Bridge worker   │  │
                                     │  └────────┬────────┘  │
                                     └───────────┼───────────┘
                                                 │
                ┌───────────────┬────────────────┼────────────────┬──────────────┐
                │               │                │                │              │
        (runtime=openclaw) (runtime=hermes) (runtime=claude) (runtime=codex)     │
                │               │                │                │              │
        HTTP /v1/responses   bubbolink-      bubbolink-      bubbolink-       (其他 runtime
                │            agent hermes    agent claude   agent codex       走相同 shim)
                ▼               ▼                ▼                ▼              ▼
        ┌────────────────┐  ┌────────────┐  ┌────────────┐  ┌────────────┐
        │ OpenClaw GW    │  │ Hermes JS  │  │ Claude CLI │  │ Codex MCP  │
        │ (外部进程)      │  │ HTTP/cmd   │  │ stream-json│  │ stdio      │
        │ 127.0.0.1:<p>  │  │ backend    │  │            │  │            │
        └────────────────┘  └────────────┘  └────────────┘  └────────────┘

核心架构(重构后):4 种 runtime 走完全统一的 long-poll 模式,CLI 端不做协议翻译。

启动 (任意 runtime bridge)
  │
  1. POST /v1/pair-codes/<code>/consume
       body  { client_user_id, runtime }
       resp  { gw_id, bridge_token, runtime }
  │
  2. 持久化 ~/.bubbolink/ext-<runtime>-last.json
  │
  3. long-poll loop(永不退出):
       ┌─ GET /v1/gateways/<gw>/requests/next?waitSeconds=25
       │    Bearer <bridge_token>
       │    ↓ 拿到 user request
       │
       ├─ 调本地 runtime(LLM HTTP / Claude CLI / Codex MCP / OpenClaw HTTP)
       │
       ├─ 流式 POST /v1/gateways/<gw>/requests/<rid>/events
       │    body 是该 runtime 的 native 事件(stream-json / OpenAI SSE chunk / codex/event 等)
       │    ↑↑↑ 服务端 internal/adapter/<runtime>/ 翻译为统一信封
       │
       └─ 心跳 POST /v1/gateways/<gw>/meta(每 10s)

关键特征(重构后)

  • CLI bridge 不知道统一 Agent SSE 协议(/v1/agents/threads/* 端点)的存在
  • CLI bridge 直接发 runtime 自己的原生事件,服务端 adapter/<runtime>/ 做 native → unified envelope 翻译
  • 之前 CLI 端的 src-ext/core/unified_relay_shim.mjs 翻译层 + src-shared/agent-client/ HTTP 封装已整体删除
  • 4 个 bridge(OpenClaw / Hermes / Claude / Codex)共用同一套 relayWorker long-poll 主循环

三个关键角色:

| 角色 | 位置 | 职责 | | ---------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Relay Server | bubbolink-server/(Go) | 握住 App ↔ runtime 之间的所有流量;internal/adapter/<runtime>/ 把 native 事件翻成统一信封 | | CLI | bin/bubbolink + bin/bubbolink-agent(Node ≥22)| 配对 + 桥接;4 种 runtime 共用 relayWorker long-poll 主循环;不做协议翻译 | | Runtime | OpenClaw gateway / Hermes(JS) / Claude CLI / Codex CLI | 实际执行 AI turn 的进程或本地 LLM 调用 |

4 种 runtime bridge 对比

| Runtime | 启动命令 | 入口文件 | native 协议(POST 给服务端的事件格式) | 服务端 adapter | | -------- | ------------------------- | --------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------- | | OpenClaw | bubbolink bridge | src/tunnel.mjs + src/tunnel_proxy.mjs + src/tunnel_state.mjs | OpenClaw /v1/responses SSE chunk | internal/adapter/openclaw | | Hermes | bubbolink-agent hermes | src-ext/runtime/hermes/index.mjs | OpenAI-compatible SSE / NDJSON | internal/adapter/hermes(共享 openclaw 逻辑) | | Claude | bubbolink-agent claude | src-ext/runtime/claude/index.mjs | Claude CLI stream-json | internal/adapter/claude | | Codex | bubbolink-agent codex | src-ext/runtime/codex/index.mjs | Codex MCP codex/event notification | internal/adapter/codex |

4 个 bridge 入口文件大小都 ≤ 800 行硬限,最大约 700 行。


2. 代码结构

bubbolink-cli/
├── bin/
│   ├── bubbolink              # 用户入口(setup / pair / bridge / service / tunnel)
│   ├── bubbolink-agent        # bridge 子进程入口(claude / codex / hermes)
│   └── bubbolink-mcp-stdio    # codex 反向 MCP stdio bridge
├── src/                       # OpenClaw bridge + tunnel + 共享工具
│   ├── core.mjs               # URL / code 归一化 / 路径解析等
│   ├── tunnel.mjs             # OpenClaw bridge 主循环(277 行)
│   ├── tunnel_proxy.mjs       # OpenClaw HTTP / SSE 转发(476 行)
│   ├── tunnel_state.mjs       # 文件锁 / 状态机(301 行)
│   └── tunnel_service.mjs     # OpenClaw 桥接的 service install/uninstall
├── src-shared/                # 纯基础设施(无业务依赖)
│   ├── logger.mjs             # 结构化 JSON 日志
│   ├── correlation.mjs        # cid 生成 / 解析
│   └── relay-defaults.json    # 默认 relay URL
├── src-ext/                   # External Agents(Claude / Codex / Hermes)
│   ├── bin.mjs                # bubbolink-agent 入口分发
│   ├── commands/              # pair / service / agent / reload / help 子命令
│   ├── core/
│   │   ├── pairCodeClient.mjs   # POST /v1/pair-codes/<code>/consume + 状态持久化
│   │   ├── relayWorker.mjs      # 4 个 bridge 共用的 long-poll 主循环
│   │   ├── relayWorkerHttp.mjs  # bridge_token Bearer header / fetchJson 工具
│   │   ├── sessionSlot.mjs      # 单 session 槽状态机
│   │   ├── stateMachine.mjs     # 通用 FSM
│   │   ├── approvalGateway.mjs  # HITL 工具审批
│   │   ├── mcpStdioClient.mjs   # MCP stdio 客户端(codex 用)
│   │   ├── bubbolinkToolsRegistry.mjs / bubbolinkToolsServer.mjs # 反向工具注入
│   │   ├── accountResolver.mjs  # accountId/clientUserId 解析
│   │   └── usageReporter.mjs    # token 用量上报
│   ├── runtime/
│   │   ├── claude/index.mjs   # Claude bridge(spawn `claude` CLI + stream-json,223 行)
│   │   ├── codex/index.mjs    # Codex bridge(spawn `codex` MCP stdio,240 行)
│   │   └── hermes/index.mjs   # Hermes bridge(OpenAI-compatible HTTP / subprocess,256 行)
│   ├── service/
│   │   └── serviceManager.mjs # launchd / systemd / Task Scheduler 统一接入
│   └── openclaw-plugin/       # bubbolink-hooks OpenClaw 插件源码(详见 docs/PLUGIN_ACTIVE_RUNS.md)
└── test/                      # node --test 测试套件(72 个 test)

Hermes 已 100% JS 化 —— 旧 runtime/hermes_bridge/ Python 目录已删除。bin/bubbolink 中所有 Python 探测 / spawn 逻辑也已清除。

已删除的代码(重构清理)

  • src-ext/core/unified_relay_shim.mjs —— 整个文件删除。之前 CLI 在这里把 legacy {kind: ...} 事件翻译成统一信封 + POST 到 /v1/agents/threads/<tid>/events,现在改由服务端 adapter 翻译
  • src-shared/agent-client/ —— 整个目录删除。CLI bridge 不再调统一协议端点,因此不需要 /v1/agents/* HTTP + SSE 客户端
  • 各 runtime 子目录里的 unified 翻译辅助文件(如 usageReporter 的 envelope 包装等)

bin/bubbolink 本身只 spawn bubbolink-agent 子进程;openclaw CLI 是外部可执行文件,通过 PATH / OPENCLAW_HOME 定位。


3. 4 种 runtime 模式

CLI 通过 runtimeKind 分流。所有 runtime 在线协议层完全一致 —— 4 个 bridge 共用 src-ext/core/relayWorker.mjs long-poll 主循环,通过相同的 3 个端点跟 relay 通讯:

  • POST /v1/pair-codes/<code>/consume(启动配对)
  • GET /v1/gateways/<gw>/requests/next?waitSeconds=25(长轮询拉用户消息)
  • POST /v1/gateways/<gw>/requests/<rid>/events(流式回 native 事件)
  • POST /v1/gateways/<gw>/meta(10s 心跳)

唯一的差异是 bridge 内部对接的本地 runtime 进程,以及推回 relay 的 native 事件格式。CLI 不做协议翻译,服务端 internal/adapter/<runtime>/ 把 native 事件统一翻译为 unified envelope 落库 agent_events 表。

3.1 OpenClaw 模式(默认)

 App ──► Relay ──► CLI bridge (Node) ──► HTTP POST /v1/responses ──► OpenClaw Gateway
                                                                          │
                                     events  ◄──────────────── SSE / JSON ┘
                                     (publish back to relay)
  • 入口:bubbolink bridgesrc/tunnel.mjs 主循环 + tunnel_proxy.mjs HTTP / SSE 转发 + tunnel_state.mjs 状态机)
  • OpenClaw 是独立的外部进程(openclaw gateway run),监听 127.0.0.1:<port>
  • CLI 把 relay long-poll 拉下来的请求转成 POST /v1/responses,再把流式响应原样切片发回 relay
  • 网关鉴权:CLI 从 ~/.openclaw/config.jsongateway.auth.token
  • 服务端 internal/adapter/openclaw 把 OpenClaw 的 SSE chunk 翻译为统一信封事件

3.2 Hermes 模式(pure JS,已替代旧 Python bridge)

 App ──► Relay ──► bubbolink-agent hermes ──► OpenAI-compatible HTTP
   long-poll        (Node 22+ stdlib fetch)        (or external command)
  • 入口:bubbolink-agent hermessrc-ext/runtime/hermes/index.mjs,256 行)
  • relayWorker long-poll:拿到 user request → 调本地 LLM → 流式回 OpenAI-compatible SSE chunk 给 relay
  • 后端 2 选 1:
    • HTTPHERMES_BRIDGE_API_BASE_URL + HERMES_BRIDGE_API_KEY(OpenAI / DeepSeek / 自建 vLLM 等)
    • subprocessHERMES_BRIDGE_COMMAND="<path-to-binary>"(newline-delimited JSON 协议)
  • 本地 transcript 存在 ~/.bubbolink/hermes/sessions/<sessionId>.json
  • 零 Python 依赖

3.3 Claude 模式

 App ──► Relay ──► bubbolink-agent claude ──► spawn `claude` CLI (stream-json)
  • 入口:bubbolink-agent claudesrc-ext/runtime/claude/index.mjs,223 行)
  • spawn 外部 claude CLI(Anthropic 官方),从 stdout 读 stream-json,直接作为事件 POST 到 /v1/gateways/<gw>/requests/<rid>/events
  • 服务端 internal/adapter/claude 解析 stream-json,把 assistant_message_delta / tool_use_block / parent_tool_use_id 等翻译成统一信封事件(content_block.delta / tool_use / subagent run_id 等)
  • 工具审批走 approvalGateway:PreToolUse hook → 推 interrupt block → 等 App 端 resume

3.4 Codex 模式

 App ──► Relay ──► bubbolink-agent codex ──► spawn `codex` MCP stdio server
  • 入口:bubbolink-agent codexsrc-ext/runtime/codex/index.mjs,240 行)
  • 通过 MCP stdio 协议跟 codex CLI 通信,反向注入 bubbolink-mcp-stdio 作为工具服务器
  • codex/event notification 直接序列化为事件 POST 给 relay
  • 服务端 internal/adapter/codexcodex/event 翻译为统一信封

4 种 runtime 使用独立的 relay 凭证和独立的 session 模型(见 AppRuntimeKind 枚举),配对流程里由 App 端决定写哪一组。


4. Relay 协议

CLI 跟 relay 之间只走 HTTP(长轮询 + POST),没有 WebSocket。关键端点:

4.1 配对阶段

| 端点 | 方向 | 用途 | | ------------------------------------------ | ----------- | -------------------------------------------------------------------- | | POST /v1/pair-sessions | App → Relay | App 创建 4 位 pair code(含 accountId / runtimeKind) | | GET /v1/pair-sessions/{code} | CLI → Relay | (openclaw 模式)查配对 session 是否存在、读出 App 设定的 runtimeKind | | POST /v1/pair-codes/{code}/consume | CLI → Relay | 统一入口:消费 4 位码,body {client_user_id, runtime} → 颁发 {gw_id, bridge_token, runtime} |

兼容性:服务端仍保留 POST /api/ext/pair-codes/<code>/consume?runtime=<rt>(legacy 老 CLI 用),新 CLI 默认调 /v1/pair-codes/...,404 / 410 时回退老端点(升级期间双端兼容)。

4.2 桥接阶段

| 端点 | 方向 | 用途 | | ----------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------- | | GET /v1/gateways/{gwId}/requests/next?waitSeconds=N | CLI/bridge → Relay | long-poll 下一个 request(204 表示超时) | | POST /v1/gateways/{gwId}/requests/{reqId}/events | CLI/bridge → Relay | 推 native 事件(start / chunk / end / error / response),服务端 adapter 翻译为 unified envelope | | POST /v1/gateways/{gwId}/chat-events | CLI → Relay | OpenClaw 模式下发送结构化聊天事件(供 App 做 structured history) | | POST /v1/gateways/{gwId}/meta | CLI/bridge → Relay | presence 心跳(online / connected / state),默认 10s 一次 | | GET/DELETE /v1/gateways/{gwId}/binding | CLI → Relay | 查询 / 解绑当前 gateway | | POST /v1/gateways/{gwId}/reset | CLI → Relay | 硬重置(bubbolink reset) |

鉴权:桥接阶段所有端点用 Authorization: Bearer <bridge_token>(pair-codes consume 颁发,单向 producer-only)。

bridge_token vs client_token

CLI 持有的是 bridge_token(producer 凭证):能 long-poll 拉 user request、能 POST events 推回。 App 侧持有的是 client_token(consumer 凭证):能 GET /v1/agents/threads/<tid>/stream 订阅、能查询 thread 状态。 两个 token 互不可换用,混用返回 403。

4.3 事件形状

桥接 worker 发回 relay 的事件统一是:

{
  "event": "start|chunk|end|error|response",
  "sessionKey": "agent:<id>:relay-gw:<gwId>::<sessionKey>",
  "runId": "...",
  "statusCode": 200,
  "contentType": "text/event-stream; charset=utf-8",
  "data": "...",
  "createdAtMs": 1713312000000
}

sessionKey 格式前后端不一致:App 侧带装饰前缀(agent: / relay-gw:),relay 事件来的是裸 key。两边比较必须归一化(Go 端 normalizeSessionKeyForMatch,Flutter 端 _sessionKeyMatchesForPreview)。


5. 典型流程

5.1 配对(bubbolink pair <code>,统一 long-poll 模式)

4 种 runtime 走完全一致的配对流程(OpenClaw 多一步本地 daemon 重启,详见 docs/PAIR_FLOW.md):

   App                          Relay                          CLI
    │                             │                             │
    │ 1. POST /v1/pair-sessions   │                             │
    │    {runtime, accountId,     │                             │
    │     clientUserId}           │                             │
    ├────────────────────────────▶│                             │
    │                             │                             │
    │ 2. App 屏幕显示 4 位 code   │                             │
    │                             │                             │
    │                             │   3. 用户在 CLI 输入 code   │
    │                             │                             │
    │                             │ 4. POST /v1/pair-codes/     │
    │                             │      <code>/consume         │
    │                             │    {client_user_id,         │
    │                             │     runtime}                │
    │                             │◀────────────────────────────┤
    │                             │                             │
    │                             │ 5. {gw_id, bridge_token,    │
    │                             │     runtime}                │
    │                             ├────────────────────────────▶│
    │                             │                             │
    │                             │   6. 持久化到               │
    │                             │      ~/.bubbolink/          │
    │                             │      ext-<runtime>-last.json│
    │                             │                             │
    │                             │   7. 进入 long-poll loop:   │
    │                             │      GET .../requests/next  │
    │                             │◀────────────────────────────┤
    │                             │                             │
    │ 8. App 端订阅 thread        │                             │
    │    GET /v1/agents/threads/  │                             │
    │    <tid>/stream             │                             │
    ├────────────────────────────▶│                             │

配对完成判定 = /pair-codes/<code>/consume 返回 gw_id + bridge_token。CLI 立刻进入 long-poll 主循环,不需要再等 App 端"确认"。

持久化位置(按 runtime 分文件,互不干扰):

~/.bubbolink/ext-openclaw-last.json
~/.bubbolink/ext-hermes-last.json
~/.bubbolink/ext-claude-last.json
~/.bubbolink/ext-codex-last.json

文件内容:{ gw_id, runtime, relay_url, bridge_token, saved_at }session_id 字段保留作老 CLI 兼容(值与 gw_id 相同)。

5.2 桥接(bubbolink bridge,OpenClaw 模式)

 ┌──────────┐                    ┌──────────┐              ┌────────────┐
 │  CLI     │  long-poll 90s     │  Relay   │              │  OpenClaw  │
 │  bridge  │───────────────────▶│          │              │  Gateway   │
 │          │◀───────────────────│          │              │ /v1/resp.. │
 │          │   request          │          │              │            │
 │          │                    │          │              │            │
 │          │─── POST /v1/resp ─────────────────────────▶  │            │
 │          │◀── SSE stream ──────────────────────────────  │            │
 │          │                    │          │              │            │
 │          │── POST events ────▶│          │              │            │
 │          │   (start/chunk/    │          │              │            │
 │          │    end)            │          │              │            │
 │          │                    │          │              │            │
 │          │── POST meta ──────▶│          │              │            │
 │          │   (heartbeat 10s)  │          │              │            │
 └──────────┘                    └──────────┘              └────────────┘
  • Bridge 使用 acquireBridgeLock 做文件锁,防止一台机器上两个 bridge 抢同一个 gateway
  • SIGINT/SIGTERM 时发一次 state: "stopped" presence,让 App 立刻看到离线(而不是等 25s TTL)

5.3 桥接(Hermes / Claude / Codex 模式 — 统一 long-poll)

3 个 External Agent runtime 共用同一套主循环,只是本地 backend 不同:

  bubbolink-agent <hermes|claude|codex>
    │
    ├── relayWorker.run(handler):
    │     loop:
    │       req = GET /v1/gateways/<gw>/requests/next?waitSeconds=25
    │       if 204 → continue   // long-poll 超时
    │       handler(req, ctx) →
    │
    ├── handler 实现(按 runtime 分流):
    │     hermes: fetch(POST OpenAI /chat/completions, stream:true)
    │             ↓ 每个 SSE chunk: ctx.publishEvent({kind:"chunk", data:...})
    │     claude: spawn `claude` CLI(stream-json mode)
    │             ↓ 每个 stream-json 事件: ctx.publishEvent({kind:"chunk", data:...})
    │     codex:  MCP stdio 双向通讯 + 反向工具注入
    │             ↓ 每个 codex/event: ctx.publishEvent({kind:"chunk", data:...})
    │
    ├── ctx.publishEvent → POST /v1/gateways/<gw>/requests/<rid>/events
    │     body 是 native 事件,**不在 CLI 翻译**
    │
    └── 心跳 POST /v1/gateways/<gw>/meta(每 10s)

服务端 internal/adapter/<runtime>/ 接到 native 事件后翻译为统一信封写入 agent_events 表。

Hermes 会话历史持久化在 ~/.bubbolink/hermes/sessions/<sessionId>.json。无 Python,无 venv,无 pip install。


6. 安装 & 快速开始

npm i -g @bubbolink/cli

OpenClaw 模式(默认):

# 1) 准备 OpenClaw 配置 + relay 凭证
bubbolink setup \
  --relay http://<relay-host>:8090 \
  --public-base http://<gateway-public-host>:<port>

# 2) 启动本地 OpenClaw gateway
openclaw gateway run --bind loopback --port <port> --force

# 3) 用 App 里生成的 code 完成配对
bubbolink pair 123456

# 4) 把桥接装成后台服务
bubbolink service install

Hermes 模式(pure JS,无 Python 依赖):

# 配置后端:HTTP(OpenAI-compatible)
export HERMES_BRIDGE_API_BASE_URL=https://api.openai.com/v1
export HERMES_BRIDGE_API_KEY=sk-xxx
export HERMES_BRIDGE_MODEL=gpt-4o-mini

# 或者用 subprocess 后端(外部 binary,NDJSON 协议)
# export HERMES_BRIDGE_COMMAND=/path/to/my-llm-script

# 配对(CLI 自动检测后端 env,未配置会报错)
bubbolink pair 123456 --runtime hermes

# 前台跑(调试)
bubbolink bridge --runtime hermes

Claude / Codex External Agents:

# Claude Code / Codex CLI 需要先在本机安装并登录

# 配对后会自动安装后台服务
bubbolink pair 1234 -r claude
bubbolink pair 1234 -r codex

# 前台调试运行(当前终端内运行,Ctrl+C 可停止)
bubbolink bridge -r claude
bubbolink bridge -r codex

重装 App 后重新配对(典型场景):

# 如果之前是 hermes 模式,配对报 "这台主机已经绑定过设备"
bubbolink reset --runtime hermes          # 只清 hermes 凭证,不动 openclaw
bubbolink pair <new-code> --runtime hermes

# openclaw 模式不传 --runtime 即可
bubbolink reset
bubbolink pair <new-code>

7. 命令参考

bubbolink setup [--relay URL] [--public-base URL]              # 写配置 + 装权限
bubbolink pair <code> [--runtime openclaw|hermes|claude|codex] # 配对;claude/codex 默认会自动装后台服务
bubbolink bridge [--runtime openclaw|hermes|claude|codex]      # 前台运行桥接;claude/codex 用 Ctrl+C 停止
bubbolink service install|stop|uninstall|restart|status [--runtime ...] # 后台服务;claude/codex 中 stop 是 uninstall 别名
bubbolink uninstall --runtime claude|codex                     # claude/codex 完整卸载:停服务 + 清本地配对状态
bubbolink reset [--runtime openclaw|hermes] [--gateway GWID]   # 硬重置绑定
bubbolink pair-url [--account ID] [--code CODE]                # 生成手动配对 URL
bubbolink tunnel on <port> [--relay URL]                       # 暴露本地 HTTP 端口
bubbolink tunnel off <port> [--relay URL]                      # 关闭端口转发
bubbolink tunnel ls                                            # 查看 tunnel 状态
bubbolink status [--runtime openclaw|hermes]                   # 查当前 runtime / 服务 / 绑定

Tunnel 快速测试

# 启一个本地测试服务(任何 HTTP 服务器都行;这里用 Node 内置)
mkdir -p /tmp/bubbolink-tunnel-demo
printf 'hello tunnel\n' >/tmp/bubbolink-tunnel-demo/index.html
node -e 'import("node:http").then(({createServer})=>{import("node:fs").then(({readFileSync})=>{createServer((_,res)=>res.end(readFileSync("/tmp/bubbolink-tunnel-demo/index.html"))).listen(8080,"127.0.0.1");console.log("listening on 127.0.0.1:8080");})})'

另一个终端:

bubbolink tunnel on 8080

示例输出:

Tunnel started
Local:  http://127.0.0.1:8080
Public: https://go-relay-test.cece.com/t/p8080-03ce6ff5
Logs:   ~/.bubbolink/logs/tunnel-8080.log
Mode:   systemd-user

说明:

  • tunnel agent 会每 10 秒向 relay 发 heartbeat
  • relay 超过约 35 秒没收到 heartbeat,会把 tunnel 视为 offline,并对公网请求直接返回 503 tunnel offline
  • bubbolink tunnel on <port> 会直接打印公网访问地址,示例里的 Public: https://... 就是可分享的访问 URL
  • 如果不传 --relay,默认使用 CLI 当前内置或环境变量指定的 relay 地址
  • 后台守护优先走当前平台的服务管理:
    • Linux: systemd --user
    • macOS: launchd
    • Windows: Task Scheduler
  • 如果当前环境没有可用的服务管理器,会自动回退到 detached 进程,并在 bubbolink tunnel on 输出里提示

Tunnel 技术实现

这套 tunnel 是标准的反向内网穿透,不要求目标机器有公网 IP,也不要求目标机器开放入站端口。

核心思路:

  • 外部用户访问的是 relay server 暴露出来的公网 URL,例如 https://go-relay-test.cece.com/t/<slug>
  • 内网机器上的 bubbolink tunnel agent 主动连接 relay,通过长轮询拉取待处理请求
  • agent 在本机访问 127.0.0.1:<port>,再把响应回传给 relay
  • relay 把响应返回给浏览器或调用方

涉及的 3 个角色:

  • Client / Browser
    • 访问公网 tunnel 地址
  • Relay Server
    • 负责创建 tunnel、分配 slug、接收公网请求、转发给 agent、返回响应
  • Tunnel Agent
    • 运行在用户机器上,负责 heartbeat、拉取请求、访问本地服务、发布响应

关键接口:

  • POST /v1/tunnels
    • 创建 tunnel,返回 idagentTokenpublicUrl
  • GET /v1/tunnels/:id/requests/next
    • agent 长轮询拉取待处理请求
  • POST /v1/tunnels/:id/heartbeat
    • agent 上报存活状态
  • POST /v1/tunnels/:id/responses/:reqId/publish
    • agent 发布本地服务响应
  • GET /t/:slug/*path
    • 公网入口,外部流量从这里进入 tunnel

工作流程:

sequenceDiagram
    participant U as "User / Browser"
    participant R as "Bubbolink Relay Server"
    participant A as "Bubbolink Tunnel Agent"
    participant L as "Local Service (127.0.0.1:8080)"

    A->>R: "POST /v1/tunnels"
    R-->>A: "id + agentToken + publicUrl"
    A->>R: "POST /v1/tunnels/:id/heartbeat"
    A->>R: "GET /v1/tunnels/:id/requests/next"

    U->>R: "GET /t/:slug/api/echo?x=1"
    R-->>A: "返回待处理 request"
    A->>L: "GET http://127.0.0.1:8080/api/echo?x=1"
    L-->>A: "200 + headers + body"
    A->>R: "POST /v1/tunnels/:id/responses/:reqId/publish"
    R-->>U: "200 + headers + body"

在线状态与恢复机制:

  • agent 每 10s 发送一次 heartbeat
  • server 超过约 35s 没收到 heartbeat,会把 tunnel 判定为 offline
  • offline 状态下,公网请求会直接返回 503 tunnel offline
  • 后台运行优先使用系统服务托管:
    • Linux: systemd --user,root 环境额外支持 systemd system service
    • macOS: launchd
    • Windows: Task Scheduler
  • 如果 agent 进程异常退出,服务管理器会自动重启;如果没有服务管理器,则退回 detached 进程模式

实现边界:

  • 当前 tunnel 第一版只支持 HTTP 请求/响应转发
  • 支持 path、query、JSON/body、常见响应头透传
  • 暂不包含 WebSocket、任意 TCP、UDP 转发
  • 公网入口当前使用路径路由 /t/<slug>,没有使用独立子域名

复杂路由跳转的适配说明:

  • 大多数复杂 Web 系统都可以正常工作,包括:
    • SPA 前端路由,例如 react-routervue-router
    • 后端页面路由,例如 /admin/users/list
    • 带 query 的跳转,例如 /orders?page=2&status=paid
    • 常见的 301/302/307/308 重定向
  • 这类场景通常没问题,因为 tunnel 会保留:
    • 请求 path
    • 原始 query string
    • 常规请求头与响应头
    • HTTP 状态码
    • x-forwarded-host
  • 真正容易出问题的通常不是“路由复杂”,而是“应用把绝对地址写死”:
    • 例如后端返回 Location: http://127.0.0.1:8080/login
    • 或前端代码里写死 http://内网IP:端口/...
    • 或登录回调地址、Cookie domain、绝对资源地址强依赖固定 host
  • 推荐应用侧尽量使用相对路径,或者正确读取反向代理后的 host/proto 信息生成跳转地址
  • 如果系统强依赖 WebSocket、HMR dev server、长连接 upgrade,当前版本需要单独验证,不能默认保证完全可用

固定的 curl 用例:

# 1) GET 根路径
curl -i https://go-relay-test.cece.com/t/p8080-03ce6ff5

# 2) GET 带 path + query
curl -i 'https://go-relay-test.cece.com/t/p8080-03ce6ff5/api/echo?name=bubbolink&mode=query'

# 3) POST JSON 到带 path + query 的入口
curl -i \
  -X POST \
  'https://go-relay-test.cece.com/t/p8080-03ce6ff5/api/echo?source=curl&lang=zh' \
  -H 'content-type: application/json' \
  -d '{"hello":"world","from":"bubbolink"}'

看 agent 日志:

tail -f ~/.bubbolink/logs/tunnel-8080.log

关闭:

bubbolink tunnel off 8080

bubbolink reset 的行为:

| 命令 | 作用对象 | | -------------------------------- | --------------------------------------------------- | | bubbolink reset | 重置 openclaw 凭证(并额外清空 hermes 凭证,兜底) | | bubbolink reset --runtime hermes| 清 hermes 凭证,openclaw 绑定不动 |

reset 做的事:

  • 调 relay DELETE /v1/gateways/{id}/binding 清服务端绑定(配对码 / 会话 / 联系人)
  • 卸载目标 runtime 的后台服务(launchd / systemd / Task Scheduler)
  • 停掉目标 runtime 正在跑的桥接进程
  • ~/.bubbolink/config.json 本地凭证:
    • openclaw → 重新生成 relayGatewayId + 轮换 token,并连带清空 hermes 凭证
    • hermes → 只删 hermesRelayGatewayId / hermesRelayClientToken / hermesRelayGatewayToken

reset 完成后即可对同一 runtime 发起新的 bubbolink pair

code 接受 1234123456XXXX-XXXX(参考 normalizeProvidedCode)。


8. 服务管理

| 平台 | 机制 | 单元文件 | | ------------------ | ----------------- | ----------------------------------------------------------- | | macOS | launchd | ~/Library/LaunchAgents/com.bubbolink.bridge[-hermes].plist | | Linux (systemd) | systemd user unit | ~/.config/systemd/user/bubbolink-bridge[-hermes].service | | Linux (无 systemd) | — | 只能手动 bubbolink bridge,service install 会报友好错误 | | Windows | Task Scheduler | task name Bubbolink Bridge |

OpenClaw 和 Hermes 两种 runtime 的服务名不同(参考 serviceNames(runtimeKind)),可以同时装一台机上跑两种:

bubbolink service status                         # 默认看 openclaw
bubbolink service status --runtime hermes        # 看 hermes 服务
bubbolink service install                        # 装 openclaw bridge(默认)
bubbolink service install --runtime hermes       # 装 hermes bridge
bubbolink service uninstall [--runtime hermes]   # 停 + 删
bubbolink service restart   [--runtime hermes]   # 重启

日志:

# OpenClaw 桥接日志
tail -f ~/.bubbolink/logs/bridge.stdout.log
tail -f ~/.bubbolink/logs/bridge.stderr.log

# Hermes 桥接日志
tail -f ~/.bubbolink/logs/bridge-hermes.stdout.log
tail -f ~/.bubbolink/logs/bridge-hermes.stderr.log

Claude / Codex(External Agents)

Claude / Codex 走的是独立的 bubbolink-agent service 管理实现,命令入口仍然是顶层 bubbolink

| 平台 | 机制 | 单元文件 | | ------------------ | ----------------- | ----------------------------------------------------------- | | macOS | launchd | ~/Library/LaunchAgents/com.bubbolink.agent.<runtime>.plist | | Linux (systemd) | systemd user unit | ~/.config/systemd/user/bubbolink-agent-<runtime>.service | | Linux (无 systemd) | — | 只能手动 bubbolink bridge -r <runtime> | | Windows | Task Scheduler | Bubbolink\\Agent-<runtime> |

常用命令:

# 配对后自动装后台服务
bubbolink pair 1234 -r claude
bubbolink pair 1234 -r codex

# 查看后台状态
bubbolink service status -r claude
bubbolink service status -r codex

# 重装后台服务
bubbolink service restart -r claude
bubbolink service restart -r codex

# 只停掉后台服务,但保留本地配对状态
bubbolink service stop -r claude
bubbolink service stop -r codex

# `service uninstall` 对 claude/codex 与 `service stop` 等价
bubbolink service uninstall -r claude
bubbolink service uninstall -r codex

# 完整卸载:停服务并清掉本地配对状态(~/.bubbolink/ext-<runtime>-last.json)
bubbolink uninstall -r claude
bubbolink uninstall -r codex

# 前台调试运行;停止方式就是 Ctrl+C
bubbolink bridge -r claude
bubbolink bridge -r codex

说明:

  • bubbolink pair <code> -r claude|codex 成功后,会自动执行后台服务安装。
  • claude/codex 来说,bubbolink service stop -r <runtime>bubbolink service uninstall -r <runtime> 在当前实现里效果相同,都会卸载对应 service,但不会删除本地配对状态。
  • 如果你想彻底解绑本机上的 Claude / Codex,请使用 bubbolink uninstall -r <runtime>,它会在停服务之外再清掉本地状态文件。

9. 状态与持久化

| 路径 | 内容 | | -------------------------------------------- | ------------------------------------------------------------- | | ~/.openclaw/openclaw.json | OpenClaw gateway 配置(模型、端口、profile、relay 配置片段) | | ~/.bubbolink/config.json | CLI 级别状态(runtimeKind、4 套 relay 凭证) | | ~/.bubbolink/hermes/sessions/<sid>.json | Hermes JS bridge 的会话历史持久化 | | ~/.bubbolink/bridge-launch.sh | OpenClaw 桥接的 launchd/systemd 启动脚本 | | ~/.bubbolink/bridge-launch-hermes.sh | Hermes 桥接的 launchd/systemd 启动脚本 | | ~/.bubbolink/logs/ | 桥接 stdout / stderr 日志 | | ~/.openclaw/bubbolink/runtime/bridge-*.lock | 桥接文件锁(按 relayUrl + gatewayId + gatewayBase 取 hash) |

~/.bubbolink/config.json 里两套凭证并存:

  • OpenClawrelayGatewayId / relayClientToken / relayGatewayToken
  • HermeshermesRelayGatewayId / hermesRelayClientToken / hermesRelayGatewayToken

runtimeKind 字段记录最近一次配对的 runtime。敏感字段(gateway.auth.token / channels.*.appSecret)在 debug 输出里会被脱敏成 ***


10. 环境变量

| 变量 | 作用 | | ----------------------------------------------------------------------- | ---------------------------------------------------------------- | | OPENCLAW_HOME | 覆盖 OpenClaw 主目录(默认 ~/.openclaw) | | BUBBOLINK_RELAY_URL | 覆盖 relay 基址(默认 https://go-relay-test.cece.com) | | BUBBOLINK_RELAY_TOKEN | Bearer token(配对/绑定端点用) | | OPENCLAW_BUBBOLINK_GATEWAY_BASE_URL | 覆盖本地 gateway 基址 | | OPENCLAW_GATEWAY_PORT / OPENCLAW_GATEWAY_HOST | 显式指定 host/port | | BUBBOLINK_RELAY_GATEWAY_ID / _CLIENT_TOKEN / _GATEWAY_TOKEN | 覆盖 openclaw 的 relay 凭证 | | BUBBOLINK_HERMES_RELAY_GATEWAY_ID / _CLIENT_TOKEN / _GATEWAY_TOKEN | 覆盖 hermes 的 relay 凭证 | | HERMES_BRIDGE_API_BASE_URL | Hermes JS bridge HTTP 后端:OpenAI-compatible base URL | | HERMES_BRIDGE_API_KEY | Hermes HTTP 后端 Bearer token | | HERMES_BRIDGE_MODEL | Hermes 默认模型名(gpt-4o-mini 等) | | HERMES_BRIDGE_COMMAND | Hermes 子进程后端:外部 binary 路径(NDJSON 协议) |


11. Hermes JS bridge 多 provider 与流式

Hermes JS bridge 通过 OpenAI-compatible HTTP 协议跑,任何兼容 POST /v1/chat/completions { stream: true } 的服务都能直接接:OpenAI、Azure OpenAI、DeepSeek、自建 vLLM、LiteLLM、Ollama 兼容层。要切 provider 就改 HERMES_BRIDGE_API_BASE_URL

流式默认按 OpenAI SSE 帧解析(data: 前缀 + [DONE] 终止符),逐 chunk 透传 为 unified content_block.delta,最终一帧合并为 content_block.completed

不需要外部多 provider 配置文件 — 后端切换通过 env 完成:

# OpenAI 官方
export HERMES_BRIDGE_API_BASE_URL=https://api.openai.com/v1
export HERMES_BRIDGE_MODEL=gpt-4o-mini

# DeepSeek
export HERMES_BRIDGE_API_BASE_URL=https://api.deepseek.com/v1
export HERMES_BRIDGE_MODEL=deepseek-chat

# 本地 vLLM / Ollama 兼容层
export HERMES_BRIDGE_API_BASE_URL=http://127.0.0.1:8000/v1
export HERMES_BRIDGE_MODEL=qwen2.5

需要走非 OpenAI 协议(如 Anthropic 原生 /messages)?用 subprocess 后端:写 一个外部 binary 实现 stdin JSON → stdout NDJSON,然后 export HERMES_BRIDGE_COMMAND=/path/to/my-binary。bridge 会 spawn 它跑每个 turn。

模型选择用 <provider>/<model-id>(例 minimax/MiniMax-M2.7)。桥接先匹配配置的 provider,没匹配到才回退到默认 Hermes API server。


12. 已知陷阱

  1. relay_client_token 可能在多设备共享 — 删除绑定前必须走 canUseClientTokenDeleteFallback 检查,避免误删别人的 binding
  2. Session key 格式前后不一致 — App 侧装饰(agent:xxx:relay-gw:GW-A::...),relay 事件是裸 key。Flutter 用 _sessionKeyMatchesForPreview 宽松匹配
  3. Windows 的 PowerShell 编码 — 服务注册时通过 Base64 传命令行,避免 CJK 路径在 cmd.exe 下被 mojibake
  4. Hermes / OpenClaw 凭证是两套AppRuntimeKind 决定写哪一组,不能互相借用;bubbolink reset 不传 --runtime 默认只重置 openclaw
  5. Hermes cron 任务与配对无关~/.hermes/cron/jobs.json 是本地文件。一次性任务(30m / 2h / 具体时间戳)执行后会自动删除,重新配对看不到是正常现象

13. 测试

需要 Node.js ≥ 22。

npm test
node --test test/cli.test.mjs

73 个测试(13 个测试文件),覆盖:

  • CLI basics / 平台路径 / OpenClaw home 解析 / XML & systemd 转义 / PID 检测 / 文件锁 / PowerShell Base64 / 服务状态 / Node 版本保护
  • pairCodeClient —— /v1/pair-codes/<code>/consume 协议契约 + legacy 端点回退
  • relayWorker —— long-poll 拉取 + publishEvent + 心跳 + 退避重试
  • claudeParser / codexEventMapper —— native 事件解析(CLI 端只解析不翻译)
  • approvalGateway / elicitationHandler —— HITL 工具审批
  • mcpStdioClient / mcpStdioBridge / bubbolinkTools —— Codex 反向 MCP 工具注入
  • usageReporter —— token 用量上报

之前 161 测试中有近一半是 unified_relay_shim 的 native → unified envelope 翻译契约测试。重构把翻译层挪到服务端 adapter 后,CLI 端只剩 73 测试覆盖 long-poll + pair-code consume + native 事件解析这些核心路径。

CI:

  • GitLab CI (.gitlab-ci.yml):Node 22 on Alpine
  • GitHub Actions (.github/workflows/test.yml):Ubuntu / macOS / Windows × Node 22

| 平台 | 状态 | | ----------------------------------- | ---------------------------- | | macOS (launchd) | 完全支持 | | Linux + systemd | 完全支持 | | Linux 无 systemd(Alpine / OpenRC) | 只支持手动 bubbolink bridge | | Windows (Task Scheduler) | 支持,CI 覆盖 |