210 lines
8.6 KiB
Markdown
210 lines
8.6 KiB
Markdown
这份详细的数据样本非常关键!它揭示了几个核心特征:
|
||
|
||
1. **自动化的 Map/Reduce 逻辑**:注意看 `edges` 中的 `dimension_mode: "down"`。这说明你的系统支持“隐式循环” —— `DirectoryScanner` 产出一个数组,而 `TraceLoader` 接收单个文件,后端会自动将数组拆解并发执行。
|
||
2. **复杂参数配置**:`ColumnOpsNode` 的参数是一个 JSON 字符串,这意味着前端需要特殊的编辑器(Monaco Editor)而不仅仅是简单的文本框。
|
||
3. **强类型约束**:Schema 中明确定义了 `DataTable`, `Array<String>`, `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<String>","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": <Switch>, "JsonEditor": <Monaco> }
|
||
│ └── 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) => <Switch checked={props.value} onCheckedChange={props.onChange} />,
|
||
'String': (props) => <Input value={props.value} onChange={props.onChange} />,
|
||
'Number': (props) => <Slider value={props.value} onChange={props.onChange} />,
|
||
|
||
// 高级控件 (通过 param_schema.widget 指定)
|
||
'JsonEditor': (props) => <JsonEditor code={props.value} language="json" />,
|
||
'FilePicker': (props) => <FilePicker path={props.value} pattern={props.schema.pattern} />,
|
||
'Select': (props) => <Select options={props.schema.options} ... />
|
||
};
|
||
|
||
export const renderWidget = (paramSchema, value, onChange) => {
|
||
// 1. 优先使用 schema 中显式定义的 widget (如 "widget": "file")
|
||
let WidgetComp = widgetMap[paramSchema.widget];
|
||
|
||
// 2. 其次根据 type 推断 (如 "type": "Boolean")
|
||
if (!WidgetComp) WidgetComp = widgetMap[paramSchema.type];
|
||
|
||
// 3. 兜底
|
||
if (!WidgetComp) WidgetComp = widgetMap['String'];
|
||
|
||
return <WidgetComp value={value} onChange={onChange} schema={paramSchema} />;
|
||
}
|
||
|
||
```
|
||
|
||
### 4.3 核心模块:Execution Visualizer (流式可视化)
|
||
|
||
在 `GraphCanvas` 中监听 Store 变化,实现动画。
|
||
|
||
```tsx
|
||
// 伪代码思路
|
||
const EdgeRenderer = ({ edge }) => {
|
||
const executionState = useExecutionStore(s => s.nodeStatus[edge.target]);
|
||
|
||
// 如果是 down 模式且正在运行,显示粒子动画
|
||
const isFlowing = executionState?.status === 'running';
|
||
const isBatching = edge.dimension_mode === 'down';
|
||
|
||
return (
|
||
<svg>
|
||
<path className={isFlowing ? 'animate-flow-dash' : ''} ... />
|
||
{isBatching && <text>自动批处理中...</text>}
|
||
</svg>
|
||
)
|
||
}
|
||
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 给 Copilot 的执行指令 (Prompt)
|
||
|
||
你可以直接复制以下内容发给 Copilot 开始下一步:
|
||
|
||
> **指令:初始化 V2 核心注册表与类型定义**
|
||
> 这里的背景:我们要重构 TraceStudio 的前端核心,采用服务端驱动 UI 的模式。
|
||
> 请在 `web/src/core` 下创建以下基础架构:
|
||
> 1. **`types.ts`**: 根据我提供的 `plugins` JSON 数据,定义 `NodeSchema`、`ParamSchema` 和 `ExecutionGraph` 的 TypeScript 接口。注意 `param_schema` 中的 `options` 和 `widget` 字段。
|
||
> 2. **`registry/WidgetRegistry.tsx`**: 创建一个注册表对象,将后端类型(String, Number, Boolean)和特殊 Widget(JsonEditor, FilePicker)映射到 React 组件(先用 Placeholder 组件代替)。实现一个 `renderWidget(schema, value, onChange)` 函数。
|
||
> 3. **`components/Inspector.tsx`**: 重写属性面板。它应该接收一个 `NodeModel`,遍历其 `param_schema`,并调用 `renderWidget` 来生成表单。
|
||
>
|
||
>
|
||
> 请确保代码严格遵循 TypeScript 类型安全,不要使用 `any`。
|
||
|
||
这样,你就确立了 V2 版本最核心的机制:**后端定义数据 -> 前端注册表解析 -> 自动生成界面**。 |