news 2026/9/13 17:01:16

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

作者头像

张小明

前端开发工程师

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

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

【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork

导读

@openwork/automations是 OpenWork 中由云端 Den 与自托管(on-prem)Den 共享的纯领域包,它不依赖任何基础设施运行时,而是以类型契约的形式完整定义了一次性(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/types(workspace 内部类型包)与zod(用于 schema 校验),其余基础设施能力全部通过端口(port)以接口形式暴露:

  • MySQL 持久化租赁(lease)成员与模型校验由 Den 提供;
  • Connect 访问模型执行适配器由 Den 提供;
  • OpenWork 桌面端只是 Den 的客户端,永远不会成为 Automations 的调度器或执行宿主

与之对应,src/ports.ts 定义了唯一的仓储边界AutomationRepository接口,其中明确注释了"Claims and revision updates must be transactional"(认领与修订更新必须事务化)。该接口涵盖了createupdatelistgetsetStatelistDueclaimheartbeatappendEventcompleterecoverExpiredLeasesrequestCancellationgetRunReceiptlistRuns等全部领域操作。

关键领域概念清单

从 src/index.ts 的导出可以看到,领域包由七个模块组成:

模块职责
contracts.ts修订摘要(revision digest)、发生次身份(occurrence identity)
schedule.ts确定性调度计算、DST 行为、未来发生次预览
engine.ts宿主机引擎适配器契约、事件序列校验器
ports.ts仓储与列表项端口(AutomationRepositoryAutomationListItem
runner.ts桌面 runner 认领窗口、在线判断、错失原因诊断
state.ts生命周期状态机与迁移校验
tick.ts到期工作选择(due-work selection)与认领推进

调度契约:一次性与循环调度的确定性计算

调度是 Automations 领域包的核心资产。调度的数据模型定义在类型包 packages/types/src/automations.ts 中,使用 zod 判别联合(discriminated union):

export 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.infer<typeof automationScheduleSchema>

要点:

  • once:指定timezone与绝对时间戳at,只触发一次;
  • daily:指定timezonehour(0–23)、minute(0–59);
  • weekly:额外指定daysOfWeek,取值范围 0–6(对应周日到周六),至少 1 个、至多 7 个;schema 会自动去重并升序排序,保证同一配置序列化后字节级一致;
  • timezone必须是合法的 IANA 时区名(如UTCAsia/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 天, // 对每个应触发日解析"墙钟时刻 → 绝对时间戳" }

几个值得注意的实现细节:

  1. 每次调用都会先用 schema 校验输入,保证发生次计算的输入永远是规范化的;
  2. 单次最多返回 5 个发生次count上限被钳制到 5);
  3. 搜索窗口上限 370 天,避免极端边界下无限循环;
  4. nextAutomationOccurrence(schedule, after)只是automationOccurrencescount: 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 偏移时,会向调用方返回一条 warning:

A 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 digest)

src/contracts.ts 中的automationRevisionDigest对修订内容(instructionsschedulemodelmaximumRuntimeMs,以及可选的actionexecutionTargetworkspaceId)做可移植的稳定摘要

  • 先通过canonical()将对象递归规范化为键按字典序排序的确定字符串;
  • 再用双哈希(FNV-1a 变体0x811c9dc50x9e3779b9混合)产出 16 位十六进制摘要;
  • 注释明确:持久化层可以额外使用密码学摘要,但领域包本身保证摘要跨进程、跨平台可复现;
  • 特别强调:只有当workspaceId被设置时才参与摘要——因为固定工作区是行为变更,而 pinning 功能出现之前创建的记录必须保持字节级一致的摘要。

发生次身份(occurrence identity)

同文件中的automationOccurrenceIdentity(input)为每次运行产出occurrenceIdidempotencyKey

const 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:<nonce>
  • idempotencyKey是 Den 持久化事件与认领去重的键,重复投递同一发生次不会产生重复运行。

生命周期状态机:受控的状态迁移

src/state.ts 定义了 Automations 的四个状态与迁移表:

const transitions: Readonly<Record<AutomationState, readonly AutomationState[]>> = { active: ["inactive", "needs_attention", "archived"], inactive: ["active", "needs_attention", "archived"], needs_attention: ["active", "inactive", "archived"], archived: [], // 终态,不可再迁移 }

canTransitionAutomation(from, to)允许同状态幂等迁移(from === to),assertAutomationTransition则在非法迁移时抛出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–500);
  • hasActiveRun(runs):存在claimedrunning状态即视为有活跃运行,用于判断是否允许新认领;
  • nextAutomationAfterClaim(automation, nextDueAt, now):认领后推进nextDueAt并刷新latestRunAtupdatedAt

配套的AutomationClaimResult判别联合(src/ports.ts)区分三种认领结果:

  • claimed:本次认领成功,携带runrevision
  • duplicate:同一发生次已被认领(幂等去重);
  • 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

宿主机引擎生命周期:幂等准入、持久化重挂载与顺序事件

AutomationEngineAdapter(src/engine.ts)是托管运行与执行引擎之间的提供者中立边界

export interface AutomationEngineAdapter { capabilities(): Promise<AutomationEngineCapabilityDeclaration> admit(request: AutomationEngineAdmissionRequest): Promise<AutomationEngineAdmissionReceipt> observe(receipt: AutomationEngineAdmissionReceipt, options?: AutomationEngineObserveOptions): AsyncIterable<AutomationEngineEvent> read(receipt: AutomationEngineAdmissionReceipt): Promise<AutomationEngineReadResult | null> cancel(receipt: AutomationEngineAdmissionReceipt): Promise<AutomationEngineCancellationResult> }

能力声明与隔离约束

automationEngineCapabilityDeclarationSchema强制适配器声明(src/engine.ts):

  • admission: "idempotent"reattachment: "receipt"eventDelivery: "ordered_at_least_once"resultPersistence: "durable"
  • cancellation可为supported / best_effort / unsupported
  • isolation明确:运行位置在云端(cloud)、无文件系统、无 shell、无浏览器、无 computer 工具、Connect 访问按运行作用域隔离(run-scoped)、网络仅限提供者与 Connect(provider-and-connect-only)。

准入协议与接收凭证

生命周期协议(src/engine.ts)的核心规则:

  1. Den 先创建并持久化准入键(admission key),再调用admit;重试同一个准入键必须返回同一份"持久化安全"的接收凭证(receipt)——这是幂等准入;
  2. 能力访问令牌(capabilityAccess,含 endpoint 与 bearerToken)只属于准入请求本身,绝不能复制进 receipt——receipt 中的attachment对 Den 是不透明的(opaque),适配器自行定义其形状与解释,但 Den 永远读不到令牌;
  3. automationEngineAdmissionRequestSchema通过superRefine交叉校验:修订必须属于该 Automation,运行必须属于该修订,防止跨实体串号。

事件持久化与崩溃恢复

事件流协议是"先持久化、后推进游标"的顺序:

  • 每个事件带稳定幂等键(idempotencyKey)严格递增的执行内序列号(sequence)
  • Den 在推进连续序列游标之前先用稳定幂等键持久化每个观察到的事件;
  • 进程重启后,Den 加载已持久化的 receipt 与游标,调用read获取持久化状态/结果,然后无需内存中的引擎句柄即可从observe(receipt, { afterSequence })恢复订阅;
  • 取消(cancel)使用同一份 receipt,因此取消操作在调度器所有权变更与 Den 重启后依然有效。

createAutomationEngineEventSequenceValidator(src/engine.ts)在事件落库前做四重校验:

  1. executionIdrunId必须匹配 receipt(防止串执行);
  2. sequence必须等于cursor + 1连续性,跳号即抛错);
  3. idempotencyKey不得重复(幂等性);
  4. 校验通过后才把事件交给 Den 持久化并推进游标。

