建立项目级标杆文档 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 残留, 已修 commit17f045a) - P0-5 致命 2 (JWT aud 缺失, 已修 commit17f045a) - P0-6 clearAllTasks 守卫 (Wave 3) - P0-8 DBViewer 黑名单漏 (已修 commit17f045a) - 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
17 KiB
P1-6 触发器双 API 合并可行性
反馈背景:Neo 倾向合并双 API(
/admin/ai/triggersvs/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 → 嵌入 <AITriggers /> + <AIOperations /> + <AITriggerJobs /> 三个组件- 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.tsxTab "biz" 嵌套BizTriggersTab(与TriggerJobs.tsx几乎重复) - 聚合容器
TriggerManager.tsxTab "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 端必须先合并字段集才能合并 API;POST run 端 /admin/ai/triggers 本就没有,不冲突。
3.2 泛用性瓶颈
/trigger-jobs 的 PATCH 当前故意不暴露 status / description,原因推测:
- 业务触发器的启停可能影响 ETL 调度,需要更严格的权限控制(admin_ai 路由有
_require_admin()守卫) - description 是给后台管理员看的"说明",业务侧可能不希望随便改
如果直接合并,需要先决策:/trigger-jobs PATCH 是否允许改 status / description?
四、合并方案对比
方案 A · 完全合并(删除 /admin/ai/triggers)
步骤
- 扩展
apps/backend/app/schemas/trigger_jobs.py::UpdateTriggerConfigRequest加status/description字段 apps/backend/app/routers/trigger_jobs.py::update_trigger_config加白名单:仅当status_new in (enabled, disabled)时通过;description 直接 UPDATEGET /api/trigger-jobs加 query 参数?job_type_prefix=ai_,等价于/admin/ai/triggers的过滤apps/admin-web/src/api/adminAI.ts删除listTriggers/updateTrigger函数apps/admin-web/src/pages/AITriggers.tsx改用triggerJobs.ts的fetchTriggerJobs({ jobTypePrefix: "ai_" })+ 新 PATCH 函数- 删除后端
/admin/ai/triggers路由 - 同步收敛
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 合并)
步骤
- 新建
apps/backend/app/services/trigger_service.py,集中list_triggers(filter)/update_trigger(id, fields)两个内核函数 routers/admin_ai.py::list_triggers与routers/trigger_jobs.py::get_trigger_jobs都调用trigger_service.list_triggers(),参数不同- 两个 PATCH 端点都调用
trigger_service.update_trigger(...),前者传 status/description 子集,后者传 cron/interval 子集 - 前端代码不动
评估
| 维度 | 值 |
|---|---|
| 工作量 | 中(后端重构,前端零改动) |
| 回归范围 | 后端 service 层单测 + 路由集成测试 |
| 长期维护成本 | 中(API 两份仍在,但底层一致) |
| 风险点 | 没有真正解决 Neo 反馈的"双 API"困惑,前端开发仍要在两个 API 客户端之间选 |
方案 C · 不合并,只补文档边界
步骤
- 在
apps/backend/app/routers/admin_ai.py与trigger_jobs.py顶部 docstring 互引:"本路由 PATCH 仅改 status/description(业务/AI 都可),改 cron/interval 请用 /trigger-jobs" - 同步
apps/backend/CLAUDE.md增加一节"触发器 API 边界"
评估
| 维度 | 值 |
|---|---|
| 工作量 | 极低(< 50 行注释) |
| 长期维护成本 | 高(每次新人都要重新理解两 API 边界) |
五、推荐实施步骤
推荐方案 A(完全合并),分 4 阶段:
阶段 1 · 后端 API 扩展(D+0)
apps/backend/app/schemas/trigger_jobs.py::UpdateTriggerConfigRequest新增字段:model_validator 改为"四选一即可"status: Literal["enabled", "disabled"] | None = None description: str | None = Noneapps/backend/app/routers/trigger_jobs.py::update_trigger_config:- 增加 status / description UPDATE 分支
- 新增 query 参数
?job_type_prefix=strfor GET - 新增 query 参数
?include_event=bool(默认 true)便于按需排除
- 在 admin_ai.py 的
/admin/ai/triggersGET/PATCH 下加 deprecation header(Deprecation: true)
阶段 2 · 前端切换调用(D+1)
apps/admin-web/src/api/triggerJobs.ts::UpdateTriggerConfigReq加 status/descriptionapps/admin-web/src/api/triggerJobs.ts::fetchTriggerJobs加可选params: { job_type_prefix?: string }apps/admin-web/src/pages/AITriggers.tsx:- import 改为
triggerJobs.ts listTriggers()→fetchTriggerJobs({ job_type_prefix: 'ai_' })updateTrigger(id, body)→updateTriggerConfig(id, body)
- import 改为
- 收敛
pages/TriggerManager.tsx::BizTriggersTabcolumns 重复定义:直接import TriggerJobs或抽公共组件<TriggerJobsTable filter={...} />
阶段 3 · 删除旧 API(D+2,灰度后)
- 删除
apps/backend/app/routers/admin_ai.py中/triggers两个端点 - 删除
apps/backend/app/services/ai/admin_service.py::list_triggers/update_trigger - 删除
apps/admin-web/src/api/adminAI.ts中listTriggers/updateTrigger/TriggerItem/TriggerUpdateRequest - 跑全量回归(pytest backend + admin-web 端到端)
阶段 4 · 文档同步(D+2)
apps/backend/CLAUDE.md触发器章节加"统一 API: /api/trigger-jobs"- 写审计
docs/audit/changes/<日期>__trigger-api-merge.md - 顺手补:
/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/triggersGET/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 交互改动