# AI 审批编排 — API 文档

> **版本**: v1.0-draft
> **最后更新**: 2026-05-19
> **维护者**: 后端团队
> **关联**: [01-prd.md](./01-prd.md) / [06-data-model.md](./06-data-model.md) / [04-state-machine.md](./04-state-machine.md)
> **认证**: 所有 endpoint 强制 JWT；权限按 §权限矩阵

---

## 📋 概述

### API 基址
- 基址：`/api/v1`
- 资源前缀：`/api/v1/ai-orchestration/*`

### 命名约定
- REST 风格
- 路径用 kebab-case
- 字段用 camelCase（响应 / 请求）
- 时间用 ISO 8601 带时区

### 错误码
统一格式（详 08-error-codes）：
```json
{ "code": "POLICY_NOT_FOUND", "message": "...", "details": {} }
```

### 权限点

| RBAC 点 | 说明 |
|---|---|
| `ai_approval:read_recommendation` | 读 AI 推荐 |
| `ai_approval:adopt` | 采纳 / 修改后采纳 / 忽略 |
| `ai_approval:override` | 推翻（modification_diff 必填） |
| `risk_tier:configure` | RiskTierRegistry 改阈值 |
| `policy_as_code:write` | ApprovalPolicy CRUD |
| `policy_as_code:read` | ApprovalPolicy 读（list / 单条） |
| `policy_as_code:read_history` | ApprovalPolicyHistory 读 |
| `calibration_dashboard:view_own` | 校准 Dashboard（自己模板） |
| `calibration_dashboard:view_all` | 校准 Dashboard（全量） |

---

## 🔢 API 清单（14 个端点）

| # | Method | Path | 用途 | 权限 |
|---|---|---|---|---|
| 1 | GET | `/api/v1/ai-orchestration/policies` | 列 ApprovalPolicy | `policy_as_code:read` |
| 2 | GET | `/api/v1/ai-orchestration/policies/:id` | 取单条 ApprovalPolicy | 同上 |
| 3 | POST | `/api/v1/ai-orchestration/policies` | 新建 ApprovalPolicy | `policy_as_code:write` |
| 4 | PUT | `/api/v1/ai-orchestration/policies/:id` | 更新 ApprovalPolicy（事务式） | 同上 |
| 5 | DELETE | `/api/v1/ai-orchestration/policies/:id` | 软删 ApprovalPolicy | 同上 |
| 6 | GET | `/api/v1/ai-orchestration/policies/:id/history` | 读 ApprovalPolicyHistory | `policy_as_code:read_history` |
| 7 | GET | `/api/v1/ai-orchestration/risk-tier-registry` | 读 Risk Tier 3 行 | `ai_approval:read_recommendation`（开放给所有 AI 相关角色） |
| 8 | PUT | `/api/v1/ai-orchestration/risk-tier-registry/:tierId` | 改 Risk Tier 阈值（仅改阈值，不增 tier） | `risk_tier:configure` |
| 9 | GET | `/api/v1/ai-orchestration/recommendations/by-task/:taskId` | 读 AiRecommendationLog by approval_task | `ai_approval:read_recommendation` |
| 10 | GET | `/api/v1/ai-orchestration/recommendations/:id` | 读单条推荐详情 | 同上 |
| 11 | POST | `/api/v1/ai-orchestration/recommendations/:id/adopt` | 采纳 AI 推荐 | `ai_approval:adopt` |
| 12 | POST | `/api/v1/ai-orchestration/recommendations/:id/override` | 推翻 AI 推荐（modification_diff 必填） | `ai_approval:override` |
| 13 | POST | `/api/v1/ai-orchestration/recommendations/:id/ignore` | 忽略 AI 推荐（不采纳不推翻，自决） | `ai_approval:adopt` |
| 14 | GET | `/api/v1/ai-orchestration/calibration` | 校准 Dashboard 指标查询 | `calibration_dashboard:view_*` |

**推荐生成内部触发**：审批 task 状态变更触发 Temporal Activity（非 REST 端点；详 §推荐生成器异步触发）。

---

## API 详情

### 1. `GET /api/v1/ai-orchestration/policies`

列 ApprovalPolicy（支持筛选 + 分页）。

