# admin-web API PRD — 批 1: Runtime Context + AI 管理 + Triggers

> 日期:2026-05-04 / Wave 1 W1-T7 / 范围:admin-ai (17) + admin-runtime-context (4) + admin-triggers (1)
> 整体 5 批计划见 [`docs/_overview/04b-feedback/P1-7-admin-api-prd-evaluation.md`](../04b-feedback/P1-7-admin-api-prd-evaluation.md)
>
> **OpenAPI 与代码同步差异警示**:本批审视发现 `docs/contracts/openapi/backend-api.json`(2026-05-04 抓取)只暴露 13 个 admin-ai 端点,而当前代码 `apps/backend/app/routers/admin_ai.py` 实际已扩到 17 个(多出 `/run/{app_type}` `/triggers` GET/PATCH `/prewarm/progress` `/trigger-event`),且完全缺失 `admin-runtime-context` 5 个端点与 `admin-triggers/unified` 端点。**这是一个 P1 级评估发现,记入第五章。**

---

## 一、批 1 端点清单(22 个)

| 序 | Method | Path | tag | 用途简述 | 路由代码 |
|---|---|---|---|---|---|
| 1 | GET | `/api/admin/ai/dashboard` | admin-ai | AI 监控总览 | admin_ai.py:117 |
| 2 | GET | `/api/admin/ai/trigger-jobs` | admin-ai | 调度任务分页列表 | admin_ai.py:138 |
| 3 | GET | `/api/admin/ai/trigger-jobs/{job_id}` | admin-ai | 调度任务详情 | admin_ai.py:166 |
| 4 | POST | `/api/admin/ai/trigger-jobs/{job_id}/retry` | admin-ai | 手动重跑触发任务 | admin_ai.py:178 |
| 5 | GET | `/api/admin/ai/run-logs` | admin-ai | AI 调用日志列表 | admin_ai.py:194 |
| 6 | GET | `/api/admin/ai/run-logs/{log_id}` | admin-ai | AI 调用日志详情 | admin_ai.py:225 |
| 7 | POST | `/api/admin/ai/cache/invalidate` | admin-ai | 批量缓存失效 | admin_ai.py:240 |
| 8 | GET | `/api/admin/ai/budget` | admin-ai | Token 预算用量 | admin_ai.py:257 |
| 9 | POST | `/api/admin/ai/batch-run` | admin-ai | 创建批量执行(预估) | admin_ai.py:269 |
| 10 | POST | `/api/admin/ai/batch-run/confirm` | admin-ai | 确认批量执行 | admin_ai.py:283 |
| 11 | GET | `/api/admin/ai/alerts` | admin-ai | 告警列表 | admin_ai.py:299 |
| 12 | POST | `/api/admin/ai/alerts/{log_id}/ack` | admin-ai | 确认告警 | admin_ai.py:317 |
| 13 | POST | `/api/admin/ai/alerts/{log_id}/ignore` | admin-ai | 忽略告警 | admin_ai.py:327 |
| 14 | POST | `/api/admin/ai/run/{app_type}` | admin-ai(代码) / 缺 OpenAPI | 按需执行单个 App | admin_ai.py:352 |
| 15 | GET | `/api/admin/ai/triggers` | admin-ai(代码) / 缺 OpenAPI | AI 触发器配置列表 | admin_ai.py:400 |
| 16 | PATCH | `/api/admin/ai/triggers/{trigger_id}` | admin-ai(代码) / 缺 OpenAPI | 更新 AI 触发器 | admin_ai.py:409 |
| 17 | GET | `/api/admin/ai/prewarm/progress` | admin-ai(代码) / 缺 OpenAPI | 72 组合预热进度 | admin_ai.py:431 |
| 18 | POST | `/api/admin/ai/trigger-event` | admin-ai(代码) / 缺 OpenAPI | 手动触发 AI 事件 | admin_ai.py:444 |
| 19 | GET | `/api/config/runtime-context` | 业务配置 / 缺 OpenAPI | 当前用户门店运行上下文 | admin_runtime_context.py:46 |
| 20 | GET | `/api/admin/runtime-context` | 业务运行上下文 / 缺 OpenAPI | 按门店查上下文 | admin_runtime_context.py:54 |
| 21 | GET | `/api/admin/runtime-context/sites` | 业务运行上下文 / 缺 OpenAPI | 列出所有门店上下文 | admin_runtime_context.py:64 |
| 22 | PATCH | `/api/admin/runtime-context` | 业务运行上下文 / 缺 OpenAPI | 切换门店运行模式 | admin_runtime_context.py:94 |
| 23 | GET | `/api/admin/triggers/unified` | 系统管理 / 缺 OpenAPI | 三表聚合触发器视图 | admin_triggers.py:145 |

> 实际为 23 个端点(章节标题写 22 因 1 个为 admin-runtime-context 与 1 个 config-runtime-context 合并归类)。下方按 三大功能区分章撰写。

---

## 二、Runtime Context 端点详细 PRD(5 个)

业务运行上下文(Runtime Context)用于多门店"沙箱/正式"模式切换,核心实体 `biz.site_runtime_context`(每个 site_id 一行)。沙箱模式下数据库写入会附加 `sandbox_instance_id` 隔离,可独立"穿越到任意业务日期"做演练。

### `GET /api/config/runtime-context`

**用途**:小程序/admin-web 通用 — 返回当前登录用户门店的业务运行上下文。

**权限**:任意登录用户(`Depends(get_current_user)`),不限角色。

**调用方**:admin-web 当前未直接调用此路径(走 `/admin/runtime-context?site_id=...`);小程序侧使用,但本批不展开。

**Request Schema**:无 query / body 参数;`site_id` 取自 JWT。

**Response Schema** ([RuntimeContextResponse](../../../apps/backend/app/schemas/runtime_context.py:16)):

| 字段 | 类型 | 说明 |
|---|---|---|
| site_id | int | 门店 ID |
| mode | "live" \| "sandbox" | 当前模式 |
| business_day_start_hour | int | 业务日切点(0-23,默认 4) |
| business_date | date | 业务日期(沙箱模式可能与真实日期不同) |
| business_now | datetime | 业务当前时刻 |
| sandbox_date | date \| null | 沙箱锚定日期(仅 sandbox 模式) |
| sandbox_instance_id | str \| null | 沙箱实例 UUID(用于数据隔离) |
| ai_mode | "live" | AI 模式(暂只支持 live) |
| status | str | 上下文状态(active 等) |
| is_sandbox | bool | 是否沙箱(冗余便利字段) |

