feat(debug): 流程调试增强(全局调试/单步/节点mock/变更影响/旧mock兼容) - #768
Conversation
以 DebugContext 为中心重设计流程调试能力,覆盖全局调试、单步调试、 统一调试上下文、变更重置规则、mock 单步与失败注入、调试态可观测、 调试历史与输入复用等需求(req1-req14),并对标 Dify/Coze 调试体验。 Co-authored-by: Cursor <cursoragent@cursor.com>
基于 2026-06-24 设计文档,按 TDD 分步编排 req1-req14 的后端实现: Phase1 调试上下文模型与统一读、Phase2 全局调试、Phase3 单步与 mock 失败注入、Phase4 变更重置与旧 mock 兼容。落地三项关键决策: SecretSingleJsonField 加密、模型归属 bkflow.template、单步用微型 DEBUG 任务。 Co-authored-by: Cursor <cursoragent@cursor.com>
按计划自洽性评审修复 5 处问题: - 单步 real 用 get_node_id_map 精确定位活动 runtime id,避免误读 start/end 事件 - 单步 real 从 elapsed_time 落库 duration_ms,对齐 req12 - node_mock 改为纯配置,不再误标节点 finished/failed, mock 角标由 execution_mode 体现 - 补 sync 子节点结构(并行网关/子流程)校准注记 - global_run 的 inputs 初值注入升级为阻塞校准并补单测 新增 test_node_mock_does_not_mark_status、 test_step_run_real_targets_activity_and_records_duration 两个回归测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
代码评审发现 SecretSingleJsonField 遇嵌套 dict/list 会抛 ValueError, 而 inputs/outputs/mock_outputs/global_vars 等调试快照天然嵌套。 决策:这些快照不加密,统一用 models.JSONField;同步更新决策TencentBlueKing#1、Task1.1 模型与测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- reset_impact 不再 get_or_create,改为 filter().first(),无基线直接返回空,避免 VIEW 权限下写入 DebugContext - build_dependency_graph 深拷贝 constants,避免 classify_constants 就地污染共享树 - 补充数据流传播/删除节点/拓扑变更/无基线只读 四类用例 Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
DebugContext 的锁原本仅在 GET /debug/context 触发回写时释放;前端关闭或 轮询中断会让模板长期停留在 running/terminating 且无 reaper 释放。新增 _reclaim_stale_lock:持锁超过 TTL(BKFLOW_DEBUG_LOCK_TTL_SECONDS,默认 600s) 时以 CAS 原子复位为 idle 并尽力撤销孤儿任务,接入 global_run/reset/step_run/ node_mock/context_var 的并发预检。 Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
审查总结
本 PR 实现了完整的流程调试增强功能,架构清晰、测试覆盖充分。以下列出需关注的问题:
Important 级别 (3 条)
sync_from_debug_task中循环内逐条查询DebugNodeState(N+1)_apply_legacy_mock_scheme循环内逐条查询TemplateMockData(N+1)DebugViewSet序列化器缺少space_id字段,TemplateRelatedResourcePermission鉴权会 ValidationError
Minor 级别 (2 条)
mock_execute非失败路径会对TaskMockData发起两次重复 DB 查询sync_from_debug_task循环内逐条ns.save()可优化为bulk_update
整体设计扎实,CAS 抢锁/超时回收/孤儿清理/幂等重入均有考虑。建议优先修复权限字段缺失和 N+1 问题。
| acts_outputs = self._acts_outputs() | ||
|
|
||
| for tpl_node_id, runtime_id in id_map.items(): | ||
| ns = DebugNodeState.objects.filter(debug_context=ctx, node_id=tpl_node_id).first() |
There was a problem hiding this comment.
⚡ N+1 查询:循环内逐条 DebugNodeState.objects.filter(...).first()。建议在循环外一次性 {ns.node_id: ns for ns in DebugNodeState.objects.filter(debug_context=ctx)} 批量加载后按字典取值。
| """ | ||
| targets = self._legacy_scheme_nodes().intersection(new_node_ids) | ||
| for node_id in targets: | ||
| default_md = TemplateMockData.objects.filter( |
There was a problem hiding this comment.
⚡ N+1 查询:targets 集合循环内逐条查 TemplateMockData。建议批量查询 TemplateMockData.objects.filter(template_id=..., node_id__in=targets, is_default=True) 后用 dict 索引。
|
|
||
|
|
||
| class DebugViewSet(GenericViewSet): | ||
| permission_classes = [AdminPermission | SpaceSuperuserPermission | TemplateRelatedResourcePermission] |
There was a problem hiding this comment.
🔒 TemplateRelatedResourcePermission.has_permission 内部用 TemplateRelatedResourceSerializer 校验请求参数,该序列化器同时要求 space_id 和 template_id。但 Debug 各序列化器只传了 template_id,对依赖 token 鉴权的非 admin 用户会导致 400 ValidationError。需在 Debug 序列化器中补上 space_id 字段(或在 permission 内从 template_id 反查 space_id)。
| return True | ||
|
|
||
| def mock_execute(self, data, parent_data): | ||
| taskflow_id = parent_data.get_one_of_inputs("task_id") |
There was a problem hiding this comment.
✨ _get_mock_fail_info 和后续 get_mock_outputs 各自调用 get_taskflow_mock_data(两次 DB 查询同一行)。非失败节点走到第 83 行时会第二次查询。可考虑在 mock_execute 入口只查一次 mock_data 后分别取 fail_nodes 和 outputs。
| for out_key, var_key in acts_outputs.get(tpl_node_id, {}).items(): | ||
| if out_key in outputs: | ||
| ctx.global_vars[var_key] = outputs[out_key] | ||
| ns.save() |
There was a problem hiding this comment.
✨ 循环内逐条 ns.save() 可改为收集后 bulk_update(updated_nodes, ["status", "duration_ms", "log_ref", "outputs", "error_detail"]),减少 DB round-trip。
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #768 +/- ##
==========================================
+ Coverage 83.10% 83.49% +0.38%
==========================================
Files 307 312 +5
Lines 18167 19044 +877
==========================================
+ Hits 15098 15900 +802
- Misses 3069 3144 +75 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
faae4c2 to
87934c7
Compare
There was a problem hiding this comment.
增量审查总结
之前报告的 5 个问题(N+1 查询 ×2、权限 space_id 缺失、mock_execute 重复查询、循环内 ns.save())在本次提交中均未修复,不再重复评论。
新发现问题 (1 条 — 🚨 Critical)
get_node_id_map端点返回Response(mapping)裸 dict,但DebugService消费时按{"result": True, "data": {...}}协议解析(id_map_resp.get("result"))。两端格式不一致会导致sync_from_debug_task和_step_run_real中拿到的id_map永远为空,调试结果无法回写。需把视图响应统一为OperationResult格式,或修改 Service 端解析逻辑。
| task_instance = self.get_object() | ||
| activities = (task_instance.execution_data or {}).get("activities", {}) | ||
| mapping = {act.get("template_node_id", act_id): act_id for act_id, act in activities.items()} | ||
| return Response(mapping) |
There was a problem hiding this comment.
🚨 响应格式不匹配:此处返回裸 mapping dict,但 DebugService.sync_from_debug_task 和 _step_run_real 按 {"result": True, "data": {...}} 解析(id_map_resp.get("result"))。建议改为 return Response({"result": True, "data": mapping, "message": ""}) 或使用 OperationResult,与 get_states/operate 等端点保持一致。
There was a problem hiding this comment.
增量审查总结
本次提交(587e054)将 real 单步调试从同步轮询改为异步启动 + context 惰性同步,架构变更合理,测试覆盖充分(新增 4 个同步场景测试用例)。
之前报告的问题状态:
- 🚨
get_node_id_map响应格式不匹配 — 仍未修复,且影响范围扩大:_step_run_real重构后将get_node_id_map调用前移到start之前并移除了 fallback(原轮询器可在 runtime_id 为空时优雅降级),现在会直接触发DebugStateError("node id map missing"),导致 real 单步必定失败 - ⚡ N+1 查询(
sync_from_debug_task循环内逐条查DebugNodeState)— 未修复 - ⚡
ns.save()循环 — 未修复 - 其余 2 条 Minor — 未修复
以上不再重复留评。
新增代码审查结果:无新问题
本次变更质量良好:
- 状态模型拆分清晰(
status管锁、last_run_status管结果) _flatten_state_children用迭代 BFS 处理子流程嵌套,避免递归深度问题include_schedule按需查询DBSchedule,一次批量 filter 覆盖所有节点- 异常路径正确清理孤儿任务并释放锁,
_acquire_lock在 try 块外保证不误释他人锁 - Migration 与 Model 字段一一对应,默认值设置合理
|
已追加 stage 回归问题修复,最新提交: 修复内容:
master 基线验证:
|
There was a problem hiding this comment.
增量审查总结
本次提交(5deccad)修复了暂停节点 schedule_type 推断、网关 template_node_id 注入/映射、状态树 ex_data 缺失兜底、调试历史状态校准四个回归问题,代码逻辑清晰,测试覆盖充分。
之前报告的问题状态:
- 🚨
get_node_id_map响应格式不匹配 — ✅ 经复核此问题不存在:TaskInstanceViewSet继承自SimpleGenericViewSet,其finalize_response会自动将Response(mapping)包装为{"result": True, "data": mapping, ...},与DebugService消费端协议一致。测试test_node_id_map.py:91也验证了response.data["data"]可正确访问。 - ⚡ N+1 查询(
sync_from_debug_task循环内逐条查DebugNodeState、ns.save()循环)— 未修复,不再重复评论 - 其余 Minor — 未修复,不再重复评论
新增代码审查结果(1 条 Minor):
| "FAILED": "failed", | ||
| "REVOKED": "revoked", | ||
| "EXPIRED": "expired", | ||
| } |
There was a problem hiding this comment.
✨ status_map 建议提取为模块级常量(参照 ENGINE_RUN_STATE_MAP 的定义方式),避免每次 history() 调用重复创建 dict,也更易复用。
There was a problem hiding this comment.
增量审查总结
本次提交(cfc9466)将 TokenResourceValidator.task_exists 从 task_list 批量查询改为 get_task_detail 精确查询,并增加了 space_id + task_id 双重校验,逻辑更健壮,测试覆盖 4 个场景(同空间通过、跨空间拒绝、ID 不匹配拒绝、查询失败拒绝)。
之前报告的问题状态:
- ⚡ N+1 查询(
sync_from_debug_task/_apply_legacy_mock_scheme)— 未修复,不再重复评论 - ✨ 循环内
ns.save()/mock_execute重复查询 — 未修复,不再重复评论
新发现问题 (2 条 — Minor):
- 新增测试文件缺少开源协议头
- 测试中存在无效 mock setup(
client.task_list.return_value已不再被调用)
| @@ -0,0 +1,64 @@ | |||
| from unittest.mock import patch | |||
There was a problem hiding this comment.
✨ 新增 Python 文件缺少开源协议头(# -*- coding: utf-8 -*- 及 MIT 版权声明),项目规范要求所有新文件顶部须包含。
| @patch("bkflow.apigw.serializers.token.TaskComponentClient") | ||
| def test_task_exists_accepts_debug_task_in_same_space(self, mock_client_cls): | ||
| client = mock_client_cls.return_value | ||
| client.task_list.return_value = { |
There was a problem hiding this comment.
✨ client.task_list.return_value 设置无效——当前 task_exists 已改用 get_task_detail,此 mock 永远不会被调用,建议删除避免误导。
There was a problem hiding this comment.
增量审查总结
本次提交(4cef2aa)收敛调试终止后的活跃节点状态为 revoked,实现简洁正确:
DebugNodeState新增revoked状态枚举,附带正确的 migrationsync_from_debug_task在引擎状态为REVOKED时,批量将活跃节点(running/waiting/paused)update 为 revoked,避免逐条操作- 测试覆盖 3 种活跃态(running、waiting/poll、paused)全部通过参数化验证
之前报告的问题状态:
- ⚡ N+1 查询(
sync_from_debug_task循环内逐条查DebugNodeState、ns.save()循环)— 未修复,不再重复评论 - ⚡
_apply_legacy_mock_schemeN+1 — 未修复,不再重复评论 - ✨ 其余 Minor — 未修复,不再重复评论
新增代码审查结果:无新问题
本次变更质量良好,逻辑正确且与现有 ENGINE_RUN_STATE_MAP / ENGINE_FINISHED_STATES 配置一致,_release_lock 会在终态时正确释放锁回到 idle。
There was a problem hiding this comment.
增量审查总结
本次提交(6403def)为 sync_from_debug_task 的早返回路径增加了遗留节点状态修复逻辑:当调试锁已释放(idle)但上次运行为 revoked 时,批量将残留的活跃节点(running/waiting/paused)收敛为 revoked。
代码质量评估:
- 逻辑正确且幂等,与 line 574-578 的正常路径 revoked 清理保持一致
- 使用批量
update()而非循环逐条操作,符合规范 - 测试用例完整覆盖场景(idle + revoked + stale waiting node),并验证 task_client 未被调用
之前报告的问题状态:
- ⚡ N+1 查询(
sync_from_debug_task循环内逐条查DebugNodeState+ns.save())— 未修复,不再重复评论 - ⚡
_apply_legacy_mock_schemeN+1 — 未修复,不再重复评论
新增代码审查结果:无新问题 ✅
There was a problem hiding this comment.
增量审查总结
本次提交(0d92d70)在 build_context_view 中补充了 mock_outputs 字段的回显,逻辑简洁正确:仅当节点处于 mock 模式且 mock_result == "success" 时才返回 mock_outputs,否则返回 None。测试覆盖了 success 预设回填和 fail 预设屏蔽两个分支。
之前报告的问题状态:
- ⚡
sync_from_debug_task循环内逐条查DebugNodeState(N+1)— 仍未修复,不再重复 - ⚡
_apply_legacy_mock_scheme循环内逐条查TemplateMockData(N+1)— 仍未修复,不再重复 - ✨ 循环内
ns.save()可改bulk_update— 仍未修复,不再重复 - ✨
status_map提取为模块级常量 — 仍未修复,不再重复
新发现问题:无
本次变更无新增问题,代码质量良好。✅
Token 资源校验修复(2026-07-23)
DEBUG调试任务因默认任务列表过滤而无法申请任务 Token 的问题。space_id。调试终止状态收敛(2026-07-30)
REVOKED时,将仍处于running、waiting、paused的节点统一收敛为节点级revoked。已释放调试上下文自愈(2026-07-30)
waiting/running/paused,导致前端继续显示“调试中”的问题。idle + last_run_status=revoked上下文执行节点状态自愈,不再访问引擎,不影响正常运行任务。调试 Mock 输出回显(2026-07-30)
debug_context.nodes[]新增mock_outputs字段。null,支持页面刷新后回填。