架构解析:Den 调度、SSE 唤醒与本地可见线程执行)
openwork 桌面执行器Desktop Runner架构解析Den 调度、SSE 唤醒与本地可见线程执行【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork导读本文基于 docs/features/automations-desktop-runner/README.md完整剖析 openwork 中 Den 调度的自动化任务由桌面端执行 这一核心设计Den 只负责持久调度与状态记账已认证的桌面安装则作为desktoprunner 通过 SSE 流被唤醒、通过有界轮询发现工作、以租约lease方式原子认领并最终以本地可见线程的形式执行自动化。读者读完后将掌握该执行模型的整体架构、SSE 指数退避通知协议、认领/心跳/事件上报/完成的完整时序以及离线场景下的失败语义与后续待加固方向。设计目标Outcome调度与执行彻底分离本功能要解决的核心问题是Den 不再需要在自己的容器里启动 OpenCode也不需要托管任何自动化执行运行时。取而代之的是一个职责清晰的边界Den 仍是唯一事实来源source of truth负责持久化以下全部状态自动化调度Automation schedules与每次发生occurrence认领租约leases与尝试次数通知游标notification cursors有序事件ordered events含用户/助手/用量/终端等类型用量统计token usage与最终结果terminal results。桌面端负责执行。一个已认证authenticated的桌面安装会把自己注册为一个desktoprunner并持续维护一条到 Den 的 Server-Sent EventsSSE流。SSE 上只传输两类最小化信息唤醒类型wake-up type与可恢复的通知游标resumable notification cursor——不携带任何任务负载避免把执行细节塞进长连接。从类型定义 packages/types/src/automations.ts 可以看到通知载荷被z.object({...}).strict()严格约束仅允许type与cursor两个字段测试 automation-runner-protocol.test.ts 甚至专门断言了带runId的多余字段会被校验拒绝防止任务数据沿 SSE 泄漏// packages/types/src/automations.ts export const automationRunnerNotificationSchema z.object({ type: z.enum([automation_work_available, automation_cancellation_available]), cursor: z.string().trim().min(1).max(40), }).strict()执行时序发现 → 原子认领 → 心跳与事件 → 完成当桌面 runner 完成 SSE 连接、或收到一条唤醒通知后会转入一个基于 HTTP 的短连接协议发现工作discover桌面通过 HTTP 向 Den 请求待处理工作。工作响应automationRunnerWorkResponseSchema是items数组每项形如{ runId, executionTarget: desktop }最多 9 条后续扩展的远程会话创建命令也复用同一响应结构。原子认领claim桌面针对一个runId发起认领。仓库层claimDesktop的实现保证同一时刻只有一个租约持有者认领条件要求租约所有者匹配、且运行状态处于claimed/running集合之外认领成功时以attempt_count 1的方式推进尝试次数失败则返回null见 repository.ts 与对应测试 Desktop runner claims are idempotent for one owner and exclude competing runners。心跳续租heartbeat认领后桌面按租约周期上报心跳心跳响应携带leaseValid、cancelRequested与新的leaseExpiresAt。若续租更新影响行数为 0即租约已过期或被他人抢占会返回租约丢失信号服务端把该情形映射为automation_run_complete_lease_lost/runner_lease_lost的 409 响应。有序事件上报events执行过程中的 user、assistant、capability_search、capability_execution、usage、warning、terminal 事件以attempt sequence双重维度排序持久化。事件主键使用desktop:${runId}:${attempt}:${sequence}前缀查询按attempt与sequence升序返回保证桌面与云端事件在各自尝试内严格有序见 repository.ts 与测试 Desktop and Cloud events remain ordered within their claimed attempt。完成complete执行结束后桌面把结果、用量与错误一并写回 Den并持久化本地原生线程身份engineReceipt.nativeThreadId与workspaceId使自动化运行在 Den 中可追溯、可回放见测试 Desktop completion durably exposes its native local thread。认领到的自动化不是黑盒任务而是以普通可见的本地 OpenWork 线程出现在桌面当前活动工作区active workspace中它使用自动化配置时选定的模型并享受与用户手动启动线程完全一致的本地 OpenCode 工具链与集成体验。这正是该特性名称 automations-desktop-runner 的含义——调度在云Den执行在用户自己的机器上。SSE 通知与有界轮询从每分钟 60 次降到 4 次SSE 端点为GET /v1/automation-runners/events是桌面 runner 的常驻存在信号。其实现位于 routes/automations/index.ts核心行为包括支持Last-Event-ID头续传断线重连时桌面带上上次消费的游标Den 从该游标之后继续下发保证可恢复语义。每条通知都带自增游标 ID桌面按游标推进消费进度。每 15 秒发送一条keepaliveSSE 帧作为存活信号keepalive 不写数据库避免每个空闲 runner 变成永久的数据库写入者测试 idle runner keepalives do not persist liveness in the database 对此有专门断言。流内每 15 秒复查一次凭据过期时间identity.expiresAt与成员有效性isActiveRunnerOwner凭据过期或成员失效即断开流。关键优化在于通知轮询的指数退避当 runner 没有待处理通知时Den 把游标查询的间隔从 1 秒开始指数退避至 15 秒上限一旦出现活动立即重置回 1 秒。退避计算在 runner-notification-poll.ts 中实现export const RUNNER_NOTIFICATION_POLL_MIN_MS 1_000 export const RUNNER_NOTIFICATION_POLL_MAX_MS 15_000 export const RUNNER_KEEPALIVE_INTERVAL_MS 15_000 export function nextRunnerNotificationPollDelay( currentDelayMs: number, receivedNotifications: boolean, ): number { if (receivedNotifications) return RUNNER_NOTIFICATION_POLL_MIN_MS return Math.min( RUNNER_NOTIFICATION_POLL_MAX_MS, Math.max(RUNNER_NOTIFICATION_POLL_MIN_MS, currentDelayMs) * 2, ) }配套的capRunnerNotificationPollDelayForKeepalive把睡眠时长约束在距下一次 keepalive 的剩余时间以内确保退避永远不会拖垮既有的 15 秒存在性心跳。效果是空闲 runner 的稳态通知查询频率从每分钟 60 次每秒 1 次降至每分钟 4 次每 15 秒 1 次同时保留了可恢复 SSE 与有界认领协议的低延迟特性。测试 idle runner notification polling backs off without delaying keepalives 验证了 1s → 2s → 4s → 8s → 15s 的完整退避曲线。桌面 runner 的注册与凭据桌面端通过 automation-runner-bridge.tsx 中的AutomationRunnerBridge组件完成自动注册仅当运行在 Electron 桌面运行时isDesktopRuntime()、已登录且 Den 允许部署自动化时组件才会创建 Den 客户端并调用凭据铸造接口。注册载荷由automationDesktopRunnerRegistrationSchema严格约束关键字段如下字段约束说明runnerId最少 8 字符由桌面本地生成并持久化在localStorage键openwork.automations.desktop-runner-id重启后保持稳定protocolVersion固定1协议版本字面量拒绝其他值supportedExecutionTargets仅[desktop]当前执行目标契约只允许 desktop注册[sandbox]会被拒绝capabilities最多 2 项目前为model_attention_v1与remote_session_v1appVersion1–80 字符取自桌面构建信息platformdarwin/win32/linux从 UA 推断concurrency1–4桌面侧并发执行上限当前桥实现固定为 1注册成功后桥会通过 Electron IPCautomationRunnerConfigure把baseUrl、token、runnerId交给桌面主进程由主进程实际维持 SSE 连接与轮询。凭据默认每 30 分钟刷新一次遇到automation_runner_identity_conflict409时会重置本地 runnerId 重新铸造。测试同时确认凭据铸造端点POST /v1/automation-runners/token被显式标记为x-mcp: false永远不会暴露为 MCP 工具且每个 runner 端点都会复查 token 所有者是否仍为活跃成员。在线状态与并发边界存在性窗口AUTOMATION_DESKTOP_RUNNER_PRESENCE_WINDOW_MS 10 * 60_00010 分钟。由于空闲 SSE 流刻意不写数据库注册刷新每几分钟一次与真实工作查询才是存在性的主要写入来源因此该窗口设计得足够宽松以覆盖空闲桌面又足够短以捕获已关闭的桌面。存在性查询是只读视图desktopRunnerPresence/desktopRunnerLastSeenAt只做select绝不update/insert——任何额外写入都会重新引入空闲数据库流量违背有界轮询的设计初衷测试 runner presence is a read-only view of existing liveness data。认领幂等性claimDesktop以lease_owner 状态集合双重条件保证幂等且对并发竞争者互斥租约恢复recoverExpiredLeases在更新时校验idlease_ownerlease_expires_at三重条件防止过期租约回收覆盖并发续租的租约。尝试上限单次运行最多 2 次尝试AUTOMATION_MAXIMUM_ATTEMPTS 2重试间隔 30 秒每次尝试的完成都要求attempt_count匹配过期尝试的迟到完成会被 409 拒绝。离线行为有界认领窗口与明确的失败语义计划scheduled类型的桌面自动化发生有有界的认领窗口claim window窗口由DEN_AUTOMATIONS_RUNNER_CLAIM_DEADLINE_MS环境变量控制测试中以 1000ms 运行并在认领路径上与事件自身下一次到期时间nextDueAt取较小值钳制。无人认领若窗口截止前没有符合条件的桌面 runner 认领Den 会持久记录运行结果为skipped错误码为runner_unavailable测试 desktop recovery deadlines hold, expire with exact causes 验证了截止前 1ms 仍在队列、截止时被过期并记录明确原因。界面呈现应用端 automations-page.tsx 会把这类运行渲染为Missed — desktop runner unavailable实际文案优先采用 Den 下发的具体原因缺失时回退到该通用文案。原因可审计仓库层的missedDesktopReason会根据实际观察到的情形给出不同文案——完全没有桌面连接、桌面连接但未及时认领、桌面正忙于另一条自动化——而不是千篇一律的笼统措辞见测试 desktop occurrences stay claimable through the recovery window with named missed causes 与 desktop recovery deadlines hold... 中的三种 message 断言。Run now 立即失败当没有任何桌面 SSE 连接在线时立即运行POST /v1/automations/:id/run会立即失败并返回No desktop runner is online而不是进入漫长的等待。不触碰模型供应商整个离线判定过程绝不会调用任何模型提供商——测试通过替换全局fetch为 503 并断言providerCompletionCalls 0来证明这一点。聚焦验证Focused proof设计文档记录了 2026-08-04 打包的 macOS 候选版本连接到本地 Den 测试 profile 后产出的两个必需结果Desktop visible thread E2E 08:45:31以普通侧边栏线程成功运行结果为DESKTOP_VISIBLE_THREAD_OK包含有序的 user/assistant/usage/terminal 事件且 token 用量已存入 Den——证明桌面可见线程 有序事件 用量回写闭环成立。Disconnected desktop final 08:46:51未被认领成为显式的 misseddesktop-runner-unavailable回执断开连接状态下执行 Run now 返回显式的 no-runner-online 响应——证明离线失败语义与在线路径一致且可预期。后续待加固Deferred hardening该特性当前为聚焦验证状态文档明确列出了后续工作仓库级repository-wide自动化套件跨平台打包验证当前验证仅覆盖 macOS多副本multi-replica在线存在性验证详尽的断线重连与竞态测试故障注入fault injection测试更广泛的安全审查。此外文档预告未来可新增sandbox执行目标以扩展执行目标契约executionTarget目前仅desktop/cloud二值但该目标尚未实现——注册 schema 中supportedExecutionTargets仍只接受字面量desktop。源码索引与进一步阅读设计文档docs/features/automations-desktop-runner/README.md协议类型与校验packages/types/src/automations.ts注册、通知、认领、心跳、事件、结果、presence、错误码全套 schema轮询退避实现ee/apps/den-api/src/automations/runner-notification-poll.ts路由与 SSE 端点ee/apps/den-api/src/routes/automations/index.ts/v1/automation-runners/events、/v1/automation-runner/work、/v1/automation-runs/:id/claim、/v1/automation-runs/:id/heartbeat、/v1/automations/:id/run认领/心跳/事件/过期恢复的仓库层实现ee/apps/den-api/src/automations/repository.ts协议测试含完整恢复窗口验证ee/apps/den-api/test/automation-runner-protocol.test.ts桌面端注册桥apps/app/src/react-app/domains/automations/automation-runner-bridge.tsx自动化列表与 Missed 文案渲染apps/app/src/react-app/domains/automations/automations-page.tsx【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考