**业务语义**:
- 任何业务接口(报表/财务/AI 触发)在沙箱模式下应使用 `business_date` 而非系统 `today()`,以保证"穿越体验"一致。
- `sandbox_instance_id` 用于在 `dws.*` 等表新增数据时附 UUID 标记,切换沙箱实例时旧数据自然失效。

**评估问题**:
- **P2 — RESTful 命名不一致**:同一资源同时暴露在 `/api/config/runtime-context`(小程序友好)和 `/api/admin/runtime-context`(管理端)。两个 path 返回的是同一个 Pydantic 模型,可考虑统一为 `/api/runtime-context`,前端按 query 决定 site 来源。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_runtime_context.py:46`
- Schema:`apps/backend/app/schemas/runtime_context.py:16`
- 服务实现:`apps/backend/app/services/runtime_context.py:88`(`get_runtime_context()`)
- 前端调用:无(批 1 范围)

---

### `GET /api/admin/runtime-context`

**用途**:系统管理端按 `site_id` 查询任意门店的业务运行上下文。

**权限**:`super_admin` 强制(`_require_super_admin`,行内函数 admin_runtime_context.py:34)。

**调用方**:`apps/admin-web/src/api/runtimeContext.ts:63` `fetchRuntimeContext(siteId)`,被沙箱配置页面/Runtime Context 总览页面消费。

**Request Schema**:

| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| site_id | query | int (≥1) | 是 | 门店 ID |

**Response Schema**:同 `/api/config/runtime-context`(`RuntimeContextResponse`)。

**业务语义**:
- super_admin 跨门店查看,与登录用户的 `site_id` 解耦。
- 与 `/api/admin/runtime-context/sites` 的差异:本端点返回单门店完整上下文(含 business_date/business_now 计算结果),而 sites 列表只返回静态字段,不计算业务日期。

**评估问题**:
- **P1 — 权限粒度过粗**:仅 super_admin 才能查任意门店。`tenant_admin` 应当能查自己 tenant 下所有门店,但当前一刀切。建议按 `tenant_id ↔ site_id` 关系判断(`auth.tenant_admins.tenant_id` 对应 `biz.sites.tenant_id`)。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_runtime_context.py:54`
- 前端调用:`apps/admin-web/src/api/runtimeContext.ts:63`

---

### `GET /api/admin/runtime-context/sites`

**用途**:列出所有可配置门店及其当前运行上下文(用于沙箱配置主表)。

**权限**:`super_admin` 强制。

**调用方**:`apps/admin-web/src/api/runtimeContext.ts:58` `fetchRuntimeSites()`,Runtime Context 主页表格 / 沙箱配置入口。

**Request Schema**:无参数。

**Response Schema**:`list[RuntimeSiteItem]`:

| 字段 | 类型 | 说明 |
|---|---|---|
| site_id | int | 门店 ID |
| site_name | str \| null | 门店名 |
| site_code | str \| null | 门店编码 |
| is_active | bool | 门店启用 |
| mode | "live"\|"sandbox"\|null | 当前模式(无配置时 null) |
| sandbox_date | date \| null | 沙箱锚定日期 |
| sandbox_instance_id | str \| null | 沙箱实例 UUID |
| ai_mode | "live" \| null | AI 模式 |
| status | str \| null | 状态 |
| updated_at | datetime \| null | 最后更新时间 |

**业务语义**:
- 直接 SQL `LEFT JOIN`,门店未配置 runtime_context 时仅返回 site 基础列。
- 排序:`is_active DESC, site_id`(启用门店在前)。

**评估问题**:
- **P1 — 缺少分页/数量限制**:当门店数 > 200 时单次响应可能超 300KB(每行 ~150B + JSON 包装)。当前查询无 LIMIT,不阻塞表 scan。建议 query 加 `page/page_size`,默认 200。
- **P2 — Response Model 缺失**:`response_model` 未声明,实际返回 `list[dict]`,OpenAPI 看不到字段约束;前端 TS 类型靠 `runtimeContext.ts:24-35` 手写,易漂移。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_runtime_context.py:64`
- 前端调用:`apps/admin-web/src/api/runtimeContext.ts:58`

---

### `PATCH /api/admin/runtime-context`

**用途**:切换某门店的业务运行上下文(live ↔ sandbox)。这是整个沙箱体系最核心的写操作。

**权限**:`super_admin` 强制。

**调用方**:`apps/admin-web/src/api/runtimeContext.ts:73` `switchRuntimeContext(payload)`,Runtime Context 详情页 / 沙箱配置 Modal。

**Request Schema** (`RuntimeSwitchRequest`,schemas/runtime_context.py:37):

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| site_id | int (≥1) | 是 | 门店 ID |
| mode | "live"\|"sandbox" | 是 | 目标模式 |
| sandbox_date | date \| null | 条件必填 | sandbox 模式必填,且 ≤ 真实今天 |
| reset_sandbox | bool | 否(默认 true) | 是否重新生成 sandbox_instance_id |
| reason | str (≤500 字符) \| null | 否 | 切换原因(审计) |

**Response Schema** (`RuntimeSwitchResponse`):

| 字段 | 类型 | 说明 |
|---|---|---|
| context | RuntimeContextResponse | 切换后的最新上下文 |
| steps | list[RuntimeTransitionStep] | 切换过程的步骤列表(对前端进度展示) |

`RuntimeTransitionStep` 结构(schemas/runtime_context.py:29):

| 字段 | 类型 | 说明 |
|---|---|---|
| key | str | 步骤键(如 cancel_etl_processes) |
| title | str | 步骤标题 |
| status | "success"\|"skipped"\|"warning"\|"failed" | 步骤结果 |
| detail | str | 详细描述 |
| count | int | 受影响数量(如取消的 ETL 数) |

**业务语义**:
- 切换前会**自动停服**当前门店的所有运行中活动:
  1. 终止内存内 ETL 执行(`task_executor.cancel`),admin_runtime_context.py:191-214
  2. 取消 `task_queue` 中 pending/running 的记录,admin_runtime_context.py:217-249
  3. 取消内存内 AI Dispatcher 调用链,admin_runtime_context.py:253-271
  4. 把 `biz.ai_trigger_jobs` 中 pending/running 的记录置为 cancelled,admin_runtime_context.py:273-306
- `biz.trigger_jobs`(全局触发器)**不被暂停**,因为它是全局调度表,无 `site_id` 列。
- `mode=sandbox` 且 `reset_sandbox=true` 或旧上下文无 sandbox_instance_id 时,生成新 UUID;否则复用旧 UUID(沙箱"继续模式")。

**典型调用流程**:
1. 用户在 admin-web 沙箱配置 Modal 选择 mode=sandbox + sandbox_date=2026-04-15 + reason="演练春节歇业"
2. 前端 PATCH 该端点
3. 后端依次:停 ETL → 停队列 → 停 AI → INSERT/ON CONFLICT 写 `biz.site_runtime_context` → 返回 steps + 新 context
4. 前端展示 5 个 step 的进度面板,引导用户确认进入沙箱

**评估问题**:
- **P0 — 长事务 + 多重资源操作风险**:整个切换过程涉及 4 个资源(进程内 task_executor / DB task_queue / 进程内 dispatcher / DB ai_trigger_jobs),没有事务包裹,任何一步异常后,部分已停部分未停,系统进入半状态。建议引入幂等键(如 `transition_id`)+ 失败时允许重试同 transition_id 不重复执行。
- **P1 — 没有 audit 表写入**:切换历史(谁/何时/原因/旧 mode → 新 mode)只 `UPDATE` 当前行,旧值丢失,违反"沙箱必可追溯"原则。建议新增 `biz.site_runtime_context_history` 表,每次切换 INSERT 一行(reason 在 P1 schema 中已支持,但只存最后一次)。
- **P2 — `_stop_runtime_activity` 内有多次 connect/close**:每个清理步骤新开连接(admin_runtime_context.py:217 / 273),应共用一个连接,减少 4-5 倍连接开销。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_runtime_context.py:94`
- 前端调用:`apps/admin-web/src/api/runtimeContext.ts:73`
- DB 表:`db/schemas/biz/site_runtime_context.sql`(权威 DDL)
- PRD 文档:`docs/prd/specs/P20-runtime-context-sandbox.md`(本会话新建)

