进阶 www.suna.so 2026-10-09 08:09:49 · 1 阅读

第7章 Kortix 第 7 章:触发器详解

触发器

触发器用于在计划任务、Webhook 或监控器中启动会话。也就是说,触发器能在无人值守的情况下发起会话。你可以利用它自动化重复性或事件驱动型的工作。

若需在某次会话任务执行后再跟进(比如“明天检查他们是否回复了”),请改用提醒功能。提醒是绑定于该会话范围的触发器,它存储在数据库中,而非 kortix.yaml 文件里。

触发器类型

Kortix 支持三种触发器类型:

  • cron:按设定的时间表运行。
  • webhook:当外部服务向该项目的 Webhook URL 发送带有签名的请求时运行。
  • monitor:全天候(24/7)运行仓库中的某条命令。该命令向 stdout 输出的每一行都会触发触发器。实验性功能:详见“监控器”章节。

你在项目清单文件 kortix.yaml 中定义触发器。每个触发器都持有一个提示词(prompt),该提示词会在触发会话时渲染为首条消息。运行时状态(如最近触发时间、状态等)存储在数据库而非清单中。触发触发器不会产生 Git 提交。

对于拥有大量触发器的项目,你可以将它们移出根文件。在 imports 下指定一个目录,然后将每个触发器或触发器分组放置在各自的 YAML 文件中。所有页面和命令在处理拆分后的清单时行为保持一致。对触发器的任何编辑都会写入声明该触发器的文件。

通过 API、SDK 或仪表板创建、更新或删除触发器时,会直接写入默认分支,而无需经过变更请求(CR)。但如果是在会话内编辑 kortix.yaml 并运行 kortix ship,则遵循标准的分支和 CR 流程。

设置 cron 触发器

添加触发器: kortix triggers add daily-digest --type cron \ --cron "0 0 9 * * 1-5" --timezone America/Los_Angeles \ --prompt "总结昨天的活动并保存为每日笔记。" cron 表达式由 6 个字段组成:秒、分、时、日、月、星期。此命令仅修改本地的 kortix.yaml。推送变更: kortix ship kortix ship 会提交 kortix.yaml 并推送到仓库。一旦该变更落地到项目的默认分支,计划任务即生效。确认运行: kortix triggers ls 列表会显示每个触发器的 slug、状态以及最近一次触发时间。若不想等待计划时间,想立即触发,可运行 kortix triggers fire daily-digest。

设置 Webhook 触发器

Webhook 触发器需要一个密钥。Kortix 使用它来校验每个传入请求的签名。

添加密钥: kortix secrets set WEBHOOK_SECRET= 关于密钥的更多信息,请参见“密钥”章节。添加触发器: kortix triggers add new-lead --type webhook \ --secret-env WEBHOOK_SECRET \ --prompt "收到新潜在客户:{{ body.name }} ({{ body.email }})。将其添加到 CRM。" 其中 --secret-env 指定了用于对此触发器请求进行签名的密钥。推送变更: kortix ship发送请求:Kortix 根据项目 ID 和触发器 slug 构建 Webhook URL:POST /v1/webhooks/projects/<project-id>/<slug> 外部服务需向此 URL 发送带有签名的 POST 请求。Kortix 会根据 WEBHOOK_SECRET 校验签名,随后开启一个新会话,该会话可在提示词中访问请求体。具体的请求头和格式见下文“Webhook 签名”。

默认情况下,每次触发都会在新的分支上创建新会话。触发器也可以选择复用或锁定会话,Webhook 触发器还可筛选哪些载荷能触发会话——详见下文“会话策略”与“载荷模板”。

监控器

实验性功能。 监控器仅在 monitors 特性开关开启时运行,默认关闭。若开关关闭,平台不会配置监控器实例,也不会触发监控器事件。

