
# 工作流防护措施

代理式工作流灵活多变，但也更容易创建重复运行，或让规划器生成超出预期的任务。Queuety 在工作流层面提供轻量级防护措施，让你在保留灵活性的同时仍能掌控工作流。

有关工作进程准入和逐作业成本元数据的更全面资源模型，请参阅 [资源管理](https://queuety.inline0.dev/zh/docs/features/resource-management)。

## 为工作流定义添加版本

使用 `version()` 为工作流定义添加应用层版本标签：

```php
Queuety::workflow( 'research_run' )
    ->version( 'research.v2' )
    ->then( PlanResearchStep::class )
    ->then( DraftResearchSummaryStep::class )
    ->dispatch();
```

版本会与工作流状态一同存储，通过 `workflow_status()` 公开、由 `wp queuety workflow status` 输出，并包含在工作流导出数据中。

Queuety 还会为每次工作流运行存储确定性的 `definition_hash`。该哈希由序列化后的工作流定义生成；部署后，如果你需要准确识别正在执行的工作流形态，它会很有用。

## 让调度具备幂等性

如果调用方可能重试同一个请求，请使用 `idempotency_key()`：

```php
Queuety::workflow( 'research_run' )
    ->idempotency_key( 'brief:42:research' )
    ->then( PlanResearchStep::class )
    ->dispatch( [ 'brief_id' => 42 ] );
```

如果使用过的键再次用于调度工作流，Queuety 会返回原工作流 ID，而不是创建重复运行。键在所有工作流名称之间全局唯一，因此不同工作流可能共用同一个键时，请将工作流名称加入键中。

以下场景尤其适用：

- API 可能被客户端或网关重试的端点
- 可能收到重复投递的 webhook 消费方
- 规划器步骤应针对每个外部实体只创建一次编排运行

幂等键存储在 `queuety_workflow_dispatch_keys` 表中。

## 设置工作流预算

使用工作流预算，在运行超出预定范围时使其失败：

```php
Queuety::workflow( 'research_run' )
    ->max_transitions( 20 )
    ->max_for_each_items( 12 )
    ->max_state_bytes( 32768 )
    ->max_cost_units( 40 )
    ->max_started_workflows( 8 )
    ->then( PlanResearchStep::class )
    ->for_each( 'tasks', ExecuteResearchTask::class, 'results' )
    ->dispatch();
```

### `max_transitions()`

限制运行失败前可以完成的工作流步骤数。这有助于保护会动态分支或可能通过 `_next_step` 返回同一区域的工作流。

### `max_for_each_items()`

限制单个 `for_each()` 步骤可展开的运行时发现项目数。这适用于规划器/执行器系统，因为代理可能发现远超预期的分支任务。

### `max_state_bytes()`

限制公开工作流状态的编码大小。这有助于避免长时间运行的工作流膨胀成大型数据块，导致检查、导出或重试变慢。

### `max_cost_units()`

限制已完成步骤消耗的总成本单位。成本单位取自 Queuety 入队作业时解析的步骤或作业元数据。常规用户工作默认为 `1`，延迟和等待等编排占位步骤使用 `0`，处理器可以通过 `config()` 或作业属性设置更高的正值。只有步骤定义的 `cost_units` 可以将值设为 `0`。

如果工作流可能返回同一区域，或者 For-Each 展开的任务成本很高、无法仅靠步骤数反映，这项限制就很有用。反复调用高成本模型的规划器可能仍未超出 `max_transitions()`，但实际成本却超过了预期范围。

### `max_started_workflows()`

限制通过 `start_workflows()` 或 `start_agents()` 启动的顶层工作流数量。子工作流调度前会检查此限制，因此如果将要创建的工作流组已超出预算，父工作流会失败，而不会只创建其中一部分。

超出预算时，Queuety 会立即使工作流失败，而不会重试该步骤。有一种例外：如果 `Queuety::signal()` 完成等待时，产生的状态转换超出预算，则异常会抛给 `signal()` 的调用方，信号插入会回滚，工作流则继续等待。

## 监控防护措施

工作流状态会公开以下信息：

- `definition_version`
- `definition_hash`
- `idempotency_key`
- `budget`

`budget` 负载包含配置的限制和当前用量，其中包括当前公开状态的字节大小。

示例：

```php
$state = Queuety::workflow_status( $workflow_id );

$state->definition_version; // 'research.v2'
$state->definition_hash;    // '4c7a...'
$state->idempotency_key;    // 'brief:42:research'
$state->budget;             // [ 'max_transitions' => 20, ... ]
```