**Query Params**:

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `formTemplateKey` | string | ❌ | 按 form_template 筛选 |
| `organizationId` | UUID | ❌ | 按组织筛选；`__GLOBAL__` 表示兜底全局 |
| `isActive` | boolean | ❌ | 默认 `true`（仅 active）；显式传 `false` 看历史 |
| `aiMode` | enum | ❌ | 按 ai_mode 筛 |
| `riskTierId` | int | ❌ | 按 tier 筛 |
| `page` | int | ❌ | 默认 1 |
| `pageSize` | int | ❌ | 默认 20；max 100 |

**Response 200**:

```json
{
  "items": [
    {
      "id": "uuid",
      "formTemplateKey": "expense",
      "organizationId": "__GLOBAL__",
      "riskTierId": 1,
      "aiMode": "RECOMMEND",
      "confidenceThreshold": 0.70,
      "monthlyAiCallBudgetUsd": 1000,
      "crossOrgVisibility": "NONE",
      "reviewerSet": null,
      "effectiveFrom": "2026-05-19T13:35:50+08:00",
      "effectiveTo": null,
      "isActive": true,
      "createdById": "uuid",
      "createdAt": "2026-05-19T13:35:50+08:00",
      "updatedAt": "2026-05-19T13:35:50+08:00"
    }
  ],
  "total": 50,
  "page": 1,
  "pageSize": 20
}
```

**错误**:
- `403 FORBIDDEN`：缺 `policy_as_code:read` 权限

---

### 2. `GET /api/v1/ai-orchestration/policies/:id`

读单条 ApprovalPolicy。

**Response 200**: 同上单 item 形态  
**错误**: `404 POLICY_NOT_FOUND`

---

### 3. `POST /api/v1/ai-orchestration/policies`

新建 ApprovalPolicy。事务内同步写 `ApprovalPolicyHistory(change_type='CREATE')`。

**Request Body**:

```json
{
  "formTemplateKey": "expense",
  "organizationId": "uuid",
  "riskTierId": 1,
  "aiMode": "RECOMMEND",
  "confidenceThreshold": 0.70,
  "monthlyAiCallBudgetUsd": 1000,
  "crossOrgVisibility": "NONE",
  "reviewerSet": null,
  "effectiveFrom": "2026-05-20T00:00:00+08:00",
  "changeReason": "新增 expense 模板 AI 推荐策略"
}
```

**校验**:
- `formTemplateKey` 必须在 form-engine FormDefinition 表存在
- `organizationId` 必须在 Organization 表存在（或 `__GLOBAL__`）
- `riskTierId IN (1, 2, 3)`
- partial unique 违反 → `409 POLICY_DUPLICATE_ACTIVE`

**Response 201**:

```json
{ "id": "uuid", "createdAt": "...", "policy": { ... 完整字段 } }
```

**错误**:
- `400 VALIDATION_ERROR`
- `403 FORBIDDEN` (`policy_as_code:write`)
- `409 POLICY_DUPLICATE_ACTIVE`（同 form_template + org 已有 active 记录）

---

### 4. `PUT /api/v1/ai-orchestration/policies/:id`

更新 ApprovalPolicy。**事务式 5 步**（详 06-data-model §表 1 策略变更流程）：
1. 读旧 active 记录
2. 旧记录 `is_active=false` + `effective_to=now()`
3. INSERT 新 active 记录（继承旧记录字段 + apply request）
4. INSERT `ApprovalPolicyHistory(change_type='UPDATE', before, after)`
5. COMMIT

**Request Body**: 同 POST，但 `formTemplateKey` / `organizationId` 不能改（改 = 新建）。

**Response 200**: 新记录的 `id`（不同于旧 id） + 完整字段。

**错误**:
- `404 POLICY_NOT_FOUND`
- `409 POLICY_INACTIVE`（试图改已 inactive 的记录）
- `429 POLICY_CHANGE_RATE_LIMIT`（24h 内同 `(form_template, org)` 变更 ≥ 3 次 → 告警 + 限流）

---

### 5. `DELETE /api/v1/ai-orchestration/policies/:id`

软删（`is_active=false` + `effective_to=now()` + history `change_type='DEACTIVATE'`）。**不物理删**。

**Response 204**

---

### 6. `GET /api/v1/ai-orchestration/policies/:id/history`

读策略变更 history。

**Query Params**: `page` / `pageSize`

**Response 200**:

```json
{
  "items": [
    {
      "id": "uuid",
      "policyId": "uuid",
      "revisionNumber": 3,
      "beforeSnapshot": { ... 完整旧字段 },
      "afterSnapshot": { ... 完整新字段 },
      "changeType": "UPDATE",
      "changeReason": "调高 expense 预算到 2000",
      "changedById": "uuid",
      "changedAt": "2026-05-19T13:35:50+08:00"
    }
  ],
  "total": 5,
  "page": 1,
  "pageSize": 20
}
```

---

### 7. `GET /api/v1/ai-orchestration/risk-tier-registry`

读 Risk Tier 全 3 行。**所有 AI 角色可读**（不限 IT/AI Ops）。

**Response 200**:

```json
{
  "tiers": [
    {
      "tierId": 1,
      "name": "低风险",
      "irreversibilityMax": 3,
      "blastRadiusMax": 3,
      "complianceMax": 3,
      "confidenceRequiredMin": 0.65,
      "defaultUiMode": "RECOMMEND"
    },
    { "tierId": 2, ... },
    { "tierId": 3, ... }
  ]
}
```

---

### 8. `PUT /api/v1/ai-orchestration/risk-tier-registry/:tierId`

改单 tier 阈值。**仅改阈值，不增不减 tier**（CHECK `tier_id IN (1,2,3)`）。

**Request Body**:

```json
{
  "irreversibilityMax": 3,
  "blastRadiusMax": 4,
  "complianceMax": 3,
  "confidenceRequiredMin": 0.68,
  "defaultUiMode": "RECOMMEND",
  "changeReason": "降低中风险阈值以覆盖更多 purchase 单据"
}
```

**Response 200**: 完整字段。

**注**：本端点变更应触发**全局重新评估 ApprovalPolicy.tier**（异步 cron 跑一次 review job），但 v1.0 不自动重评 → 仅 audit + 通知 AI Ops。

---

### 9. `GET /api/v1/ai-orchestration/recommendations/by-task/:taskId`

按审批 task 查所有推荐（通常 1 条；重试场景可能多条）。

**Response 200**:

```json
{
  "taskId": "uuid",
  "recommendations": [
    {
      "id": "uuid",
      "approvalTaskId": "uuid",
      "formTemplateKey": "expense",
      "policyId": "uuid",
      "riskTierId": 1,
      "status": "READY",
      "recommendedAction": "APPROVE",
      "agentConfidence": 0.85,
      "calibratedConfidence": null,
      "effectiveUxMode": "RECOMMEND",
      "reasoningChain": "本次报销金额 ¥850 在历史报销均值 ±20% 内（历史样本 8 条）...",
      "riskIndicators": [
        { "name": "金额合理", "severity": "low", "snippet": "850 在 700-1000 区间" }
      ],
      "historicalSimilarTaskIds": ["uuid", "uuid", "uuid"],
      "totalCostUsd": 0.0234,
      "totalDurationMs": 1450,
      "createdAt": "2026-05-19T13:35:50+08:00",
      "adoptedAt": null,
      "adoptedById": null,
      "overriddenAt": null,
      "overriddenById": null,
      "modificationDiff": null,
      "modificationReason": null
    }
  ]
}
```

**注**：`reasoningChain` 已脱敏（PII redact）；前端 hover/滚动行为采集字段 `readingDurationMs / scrollDistancePx` 不在 GET 响应（避免误用）。

---

### 10. `GET /api/v1/ai-orchestration/recommendations/:id`

读单条推荐详情。同上单 item 形态。

---

### 11. `POST /api/v1/ai-orchestration/recommendations/:id/adopt`

审批人采纳 AI 推荐。

**Request Body**:

```json
{
  "readingDurationMs": 8500,
  "scrollDistancePx": 320,
  "checkboxesAcknowledged": ["risk_amount_low", "risk_history_consistent"]
}
```

**业务规则**:
- `effective_ux_mode = RECOMMEND_WITH_HARD_GATE`：`checkboxesAcknowledged` 必须含**所有** `risk_indicators[]` 名（少一 → 400）
- `effective_ux_mode = RECOMMEND_WITH_HARD_GATE`：`readingDurationMs >= max(5000, min(15000, 字数/8 * 1000))`（少 → 400）
- `effective_ux_mode = OFF / SUGGEST_ONLY`：拒绝（404）—— 这些状态不应有"采纳"动作
- `status` 必须是 `READY`（否则 409）
- 写 `AuditLog.action='AI_ADOPTED'`