---

## 三、AI 管理(admin-ai)端点详细 PRD(17 个)

> 通用前提:全部需要 JWT 认证 + admin 角色之一(`site_admin`/`tenant_admin`/`super_admin`),由 `_require_admin()` 在 admin_ai.py:70 注入(单独自动校验 `auth.admin_users.is_active`)。所有写操作均会落 `ai_run_logs` / `ai_trigger_jobs` 等表。

### `GET /api/admin/ai/dashboard`

**用途**:AI 监控总览(今日调用数、成功率、token、延迟、7 天趋势、各 App 分布、最近告警、健康状态)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:238` `getDashboard(query?)`,被 AI 仪表盘页面 (`AIDashboard.tsx` 系列) 消费。

**Request Schema**(全部 query):

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| site_id | int | 否 | 门店筛选(空=全门店) |
| range_days | int (1-365) | 否 | 回溯天数(1=今日 / 3 / 7 / 10) |
| date_from | str (YYYY-MM-DD) | 否 | 起始日期(与 date_to 成对) |
| date_to | str (YYYY-MM-DD) | 否 | 结束日期 |

**Response Schema** (`DashboardResponse`,schemas/admin_ai.py:59):

| 字段 | 类型 | 说明 |
|---|---|---|
| today_calls | int | 当日调用总次数 |
| today_success_rate | float | 当日成功率(0-1) |
| today_tokens | int | 当日总 token 消耗 |
| today_avg_latency_ms | float | 当日平均延迟 ms |
| trend_7d | list[DailyTrend] | 7 天每日 calls + success_rate |
| app_distribution | list[AppDistItem] | 各 app_type 调用占比 |
| budget | BudgetInfo | 日/月预算用量 |
| recent_alerts | list[AlertItem] | 最近 10 条告警 |
| app_health | list[AppHealthItem] | 各 App 最近一次调用状态 |

**业务语义**:
- 命名"today"但实际语义是"按 query 时间窗"(若传 range_days=7,today_calls 实际是 7 天累计)— **命名误导,P1**。
- 7 天 trend 始终基于自然 7 天,不受 query 影响,与上方"按窗口聚合"的字段语义不一致。

**评估问题**:
- **P1 — 字段命名误导**:`today_calls` 等字段在 `range_days>1` 时含义变成"窗口累计",前端容易误展示。建议改名为 `period_calls` 或新增 `period` 字段标明聚合范围。
- **P1 — 4 个 query 互斥关系未约束**:`range_days` 与 `date_from/date_to` 同时传时,后端实现按谁优先未在 schema 体现,只在 service 推断(admin_service.py:69+)。OpenAPI 看不出来,前端易传错。建议加 model_validator 强制互斥。
- **P2 — 9 个子查询并发执行**:dashboard 一次响应内并行/串行执行 5+ 个 SQL,端到端可能 800ms+;建议引入 5-30s 缓存。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:117`
- 服务:`apps/backend/app/services/ai/admin_service.py:40`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:238`

---

### `GET /api/admin/ai/trigger-jobs`

**用途**:AI 调度任务(biz.ai_trigger_jobs)分页列表 — 这是 AI 触发链的"运行实例历史"。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:246` `getTriggerJobs(params)`,AI 触发任务历史页面消费。

**Request Schema**(全部 query):

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| event_type | str | 否 | consumption / dws_completed / note_created / task_assigned |
| status | str (alias `status_filter`) | 否 | pending / running / success / failed / cancelled |
| site_id | int | 否 | 门店 |
| date_from / date_to | YYYY-MM-DD | 否 | 创建时间范围 |
| page | int (≥1) | 否 | 默认 1 |
| page_size | int (1-100) | 否 | 默认 20 |

**Response Schema** (`TriggerJobListResponse`):

| 字段 | 类型 | 说明 |
|---|---|---|
| items | list[TriggerJobItem] | id/event_type/member_id/status/app_chain/is_forced/site_id/started_at/finished_at/created_at |
| total | int | 总条数 |
| page | int | 当前页 |
| page_size | int | 页大小 |
| today_skipped_duplicates | int | 今日去重跳过数(全局,不受 filter 影响) |

