# AI 审批编排 — 状态机

> **版本**: v1.0-draft
> **最后更新**: 2026-05-19
> **维护者**: 后端团队
> **关联**: [06-data-model.md](./06-data-model.md) §`ai_recommendation_logs.status` / [07-api.md](./07-api.md) §11-13

---

## 📋 概述

本模块的状态机集中在 **AiRecommendation 单条推荐**的生命周期。

ApprovalPolicy / RiskTierRegistry / CalibrationMetricsSnapshot 是配置 / 物化数据，**无状态机**。

approval-engine 自身的 NodeType / ApprovalTaskAction 不变（详 §与 approval-engine 的衔接）。

---

## 🔄 AiRecommendation 状态机

### 状态定义

| 状态 | 含义 | 终态 |
|---|---|---|
| `PENDING` | Workflow 已创建，等待 LLM 调用 + 解析 | ❌ |
| `READY` | LLM 返回，等待审批人决策 | ❌ |
| `ADOPTED` | 审批人采纳 AI 推荐 | ✅ |
| `OVERRIDDEN` | 审批人推翻（modification_diff 必填） | ✅ |
| `IGNORED` | 审批人忽略（自决，不基于 AI） | ✅ |
| `FAILED` | LLM 调用 / parse / budget / PII / 跨境 任一失败 | ✅ |

### 状态转换图

```
                                  +-------------+
                                  |   (start)   |
                                  +------+------+
                                         |
                       approval-engine task → PENDING
                                         |
                                         v
                                  +------+------+
                       +----------+   PENDING   +-----------+
                       |          +------+------+           |
                       |                 |                  |
            LlmInvoke timeout/parse/    |          IdempotencyHit
            budget/pii/compliance     LLM OK     (复用旧 AiRecommendationLog)
                       |                 |                  |
                       v                 v                  |
                +------+------+   +------+------+           |
                |   FAILED    |   |    READY    |<----------+
                +-------------+   +------+------+
                  (终态)           /     |     \
                                  /      |      \
                       POST adopt POST override POST ignore
                              /          |          \
                             v           v           v
                       +-----+----+ +----+-------+ +-+--------+
                       | ADOPTED  | | OVERRIDDEN | | IGNORED  |
                       +----------+ +------------+ +----------+
                          (终态)       (终态)         (终态)
```

### 触发与守卫