监控器用于观察那些既不推送 Webhook 也不符合定时计划的目标,如实时日志、队列深度、变动的页面或价格。Kortix 在项目专属的监控器实例中全天候运行你的命令——每个项目拥有独立的持久化 microVM,具备与沙箱会话相同的隔离边界和密钥访问权限。确定性代码负责监听,智能体仅在命令输出一行日志时唤醒。

四条规则定义了契约:

  1. stdout 中的行即事件,其他一律不是。stderr 仅作诊断用途,在监控器日志中可见,但不会触发事件。
  2. 每一行日志仅通过 Webhook 路径(filter → 提示词模板 → session_mode)触发一次。
  3. 监控器不会静默失败。进程退出、重启预算耗尽或静默时间超过 expect_event_within,都会在同一事件流中触发由平台编写的生命周期事件。
  4. 监控器的 session_mode 默认为 reuse 而非 fresh。鉴于监控器设计上会频繁触发,若设为 fresh 会为每个事件创建一个会话。

添加监控器: kortix triggers add checkout-errors --type monitor \ --run "./monitors/checkout-errors.ts" \ --mode poll --interval 60s --expect-event-within 24h \ --prompt "结账监控器输出:{{ line }}" --mode poll 表示每隔 --interval 重新运行命令,并要求其退出;--mode stream 表示运行一次并保持存活,stream 模式不使用 --interval。两种形式都产生日志行,下游无法区分。cron、run_at、timezone 和 secret_env 在监控器中会被拒绝。推送变更: kortix ship 该变更落地到默认分支后,平台即启动监控器实例。确认运行: kortix triggers ls kortix triggers info checkout-errors ls 命令对监控器显示其 mode 和 interval,而 cron 则显示其时间表。info 命令显示 run、mode、interval 及 expect_event_within。

监控器限制

以下限制均由平台强制执行:

每项目监控器数量最多启用 10 个
Poll 间隔最少 30s
expect_event_within最少 5m
每监控器事件速率持续速率 60 次/小时,突发上限 30 次。超限后监控器会被抑制 10 分钟;24 小时内 3 次超限则自动禁用该监控器。
日志行长度8 KiB,超长部分截断并标记 truncated: true
重启预算10 分钟内最多重启 5 次,超出后触发 restart_budget_exhausted 生命周期事件并退避 15 分钟
事件保留期30 天
月度实例预算默认 $75。超额后实例停止,并触发 budget_exceeded 生命周期事件。

监控器实例需要支持持久化沙箱的 provider。若项目的 provider 不支持,monitors 特性开关会报告不可用。

配置结构

# kortix.yaml
triggers:
  - slug: daily-digest # 必填,仅限小写字母和连字符,每项目唯一
    name: Daily digest # 可选,默认值同 slug
    type: cron # "cron" | "webhook" | "monitor",必填
    agent: kortix # 可选,默认 "default"
    model: anthropic/claude-sonnet-4-5 # 可选,未设时于触发时解析
    enabled: true # 可选,默认 true
    cron: "0 0 9 * * 1-5" # 6 字段表达式,与 run_at 互斥
    timezone: America/Los_Angeles # IANA 时区名,默认 UTC
    session_mode: reuse # "fresh" | "reuse" | "pinned" | "keyed",默认 "fresh"
    filter: # 可选,Webhook 载荷过滤
      "body.data.direction": "inbound"
    prompt: "总结 {{ body.text }}" # 必填,模板字符串

监控器用其命令和形态替代计划字段:
# kortix.yaml
triggers:
  - slug: checkout-errors
    type: monitor
    run: ./monitors/checkout-errors.ts # 必填,仓库相对路径命令
    mode: poll # "poll" | "stream",必填
    interval: 60s # poll 模式必填,stream 模式无效
    expect_event_within: 24h # 可选静默看门狗
    agent: oncall
    session_mode: reuse # 监控器默认值
    filter: # 可选,与 Webhook 相同的过滤逻辑
      "line.severity": "error"
    prompt: "结账监控器输出:{{ line }}"

