# P1-6 触发器双 API 合并可行性 > 反馈背景:Neo 倾向合并双 API(`/admin/ai/triggers` vs `/trigger-jobs`), > "看下数据获取的泛用性,如果合适则合并"。 > 本文档基于现状代码评估泛用性、字段差异、合并方案与回归风险。 调研日期:2026-05-04 调研范围:`apps/backend/app/routers/{admin_triggers,trigger_jobs,admin_ai}.py` / `apps/backend/app/services/ai/admin_service.py` / `apps/admin-web/src/api/{adminAI,triggerJobs,triggers}.ts` / `apps/admin-web/src/pages/{AITriggers,TriggerJobs,TriggerManager}.tsx` / 测试库 `biz.{trigger_jobs,ai_trigger_jobs}` 实际数据 --- ## 一、双 API 字段对比(实际是 3 个 API) 调研中发现**当前实际有 3 个相关 API**,不是 2 个,先把全图列清楚: | API 路径 | 文件 | 数据源 | 用途 | |---|---|---|---| | `GET /api/trigger-jobs` | `routers/trigger_jobs.py` | `biz.trigger_jobs` | 通用:所有定时任务(业务+AI 混合) | | `PATCH /api/trigger-jobs/:id/config` | 同上 | `biz.trigger_jobs` | 通用:改 cron / interval | | `POST /api/trigger-jobs/:id/run` | 同上 | `biz.trigger_jobs` | 通用:手动执行 | | `GET /api/admin/ai/triggers` | `routers/admin_ai.py:400` | `biz.trigger_jobs` (filter `job_type LIKE 'ai_%' OR job_name='task_generator'`) | AI 视角:仅 AI 相关触发器 | | `PATCH /api/admin/ai/triggers/:id` | 同上 | `biz.trigger_jobs` | AI 视角:改 cron / status / description | | `GET /api/admin/triggers/unified` | `routers/admin_triggers.py` | 聚合 `biz.trigger_jobs` + `biz.ai_trigger_jobs` + `public.scheduled_tasks` | 只读:跨数据源全景视图 | **核心事实**: - `/admin/ai/triggers` 与 `/trigger-jobs` 操作的是**同一张表 `biz.trigger_jobs`**,前者只是加了 `WHERE job_type LIKE 'ai_%'` 过滤 - `/admin/triggers/unified` 是只读聚合视图,跨 3 张表,与上述两个 API 不同源 - `biz.ai_trigger_jobs` 是 AI 调用链历史记录表(事件实例),**不是触发器配置表**;当前测试库 0 条记录 ### 1.1 GET 响应字段对比 | 字段 | `/trigger-jobs` (TriggerJobItem) | `/admin/ai/triggers` (TriggerItem) | |---|---|---| | id | int | int | | job_name | string | string | | job_type | **string** | **string** | | trigger_condition | string | string | | trigger_config | dict\|null | dict(不可为 null) | | last_run_at | string\|null | string\|null | | next_run_at | string\|null | string\|null | | status | string | string | | description | string\|null | string\|null | | last_error | string\|null | string\|null | | **created_at** | **string\|null** ✅ | **未返回** ❌ | **差异 = 1 个字段**:`/admin/ai/triggers` 不返回 `created_at`。两个返回值**95% 重合**。 ### 1.2 PATCH 入参字段对比 | 字段 | `/trigger-jobs/:id/config` (UpdateTriggerConfigRequest) | `/admin/ai/triggers/:id` (TriggerUpdateRequest) | |---|---|---| | cron_expression | ✅ | ✅ | | interval_seconds | ✅ | ❌ | | status | ❌ | ✅ | | description | ❌ | ✅ | | 校验规则 | "至少一个字段" + cron 正则 + interval ≥ 1 | 无 model_validator,cron 正则用 `jsonb_set` 写入 | **差异显著**: - `/admin/ai/triggers` 支持改 status(启停)和 description,**`/trigger-jobs` 不支持** - `/trigger-jobs` 支持改 interval,**`/admin/ai/triggers` 不支持** - 这是**互补的字段集**,不是冗余 ### 1.3 排序与过滤差异 | 维度 | `/trigger-jobs` | `/admin/ai/triggers` | |---|---|---| | WHERE | 无 | `job_type LIKE 'ai_%' OR job_name = 'task_generator'` | | ORDER BY | `id` | `trigger_condition DESC, job_name` | ### 1.4 测试库实际数据(biz.trigger_jobs 9 行) ``` id job_name job_type condition status 1 task_generator task_generator cron enabled ← AI 列表也包含(job_name 命中) 2 task_expiry_check task_expiry_check interval enabled 3 recall_completion_check recall_completion_check event enabled 4 note_reclassify_backfill note_reclassify_backfill event enabled 57 ai_consumption_settled ai_consumption_settled event enabled ← AI 58 ai_note_created ai_note_created event enabled ← AI 59 ai_task_assigned ai_task_assigned event enabled ← AI 60 ai_dws_completed ai_dws_completed event enabled ← AI 61 ai_dws_prewarm_1000 ai_dws_prewarm cron enabled ← AI ``` `/admin/ai/triggers` 命中 6 行(5 个 ai_ 前缀 + 1 个 task_generator); `/trigger-jobs` 命中全部 9 行; `/admin/triggers/unified` 命中 9 行(biz)+ 0 行(ai_trigger_jobs,0 条记录)+ N 行 etl。 --- ## 二、前端依赖分析 ### 2.1 三个页面的数据消费 | 页面 | 数据源 | 消费的字段 | |---|---|---| | `pages/AITriggers.tsx` (独立) | `listTriggers()` → `/admin/ai/triggers` | id / job_name / trigger_condition / trigger_config.cron_expression / trigger_config.event_name / status / description / last_run_at / next_run_at / last_error | | `pages/TriggerJobs.tsx` (独立) | `fetchTriggerJobs()` → `/trigger-jobs` | id / job_name / trigger_condition / trigger_config.\*(cron+interval+event_name)/ status / description / last_run_at / next_run_at / last_error | | `pages/TriggerManager.tsx` (容器) | 4 个 Tab:
- `all` Tab → `fetchUnifiedTriggers()` → `/admin/triggers/unified`
- `biz` Tab → `fetchTriggerJobs()` → `/trigger-jobs`(**BizTriggersTab 内嵌**)
- `ai` Tab → 嵌入 `` + `` + `` 三个组件
- `etl` Tab → `fetchSchedules()` → ETL API | 按 Tab 分别展示 | ### 2.2 字段消费交集 `AITriggers.tsx` 与 `TriggerJobs.tsx` / `BizTriggersTab` 消费的字段**完全相同**(除 `created_at` 在 AITriggers 未消费)。 ### 2.3 操作能力消费 | 操作 | AITriggers (in TriggerManager AI Tab) | BizTriggersTab (in TriggerManager biz Tab) | TriggerJobs (独立页面) | |---|---|---|---| | 启停(status 切换) | ✅ Switch 组件 | ❌ | ❌ | | 改 cron | ✅ Modal | ✅ Modal | ❌(只有列表 + run) | | 改 interval | ❌ | ✅ Modal | ❌ | | 改 description | ✅ Modal | ❌ | ❌ | | 手动执行 | ❌ | ❌ | ✅ Button | | 清空所有任务 | ❌ | ❌ | ✅(业务专用) | **核心事实**:三个页面消费字段几乎一致,但**操作能力是互补的**。`/admin/ai/triggers` PATCH 比 `/trigger-jobs/:id/config` PATCH 多了 status / description,少了 interval;前端 UI 也按各自支持的字段开放对应控件。 ### 2.4 路由注册情况(admin-web 主导航) `apps/admin-web` 当前 4 处入口: - 独立页面 `TriggerJobs.tsx`(路径见路由表) - 独立页面 `AITriggers.tsx` - 聚合容器 `TriggerManager.tsx` Tab "biz" 嵌套 `BizTriggersTab`(与 `TriggerJobs.tsx` 几乎重复) - 聚合容器 `TriggerManager.tsx` Tab "ai" 嵌套 `` **已经有冗余**:`BizTriggersTab` 的 columns 与 `TriggerJobs.tsx` 重复定义。这是当前需要先收敛的 UI 层债务。 --- ## 三、数据获取泛用性评估 ### 3.1 三个 API 的"可替代性"矩阵 | 场景 | 可用 API | 评估 | |---|---|---| | 列出全部触发器(业务+AI) | `/trigger-jobs` ✅ / `/admin/triggers/unified`(聚合,包含 etl) | 单源场景下 `/trigger-jobs` 已够用 | | 仅看 AI 触发器 | `/admin/ai/triggers` ✅ / `/trigger-jobs` + 前端过滤 | 后端过滤省一次网络传输;前端过滤更灵活 | | 跨表全景(biz + ai_jobs + etl) | 仅 `/admin/triggers/unified` | 只读,无替代 | | 改 status / description | 仅 `/admin/ai/triggers` PATCH | 缺口(`/trigger-jobs` 不支持) | | 改 interval | 仅 `/trigger-jobs/:id/config` PATCH | 缺口(`/admin/ai/triggers` 不支持) | | 手动 run | 仅 `/trigger-jobs/:id/run` POST | 缺口 | **结论**:GET 端可合并(`/trigger-jobs?job_type_prefix=ai_` 即可替代 `/admin/ai/triggers`);PATCH 端**必须先合并字段集**才能合并 API;POST run 端 `/admin/ai/triggers` 本就没有,不冲突。 ### 3.2 泛用性瓶颈 `/trigger-jobs` 的 PATCH 当前**故意不暴露 status / description**,原因推测: 1. 业务触发器的启停可能影响 ETL 调度,需要更严格的权限控制(admin_ai 路由有 `_require_admin()` 守卫) 2. description 是给后台管理员看的"说明",业务侧可能不希望随便改 如果直接合并,需要先决策:**`/trigger-jobs` PATCH 是否允许改 status / description**? --- ## 四、合并方案对比 ### 方案 A · 完全合并(删除 `/admin/ai/triggers`) #### 步骤 1. 扩展 `apps/backend/app/schemas/trigger_jobs.py::UpdateTriggerConfigRequest` 加 `status` / `description` 字段 2. `apps/backend/app/routers/trigger_jobs.py::update_trigger_config` 加白名单:仅当 `status_new in (enabled, disabled)` 时通过;description 直接 UPDATE 3. `GET /api/trigger-jobs` 加 query 参数 `?job_type_prefix=ai_`,等价于 `/admin/ai/triggers` 的过滤 4. `apps/admin-web/src/api/adminAI.ts` 删除 `listTriggers` / `updateTrigger` 函数 5. `apps/admin-web/src/pages/AITriggers.tsx` 改用 `triggerJobs.ts` 的 `fetchTriggerJobs({ jobTypePrefix: "ai_" })` + 新 PATCH 函数 6. 删除后端 `/admin/ai/triggers` 路由 7. 同步收敛 `BizTriggersTab` 与 `TriggerJobs.tsx` 的 column 重复定义 #### 评估 | 维度 | 值 | |---|---| | 工作量 | 中(schemas/路由/前端 API 层/2 个组件改造) | | 回归范围 | AITriggers 页面端到端 + TriggerManager AI/biz Tab + TriggerJobs 独立页 + `__tests__/adminAiAppTypes.test.ts` 类的对齐测试 | | 长期维护成本 | 低(单源真相) | | 风险点 | 1)改 status / description 的权限放开后,业务触发器(id 1-4)可能被误改;建议加守卫"业务触发器 status 改动需二次确认"。2)admin-web 之外的调用方(如 mcp-server)需排查 | ### 方案 B · 字段子集合并(保留两 API,底层 service 合并) #### 步骤 1. 新建 `apps/backend/app/services/trigger_service.py`,集中 `list_triggers(filter)` / `update_trigger(id, fields)` 两个内核函数 2. `routers/admin_ai.py::list_triggers` 与 `routers/trigger_jobs.py::get_trigger_jobs` 都调用 `trigger_service.list_triggers()`,参数不同 3. 两个 PATCH 端点都调用 `trigger_service.update_trigger(...)`,前者传 status/description 子集,后者传 cron/interval 子集 4. 前端代码不动 #### 评估 | 维度 | 值 | |---|---| | 工作量 | 中(后端重构,前端零改动) | | 回归范围 | 后端 service 层单测 + 路由集成测试 | | 长期维护成本 | 中(API 两份仍在,但底层一致) | | 风险点 | 没有真正解决 Neo 反馈的"双 API"困惑,前端开发仍要在两个 API 客户端之间选 | ### 方案 C · 不合并,只补文档边界 #### 步骤 1. 在 `apps/backend/app/routers/admin_ai.py` 与 `trigger_jobs.py` 顶部 docstring 互引:"本路由 PATCH 仅改 status/description(业务/AI 都可),改 cron/interval 请用 /trigger-jobs" 2. 同步 `apps/backend/CLAUDE.md` 增加一节"触发器 API 边界" #### 评估 | 维度 | 值 | |---|---| | 工作量 | 极低(< 50 行注释) | | 长期维护成本 | 高(每次新人都要重新理解两 API 边界) | --- ## 五、推荐实施步骤 **推荐方案 A(完全合并)**,分 4 阶段: ### 阶段 1 · 后端 API 扩展(D+0) 1. `apps/backend/app/schemas/trigger_jobs.py::UpdateTriggerConfigRequest` 新增字段: ```python status: Literal["enabled", "disabled"] | None = None description: str | None = None ``` model_validator 改为"四选一即可" 2. `apps/backend/app/routers/trigger_jobs.py::update_trigger_config`: - 增加 status / description UPDATE 分支 - 新增 query 参数 `?job_type_prefix=str` for GET - 新增 query 参数 `?include_event=bool`(默认 true)便于按需排除 3. 在 admin_ai.py 的 `/admin/ai/triggers` GET/PATCH 下加 deprecation header(`Deprecation: true`) ### 阶段 2 · 前端切换调用(D+1) 1. `apps/admin-web/src/api/triggerJobs.ts::UpdateTriggerConfigReq` 加 status/description 2. `apps/admin-web/src/api/triggerJobs.ts::fetchTriggerJobs` 加可选 `params: { job_type_prefix?: string }` 3. `apps/admin-web/src/pages/AITriggers.tsx`: - import 改为 `triggerJobs.ts` - `listTriggers()` → `fetchTriggerJobs({ job_type_prefix: 'ai_' })` - `updateTrigger(id, body)` → `updateTriggerConfig(id, body)` 4. 收敛 `pages/TriggerManager.tsx::BizTriggersTab` columns 重复定义:直接 `import TriggerJobs` 或抽公共组件 `` ### 阶段 3 · 删除旧 API(D+2,灰度后) 1. 删除 `apps/backend/app/routers/admin_ai.py` 中 `/triggers` 两个端点 2. 删除 `apps/backend/app/services/ai/admin_service.py::list_triggers` / `update_trigger` 3. 删除 `apps/admin-web/src/api/adminAI.ts` 中 `listTriggers` / `updateTrigger` / `TriggerItem` / `TriggerUpdateRequest` 4. 跑全量回归(pytest backend + admin-web 端到端) ### 阶段 4 · 文档同步(D+2) 1. `apps/backend/CLAUDE.md` 触发器章节加"统一 API: /api/trigger-jobs" 2. 写审计 `docs/audit/changes/<日期>__trigger-api-merge.md` 3. 顺手补:`/admin/triggers/unified` 仍保留(跨表只读视图),但在 docstring 标明"如果只看 biz 表用 /trigger-jobs,跨 etl/ai_jobs 时再用 unified" --- ## 六、给 Neo 的决策清单 | 问 | 选项 | 推荐 | |---|---|---| | Q1 | 采纳方案 A(完全合并),还是方案 B(保留双 API 仅合并底层)? | **方案 A** | | Q2 | 合并后 `/trigger-jobs` PATCH 允许改 `status`,业务触发器(id 1-4,task_generator/task_expiry_check 等)是否需要"业务触发器禁止禁用"守卫? | 推荐加一个白名单:`PROTECTED_JOB_NAMES = {"task_generator"}`,禁用时 422 | | Q3 | `/admin/triggers/unified` 是否保留?(它聚合了 ai_trigger_jobs + scheduled_tasks,与单表 API 不同源) | 保留(跨数据源场景仍需要) | | Q4 | 合并后是否把独立页面 `pages/AITriggers.tsx` 也删掉,仅保留 `TriggerManager.tsx` 的 Tab 视图? | 推荐删(已被 TriggerManager 覆盖) | | Q5 | `pages/TriggerJobs.tsx` 独立页面是否也合并到 `TriggerManager.tsx`?路由表清理? | 推荐合并;路由表保留旧路径 redirect → TriggerManager?tab=biz | | Q6 | 灰度策略:直接 D+0 删除旧 API,还是 D+0 加 deprecation header → D+7 删除? | 推荐 D+0 加 header,D+2 删除(admin-web 是单仓库唯一调用方,可短窗口) | --- ## 附录:关键文件清单 ### 后端路由 - `apps/backend/app/routers/trigger_jobs.py` — 通用 trigger_jobs API(保留并扩展) - `apps/backend/app/routers/admin_ai.py:400-440` — `/admin/ai/triggers` GET/PATCH(待删除) - `apps/backend/app/routers/admin_triggers.py` — `/admin/triggers/unified` 跨数据源聚合(保留) ### 后端 service - `apps/backend/app/services/ai/admin_service.py:752-820` — `list_triggers` / `update_trigger`(待删除,逻辑迁移到 trigger_jobs 路由) - `apps/backend/app/services/trigger_scheduler.py:346` — `list_trigger_jobs`(保留,是基础设施) ### 后端 schema - `apps/backend/app/schemas/trigger_jobs.py` — `TriggerJobItem` / `UpdateTriggerConfigRequest`(扩展 status/description) - `apps/backend/app/schemas/admin_ai.py:250-269` — `TriggerItem` / `TriggerUpdateRequest`(待删除) - `apps/backend/app/schemas/admin_triggers.py:14` — `UnifiedTriggerItem`(保留) ### 前端 API - `apps/admin-web/src/api/triggerJobs.ts` — 主 API 客户端(扩展) - `apps/admin-web/src/api/adminAI.ts:337-366` — `listTriggers` / `updateTrigger` / `TriggerItem`(待删除) - `apps/admin-web/src/api/triggers.ts` — `fetchUnifiedTriggers`(保留) ### 前端页面 - `apps/admin-web/src/pages/TriggerManager.tsx` — 容器页(保留) - `apps/admin-web/src/pages/AITriggers.tsx` — 改用 triggerJobs API 后保留组件,删 listTriggers 调用 - `apps/admin-web/src/pages/TriggerJobs.tsx` — 独立页面(建议删除并 redirect) - `apps/admin-web/src/pages/AITriggerJobs.tsx` — 注意:这是 ai_trigger_jobs 历史日志页面,与本调研 API 无关(用 `/admin/ai/trigger-jobs` 是另一组端点) ### 数据库 - `db/zqyy_app/schemas/biz.sql:344-360` — `biz.trigger_jobs` 表定义 - `db/zqyy_app/schemas/biz.sql:48-150` — `biz.ai_trigger_jobs` 表定义(事件历史,与触发器配置不同) ### 测试 - `apps/admin-web/src/__tests__/adminAiAppTypes.test.ts` — 现有对齐测试(合并后扩展,加上 trigger PATCH 字段对齐用例) ### 审计参考 - `docs/audit/changes/2026-03-23__trigger-jobs-admin-web-miniprogram-cleanup.md` — 上次 trigger_jobs 改动 - `docs/audit/changes/2026-03-24__trigger-jobs-clear-task-interaction.md` — clear-all 交互改动