**Response 200**:

```json
{ "recommendationId": "uuid", "newStatus": "ADOPTED", "adoptedAt": "...", "auditLogId": "uuid" }
```

**错误**:
- `400 INSUFFICIENT_ACKNOWLEDGEMENT`：checkbox 未全勾选
- `400 READING_TIME_TOO_SHORT`：未达倒计时
- `403 FORBIDDEN`
- `404 NOT_FOUND`
- `409 INVALID_STATUS`（status ≠ READY）

---

### 12. `POST /api/v1/ai-orchestration/recommendations/:id/override`

审批人推翻 AI 推荐（人工决策）。

**Request Body**:

```json
{
  "newAction": "REJECT",
  "modificationDiff": {
    "from": { "action": "APPROVE", "comment": "AI: 金额合理可批" },
    "to": { "action": "REJECT", "comment": "供应商资质未审" }
  },
  "modificationReason": "供应商首次合作未通过资质审核",
  "readingDurationMs": 12500,
  "scrollDistancePx": 580
}
```

**业务规则**:
- Tier 2/3：`modificationReason.length >= 20`（否则 400）
- Tier 3 + `effective_ux_mode = RECOMMEND_WITH_HARD_GATE`：`readingDurationMs` 必须满足倒计时
- 写 `AuditLog.action='AI_OVERRIDDEN'` + `modification_diff` + `modified_action`

**Response 200**:

```json
{ "recommendationId": "uuid", "newStatus": "OVERRIDDEN", "overriddenAt": "...", "auditLogId": "uuid" }
```

**错误**:
- `400 MODIFICATION_REASON_TOO_SHORT`
- `400 READING_TIME_TOO_SHORT`
- `403 / 404 / 409`（同上）

---

### 13. `POST /api/v1/ai-orchestration/recommendations/:id/ignore`

审批人忽略 AI 推荐（不基于 AI 推荐做决策，自决）。

**Request Body**:

```json
{
  "readingDurationMs": 3000,
  "scrollDistancePx": 0
}
```

**业务规则**:
- 任何 tier 都可调用
- 写 `AuditLog.action='AI_UNAVAILABLE'`（按"AI 推荐未被采用"语义记录；区别于 system failure）

**Response 200**: 同 11/12

---

### 14. `GET /api/v1/ai-orchestration/calibration`

校准 Dashboard 指标查询。

**Query Params**:

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `formTemplateKey` | string | ❌ | 按模板筛 |
| `organizationId` | UUID | ❌ | 按组织筛；权限 `view_own` 时强制 = createdBy 的模板 |
| `riskTierId` | int | ❌ | 按 tier 筛 |
| `timeWindow` | enum | ✅ | `1h / 24h / 7d / 30d` |
| `metric` | enum | ❌ | `all / modification_rate / time_to_decision / ai_unavailable_count / confidence_distribution / cost`；默认 all |

**Response 200**:

```json
{
  "timeWindow": "7d",
  "hourBuckets": [
    {
      "hourBucket": "2026-05-19T00:00:00+08:00",
      "formTemplateKey": "expense",
      "organizationId": "uuid",
      "riskTierId": 1,
      "totalRecommendations": 245,
      "adoptedCount": 198,
      "overriddenCount": 32,
      "ignoredCount": 15,
      "failedCount": 0,
      "modificationRate": 0.139,
      "timeToDecisionP50Ms": 8200,
      "timeToDecisionP99Ms": 24500,
      "confidenceBuckets": {
        "low": 2, "midLow": 18, "midHigh": 142, "high": 83
      },
      "totalCostUsd": 5.73,
      "lastRefreshedAt": "2026-05-19T13:35:50+08:00"
    }
  ],
  "summary": {
    "modificationRate": 0.142,
    "ai_unavailable_count": 3,
    "totalCostUsd": 38.45,
    "monthlyBudgetUsd": 1000,
    "budgetUsagePct": 3.8
  }
}
```

**权限分支**:
- `calibration_dashboard:view_all` → 不限 `organizationId`
- `calibration_dashboard:view_own` → 强制 `organizationId IN (用户管理的 organization_id 集合)` + `formTemplateKey IN (SELECT key FROM FormTemplate WHERE createdById = userId)`