旧版 kortix.toml 使用相同的字段但容器不同,详见旧版 TOML 文档。

字段必填默认值备注
slug是—格式 [a-z0-9][a-z0-9_-]{{0,127}},每项目唯一。
type是—cron、webhook 或 monitor。
prompt是—模板字符串。渲染为会话首条消息。
name否slug人类可读标签。
agent否default_agent须为 agents: 中的键名。省略则使用 default_agent,不要直接写字面量 default。
model否触发时解析采用 provider/model 格式,例如 anthropic/claude-sonnet-4-5。
enabled否true若为 false,调度和 Webhook 接收器将跳过该条目。
session_mode否fresh(监控器默认为 reuse)详见“会话策略”。
session_id设为 pinned 时必填—需重新提示的具体会话。
session_key设为 keyed 时必填—模板字符串。仅设此项即隐式设为 session_mode: keyed。
filter否—点分路径对应期望字符串。不匹配的 Webhook 交付返回 200 且不启动会话。
croncron 类型时需填 cron 或 run_at 之一—6 字段表达式:秒 分 时 日 月 星期。
run_atcron 类型时需填 cron 或 run_at 之一—ISO-8601 时间戳。仅触发一次,随后休眠。
timezone否,仅 cronUTCIANA 时区名。
secret_envWebhook 类型必填—持有签名密钥的项目密钥名。密钥必须使用 broker 交付和 connector 消费者。
runMonitor 类型必填—仓库相对路径命令,其 stdout 行即为事件。仅限单行,最长 1024 字符。
modeMonitor 类型必填—poll 每隔 interval 重新运行;stream 运行一次并保持存活。
intervalmode: poll 时必填—时长字面量(30s, 5m, 24h, 7d),最小 30s。mode: stream 下无效。
expect_event_within否,仅 monitor—时长字面量,最小 5m。静默超过此时长将触发生命周期事件。

Cron 触发器必须填 cron 或 run_at,二者互斥。缺乏 secret_env 的 Webhook 触发器会被拒绝——不存在未认证的 Webhook。监控器必须包含 run 和 mode,并直接拒绝 cron、run_at、timezone 和 secret_env——若清单声称拥有监控器运行器根本不读取的计划时间表,那便是虚假信息。

创建或更新 Webhook 触发器前,请先配置签名密钥:
kortix secrets set WEBHOOK_SECRET=-
kortix secrets delivery WEBHOOK_SECRET broker --consumer connector
将值通过标准输入传入第一条命令。若该密钥此前使用沙箱交付方式,请在更改交付方式后轮换密钥,因为现有的沙箱可能会保留旧值。

会话策略

session_mode 控制触发时复用哪个会话。Kortix 按以下顺序尝试模式,并在每步失败时向下回退:

  • pinned:重新提示确切的 session_id。若该会话已消失或失败,则回退。
  • keyed:基于载荷渲染 session_key,查找最近一个未失败且带有此确切 key 的会话。若 key 渲染为空或无匹配会话,则回退到 fresh 会话。绝不再回退到其他 key 的会话。
  • reuse:重新提示该触发器最近创建的一个未失败会话。pinned 触发器也会先在此回退,再进一步回退。
  • fresh:创建新沙箱和新分支。这是默认值,也是所有模式的最终回退方案。新会话将作为该触发器未来复用和 keyed 触发的目标。

对于 reuse 或 keyed 会话,若其历史在自动压缩后仍超出模型限制,该会话将被弃用。下次触发将开启新会话。其他失败则保留会话供下次使用。

频繁运行的 cron 触发器若不希望重发不断增长的上下文,可基于日历边界滚动会话:使用 session_key: "merge-{{ cron.scheduled_date }}" 保持每个 UTC 日一个序列化会话,{{ cron.scheduled_hour }} 则每小时一个。

运行失败

