跳到主要内容

数据截至 (上游 commit a9c304f343f1)

远程与云:远程工作区、OpenWork MCP 与 Den 控制面

30 秒导读: OpenWork 的口号是 "local-first, cloud-ready"——先在你自己电脑上一键跑起来,需要时再显式把它推向远程和团队。本章讲这条路上的三块拼图:远程工作区(桌面 UI 面对的永远是一个 openwork-server 端点,本地远程同构,换 URL+token 就连上云 worker);OpenWork MCP(Den 云把组织里发布的能力装进一个远程 MCP server,任何兼容 agent 用两个元工具按需取用);桌面↔云同步(组织资源快照对账,把云端发布的 provider/插件推进本地工作区)。历史上这里还有一根 Slack/Telegram 消息桥(opencode-router)——它已在上游被整体移除(§7)。

本章是 OpenWork 系列的第 6 章。前几章讲的是"单机怎么转": 桌面外壳主机运行时(引擎托管)openwork-server 鉴权代理可扩展性前端。 这一章讲"怎么从单机走出去"。凡涉及 managed credentials 与 actor scopes 的地方,回指第 2、3 章。


1. 这是什么(零基础也能懂)

一句话定义: 本章讲 OpenWork 的三层"出门"路径——分享出去(局域网一条链接)、连出去(桌面连远程 worker)、接上云(Den 控制面 + OpenWork MCP + 资源同步)。

解决什么问题 / 给谁用:

想象你已经用 OpenWork 桌面版在自己电脑上跑起了一个 AI agent,能改代码、能读文件。现在的痛点是:

  • 你出门了,只有手机,想让它继续干活;
  • 你想让同事不用装任何东西就能用你这套配置好的 agent;
  • 团队管理员想把"谁能用哪些技能/模型/MCP"集中管理,而不是挨个教大家配。

OpenWork 的答案分别是:远程工作区(把 UI 指向一台常驻的 openwork-server)、OpenWork MCP(把能力发布到云端,任何 agent 客户端都能连)、Den 控制面(组织、市场、托管推理)。

用起来什么样(最小示例)。 管理员在 Den 发布能力后,团队成员给任意 MCP 客户端加一个远程 server(README "Use OpenWork from any agent"):

claude mcp add --transport http openwork https://api.openworklabs.com/mcp/agent
# 或 codex mcp add openwork --url https://api.openworklabs.com/mcp/agent

加好后 agent 多了两个工具:search_capabilities 找出"我能用什么",execute_capability 精确执行一条返回的能力。

一句话直觉/类比: 把 Den 当作团队的应用商店 + 门禁系统:技能、插件、MCP 连接是货架上的商品;search_capabilities 是货架索引;execute_capability 是刷工牌取货;桌面工作区则是"把货架同步到家门口"的自动补货车。

本节不涉及底层代码。记住一件事:远程化不改变 openwork-server 这个"唯一闸门"的形状——变的只是它跑在哪台机器上。


2. 顶层全景(它大概怎么转)

2.1 三条出门路径一张图

怎么读这张图: 左列是用户视角的三个动作,中列是各自的机制,右列是最终落到的地方。

用户动作 机制 落点
┌────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ 打开远程访问 │──▶ 绑 0.0.0.0 + 连接 URL │──▶ 局域网/mDNS 分享 │
│ (本地桌面) │ │ (buildConnectUrls) │ │ 同一套 UI/token │
└────────────┘ └──────────────────────┘ └─────────────────────┘
┌────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ Connect │──▶ URL + token + 诊断 │──▶ 远程 worker 上的 │
│ remote │ │ (diagnostics/链接) │ │ openwork-server │
└────────────┘ └──────────────────────┘ └─────────────────────┘
┌────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ 加 OpenWork │──▶ Den agent MCP │──▶ 组织发布的能力 │
│ MCP │ │ (search/execute) │ │ (技能/插件/程序/MCP) │
└────────────┘ └──────────────────────┘ └─────────────────────┘

2.2 部件一句话职责

