资讯动态

OpenWork Automations 领域包:无基础设施依赖的定时任务调度契约与宿主机引擎生命周期设计

发布时间:2026/9/13 17:01:19 来源:尧图企业网站定制
OpenWork Automations 领域包无基础设施依赖的定时任务调度契约与宿主机引擎生命周期设计【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork导读openwork/automations是 OpenWork 中由云端 Den 与自托管on-premDen 共享的纯领域包它不依赖任何基础设施运行时而是以类型契约的形式完整定义了一次性once、每日daily与每周weekly调度的确定性计算、DST 行为、生命周期迁移、修订摘要revision digest、发生次标识occurrence identity、幂等性、仓储与引擎适配器端口、到期工作选择、有界错失恢复以及仓储一致性辅助工具。本文基于 packages/automations/README.md 并深入该包源码讲解 Automations 领域模型的全部契约细节、调度算法的确定性实现、Den 宿主机引擎生命周期协议以及如何通过内置的一致性校验工具验证自己的仓储实现帮助你在自托管或二次开发场景中正确接入该领域包。领域边界纯契约无运行时适配器openwork/automations的设计目标可以概括为一句话它只拥有领域逻辑绝不拥有运行时。这一点在包的 README 中开宗明义——Pure, infrastructure-free Automations domain shared by hosted and on-prem Den。从 package.json 可以看到该包的运行时依赖仅有openwork/typesworkspace 内部类型包与zod用于 schema 校验其余基础设施能力全部通过端口port以接口形式暴露MySQL 持久化、租赁lease、成员与模型校验由 Den 提供Connect 访问与模型执行适配器由 Den 提供OpenWork 桌面端只是 Den 的客户端永远不会成为 Automations 的调度器或执行宿主。与之对应src/ports.ts 定义了唯一的仓储边界AutomationRepository接口其中明确注释了Claims and revision updates must be transactional认领与修订更新必须事务化。该接口涵盖了create、update、list、get、setState、listDue、claim、heartbeat、appendEvent、complete、recoverExpiredLeases、requestCancellation、getRunReceipt、listRuns等全部领域操作。关键领域概念清单从 src/index.ts 的导出可以看到领域包由七个模块组成模块职责contracts.ts修订摘要revision digest、发生次身份occurrence identityschedule.ts确定性调度计算、DST 行为、未来发生次预览engine.ts宿主机引擎适配器契约、事件序列校验器ports.ts仓储与列表项端口AutomationRepository、AutomationListItemrunner.ts桌面 runner 认领窗口、在线判断、错失原因诊断state.ts生命周期状态机与迁移校验tick.ts到期工作选择due-work selection与认领推进调度契约一次性与循环调度的确定性计算调度是 Automations 领域包的核心资产。调度的数据模型定义在类型包 packages/types/src/automations.ts 中使用 zod 判别联合discriminated unionexport const automationScheduleSchema z.discriminatedUnion(kind, [ z.object({ kind: z.literal(once), timezone: timezoneSchema, at: timestampSchema }), z.object({ kind: z.literal(daily), timezone: timezoneSchema, hour: z.number().int().min(0).max(23), minute: z.number().int().min(0).max(59), }), z.object({ kind: z.literal(weekly), timezone: timezoneSchema, daysOfWeek: z.array(z.number().int().min(0).max(6)).min(1).max(7) .transform((days) [...new Set(days)].sort((left, right) left - right)), hour: z.number().int().min(0).max(23), minute: z.number().int().min(0).max(59), }), ]) export type AutomationSchedule z.infertypeof automationScheduleSchema要点once指定timezone与绝对时间戳at只触发一次daily指定timezone、hour0–23、minute0–59weekly额外指定daysOfWeek取值范围 0–6对应周日到周六至少 1 个、至多 7 个schema 会自动去重并升序排序保证同一配置序列化后字节级一致timezone必须是合法的 IANA 时区名如UTC、Asia/Shanghai类型包通过Intl.DateTimeFormat校验。发生次搜索自动化发生次计算的确定性实现src/schedule.ts 中automationOccurrences(input, options)是全部调度计算的核心返回{ occurrences: number[]; warnings: string[] }export function automationOccurrences( input: AutomationSchedule, options: AutomationOccurrenceSearchOptions, ): { occurrences: number[]; warnings: string[] } { const schedule automationScheduleSchema.parse(input) assertAutomationTimezone(schedule.timezone) const count Math.max(0, Math.min(options.count ?? 5, 5)) // once: 直接返回 at after 的单个时间戳 // daily/weekly: 从 after 的本地时间开始逐日扫描最多 370 天 // 对每个应触发日解析墙钟时刻 → 绝对时间戳 }几个值得注意的实现细节每次调用都会先用 schema 校验输入保证发生次计算的输入永远是规范化的单次最多返回 5 个发生次count上限被钳制到 5搜索窗口上限 370 天避免极端边界下无限循环nextAutomationOccurrence(schedule, after)只是automationOccurrences取count: 1的便捷封装。DST 行为墙钟时间到绝对时间戳的解析时区处理是调度系统最容易出错的地方。schedule.ts通过Intl.DateTimeFormat以目标时区格式化时间戳解析出本地年月日时分与星期再用 UTC 构造名义时间戳nominal随后在nominal ± 18 小时的窗口内逐分钟扫描寻找本地时间恰好等于目标墙钟时间的绝对时间戳const nominal Date.UTC(date.year, date.month - 1, date.day, hour, minute) const start nominal - SEARCH_WINDOW_HOURS * 60 * 60 * 1_000 // 18 小时前 const end nominal SEARCH_WINDOW_HOURS * 60 * 60 * 1_000 // 18 小时后这样既处理了春季调快spring-forward时墙钟时间不存在的情况返回shifted: true向后取下一个有效分钟也处理了秋季调慢fall-back时墙钟时间重复的情况优先返回精确匹配不重复触发。当发生 DST 偏移时会向调用方返回一条 warningA wall-clock occurrence falls inside a daylight-saving transition and was shifted to the next valid minute in timezone.previewAutomationSchedule(input, options)是对外暴露的预览入口返回{ schedule, generatedAt, occurrences, warnings }其中occurrences固定取 5 个warnings用于向前端透传 DST 提示——这正是调度创建/编辑界面未来 5 次运行时间预览的数据来源。有界错失恢复只补最近一次绝不重放积压src/schedule.ts 末尾的recoverableAutomationOccurrence实现了一个重要的产品语义/** Returns at most the latest missed occurrence; older backlog is never replayed. */ export function recoverableAutomationOccurrence( schedule: AutomationSchedule, input: { after: number; now: number }, ): number | null { const occurrences automationOccurrences(schedule, { after: input.after, count: 5 }).occurrences .filter((occurrence) occurrence input.now) return occurrences.at(-1) ?? null }即当 Den 宕机一段时间后重启面对积压的多个错失发生次只认领最近一次更早的积压直接丢弃避免恢复时瞬间洪泛执行历史任务。这与 runner 模块中错失窗口不会跨到下一个发生次的设计互为犄角。修订摘要与发生次身份幂等性的两个基石修订摘要revision digestsrc/contracts.ts 中的automationRevisionDigest对修订内容instructions、schedule、model、maximumRuntimeMs以及可选的action、executionTarget、workspaceId做可移植的稳定摘要先通过canonical()将对象递归规范化为键按字典序排序的确定字符串再用双哈希FNV-1a 变体0x811c9dc5与0x9e3779b9混合产出 16 位十六进制摘要注释明确持久化层可以额外使用密码学摘要但领域包本身保证摘要跨进程、跨平台可复现特别强调只有当workspaceId被设置时才参与摘要——因为固定工作区是行为变更而 pinning 功能出现之前创建的记录必须保持字节级一致的摘要。发生次身份occurrence identity同文件中的automationOccurrenceIdentity(input)为每次运行产出occurrenceId与idempotencyKeyconst occurrence input.scheduledFor null ? manual:${input.nonce} : String(input.scheduledFor) const stable [input.automationId, occurrence].map(encodeURIComponent).join(:) return { occurrenceId: automation-occurrence:${stable}, idempotencyKey: automation:${stable}, }设计要点定时发生次以automationId scheduledFor绝对时间戳作为稳定身份手动发生次必须携带nonce否则直接抛错身份为manual:nonceidempotencyKey是 Den 持久化事件与认领去重的键重复投递同一发生次不会产生重复运行。生命周期状态机受控的状态迁移src/state.ts 定义了 Automations 的四个状态与迁移表const transitions: ReadonlyRecordAutomationState, readonly AutomationState[] { active: [inactive, needs_attention, archived], inactive: [active, needs_attention, archived], needs_attention: [active, inactive, archived], archived: [], // 终态不可再迁移 }canTransitionAutomation(from, to)允许同状态幂等迁移from toassertAutomationTransition则在非法迁移时抛出Invalid Automation transition: from - to。此外isTerminalAutomationRunStatus将运行状态succeeded / failed / cancelled / skipped视为终态。Den 的/v1/automations/:id/activate与/v1/automations/:id/deactivate路由见 ee/apps/den-api/src/routes/automations/index.ts正是基于这一状态机的薄封装。到期工作选择与认领推进src/tick.ts 是调度器心跳逻辑的纯函数部分selectDueAutomations(candidates, { now, limit })过滤出state active且nextDueAt now的条目按nextDueAt升序相同时按automationId字典序排序取前limit个limit 被钳制在 1–500hasActiveRun(runs)存在claimed或running状态即视为有活跃运行用于判断是否允许新认领nextAutomationAfterClaim(automation, nextDueAt, now)认领后推进nextDueAt并刷新latestRunAt、updatedAt。配套的AutomationClaimResult判别联合src/ports.ts区分三种认领结果claimed本次认领成功携带run与revisionduplicate同一发生次已被认领幂等去重overlap已有活跃运行新发生次与现有运行重叠应跳过。桌面 runner 的窗口语义与错失诊断OpenWork 桌面端不参与调度只作为执行 runner。 src/runner.ts 为此定义了三个关键语义有界认领窗口export const AUTOMATION_MIN_CLAIM_WINDOW_MS 60_000 export function desktopClaimDeadline(input: { now: number; windowMs: number; nextDueAt: number | null }): number { const requested input.now input.windowMs const bounded input.nextDueAt null ? requested : Math.min(requested, input.nextDueAt) const floor input.now Math.min(input.windowMs, AUTOMATION_MIN_CLAIM_WINDOW_MS) return Math.max(floor, bounded) }注释给出了关键设计动机桌面是笔记本电脑会休眠、重启、切换网络所以认领窗口是恢复窗口而非存活检查——只要桌面在窗口内回来就仍会执行该发生次而不是让操作者面对一次错失运行。同时窗口绝不会跨到下一个发生次一个仍未认领的每小时 10:00 运行必须在 11:00 到期前释放否则后续发生次会因重叠而被跳过。持久在线判断export function desktopRunnerConnected(input: { lastSeenAt: number | null; now: number }): boolean { return input.lastSeenAt ! null input.now - input.lastSeenAt AUTOMATION_DESKTOP_RUNNER_PRESENCE_WINDOW_MS }在线状态是**持久化durable而非实时live**的注册每隔几分钟刷新一次空闲事件流刻意避免写数据库因此桌面在最后一次被看到之后的一段时间内仍被视为在线。错失原因诊断missedDesktopRunMessage将错失原因区分为三种可操作的结果源码注释直言一个笼统的结果曾掩盖真实缺陷数周桌面正忙busy→Missed — the desktop was busy with another Automation run.桌面在线但未认领 →Missed — the connected desktop did not pick this up in time.无桌面在线 →Missed — no desktop was connected.这与 docs/features/automations-desktop-runner/README.md 描述的离线行为一致认领窗口到期后 Den 持久化记录skippedrunner_unavailable应用显示Missed — desktop runner unavailable而没有任何桌面 SSE 连接时手动触发 Run now 会立即失败并返回No desktop runner is online。宿主机引擎生命周期幂等准入、持久化重挂载与顺序事件AutomationEngineAdaptersrc/engine.ts是托管运行与执行引擎之间的提供者中立边界export interface AutomationEngineAdapter { capabilities(): PromiseAutomationEngineCapabilityDeclaration admit(request: AutomationEngineAdmissionRequest): PromiseAutomationEngineAdmissionReceipt observe(receipt: AutomationEngineAdmissionReceipt, options?: AutomationEngineObserveOptions): AsyncIterableAutomationEngineEvent read(receipt: AutomationEngineAdmissionReceipt): PromiseAutomationEngineReadResult | null cancel(receipt: AutomationEngineAdmissionReceipt): PromiseAutomationEngineCancellationResult }能力声明与隔离约束automationEngineCapabilityDeclarationSchema强制适配器声明src/engine.tsadmission: idempotent、reattachment: receipt、eventDelivery: ordered_at_least_once、resultPersistence: durablecancellation可为supported / best_effort / unsupportedisolation明确运行位置在云端cloud、无文件系统、无 shell、无浏览器、无 computer 工具、Connect 访问按运行作用域隔离run-scoped、网络仅限提供者与 Connectprovider-and-connect-only。准入协议与接收凭证生命周期协议src/engine.ts的核心规则Den 先创建并持久化准入键admission key再调用admit重试同一个准入键必须返回同一份持久化安全的接收凭证receipt——这是幂等准入能力访问令牌capabilityAccess含 endpoint 与 bearerToken只属于准入请求本身绝不能复制进 receipt——receipt 中的attachment对 Den 是不透明的opaque适配器自行定义其形状与解释但 Den 永远读不到令牌automationEngineAdmissionRequestSchema通过superRefine交叉校验修订必须属于该 Automation运行必须属于该修订防止跨实体串号。事件持久化与崩溃恢复事件流协议是先持久化、后推进游标的顺序每个事件带稳定幂等键idempotencyKey与严格递增的执行内序列号sequenceDen 在推进连续序列游标之前先用稳定幂等键持久化每个观察到的事件进程重启后Den 加载已持久化的 receipt 与游标调用read获取持久化状态/结果然后无需内存中的引擎句柄即可从observe(receipt, { afterSequence })恢复订阅取消cancel使用同一份 receipt因此取消操作在调度器所有权变更与 Den 重启后依然有效。createAutomationEngineEventSequenceValidatorsrc/engine.ts在事件落库前做四重校验executionId与runId必须匹配 receipt防止串执行sequence必须等于cursor 1连续性跳号即抛错idempotencyKey不得重复幂等性校验通过后才把事件交给 Den 持久化并推进游标。引擎结果automationEngineResultSchema同样有交叉约束succeeded状态不得携带errorfailed/cancelled状态必须携带error。仓储一致性校验接入自有存储的正确姿势自托管或二次开发时若需要实现自己的AutomationRepository领域包提供了现成的一致性校验器。src/testing.ts 中的verifyAutomationRepositoryConformance(repository)会依次验证事务化创建即 active新建的 Automation 状态必须为active初始修订持久化get返回的修订 id 必须等于创建时的修订 id组织隔离用其他organizationId查询必须返回null修订不可变update后修订版本号必须递增且新修订 id 不能复用旧修订不允许原地修改认领去重同一发生次被 replica A 认领后replica B 再认领必须返回非claimed结果验证 scheduled 与 recovery 两种触发方式的去重。该函数返回checked: string[]逐条列出通过的检查项任何一项失败都会抛出异常——这正是 ee/apps/den-api/src/automations/repository.ts 中 MySQL 仓储实现所通过的测试基准。包内同时提供 engine 测试辅助src/engine-testing.ts与核心行为测试src/core.test.ts、src/schedule.test.ts 等。在 Den 中的实际接线虽然领域包本身不提供运行时但仓库中的云端 DenEE 部分展示了完整接线方式ee/apps/den-api/src/app.ts 注册自动化路由并通过configureCloudAgentExecutor/configureCloudWorkflowExecutor配置云执行器ee/apps/den-api/src/automations/repository.ts 以 MySQL 实现AutomationRepository端口ee/apps/den-api/src/automations/service.ts 使用AUTOMATION_MIN_CLAIM_WINDOW_MS与desktopRunnerConnected编排认领与在线判断ee/apps/den-api/src/routes/automations/index.ts 暴露/v1/automations系列 REST 路由创建、列表、激活/停用、手动 Run now、运行记录查询等。桌面端通过 SSE 订阅唤醒、以 HTTP 原子认领发生次并以认领→心跳→顺序事件→取消→完成的协议回报执行过程具体离线与错失行为见 docs/features/automations-desktop-runner/README.md。小结openwork/automations是一个教科书式的领域层与基础设施解耦实践所有调度计算含 DST、状态机、幂等身份、顺序事件协议与认领语义都以纯函数和 zod schema 形式沉淀在 packages/automations/src 中可被云端与自托管 Den 零成本复用而 MySQL、租赁、模型校验、Connect 与执行引擎全部由 Den 通过AutomationRepository与AutomationEngineAdapter两个端口注入。理解这套契约既是接入自托管调度的前提也是审查 OpenWork 自动化可靠性的最佳入口。【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价