Files
Neo-ZQYY/docs/_overview/admin-api-prd/batch1-runtime-context-and-ai.md
Neo c58599d29b docs(prd): admin-web API 全景总览 + 批 1 PRD (W1-T7 / P1-7)
Wave 1 Day 4 admin-web 后端 API PRD 批 1 撰写。

00-overview.md (338 行):
- 151 端点 / 34 标签全清单(实际 vs P1-7 估算 80,多 71 个)
- 5 批 PRD 拆分映射
- OpenAPI 与代码不同步告警(本批缺 10+ 端点,Wave 5 修复抓取脚本)

batch1-runtime-context-and-ai.md (924 行):
- 23 端点 PRD: admin-ai 17 + runtime-context 5 + triggers 1
- 41 评估发现: P0x8 / P1x20 / P2x13
- 每端点带 file:line 引用 + 调用方定位

工作量修正: P1-7 估算 60-65h -> 实际 100-130h (按 5 批分散到 Wave 1-5)。

参考: docs/audit/changes/2026-05-04__wave1_t7_admin_api_prd_batch1.md
2026-05-04 09:54:35 +08:00

925 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 访问)