部件干什么在哪个文件
连接 URL 构造远程访问打开时算 LAN/mDNS URLapps/desktop/electron/runtime.mjs:504(buildConnectUrls)
远程工作区表单收 Worker URL / token / 目录三要素apps/app/src/react-app/domains/workspace/remote-workspace-fields.tsx:32(RemoteWorkspaceFields)
连接前诊断探 health/status、拒绝非 OpenWork 远端apps/app/src/react-app/domains/workspace/remote-workspace-diagnostics.ts(RemoteWorkspaceConnectionTarget)
选远端工作区从远端列表挑目标apps/desktop/electron/remote-workspace.mjs:21(selectOpenworkWorkspaceForConnection)
深链接云 web 一键跳桌面连接apps/app/src/app/lib/openwork-links.ts:48 + apps/desktop/electron/connect-link.mjs:201(verifyConnectLinkToken)
Den agent MCPsearch_capabilities/execute_capability 两个元工具ee/apps/den-api/src/mcp/agent.ts:368(registerAgentMcpRoutes)
能力注册表汇总/检索/执行各来源能力ee/apps/den-api/src/mcp/capability-registry.ts
桌面云同步组织资源快照对账apps/server/src/desktop-cloud-sync.ts:459(syncDesktopCloudResources)
云 provider 同步把托管 provider 物化进引擎配置apps/server/src/cloud-provider-sync.ts
worker 心跳Daytona worker 上报活跃度apps/server/src/worker-activity-heartbeat.ts:155

3. 核心原理(逐个机制,由浅入深)

3.1 本地分享:打开远程访问才有 URL

它要解决的小问题: 默认只绑 127.0.0.1,想让同网段的人访问,得显式放开并给出一个"能连的 URL"。

机制。 桌面 startOpenworkServer 只在 remoteAccessEnabled 时把 host 从 127.0.0.1 换成 0.0.0.0,此时才调 buildConnectUrls(port)(apps/desktop/electron/runtime.mjs:504、调用点 :1928)算出 LAN IP 与 .local mDNS 两类地址;不开远程访问时三个 URL 一律 null。UI 侧由 remote-access-restart.ts 处理"打开远程访问需要重启运行时"的交互(apps/app/src/react-app/domains/workspace/remote-access-restart.ts),分享入口在 share-workspace-modal.tsx / share-workspace-access-panel.tsx(apps/app/src/react-app/domains/workspace/,状态机在 share-workspace-state.ts)。

安全边界不变。 无论是局域网还是公网,进 OpenCode 的请求仍然全部穿过 openwork-server 的三级 token 闸门(第 3 章)——分享出去的是受限作用域的访问,不是裸引擎。

3.2 远程工作区:换一个 baseUrl 的事

它要解决的小问题: agent 得常驻在"永远在线"的机器上;你的笔记本合盖它就没了。

对称性是关键设计。 桌面 UI 面对的永远是一个 openwork-server 的 HTTP 端点(第 3 章)。本地模式下它是 Electron 在 localhost 起的;远程模式下,它只是换成一台远程机器的 URL——UI 逻辑几乎不变,变的只是 baseUrl 和一个 token。这就是 "cloud-ready" 能低成本实现的原因。

连接三要素RemoteWorkspaceFields(apps/app/src/react-app/domains/workspace/remote-workspace-fields.tsx:32)收集:

字段说明
Worker URL远程 worker 地址,如 https://worker.example.com
Access tokenworker 要求时才填(呼应第 3 章 bearer 鉴权)
Remote directory可选;把工作区定位到远端机上的某个子目录

连接前先体检。 remote-workspace-diagnostics.ts 把目标归一成 RemoteWorkspaceConnectionTarget(:11,{ kind: "openwork", baseUrl, token, workspaceId }),再跑一串 test* 探测:URL 合法性、health/status 可达性;非 OpenWork 的远端会被明确拒绝——"Only remote workers can be tested."(remote-workspace-diagnostics.ts:157)。

选中远端上的哪个工作区selectOpenworkWorkspaceForConnection(apps/desktop/electron/remote-workspace.mjs:21)决定:给了目录就按目录匹配远端列表,没给就用远端 activeId 或第一个。