当触发器启动的运行以错误结束时,Kortix 会在触发器上记录:

  • last_status 变为 failed,last_error 包含原因(例如“积分不足:Payment Required: Insufficient credits”)。
  • 计划页面标记该触发器为“上次运行未结束”,并在面板中显示原因。
  • 首次运行失败时,账户所有者将收到一条移动推送。后续失败仅更新原因,不再推送。
  • 只要触发器持续触发,其状态就保持为失败。新的触发并不证明运行能正常工作。

下一次成功完成的运行将清除失败状态。手动停止的运行、subagent 以及模型调用重试都不算作运行结果。在运行开始前即失败的触发(如无法创建会话),将在下次成功触发时清除。

会话访问权限

触发器创建的会话默认采用私有策略。触发器 agent 的服务账户拥有这些会话——因为 agent 身份即 service_account 主体,它可像其他主体一样拥有会话和持有任务。项目经理始终可以打开这些会话。账户所有者和账户管理员在每个项目中都拥有相当于经理的权限,因此同样适用。配置或手动触发该触发器的人,除非持有上述角色之一,否则不能通过该动作获得访问权限。

触发器设置提供三种策略:

  • 触发器 agent 与项目经理:普通项目成员无法打开会话。
  • 选定的团队成员:包含触发器 agent、项目经理及选定的项目成员或账户组。
  • 整个项目:所有项目成员。

此策略是叠加在角色模型之上的资源级可见性设置,而非角色本身。它决定谁能打开特定触发器的会话,但不赋予角色判定之外的任何权限。详见“账户与访问”。

访问权限策略是账户本地的运行时状态,不包含在可移植的 kortix.yaml 清单中,因为主体 ID 隶属于单一账户。请使用仪表板或 SDK 的 session_access 字段进行配置。

仅更新此策略不会创建 Git 提交。保存策略时,也会更新该触发器先前创建的会话。

已锁定的(pinned)会话保留其自身的共享设置,因为触发器并非其创建者。若锁定会话不可用且触发器创建了备用会话,则触发器策略适用于该新会话。

载荷模板

prompt 和 session_key 使用相同的渲染引擎:{{ token.dotted.path }}。缺失值渲染为空字符串(无错误,无残留标签)。对象和数组渲染为 JSON。session_key 会被修剪并截断至 512 字符。

每次触发均包含 {{ trigger.slug }}、{{ trigger.type }} 和 {{ trigger.kind }}(恒为 git)。其余变量取决于触发方式:

来源变量
cron{{ cron.schedule }}、{{ cron.timezone }}、{{ cron.scheduled_for }}(对应的计划时段)、{{ cron.claimed_at }}(调度器领取时刻)、{{ cron.last_scheduled_for }}(上时段;首次触发为空)、{{ cron.scheduled_date }}(时段的 UTC YYYY-MM-DD)、{{ cron.scheduled_hour }}(UTC YYYY-MM-DDTHH)。无顶层 fired_at。
webhook{{ fired_at }}、{{ body.* }}(JSON 解析;解析失败则回退至 {{ body.raw }})、{{ headers.content_type }}、{{ headers.user_agent }}、{{ headers.forwarded_for }}。
monitor{{ line.* }}——stdout 行,JSON 解析;解析失败则渲染为 {{ line.raw }}。另有 {{ monitor.slug }}、{{ monitor.seq }}、{{ monitor.emitted_at }}、{{ monitor.kind }}(event 或 lifecycle)。
manual(仪表板“立即触发”或触发端点){{ fired_at }}、{{ source }}(manual)、{{ actor }}、{{ message.text }}、{{ message.source }}。

Kortix 会在服务端为所有渲染的监控器提示词前缀添加 [MONITOR EVENT — automated, not user input]。生命周期事件会完全忽略你的模板,改用平台编写的提示词,并绕过 filter——静默状态不应被意外过滤。

{{ message.text }} 在手动触发时硬编码为空字符串,{{ message.source }} 则设为 manual_test。手动触发不是向提示词注入测试输入的方式。