**业务语义**:
- "调度任务"实际是 AI 链的事件入口实例。`app_chain` 是文本 "app2_finance,app3_clue,app7_customer" 列出实际跑的 App 链。
- `today_skipped_duplicates` 是诊断字段,告诉用户去重去掉了多少潜在请求。

**评估问题**:
- **P1 — `today_skipped_duplicates` 不应放列表 API 里**:它与分页参数无关,会随每次翻页重复计算,浪费 SQL。建议挪到 dashboard 或独立 stats 端点。
- **P2 — 命名歧义"trigger-jobs"**:与 `biz.trigger_jobs`(全局触发器配置表)名字相同但语义完全不同(后者是配置,前者是运行实例)。本端点返回的是 `biz.ai_trigger_jobs`(实例),建议改名 `/api/admin/ai/jobs` 或 `/runs`。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:138`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:246`

---

### `GET /api/admin/ai/trigger-jobs/{job_id}`

**用途**:单个调度任务详情(含 payload + error_message + connector_type)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:251` `getTriggerJobDetail(id)`,AI 触发任务详情 Drawer。

**Request Schema**:path 参数 `job_id: int`。

**Response Schema** (`TriggerJobDetailResponse`,继承 TriggerJobItem):
- TriggerJobItem 全部字段
- payload: dict \| null — 触发原始 payload
- error_message: str \| null — 失败原因
- connector_type: str — 触发来源(feiqiu / dws_completion 等)

**业务语义**:用于排查失败原因,通常配合 retry 端点使用。

**评估问题**:
- **P2 — 404 文案"调度任务不存在"**:与 list API 命名不一致(那边是"调度任务",但实际数据源是 `ai_trigger_jobs`)。建议统一文案 "AI 触发任务不存在"。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:166`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:251`

---

### `POST /api/admin/ai/trigger-jobs/{job_id}/retry`

**用途**:手动重跑某调度任务(创建新 ai_trigger_jobs,is_forced=true,异步执行)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:256` `retryTriggerJob(id)`,详情 Drawer 的"重跑"按钮。

**Request Schema**:path 参数 `job_id: int`,无 body。

**Response Schema** (`RetryResponse`):

| 字段 | 类型 | 说明 |
|---|---|---|
| trigger_job_id | int | 新建的任务 ID |
| status | str | "pending" |

**业务语义**:
- 创建一个新的 ai_trigger_jobs 记录(继承原 payload + event_type),`is_forced=true` 跳过去重校验。
- 失败抛 ValueError → 404(由 service 抛)。

**评估问题**:
- **P1 — 缺幂等键**:用户连点 N 次按钮会创建 N 条 retry job。POST 应支持 idempotency-key header 或前端主动节流。
- **P2 — 重跑后无前端轮询提示**:返回的 trigger_job_id 是新 ID,但前端目前不轮询其完成状态(需要重刷列表才能看到结果)。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:178`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:256`

---

### `GET /api/admin/ai/run-logs`

**用途**:AI 调用日志(biz.ai_run_logs)分页列表(每个 App 调用一次写一行)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:262` `getRunLogs(params)`,AI 调用日志页面。

**Request Schema**(query):

| 参数 | 类型 | 说明 |
|---|---|---|
| app_type | str | app2_finance / app3_clue / ... |
| status (alias) | str | success / failed / timeout / circuit_open |
| trigger_type | str | event / cron / manual / batch |
| site_id | int | 门店 |
| date_from / date_to | YYYY-MM-DD | 时间范围 |
| page / page_size | int | 同 trigger-jobs |

**Response Schema** (`RunLogListResponse`):
- items: list[RunLogItem] (id / app_type / trigger_type / member_id / tokens_used / latency_ms / status / site_id / created_at)
- total / page / page_size

**业务语义**:列表只展示元信息,不含 prompt / response 大字段(由详情 API 返回)。

**评估问题**:
- **P0 — 大表性能**:`ai_run_logs` 是高频写入表,生产数据量预计每月 100w+,目前 ORDER BY created_at DESC 若 created_at 无索引会慢。需要确认 DDL 是否建索引(`db/schemas/biz/ai_run_logs.sql`)。
- **P1 — 缺 status="cancelled" 文档**:实际数据源里因为 runtime-context 切换会产生 cancelled 状态(见上方第二章 PATCH 流程),但本 API filter 文档未列出。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:194`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:262`

---

### `GET /api/admin/ai/run-logs/{log_id}`

**用途**:单条调用日志详情(含完整 prompt / response,**不脱敏**)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:267` `getRunLogDetail(id)`,详情 Drawer。

**Request Schema**:path `log_id: int`。

**Response Schema** (`RunLogDetailResponse`,继承 RunLogItem):
- request_prompt: str \| null
- response_text: str \| null
- error_message: str \| null
- session_id: str \| null
- finished_at: str \| null

**业务语义**:用于排查 AI 应用的 prompt 工程问题,故意不脱敏。

**评估问题**:
- **P0 — PII 泄露面**:prompt 通常包含会员姓名/手机号/消费明细,`tenant_admin` 跨 tenant 也能查,违反多租户隔离原则。建议:除 super_admin 外,其他 admin 仅能查本 site_id 下的 log;或对 prompt/response 做按角色脱敏。
- **P1 — 大字段无大小限制**:某次失败的 response_text 可能 50KB+,会拖慢 JSON 序列化。建议加 `?include_raw=false` query 默认只返回截断。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:225`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:267`

---

### `POST /api/admin/ai/cache/invalidate`

**用途**:批量缓存失效(`ai_cache.status='invalidated'`)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:273` `invalidateCache(body)`,缓存管理页 / Dashboard 操作按钮。

**Request Schema** (`CacheInvalidateRequest`):

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| site_id | int | **是** | 门店,强制 |
| app_type | str \| null | 否 | 不传=该门店全部 App |
| member_id | int \| null | 否 | 不传=该门店全部成员 |

**Response Schema**:`{ affected_count: int }`。

**业务语义**:
- `site_id` 必填是为了防止误清全平台缓存(P1-5 文档曾讨论)。
- 实际不删除行,只置 status,仍保留历史。

**评估问题**:
- **P1 — site_id 必填但 super_admin 也无法跨站清理**:某些场景下 super_admin 想"全平台失效",目前只能 N 次调用。建议 super_admin 角色允许 `site_id=null`。
- **P2 — 没有审计字段**:谁清的、为什么清,没记录,出问题难追溯。建议加 `reason` 字段并写 audit_log 表。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:240`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:273`

---

### `GET /api/admin/ai/budget`

**用途**:Token 预算用量(日/月已用/上限/百分比)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:279` `getBudget()`,Dashboard 顶部 + 预算管理页。