从云 web 一键跳桌面。 Den 的 web 端生成 connect-remote 深链接,桌面用 openwork-links.ts 解析(apps/app/src/app/lib/openwork-links.ts:48),直接进入连接流程。企业激活走的 connect-link 是签名的紧凑 JWS(EdDSA/Ed25519,packages/connect-link/src/node.ts 顶部注释):den-api 用 packages/connect-link 签发,Electron 主进程内置一份零依赖的验证镜像 verifyConnectLinkToken(apps/desktop/electron/connect-link.mjs:201),并声明"node 实现是测试所锚定的参考实现"——同一套验证逻辑、两份实现、测试互相咬合。

3.3 OpenWork MCP:两个元工具装下整个组织的能力

它要解决的小问题: 组织里发布的能力(技能、插件、MCP 连接、程序)在不断变。如果每个能力都做成一个独立工具,agent 的工具列表会爆炸,而且每发布一个都要改客户端配置。

思路:元工具 + 精确执行。 Den 把所有能力装进一个远程 MCP server(/mcp/agent),只暴露两个工具(工具名常量见 ee/apps/den-api/src/mcp/agent.ts:101 一带):

工具干什么
search_capabilities按意图检索"我能用什么",返回带 kind 的精确匹配
execute_capability执行一条 search_capabilities 返回的精确匹配

路由注册在 registerAgentMcpRoutes(ee/apps/den-api/src/mcp/agent.ts:368),同一文件里还挂了 OAuth protected-resource 元数据端点(:369-372)——客户端加 MCP 时弹浏览器登录、选组织(README "Use OpenWork from any agent")。检索与匹配在 ee/apps/den-api/src/mcp/search.ts(compareCapabilityMatches 等),各来源能力(市场技能、程序、动态 artifact app、_builtin 技能…)的汇总/执行在 ee/apps/den-api/src/mcp/capability-registry.ts(searchCapabilityRegistry / executeCapability)。

值得看的取舍(行为说明,agent.ts:160-167): 能力的执行必须execute_capability 的精确匹配——先 search_capabilities 用 2-4 个关键词变体检索,execute_capability 只接受检索返回的精确名字,而不是把每个能力展开成直连工具;独立 URL 导入的 app 则被明确推迟为未来工作。这是"能力市场"与"把第三方直接接进你的 agent"之间的安全分界。

3.4 桌面↔云同步:资源快照对账

它要解决的小问题: 组织管理员在 Den 改了发布内容(加了 provider、升了插件),桌面工作区怎么知道、怎么决定要不要跟进?

机制:快照 + diff + 待办。 Den 侧把"这个组织/成员/团队当前应有什么"打成一份 ResourceSnapshot(apps/server/src/desktop-cloud-sync.ts:17-24:llmProvidersmarketplaces→plugins→configItems 两层,各带 lastUpdatedAt)。它经 POST /workspace/:id/desktop-cloud-sync(apps/server/src/server.ts:2203)进入 openwork-server,由 syncDesktopCloudResources(desktop-cloud-sync.ts:459)与本地已安装的 cloud imports 做 diff,产出 new/modified/removed 三类 pendingChanges(:27-40)记进工作区配置的 desktopCloudSync 分区——先记账、后生效,而不是直接改你的安装。

一个踩过坑的细节写在路由里(apps/server/src/server.ts:2221-2223 注释):插件库拥有 plugins/marketplaces,但 provider 导入基线住在工作区配置里——早期实现把合并后的 cloudImports 整包写回,把 providers 抹掉了,引发 provider-sync 的 dispose/create 循环;现在写回时刻意只更新 desktopCloudSync 字段。

provider 的物化cloud-provider-sync.ts 完成:把 Den 会话(CloudProviderDenSession,:26)解析出的托管 provider 同步成引擎可用的配置。文件里两处注释点明它是从 den-api 移植的(cloud-provider-sync.ts:399),并保留 den-api 稳定的、不泄密的 owp:v1 指纹格式(:489)——桌面与云两侧对同一 provider 必须算出同一指纹,否则会反复重建。

3.5 Den(EE):把 worker 与推理做成托管服务

⚠ 组件性质: ee/ 目录下的一切(den-apiden-webinferenceden-worker-runtimeden-worker-proxyden-controllerdiagnosticslanding 等)是 EE 商业组件,单独授权(ee/LICENSE)。本节只做高层概览。

部件分工(高层):

