Files
deerflow-code/web-code/components/angentchat/doc.md
T
2026-05-27 19:43:15 +08:00

827 lines
39 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.
# 6 步法推演问答页面 — 开发说明文档
> 所在目录:`src/components/angentchat/`(主页面 `SixStepChatPage.tsx`)
> 路由地址:`/qa-center/sixstep-chat`
> 菜单入口:问答中心 → **6bf**(侧栏文案见 `src/components/Sidebar.tsx`)
> 同模块相关路由:`/qa-center/sixstep-agents`、`/qa-center/sixstep-agent-edit`
> **最后更新(摘要)**:2026-05-12 — 流式 **`error_log`** / **`errors[]`** 与 `StreamErrorNotice`(后端 skill 失败摘要可含异常片段);`buildLayout` 根节点 **`rootTitle` / `rootDetail`**(已选专家);**`SixStepFlowFullscreen`** 父节点 **收起/展开子树**(过渡动画、缩略图随可见节点);**`/observability`** 独立全站路由。
---
## 一、功能概述
「6bf 推演问答」由 `SixStepChatPage` 对接后端 **SixStep MCP Backend**。主链路通过 **`POST /api/orchestrate/stream`** 统一流式返回:后端先做 **意图识别**(`intent` 事件),再分支为 **查找专家**(`expert_*`)、**6 步法编排**(信息准备 → 目的 → 行为体 → 预期行为与驱动因素 → 叙事 → 最终结果)、**追问**(`follow_up`,`delta` 流式 Markdown)或 **澄清**(`clarify`)。支持 **结构化卡片** 与 **Markdown** 输出切换;结构化模式下可打开 **流程图沙箱 / 全屏流程图**(`SixStepFlowSandbox` / `SixStepFlowFullscreen`),展示编排返回的实体关系。
单 MCP 调试仍可直接调用 **`POST /api/agents/{key}/invoke`**(例如 `find_expert`、`create_expert`);6bf 主页面以编排流为主,专家查找也可由流内 `intent` 分支触发。对接细节见同目录 **`INTENT_INTEGRATION.md`**。
页面布局为 **顶栏 + 下方主体**:顶栏含页面标题、`SixStepProgressHorizontal`(横向主/子步骤进度)、以及结构化·Markdown、聊天记录、流程图沙箱等控制;主体为 **可收起的会话列表轨**(`SixStepChatHistory`)、**中间对话区与输入**(`SixStepConversation` + `ChatInputComposer`,含专家匹配与 MCP 选择弹窗)、以及 **可选的右侧沙箱**(宽度可拖拽,可关闭)。全屏流程图仍以 `fixed` 遮罩覆盖视口,独立于上述布局。
```
┌──────────────────────────────────────────────────────────────────┐
│ 顶栏:标题 · SixStepProgressHorizontal · 输出格式 · 聊天记录 · 流程图 │
├───┬────────────────────────────────────────────────────┬─────────┤
│会话│ SixStepConversation(多轮结构化阶段) │ 流程图 │
│列表│ + ChatInputComposer │ 沙箱 │
│轨 │ │(可关闭) │
└───┴────────────────────────────────────────────────────┴─────────┘
```
---
## 二、目录结构与各文件职责
```
src/components/angentchat/
│
├── types.ts # 数据模型类型(含 phase / intent / rawSteps 等)
├── mockData.ts # 本地模拟流式控制器(与 api 侧事件对齐,便于离线演示)
├── api.ts # runIntentOrchestrateStream、runSixStepDeduction、runExpertSearch/Create
├── flowLayout.ts # SVG 流程图布局
├── styles.css # 动画与工具类
│
├── SixStepChatPage.tsx # 推演主页面(状态中枢)
├── SixStepProgressHorizontal.tsx# 顶栏横向主/子步骤进度(主页面使用)
├── SixStepProgress.tsx # 纵向进度面板(组件保留;布局以横向为准)
├── SixStepChatHistory.tsx # 会话列表轨(展开/收起)
├── SixStepConversation.tsx # 对话区(阶段卡片 + 专家匹配)
├── SixStepCards.tsx # 阶段卡片原子组件
├── ExpertMatchCards.tsx # 专家匹配卡片
├── McpSelectModal.tsx # MCP 多选确认弹窗
├── StreamErrorNotice.tsx # 助手消息底部:汇总 SSE error_log / 网络类错误的可折叠提示条
├── SixStepFlowSandbox.tsx # 右侧流程图沙箱
├── SixStepFlowFullscreen.tsx # 全屏流程图
├── AgentCardList.tsx # 路由 sixstep-agents:MCP 卡片列表
├── AgentEditPage.tsx # 路由 sixstep-agent-edit:单 MCP 调试编辑
├── AgentChatMessageList.tsx # 智能体对话消息列表(编辑页等)
├── INTENT_INTEGRATION.md # 意图流与前后端字段说明(对接备忘)
└── doc.md # 本文档
```
---
## 三、数据模型(`types.ts`)
所有类型均从此文件导出,是各组件之间传递数据的唯一契约。
### 3.1 基础枚举
| 类型 | 取值 | 说明 |
|------|------|------|
| `MainStepKey` | `'info' \| 'sixstep' \| 'final'` | 三个主步骤的键名 |
| `SubStepKey` | `'purpose' \| 'actor' \| 'behavior' \| 'narrative'` | 6步法的四个子阶段键名 |
| `StepStatus` | `'pending' \| 'running' \| 'done'` | 步骤状态,驱动进度面板和动画 |
| `SixStepPhase` | `'idle'` \| `'intent-detecting'` \| `'expert-match'` \| `'expert-confirm'` \| `'generate'` \| `'paused'` \| `'completed'` \| `'clarifying'` \| `'follow-up'` | 助手消息所处交互阶段(与编排流、停止、追问等配合) |
| `SelectableMcpKey` | `'purpose' \| 'actor' \| 'behavior_factor' \| 'narrative'` | MCP 弹窗可选子链,会随请求传给后端 `selected_mcps` |
`BackendStepResult`:与后端 `step_end` 原始结构对齐,用于 **`continue_analysis`** 时把 `last_steps` 回传编排接口;`SixStepMessage.rawSteps` 在流式过程中累积,`resumedFrom` 对应 **`resume`** 事件中的继续起点。
### 3.2 卡片数据结构
每类卡片都有独立接口,并通过外键(`purposeId`、`actorId`、`driverId`)建立父子关联,构成树形层级关系。
```
PurposeCard (目的)
└─ ActorCard.purposeId → PurposeCard.id
└─ BehaviorDriverCard.actorId → ActorCard.id
└─ NarrativeCard.driverId → BehaviorDriverCard.id
```
| 接口 | 字段 | 说明 |
|------|------|------|
| `PurposeCard` | `id, index, title, reason` | 目的卡片,包含标题和推演理由 |
| `ActorCard` | `id, index, purposeId, title, content` | 行为体卡片,`purposeId` 关联所属目的 |
| `BehaviorDriverCard` | `id, index, actorId, expectedBehavior, driver` | 预期行为 + 驱动因素二合一卡片,`actorId` 关联所属行为体 |
| `NarrativeCard` | `id, index, driverId, title, reason` | 叙事卡片,`driverId` 关联所属驱动因素(`BehaviorDriverCard.id`) |
### 3.3 阶段数据结构
每个子阶段有独立的 `*StageData` 接口:
```typescript
interface PurposeStageData {
thinking: string; // 模型思考过程文本(流式填充)
cards: PurposeCard[]; // 已生成的卡片列表(逐张追加)
complete: boolean; // 该阶段是否完全结束
}
// ActorStageData / BehaviorStageData / NarrativeStageData 结构类似
```
### 3.4 消息总线:`SixStepMessage`
这是整个页面最核心的数据结构,一条助手消息包含所有推演状态:
```typescript
interface SixStepMessage {
id: string;
role: 'user' | 'assistant';
content?: string;
timestamp: Date;
outputFormat?: 'structured' | 'markdown';
markdownContent?: string;
stages?: SixStepStages;
finalAnswer?: string;
mainStepStatus?: Record<MainStepKey, StepStatus>;
subStepStatus?: Record<SubStepKey, StepStatus>;
currentSubStep?: SubStepKey;
currentMainStep?: MainStepKey;
phase?: SixStepPhase;
expertMatch?: ExpertMatchData;
selectedExpert?: ExpertCard;
selectedMcps?: SelectableMcpKey[];
intent?: { name: string; confidence?: number; reason?: string };
clarifyQuestion?: string;
rawSteps?: BackendStepResult[];
resumedFrom?: string;
effectiveUserInput?: string;
errors?: StreamErrorItem[];
}
```
**`errors` / `StreamErrorItem`**:`runIntentOrchestrateStream` 收到 SSE **`error_log`** 时追加;致命失败仍会走 **`error`** 事件并映射为同类结构。`SixStepConversation` 中 **`StreamErrorNotice`** 读取 `message.errors` 展示摘要(可折叠列表)。
**关键设计**:`mainStepStatus` / `subStepStatus` 驱动顶栏 `SixStepProgressHorizontal`;`stages` 驱动中间对话渲染;`currentSubStep` 驱动流程图沙箱蚂蚁线。`phase` / `intent` / `rawSteps` 与编排流、暂停继续、追问分支一致。所有信息集中在一个对象内,组件通过 props 按需取用。
---
## 四、模拟数据生成器(`mockData.ts`)
> **线上主路径**:`SixStepChatPage` 使用 **`runIntentOrchestrateStream`** 消费真实 `/api/orchestrate/stream`。`mockData.ts` 中的 **`runSixStepDeduction`** 仍保留,用于本地演示或与 `api.ts` 中纯编排解析逻辑对齐;新功能(意图、专家 SSE、`resume` 等)以 `runIntentOrchestrateStream` 为准。
### 4.1 种子数据
文件顶部定义了四组静态种子数组:
| 变量 | 长度 | 用途 |
|------|------|------|
| `PURPOSE_SEED` | 3 | 目的卡片内容模板 |
| `ACTOR_SEED` | 7 | 行为体卡片内容模板 |
| `BEHAVIOR_SEED` | 8 | 预期行为与驱动因素卡片内容模板 |
| `NARRATIVE_SEED` | 8 | 叙事卡片内容模板 |
使用时循环取模(`(counter - 1) % SEED.length`),确保不越界。
### 4.2 卡片生成规则
| 阶段 | 数量规则 |
|------|---------|
| 目的 | 固定 3 张(取自 `PURPOSE_SEED` 全量) |
| 行为体 | 每个目的对应的数量:第 0 个目的 2 张,其余 1 张,共 4 张 |
| 预期行为/驱动 | 每个行为体:偶数索引 2 张,奇数索引 1 张,共 6 张 |
| 叙事 | 每个预期行为:偶数索引 2 张,奇数索引 1 张,共 9 张 |
最终流程图节点总数:`1(智能体)+ 3 + 4 + 6×2(预期行为+驱动因素)+ 9 = 29 节点`。
### 4.3 流式控制器:`runSixStepDeduction`
```typescript
export function runSixStepDeduction(
message: SixStepMessage, // 直接在此对象上原地修改字段
options: {
onUpdate: (msg: SixStepMessage) => void; // 每次有新内容时回调
onComplete?: () => void; // 全部完成时回调
}
): SixStepMockController
```
返回的 `SixStepMockController` 包含:
- `message`: 正在被修改的消息对象引用
- `stop()`: 立即取消所有待执行的 `setTimeout`,中止推演
- `promise`: 推演完成后 resolve 的 Promise(stop 后不再 resolve)
**内部时序**(所有延迟均可通过顶部常量调整):
```
常量 默认值 说明
TYPE_INTERVAL 28ms 每次打字输出 4 个字符后的等待间隔
STAGE_GAP 380ms 阶段切换之间的等待时长
```
执行顺序(伪代码):
```
setMain('info', 'running') → emit
wait(800ms) ← 模拟信息准备耗时
setMain('info', 'done')
setMain('sixstep', 'running') → emit
wait(STAGE_GAP)
setSub('purpose', 'running') → emit
typeInto(PURPOSE_THINKING) ← 逐字打出思考过程
append cards 逐张(wait 280ms/张)
setSub('purpose', 'done') → emit
wait(STAGE_GAP)
setSub('actor', 'running') → emit
typeInto(ACTOR_THINKING)
append cards 逐张(wait 220ms/张)
setSub('actor', 'done') → emit
wait(STAGE_GAP)
setSub('behavior', 'running') → emit
typeInto(BEHAVIOR_THINKING)
append cards 逐张(wait 200ms/张)
setSub('behavior', 'done') → emit
wait(STAGE_GAP)
setSub('narrative', 'running') → emit
typeInto(NARRATIVE_THINKING)
append cards 逐张(wait 200ms/张)
setSub('narrative', 'done')
setMain('sixstep', 'done')
setMain('final', 'running') → emit
wait(STAGE_GAP)
typeInto(FINAL_ANSWER) ← 逐字打出最终结论
setMain('final', 'done') → emit
onComplete()
```
**`emit` 机制**:每次调用 `onUpdate({ ...message, stages: { ...message.stages } })`,将消息对象浅拷贝后传给父组件,触发 React re-render。
---
## 五、流程图布局工具(`flowLayout.ts`)
此文件被沙箱和全屏组件共同引用,避免重复计算。
### 5.1 核心函数:`buildLayout`
```typescript
function buildLayout(
purposes: PurposeCard[],
actors: ActorCard[],
behaviors: BehaviorDriverCard[],
narratives: NarrativeCard[],
options?: BuildLayoutOptions // 可覆盖节点尺寸和间距,以及根节点文案
): FlowLayout
```
**`BuildLayoutOptions` 扩展**:除 `nodeWidth`、`nodeHeight`、`levelGap`、`nodeGap`、`paddingX`、`paddingY` 外,可选 **`rootTitle`**、**`rootDetail`**。用于将层级 0 的 `agent-root` 从默认「智能体 / 协同推演…」改为 **当前已选专家**(`SixStepFlowSandbox` / `SixStepFlowFullscreen` 在存在 `message.selectedExpert` 时传入专家姓名与简介摘要)。
返回 `FlowLayout`:
```typescript
interface FlowLayout {
nodes: LayoutNode[]; // 已计算好坐标的节点列表
edges: LayoutEdge[]; // 已确定连接关系和颜色对的边列表
width: number; // 整体画布宽度(用于 SVG viewBox)
height: number; // 整体画布高度
}
```
**布局算法**:改良的"叶节点优先后序遍历":
1. 从 agent 根节点开始,递归 DFS 处理子树
2. 叶节点(无子节点的节点)按从左到右顺序分配 x 坐标,每次递增 `nodeWidth + nodeGap`
3. 非叶节点的 x = (最左子节点 x + 最右子节点 x) / 2,确保父节点居中于子节点上方
4. 所有节点的 y = `paddingY + level × (nodeHeight + levelGap)`,level 即树的深度
节点层级(`level`)对应关系:
| level | variant | 说明 |
|-------|---------|------|
| 0 | `agent` | 智能体根节点 |
| 1 | `purpose` | 目的 |
| 2 | `actor` | 行为体 |
| 3 | `behavior` | 预期行为 |
| 4 | `driver` | 驱动因素 |
| 5 | `narrative` | 叙事 |
注意:`BehaviorDriverCard` 一张卡片会生成两个节点(`behavior` + `driver`),id 分别为 `${card.id}-behavior` 和 `${card.id}-driver`。
### 5.2 共享常量
| 常量 | 说明 |
|------|------|
| `EDGE_COLOR` | 按边类型分别定义深色/浅色主题颜色(5 组) |
| `EDGE_LABEL` | 每种边类型的中文图例说明 |
| `CURRENT_PAIR_BY_STAGE` | `SubStepKey → LayoutEdge['pair']` 映射,决定哪组边在哪个阶段展示蚂蚁线 |
| `STATUS_TITLE` | `SubStepKey → string` 映射,沙箱/全屏顶部标题文字 |
| `VARIANT_ABBREV` | 节点类型 → 简化缩写(沙箱专用,如 `purpose → 'md'`) |
| `VARIANT_LABEL` | 节点类型 → 完整中文标签 |
### 5.3 辅助函数:`buildEdgePath`
生成 SVG 三次贝塞尔曲线路径字符串:
```
M x1 y1 C x1 my, x2 my, x2 y2
```
控制点均取中间 y 坐标(`(y1+y2)/2`),形成平滑 S 型连线,不与节点重叠。
---
## 六、CSS 动画(`styles.css`)
| 类名 | 动画 | 用途 |
|------|------|------|
| `.sixstep-ant-line` | `stroke-dasharray: 6 6` + `dashoffset` 循环 | SVG 边上的蚂蚁线流动效果(当阶段正在生成时) |
| `.sixstep-pulse` | box-shadow 呼吸光晕 | 进度面板当前步骤图标、沙箱标题图标 |
| `.sixstep-shimmer-text` / `...-light` | 渐变横扫文字 | 进度面板"生成中"文字、沙箱标题生成中状态 |
| `.sixstep-spin-soft` | 360° 匀速旋转 | loading 图标 |
| `.sixstep-fade-up` | 从下方淡入 | 子阶段卡片逐步出现 |
| `.sixstep-node-pop` | scale 弹出 + 高光 | 流程图新增节点的弹出动画(持续 600ms) |
| `.sixstep-ring-burst` | SVG circle `r` 从 0 扩散到 60,同时 opacity→0 | 节点出现时的扩散光环效果(持续 700ms) |
| `.sixstep-canvas-grab` / `.sixstep-canvas-grabbing` | `cursor: grab/grabbing` | 流程图画布拖拽手型 |
| `.sixstep-toolbar-btn` | 基础尺寸 + hover 过渡 | 工具栏图标按钮 |
| `.sixstep-minimap-rect` | 静态样式 | 缩略图视口矩形指示框 |
| `.sixstep-resize-handle` | 竖线拖拽手柄 | 沙箱左边缘的宽度调节拖拽条 |
---
## 七、主页面(`SixStepChatPage.tsx`)
### 7.1 职责
主页面是整个功能的**状态中枢**,负责:
- 多会话列表与 **`/api/conversations`** 持久化(`filter_key` / `filter_value` 标识 6bf 页)。
- 创建并管理 **`runIntentOrchestrateStream`**(编排 + 意图)与 **`runExpertSearch` / `runExpertCreate`**(仍可用于「换一批专家」「创建专家」等直连 invoke)的控制器生命周期。
- 将状态与专家相关回调分发给 `SixStepConversation`、`McpSelectModal`、`SixStepFlowSandbox` 等。
- 管理沙箱宽度拖拽。
### 7.2 状态一览(节选)
| 状态 | 类型 | 说明 |
|------|------|------|
| `conversations` / `activeConvId` | 多会话 | 当前选中会话及其 `messages` |
| `input` | `string` | 输入框文本 |
| `isGenerating` | `boolean` | 是否正在请求流式接口 |
| `outputFormat` | `'structured' \| 'markdown'` | 传给后端的 `output_format` |
| `sandboxOpen` / `sandboxWidth` / `fullscreenOpen` | UI | 流程图沙箱与全屏 |
| `historyOpen` / `loading` | UI / 加载 | 聊天记录侧栏与首屏拉取 |
| `activeAssistantMsgId` | `string \| null` | 当前高亮/流式更新的助手消息 |
| `mcpModal` | `{ open, messageId, expert }` | MCP 多选弹窗状态 |
### 7.3 Ref 一览
| Ref | 类型 | 用途 |
|-----|------|------|
| `controllerRef` | `SixStepMockController \| null` | `runIntentOrchestrateStream` 的 `stop()` |
| `expertControllerRef` | `ExpertRunController \| null` | 专家搜索/创建流的 `stop()` |
| `conversationsRef` | 会话数组 | 持久化前读取最新消息 |
| `containerRef` / `resizeRef` | DOM / 拖拽 | 沙箱宽度拖拽 |
### 7.4 核心逻辑
**`handleSend`**
1. 追加用户消息;创建初始助手消息(`buildInitialAssistantMessage`,`phase` 随流程推进)。
2. 调用 **`runIntentOrchestrateStream`**,请求体为 **`IntentOrchestrateRequest`**:`user_input`、`output_format`、`intent_mode`(默认 `auto`)、`current_phase`(含上一条助手为 **`paused`** 时传 `paused`)、`selected_expert`、`selected_mcps`、`expert_candidates`、**`last_steps`(来自上一条助手 `rawSteps`)**、`conversation_history` 等。
- 暂停继续时会继承上一条助手的 `rawSteps` 与 `selectedMcps`(`pausedSelectedMcps`),并显式回传 `selected_mcps` / `last_steps`,避免恢复时丢失步骤选择与历史上下文。
3. `onUpdate` 内更新当前会话中对应 `id` 的助手消息;结束后 `persistConversation`。
**专家与 MCP**
- 用户确认专家后 **`handleMcpModalConfirm`**:写入 `selectedExpert` / `selectedMcps`,并可用 **`intent_mode: 'force_orchestrate'`** 立即启动分析(无需用户再次输入)。
- **`handleRefetchExpert`**:可用 `force_find_expert` 并排除已有候选。
**`handleStop`**
- `AbortController` 中止流;将未完成步骤从 `running` 收回;助手 **`phase` 置为 `paused`**,便于下次发送走 **`continue_analysis`** / `rerun_from_step` / `revise_input_continue` / `change_steps_continue` / `edit_stage_continue`。
**流内关键事件补充**
- `resume`:恢复继续分析时,会先把已有 `stages` 中的 `purpose/actor/behavior` 卡片回填到转换缓存,确保后续 `step_end` 的父子映射准确。
- `rerun`:前端会按 `affected_steps` 清空受影响阶段(`stages`、`subStepStatus`、`rawSteps` 与局部卡片缓存),再接收重跑后的 `step_*`。
- `change_steps`:按 `removed_steps` 清理被移除阶段与 `rawSteps`,保留未移除阶段并继续执行 `steps_to_run`。
- `edit_apply`:先更新被编辑卡片内容,再按 `affected_steps` 清理后续阶段并触发重跑。
- `intent`:包含 `slots`(如 `step`、`new_input`),用于更细粒度前端状态更新。
**沙箱宽度拖拽**:逻辑同前(`mousemove` / `mouseup`,保证对话区最小宽度)。
**活跃助手消息派生**:
```typescript
const activeAssistantId = /* activeAssistantMsgId 或最后一条 assistant */;
const activeAssistant = messages.find((m) => m.id === activeAssistantId);
const liveMainStatus = activeAssistant?.mainStepStatus ?? DEFAULT_MAIN_STATUS;
const liveSubStatus = activeAssistant?.subStepStatus ?? DEFAULT_SUB_STATUS;
```
`liveMainStatus` / `liveSubStatus` 传给 **`SixStepProgressHorizontal`**;`activeAssistant` 传给沙箱与全屏。
### 7.5 Props 传递图
```
SixStepChatPage
│
├─► SixStepChatHistory # 会话列表(与顶栏「聊天记录」联动)
├─► SixStepProgressHorizontal # mainStatus / subStatus / isDark
├─► SixStepConversation
│ messages, isGenerating, activeMessageId,
│ expertHandlers(选专家 / 换一批 / 创建 / 确认)
├─► ChatInputComposer # onSend → handleSend, onStop → handleStop
├─► SixStepFlowSandbox # message={activeAssistant}
├─► SixStepFlowFullscreen
└─► McpSelectModal # 专家确认后选 MCP
```
---
## 八、左侧进度面板(`SixStepProgress.tsx`)
> **主页面顶栏**使用的是 **`SixStepProgressHorizontal.tsx`**(横向主/子步骤);本节描述纵向 `SixStepProgress` 的 Props 与交互,组件仍保留供其他布局复用。
### 8.1 Props
| Prop | 类型 | 必填 | 说明 |
|------|------|------|------|
| `mainStatus` | `Record<MainStepKey, StepStatus>` | ✓ | 三主步骤状态 |
| `subStatus` | `Record<SubStepKey, StepStatus>` | ✓ | 四子阶段状态 |
| `collapsed` | `boolean` | 可选 | 是否折叠,默认 `false` |
| `onToggleCollapse` | `() => void` | 可选 | 展开/收起回调 |
### 8.2 展开视图(`collapsed = false`)
从上到下展示三个主步骤卡片,每个卡片包含:
- 彩色图标(每个步骤有独立配色,running 时呼吸脉冲)
- 标题 + 进度徽标(`已完成 / 生成中 / 待开始`)
- 副文本描述(running 时展示 shimmer 动画)
- 步骤之间用竖线连接(完成后变绿色)
当 `sixstep` 阶段进入 running 或 done,在其卡片内部展开四个子阶段列表(`SUB_STEPS`),每个子阶段包含:
- 彩色图标(与流程图对应色)
- 标题 + 状态图标
- 时间线左侧的节点圆点 + 连接竖线
右上角收起按钮(`ChevronLeft`)触发 `onToggleCollapse`。
### 8.3 折叠视图(`collapsed = true`)
渲染轻量的 `CollapsedView` 子组件:
- 宽度由父容器通过 `w-14` 控制(56px),有 `transition-all duration-300` 过渡动画
- 顶部 `ChevronRight` 按钮用于展开
- 竖向排列三个主步骤彩色图标 + 状态圆点
- 若 `sixstep` 已解锁,在其下方额外展示四个小尺寸子阶段图标(`w-7 h-7`)
- 所有元素均有 `title` 属性,鼠标悬停显示步骤名称
### 8.4 配色方案
步骤颜色定义在 `MAIN_STEPS` 和 `SUB_STEPS` 数组的 `color` 字段中:
| 步骤 | 深色 | 浅色 |
|------|------|------|
| 信息准备 | sky-300 / sky-500 | sky-600 / sky-50 |
| 6步法推演 | violet-300 / violet-500 | violet-600 / violet-50 |
| 最终结果 | emerald-300 / emerald-500 | emerald-600 / emerald-50 |
| 目的 | violet-300 | orange |
| 行为体 | cyan-300 | red |
| 预期行为/驱动 | emerald-300 | emerald |
| 叙事 | amber-300 | amber |
---
## 九、对话列表(`SixStepConversation.tsx`)
### 9.1 Props
| Prop | 类型 | 说明 |
|------|------|------|
| `messages` | `SixStepMessage[]` | 全部消息 |
| `isGenerating` | `boolean` | 是否正在生成 |
| `activeMessageId` | `string \| null` | 当前流式聚焦的助手消息 id |
| `expertHandlers` | 可选对象 | `onSelectExpert` / `onRefetchExpert` / `onCreateExpert` / `onConfirmExpert` |
### 9.2 消息渲染策略
每条消息根据 `role` 字段分叉:
- `role === 'user'` → 渲染 `UserMessage`:右对齐气泡 + 用户头像
- `role === 'assistant'` → 渲染 `AssistantMessage`:左对齐 + 机器人头像(生成中时呼吸脉冲 + 星光图标)
消息列表容器 `ref` 监听内容变化,每次 `messages` 或 `isGenerating` 变化后自动滚动到底部。
### 9.3 助手消息 `AssistantMessage`
内部读取 `message.stages` 和 `message.mainStepStatus`、`message.subStepStatus`,根据是否存在各阶段数据来条件渲染对应的阶段块。底部(及部分分支末尾)挂载 **`StreamErrorNotice`**,展示 **`message.errors`**(来自 SSE `error_log` 等)。
助手消息结构示意:
```
AssistantMessage
│
├─ 信息准备中(仅当 info=running 且无任何 stage)→ shimmer 提示
│
├─ PurposeStage (当 stages.purpose 存在时)
│ ├─ InlineCollapsible (以 StageHeader 为标题)
│ │ ├─ ThinkingBlock (思考过程)
│ │ └─ InlineCollapsible (目的卡片列表)
│ │ └─ PurposeCardItem × N
│
├─ ActorStage (当 stages.actor 存在时)
│ ├─ InlineCollapsible
│ │ ├─ ThinkingBlock
│ │ └─ InlineCollapsible (行为体卡片列表)
│ │ └─ 按 purpose 分组
│ │ ├─ ParentGroupHeader (所属目的)
│ │ └─ ActorCardItem × N
│
├─ BehaviorStage (当 stages.behavior 存在时)
│ ├─ InlineCollapsible
│ │ ├─ ThinkingBlock
│ │ └─ InlineCollapsible (预期行为/驱动因素卡片列表)
│ │ └─ 按 actor 分组
│ │ ├─ ParentGroupHeader (所属行为体)
│ │ └─ BehaviorCardItem × N(2列网格)
│
├─ NarrativeStage (当 stages.narrative 存在时)
│ ├─ InlineCollapsible
│ │ ├─ ThinkingBlock
│ │ └─ InlineCollapsible (叙事卡片列表)
│ │ └─ 按 driver 分组
│ │ ├─ ParentGroupHeader (所属驱动因素)
│ │ └─ NarrativeCardItem × N
│
└─ FinalAnswer (当 finalAnswer 存在或 final=running 时)
│
└─ StreamErrorNotice (当 message.errors 非空)
```
**分组计算**:`ActorStage`、`BehaviorStage`、`NarrativeStage` 都在内部用 `useMemo` 计算分组,将卡片按父 ID 聚合到 Map,再按父实体列表顺序输出。
---
## 十、卡片原子组件(`SixStepCards.tsx`)
此文件作为"组件库",为对话列表提供所有可复用的卡片和容器组件。
### 10.1 `InlineCollapsible`
通用折叠面板,思考过程区块、卡片列表区块均复用此组件。
| Prop | 说明 |
|------|------|
| `title` | 标题区 ReactNode |
| `children` | 折叠内容 |
| `defaultOpen` | 默认展开状态(默认 `true`) |
| `variant` | `'default'`(标准边框背景)或 `'subtle'`(更淡)|
展开/收起使用 `grid-template-rows` 动画(`0fr → 1fr`),配合 `opacity` 过渡,无需 JS 计算高度。
### 10.2 各卡片组件
| 组件 | 卡片类型 | 布局特点 |
|------|---------|---------|
| `PurposeCardItem` | 目的 | 紫/橙渐变背景,索引徽标 + Target 图标 + 标题 + 理由 |
| `ActorCardItem` | 行为体 | 青/红渐变背景,UserCheck 图标 + 标题 + 内容 |
| `BehaviorCardItem` | 预期行为/驱动因素 | 翠绿渐变背景,上下两部分:TrendingUp 图标+预期行为 / Zap 图标+驱动因素 |
| `NarrativeCardItem` | 叙事 | 琥珀渐变背景,ScrollText 图标 + 标题 + 理由 |
所有卡片均有 `.sixstep-fade-up` 进入动画,`hover` 时边框加深。
### 10.3 `ParentGroupHeader`
显示某类卡片所属父实体名称的标签行,用于行为体、预期行为/驱动、叙事列表的分组标题。
| Prop | 说明 |
|------|------|
| `label` | 关系说明(如"所属目的") |
| `parentTitle` | 父实体名称 |
| `count` | 该组卡片数量 |
| `variant` | `'actor' \| 'behavior' \| 'narrative'`,决定图标和配色 |
### 10.4 `ThinkingBlock`
展示思考过程文本的区块,`loading` 为 `true` 时标题行展示 "生成中" shimmer 文字。
### 10.5 `StageHeader`
折叠面板标题,包含阶段图标、阶段标题、卡片数量徽标、状态徽标(已完成/生成中)。`loading = true` 时图标呼吸脉冲,副文本展示 shimmer 动画。
---
## 十一、流程图沙箱(`SixStepFlowSandbox.tsx`)
### 11.1 Props
| Prop | 类型 | 说明 |
|------|------|------|
| `message` | `SixStepMessage \| undefined` | 活跃助手消息,用于提取各阶段卡片数据 |
| `width` | `number` | 当前沙箱宽度(px),由主页面拖拽控制 |
| `onResizeStart` | `(e: React.MouseEvent) => void` | 拖拽手柄 mousedown 回调 |
| `onClose` | `() => void` | 关闭沙箱回调 |
| `onOpenFullscreen` | `() => void` | 打开全屏回调 |
### 11.2 布局参数(`COMPACT_OPTIONS`)
沙箱使用缩小版布局,节点小于全屏版:
```typescript
const COMPACT_OPTIONS = {
nodeWidth: 120, nodeHeight: 60,
levelGap: 80, nodeGap: 24,
paddingX: 32, paddingY: 28,
};
```
### 11.3 简化节点(`CompactNode`)
每个节点只显示**彩色图标 + 阶段缩写**(`VARIANT_ABBREV`):
| 层级 | 缩写 |
|------|------|
| 智能体(或根标题改为已选专家名时仍为 variant `agent`) | AI |
| 目的 | md |
| 行为体 | xwt |
| 预期行为 | yqxw |
| 驱动因素 | qdys |
| 叙事 | xs |
底部固定图例说明对应关系。
### 11.4 新增节点动画
通过 `seenIdsRef`(`Set<string>`)记录已渲染过的节点 ID。每次 `layout.nodes` 变化时,找出新增的 ID,将其加入 `appearingIds` 状态,700ms 后自动移除。
`CompactNode` 接收 `appearing` prop,为 `true` 时:
1. 为 `<g>` 添加 `.sixstep-node-pop` CSS 类(scale 弹出)
2. 在节点中心绘制一个 SVG `<circle>` 并添加 `.sixstep-ring-burst` CSS 类(扩散光环)
### 11.5 交互能力
**平移(Pan)**:
- `mousedown` 记录起始坐标和初始 `tx/ty` 到 `panStart.current`
- 通过 `useEffect` 监听 `isPanning` 状态,动态注册/注销 `window` 的 `mousemove` 和 `mouseup`
- `mousemove` 计算偏移量 `(e.clientX - startX, e.clientY - startY)` 叠加到初始平移值
**缩放(Zoom)**:
- 通过 `useEffect` 向 `containerRef` 注册原生 `wheel` 事件(`{ passive: false }`),确保 `e.preventDefault()` 生效,不触发浏览器页面缩放
- 以鼠标位置为缩放中心:`nextTx = mx - (mx - prev.tx) * (nextScale / prev.scale)`
- 缩放范围:0.3x ~ 2.5x
**工具栏**:
| 按钮 | 功能 |
|------|------|
| `+` | 按 1.2 倍放大 |
| `-` | 按 1.2 倍缩小 |
| ↺ | 恢复 1:1(translate 0,0 scale 1) |
| ⊕ | 定位画布中央(保持当前缩放比,调整平移使图形居中) |
| ⤢ | 触发 `onOpenFullscreen` |
首次有内容时自动执行 `fitToView`(等比缩放 + 居中),由 `fitAppliedRef` 控制只执行一次。
### 11.6 连线说明
SVG 中的 `<defs>` 为每种边类型各定义一个箭头 marker(id 加 `sixstep-sandbox-` 前缀,与全屏版 marker 不冲突)。当 `activePair === edge.pair` 时,该边添加 `.sixstep-ant-line` CSS 类展示蚂蚁线动画。
### 11.7 调整宽度
左侧有一个 `position: absolute` 的 `.sixstep-resize-handle` 竖线拖拽条,`mousedown` 触发 `onResizeStart`(由主页面处理具体逻辑)。
---
## 十二、全屏流程图视图(`SixStepFlowFullscreen.tsx`)
### 12.1 Props
| Prop | 类型 | 说明 |
|------|------|------|
| `message` | `SixStepMessage \| undefined` | 与沙箱相同的活跃助手消息 |
| `onClose` | `() => void` | 关闭全屏回调 |
### 12.2 与沙箱的区别
| 特性 | 沙箱 | 全屏 |
|------|------|------|
| 布局参数 | 紧凑(节点 120×60) | 标准(节点 200×84) |
| 节点内容 | 缩写 + 图标 | 完整标题 + 详情 + 图标 |
| 节点出现动画 | 有(pop + ring-burst) | 无 |
| 子树收起/展开 | 无 | 有(点击**有子节点**的父节点;右下角「-」收起 / 「+」展开;画布左下角有操作提示) |
| 缩略图 | 无 | 有(右下角;仅绘制当前可见节点) |
| 下载 | 无 | 有(导出 SVG) |
| Esc 关闭 | 无 | 有 |
| 层级 | flex 子元素 | `fixed inset-0 z-50`(全视口遮罩) |
| 缩放范围 | 0.3x ~ 2.5x | 0.2x ~ 3x |
### 12.3 详细节点(`DetailNode`)
使用 `foreignObject` 嵌入 HTML,展示:
- 彩色图标包裹 + 大写类型标签(`VARIANT_LABEL`)
- 节点标题(`line-clamp-1`)
- 详情内容(`line-clamp-2`)
深色/浅色主题的渐变背景、图标颜色均通过 `VARIANT_TOKEN` 查表获取。
**收起子树**:当 `flowLayout` 中节点带 **`children`** 时,卡片右上角显示 **`+` / `-`**,点击节点(或该角标)切换 **`collapsedNodeIds`**:自该节点向下的后代节点与相关边不再纳入 **`targetVisibleNodeSet`**,并以约 **240ms** 的透明度过渡(进入/离开集合合并渲染,避免闪烁)。重新生成布局时会剔除已不存在的节点 id,避免残留收起状态。根节点若被选为专家展示,仍可按树结构收起其下子链。
### 12.4 缩略图(`Minimap`)
固定 220×150px,渲染在右下角。
内部逻辑:
1. 计算整体布局与 minimap 尺寸的缩放比 `ratio = Math.min((W-10)/layoutW, (H-10)/layoutH)`
2. 按 `ratio` 缩小绘制每个节点矩形(使用 `MINIMAP_VARIANT_FILL` 颜色,无文字)
3. 根据当前 `transform`(`tx, ty, scale`)和视口尺寸,计算可见区域在世界坐标的矩形,再乘 `ratio` 映射到 minimap 坐标系,绘制蓝色描边矩形(`.sixstep-minimap-rect`)
4. 点击 minimap:将点击的 minimap 坐标逆映射到世界坐标,调用 `onJump` 将视口移动到以该点为中心
### 12.5 SVG 下载
点击下载按钮时:
1. 克隆 `svgRef.current`(当前全屏 SVG 元素)
2. 设置 `width/height` 为实际布局尺寸,移除 transform
3. 用 `XMLSerializer` 序列化为字符串
4. 创建 `Blob` 和临时 `<a>` 标签,触发下载
5. 文件名格式:`6步法推演-YYYY-MM-DD-HH-mm-ss.svg`
> ⚠️ 注意:由于节点内容在 `foreignObject` 中使用 HTML + CSS 类名,下载的 SVG 打开时若外部查看器不支持 CSS,样式会丢失。可在未来改为纯 SVG 文本节点渲染以提升兼容性。
---
## 十三、整体数据流图
```
用户输入(回车)
│
▼
SixStepChatPage.handleSend()
│
├─► 当前会话追加 UserMessage + 初始 AssistantMessage
│
└─► runIntentOrchestrateStream(assistant, { onUpdate, onComplete }, IntentOrchestrateRequest)
│
│ readSSE('/api/orchestrate/stream', body)
│
│ intent / expert_* / clarify / resume / rerun / change_steps / edit_apply
│ start / step_* / flow / done / delta(追问或 markdown)
│ 每次 onUpdate → 更新会话中该 assistant 的 SixStepMessage
│
▼ React re-render
│
├─► SixStepProgressHorizontal(liveMainStatus / liveSubStatus)
├─► SixStepConversation(messages、expertHandlers、intent/clarify 等)
├─► SixStepFlowSandbox / Fullscreen(stages → buildLayout)
└─► persistConversation(SQLite)
```
---
## 十四、路由注册
**`src/App.tsx`**:
```tsx
// 路由定义
<Route path="sixstep-chat" element={<SixStepChatPage />} />
<Route path="/observability" element={<ObservabilityPage />} />
// 导航路径映射(handleNavigate 函数)
else if ([..., 'sixstep-chat'].includes(page)) {
path = `/qa-center/${page}`;
}
```
**可观测性页面**(调用耗时 / 错误日志筛选):全站路由 **`/observability`**,侧栏顶级「可观测性」,对接后端 **`GET /api/observability/calls`**、**`/errors`**、**`/options`** 等(详见 `backend/doc.md`)。与 6bf 页面独立,不在 `/qa-center` 前缀下。
**`src/components/Sidebar.tsx`**:
```tsx
// 菜单配置(问答中心 children)
{ id: 'sixstep-chat', label: '6bf', icon: Workflow }
// qaPages 集合(控制问答中心父菜单高亮)
const qaPages = new Set([..., 'sixstep-chat']);
```
---
## 十五、流式错误与 `error_log`(`api.ts`)
编排 SSE 除 **`error`** 外,可能下发 **`error_log`**(步骤失败摘要、skill 降级等)。`runIntentOrchestrateStream` 将其归一为 **`StreamErrorItem`** 并 **`push` 到 `message.errors`**;网络异常也会写入类似结构。后端在可执行 **skill 失败已跳过**时,**`message` 文案**可能附带 **异常原因前若干字符**(与 `backend/app/mcp/base.py` 截断策略一致),便于在气泡内区分超时与其它错误。
**`StreamErrorNotice`**:仅在对应助手气泡内展示,支持折叠展开、错误/警告计数摘要;不参与 `showAnalysisContent` 隐藏逻辑(专家阶段亦可显示)。
---
## 十六、扩展指南
### 接入编排与意图流
1. 主入口为 **`runIntentOrchestrateStream`**(`api.ts`),与后端 **`/api/orchestrate/stream`** 事件一一对应;扩展新意图时通常同时改 **`backend/app/intent/detector.py`** 与 **`runIntentOrchestrateStream`** 的分支解析。
- 当前除 `continue_analysis` 外,还支持 `rerun_from_step`、`revise_input_continue`、`change_steps_continue`、`edit_stage_continue` 分支。
2. 保持 **`onUpdate(SixStepMessage)`** 契约:`SixStepChatPage` 只需合并助手消息字段。
3. 纯编排、无意图分支的调试可继续用 **`runSixStepDeduction`**(同文件内,仅 `start` / `step_*` / `flow` / `done`)。
详见 **`INTENT_INTEGRATION.md`** 与仓库 **`docs/xvqiu.md`**。
### 某子步(如行为体、叙事)体感很慢时
前端 **`step_end`** 事件里会带上该步的 **`duration_ms`**(整步总耗时),便于在 UI 或调试时对比各步。
更细拆分(skill 耗时 vs 拼 prompt 扩展 vs LLM)在**后端**完成:每次 `POST /api/agents/{key}/invoke` 及编排内对 `MCPAgent.invoke` 的调用,服务端会打 **`mcp_timing`** / **`mcp_skill_timing`** 日志(见 **`backend/doc.md`**「MCP 调用接口」与 **`backend/DEVELOPMENT.md`** §5.2)。排查时在运行 `python -m app.main` 的终端查看即可;无需改前端请求体。
### 替换流程图引擎(如 G6 / React Flow)
仅需替换 `SixStepFlowSandbox.tsx` 和 `SixStepFlowFullscreen.tsx` 中的 SVG 渲染部分:
- `buildLayout()` 的 `LayoutNode` 数组可直接转换为 G6 节点
- `LayoutEdge` 数组可直接转换为 G6 边
- `CURRENT_PAIR_BY_STAGE` 和 `EDGE_COLOR` 仍适用
### 新增推演子阶段
1. 在 `types.ts` 的 `SubStepKey` 中新增键名
2. 在 `SixStepStages` 中新增对应 `*StageData` 字段
3. 在 `mockData.ts` 的 `runSixStepDeduction` 中新增生成逻辑(若仍需要离线演示)
4. 在 `SixStepConversation.tsx` 的 `AssistantMessage` 中新增阶段渲染块
5. 在 `SixStepProgress.tsx` / **`SixStepProgressHorizontal.tsx`** 的 `SUB_STEPS` 中新增步骤项
6. 若需要流程图新层级,在 `flowLayout.ts` 的 `buildLayout` 中新增节点构建逻辑