引擎结果(automationEngineResultSchema)同样有交叉约束:succeeded状态不得携带errorfailed/cancelled状态必须携带error

仓储一致性校验:接入自有存储的正确姿势

自托管或二次开发时若需要实现自己的AutomationRepository,领域包提供了现成的一致性校验器。src/testing.ts 中的verifyAutomationRepositoryConformance(repository)会依次验证:

  1. 事务化创建即 active:新建的 Automation 状态必须为active
  2. 初始修订持久化get返回的修订 id 必须等于创建时的修订 id;
  3. 组织隔离:用其他organizationId查询必须返回null
  4. 修订不可变update后修订版本号必须递增,且新修订 id 不能复用旧修订(不允许原地修改);
  5. 认领去重:同一发生次被 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 中的实际接线

虽然领域包本身不提供运行时,但仓库中的云端 Den(EE 部分)展示了完整接线方式:

  • 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_MSdesktopRunnerConnected编排认领与在线判断;
  • 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 通过AutomationRepositoryAutomationEngineAdapter两个端口注入。理解这套契约,既是接入自托管调度的前提,也是审查 OpenWork 自动化可靠性的最佳入口。

【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 17:00:56

Java+Vue垃圾分类小程序开发实战与架构解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 16:58:48

手写User-Based协同过滤推荐系统:从稀疏矩阵到前端展示全链路实现

简介&#xff1a;本资源是一套完整的基于Python的协同过滤推荐算法电影推荐系统&#xff0c;专为计算机相关专业本科生毕业设计、课程设计及项目实战学习者打造&#xff0c;有效解决推荐系统原理理解与工程落地脱节问题。压缩包共1197个文件&#xff0c;含22个核心Python源码文…

作者头像 李华
网站建设 2026/9/13 16:58:28

UnoCSS Autocomplete 完全指南:为原子化 CSS 打造智能补全

UnoCSS Autocomplete 完全指南&#xff1a;为原子化 CSS 打造智能补全 【免费下载链接】unocss The instant on-demand atomic CSS engine. 项目地址: https://gitcode.com/GitHub_Trending/un/unocss UnoCSS 的 Autocomplete 是一套面向智能提示的可定制机制&#xff0…

作者头像 李华