EE 部件角色(高层)依据
den-api控制面(Hono):组织/团队/成员、能力市场、agent MCP、worker 生命周期ee/apps/den-api/src/app.ts(挂 registerAgentMcpRoutes)
den-web云前端:登录、组织管理、市场发布、跳桌面连接ee/apps/den-web/
inference托管推理网关:凭托管 key 代理到底层 provider,带用量限额ee/apps/inference/src/proxy.tskeys.ts
den-worker-runtimeworker 运行时根:装 openwork-server、构建期 vendor 匹配版本的 opencodeee/apps/den-worker-runtime/README.md
den-worker-proxyworker 前置代理(签名预览 URL、转发)ee/apps/den-worker-proxy/src/app.ts

托管推理解决"人手一把 provider key"。 团队成员不各自持有模型 API key,而是拿托管 inference key;网关在 findActiveInferenceKey(ee/apps/inference/src/keys.ts:17)解析出真正的 provider key(如 getOpenRouterProviderKey),并施加限额(limits.ts)。这正是第 2 章托管引擎凭据在云侧的对应物:引擎凭据管"谁能连引擎",inference key 管"谁能用模型"。

worker 的休眠靠心跳。 托管 worker(Daytona)上的 openwork-server 周期上报"最近有没有人在用"(startWorkerActivityHeartbeat,apps/server/src/worker-activity-heartbeat.ts:155;启用条件见第 2 章 §3.5),平台据此休眠闲置 worker。云上 worker 与桌面跑的是同一个服务器入口、同一套引擎托管(第 2 章 §3.6)。


4. local ↔ cloud 之间,凭据怎么衔接

把散在各处的凭据线索串成一张表:

凭据保护谁 → 谁在哪
OpenCode Basic Authopenwork-server → 它托管的 OpenCode 引擎apps/server/src/managed-opencode.ts(随机生成,第 2 章)
worker Access token桌面/客户端 → 远程 openwork-serverConnect remote 表单 + 第 3 章 bearer 鉴权
connect-link tokenDen web → 桌面 Electron(组织激活/跳转)packages/connect-link(EdDSA 签名)+ apps/desktop/electron/connect-link.mjs:201(验证)
MCP OAuth 会话agent 客户端 → /mcp/agentee/apps/den-api/src/mcp/agent.ts(protected-resource 元数据端点)
托管 inference key成员/引擎 → inference 网关 → 真正的 provideree/apps/inference/src/keys.ts:17

一句话串起来: 引擎凭据让服务器独占引擎,worker token 让远程访问被作用域约束,签名的 connect-link 让"云发给桌面的话"不可伪造,MCP OAuth 让任意 agent 客户端能安全地代表某个组织成员,托管 inference key 让模型调用不必人手发密钥——五层凭据各管一段。


5. 巧妙之处(可借鉴的技术)

  • 本地远程同构,一个 baseUrl 走天下。 UI 永远只认"一个 openwork-server 端点"(RemoteWorkspaceConnectionTarget 只有三四个字段),从单机到云 worker 不改一行 UI 逻辑。
  • 元工具做能力发现。 agent MCP 端点常驻只注册 search_capabilities/execute_capability/create_skill 三个工具(Code Mode 与 Artifact 呈现工具门控开启),组织发布再多能力也不撑爆工具列表;执行必须走"精确匹配"——execute_capability 只接受检索返回的精确名字(ee/apps/den-api/src/mcp/agent.ts:349-367:167)。
  • 同步先记账后生效。 云端资源变更先落成 pendingChanges 对账记录(desktop-cloud-sync.ts:459),不直接改安装——用户保留"要不要跟进"的控制权。
  • 同一验证逻辑、两份实现、测试互锚。 connect-link 的 node 参考实现与 Electron 零依赖镜像(connect-link.mjs:201)由同一组测试咬合(packages/connect-link/src/node.ts 顶部注释)。
  • 指纹稳定化防重建循环。 provider 同步保留 den-api 的 owp:v1 指纹格式(cloud-provider-sync.ts:489),两侧对同一 provider 算出同一指纹,避免 dispose/create 抖动。

