嵌入通信协议
本文档面向需要在自身应用中嵌入 MaaPipelineEditor(下称 MPE)的开发者。MPE 通过 iframe 嵌入模式,使宿主应用(如 VSCode 插件、Web IDE 等)可以在不依赖 LocalBridge 后端服务的情况下,将流程编辑器集成到自有的界面中。
概述
在嵌入模式下,MPE 以 iframe 为容器运行,宿主与 MPE 之间通过 window.postMessage 进行双向通信。嵌入模式不连接任何后端服务,所有文件操作由宿主代理完成。
| 维度 | 嵌入模式 | 在线/独立模式 |
|---|---|---|
| 通信方式 | postMessage | WebSocket |
| 后端依赖 | 无 | LocalBridge |
| 文件访问 | 宿主代理 | LB 文件服务 |
| 设备连接 | 不提供 | 通过 LB |
| 调试功能 | 不提供 | 通过 LB |
| UI 定制 | 宿主可配置 | 不可配置 |
激活方式
在 iframe 的 src 中添加 URL 参数即可激活嵌入模式:
https://mpe.codax.site/stable/?embed=true&origin=vscode-maa| 参数 | 必填 | 说明 |
|---|---|---|
embed | 是 | 设为 true 激活嵌入模式 |
origin | 否 | 声明宿主来源,用于调试与可选的 origin 校验 |
origin 参数说明
origin 的值可以是两种形式:
- 标识符(如
vscode-maa、test-host):仅用于日志记录和调试,不做严格的 postMessage origin 校验 - 完整 URL(如
https://my-app.com):MPE 会对收到的 postMessage 消息做严格的event.origin匹配,不匹配的消息会被丢弃
如果 origin 以 http 开头,MPE 会启用严格的 origin 校验;否则仅作为标识符使用。
消息格式
所有 postMessage 消息使用统一的信封格式:
interface EmbedMessage {
protocol: "mpe-embed"; // 协议标识,防止消息串扰
version: string; // 协议版本,如 "1.0.0"
type: string; // 消息类型
requestId?: string; // 请求 ID,用于请求-响应模式匹配
payload: any; // 消息体
}MPE 仅处理 protocol 为 "mpe-embed" 的消息,其余 postMessage 消息会被静默忽略。
握手流程
iframe 加载完成后,宿主需主动发起握手:
宿主 MPE (iframe)
│ │
│ ─── mpe:init ──────────────► │
│ { capabilities, ui } │
│ │
│ ◄──── mpe:ready ───────────── │
│ { version, supportedCaps } │
│ │
│ (握手完成,正式通信) │- iframe 加载完成后,宿主发送
mpe:init,携带权限声明与UI 配置 - MPE 根据声明配置自身,回复
mpe:ready - 握手完成前,MPE 忽略所有非
mpe:init消息 - 若 5 秒内未收到
mpe:init,MPE 会使用默认权限集自动完成握手并继续运行
注意
mpe:init 消息中的 requestId 必须原样回填到 mpe:ready 中,否则宿主的请求会超时。建议宿主侧设置 10 秒超时。
宿主 → MPE 消息
| 类型 | 说明 | payload | 响应 |
|---|---|---|---|
mpe:init | 握手 + 权限/UI 声明 | EmbedInitConfig | mpe:ready |
mpe:loadPipeline | 加载 pipeline JSON 数据 | { fileName?, data } | mpe:loadResult |
mpe:save | 请求 MPE 回传当前流程数据 | {} | mpe:saveData |
mpe:selectNode | 选中指定节点 | { nodeId } | — |
mpe:focusNode | 聚焦并视口定位到指定节点 | { nodeId } | — |
mpe:state | 查询 MPE 当前状态 | { fields: string[] } | mpe:stateResult |
加载流程数据
mpe:loadPipeline 的 data 字段为标准 MaaFramework pipeline JSON 对象。MPE 收到后会调用内部解析器将其转换为流程图渲染。
{
type: "mpe:loadPipeline",
requestId: "uuid",
payload: {
fileName: "pipeline.json", // 可选,用于 saveData 回填
data: { /* Pipeline JSON */ }
}
}MPE 响应 mpe:loadResult:
{
type: "mpe:loadResult",
requestId: "uuid", // 与请求相同的 requestId
payload: {
success: true,
fileName: "pipeline.json"
}
}保存流程数据
宿主发送 mpe:save 后,MPE 将当前流程图序列化为 pipeline JSON 并回传:
{
type: "mpe:saveData",
requestId: "uuid",
payload: {
fileName: "pipeline.json",
data: { /* Pipeline JSON */ }
}
}节点选中与聚焦
mpe:selectNode 和 mpe:focusNode 都通过 nodeId 定位节点。MPE 的实现支持两种查找方式:
- 按节点内部 ID(ReactFlow 的
node.id)精确匹配 - 按节点标签回退(
node.data.label,即节点显示名称)——当按 ID 找不到时自动尝试按标签匹配
如果两种方式都找不到节点,MPE 会发送 mpe:error:
{
type: "mpe:error",
payload: {
code: "node_not_found",
message: "Node not found: 节点名称"
}
}mpe:focusNode 定位成功后,MPE 会调用 fitView 将视口平滑移动到目标节点(动画时长 300ms)。
状态查询
mpe:state 通过 fields 数组查询 MPE 的当前状态。目前支持的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
version | string | 嵌入协议版本,如 "1.0.0" |
nodesCount | number | 当前节点数量 |
edgesCount | number | 当前边数量 |
fileName | string | null | 当前文件名 |
readOnly | boolean | 是否处于只读模式 |
MPE → 宿主消息
| 类型 | 说明 | payload |
|---|---|---|
mpe:ready | 初始化完成,握手响应 | { version, supportedCaps } |
mpe:loadResult | 加载结果 | { success, fileName?, error? } |
mpe:saveData | 回传流程数据 | { fileName, data } |
mpe:change | 流程图变更通知 | { type, detail } |
mpe:nodeSelect | 用户选中节点 | { nodeId, nodeData? } |
mpe:saveRequest | MPE 主动请求保存(如 Ctrl+S) | { hint } |
mpe:stateResult | 状态查询结果 | { [field]: value } |
mpe:error | 错误通知 | { code, message, detail? } |
变更通知
MPE 在流程图发生变更时,会向宿主推送 mpe:change 消息。变更类型包括:
| type | 说明 | detail |
|---|---|---|
node.add | 添加节点 | { nodeId, taskName } |
node.delete | 删除节点 | { nodeCount, edgeCount } |
node.update | 节点数据变更 | { nodeCount, edgeCount } |
edge.add | 添加边 | { edgeId, source, target } |
edge.delete | 删除边 | { nodeCount, edgeCount } |
警告
变更通知为尽力交付(best-effort),不做消息确认,宿主不应依赖其做精确的状态同步。需要精确状态时应使用 mpe:save 获取全量数据。
变更通知内置 300ms 防抖:在防抖窗口内发生多次变更时,仅发送最后一次的通知(detail 取最终值)。
保存请求
当用户在 MPE 内按下 Ctrl+S(或 Cmd+S)时,MPE 会发送 mpe:saveRequest 通知宿主:
{
type: "mpe:saveRequest",
payload: { hint: "user-triggered" }
}宿主收到后可自行决定是否触发保存流程(发送 mpe:save → 接收 mpe:saveData)。
能力声明
宿主在 mpe:init 中通过 capabilities 字段声明期望的能力集合,MPE 会据此限制对应功能:
interface EmbedCapabilities {
readOnly: boolean; // 只读模式,禁止编辑流程图
allowCopy: boolean; // 允许复制/粘贴节点
allowUndoRedo: boolean; // 允许撤销/重做
allowAutoLayout: boolean; // 允许自动布局
allowAI: boolean; // 允许 AI 辅助功能
allowSearch: boolean; // 允许搜索面板
allowCustomTemplate: boolean; // 允许自定义模板
}各能力的作用范围:
| 能力 | 生效方式 |
|---|---|
readOnly | 禁用节点拖拽、连接、双击创建、右键菜单;过滤掉所有 remove/add/position 类型的节点变更 |
allowCopy | 控制 Ctrl+C / Ctrl+V 是否可用 |
allowUndoRedo | 控制撤销/重做快捷键 |
allowAutoLayout | 控制自动布局按钮是否可用,不可用时点击会触发 mpe:error(capability_denied) |
allowAI | 控制 AI 搜索按钮是否显示 |
allowSearch | 控制搜索面板是否显示 |
allowCustomTemplate | 控制自定义模板面板是否显示 |
默认权限集
若宿主未在 5 秒内发送 mpe:init,MPE 使用以下默认值自动完成握手:
| 能力 | 默认值 |
|---|---|
readOnly | false |
allowCopy | true |
allowUndoRedo | true |
allowAutoLayout | true |
allowAI | false |
allowSearch | true |
allowCustomTemplate | true |
默认集偏保守,仅开放核心编辑能力,关闭需要外部依赖(如 AI 服务)的功能。
UI 控制
宿主在 mpe:init 中通过 ui 字段控制 MPE 的界面元素:
interface EmbedUIConfig {
hideHeader: boolean; // 隐藏顶部导航栏
hideToolbar: boolean; // 隐藏左侧工具栏
hiddenPanels: string[]; // 隐藏的面板 ID 列表
}可隐藏的面板 ID
| 面板 ID | 说明 |
|---|---|
field | 字段面板 |
edge | 边面板 |
search | 搜索面板 |
file | 文件面板 |
config | 配置面板 |
ai-history | AI 历史面板 |
local-file | 本地文件面板 |
error | 错误面板 |
recognition-history | 识别历史面板 |
toolbar | 工具栏面板 |
logger | 日志面板 |
exploration | 流程探索面板 |
提示
顶部导航栏的本地服务连接按钮在嵌入模式下会始终显示为 "EmbedBridge",且点击不会触发断开连接。
完整的 EmbedInitConfig
interface EmbedInitConfig {
capabilities: EmbedCapabilities;
ui: EmbedUIConfig;
}生命周期
宿主创建 iframe
│
▼
MPE 加载,检测 embed=true
│
▼
MPE 进入嵌入模式,等待 mpe:init
│
├──── 超时 5s ──► 使用默认权限集自动完成握手
│
▼
收到 mpe:init,配置权限与 UI
│
▼
回复 mpe:ready,握手完成
│
▼
正常通信(loadPipeline / change / save ...)
│
▼
宿主销毁 iframe(可选发送 mpe:destroy)
│
▼
MPE 清理资源安全考量
Origin 校验
MPE 在接收 postMessage 时按以下规则校验:
- 所有消息必须携带
protocol: "mpe-embed" - 若 URL
origin参数以http开头,则要求event.origin严格匹配 - 若
origin参数为标识符形式(如vscode-maa),仅记录日志,不阻断消息
版本协商
握手时 MPE 在 mpe:ready 中声明自身支持的协议版本(当前为 1.0.0)。版本策略遵循语义化版本:
- 主版本号变更:不兼容的协议变更(消息格式、握手流程等)
- 次版本号变更:向后兼容的功能新增(新增消息类型、新增 capability 字段)
- 修订号变更:向后兼容的问题修正
宿主侧集成示例
以下是一个完整的宿主侧集成示例,展示了 iframe 创建、握手、消息收发、保存代理的完整流程:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>MPE 嵌入示例</title>
<style>
body {
margin: 0;
height: 100vh;
display: flex;
}
#mpe-frame {
flex: 1;
border: none;
}
</style>
</head>
<body>
<iframe
id="mpe-frame"
src="https://mpe.codax.site/stable/?embed=true&origin=my-app"
></iframe>
<script>
const iframe = document.getElementById("mpe-frame");
const pendingRequests = new Map();
let requestIdCounter = 0;
// 发送消息,支持请求-响应模式
function sendMessage(type, payload, needResponse = false) {
const requestId = needResponse
? `req-${++requestIdCounter}`
: undefined;
const msg = {
protocol: "mpe-embed",
version: "1.0.0",
type,
...(requestId ? { requestId } : {}),
payload,
};
iframe.contentWindow.postMessage(msg, "*");
if (needResponse) {
return new Promise((resolve, reject) => {
pendingRequests.set(requestId, { resolve, reject });
setTimeout(() => {
if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId);
reject(new Error(`Request ${type} timeout`));
}
}, 10000);
});
}
}
// 监听 MPE 消息
window.addEventListener("message", (event) => {
const data = event.data;
if (!data || data.protocol !== "mpe-embed") return;
// 处理请求-响应匹配
if (data.requestId && pendingRequests.has(data.requestId)) {
const { resolve } = pendingRequests.get(data.requestId);
pendingRequests.delete(data.requestId);
resolve(data.payload);
return;
}
switch (data.type) {
case "mpe:ready":
console.log("MPE ready, version:", data.payload.version);
console.log("Supported capabilities:", data.payload.supportedCaps);
break;
case "mpe:change":
console.log(
"Pipeline changed:",
data.payload.type,
data.payload.detail,
);
// 可在此处实现"未保存"标记
break;
case "mpe:saveRequest":
console.log("MPE requests save:", data.payload.hint);
// 触发保存流程
triggerSave();
break;
case "mpe:saveData":
console.log("Received pipeline data for:", data.payload.fileName);
// 将 data.payload.data 写入文件系统
break;
case "mpe:error":
console.error(
"MPE error:",
data.payload.code,
data.payload.message,
);
break;
}
});
// iframe 加载完成后发起握手
iframe.addEventListener("load", () => {
setTimeout(async () => {
try {
const result = await sendMessage(
"mpe:init",
{
capabilities: {
readOnly: false,
allowCopy: true,
allowUndoRedo: true,
allowAutoLayout: true,
allowAI: false,
allowSearch: true,
allowCustomTemplate: false,
},
ui: {
hideHeader: false,
hideToolbar: false,
hiddenPanels: [
"config",
"local-file",
"logger",
"exploration",
],
},
},
true,
);
console.log("Handshake success:", result);
} catch (err) {
console.error("Handshake failed:", err);
}
}, 500);
});
// 加载 pipeline
async function loadPipeline(fileName, pipelineData) {
const result = await sendMessage(
"mpe:loadPipeline",
{
fileName,
data: pipelineData,
},
true,
);
console.log("Load result:", result);
}
// 保存 pipeline
async function triggerSave() {
const data = await sendMessage("mpe:save", {}, true);
console.log("Save data received:", data);
// 将 data.data 写入文件系统
}
// 选中节点
function selectNode(nodeId) {
sendMessage("mpe:selectNode", { nodeId });
}
// 聚焦节点
function focusNode(nodeId) {
sendMessage("mpe:focusNode", { nodeId });
}
</script>
</body>
</html>VSCode WebView 桥接
VSCode WebView 的 vscode.postMessage 与 iframe 内的 window.parent.postMessage 需要一层中继。宿主需在 WebView 的 HTML 中(iframe 外层)注入中继脚本:
<script>
const vscode = acquireVsCodeApi();
// iframe → 扩展进程
window.addEventListener("message", (event) => {
if (event.data?.protocol === "mpe-embed") {
vscode.postMessage(event.data);
}
});
</script>
<iframe id="mpe-frame" src="...?embed=true&origin=vscode-maa"></iframe>扩展进程中收到消息后,通过 iframe 的 contentWindow.postMessage 转发给 MPE:
// 扩展进程 → iframe
panel.webview.onDidReceiveMessage((msg) => {
const iframe = document.getElementById("mpe-frame");
iframe.contentWindow.postMessage(msg, "*");
});MPE 侧代码完全不受影响,使用标准的 window.parent.postMessage 即可。
测试环境
项目源码的 /Iframe 目录下提供了一个完整的测试父级环境(index.html + test-host.js + test-host.css),可用于本地验证通信协议的所有消息类型。该测试环境独立于主项目技术栈,直接用浏览器打开即可使用。
测试环境提供了:
- 能力开关(复选框形式配置 capabilities)
- UI 配置(面板隐藏选项)
- 所有消息类型的发送按钮
- Pipeline JSON 输入区
- 实时消息日志(按方向着色:接收为绿色,发送为蓝色,错误为红色)
- 请求-响应超时提示
提示
测试环境不依赖任何构建工具,直接将 dist/ 构建产物与 Iframe/ 目录部署到同一静态服务器即可测试。