**Request Schema**:无参数。

**Response Schema** (`BudgetResponse`):

| 字段 | 类型 | 说明 |
|---|---|---|
| daily_used / daily_limit / daily_pct | int / int / float | 当日 |
| monthly_used / monthly_limit / monthly_pct | int / int / float | 当月 |

**业务语义**:limit 来自配置(env / settings),used 来自 ai_run_logs 当日累加。

**评估问题**:
- **P1 — 全局预算无 site 维度**:多门店时一个超额会影响全平台。生产实际中应支持按 site 配额(`tenant_admin` 视角看自己 tenant 配额)。当前是全局口径。
- **P2 — 与 dashboard.budget 字段重复**:`DashboardResponse.budget` 与本端点字段一致,可合并(批 1 评估第五章重复 API)。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:257`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:279`

---

### `POST /api/admin/ai/batch-run`

**用途**:创建批量执行请求,返回预估(token / 调用次数)— **不立即执行**。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:285` `createBatchRun(body)`,批量执行 Modal 第一步。

**Request Schema** (`BatchRunRequest`):

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_types | list[str] | 是 | 要执行的 app_type 列表 |
| member_ids | list[int] | 是 | 目标成员 ID 列表 |
| site_id | int | 是 | 门店 |

**Response Schema** (`BatchRunEstimate`):

| 字段 | 类型 | 说明 |
|---|---|---|
| batch_id | str | 用于后续 confirm 的批次 ID |
| estimated_calls | int | 预估调用次数 |
| estimated_tokens | int | 预估 token 消耗 |

**业务语义**:两阶段提交模式,先预估再确认,避免误操作直接耗 token。

**评估问题**:
- **P0 — batch_id 生命周期未声明**:Schema 没说 batch_id 多久过期,如果不调用 confirm 是否清理。建议声明 30 分钟过期。
- **P1 — member_ids 列表无上限**:传入 10000 个 member_id 时 estimate SQL 会全表扫,建议加 `len(member_ids) <= 500` 校验。
- **P2 — 不支持"按全门店所有 member"模式**:必须显式列 ID,不便用作"批量为所有会员预热"。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:269`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:285`

---

### `POST /api/admin/ai/batch-run/confirm`

**用途**:确认批量执行,后台异步执行(不等待返回)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:290` `confirmBatchRun(batchId)`,Modal 第二步确认。

**Request Schema** (`BatchRunConfirm`):`{ batch_id: str }`

**Response Schema** (`BatchRunConfirmResponse`):`{ status: "started" }`

**业务语义**:从内存/缓存中按 batch_id 取出预估时记录的 app_types/member_ids,投递到 dispatcher。

**评估问题**:
- **P0 — 状态查询缺失**:用户 confirm 后无法查询批次进度(已完成 X / 总 Y),只能去 run-logs 列表里逐条筛选。建议新增 `GET /api/admin/ai/batch-run/{batch_id}/status`。
- **P1 — batch_id 失效错误码不明**:抛 400 但只带文案"...",前端难精准提示。建议规范错误码 `batch_not_found` / `batch_expired`。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:283`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:290`

---

### `GET /api/admin/ai/alerts`

**用途**:告警列表(从 `ai_run_logs` WHERE status IN ('failed','timeout','circuit_open') 派生)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:298` `getAlerts(params)`,告警中心页。

**Request Schema**:

| 参数 | 类型 | 说明 |
|---|---|---|
| alert_status | str | pending / acknowledged / ignored |
| site_id | int | 门店 |
| page / page_size | int | 默认 1 / 20 |

**Response Schema** (`AlertListResponse`):items + total + page + page_size,item 字段同 AlertItem。

**业务语义**:不是独立 alerts 表,是 run-logs 失败行的视图(alert_status 列存在 run-logs 上)。

**评估问题**:
- **P1 — 与 run-logs 严重重复**:本 API 实际是 `run-logs?status=in(failed,timeout,circuit_open)` 的语法糖。建议合并到 run-logs(扩 status 多选),减少端点数。
- **P2 — 告警去重缺失**:同一类型连续失败 100 次会出 100 条 alert,无聚合。建议引入 fingerprint 字段(app_type + error_message hash)做合并。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:299`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:298`

---

### `POST /api/admin/ai/alerts/{log_id}/ack`

**用途**:确认告警(alert_status → acknowledged)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:303` `ackAlert(id)`,告警中心 / 详情。

**Request Schema**:path `log_id: int`,无 body。

**Response Schema** (`AlertActionResponse`):`{ id: int, alert_status: "acknowledged" }`

**业务语义**:UPDATE ai_run_logs.alert_status,无业务副作用。

**评估问题**:
- **P1 — 不记录 ack_by / ack_at**:谁在何时确认无审计。建议 schema 加 ack_by_user_id / ack_at 字段,DB 也补列。
- **P2 — RESTful 语义模糊**:POST 改状态偏向 PATCH,建议 `PATCH /alerts/{id}` body `{ alert_status: "acknowledged" }`,与 ignore 端点合并。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:317`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:303`

---

### `POST /api/admin/ai/alerts/{log_id}/ignore`

