远程开发好几年了,IDE 和代码都能远程,凭啥 AI 编程还得绑在终端里?折腾了一下 OpenCode + OpenChamber,现在手机、公司、家里打开浏览器就能接着写。

Why

我笔记本性能不差,但工作得用 Windows。Windows 下搞开发有多蛋疼用过的人都知道——换行符、路径、包管理器、Docker,每一样都在磨你耐心。所以好几年前我就在 Homelab 里搭了一台 7×24 的 Rocky Linux 开发机,JetBrains Gateway 连上去跑 IntelliJ + GoLand + PyCharm + WebStorm + RustRover,再挂一个 VS Code Remote,早年也用过 Cursor Remote。所有 IDE 都在服务端,笔记本就是个带屏幕的终端。

公司那边我也有一台类似的个人开发服务器。回家开发公司项目,或者到了公司想接着搞家里的实验,连回去就是同一个 workspace,不用搬代码。

代价就是断网即失业,VPN 焊死在身上。

IDE 和代码早就远程了,AI 编程咋还绑在我本地的终端里?

最早用的 Cursor,配合 Remote SSH 也能远程,但它只是个 IDE 助手,不是 Agent——你没法把浏览器一关让它自己在后台跑。后来换了 Claude Code、Kimi Code,能做 Agent 了,但绑在终端里,远程场景一下两个痛点全暴露了:

手机:路上冒出个想法想给正在跑的项目下个指令,掏出手机 SSH 到服务器敲命令行?别逗了。OpenChamber 有个 iOS App(TestFlight 就能装),连上 VPN 打开就是一个完整的 AI 编程界面——打字说人话,Agent 去干活。它还能推通知,任务跑完或者需要确认的时候手机直接弹,不用时不时掏出来刷新。

跨地点:以前用纯终端 opencode TUI,在家给个人项目开了个 session,到公司想继续搞——得 SSH 回去找 session ID restore。反着来也一样。现在会话状态全在服务端,不管在哪打开浏览器,所有 session 都在那排着,点开继续。跟 JetBrains Gateway 一个体验——连上去就是你的 workspace。

说白了就是把 AI Agent 从"命令行工具"变成了开发机上的一个常驻服务。

How

架构很简单:

手机(iOS App) ──→ VPN ──→ [nginx/HTTPS] ──→ dev 机 :4095
公司电脑 ──→ VPN ──→ [nginx/HTTPS] ──→ 同上
                               │
                      OpenChamber (Web UI)
                      监听 0.0.0.0:4095
                               │
                  OPENCODE_HOST=http://localhost:4096
                               │
                      OpenCode Server
                      仅监听 127.0.0.1:4096

OpenChamber 监听所有网口,OpenCode 只绑 127.0.0.1,中间靠 OPENCODE_HOST 环境变量通信。前面套不套 nginx 随意——直接 http://开发机:4095 也能用。我因为 Homelab 本来就有 nginx 在跑,顺手加了 HTTPS,顺便让 iOS App 的推送正常工作(Apple 那边没 TLS 不让推)。

家里和公司各部署一套,浏览器里就是两个独立的 AI 编程环境。

装 OpenCode

curl -fsSL https://opencode.ai/install | bash

二进制落到 ~/.opencode/bin/opencode,加 PATH 就行。我这边版本 v1.18.15。

三个核心插件——websearch(联网搜索)、mem(跨会话记忆)、pty(后台跑长任务)——通过 npm 包加载,需要在配置目录下装一下依赖:

cd ~/.config/opencode
npm init -y
npm install @opencode-ai/plugin@1.18.4

这包的版本跟 opencode 二进制是对应的,别手贱升级。

装 OpenChamber

pnpm add -g @openchamber/web

落到 ~/.local/share/pnpm/bin/openchamber。没 pnpm 的话 npm i -g pnpm 先。

配 systemd

俩 service,前后端分开:

/etc/systemd/system/opencode.service

[Unit]
Description=OpenCode Server
After=network.target

[Service]
Type=simple
User=milkfish
WorkingDirectory=/home/milkfish
ExecStart=/home/milkfish/.opencode/bin/opencode web --hostname 127.0.0.1
Restart=on-failure
RestartSec=5
Environment="PATH=/home/milkfish/n/bin:/home/milkfish/.local/share/pnpm/bin:/usr/local/bin:/usr/bin:/bin"

[Install]
WantedBy=multi-user.target

/etc/systemd/system/openchamber.service

[Unit]
Description=OpenChamber Web
After=opencode.service network.target

