> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kazzle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 自动化

> 在 Kazzle 应用中安排任务并响应 webhook。

自动化是应用中自动运行的部分——按计划运行，或在外部服务发送事件时运行。

你在 `kazzle.config.ts` 中的 process 组件上声明它们。一个组件可以包含任意数量的触发器。

## 结构

```ts theme={"theme":"material-theme-darker"}
{
  name: 'events',
  type: 'process',
  path: './components/events/index.ts',
  processMode: 'persistent', // or 'triggered'
  triggers: [
    { name: 'daily-digest', kind: 'schedule', schedule: '0 9 * * *', path: '/cron/daily-digest' },
    { name: 'stripe',       kind: 'webhook',                          path: '/webhook/stripe' },
  ],
}
```

这里发生了两件事：

* `processMode` 选择**生命周期**——长期运行的服务器，或每个触发器运行一次。
* `triggers[]` 列出应该触发此组件的**事件**。

这两部分是独立的。持久服务器可以有 cron。临时进程可以有 webhook。选择适合工作负载的生命周期，然后附加任意数量的触发器。

## `processMode`

| 模式               | 运行内容                         | 何时使用                                       |
| ---------------- | ---------------------------- | ------------------------------------------ |
| `persistent`（默认） | 长期运行的 HTTP 服务器。触发器被 POST 到它。 | 组件已经提供 HTTP 服务，或在内存中保持状态（队列、websocket、缓存）。 |
| `triggered`      | 每个触发器生成入口脚本并退出。              | 纯后台任务——夜间清理、单个 Stripe webhook 处理器等。无空闲服务器。 |

## 触发器

每个触发器有一个 `name`（在组件内唯一）、一个 `kind`，以及——取决于模式——一个 `schedule` 和/或 `path`。

| 字段         | 何时必需                            | 说明                                  |
| ---------- | ------------------------------- | ----------------------------------- |
| `name`     | 总是                              | 用作 webhook URL 段和日志中。使用 kebab-case。 |
| `kind`     | 总是                              | `'schedule'` 或 `'webhook'`。         |
| `schedule` | 当 `kind: 'schedule'` 时          | 5 字段 cron 表达式。分钟分辨率是最小单位。           |
| `path`     | 当 `processMode: 'persistent'` 时 | 触发器到达的服务器上的 HTTP 路由。                |

## 持久模式——HTTP 到服务器

当持久组件的触发器触发时，Kazzle 在声明的 `path` 处 POST 到你的服务器。请求包含：

| 标头                                               | 说明                               |
| ------------------------------------------------ | -------------------------------- |
| `Authorization: Bearer ${KAZZLE_TRIGGER_SECRET}` | 验证这个。拒绝不匹配的调用。                   |
| `x-kazzle-trigger-name`                          | 清单中触发器的 `name`。                  |
| `x-kazzle-trigger-run-id`                        | 用于日志关联的不透明 ID。                   |
| `x-kazzle-triggered-by`                          | `cron` \| `webhook` \| `manual`。 |

对于 webhook 触发器，原始请求体作为 POST 体转发。对于计划触发器，体为空。

```ts theme={"theme":"material-theme-darker"}
// components/events/index.ts (persistent mode)
const TRIGGER_SECRET = process.env.KAZZLE_TRIGGER_SECRET ?? '';

Bun.serve({
  port: Number(process.env.PORT),
  hostname: process.env.HOST,
  async fetch(req) {
    const url = new URL(req.url);

    if (req.method === 'POST' && url.pathname === '/cron/daily-digest') {
      if (req.headers.get('authorization') !== `Bearer ${TRIGGER_SECRET}`) {
        return new Response('Unauthorized', { status: 401 });
      }
      await sendDigest();
      return Response.json({ ok: true });
    }

    if (req.method === 'POST' && url.pathname === '/webhook/stripe') {
      if (req.headers.get('authorization') !== `Bearer ${TRIGGER_SECRET}`) {
        return new Response('Unauthorized', { status: 401 });
      }
      const event = await req.json();
      await handleStripe(event);
      return Response.json({ ok: true });
    }

    return new Response('not found', { status: 404 });
  },
});
```

## 触发模式——每个触发器一次性

当 `triggered` 组件的触发器触发时，Kazzle 生成入口脚本并等待它退出。没有 `path`；脚本从环境变量了解哪个触发器触发。

| 环境变量              | 值                                |
| ----------------- | -------------------------------- |
| `TRIGGER_NAME`    | 清单中触发器的 `name`。                  |
| `TRIGGERED_BY`    | `cron` \| `webhook` \| `manual`。 |
| `RUN_ID`          | 用于日志关联的不透明 ID。                   |
| `WEBHOOK_PAYLOAD` | JSON 体（仅 webhook 触发器）。           |

```ts theme={"theme":"material-theme-darker"}
// components/events/index.ts (triggered mode)
const trigger = process.env.TRIGGER_NAME;
const runId = process.env.RUN_ID;

if (trigger === 'daily-digest') {
  await sendDigest();
} else if (trigger === 'stripe') {
  const event = JSON.parse(process.env.WEBHOOK_PAYLOAD ?? '{}');
  await handleStripe(event);
}

console.log(`run ${runId} done`);
```

触发组件在生产中没有空闲机器——它们按调用生成并在退出时关闭。

## Webhook URL

```text theme={"theme":"material-theme-darker"}
POST https://api.kazzle.app/webhooks/{spaceId}/{appId}/{componentName}/{triggerName}
```

`triggerName` 段必须与该组件 `triggers[]` 中的 `kind: 'webhook'` 条目匹配。未知的触发器名称返回 404。

## 计划分辨率

Cron 表达式是 5 字段（分钟、小时、月份日期、月份、周几），分钟分辨率是最小单位。子分钟计划在清单验证时被拒绝。

## 运行如何被记录

每个触发器触发写入一个 `process_runs` 行，包含 `trigger_name`、`triggered_by`、`run_id` 和运行的退出状态。你可以从自己的代码查询这些，或在应用的运行视图中检查它们。

## 信用不足

失败的运行被记录和记录，但计划按其正常节奏继续运行——不稳定的运行永远不会禁用触发器。停止运行的唯一因素是信用：每个触发器触发都会针对空间的余额进行检查，当空间信用不足（或未设置计费）时，运行会被跳过并返回 `402`。这是自我恢复的——计划保持启用，充值后的下一次触发正常运行，无需手动恢复。

## 稍后添加自动化

简单的应用可以不带触发器开始，稍后获得它们——添加每日摘要、连接 Stripe、运行清理。组件的生命周期（`processMode`）和触发器（`triggers[]`）是独立的，所以你可以更改它们而无需重写应用的其余部分。
