这份详细的数据样本非常关键!它揭示了几个核心特征: 1. **自动化的 Map/Reduce 逻辑**:注意看 `edges` 中的 `dimension_mode: "down"`。这说明你的系统支持“隐式循环” —— `DirectoryScanner` 产出一个数组,而 `TraceLoader` 接收单个文件,后端会自动将数组拆解并发执行。 2. **复杂参数配置**:`ColumnOpsNode` 的参数是一个 JSON 字符串,这意味着前端需要特殊的编辑器(Monaco Editor)而不仅仅是简单的文本框。 3. **强类型约束**:Schema 中明确定义了 `DataTable`, `Array`, `Number` 等类型。 基于你提供的这些真实数据,我为你制定了 **TraceStudio V2 API 协议与前端架构白皮书**。这是给 Copilot 执行的终极蓝图。 --- # 📜 TraceStudio V2 架构与协议白皮书 > **设计目标**:构建一个服务端驱动 (Server-Driven)、支持自动批处理 (Auto-Batching)、具备流式反馈 (Streaming) 的高性能数据分析平台。 ## 1. 核心概念定义 在开始 API 设计前,必须统一前后端的领域术语: * **Manifest (图谱)**: 后端返回的 `plugins` JSON,定义了所有能力的边界。 * **Blueprint (蓝图)**: 前端发送给后端的 `nodes` + `edges` JSON,描述计算逻辑。 * **Job (作业)**: 蓝图的一次实例化运行。 * **Artifact (产物)**: 节点运行产生的结果(预览数据、CSV文件、图表)。 * **Dimension Expansion (维度展开)**: 当 `dimension_mode: "down"` 时,后端自动将 List 拆解为 Item 进行并发处理的过程。 --- ## 2. API 协议规范 (The Protocol) ### 2.1 静态契约:能力发现 (Schema Discovery) 前端启动时调用,建立组件注册表。 * **Endpoint**: `GET /api/schema/manifest` ```json 节点结构 { "nodes":{"DirectoryScanner":{"display_name":"目录扫描器","category":"IO/Scanner","description":"扫描目录并输出文件路径列表","icon":"📁","node_type":"input","class_name":"DirectoryScanner","node_logic":"standard","supports_preview":true,"inputs":[],"outputs":[{"name":"count","type":"Number","description":"文件数量"},{"name":"files","type":"Array","description":"文件路径列表(数组)"}],"param_schema":[{"name":"max_files","type":"Number","default":0,"description":"最大文件数(0=无限制)","min":0,"step":1},{"name":"reverse_sort","type":"Boolean","default":false,"description":"反向排序"},{"name":"sort_by","type":"String","default":"name","description":"排序方式","options":["name","size","modified","created","none"]},{"name":"recursive","type":"Boolean","default":false,"description":"是否递归扫描子目录"},{"name":"pattern","type":"String","default":"*.utrace","description":"文件匹配模式(支持 glob)","required":true},{"name":"directory","type":"String","default":"","description":"要扫描的目录(相对于用户目录)","required":true}],"context_vars":{},"cache_policy":"none"} } ``` ### 2.2 动态契约:作业提交 (Job Submission) 前端不等待结果,只获取“工单号”。 * **Endpoint**: `POST /api/execution/queue` * **Payload**: 直接使用你提供的 `nodes`, `edges`, `global_context` 结构。 * **Response**: ```json { "job_id": "job_20260110_xf82", "status": "queued", "queue_position": 1 } ``` ### 2.3 交互契约:节点预览 (Lazy Interaction) 当用户点击某个已经跑完的节点时,前端才去拉取大数据。 * **Endpoint**: `GET /api/execution/{job_id}/node/{node_id}/preview` * **Query**: `?limit=100&offset=0` * **Response**: ```json { "type": "DataTable", "schema": {"Duration": "Float64", "Name": "String"}, "data": [{"Duration": 33.5, "Name": "MainThread"}, ...] // 前100行 } ``` --- ## 3. WebSocket 流式协议 (The Streaming Experience) 这是实现“节点流动、即时更新”的关键。后端在执行过程中通过 WS 推送 Event。 **连接**: `WS /ws/events?client_id={uuid}` #### 事件类型定义: 1. **`JOB_STARTED`**: 整个图开始运行。 2. **`NODE_STATUS` (状态流转)**: * Payload: `{"node_id": "n_xxx", "status": "running" | "completed" | "error"}` * *UI 表现*: 节点边框变色,Loading 圈转动。 3. **`NODE_PROGRESS` (进度)**: * Payload: `{"node_id": "n_xxx", "progress": 45, "message": "Parsing events..."}` * *UI 表现*: 节点上方出现微型进度条。 4. **`DIMENSION_EMIT` (维度展开 - 关键!)**: * 当 `DirectoryScanner` 扫出 100 个文件,后续节点会运行 100 次。 * Payload: `{"node_id": "n_trace_loader", "batch_index": 5, "total_batches": 100}` * *UI 表现*: 连接线 (Edge) 上显示“粒子流动”动画,数字 `5/100` 跳动。 5. **`PREVIEW_READY` (预览就绪)**: * Payload: `{"node_id": "n_xxx", "preview_type": "table_summary", "rows": 5000}` * *UI 表现*: 节点变成“可点击”状态,显示“✅ 5k rows”。 --- ## 4. 前端架构设计 (React + TS + Zustand) 为了支撑这套协议,前端必须采用 **Registry (注册表)** 模式。 ### 4.1 目录结构重构 ```text src/core/ ├── schema/ # 后端定义的静态类型 │ ├── NodeDefinition.ts # 对应 Manifest 中的节点 │ └── WidgetDefinition.ts # 控件类型定义 ├── registry/ # 核心:JSON -> React 组件的映射 │ ├── WidgetRegistry.tsx # { "Boolean": , "JsonEditor": } │ └── TypeRegistry.ts # { "DataTable": "#blue", "String": "#yellow" } └── store/ └── executionStore.ts # 管理 WebSocket 消息和节点状态 ``` ### 4.2 核心模块:WidgetRegistry (控件工厂) 处理你那个复杂的 `param_schema`。 ```tsx // src/core/registry/WidgetRegistry.tsx import { Switch } from '@/components/ui/switch'; import { JsonEditor } from '@/components/widgets/JsonEditor'; import { FilePicker } from '@/components/widgets/FilePicker'; const widgetMap = { // 基础类型 'Boolean': (props) => , 'String': (props) => , 'Number': (props) => , // 高级控件 (通过 param_schema.widget 指定) 'JsonEditor': (props) => , 'FilePicker': (props) => , 'Select': (props) =>