**用途**:忽略告警(alert_status → ignored)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:308` `ignoreAlert(id)`。

**Request / Response Schema**:同 ack,只是 alert_status 变为 "ignored"。

**业务语义**:同 ack,业务上区分"已修"vs"忽略"。

**评估问题**:
- **P1 — 与 ack 几乎相同**:两个端点只是 status 值不同,结构 100% 重复。建议合并为 `PATCH /alerts/{id}` body 带 status。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:327`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:308`

---

### `POST /api/admin/ai/run/{app_type}` (代码已扩,OpenAPI 缺失)

**用途**:按需执行单个 App,跳过链路编排(直接走 dispatcher.run_single_app)。

**权限**:任意 admin 角色。`triggered_by` 字段记 `admin:{user_id}`。

**调用方**:`apps/admin-web/src/api/adminAI.ts:332` `runApp(appType, body)`,缓存详情页/告警页/AI 预热页"重新生成"按钮。

**Request Schema**(path + body):
- path: `app_type: str` ∈ `_SUPPORTED_APP_TYPES`(8 个)
- body (`RunAppRequest`,schemas/admin_ai.py:202):
  - site_id: int (必填)
  - member_id / assistant_id / time_dimension / area / note_content / noted_by_name / noted_by_created_at:可选,各 App 不同要求

**Response Schema** (`RunAppResponse`):

| 字段 | 类型 | 说明 |
|---|---|---|
| app_type | str | 实际执行的 App |
| success | bool | 失败时为 false |
| result | dict \| null | 千问返回 JSON |
| error | str \| null | 失败描述 |

**业务语义**:
- 直接调用 `dispatcher.run_single_app`,熔断/限流/预算检查由 _run_step 自动执行。
- 失败不抛 HTTPException 而是 success=false,便于前端在批量预热场景下统一处理。

**评估问题**:
- **P1 — RunAppRequest 字段全部可选,实际各 App 必填字段不同**:schema 不强制(app2_finance 不填 member_id 也通过校验,到 dispatcher 才报错)。建议拆 8 个 sub-schema 用 discriminated union 或在 router 层显式校验。
- **P2 — 422 路径错误信息只有"不支持的 app_type"**:失败时不告诉前端 `_SUPPORTED_APP_TYPES` 是哪 8 个完整列表(实际 detail 已列,前端可消费)。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:352`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:332`
- 测试:`apps/admin-web/src/__tests__/adminAiAppTypes.test.ts`(回归对齐 8 个 app_type)

---

### `GET /api/admin/ai/triggers` (代码已扩,OpenAPI 缺失)

**用途**:列出所有 AI 相关触发器(`biz.trigger_jobs` WHERE `job_type LIKE 'ai_%' OR job_name='task_generator'`)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:358` `listTriggers()`,AI 触发器配置页 (`AITriggers.tsx`)。

**Request Schema**:无参数。

**Response Schema**:`list[TriggerItem]`(schemas/admin_ai.py:250),字段:id/job_name/job_type/trigger_condition/trigger_config(dict)/status/description/last_run_at/next_run_at/last_error。

**业务语义**:展示 6 个 AI 相关触发器(`task_generator` + 5 个 `ai_*`),用于启停/改 cron。

**评估问题**:
- **P0 — 与 `/api/trigger-jobs` 严重重复**:已在 `docs/_overview/04b-feedback/P1-6-trigger-api-merge.md` 详细分析。GET 字段 95% 重合,仅缺 `created_at`;PATCH 字段互补(本端点支持 status/description,trigger-jobs 支持 interval)。**直接引用 P1-6,本批不重复分析,Wave 2 按方案 A 合并**。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:400`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:358`
- P1-6 合并方案:`docs/_overview/04b-feedback/P1-6-trigger-api-merge.md`

---

### `PATCH /api/admin/ai/triggers/{trigger_id}` (代码已扩,OpenAPI 缺失)

**用途**:更新 AI 触发器(启停 / cron / description)。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:363` `updateTrigger(id, body)`。

**Request Schema** (`TriggerUpdateRequest`):3 字段全可选,至少一个。
- status: enabled / disabled
- cron_expression: 标准 5 段 cron
- description: 文本

**Response Schema**:`TriggerItem` 完整对象。

**业务语义**:对 `biz.trigger_jobs` 的 UPDATE,但 cron 实际写入 `trigger_config jsonb` 字段(jsonb_set)。

**评估问题**:
- **P0 — 同上,与 `/api/trigger-jobs/:id/config` PATCH 互补,需合并**(P1-6 方案 A)。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:409`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:363`

---

### `GET /api/admin/ai/prewarm/progress` (代码已扩,OpenAPI 缺失)

**用途**:查询 app2_finance 72 组合(8 area × 9 time_dimension)的预热进度。

**权限**:任意 admin 角色。

**调用方**:`apps/admin-web/src/api/adminAI.ts:383` `getPrewarmProgress(siteId)`,AI 预热页 (`AIPrewarm.tsx`)。

**Request Schema**:query `site_id: int` 必填。

**Response Schema** (`PrewarmProgressResponse`):

| 字段 | 类型 | 说明 |
|---|---|---|
| total | int | 固定 72 |
| done | int | 已生成数 |
| missing | list[PrewarmMissingItem] | 缺失列表(target_id / time_dimension / area) |
| last_updated | str \| null | 最后更新 |

**业务语义**:配合 `AIPrewarm.tsx` 的"一键补跑"按钮,前端逐条 POST `/api/admin/ai/run/app2_finance`。

**评估问题**:
- **P1 — total 硬编码 72**:如果 area 或 time_dimension 维度发生变化(如 2026-04-23 新增 app2a 区域),硬编码会偏离实际。建议从 `meta.app2_combinations` 这种配置表读取。
- **P2 — 仅支持 app2_finance**:命名 `prewarm/progress` 但只服务一个 App;若未来 app2a/app7 也要预热,需扩展或新建端点。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:431`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:383`
- 前端使用:`apps/admin-web/src/pages/AIPrewarm.tsx`

---

### `POST /api/admin/ai/trigger-event` (代码已扩,OpenAPI 缺失)

**用途**:手动触发 AI 事件链,默认 `is_forced=true` 跳过去重 — 这是**沙箱演练 / 故障重放**最关键端点。

**权限**:任意 admin 角色。日志会记 `user_id`(admin_ai.py:478)。

**调用方**:`apps/admin-web/src/api/adminAI.ts:406` `triggerEvent(body)`,AI 调试 / 沙箱演练 / 预热全量。

**Request Schema** (`ManualTriggerRequest`):

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| event_type | str | 是 | consumption / dws_completed / note_created / task_assigned |
| site_id | int | 是 | 门店 |
| member_id | int \| null | 否 | 部分事件需要 |
| assistant_id | int \| null | 否 | app4/app5 需要 |
| payload | dict \| null | 否 | 事件原始 payload |
| is_forced | bool | 否(默认 true) | 是否跳过去重 |

**Response Schema** (`ManualTriggerResponse`):`{ trigger_job_id: int, status: "pending" }`

**业务语义**:
- 直接构造 `TriggerEvent` 对象投递给 `dispatcher.handle_trigger`。
- `is_forced=true` 会绕过 `biz.ai_trigger_jobs` 的去重(同 site_id+event_type+payload 24 小时内只跑一次)。

**评估问题**:
- **P0 — 沙箱模式下未校验 runtime_context**:当门店处于 sandbox 模式时,理论上 AI 调用应附 sandbox_instance_id 隔离;当前端点对此无显式校验,可能污染生产数据。建议在 dispatcher 入口强制校验 site_runtime_context 并附标。
- **P1 — payload 无 schema 约束**:任意 dict 都能传,容易传错(如 consumption 事件需要 settle_at,缺失时 dispatcher 才报错)。建议按 event_type 拆 4 个 sub-schema。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_ai.py:444`
- 前端调用:`apps/admin-web/src/api/adminAI.ts:406`
- 前端使用:`apps/admin-web/src/pages/AIPrewarm.tsx`