| 转换 | 触发 | 守卫（必满足） | 副作用 |
|---|---|---|---|
| `(start) → PENDING` | approval-engine task 进入 `PENDING` 状态 + ApprovalPolicy 命中 `ai_mode ≠ OFF` + 月度 budget 未超 | Temporal Workflow `GenerateAiRecommendation` 启动 | INSERT AiRecommendationLog (status=PENDING) + 启动 Workflow |
| `PENDING → READY` | LlmInvoke Activity 成功 + ParseOutput 成功 | `recommended_action` / `agent_confidence` / `reasoning_chain` / `effective_ux_mode` 全部非空 | UPDATE status=READY；写 `AuditLog.action='AI_RECOMMENDED'`；push 通知审批人 |
| `PENDING → FAILED (LLM_TIMEOUT)` | LlmInvoke 超时 30s + 重试 1 次仍失败 | - | UPDATE status=FAILED + failure_reason='LLM_TIMEOUT'；写 audit；不阻塞审批 |
| `PENDING → FAILED (PARSE_ERROR)` | ParseOutput 失败（LLM 输出非合法 JSON / 缺字段） | - | 同上 reason='PARSE_ERROR' |
| `PENDING → FAILED (BUDGET_EXCEEDED)` | ApprovalPolicy.monthly_ai_call_budget_usd 累计已超 90% | - | 同上 reason='BUDGET_EXCEEDED' + Policy 自动降级 `ai_mode=OFF` + 写 ApprovalPolicyHistory `change_type=AUTO_BUDGET_DOWNGRADE` + 告警 AI Ops |
| `PENDING → FAILED (MODEL_ROUTER_EXHAUSTED)` | ModelRouter 选不到可用模型 | - | reason='MODEL_ROUTER_EXHAUSTED' |
| `PENDING → FAILED (COMPLIANCE_BLOCKED)` | 跨境合规拒绝（region routing 找不到合法路径） | - | reason='COMPLIANCE_BLOCKED' |
| `PENDING → FAILED (PII_SAFEGUARD_VIOLATION)` | PiiRedact 识别到疑似 PII 字段但未在 `FormVersion._ai.sensitive_fields` 声明 | - | reason='PII_SAFEGUARD_VIOLATION' + 告警 AI Ops 抽查 sensitive_fields 清单 |
| `PENDING → READY (idempotency)` | IdempotencyCheck 命中 `(approval_task_id, turn_id, prompt_hash)` 2 min 窗口 | 旧 AiRecommendationLog.status='READY' | 复用旧记录，**不新建**；跳过 LLM 调用；skip audit `AI_RECOMMENDED`（已记） |
| `READY → ADOPTED` | `POST /recommendations/:id/adopt` | • status=READY<br>• Tier 3 / HARD_GATE：`readingDurationMs >= max(5s, min(15s, 字数/8))` + `checkboxesAcknowledged` 覆盖全部 `risk_indicators[].name`<br>• 用户具 `ai_approval:adopt` 权限 | UPDATE status=ADOPTED + adopted_at/by + reading_duration + scroll_distance；写 `AuditLog.action='AI_ADOPTED'`；approval-engine task 走"按 recommended_action 执行"路径 |
| `READY → OVERRIDDEN` | `POST /recommendations/:id/override` | • status=READY<br>• `modificationDiff` 非空<br>• Tier 2/3：`modificationReason.length >= 20`<br>• Tier 3 / HARD_GATE：阅读倒计时同上<br>• 用户具 `ai_approval:override` 权限 | UPDATE status=OVERRIDDEN + override 字段；写 `AuditLog.action='AI_OVERRIDDEN'` + modification_diff + modified_action；approval-engine 按 `modificationDiff.to.action` 执行 |
| `READY → IGNORED` | `POST /recommendations/:id/ignore` | • status=READY<br>• 用户具 `ai_approval:adopt` 权限 | UPDATE status=IGNORED；写 `AuditLog.action='AI_UNAVAILABLE'`（与 system failure 同 action 但区别在 `ai_unavailable_reason='USER_IGNORED'`） |

### 不允许的转换

| ❌ 转换 | 原因 |
|---|---|
| `FAILED → 任何` | FAILED 是终态；重试 = 新一条 AiRecommendationLog（新 idempotency_key） |
| `ADOPTED / OVERRIDDEN / IGNORED → 任何` | 4 个终态不可逆 |
| `READY → READY` | 不更新现有；如需重新生成 → 新一条 |
| `PENDING → ADOPTED / OVERRIDDEN / IGNORED` | 必须先达 READY；PENDING 时审批人 UI 不显示采纳/推翻按钮 |

### 状态读写权限矩阵

| 状态变更 | 写者 | RBAC |
|---|---|---|
| `(start) → PENDING` | 系统（Temporal Workflow） | - |
| `PENDING → READY` | 系统（Temporal Workflow） | - |
| `PENDING → FAILED` | 系统（Temporal Workflow） | - |
| `READY → ADOPTED` | 审批人 | `ai_approval:adopt` |
| `READY → OVERRIDDEN` | 审批人 | `ai_approval:override` |
| `READY → IGNORED` | 审批人 | `ai_approval:adopt` |

---

## ⏱️ 状态时间约束

| 约束 | 阈值 |
|---|---|
| PENDING 最长存活 | 60s；超时 → FAILED reason='LLM_TIMEOUT' |
| READY → 终态 决策时间 | 不强制；但 audit `time_to_decision` 入 CalibrationMetricsSnapshot |
| READY 在审批 task 关闭后 | 走 → IGNORED（系统自动；审批人未来得及决策即 task 已合并） |
| Idempotency 窗口 | 2 min（详 06-data-model `idempotency_key`） |

---

## 🔗 跨模块状态关联

### 与 approval-engine

approval-engine **审批 task 状态** 跟本模块 **AiRecommendation 状态** 是**两条独立轨道**：