6. 边界与局限(诚实)

  • 远程工作区只认 OpenWork worker。 诊断会明确拒绝非 OpenWork 远端(remote-workspace-diagnostics.ts:157),不能拿它连任意 HTTP 服务。
  • 桌面云同步依赖 Den 会话。 快照由云侧推入(POST /workspace/:id/desktop-cloud-sync),没有组织账号就没有这条线;本地技能/MCP 安装不受影响。
  • EE 是闭源商业件。 本章对 den-* 只做高层描述;agent MCP、市场、计费的实现细节不在开源可读范围的核心承诺内(文件可读但授权独立)。
  • agent MCP 的行为面在快速演进。 文件头注释当前罗列的面是:常驻的能力路由工具 + 第一方 skill 创建 App + 门控的 Code Mode 执行与 Artifact 呈现(ee/apps/den-api/src/mcp/agent.ts:349-367),具体清单以源码为准。
  • 远程 worker 的网络质量没有魔法。 UI 与引擎之间隔了公网,SSE 事件流与文件 API 的体验受 worker 位置制约;这是架构选择(复用同一 server)的固有代价。

7. 已移除:opencode-router 消息桥(考古)

本章旧版的主角 apps/opencode-router/——把 Slack/Telegram 消息按目录路由到不同 OpenCode 工作区、带配对码准入与 per-user 模型选择的那根"电话总机"——已在上游被整体移除,仓库中已无 apps/opencode-router 目录,ChannelNameBridgeStorepath-scope 等符号均不可再 grep 到。它的三个遗留诉求分别有了新去向:

旧能力新去向
"从聊天工具触达 agent"OpenWork MCP(§3.3):任何 MCP 客户端(含聊天机器人框架)都可接
"按对话路由到不同目录"远程工作区 + 多工作区注册表(第 3 章)
"bot 首次准入(配对码)"MCP OAuth 组织登录(§3.3)+ 三级 token 作用域(第 3 章)

8. 代码地图(导航索引)

按符号名可 grep,比行号抗漂移。行号 as-of sourceCommit a9c304f

主题文件路径符号名
远程访问 URLapps/desktop/electron/runtime.mjsbuildConnectUrls
远程工作区表单apps/app/src/react-app/domains/workspace/remote-workspace-fields.tsxRemoteWorkspaceFields
连接目标/诊断apps/app/src/react-app/domains/workspace/remote-workspace-diagnostics.tsRemoteWorkspaceConnectionTargettest* 探测族
分享工作区 UIapps/app/src/react-app/domains/workspace/share-workspace-modal.tsxShareWorkspaceModalshare-workspace-state.ts
远程访问重启交互apps/app/src/react-app/domains/workspace/remote-access-restart.ts(开启远程访问需重启运行时)
桌面选远端工作区apps/desktop/electron/remote-workspace.mjsselectOpenworkWorkspaceForConnectionopenworkWorkspaceDisplayName
深链接解析apps/app/src/app/lib/openwork-links.tsconnect-remote 路由识别
connect-link 验证(桌面)apps/desktop/electron/connect-link.mjsverifyConnectLinkToken
connect-link 签发(参考实现)packages/connect-link/src/node.tsgenerateConnectLinkKeyPair、EdDSA 签名/验证
Den agent MCPee/apps/den-api/src/mcp/agent.tsregisterAgentMcpRoutesSEARCH_CAPABILITIES_TOOL_NAMEEXECUTE_CAPABILITY_TOOL_NAME
能力检索/匹配ee/apps/den-api/src/mcp/search.tscompareCapabilityMatches
能力注册表ee/apps/den-api/src/mcp/capability-registry.tssearchCapabilityRegistryexecuteCapability
桌面云同步核心apps/server/src/desktop-cloud-sync.tsResourceSnapshotsyncDesktopCloudResourcesDesktopCloudSyncChange
云同步路由apps/server/src/server.tsPOST /workspace/:id/desktop-cloud-sync
云 provider 物化apps/server/src/cloud-provider-sync.tsCloudProviderDenSessionparseCloudProviderDenSession
托管推理网关ee/apps/inference/src/proxy.tskeys.tsfindActiveInferenceKeygetOpenRouterProviderKey
worker 运行时ee/apps/den-worker-runtime/README(装 openwork-server + vendored opencode)
worker 心跳apps/server/src/worker-activity-heartbeat.tsstartWorkerActivityHeartbeatresolveWorkerActivityHeartbeatConfig