主题
子任务节点动画系统
本文引用的源码文件
本文引用的文件
目录
引言
本文件聚焦“子任务节点动画系统”,即 Chrome 扩展侧边栏中,子任务执行时如何展示“当前正在执行的节点”、如何区分成功/失败/部分完成状态,以及如何在取消或完成后让动效正确停止。该系统横跨三个层面:
- 后台执行层:负责子任务计划、节点执行、进度回写与超时控制。
- 界面渲染层:负责把子任务清单、节点列表、当前执行节点、统计按钮等渲染到 DOM。
- 样式与动效层:通过 CSS 变量 Token 体系实现呼吸底、左侧光条、旋转环、图标脉冲等动效,并保证在取消/完成后自动静止。
该系统的目标是让用户在几十上百个节点中,一眼看出“到底哪一条在跑”,同时避免“取消后还在转圈”“已完成节点误以为仍在运行”等常见交互问题。
项目结构定位
子任务节点动画系统主要位于以下路径:
| 层级 | 文件 | 职责 |
|---|---|---|
| 后台执行 | src/background/react-loop.js | 生成子任务计划、执行子任务、记录部分完成进度、计算超时预算 |
| 界面逻辑 | src/side_panel/chat-manager.js | 解析子任务清单、渲染待执行行/活动行/节点列表、处理过滤点击、分类最终结果 |
| 流式清理 | src/side_panel/chat-streaming.js | 取消流时的 UI 收尾,包括摘掉活动态、还原静态状态位 |
| 样式与动效 | src/side_panel/styles.css | 定义子任务卡片、节点行、当前节点强调、部分完成、reduced-motion 降级 |
| 回归测试 | test/unit/subtask-current-node-anim.unit.test.js | 验证当前节点动效作用域、取消链路、主题 Token 使用 |
| 语义修复测试 | test/unit/subtask-partial-outcome.unit.test.js | 验证“部分完成”三态与失败上报摘要 |
| 超时预算测试 | test/unit/subtask-timeout-budget.unit.test.js | 验证子任务超时放开、中断原因口径、已完成节点摘要回灌 |
graph TB RL["后台执行<br/>react-loop.js"] --> CM["界面渲染<br/>chat-manager.js"] CM --> CS["流式清理<br/>chat-streaming.js"] CM --> STY["样式与动效<br/>styles.css"] TEST1["动效回归测试<br/>subtask-current-node-anim.unit.test.js"] --> CM TEST1 --> STY TEST2["部分完成语义测试<br/>subtask-partial-outcome.unit.test.js"] --> CM TEST3["超时预算测试<br/>subtask-timeout-budget.unit.test.js"] --> RL
图表来源
- src/background/react-loop.js#L2902-L2989
- src/side_panel/chat-manager.js#L803-L902
- src/side_panel/chat-streaming.js#L2160-L2186
- src/side_panel/styles.css#L4053-L4430
- test/unit/subtask-current-node-anim.unit.test.js#L1-L115
- test/unit/subtask-partial-outcome.unit.test.js#L1-L21
- test/unit/subtask-timeout-budget.unit.test.js#L1-L24
章节来源
核心组件
子任务节点动画系统由四个关键能力组成:
子任务计划解析与权威清单
从后台下发的执行日志中提取nodeType='subtask_plan'的权威子任务清单,用于界面展示“计划了哪些、还没执行到哪些”。节点过滤与统计
提供“✓N / ✗N / ⊘N”三类统计按钮,支持切换过滤状态;同一状态再点一次取消过滤,点击另一状态切换过滤。当前执行节点动效
当子任务处于活动态时,当前执行节点的状态位从静态“○”变为旋转环,整行出现呼吸底和左侧光条,节点图标有脉冲效果,文字加粗提亮。取消与完成后的动效收敛
取消流时移除.subtask-progress-active,并把旋转环替换为静态“○”;完成或失败时也移除活动态,避免已完成节点继续动效。
章节来源
- src/side_panel/chat-manager.js#L803-L902
- src/side_panel/chat-streaming.js#L2160-L2186
- src/side_panel/styles.css#L4111-L4171
架构总览
子任务节点动画系统的数据与控制流如下:
sequenceDiagram participant User as "用户" participant Panel as "侧边栏界面<br/>chat-manager.js" participant Stream as "流式处理<br/>chat-streaming.js" participant Background as "后台执行<br/>react-loop.js" participant Styles as "样式系统<br/>styles.css" User->>Panel : 查看子任务卡片 Panel->>Background : 读取执行日志/子任务计划 Background-->>Panel : subtask_plan + 节点状态更新 Panel->>Panel : 渲染待执行行/活动行/节点列表 Panel->>Styles : 添加 .subtask-progress-active Styles-->>Panel : 显示旋转环/呼吸底/左侧光条 User->>Stream : 点击停止/取消 Stream->>Panel : finalizeCancelledStream Stream->>Styles : 移除 .subtask-progress-active Stream->>Panel : 状态位还原为静态 ○
图表来源
- src/side_panel/chat-manager.js#L829-L902
- src/side_panel/chat-streaming.js#L2160-L2186
- src/background/react-loop.js#L2958-L2989
- src/side_panel/styles.css#L4111-L4171
详细组件分析
1. 子任务计划与节点渲染
后台在执行主任务时,会生成权威子任务清单,并通过执行日志下发给侧边栏。界面侧通过解析 subtask_plan 消息,构建子任务条目数组,并按 index 升序插入容器,保证视觉顺序等于计划顺序。
关键点:
- 权威清单来自后台,不是前端临时拼凑。
- 待执行行和活动行可能以不同顺序到达,因此必须按
data-subtask-index排序插入。 - 未命中 plan 的消息或旧历史会自动降级为无清单模式。
flowchart TD
Start(["收到执行日志"]) --> Extract["提取 subtask_plan 清单"]
Extract --> HasPlan{"是否命中权威清单?"}
HasPlan --> |是| BuildItems["构建子任务条目数组"]
HasPlan --> |否| Fallback["降级为无清单模式"]
BuildItems --> InsertOrder["按 index 升序插入容器"]
InsertOrder --> RenderPending["渲染待执行行"]
RenderPending --> RenderActive["渲染活动行/节点列表"]
Fallback --> End(["结束"])
RenderActive --> End
图表来源
章节来源
2. 节点过滤与统计
节点过滤由两个纯函数驱动:
| 函数 | 输入 | 输出 | 行为 |
|---|---|---|---|
toggleNodeFilter | 当前过滤状态、点击的目标状态 | 新过滤状态 | 同一状态再点取消,点击另一状态切换 |
applyNodeFilter | 节点数组、过滤状态 | 过滤后的节点数组 | success/failed 精确匹配,other 保留既非成功也非失败的节点 |
UI 上,统计按钮带有 data-node-filter,点击后:
- 切换
_nodeFilter - 若激活过滤,自动展开子任务卡片
- 调用
_renderNodes重新渲染节点列表
flowchart TD
Click["点击 ✓N/✗N/⊘N"] --> Toggle["toggleNodeFilter(current, clicked)"]
Toggle --> FilterState{"是否激活过滤?"}
FilterState --> |是| Expand["添加 subtask-expanded"]
FilterState --> |否| Collapse["保持或收起"]
Expand --> Render["_renderNodes()"]
Collapse --> Render
Render --> Apply["applyNodeFilter(nodes, filter)"]
Apply --> UpdateDOM["更新节点显隐"]
图表来源
章节来源
3. 当前执行节点动效
当前执行节点动效的核心约束是:动效只应在子任务卡片处于活动态时生效。如果基础规则自带 animation,会导致已完成子任务中的未完成节点一直转圈;如果取消链路不摘掉活动态,用户点了停止后界面仍会假装在跑。
动效包含四层:
| 动效 | 触发条件 | 表现 |
|---|---|---|
| 状态位旋转环 | 子任务活动态 + 节点 processing | 用 <span class="subtask-node-spinner"> 替代静态 “○” |
| 整行呼吸底 | 子任务活动态 + 节点 processing | 背景色与左侧光条渐变 |
| 节点图标脉冲 | 子任务活动态 + 节点 processing | 图标透明度与缩放变化 |
| 文字加粗提亮 | 子任务活动态 + 节点 processing | 当前节点名称更醒目 |
CSS 中所有颜色均使用主题 Token(如 var(--brand-violet-rgb)),不使用硬编码十六进制色值,从而兼容亮色、暗色和跟随系统主题。
classDiagram
class SubtaskCard {
+string status
+boolean isRunning
+Array nodes
}
class NodeItem {
+string status
+string name
+boolean isProcessing
}
class Spinner {
+boolean isActive
+animationDuration
}
class BreatheEffect {
+backgroundGradient
+leftGlow
}
class IconPulse {
+opacityAnimation
+scaleAnimation
}
SubtaskCard --> NodeItem : "包含多个节点"
SubtaskCard --> Spinner : "活动态时启用"
SubtaskCard --> BreatheEffect : "processing 节点高亮"
SubtaskCard --> IconPulse : "processing 节点图标脉冲"
图表来源
章节来源
4. 取消链路与动效收敛
取消流程由 finalizeCancelledStream 负责。它需要完成三件事:
- 移除所有
.subtask-progress-active,使呼吸底和旋转环停止。 - 把所有
.subtask-node-spinner替换为静态 “○”,避免“取消后还在转圈”。 - 移除其他 streaming 相关动画元素,并标记消息为已取消。
flowchart TD Cancel["用户点击停止"] --> Finalize["finalizeCancelledStream(element)"] Finalize --> RemoveActive["移除 .subtask-progress-active"] RemoveActive --> ReplaceSpinner["替换 .subtask-node-spinner 为 ○"] ReplaceSpinner --> CleanupOther["移除其他 spinner 与 streaming 类"] CleanupOther --> Done(["取消完成"])
图表来源
章节来源
- src/side_panel/chat-streaming.js#L2160-L2186
- test/unit/subtask-current-node-anim.unit.test.js#L99-L114
5. 部分完成语义与失败上报
“部分完成”是一个重要语义修复:当内层 ReAct 循环抛异常但多数节点已成功时,不应简单显示红色“失败”,而应显示橙色“部分完成”,并附带真实中断原因。同时,失败上报给父模型的 Result 中附加已完成节点的产出摘要,避免父模型“带着失败继续”却完全不知道已完成的工作。
关键实现点:
classifySubtaskOutcome根据整体状态、成功节点数、总节点数判断是否为部分完成。- 阈值默认 0.8,即成功率 ≥80% 且整体失败时,UI 显示“部分完成”。
- 后台通过
recordSubtaskPartialProgress滚动写入已完成节点摘要,随父 checkpoint 落盘。
flowchart TD
Input["子任务结果<br/>status + succeedNodes + totalNodes"] --> Classify["classifySubtaskOutcome"]
Classify --> Success{"success ?"}
Success --> |是| MarkDone["标记为成功"]
Success --> |否| CheckRate{"succeedNodes / totalNodes >= 0.8 ?"}
CheckRate --> |是| Partial["标记为部分完成"]
CheckRate --> |否| Fail["标记为失败"]
Partial --> Summary["buildSubtaskPartialSummary"]
Fail --> Summary
Summary --> ParentReport["上报父模型 Result"]
图表来源
- src/side_panel/chat-manager.js#L760-L760
- src/background/react-loop.js#L547-L562
- test/unit/subtask-partial-outcome.unit.test.js#L1-L21
章节来源
- src/side_panel/chat-manager.js#L760-L760
- src/background/react-loop.js#L547-L562
- test/unit/subtask-partial-outcome.unit.test.js#L1-L21
6. 子任务超时预算与中断原因
早期版本中,子任务曾被硬编码的 15 分钟上限封顶,导致设置页“整体循环超时”对子任务无效。修复后:
- 子任务超时上限只由配置值决定。
- 父任务可通过
apiParams.subtaskLoopTimeoutMs注入更紧预算。 - 中断文案需包含实际耗时与完整豁免项(澄清等待、敏感操作确认等待)。
- 子任务不存自己的 checkpoint,中断即整体失败由父任务重试;因此已完成节点摘要必须回灌,否则重跑等于从零重做。
flowchart TD
Config["设置页 loopTimeout"] --> Compute["computeSubtaskLoopTimeoutMs"]
ParentBudget["父任务剩余时间"] --> MinCheck{"是否注入更紧预算?"}
MinCheck --> |是| UseParent["取 min(loopTimeout, subtaskLoopTimeoutMs)"]
MinCheck --> |否| UseConfig["使用配置值"]
UseParent --> Timeout["子任务超时判定"]
UseConfig --> Timeout
Timeout --> Reason["生成中断原因文案"]
Reason --> Replay["已完成节点摘要回灌"]
图表来源
章节来源
依赖关系分析
子任务节点动画系统的依赖关系可以概括为:
- chat-manager.js 依赖后台下发的执行日志,负责解析、渲染、交互。
- chat-streaming.js 依赖 chat-manager 生成的 DOM,负责取消时的清理。
- styles.css 被 chat-manager 和 chat-streaming 共同消费,提供视觉与动效。
- react-loop.js 是数据源,负责子任务计划、执行、进度、超时。
- 单元测试 通过读取源码片段断言行为,属于“源码级回归测试”,而非运行时集成测试。
graph LR RL["react-loop.js"] --> CM["chat-manager.js"] CM --> CS["chat-streaming.js"] CM --> STY["styles.css"] TEST1["subtask-current-node-anim.unit.test.js"] --> CM TEST1 --> STY TEST2["subtask-partial-outcome.unit.test.js"] --> CM TEST3["subtask-timeout-budget.unit.test.js"] --> RL
图表来源
- src/background/react-loop.js#L2902-L2989
- src/side_panel/chat-manager.js#L803-L902
- src/side_panel/chat-streaming.js#L2160-L2186
- src/side_panel/styles.css#L4053-L4430
- test/unit/subtask-current-node-anim.unit.test.js#L1-L115
- test/unit/subtask-partial-outcome.unit.test.js#L1-L21
- test/unit/subtask-timeout-budget.unit.test.js#L1-L24
章节来源
- src/side_panel/chat-manager.js#L803-L902
- src/side_panel/chat-streaming.js#L2160-L2186
- src/side_panel/styles.css#L4053-L4430
性能与可访问性
性能考虑
- 动效全部基于 CSS,不依赖 JavaScript 动画循环,减少 JS 主线程压力。
- 旋转环、呼吸底、图标脉冲仅在
.subtask-progress-active下生效,避免已完成节点持续消耗 GPU。 - reduced-motion 媒体查询下关闭呼吸底和图标脉冲,仅保留常亮高亮,降低对低性能设备的负担。
可访问性考虑
- 使用
prefers-reduced-motion: reduce提供降级方案,确保对动画敏感的用户仍能感知“当前节点”。 - 颜色全部走主题 Token,避免硬编码色值导致暗色模式下对比度不足。
- 统计按钮具备 hover 与 active 反馈,提升可点击性。
章节来源
- src/side_panel/styles.css#L4158-L4171
- src/side_panel/styles.css#L4348-L4377
- test/unit/subtask-current-node-anim.unit.test.js#L85-L96
故障排查指南
| 现象 | 可能原因 | 排查位置 | 修复建议 |
|---|---|---|---|
| 取消后节点仍旋转 | 未移除 .subtask-progress-active 或未替换 .subtask-node-spinner | chat-streaming.js 的 finalizeCancelledStream | 确保移除活动态并将旋转环替换为静态 “○” |
| 已完成节点一直呼吸 | 基础规则自带 animation,未限定在 .subtask-progress-active | styles.css 中 .subtask-node-spinner 与 .subtask-node.processing | 将 animation 移到 .subtask-progress-active 作用域 |
| 无法区分当前节点 | 缺少旋转环/呼吸底/左侧光条 | styles.css 当前节点强调块 | 检查 keyframes 与作用域是否正确 |
| 部分完成显示为失败 | classifySubtaskOutcome 阈值或状态判断错误 | chat-manager.js | 核对 succeedNodes/totalNodes 与阈值 0.8 |
| 子任务 15 分钟被杀 | 仍受硬编码超时限制 | react-loop.js 子任务超时计算 | 确认放开硬编码上限,改用配置值 |
| 重跑子任务从零开始 | 未完成节点摘要未回灌 | react-loop.js 的 recordSubtaskPartialProgress | 确保随父 checkpoint 落盘 |
章节来源
- src/side_panel/chat-streaming.js#L2160-L2186
- src/side_panel/styles.css#L4111-L4171
- src/side_panel/chat-manager.js#L760-L760
- src/background/react-loop.js#L547-L562
- src/background/react-loop.js#L1421-L1431
结论
子任务节点动画系统通过“后台执行 + 界面渲染 + 样式动效 + 回归测试”四层协作,解决了用户在大量子任务节点中难以识别“当前执行节点”的问题。其设计重点在于:
- 作用域隔离:动效只在
.subtask-progress-active下生效,避免已完成节点误动。 - 取消收敛:取消链路明确摘掉活动态并还原静态状态位。
- 语义清晰:部分完成与失败分离,失败上报附带已完成节点摘要。
- 主题合规:所有颜色使用 Token,兼容亮/暗/跟随系统主题。
- 测试保障:通过源码级单测锁定关键行为,防止动效回归。
这套机制不仅提升了用户体验,也为后续扩展更多子任务可视化能力提供了稳定基础。