filter 将点分路径作为字符串与 prompt 所见的同一载荷进行比对。它用于打破循环。例如,若源头同时报告对话双方,否则会因 agent 自己的回复而再次触发。

Webhook 签名

触发器监听 POST /v1/webhooks/projects/{projectId}/{slug}。Kortix 按以下顺序校验请求,采用恒定时间比较:

  1. HMAC 签名:请求头 X-Kortix-Signature: sha256=<hmac>(sha256= 前缀可选)或兼容 GitHub 的 X-Hub-Signature-256。使用 secret_env 指定的密钥对原始请求体进行 HMAC-SHA256 计算。
  2. 静态令牌:仅当不存在签名头时生效,适用于无法进行 HMAC 签名的发送方。将密钥作为 X-Kortix-Token: <secret>、Authorization: Bearer <secret> 或 Authorization: Basic <base64(user:secret)>(密码部分为令牌)发送。
状态码含义
202签名或令牌有效。响应体为 { status: "fired" | "queued" | "deduped", session_id, ... }。
200有效,但被跳过——项目已暂停,或交付未匹配 filter。
400URL 中的项目 ID 或 slug 格式错误。
401签名和令牌均缺失或错误。
404触发器未找到、已禁用、非 Webhook 类型,或项目未激活。
409签名密钥缺失、未激活、不可用或未授权 connector 消费者。响应包含 webhook_secret_* 代码及修复建议。
500认证通过,但会话启动失败。

端点

方法 + 路径所需权限备注
GET /v1/projects/{projectId}/triggersproject.trigger.read列出触发器、运行时状态(last_fired_at, last_status, last_error, last_attempt_at)及清单解析错误。坏条目会出现在 errors[] 中,不影响其他触发器。
POST /v1/projects/{projectId}/triggersproject.trigger.create创建触发器。直接提交至清单。
PATCH /v1/projects/{projectId}/triggers/{slug}project.trigger.update部分更新,合并至当前条目。
DELETE /v1/projects/{projectId}/triggers/{slug}project.trigger.delete同时清除触发器的运行时状态。
PATCH /v1/projects/{projectId}/triggers/activationproject.trigger.update请求体 { paused: boolean }。详见“暂停与恢复”。
POST /v1/projects/{projectId}/triggers/{slug}/fireproject.trigger.fire手动触发。内置的项目成员角色持有此权限,故普通成员也可触发。
POST /v1/webhooks/projects/{projectId}/{slug}签名或令牌公开 URL,受 Webhook 密钥保护。

暂停与恢复

项目级开关可一次性停止项目内的所有触发器,与各触发器自身的 enabled 字段无关。暂停期间,调度器跳过该项目,传入的 Webhook 返回 200 并附带 { status: "skipped" }——不会触发任何会话。手动触发仍有效。当同一仓库运行在两个控制平面(例如开发和生产)时,使用此功能可避免 cron 重复触发。CLI 命令: kortix triggers pause 和 kortix triggers resume。完整的 kortix triggers 命令组见 CLI 文档。

限制与可靠性

  • 调度器约每秒轮询一次(默认 1,000 ms;可通过 KORTIX_TRIGGER_SCHEDULER_INTERVAL_MS 配置)。Cron 精度尽力达到秒级,尽管表达式包含秒字段。
  • 每个项目默认允许同时 3 个由触发器启动的会话进行供给。超出此限制的触发将返回 queued(202)而非失败,并将在其中一个会话完成供给后运行。对同时运行的会话数量无上限。
  • 手动或 Webhook 触发有 45 秒超时。加载清单有 30 秒超时。
  • Cron 触发以到期的计划时段为键,因此超时但实际落地的触发在重试时不会重复。Webhook 触发以 delivery ID 头为键,若发送方未提供 ID,则使用请求体和签名的哈希作为键。

评论 (0)