[Service]
Type=simple
User=milkfish
WorkingDirectory=/home/milkfish
ExecStartPre=-/usr/bin/rm -f /home/milkfish/.config/openchamber/run/openchamber-4095.pid /home/milkfish/.config/openchamber/run/openchamber-4095.json
Environment="OPENCODE_HOST=http://localhost:4096"
Environment="OPENCODE_SKIP_START=true"
Environment="PATH=/home/milkfish/n/bin:/home/milkfish/.local/share/pnpm/bin:/home/milkfish/.opencode/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/home/milkfish/.local/share/pnpm/bin/openchamber --foreground --host 0.0.0.0 --port 4095 --ui-password <你的密码>
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

这里 systemd 有个很坑的地方——不读 .bashrc,PATH 是干净的。所以 Environment="PATH=..." 必须写全。我的 PATH 里有三个关键路径:n/bin(Node 版本管理器)、pnpm/bin(全局包)、.opencode/bin,少一个就报 command not found

ExecStartPre 那行的作用是启动前清理上次异常退出残留的 PID 文件。最前面的 - 表示"文件不存在也别报错",别漏了。OPENCODE_SKIP_START=true 是让 OpenChamber 别自己再起一个 OpenCode——已经有第一个 service 管了,两个一起跑会冲突。

启动:

systemctl daemon-reload
systemctl enable --now opencode openchamber

浏览器打开 http://你的IP:4095,输密码进去就行。密码建议 openssl rand -hex 16 随机一把——这货有完整的 bash 执行权限,别整个 123456 糊弄自己。

配置文件

~/.config/opencode/opencode.jsonc

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["opencode-websearch", "opencode-mem", "opencode-pty"],
  "mcp": {
    "context7": {
      "type": "local",
      "command": ["npx", "-y", "@upstash/context7-mcp"]
    }
  },
  "provider": {
    "moonshotai-cn": {
      "options": { "baseURL": "https://api.moonshot.cn/v1" },
      "models": {
        "kimi-k2.6": { "options": { "websearch": "always" } }
      }
    }
  },
  "permission": {
    "bash": {
      "go *": "allow",
      "npm *": "allow",
      "make *": "allow",
      "docker *": "allow",
      "terraform *": "deny"
    }
  }
}

这里 provider 那段不是配 OpenCode 用哪个模型干活——干活用的模型启动后 /connect 选就行,内置了 DeepSeek 之类的一堆 provider。这段是给 opencode-websearch 插件用的:它需要一个能联网搜索的模型来做 RAG,我选了 Kimi(api.moonshot.cn 国内直连友好,websearch: "always" 让它每次搜)。

MCP 挂了 context7,让 Agent 能实时查最新文档,不会凭训练数据在那瞎编 API。

权限那块 go/npm/make/docker 自动放行,terraform 自动拒绝(怕它乱动基础设施),其余弹窗确认。

文件名必须用 .jsonc。如果你同时放了 opencode.jsonopencode.jsonc,它只读一个——我就是这么浪费了二十分钟调配置。

同目录下还有一个 opencode-mem.jsonc

{
  "autoCaptureEnabled": true,
  "opencodeProvider": "deepseek",
  "opencodeModel": "deepseek-v4-flash",
  "compaction": { "enabled": true, "memoryLimit": 10 },
  "chatMessage": {
    "enabled": true,
    "maxMemories": 3,
    "excludeCurrentSession": true,
    "injectOn": "first"
  }
}

首次运行会自动下载个 ONNX 嵌入模型(nomic-embed-text-v1,547MB),把项目上下文向量化存 SQLite。新对话第一条消息自动注入最多 3 条相关记忆。memoryLimit: 10 控制每个项目最多保留 10 条压缩记忆。这货的缓存目录 ~/.opencode-mem/data/.cache/ 加上日志和记忆数据轻松破 600MB,目前没有自动清理,自己勤快点进去删。

nginx 反代(可选)

我 Homelab 本来就有 nginx 在跑,顺手给 OpenChamber 套了一层。不加也能用——直接 http://开发机:4095 就行。但 iOS App 要求 TLS 才能推送,所以我挂了 HTTPS。

这货的通信不是单一协议——终端走 WebSocket、事件流走 SSE、普通请求走 HTTP。location 规则写错了就各种诡异。贴一下我的配置:

