openwork Den Worker Runtime 深度解析:云端工作节点的 OpenCode 预置与启动机制
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
openwork 的云工作节点(Worker)在 Render / Daytona 上承载 Agent 运行环境,其关键挑战是让每个新启动的节点都拥有与仓库版本精确一致的 OpenCode 二进制。本文以 ee/apps/den-worker-runtime/README.md 为骨架,结合install-opencode.mjs、provisioner.ts、Dockerfile.daytona-snapshot与constants.json的源码实现,完整还原「版本钉扎 → 构建期 vendor → 节点启动」的全链路设计。读完你将掌握:控制平面如何编排 Worker、OpenCode 如何被预置进镜像、openwork-server的启动参数语义,以及这套机制如何规避首次启动时对 GitHub 的运行时依赖。
一、这个目录在 openwork 中扮演什么角色
den-worker-runtime是云端渲染 Worker 服务的构建根目录。README 开篇就定义了它的契约:Render worker services use this directory asrootDir,即 Render 平台在构建 Worker 服务时,以此为工作目录执行构建命令与启动命令。
从控制平面的默认配置可以印证这一点。在 ee/apps/den-api/src/env.ts 中,RENDER_WORKER_ROOT_DIR的默认值正是:
workerRootDir: parsed.RENDER_WORKER_ROOT_DIR ?? "ee/apps/den-worker-runtime",也就是说,每次调用 Render API 创建 Worker 服务时,控制面都会把这个仓库子目录作为rootDir传入。整个目录目前只包含三个文件:
- README.md — 目录定位说明
- scripts/install-opencode.mjs — 构建期 OpenCode 预置脚本
- Dockerfile.daytona-snapshot — Daytona 快照镜像构建定义
README 描述了控制平面完整的构建与启动链路:
控制平面安装
openwork-server,从仓库根目录的constants.json读取钉扎(pinned)的 OpenCode 版本,在 Render 构建期间运行scripts/install-opencode.mjs,然后以openwork-server命令启动 Worker。这一额外的构建步骤将匹配的opencode发布资产 vendor 进./bin/opencode,使运行时不再依赖首次启动时的 GitHub 下载。
这是整个机制的"一句话总纲",下面逐层拆解。
二、版本钉扎:constants.json 是唯一的版本事实来源
预置机制的第一步是确定"要装哪个版本的 OpenCode"。版本不是硬编码在脚本里的,而是统一收口在仓库根目录的 constants.json:
{ "opencodeVersion": "v1.18.18", "opencodeV2Version": "0.0.0-beta-19086" }install-opencode.mjs 中的resolveOpencodeVersion()负责读取并规范化这个值:
function resolveOpencodeVersion() { const versionPath = resolve(repoRoot, "constants.json"); if (!existsSync(versionPath)) { throw new Error(`Missing pinned constants file at ${versionPath}`); } const parsed = JSON.parse(readFileSync(versionPath, "utf8")); const version = String(parsed.opencodeVersion ?? "").trim().replace(/^v/, ""); if (!version) { throw new Error(`Pinned OpenCode version is missing from ${constantsPath}`); } return version; }几个值得注意的实现细节:
- 单一事实来源:
repoRoot由脚本自身位置向上推导三层(runtimeRoot → repoRoot),保证无论从哪个 CWD 调用都能找到仓库根。 - 前导
v剥离:constants.json中写的是v1.18.18,解析时统一去掉v前缀,得到干净的1.18.18用于拼下载 URL。 - fail-fast:文件缺失或版本为空会直接抛错,避免带病构建出无 OpenCode 的镜像。
Daytona 快照构建脚本 create-daytona-openwork-snapshot.sh 使用了同一套读取逻辑(用 Node 读取并 stripv),保证 Render 与 Daytona 两条路径拿到的版本完全一致:
OPENWORK_SERVER_VERSION="${OPENWORK_SERVER_VERSION:-$(node -e '...' "$ROOT_DIR/apps/server/package.json")}" OPENCODE_VERSION="$(node -e '...' "$ROOT_DIR/constants.json")"三、install-opencode.mjs:构建期 vendor 的完整实现
这是整个机制的核心脚本。它的目标非常明确:在构建阶段把 OpenCode 的可执行文件放进./bin/opencode,让镜像自包含。下面按执行流程逐段拆解。
3.1 输出路径与目标文件名
const runtimeRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); const repoRoot = resolve(runtimeRoot, "..", "..", ".."); const outputDir = resolve(runtimeRoot, "bin"); const outputName = process.platform === "win32" ? "opencode.exe" : "opencode"; const outputPath = join(outputDir, outputName); const versionStampPath = join(outputDir, "opencode.version");输出固定在ee/apps/den-worker-runtime/bin/opencode(Windows 下为opencode.exe),同时写入一个opencode.version版本戳文件,供幂等判断使用。
3.2 平台资产矩阵
不同的platform-arch组合对应不同的发布资产名,resolveAssetName()维护了完整的映射表:
| 目标平台 | 资产文件名 |
|---|---|
| darwin-arm64 | opencode-darwin-arm64.zip |
| darwin-x64 | opencode-darwin-x64-baseline.zip |
| linux-arm64 | opencode-linux-arm64.tar.gz |
| linux-x64 | opencode-linux-x64-baseline.tar.gz |
| win32-arm64 | opencode-windows-arm64.zip |
| win32-x64 | opencode-windows-x64-baseline.zip |
不支持的组合(如 32 位、FreeBSD 等)直接抛Unsupported platform for opencode bundle错误。注意 x64 使用的是-baseline后缀的资产——这是为了在更广的 x86_64 CPU 指令集兼容面上运行,牺牲少量性能换取兼容性,与后续 Dockerfile 中的选择一致。
3.3 幂等缓存:版本戳决定是否重跑
if ( existsSync(outputPath) && existsSync(versionStampPath) && readFileSync(versionStampPath, "utf8").trim() === versionLabel ) { console.log(`[den-worker-runtime] opencode ${versionLabel} already bundled at ${outputPath}`); process.exit(0); }如果bin/opencode已存在,且bin/opencode.version内容与当前钉扎版本一致,脚本直接短路退出。这对 Render 的增量构建和本地重复构建都很有价值——版本没变就不必重新下载解压。
3.4 下载源与重试策略
const downloadUrl = process.env.OPENWORK_OPENCODE_DOWNLOAD_URL?.trim() || (version ? `https://github.com/anomalyco/opencode/releases/download/v${version}/${assetName}` : null);下载 URL 有两个来源:
- 环境变量
OPENWORK_OPENCODE_DOWNLOAD_URL显式覆盖(用于企业内网镜像、私有制品库等场景); - 缺省时拼官方 release 地址
https://github.com/anomalyco/opencode/releases/download/v${version}/${assetName}。
downloadWithRetries实现最多 3 次重试,失败后按attempt * 1000毫秒递增退避:
for (let attempt = 1; attempt <= 3; attempt += 1) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const buffer = Buffer.from(await response.arrayBuffer()); writeFileSync(destination, buffer); return; } catch (error) { lastError = error; if (attempt < 3) { await new Promise((resolvePromise) => setTimeout(resolvePromise, attempt * 1000)); } } }3.5 解压、定位二进制与落盘
extractArchive按扩展名分派解压方式:
.tar.gz→ 直接调用系统tar -xzf;.zip→ Windows 上用 PowerShell 的Expand-Archive,其他平台用unzip -q;- 其他格式一律报错。
由于发布资产的目录结构可能因版本而异,findBinary用深度优先遍历递归搜索解压目录,直到找到名为opencode的可执行文件:
function findBinary(searchRoot) { const stack = [searchRoot]; while (stack.length > 0) { const current = stack.pop(); if (!current) continue; for (const entry of readdirSync(current, { withFileTypes: true })) { const entryPath = join(current, entry.name); if (entry.isDirectory()) { stack.push(entryPath); continue; } if (entry.isFile() && entry.name === outputName) { return entryPath; } } } throw new Error(`Unable to find ${outputName} inside extracted archive`); }最后copyFileSync拷贝到bin/opencode,非 Windows 平台chmodSync(outputPath, 0o755)赋予执行权限,写入版本戳,并在finally中清理临时目录。整个下载-解压过程在临时目录完成,失败不会污染bin/目录。
四、控制平面的编排:provisioner.ts 如何调用这一切
脚本不会被人手动调用,真正驱动它的是 den-api 控制平面。在 ee/apps/den-api/src/workers/provisioner.ts 的provisionWorkerOnRender中,构建命令与启动命令被拼装后通过 Render API 下发。
4.1 构建命令(buildCommand)
const buildCommand = [ `npm install -g ${shellQuote(openworkServerPackage)}`, "node ./scripts/install-opencode.mjs", ].join(" && ");两步走:
npm install -g openwork-server(或openwork-server@<版本>,由RENDER_WORKER_OPENWORK_VERSION控制)——全局安装服务端;node ./scripts/install-opencode.mjs——由于rootDir是ee/apps/den-worker-runtime,脚本中的repoRoot推导会正确落到仓库根,读取constants.json并完成 vendor。
4.2 启动命令(startCommand)
控制面生成的启动脚本是理解 Worker 运行方式的关键,其中包含多个核心环境变量:
set -u mkdir -p /tmp/workspace plugin_dir="$(npm root -g)/openwork-server/dist/opencode-plugins" if [ ! -d "$plugin_dir" ]; then echo "openwork-server extension plugins missing at $plugin_dir" >&2 exit 1 fi attempt=0 while [ "$attempt" -lt 3 ]; do attempt=$((attempt + 1)) if OPENWORK_MANAGE_OPENCODE=1 OPENWORK_OPENCODE_BIN=./bin/opencode OPENWORK_EXTENSIONS_PLUGIN_DIR="$plugin_dir" openwork-server --workspace /tmp/workspace --host 0.0.0.0 --port "${PORT:-10000}" --cors '*' --approval manual --verbose; then exit 0 fi status=$? echo "openwork-server failed (attempt $attempt, exit $status); retrying in 3s" sleep 3 done exit 1对关键参数的语义解读:
| 参数 / 环境变量 | 含义 |
|---|---|
OPENWORK_MANAGE_OPENCODE=1 | 让openwork-server接管 OpenCode 进程的生命周期管理(spawn、探活、回收),而不是依赖外部 supervisor |
OPENWORK_OPENCODE_BIN=./bin/opencode | 指向构建期 vendor 的二进制路径,这正是「不依赖首次启动 GitHub 下载」的直接落点 |
OPENWORK_EXTENSIONS_PLUGIN_DIR="$plugin_dir" | 指定 OpenCode 扩展插件目录,取自全局npm root -g下openwork-server/dist/opencode-plugins |
--workspace /tmp/workspace | 工作区根目录,启动前mkdir -p确保存在 |
--host 0.0.0.0 --port ${PORT:-10000} | 监听所有网卡,端口默认 10000,可被 Render 注入的PORT覆盖 |
--cors '*' | 放开跨域,便于桌面端与控制平面直连 |
--approval manual | 工具调用采用人工审批模式(manual approval) |
| 3 次重试循环 | 启动失败时等待 3 秒重试,最多 3 次,全失败则退出非零码让 Render 标记部署失败 |
插件目录缺失时立即失败(fail-fast),保证镜像自检不通过就不会进入服务状态。同时注意OPENWORK_TOKEN、OPENWORK_HOST_TOKEN、DEN_WORKER_ID三个环境变量随服务创建一并注入,用于 Worker 与控制平面之间的鉴权与身份标识。
4.3 部署生命周期管理
provisionWorkerOnRender的完整流程还包括:
- 生成服务名(
slug化,最长 62 字符):den-worker-<name>-<workerId前8位>; - 调用 Render API 创建
web_service,携带healthCheckPath: "/health"、region、plan; - 轮询 deploy 状态直至
live(超时由RENDER_PROVISION_TIMEOUT_MS控制,默认 900000ms,轮询间隔默认 5000ms); - 等待
/health端点就绪(RENDER_HEALTHCHECK_TIMEOUT_MS默认 180000ms); - 可选附加自定义域名(
customDomainForWorker+ Vercel DNS 记录)并验证其健康(RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS默认 240000ms)。
反向上,deprovisionWorker通过服务名/URL host 匹配 Render 服务并调用/suspend挂起,实现资源回收。这套编排逻辑定义了控制平面侧的全部超时与重试行为,与env.ts中render.*配置一一对应。
五、相关配置参数一览(env.ts)
Worker 运行时相关配置全部收口在 ee/apps/den-api/src/env.ts 的render段:
render: { apiBase: parsed.RENDER_API_BASE ?? "https://api.render.com/v1", apiKey: parsed.RENDER_API_KEY, ownerId: parsed.RENDER_OWNER_ID, workerRepo: parsed.RENDER_WORKER_REPO ?? "https://github.com/different-ai/openwork", workerBranch: parsed.RENDER_WORKER_BRANCH ?? "dev", workerRootDir: parsed.RENDER_WORKER_ROOT_DIR ?? "ee/apps/den-worker-runtime", workerPlan: parsed.RENDER_WORKER_PLAN ?? "standard", workerRegion: parsed.RENDER_WORKER_REGION ?? "oregon", workerOpenworkVersion: parsed.RENDER_WORKER_OPENWORK_VERSION, workerNamePrefix: parsed.RENDER_WORKER_NAME_PREFIX ?? "den-worker", workerPublicDomainSuffix: parsed.RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX, ... }部署者可通过以下环境变量按需覆盖:
| 环境变量 | 默认值 | 作用 |
|---|---|---|
RENDER_API_BASE | https://api.render.com/v1 | Render API 地址 |
RENDER_API_KEY | 无(必需) | Render API 鉴权,缺失时provisionWorkerOnRender抛错 |
RENDER_OWNER_ID | 无(必需) | Render 资源归属账号 |
RENDER_WORKER_REPO | https://github.com/different-ai/openwork | Worker 服务绑定的仓库 |
RENDER_WORKER_BRANCH | dev | 构建所用分支 |
RENDER_WORKER_ROOT_DIR | ee/apps/den-worker-runtime | 本主题的核心:构建/启动的 rootDir |
RENDER_WORKER_PLAN | standard | Render 实例套餐 |
RENDER_WORKER_REGION | oregon | 部署区域 |
RENDER_WORKER_OPENWORK_VERSION | 未设置(取最新) | 钉扎openwork-server的 npm 版本 |
RENDER_WORKER_NAME_PREFIX | den-worker | 服务名前缀 |
RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX | 无 | 自定义域名后缀,用于 vanity domain |
另外,PROVISIONER_MODE(stub/render/daytona)决定provisionWorker走哪条路径,DAYTONA_SNAPSHOT等DAYTONA_*变量则服务 Daytona 分支。
六、Daytona 快照路径:另一种自包含运行方式
除了 Render 即时构建,仓库还提供了 Daytona 沙箱快照方案,镜像定义在 Dockerfile.daytona-snapshot。
6.1 多阶段构建与交叉编译
FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS openwork-builder ... RUN pnpm install --frozen-lockfile --filter openwork-server... --filter @openwork/app... RUN pnpm --filter openwork-server build; \ pnpm --filter openwork-server exec bun ./script/build.ts --outdir dist/bin --target "$target"; \ install -m 0755 "apps/server/dist/bin/openwork-server-${target}" /tmp/openwork-server; \ pnpm --filter @openwork/app build:web注释解释了设计动机:构建器在构建主机的原生平台上运行($BUILDPLATFORM),用 Bun 的--target交叉编译openwork-server,Web 构建与架构无关,从而避免在 QEMU 下模拟整个工具链——文件头明确记录了 node 的 libuv 在 qemu-x86_64 下会触发uv__io_poll EEXIST断言崩溃这一工程坑。
pnpm install阶段特意安装了python3 make g++:因为 better-sqlite3 v13 没有 install 脚本,pnpm 隐式的 node-gyp 构建会现场编译带binding.gyp的包(v12 则直接下载预编译产物),需要完整工具链。
6.2 运行阶段:同样的 vendor 思路
ARG OPENWORK_SERVER_VERSION=0.18.1 ARG OPENCODE_VERSION ARG OPENCODE_DOWNLOAD_URL= RUN set -eux; \ test -n "$OPENCODE_VERSION"; \ case "$(dpkg --print-architecture)" in \ amd64) asset="opencode-linux-x64-baseline.tar.gz" ;; \ arm64) asset="opencode-linux-arm64.tar.gz" ;; \ esac; \ url="$OPENCODE_DOWNLOAD_URL"; \ if [ -z "$url" ]; then \ url="https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/${asset}"; \ fi; \ curl -fsSL "$url" -o "$tmpdir/$asset"; \ tar -xzf "$tmpdir/$asset" -C "$tmpdir"; \ binary="$(find "$tmpdir" -type f -name opencode | head -n 1)"; \ test -n "$binary"; \ install -m 0755 "$binary" /usr/local/bin/opencode; \ rm -rf "$tmpdir"与install-opencode.mjs完全同构:支持OPENCODE_DOWNLOAD_URL覆盖下载源、同样使用-baseline资产、同样递归find二进制并安装到 PATH。构建阶段还会把opencode-plugins(openwork-extensions-preview.js、openwork-capabilities-knowledge.js、openwork-office-attachments.js等)复制进镜像。
6.3 运行时断言(RUNTIME_ASSERTS)
ARG RUNTIME_ASSERTS=1 RUN if [ "$RUNTIME_ASSERTS" = "1" ]; then \ test -f /opt/openwork/opencode-plugins/openwork-extensions-preview.js \ && ... \ && test "$(openwork-server --version)" = "$OPENWORK_SERVER_VERSION" \ && opencode --version; \ else \ echo "RUNTIME_ASSERTS disabled (cross-build); binaries must be verified on target hardware"; \ fi镜像构建完成后立即在目标架构下执行插件存在性、openwork-server版本一致性、opencode --version三类断言。create-daytona-openwork-snapshot.sh会根据构建主机架构自动开关:x86_64 主机上断言开启(RUNTIME_ASSERTS=1),非 x86_64(如 arm64 Mac 交叉构建)则关闭——因为此时二进制在 qemu 下运行会报Illegal instruction,注释明确要求"binaries must be verified on target hardware"。
6.4 一键打快照
scripts/create-daytona-openwork-snapshot.sh 封装了完整流程:读取.env.daytona(可选)→ 解析openwork-server与 OpenCode 版本 →docker buildx build --platform linux/amd64 --load→ 用daytona snapshot push推送到远端,并自动删除同名旧快照。脚本末尾提示把DAYTONA_SNAPSHOT=<name>写入.env.daytona,Den 控制平面即可直接用快照拉起 Worker,连构建环节都省掉了。
七、机制收益与适用边界
综合源码,这套「构建期 vendor」设计带来的核心收益清晰可验证:
- 消除首启抖动:Worker 节点从镜像启动到可服务之间,不再需要现场访问 GitHub 下载 OpenCode。控制面启动脚本中
OPENWORK_OPENCODE_BIN=./bin/opencode直接指向构建产物,首次启动即为最终状态。 - 版本一致性:
constants.json是唯一版本事实来源,Render 构建、Daytona 快照、OPENCODE_DOWNLOAD_URL覆盖三条路径共享同一版本,杜绝"控制面升级了、节点还跑旧引擎"的漂移。 - 构建可重入:版本戳 + 幂等短路让重复构建零成本;下载重试与临时目录隔离让失败构建不污染产物。
- 多平台覆盖:6 组平台资产映射 + Dockerfile 的 amd64/arm64 分支,配合
RUNTIME_ASSERTS区分原生构建与交叉构建的验证策略。
需要注意的适用前提:资产下载发生在构建期而非运行时,因此构建环境必须有外网访问(除非通过OPENWORK_OPENCODE_DOWNLOAD_URL/OPENCODE_DOWNLOAD_URL指向内网镜像);-baseline资产的选择意味着以兼容性换取性能;而跨架构交叉构建的镜像,必须按 Dockerfile 注释要求在真实目标硬件上完成运行验证。对需要自建 Worker 池或复刻这套云端 Agent 运行时的开发者而言,den-worker-runtime目录加上provisioner.ts的编排逻辑,就是一份可整体照搬的参考实现。
【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考