**错误**:
- `400 TIME_WINDOW_REQUIRED`
- `403 FORBIDDEN`

---

## 推荐生成器异步触发（非 REST 端点）

**触发**：approval-engine 审批 task 状态变更（`PENDING` 进入）→ Temporal Workflow `GenerateAiRecommendation`。

**Workflow 步骤**（详 04-state-machine）:

```
GenerateAiRecommendation(approvalTaskId, organizationId, formTemplateKey)
  ├─ Activity 1: ResolvePolicy → 命中 ApprovalPolicy（fail-safe ai_mode=OFF）
  ├─ Activity 2: IdempotencyCheck → 2 min 窗口内同 prompt_hash 返回旧结果
  ├─ Activity 3: PiiRedact → 按 FormVersion._ai.sensitive_fields redact
  ├─ Activity 4: FetchSimilar → 调 knowledge-base RAG (cross_org_visibility 控制)
  ├─ Activity 5: ModelRouterSelect → 按 tier + region routing 选模型
  ├─ Activity 6: LlmInvoke → 调 LLM (timeout 30s; retry 1)
  ├─ Activity 7: ParseOutput → 解析 recommended_action / confidence / reasoning_chain / risk_indicators
  ├─ Activity 8: ComputeEffectiveUxMode → 按降级表（01-prd §5.4.2 step 7）
  ├─ Activity 9: PersistLog → 写 AiRecommendationLog (status=READY/FAILED)
  └─ Activity 10: Notify → push 通知给审批人（侧栏数据就绪）
```

**失败重试**：Activity 6 LlmInvoke 重试 1 次；Activity 4/5/6/7 任一 fail → 落 status=FAILED + failure_reason；不阻塞审批主流程。

**幂等保证**：Activity 1 计算 prompt_hash + idempotency_key；Activity 2 命中 → 直接返回旧 AiRecommendationLog.id，跳过 LLM 调用。

---

## 跨模块 API 影响（非本模块实施）

| 模块 | 改动 | 说明 |
|---|---|---|
| **approval-center** | `GET /api/v1/approval-center/inbox/:taskId` 响应加 `aiRecommendation` 字段 | 内容形态 = §9 response.recommendations[0]；前端侧栏消费 |
| **approval-center** | inbox 列表响应每条加 `aiHandledFlag: 'recommended' / 'adopted' / 'overridden' / 'ignored' / null` | UI 列表"AI 已审"标识 |

详 approval-center [`07-api.md`](../approval-center/07-api.md) 扩展。

---

## 性能基准

| 端点 | p99 目标 |
|---|---|
| GET /policies | < 100ms |
| POST /policies | < 200ms（含 history 写） |
| PUT /policies/:id | < 300ms（5 步事务） |
| GET /recommendations/by-task/:taskId | < 100ms |
| POST /recommendations/:id/adopt|override|ignore | < 200ms |
| GET /calibration | < 2s（走物化表） |
| 推荐生成 Workflow 端到端 | < 3s（含 LLM 调用） |

---

## ❗️待确认项

- [ ] `POST /override` 时是否同步通知审批人 supervisor（高 modification rate 触发 review）？建议 v1.0 仅 audit，v1.1 加通知
- [ ] `effective_ux_mode = RECOMMEND_WITH_HARD_GATE` 时倒计时是 server 端校验 readingDurationMs 还是 client 端 disabled 按钮兜底？**推荐**：双层（client UX + server enforcement）
- [ ] 推荐生成 Workflow 是否复用 approval-engine 现有 Temporal worker pool 还是独立 worker？**推荐**：独立 worker（避免 LLM 长任务挤占审批主流程）

---

## 🔗 关联

- [01-prd.md](./01-prd.md)
- [06-data-model.md](./06-data-model.md)
- [04-state-machine.md](./04-state-machine.md)
- [08-error-codes.md](./08-error-codes.md)
- approval-engine [`07-api.md`](../approval-engine/07-api.md)
- approval-center [`07-api.md`](../approval-center/07-api.md)
- audit-system [`07-api.md`](../audit-system/07-api.md)
- standards [`21-agent-business-module-integration.md`](../../standards/21-agent-business-module-integration.md)
