8.6 KiB
8.6 KiB
这份详细的数据样本非常关键!它揭示了几个核心特征:
- 自动化的 Map/Reduce 逻辑:注意看
edges中的dimension_mode: "down"。这说明你的系统支持“隐式循环” ——DirectoryScanner产出一个数组,而TraceLoader接收单个文件,后端会自动将数组拆解并发执行。 - 复杂参数配置:
ColumnOpsNode的参数是一个 JSON 字符串,这意味着前端需要特殊的编辑器(Monaco Editor)而不仅仅是简单的文本框。 - 强类型约束:Schema 中明确定义了
DataTable,Array<String>,Number等类型。
基于你提供的这些真实数据,我为你制定了 TraceStudio V2 API 协议与前端架构白皮书。这是给 Copilot 执行的终极蓝图。
📜 TraceStudio V2 架构与协议白皮书
设计目标:构建一个服务端驱动 (Server-Driven)、支持自动批处理 (Auto-Batching)、具备流式反馈 (Streaming) 的高性能数据分析平台。
1. 核心概念定义
在开始 API 设计前,必须统一前后端的领域术语:
- Manifest (图谱): 后端返回的
pluginsJSON,定义了所有能力的边界。 - Blueprint (蓝图): 前端发送给后端的
nodes+edgesJSON,描述计算逻辑。 - Job (作业): 蓝图的一次实例化运行。
- Artifact (产物): 节点运行产生的结果(预览数据、CSV文件、图表)。
- Dimension Expansion (维度展开): 当
dimension_mode: "down"时,后端自动将 List 拆解为 Item 进行并发处理的过程。
2. API 协议规范 (The Protocol)
2.1 静态契约:能力发现 (Schema Discovery)
前端启动时调用,建立组件注册表。
- Endpoint:
GET /api/schema/manifest
节点结构
{
"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:
{
"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:
{
"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}
事件类型定义:
JOB_STARTED: 整个图开始运行。NODE_STATUS(状态流转):
- Payload:
{"node_id": "n_xxx", "status": "running" | "completed" | "error"} - UI 表现: 节点边框变色,Loading 圈转动。
NODE_PROGRESS(进度):
- Payload:
{"node_id": "n_xxx", "progress": 45, "message": "Parsing events..."} - UI 表现: 节点上方出现微型进度条。
DIMENSION_EMIT(维度展开 - 关键!):
- 当
DirectoryScanner扫出 100 个文件,后续节点会运行 100 次。 - Payload:
{"node_id": "n_trace_loader", "batch_index": 5, "total_batches": 100} - UI 表现: 连接线 (Edge) 上显示“粒子流动”动画,数字
5/100跳动。
PREVIEW_READY(预览就绪):
- Payload:
{"node_id": "n_xxx", "preview_type": "table_summary", "rows": 5000} - UI 表现: 节点变成“可点击”状态,显示“✅ 5k rows”。
4. 前端架构设计 (React + TS + Zustand)
为了支撑这套协议,前端必须采用 Registry (注册表) 模式。
4.1 目录结构重构
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。
// 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 变化,实现动画。
// 伪代码思路
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下创建以下基础架构:
types.ts: 根据我提供的pluginsJSON 数据,定义NodeSchema、ParamSchema和ExecutionGraph的 TypeScript 接口。注意param_schema中的options和widget字段。registry/WidgetRegistry.tsx: 创建一个注册表对象,将后端类型(String, Number, Boolean)和特殊 Widget(JsonEditor, FilePicker)映射到 React 组件(先用 Placeholder 组件代替)。实现一个renderWidget(schema, value, onChange)函数。components/Inspector.tsx: 重写属性面板。它应该接收一个NodeModel,遍历其param_schema,并调用renderWidget来生成表单。请确保代码严格遵循 TypeScript 类型安全,不要使用
any。
这样,你就确立了 V2 版本最核心的机制:后端定义数据 -> 前端注册表解析 -> 自动生成界面。