# AI Runtime 接入文档

面向 Runtime 开发者和 iOS/Admin 前端开发者的 API 参考。

---

## 1. 错误码枚举

### 1.1 Internal API 错误码（Runtime ↔ API）

| 错误码 | HTTP | 说明 | retryable |
|--------|------|------|:---------:|
| `JOB_NOT_FOUND` | 404 | Job 不存在 | false |
| `JOB_NOT_ACTIVE` | 409 | Job 状态不是 locked/running | false |
| `JOB_ALREADY_LOCKED` | 409 | 已被其他 Runtime 锁定 | true |
| `SNAPSHOT_NOT_FOUND` | 404 | 快照不存在（已废弃：自动重建） | false |
| `CREDENTIAL_NOT_FOUND` | 404 | 凭证不存在或已删除 | false |
| `CREDENTIAL_REQUIRED` | 400 | user_deepseek_key 模式缺少 credentialId | false |
| `RESULT_ALREADY_EXISTS` | 409 | 重复提交（幂等 key 冲突） | false |
| `RESULT_SCHEMA_UNSUPPORTED` | 422 | schema 版本不支持 | false |
| `INTERNAL_ERROR` | 500 | 服务器内部错误 | true |

### 1.2 User API 错误码（iOS / Admin → API）

| 错误码 | HTTP | 说明 |
|--------|------|------|
| `AI_ANALYSIS_DISABLED` | 400 | 用户未开启 AI 分析 |
| `INVALID_JOB_TYPE` | 400 | jobType 不在支持列表中 |
| `INVALID_TARGET_TYPE` | 400 | quiz/flashcard 必须 targetType=knowledge_base |
| `CREDENTIAL_REQUIRED` | 400 | user_deepseek_key 模式需要绑定凭证 |
| `CREDENTIAL_NOT_FOUND` | 404 | 凭证不存在或无权访问 |
| `DAILY_JOB_LIMIT_EXCEEDED` | 400 | 超过每日 AI Job 数量上限 |
| `DAILY_TOKEN_BUDGET_EXCEEDED` | 400 | 超过每日 Token 预算上限 |
| `PLATFORM_CIRCUIT_OPEN` | 503 | 平台熔断器开启，拒绝新请求 |
| `PLATFORM_CIRCUIT_HALF_OPEN` | 503 | 熔断器半开，仅允许少量请求 |
| `PLATFORM_TOKEN_BUDGET_EXCEEDED` | 400 | 平台日 Token 预算耗尽 |
| `PLATFORM_COST_BUDGET_EXCEEDED` | 400 | 平台日成本预算耗尽 |

### 1.3 Runtime 上报的错误码

Runtime 通过 `POST /internal/runtime/jobs/{jobId}/fail` 上报的错误码：

| 错误码 | 说明 | retryable |
|--------|------|:---------:|
| `JOB_CANCELLED` | 用户取消 Job | false |
| `JOB_TIMEOUT` | Job 执行超时（默认 120s） | false |
| `MODEL_TIMEOUT` | DeepSeek API 超时/503/502/500 | true |
| `MODEL_RATE_LIMIT` | DeepSeek 429 限流 | true |
| `MODEL_NETWORK_ERROR` | 网络连接失败 | true |
| `SNAPSHOT_FETCH_FAILED` | 快照拉取失败 | true |
| `INVALID_SNAPSHOT` | 快照版本不兼容 | false |
| `CREDENTIAL_RESOLVE_FAILED` | 凭证解析失败 | false |
| `UNSUPPORTED_JOB_TYPE` | jobType 不支持 | false |
| `MODEL_RESPONSE_INVALID` | 模型返回无效 JSON | false |
| `MODEL_JSON_PARSE_FAILED` | 模型输出 JSON 解析失败 | false |

---

## 2. 限流说明

### 2.1 用户级限流

通过 `UserAiSettings` 控制，默认值：

| 参数 | 默认值 | 说明 |
|------|--------|------|
| `maxDailyAiJobs` | 20 | 每日 AI Job 创建上限 |
| `maxDailyTokenBudget` | 100,000 | 每日 Token 消耗上限 |

超出限制返回 `DAILY_JOB_LIMIT_EXCEEDED` 或 `DAILY_TOKEN_BUDGET_EXCEEDED`。

限流粒度：`(userId, localDate, apiKeyMode)`，platform_key 和 user_deepseek_key 独立计数。

### 2.2 平台级熔断

平台密钥（platform_key）模式启用的保护机制：