server {
    listen 443 ssl http2;
    server_name openchamber.your-domain.com;

    # WebSocket 终端 — 精确匹配
    location = /api/terminal/ws {
        proxy_pass http://开发机IP:4095;
        include snippets/proxy-websocket.conf;
        proxy_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
    location = /api/global/event/ws {
        proxy_pass http://开发机IP:4095;
        include snippets/proxy-websocket.conf;
        proxy_buffering off;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
    location = /api/event/ws {
        proxy_pass http://开发机IP:4095;
        include snippets/proxy-websocket.conf;
        proxy_buffering off;
        proxy_read_timeout 3600s;
    }

    # SSE 事件流 — 关缓冲、关 gzip
    location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ {
        proxy_pass http://开发机IP:4095;
        include snippets/proxy-common.conf;
        proxy_set_header Accept "text/event-stream";
        proxy_buffering off;
        gzip off;
        add_header X-Accel-Buffering "no" always;
        proxy_read_timeout 3600s;
    }
    location ~ ^/api/terminal/.+/stream$ {
        proxy_pass http://开发机IP:4095;
        include snippets/proxy-common.conf;
        proxy_set_header Accept "text/event-stream";
        proxy_buffering off;
        gzip off;
        add_header X-Accel-Buffering "no" always;
        proxy_read_timeout 3600s;
    }

    # 普通 API — 长超时
    location /api {
        proxy_pass http://开发机IP:4095;
        include snippets/proxy-common.conf;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }

    # 静态资源
    location / {
        proxy_pass http://开发机IP:4095;
        include snippets/proxy-common.conf;
    }
}

nginx 这层踩过几个坑,说说:

location 优先级=(精确)> ~(正则)> /(前缀)。写反了 WebSocket 请求会被普通 API 规则吞掉,握手直接挂。

SSE 被缓冲:nginx 默认缓冲上游响应,SSE 事件攒一波再推给前端,体感就是卡死。proxy_buffering off + gzip off + X-Accel-Buffering "no" 三件套缺一不可。

超时:Agent 长任务跑几分钟很正常,默认 60s 的 proxy_read_timeout 直接给你 504。

WebSocket 头:需要全局 map $http_upgrade $connection_upgrade,location 里设 proxy_set_header Upgrade $http_upgrade,不然 Upgrade 不上去。

踩过的几个坑

PATH 没配全,systemd 起不来systemctl start openchamber 然后 status 显示 node: not found。说了,systemd 不读 .bashrc,PATH 一行不能省。Node、pnpm 全局包、opencode 三个路径一个别少。

异常退出后 4095 "端口被占"。不是真被占,是上次残留了 PID 文件,openchamber 启动检查以为实例还在。所以 ExecStartPre 那行 rm 很关键,前面那个 - 也别漏。

systemctl restart opencode 会话全丢。会话存内存里的,重启就没了。正确姿势是 openchamber restart——OpenChamber CLI 自带的命令,会先通知前端断开再重启后端再恢复会话。systemctl restart 只留给启动失败自动恢复用。

.json.jsonc 同时存在。改了 .jsonc 半天不生效,发现还有个 .json 躺在同目录。删掉一个,只留 .jsonc

opencode web 默认端口到底是多少。写 opencode web --hostname 127.0.0.1 不知道它起了哪个端口——默认 4096。OPENCODE_HOST 也得对齐到 http://localhost:4096。OpenChamber 是 --port 4095。俩差 1,别混。

ONNX 模型吃磁盘~/.opencode-mem/data/.cache/ 下 547MB 的嵌入模型是首次用的时候自动下的,加上日志记忆数据超 600MB。目前没有自动清理,定期进去删旧日志。

日常怎么用

在家搞个人项目就浏览器开家里的 OpenChamber,Agent 在服务端跑,本地就个窗口。彩色 diff、文件树、实时进度条,比终端 TUI 舒服多了。

到了公司打开公司那台开发机的 OpenChamber,接着写公司代码。两边各一个,浏览器书签切一下的事,跟切 JetBrains Gateway 的 SSH Host 一样自然。

路上掏出手机,iOS App 连 VPN,两边都能看。改个小 Bug 或者看 Agent 跑得咋样,推送会主动通知你任务完了。

同一个需求我经常丢给 2-3 个模型同时改,在 OpenChamber 里并排看结果,选最好的那个。每个模型跑在独立的 git worktree 里互不干扰。DeepSeek 免费又快,复杂逻辑丢 Kimi。

设好 Session Goal,关电脑关手机,Agent 自己在服务器上跑。第二天来看——这才是这玩意真正的价值。

End

这套东西不是最傻瓜式的——要折腾 systemd、调 PATH、跟 nginx location 优先级斗智斗勇——但跑通之后就是模型自由 + 代码不离开自己机器 + 随时随地能用。对我来说,把 AI Agent 也"远程化"是迟早的事,OpenCode + OpenChamber 在"远程 + 自托管 + 多端"这个三角里刚好踩在我需要的平衡点上。