从 SSH 依赖到 API 发现:CNB Workspace Proxy URL 的采坑之旅

TL;DR

每个 CNB Workspace 都有一个 business_id,它是外部访问的子域名。 通过 API GET /workspace/list 就能拿到,不需要 SSH。 但 API 偶尔返回 errcode=5,端口也因项目而异——这两个坑,值得单独记一笔。

背景

我在 CNB 平台上跑了好几个 Workspace:Gallery(前端展示)、DesktopOwn(桌面环境)、opencode(AI Agent)。它们各自暴露不同的服务端口——Ollama 在 11434,noVNC 桌面在 10001,React 开发服务器在 5173。

问题只有一个:我怎么在外网访问这些服务?

CNB 为每个 Workspace 提供了一个 Proxy URL,格式类似 https://5mxv4etp88-10001.cnb.run。问题是,这个 URL 无法直接预测——你要么 SSH 进去看环境变量,要么另寻他路。

而我遇到的情况是:DesktopOwn 这个 Workspace,压根不支持 SSH(它的自定义镜像没有开 SSH 服务)。

所以只能另想办法。

发现:business_id == Proxy Hash

事情是从一次偶然的对比开始的。

我用 cnb workspace list-workspaces --status running 查看正在跑的 Workspace,发现输出里有一列 business_id。而另一边,能 SSH 的 Workspace 里 env | grep CNB_VSCODE_PROXY_URI 出来的 hash 值,跟 business_id 一模一样。

那一刻的直觉:这两个值就是同一个东西。

于是我用一个 Workspace 做了验证:

SSH 内:  CNB_VSCODE_PROXY_URI = https://ky6otflwmk-{{port}}.cnb.run
API 里:  business_id          = ky6otflwmk

完全吻合。

这就意味着:不需要 SSH,只需要调用一个 API 就能拿到 Proxy URL 的全部拼图

流程简化为:

API 获取 business_id → 知道项目端口 → 构造完整 URL

踩坑 1:API 返回 errcode=5

验证通过后,我打算把这条路固化到自动化脚本里。结果第二天就翻车了。

我新启动了一个 Workspace,想用 API 拿新的 business_id。结果:

{
  "errcode": 5,
  "errmsg": "success",
  "list": []
}

list 是空的,business_id 根本拿不到。

可是 Workspace 明明在跑。CLI 命令行也能看到。为什么同样的 API 路径返回空?

尝试了几次之后发现:这似乎是 API 的一个间歇性问题——偶尔返回空列表,重试一次就恢复正常。

教训:API 返回空列表时不要慌。重试一次(或者隔几秒再试),通常就能拿到数据。

踩坑 2:端口不是固定的

解决了 business_id 的问题后,我以为万事大吉了。然后遇到了第二个坑。

Proxy URL 的完整格式是:

https://{business_id}-{port}.cnb.run

port 是多少?直觉告诉我,CNB 的端口应该都是 10001。毕竟之前见过的 Workspace 端口全是这个。

但当我尝试用 10001 去访问 opencode Workspace 时——连不上

查了一下才知道:CNB_VSCODE_PROXY_URI 环境变量本身是模板:

https://{business_id}-{{port}}.cnb.run

其中的 {{port}} 是模板占位符,具体替换成什么值,完全由每个项目在 .cnb.yml 中通过 sed 决定

也就是说:

  • DesktopOwn 的 .cnb.yml 写了 sed "s/{{port}}/10001/" → 端口 10001
  • opencode 的 .cnb.yml 写了 sed "s/{{port}}/5173/" → 端口 5173
  • ollama 跑在 11434

端口不是平台统一分配的,是项目自己决定的。

踩坑 3:CLI --branch 参数其实是坏的

这个坑虽然跟 Proxy URL 不完全直接相关,但它影响了整个自动化流程。

我本来想用 CLI 启动 Workspace 然后拿 proxy URL:

cnb workspace start-workspace --branch main

结果请求体永远是 {"branch":"master"}。无论你怎么传 --branch,CLI 发出去的都是 master

所以如果想在非默认分支上启动 Workspace,必须绕过 CLI,直接用:

curl -X POST "https://api.cnb.cool/workspace/start" \
  -H "Content-Type: application/json" \
  -d '{"slug":"my-project","branch":"main"}'

最终工作流

综合所有踩坑,最终的可靠流程是:

  1. 调用 API GET /workspace/list → 拿到 running workspace 的 business_id
  2. 查端口表(或读 .cnb.yml)→ 确认项目用哪个端口
  3. 构造 URL https://{business_id}-{port}.cnb.run
  4. 验证:curl 过去看看有没有响应

如果 API 返回 errcode=5 → 重试一次。 如果 Workspace 重启了 → business_id 不变,URL 不变(在同一 Workspace 的生命周期内是稳定的)。 如果需要启动新 Workspace → 用原始 API 而不是 CLI(避免 --branch bug)。

验证

# 获取 business_id
curl -s "https://api.cnb.cool/workspace/list" \
  -H "accept: application/json" \
  -H "Authorization: Bearer $CNB_TOKEN" \
  | python3 -c "
import sys, json
d = json.load(sys.stdin)
for w in d.get('list', []):
    print(f'{w[\"sn\"]:25s} business_id={w.get(\"business_id\",\"-\")}')" \
  | grep running

# 构造并验证
BUSINESS_ID="5mxv4etp88"  # 替换为上面拿到的值
PORT=10001               # 替换为项目对应的端口
curl -sI "https://${BUSINESS_ID}-${PORT}.cnb.run" | head -5

相关

  • Skill 文档: ~/.agents/skills/cnb-workspace-proxy-url/SKILL.md
  • ADR: knowledge/notes/adr/0001-ollama-proxy-architecture.md