Files
Neo-ZQYY/docs/_overview/04b-feedback/P1-6-trigger-api-merge.md
Neo 509cf43284 chore(docs): Wave 0 调研产出 + P0/P1/P2 反馈调研
建立项目级标杆文档 docs/_overview/ 作为产品全景索引,
解决"PRD 零碎、文档膨胀、跨子系统调研无入口"的问题。

主要内容:
- 00-index 总索引 + 维护协议 + 与 CLAUDE.md 关系
- 01-product-overview 产品全景脑图(6 角色 / 6 子系统 / 数据流 /
  7 业务概念 / 8+1 AI 矩阵 / 22 术语)
- 02a-miniprogram-page-matrix 小程序 21 页业务指纹
- 02b-adminweb-page-matrix admin-web 19 路由业务指纹
- 03-test-spec 测试规范 (L1-L5 分层 + 走查模板 + 75-95 case 估算)
- 04-doc-conflicts 39 条冲突索引(P0×8 / P1×13 / P2×13 + 5 子项)
- 04a/b/c-conflicts-*-detail 业务故事卡(7 字段:关联/逻辑/影响/选项/判定)
- 05-orphan-pages-cleanup admin-web 6 孤儿页面处置(1 归档 + 4 保留)
- WAVES-MASTER-PLAN.md 全 Wave 主计划(0-5,共 22-32 工作日)
- WAVE-1-KICKOFF.md Wave 1 实施 kickoff
- GLOBAL-DECISION-DASHBOARD.md 全局决策仪表板

反馈调研产物:
- 04a-feedback/ P0 两轮反馈(8+8 项决策 + D-1/2/3 + F-1/2 子代理产出)
- 04b-feedback/ P1 两轮反馈(13+1+5 项 + E-1/2/3/4 + G-1/2 子代理产出)
- 04c-feedback/ P2 反馈(13 项 + 5 子项 + H-1/2/3 子代理产出)
- NEO-DECISIONS-LOG 累积决策记录

关键追加发现 8 处 D Bug(原蓝本 0):
- P0-3 看板沙箱接入(Wave 1 W1-T1)
- P0-5 致命 1 (4 处 fdw_etl 残留, 已修 commit 17f045a)
- P0-5 致命 2 (JWT aud 缺失, 已修 commit 17f045a)
- P0-6 clearAllTasks 守卫 (Wave 3)
- P0-8 DBViewer 黑名单漏 (已修 commit 17f045a)
- P1-3 task-detail 跳转传 task_id 而非 customer_id
- P2-7 board-finance 隐式 null
- 2 个独立 Bug (page_context.created_at + ClueCategory 字典)

参考: docs/_overview/00-index.md
2026-05-04 07:38:28 +08:00

300 lines
17 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.
# 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_validatorcron 正则用 `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_jobs0 条记录)+ 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<br>- `all` Tab → `fetchUnifiedTriggers()``/admin/triggers/unified`<br>- `biz` Tab → `fetchTriggerJobs()``/trigger-jobs`**BizTriggersTab 内嵌**<br>- `ai` Tab → 嵌入 `<AITriggers />` + `<AIOperations />` + `<AITriggerJobs />` 三个组件<br>- `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" 嵌套 `<AITriggers />`
**已经有冗余**`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 端**必须先合并字段集**才能合并 APIPOST 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 改动需二次确认"。2admin-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` 或抽公共组件 `<TriggerJobsTable filter={...} />`
### 阶段 3 · 删除旧 APID+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-4task_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 加 headerD+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 交互改动