| approval-engine ApprovalTask 状态 | 本模块 AiRecommendation 状态 |
|---|---|
| PENDING | (start) → PENDING → READY |
| APPROVED | ADOPTED（若按 AI）/ OVERRIDDEN（若推翻）/ IGNORED（若 IGNORE） |
| REJECTED | 同上 |
| RETURNED | OVERRIDDEN（推翻 + newAction=RETURN）/ IGNORED |
| WITHDRAWN | IGNORED（审批人无机会决策）/ READY 残留（task 关闭时清扫） |

**关键不变量**：审批人对审批 task 的最终决策（APPROVE / REJECT / RETURN）**永远是 approval-engine 的真理**；本模块 AiRecommendation 仅记录"AI 推荐是什么、审批人是否采纳"。

### approval-engine 的 AI_STAGE / ANALYSIS_FAILED 留位（阶段 2 out-of-scope）

approval-engine `NodeType` enum 当前 8 值（START / END / USER_TASK / SERVICE_TASK / EXCLUSIVE_GATEWAY / PARALLEL_GATEWAY / INCLUSIVE_GATEWAY / SUB_PROCESS）。

**阶段 2 计划新增**：

```typescript
enum NodeType {
  // ... 现有 8 值
  AI_STAGE = 'AI_STAGE',          // ⚠️ 阶段 2 新增；本模块 v1.0 不实施
}
```

approval-engine `ApprovalTaskAction` enum 当前 24 值。

**阶段 2 计划新增**：

```typescript
enum ApprovalTaskAction {
  // ... 现有 24 值
  ANALYSIS_FAILED = 'ANALYSIS_FAILED',   // ⚠️ 阶段 2 新增；本模块 v1.0 不实施
}
```

**为什么留位**：阶段 1 的 L1 辅助审批仅在审批人侧栏给推荐，不进入审批流程图节点；阶段 2 的 L2 才会在流程定义里加 `AI_STAGE` 节点 + `ANALYSIS_FAILED` task action。**本模块 v1.0 不动 approval-engine 状态机**——standards/16 跨模块约束。

---

## 🧱 守卫实现要点

### 反 rubber-stamp 倒计时（Tier 3 + HARD_GATE）

服务端校验 `POST /adopt` / `/override`：

```typescript
const requiredReadingMs = Math.max(5000, Math.min(15000, Math.ceil(reasoningChain.length / 8 * 1000)));
if (req.readingDurationMs < requiredReadingMs) {
  throw new BadRequest('READING_TIME_TOO_SHORT', { required: requiredReadingMs, actual: req.readingDurationMs });
}
```

客户端 UX 同步 disabled 按钮倒计时（双层防御，详 01-prd §7.3 + 05-ui-spec）。

### checkbox 全勾校验

```typescript
const requiredCheckboxes = recommendation.riskIndicators.map(r => r.name);
const missing = requiredCheckboxes.filter(n => !req.checkboxesAcknowledged.includes(n));
if (missing.length > 0) {
  throw new BadRequest('INSUFFICIENT_ACKNOWLEDGEMENT', { missing });
}
```

### Idempotency 兑现

```typescript
const idempotencyKey = sha256(`${approvalTaskId}:${turnId}:${promptHash}`);
const existing = await prisma.aiRecommendationLog.findFirst({
  where: { idempotencyKey, createdAt: { gte: subMinutes(new Date(), 2) } }
});
if (existing) return existing;  // 跳过 LLM 调用
```

### Workflow 自动 IGNORED 清扫

approval-engine task 终态后 5 分钟 cron：
- 扫所有 status=READY 的 AiRecommendationLog
- 对应 approval_task 已是终态 → UPDATE status=IGNORED + 写 audit `ai_unavailable_reason='task_closed_before_decision'`

---

## 🔗 关联

- [01-prd.md](./01-prd.md) §5.4 推荐生成器
- [06-data-model.md](./06-data-model.md) `ai_recommendation_logs.status` 字段
- [07-api.md](./07-api.md) §11-13 状态变更端点 + §推荐生成器异步触发
- [08-error-codes.md](./08-error-codes.md) 守卫错误码
- approval-engine [`04-state-machine.md`](../approval-engine/04-state-machine.md) NodeType / ApprovalTaskAction（阶段 2 留位）
- audit-system [`06-data-model.md`](../audit-system/06-data-model.md) AuditAction 4 新值