---

## 四、Triggers 端点详细 PRD(P1-6 预备合并范围)

### `GET /api/admin/triggers/unified`

**用途**:聚合三张表的触发器数据,提供"全景视图"(只读)。

**权限**:任意登录用户(`Depends(get_current_user)`),**未限 admin 角色** — 这是 P1 评估发现。

**调用方**:`apps/admin-web/src/api/triggers.ts:21` `fetchUnifiedTriggers()`,`TriggerManager.tsx` 主 Tab `all`。

**Request Schema**:无参数。

**Response Schema**:`list[UnifiedTriggerItem]`(schemas/admin_triggers.py:14):

| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 统一 ID(etl 来源使用 ROW_NUMBER+100000 偏移避免冲突) |
| name | str | 名称 |
| source | "biz" \| "ai" \| "etl" | 来源 |
| trigger_condition | str | event / cron / interval / unknown |
| status | str | enabled / disabled / idle / failed 等 |
| last_run_at / next_run_at | str \| null | ISO 时间 |
| last_error | str \| null | 错误 |

**业务语义**:
- 数据源 1:`biz.trigger_jobs`(全量 9 行) → source="biz"
- 数据源 2:`biz.ai_trigger_jobs`(最近 100 条事件实例) → source="ai"
- 数据源 3:`public.scheduled_tasks`(ETL 调度) → source="etl"
- 任一数据源失败,记 warning 日志后跳过,返回其他源数据 — 容错设计良好。

**典型调用流程**:
1. `TriggerManager.tsx` 切到 `all` Tab
2. 前端 GET 该端点拿到 9 + 100 + N 条混合记录
3. 前端按 source 字段分组渲染

**评估问题**:
- **P0 — 权限过松**:仅 `get_current_user`,任何登录用户(含小程序用户!)都能查,泄露其他门店触发器配置信息。**必须收紧到 admin 角色**。
- **P1 — 三个数据源混装,id 冲突靠 +100000 偏移**:`scheduled_tasks.id` 是 UUID,本端点用 `ROW_NUMBER() + 100000` 强转 int(admin_triggers.py:115),不稳定 — `ORDER BY created_at` 一变,id 就变,前端无法长期跟踪。建议用 `(source, original_id)` 复合 ID。
- **P1 — 数据源 2 (ai_trigger_jobs) 语义错位**:这是事件实例不是触发器配置,与 source="biz" 的"触发器"不对等。聚合页面把它们混在一起会让用户混淆 — 已在 P1-6 文档第二章指出。
- **P2 — `LIMIT 100` 写死**:ai 数据源固定取最近 100,不可调,大批量场景信息缺失。

**关联**:
- 路由代码:`apps/backend/app/routers/admin_triggers.py:145`
- Schema:`apps/backend/app/schemas/admin_triggers.py:14`
- 前端调用:`apps/admin-web/src/api/triggers.ts:21`
- 前端使用:`apps/admin-web/src/pages/TriggerManager.tsx`(主 Tab)
- P1-6 合并方案:`docs/_overview/04b-feedback/P1-6-trigger-api-merge.md`

---

## 五、批 1 评估总结

### 5.1 P0/P1/P2 问题清单(28 个)

| 等级 | 端点 | 问题 |
|---|---|---|
| **P0** | PATCH /admin/runtime-context | 长事务无幂等,4 个资源操作部分失败导致半状态 |
| **P0** | GET /admin/ai/run-logs | ai_run_logs 大表查询性能依赖索引,需确认 DDL |
| **P0** | GET /admin/ai/run-logs/{id} | prompt 完整返回造成 PII 跨 tenant 泄露 |
| **P0** | POST /admin/ai/batch-run | batch_id 生命周期未声明 |
| **P0** | POST /admin/ai/batch-run/confirm | 无批次进度查询端点 |
| **P0** | GET /admin/ai/triggers + PATCH | 与 /api/trigger-jobs 重复(P1-6) |
| **P0** | POST /admin/ai/trigger-event | 沙箱模式下未校验 runtime_context |
| **P0** | GET /admin/triggers/unified | 权限仅 `get_current_user`,任意登录可查 |
| **P1** | GET /admin/runtime-context | tenant_admin 应能查本 tenant 门店,当前一刀切 super_admin |
| **P1** | GET /admin/runtime-context/sites | 缺分页 + response_model 缺失 |
| **P1** | PATCH /admin/runtime-context | 切换历史无 audit 表 |
| **P1** | GET /admin/ai/dashboard | today_* 字段在 range_days>1 时命名误导 |
| **P1** | GET /admin/ai/dashboard | range_days vs date_from/to 互斥未约束 |
| **P1** | GET /admin/ai/trigger-jobs | today_skipped_duplicates 不应放列表 API |
| **P1** | POST /admin/ai/trigger-jobs/{id}/retry | 无幂等键,连点产生多重 retry |
| **P1** | GET /admin/ai/run-logs | filter 文档缺 cancelled 状态 |
| **P1** | GET /admin/ai/run-logs/{id} | 大字段无 size 上限 |
| **P1** | POST /admin/ai/cache/invalidate | super_admin 无法跨站清理 |
| **P1** | GET /admin/ai/budget | 无 site 维度 |
| **P1** | POST /admin/ai/batch-run | member_ids 无上限 |
| **P1** | POST /admin/ai/batch-run/confirm | 错误码不规范(batch_not_found / batch_expired 缺失) |
| **P1** | GET /admin/ai/alerts | 与 run-logs 严重重复 |
| **P1** | POST /admin/ai/alerts/{id}/ack | 无 ack_by / ack_at 审计 |
| **P1** | POST /admin/ai/alerts/{id}/ignore | 与 ack 99% 重复 |
| **P1** | POST /admin/ai/run/{app_type} | RunAppRequest 各 App 必填差异未拆 |
| **P1** | GET /admin/ai/prewarm/progress | total=72 硬编码 |
| **P1** | POST /admin/ai/trigger-event | payload 无 schema 约束 |
| **P1** | GET /admin/triggers/unified | id 用 ROW_NUMBER+100000 不稳定;混装语义错位 |
| **P2** | GET /config/runtime-context | 与 /admin/runtime-context 命名不一致 |
| **P2** | PATCH /admin/runtime-context | 多次 connect/close,可共用连接 |
| **P2** | GET /admin/ai/dashboard | 9 子查询无缓存 |
| **P2** | GET /admin/ai/trigger-jobs | "trigger-jobs" 命名歧义 |
| **P2** | GET /admin/ai/trigger-jobs/{id} | 404 文案不一致 |
| **P2** | POST /admin/ai/trigger-jobs/{id}/retry | 无前端轮询提示 |
| **P2** | POST /admin/ai/cache/invalidate | 无审计字段 |
| **P2** | GET /admin/ai/budget | 与 dashboard.budget 重复 |
| **P2** | POST /admin/ai/batch-run | 不支持"全门店所有 member" |
| **P2** | GET /admin/ai/alerts | 告警去重缺失 |
| **P2** | POST /admin/ai/alerts/{id}/ack | RESTful 语义模糊(POST 应改 PATCH) |
| **P2** | POST /admin/ai/run/{app_type} | 422 错误信息可优化 |
| **P2** | GET /admin/ai/prewarm/progress | 仅服务 app2_finance,命名过窄 |
| **P2** | GET /admin/triggers/unified | LIMIT 100 写死 |