```
├── 日 Token 预算：10,000,000 tokens
├── 日成本预算：$500 (50000 cents)
├── 熔断阈值：连续 10 次失败 → open
├── 半开限制：最多 2 个活跃 platform_key Job
└── 恢复：单次成功 → closed（重置失败计数）
```

熔断状态流转：
```
closed ──[10次连续失败]──> open ──[手动半开]──> half_open ──[成功]──> closed
                         │                                    │
                         └───[超限]───<───────────────[超限]───┘
```

### 2.3 API 全局限流

通过 NestJS ThrottlerModule 控制，默认限制：

| 端点 | 限制 | 窗口 |
|------|------|------|
| `/ai/*` 用户端点 | 60 次/分钟 | 60s |
| `/internal/runtime/*` | 300 次/分钟 | 60s |

超限返回 HTTP 429。

---

## 3. 变更日志

### v1.1.0（2026-06）

**新增**：
- Heartbeat 响应新增 `cancelRequested` 字段（HTTP 200 替代 204）
- `getSnapshot` 自动重建过期/缺失快照（不再返回 SNAPSHOT_EXPIRED）
- `submitFailure` 支持 `JOB_CANCELLED` 错误码
- 用户 API：`POST /ai/jobs` 创建分析 Job（11 步流程含幂等、配额、熔断）
- 用户 API：`GET/POST/PUT/DELETE /ai/model-credentials` 凭证管理
- 用户 API：`GET /ai/analyses`, `/ai/recommendations`, `/ai/weak-points` 分析结果查询
- 用户 API：`GET /ai/quizzes`, `/ai/flashcards` 已发布内容查询
- 平台预算熔断：`PlatformBudgetService` 日 Token/成本限制 + 熔断器
- 结果落库：`persistResult` 支持 5 种 jobType（learning_state_analysis / weak_point_analysis / next_action_planning / quiz_generation / flashcard_generation）
- 候选转换：`convertQuizCandidates` / `convertFlashcardCandidates` 去重 + 正式记录创建
- Job 完成通知：`notifyJobComplete` 推送通知给用户

**修复**：
- `getProfile` 的 `?? {}` 修复（之前 Promise 永远 truthy）
- `HeartbeatResponse.cancel_requested: bool` 客户端适配
- N+1 去重查询 → 批量预取（Quiz/Flashcard convertCandidates）
- `persistResult` / `notifyJobComplete` fire-and-forget 增加日志 + 计数器
- `job-reaper` take:500 上限 → 游标批处理循环
- `checkAndReserve` → `checkQuota` 重命名（澄清只读语义）
- `testCredential` 使用 ConfigService 获取 DeepSeek baseUrl
- Quota 测试拆分 `mockSettingsFindUnique` / `mockUsageFindUnique`

### v1.0.0（2026-05）

**初始版本**：
- Internal API：Poll / Lock / Heartbeat / GetSnapshot / ResolveCredential / SubmitResult / SubmitFailure / SubmitInvocationLogs
- User API：Profile / Settings / Learning Profile CRUD
- 鉴权：InternalAuthGuard（x-internal-api-key）+ JWT（用户端点）
- 加密：CredentialEncryptionService（AES-256-GCM）
- 配额：UserAiQuotaService（checkQuota + incrementJobCount + recordTokenUsage）

---

## 4. 接入示例

### 4.1 Runtime 心跳 + 取消检测（v1.1）

```
POST /internal/runtime/jobs/{jobId}/heartbeat
Authorization: Bearer <RUNTIME_SERVICE_TOKEN>
x-runtime-instance-id: runtime-001
```

响应 200：
```json
{
  "jobId": "job-abc123",
  "lockUntil": 1700000000123,
  "cancelRequested": false
}
```

`cancelRequested=true` 时，Runtime 应在下一个 I/O 检查点中止执行并提交 `JOB_CANCELLED` fail。

### 4.2 前端创建分析 Job

```
POST /ai/jobs
Authorization: Bearer <USER_JWT>

{
  "jobType": "quiz_generation",
  "targetType": "knowledge_base",
  "targetId": "kb-001",
  "apiKeyMode": "platform_key",
  "idempotencyKey": "client-uuid-xxx",
  "questionCount": 10
}
```

幂等：相同 `idempotencyKey` 重复请求返回已有 Job ID（不重复创建）。

---

## 5. 相关资源

- [Internal API 通信协议](./ai-runtime-internal-api-protocol.md)
- [AI Runtime 架构](./ai-runtime-architecture.md)
- [Job 状态机](./ai-job-state-machine.md)
- [数据权限与隐私](./ai-data-permission-privacy.md)
- [用户 AI API 文档](./ai-runtime-user-api.md)