**汇总**:P0 = 8 / P1 = 20 / P2 = 13 / 合计 41 个发现(超过 P1-7 评估单 API 1-3 个的预期上限,说明本批是"高度集中的 AI 治理域,问题密集")。

### 5.2 关键发现

**关键发现 1 — OpenAPI 与代码严重不同步**:`docs/contracts/openapi/backend-api.json` 缺失 10 个本批端点(/admin/runtime-context 全 4 个 + /admin/triggers/unified + /admin/ai/run/{app_type} + /admin/ai/triggers 2 个 + /admin/ai/prewarm/progress + /admin/ai/trigger-event)。`/api/config/runtime-context` 也缺失。建议 Wave 1 W1-T8 检查 OpenAPI 抓取脚本(很可能是 router include 顺序或某个 condition 排除了)。

**关键发现 2 — 触发器域有"3 套 + 2 套互补字段集 + 1 套聚合视图"的混乱**:本批暴露 3 个触发器相关端点,加上批外 `/api/trigger-jobs` 一组,共 4 个名字相似但语义不同的端点。前端 `TriggerManager.tsx` / `AITriggers.tsx` / `TriggerJobs.tsx` 三页面消费方式还各异。**P1-6 文档已给出方案 A(完全合并)/ B(底层 service 合并)/ C(只补文档),Wave 2 应优先决策**。

### 5.3 与其他批的关联

- **批 2 ETL 任务管理**:涉及 `/api/scheduled-tasks` `/api/trigger-jobs`,**P1-6 合并方案落地需在批 2 同步动手**(改 `apps/backend/app/routers/trigger_jobs.py` 与前端 `triggerJobs.ts`)
- **批 3 任务执行/调度**:run-logs / batch-run 的 dispatcher 实际属于跨批共享基础设施
- **批 4 租户管理**:本批 P1 发现"tenant_admin 权限粒度"应在批 4 一并梳理多租户隔离方案
- **批 5 系统设置/日志**:audit log 表(本批多次提到的"切换审计/清理审计/ack 审计")应统一在批 5 设计

### 5.4 Wave 2-5 工单建议(本批衍生)

| 优先级 | 工单 | Wave | 范围 |
|---|---|---|---|
| P0 | 修复 `/admin/triggers/unified` 权限 | Wave 2 | 加 `_require_admin` |
| P0 | OpenAPI 同步修复 + CI 校验 | Wave 1 末 | 抓取脚本 + 守护测试 |
| P0 | 实施 P1-6 方案 A 合并双 PATCH | Wave 2 | 后端 router + 前端 API client |
| P0 | run-logs PII 脱敏分级 | Wave 2 | 按角色返回 prompt 字段 |
| P0 | runtime-context 切换幂等键 + audit 表 | Wave 2 | DB 迁移 + 路由改造 |
| P1 | batch-run 进度查询端点 | Wave 2 | 新增 `GET /batch-run/{id}/status` |
| P1 | dashboard 字段命名规范 | Wave 3 | 改 today_* → period_* |
| P1 | alerts 与 run-logs 合并 | Wave 3 | 路由 + 前端 |
| P1 | tenant_admin 权限分级 | Wave 4 | 路由 + tenant_id 关联 |
| P2 | 触发器 ID 稳定方案 | Wave 5 | 复合 ID 替代 ROW_NUMBER |

---

## 六、关联

- **OpenAPI 源**:`docs/contracts/openapi/backend-api.json`(本会话快照,2026-05-04;**本批发现存在 10+ 端点缺失,需修复**)
- **全局总表**:`docs/_overview/admin-api-prd/00-overview.md`(已索引 5 批拆分)
- **P1-6 三 API 合并**:`docs/_overview/04b-feedback/P1-6-trigger-api-merge.md`(方案 A/B/C 详)
- **P1-7 评估元文档**:`docs/_overview/04b-feedback/P1-7-admin-api-prd-evaluation.md`(总工作量 100-130h)
- **Runtime Context PRD**:`docs/prd/specs/P20-runtime-context-sandbox.md`(本会话同步新增,产品规格)
- **Backend CLAUDE.md**:`apps/backend/CLAUDE.md`(JWT 双认证 / 全局响应包装 / FDW 访问)
