基于 Agent 实现 Draw.io 智能绘图功能

前言

大家好,最近学习了 Draw.io MCP 相关内容后,我也自己动手实现了一个 Draw.io 智能绘图功能。今天来分享下整个设计思路和踩坑过程~

核心功能: 输入自然语言描述,AI 自动生成 Draw.io 流程图/架构图,支持在线预览和编辑。

适用场景:

  • 快速生成业务流程图、时序图
  • 系统架构图可视化
  • 技术文档配图
  • 需求评审时快速出图

技术背景

在深入实现之前,先了解几个核心概念:

什么是 MCP(Model Context Protocol)?

MCP 是 Anthropic 推出的一种开放协议,用于连接 AI 模型与外部工具/数据源。它定义了一套标准的通信方式,让 AI 能够:

  • 调用外部工具执行操作
  • 访问外部数据源获取上下文
  • 与第三方服务进行交互

在本项目中,MCP 用于让 AI 直接控制 Draw.io 进行绘图操作。

mxGraph XML 格式

Draw.io 底层使用 mxGraph 库,图表数据以 XML 格式存储。一个简单的流程图 XML 结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<mxGraphModel>
<root>
<mxCell id="0"/> <!-- 根节点 -->
<mxCell id="1" parent="0"/> <!-- 默认图层 -->
<mxCell id="node1" value="开始" <!-- 图形节点 -->
style="ellipse;fillColor=#d5e8d4;"
vertex="1" parent="1">
<mxGeometry x="100" y="100" width="80" height="40"/>
</mxCell>
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;" <!-- 连接线 -->
edge="1" source="node1" target="node2"/>
</root>
</mxGraphModel>

核心属性解析:

  • **vertex=”1”**:标识这是一个节点(非连接线)
  • **edge=”1”**:标识这是一条连接线
  • source/target:连接线的起点和终点节点 ID
  • style:样式字符串,控制颜色、形状、边框等

技术方案选型

在设计之初,我考虑了 两种工作模式

XML 模式

1
用户输入 → AI 生成 XML → 前端解析渲染 → 预览/编辑
  • AI 直接生成 Draw.io 的 mxGraph XML 格式
  • 前端解析渲染,支持预览和编辑
  • 不依赖外部服务,纯后端处理
  • 优点: 轻量、稳定、无需额外安装、响应快
  • 缺点: AI 生成的 XML 可能有格式问题

MCP 模式

1
用户输入 → AI 生成指令 → MCP Server → 控制浏览器 Draw.io
  • 通过 Draw.io MCP Server 实时控制浏览器
  • 画的图像更加精准,支持更复杂的操作
  • 优点: 绘制精准、支持复杂图形
  • 缺点: 需要用户安装浏览器扩展、依赖外部服务

方案对比

维度XML 模式MCP 模式
部署复杂度⭐ 简单⭐⭐⭐ 复杂
用户体验⭐⭐ 需前端渲染⭐⭐⭐ 实时绘制
稳定性⭐⭐⭐ 高⭐⭐ 依赖外部服务
绘制精度⭐⭐ 中等⭐⭐⭐ 高
响应速度⭐⭐⭐ 快⭐⭐ 较慢

最终选择: 以 XML 模式为主,MCP 模式作为可选增强。因为 XML 模式更轻量、部署更简单,用户无需额外配置即可使用。

系统架构

先用 AI 生成一个简单的架构图,看看效果~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
┌─────────────────────────────────────────────────────────────────────────────┐
│ 前端 (React/TypeScript) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────┐ │
│ │ ChatPanel │ │ DrawioViewer │ │ 流程图拖拽配置界面 │ │
│ │ (消息展示) │ │ (图表渲染) │ │ (节点/边配置) │ │
│ └────────┬────────┘ └────────┬────────┘ └──────────────┬──────────────┘ │
└───────────┼─────────────────────┼──────────────────────────┼────────────────┘
│ │ │
│ SSE │ XML │ REST API
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 后端 (Spring Boot 3.x) │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ AiAgentController │ │ DrawioStrategy │ │ AiAgentDrawAdmin │ │
│ │ (流式响应) │ │ (绘图策略) │ │ Controller │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌─────────────────────┐ │
│ │ │ │ DrawConfigParser │ │
│ │ │ │ (配置解析器) │ │
│ │ │ └──────────┬──────────┘ │
│ │ ▼ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ DrawioMcpClient │ │ │
│ │ │ (MCP WebSocket) │ │ │
│ └──────────────┴──────────┬──────────┴─────────────┘ │
└────────────────────────────────────────┼────────────────────────────────────┘

│ WebSocket

┌─────────────────────────┐
│ drawio-mcp-server │
│ (Node.js MCP Server) │
└────────────┬────────────┘

│ Browser Extension

┌─────────────────────────┐
│ Draw.io Web App │
│ (app.diagrams.net) │
└─────────────────────────┘

效果预览

功能主界面

生成流程图示例

编辑预览功能

这是优化后的最终效果,有一些线条过长可能需要手动调整,但整体效果还是不错的。

数据流转过程

以 XML 模式为例,完整的数据流转过程如下:

image-20251204145741793

核心实现

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├── controller/
│ └── AiAgentController.java // SSE 流式响应接口
├── strategy/
│ ├── IDrawioStrategy.java // 绘图策略接口
│ └── impl/
│ └── DrawioStrategyImpl.java // XML/MCP 模式实现
├── gateway/
│ ├── IDrawioMcpGateway.java // MCP 网关接口
│ └── impl/
│ └── DrawioMcpClient.java // WebSocket MCP 客户端
├── parser/
│ └── DrawConfigParser.java // 配置解析器
├── model/
│ ├── DrawioRequest.java // 请求实体
│ └── DrawioResponse.java // 响应实体
└── config/
└── DrawioConfig.java // 配置类

1. Prompt 工程(最关键)

这是 最花时间调试 的部分!Prompt 的质量直接决定了生成图表的效果。

遇到的问题

最开始生成的 XML 经常有问题:

问题现象出现频率根本原因
XML 格式错误,无法解析30%AI 混入了 Markdown 或 JS 代码
节点全部挤在左上角40%没有给出明确的坐标规则
连接线断开或错位25%source/target ID 不匹配
样式丢失,全是白框20%style 属性格式不对
图形超出画布15%坐标计算没有边界限制

Prompt 优化迭代过程

V1 版本(失败): 只告诉 AI “生成 Draw.io 流程图”

1
问题:AI 不知道 mxGraph XML 格式,生成的是 SVG 或纯文本描述

V2 版本(部分成功): 给出 XML 示例

1
问题:AI 能生成 XML,但坐标混乱,节点重叠严重

V3 版本(基本可用): 添加布局规则和样式模板

1
2
改进:明确坐标起点、间距、节点尺寸
问题:复杂流程图的分支处理不好

V4 版本(最终版): 完整约束 + 示例 + 要求

1
2
3
4
5
关键改进:
1. 强调"不要添加任何 JavaScript 代码"
2. 给出完整的节点和连接线示例
3. 明确不同形状的 style 模板
4. 定义严格的布局规则

经过反复调试,最终确定了以下 Prompt 模板:

XML 模式 Prompt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final String SYSTEM_PROMPT = """
你是一个专业的 Draw.io 图表生成助手。根据用户的描述,生成 Draw.io mxGraph XML 格式的图表。

**重要:你必须严格按照以下 XML 模板格式生成,不要添加任何 JavaScript 代码或其他格式!**

XML 模板格式:
```xml
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- 矩形节点示例 -->
<mxCell id="node1" value="节点文字" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>
</mxCell>
<!-- 连接线示例 -->
<mxCell id="edge1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="node1" target="node2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
常用样式:
- 普通矩形:style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;"
- 菱形判断:style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;"
- 开始/结束:style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;"
- 连接线:style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;"
- 带箭头文字的连接线:在 mxCell 的 value 属性中添加文字

布局规则:
- 起点位置:x=100, y=80
- 矩形大小:width=120, height=60
- 垂直间距:100px
- 水平间距:150px

生成要求:
1. 先简要描述图表结构
2. 然后输出完整的 XML 代码(用 ```xml 包裹)
3. XML 必须是有效的,以 <mxGraphModel> 开始,以 </mxGraphModel> 结束
4. 每个节点必须有唯一的 id
5. 连接线必须指定 source 和 target 属性
""";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#### MCP 模式 Prompt

```java
String mcpPrompt = """
你是一个 Draw.io 绘图助手。根据用户需求,生成绘图指令。

请以 JSON 数组格式输出绘图步骤,每个步骤包含:
- action: 操作类型 (add-rectangle, add-edge, add-cell-of-shape)
- params: 参数对象

布局规则:
- 起始位置: x=100, y=100
- 矩形大小: width=120, height=60
- 水平间距: 150, 垂直间距: 100

示例输出:
```json
[
{"action": "add-rectangle", "params": {"x": 100, "y": 100, "width": 120, "height": 60, "text": "开始"}},
{"action": "add-rectangle", "params": {"x": 100, "y": 200, "width": 120, "height": 60, "text": "处理"}},
{"action": "add-edge", "params": {"source_id": "{{id_0}}", "target_id": "{{id_1}}", "text": ""}}
]
""";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

> **踩坑提醒:** 一定要给 AI 明确的坐标规则,否则生成的图会挤在一起或者飞出画布!

### 2. 绘图策略执行器

```java
@Override
public void execute(ExecuteCommandEntity requestParameter, ResponseBodyEmitter emitter) throws Exception {
log.info("开始Draw.io绘图 sessionId:{}", requestParameter.getSessionId());

if (!enabled) {
sendResult(emitter, "summary", "Draw.io 绘图功能暂未启用", requestParameter.getSessionId());
sendCompleteResult(emitter, requestParameter.getSessionId());
return;
}

try {
// 判断使用哪种模式
if (useMcp && drawioMcpGateway != null && drawioMcpGateway.isEnabled()) {
executeMcpMode(requestParameter, emitter);
} else {
executeXmlMode(requestParameter, emitter);
}
} catch (Exception e) {
log.error("Draw.io 绘图异常:{}", e.getMessage(), e);
sendResult(emitter, "summary", "绘图出错:" + e.getMessage(), requestParameter.getSessionId());
sendResult(emitter, "thinking", "任务执行完成", requestParameter.getSessionId());
} finally {
sendCompleteResult(emitter, requestParameter.getSessionId());
}
}

3. MCP 客户端实现

MCP 网关接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public interface IDrawioMcpGateway {

/** 调用 MCP 工具 */
JSONObject callTool(String toolName, Map<String, Object> params);

/** 添加矩形 */
JSONObject addRectangle(int x, int y, int width, int height, String text, String style);

/** 添加连接线 */
JSONObject addEdge(String sourceId, String targetId, String text, String style);

/** 获取选中的单元格 */
JSONObject getSelectedCell();

/** 删除单元格 */
JSONObject deleteCell(String cellId);

/** 获取形状分类 */
JSONObject getShapeCategories();

/** 添加指定形状 */
JSONObject addCellOfShape(String shapeName, int x, int y, int width, int height, String text);

/** 获取分页模型 */
JSONObject listPagedModel(int page, int pageSize);

/** 是否已连接 */
boolean isConnected();

/** 是否启用 */
boolean isEnabled();
}

WebSocket 客户端核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Override
public JSONObject callTool(String toolName, Map<String, Object> params) {
if (!enabled) {
return errorResponse("Draw.io MCP 未启用");
}

if (!connected) {
connect();
if (!connected) {
return errorResponse("无法连接到 MCP 服务器");
}
}

String requestId = String.valueOf(requestIdCounter.incrementAndGet());
CompletableFuture<JSONObject> future = new CompletableFuture<>();
pendingRequests.put(requestId, future);

try {
JSONObject request = new JSONObject();
request.put("jsonrpc", "2.0");
request.put("id", requestId);
request.put("method", "tools/call");
JSONObject toolParams = new JSONObject();
toolParams.put("name", toolName);
toolParams.put("arguments", params);
request.put("params", toolParams);

String requestJson = request.toJSONString();
log.debug("[DrawioMcpClient] 发送请求: {}", requestJson);
webSocket.sendText(requestJson, true);

// 等待响应,设置超时
return future.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
pendingRequests.remove(requestId);
log.error("[DrawioMcpClient] 请求超时: {}", toolName);
return errorResponse("请求超时");
} catch (Exception e) {
pendingRequests.remove(requestId);
log.error("[DrawioMcpClient] 调用工具失败: {}", toolName, e);
return errorResponse("调用失败: " + e.getMessage());
}
}

绘图操作封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 添加矩形
@Override
public JSONObject addRectangle(int x, int y, int width, int height, String text, String style) {
return callTool("add-rectangle", Map.of(
"x", x,
"y", y,
"width", width,
"height", height,
"text", text,
"style", style != null ? style : "whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;"
));
}

// 添加连接线
@Override
public JSONObject addEdge(String sourceId, String targetId, String text, String style) {
Map<String, Object> params = new HashMap<>();
params.put("source_id", sourceId);
params.put("target_id", targetId);
if (text != null) params.put("text", text);
if (style != null) params.put("style", style);
return callTool("add-edge", params);
}

4. XML 解析与修复

AI 生成的 XML 不一定总是合法的,这是整个系统稳定性的关键环节。

常见问题与修复策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* Draw.io XML 解析与修复工具类
*/
public class DrawioXmlParser {

private static final Pattern XML_PATTERN =
Pattern.compile("<mxGraphModel[\\s\\S]*?</mxGraphModel>");

/**
* 从 AI 响应中提取 XML
* AI 可能返回:1. 纯 XML 2. Markdown 代码块包裹 3. 带说明文字
*/
public static String extractXml(String aiResponse) {
if (aiResponse == null || aiResponse.isEmpty()) {
return null;
}

Matcher matcher = XML_PATTERN.matcher(aiResponse);
if (matcher.find()) {
return matcher.group();
}
return null;
}

/**
* 修复 XML 中的常见问题
*/
public static String fixXml(String xml) {
if (xml == null) return null;

// 1. 修复未转义的 & 符号(最常见问题)
// 注意:要避免重复转义已经是 &amp; 的情况
xml = xml.replaceAll("&(?!(amp|lt|gt|quot|apos);)", "&amp;");

// 2. 修复中文引号
xml = xml.replace(""", "\"").replace(""", "\"");
xml = xml.replace("'", "'").replace("'", "'");

// 3. 移除可能的 BOM 头
xml = xml.replace("\uFEFF", "");

// 4. 修复换行符
xml = xml.replace("\r\n", "\n").replace("\r", "\n");

// 5. 移除 XML 声明(Draw.io 不需要)
xml = xml.replaceFirst("<\\?xml[^>]*\\?>\\s*", "");

return xml.trim();
}

/**
* 验证 XML 是否合法
*/
public static boolean validateXml(String xml) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 禁用外部实体,防止 XXE 攻击
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
builder.parse(new InputSource(new StringReader(xml)));
return true;
} catch (Exception e) {
log.warn("XML 验证失败: {}", e.getMessage());
return false;
}
}
}

完整的提取与修复流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 处理 AI 响应,提取并修复 XML
*/
private String processAiResponse(String aiResponse) {
// Step 1: 提取 XML
String xml = DrawioXmlParser.extractXml(aiResponse);
if (xml == null) {
log.error("无法从 AI 响应中提取 XML");
throw new DrawioException("AI 未生成有效的图表数据");
}

// Step 2: 修复常见问题
xml = DrawioXmlParser.fixXml(xml);

// Step 3: 验证 XML 格式
if (!DrawioXmlParser.validateXml(xml)) {
log.warn("XML 格式验证失败,尝试进一步修复...");
// 可以添加更激进的修复策略
xml = attemptAggressiveFix(xml);
}

// Step 4: 添加标记,方便前端识别
return "<!-- DRAWIO_START -->\n" + xml + "\n<!-- DRAWIO_END -->";
}

5. 前端渲染组件

前端组件负责将 AI 生成的 XML 渲染为可视化图表,并提供编辑能力。

技术选型

方案优点缺点选择
mxGraph 本地渲染完全离线、可控库体积大、API 复杂
Draw.io iframe 嵌入功能完整、体验好依赖外部服务
SVG 转换轻量丢失交互能力

最终选择 Draw.io iframe 嵌入方案,通过 URL 参数传递压缩后的 XML 数据。

XML 编码流程

Draw.io 使用特殊的 URL 格式加载图表数据:

1
https://app.diagrams.net/#R{compressed_base64_xml}

编码过程:XML → UTF-8 bytes → deflate 压缩 → Base64 → URL 编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import pako from 'pako';

export const DrawioViewer: React.FC<DrawioViewerProps> = ({ xml, className = '' }) => {
const [showPreview, setShowPreview] = useState(false);
const [copied, setCopied] = useState(false);
const [previewError, setPreviewError] = useState<string | null>(null);

// 使用 pako 压缩并编码 XML
const encodedXml = useMemo(() => {
try {
// Step 1: 将 XML 转换为 UTF-8 字节数组
const encoder = new TextEncoder();
const data = encoder.encode(xml);

// Step 2: 使用 deflate 压缩(level 9 最大压缩率)
const compressed = pako.deflateRaw(data, { level: 9 });

// Step 3: 转换为 Base64
// 注意:需要处理大数组,避免栈溢出
let binary = '';
const chunkSize = 8192;
for (let i = 0; i < compressed.length; i += chunkSize) {
const chunk = compressed.slice(i, i + chunkSize);
binary += String.fromCharCode.apply(null, Array.from(chunk));
}
const base64 = btoa(binary);

// Step 4: URL 安全编码
return encodeURIComponent(base64);
} catch (err) {
console.error('XML 编码失败:', err);
setPreviewError('图表编码失败,请检查 XML 格式');
return null;
}
}, [xml]);

// 预览 URL(只读模式)
const previewUrl = useMemo(() => {
if (!encodedXml) return null;
// lightbox=1: 只读预览模式
// nav=1: 显示导航
// edit=_blank: 编辑时新开标签
return `https://viewer.diagrams.net/?lightbox=1&nav=1&edit=_blank#R${encodedXml}`;
}, [encodedXml]);

// 在 Draw.io 中打开编辑(新标签页)
const openInDrawio = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (encodedXml) {
// splash=0: 跳过启动画面
const editUrl = `https://app.diagrams.net/?splash=0#R${encodedXml}`;
window.open(editUrl, '_blank', 'noopener,noreferrer');
}
};

// 复制 XML 到剪贴板
const copyXml = async () => {
try {
await navigator.clipboard.writeText(xml);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('复制失败:', err);
}
};

// 下载 XML 文件
const downloadXml = () => {
const blob = new Blob([xml], { type: 'application/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `diagram-${Date.now()}.drawio`;
a.click();
URL.revokeObjectURL(url);
};

return (
<div className={`drawio-viewer ${className}`}>
{/* 工具栏 */}
<div className="toolbar">
<button onClick={() => setShowPreview(!showPreview)}>
{showPreview ? '隐藏预览' : '显示预览'}
</button>
<button onClick={openInDrawio}>在 Draw.io 中编辑</button>
<button onClick={copyXml}>{copied ? '已复制' : '复制 XML'}</button>
<button onClick={downloadXml}>下载</button>
</div>

{/* 预览区域 */}
{showPreview && previewUrl && (
<iframe
src={previewUrl}
className="preview-frame"
style={{ width: '100%', height: '500px', border: 'none' }}
title="Draw.io Preview"
/>
)}

{/* 错误提示 */}
{previewError && <div className="error">{previewError}</div>}
</div>
);
};

XML 提取工具函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export const extractDrawioXml = (content: string): string | null => {
if (!content) return null;

// 查找 DRAWIO 标记
const startMarker = '<!-- DRAWIO_START -->';
const endMarker = '<!-- DRAWIO_END -->';

const startIndex = content.indexOf(startMarker);
const endIndex = content.indexOf(endMarker);

if (startIndex !== -1 && endIndex !== -1) {
return content.substring(startIndex + startMarker.length, endIndex).trim();
}

// 查找 mxGraphModel
const mxStart = content.indexOf('<mxGraphModel');
const mxEnd = content.indexOf('</mxGraphModel>');

if (mxStart !== -1 && mxEnd !== -1) {
return content.substring(mxStart, mxEnd + 15).trim();
}

return null;
};

6. MCP 服务器配置

如果需要使用 MCP 模式,配置如下:

1
2
3
4
5
6
7
8
{
"mcpServers": {
"drawio": {
"command": "npx",
"args": ["-y", "drawio-mcp-server"]
}
}
}

效果展示

示例 1:用户登录流程图

输入:

1
请生成一个用户登录的流程图,包含:输入账号密码、验证、登录成功/失败等步骤

输出:

用户登录流程图

示例 2:微服务架构图

输入:

1
请生成一个电商系统的微服务架构图,包含:用户服务、订单服务、商品服务、支付服务、网关、注册中心等

输出:

微服务架构图

踩坑总结

在开发过程中遇到了不少坑,这里详细总结一下,希望能帮助大家少走弯路。

坑 1:XML 特殊字符未转义

现象: 前端解析 XML 报错 Invalid XML

原因: AI 生成的节点文字包含 &<> 等特殊字符

解决方案:

1
2
// 使用正则避免重复转义
xml = xml.replaceAll("&(?!(amp|lt|gt|quot|apos);)", "&amp;");

坑 2:节点坐标混乱

现象: 所有节点挤在左上角,或者分散到画布外

原因: Prompt 中没有给出明确的坐标规则,AI 自由发挥

解决方案: 在 Prompt 中明确定义:

1
2
3
4
5
6
布局规则:
- 起点位置:x=100, y=80
- 矩形大小:width=120, height=60
- 垂直间距:100px(节点中心到中心)
- 水平间距:150px(用于并行分支)
- 画布边界:x∈[50, 800], y∈[50, 1100]

坑 3:连接线 source/target 不匹配

现象: 连接线断开,没有连到正确的节点

原因: AI 生成的连接线 source/target ID 与节点 ID 不一致

解决方案:

  1. 在 Prompt 中强调 ID 一致性
  2. 后端添加 ID 校验逻辑
    1
    2
    3
    4
    5
    6
    7
    // 验证连接线的 source 和 target 是否存在
    Set<String> nodeIds = extractNodeIds(xml);
    for (Edge edge : extractEdges(xml)) {
    if (!nodeIds.contains(edge.getSource()) || !nodeIds.contains(edge.getTarget())) {
    log.warn("连接线引用了不存在的节点: {} -> {}", edge.getSource(), edge.getTarget());
    }
    }

坑 4:MCP WebSocket 连接不稳定

现象: MCP 模式下经常超时或断连

原因: 网络抖动、MCP Server 重启等

解决方案: 实现自动重连机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Scheduled(fixedDelay = 30000)  // 每 30 秒检查一次
public void checkConnection() {
if (enabled && !connected) {
log.info("检测到 MCP 连接断开,尝试重连...");
connect();
}
}

private void connect() {
int retryCount = 0;
while (retryCount < MAX_RETRY && !connected) {
try {
doConnect();
} catch (Exception e) {
retryCount++;
log.warn("MCP 连接失败,第 {} 次重试", retryCount);
Thread.sleep(1000 * retryCount); // 指数退避
}
}
}

坑 5:大数组 Base64 编码栈溢出

现象: 复杂图表在前端编码时报错 Maximum call stack size exceeded

原因: String.fromCharCode.apply(null, largeArray) 参数过多

解决方案: 分块处理

1
2
3
4
5
6
7
8
9
10
11
// 错误写法
const base64 = btoa(String.fromCharCode.apply(null, Array.from(compressed)));

// 正确写法:分块处理
let binary = '';
const chunkSize = 8192;
for (let i = 0; i < compressed.length; i += chunkSize) {
const chunk = compressed.slice(i, i + chunkSize);
binary += String.fromCharCode.apply(null, Array.from(chunk));
}
const base64 = btoa(binary);

坑 6:SSE 连接超时

现象: 复杂图表生成时间长,SSE 连接被断开

原因: Nginx 或网关默认超时时间太短

解决方案:

1
2
3
4
5
6
# Nginx 配置
location /api/ai/ {
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_buffering off; # 关闭缓冲,实现真正的流式
}
1
2
3
4
5
6
7
8
9
10
// Spring Boot 配置
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(300000); // 5 分钟
}
};
}

性能优化

1. AI 响应缓存

对于相同或相似的绘图请求,可以缓存 AI 响应:

1
2
3
4
@Cacheable(value = "drawio", key = "#request.hashCode()")
public String generateDiagram(DrawioRequest request) {
// 调用 AI 生成
}

2. XML 压缩传输

前端到后端传输时,也可以压缩 XML 减少带宽:

1
2
3
// 发送请求时压缩
const compressed = pako.deflate(xml);
const base64 = btoa(String.fromCharCode(...compressed));

3. 懒加载预览

图表预览使用懒加载,只有用户点击时才加载 iframe:

1
2
3
4
5
6
7
8
9
10
11
const [shouldLoad, setShouldLoad] = useState(false);

return (
<div onClick={() => setShouldLoad(true)}>
{shouldLoad ? (
<iframe src={previewUrl} />
) : (
<div className="placeholder">点击加载预览</div>
)}
</div>
);

安全考虑

1. XSS 防护

AI 生成的 XML 可能包含恶意脚本,需要过滤:

1
2
3
4
// 移除可能的脚本标签
xml = xml.replaceAll("<script[^>]*>.*?</script>", "");
// 移除事件处理器
xml = xml.replaceAll("\\s+on\\w+\\s*=", " data-removed=");

2. XXE 防护

解析 XML 时禁用外部实体:

1
2
3
4
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

3. 限流保护

AI 调用成本较高,需要限流:

1
2
3
4
5
6
7
8
@RateLimiter(name = "drawio", fallbackMethod = "rateLimitFallback")
public void generateDiagram(DrawioRequest request) {
// ...
}

public void rateLimitFallback(DrawioRequest request, RequestNotPermitted ex) {
throw new TooManyRequestsException("请求过于频繁,请稍后再试");
}

技术栈总结

层级技术选型说明
后端框架Spring Boot 3.x主框架
流式响应SSE (ResponseBodyEmitter)实时返回 AI 响应
MCP 通信Java WebSocket连接 MCP Server
前端框架React + TypeScriptUI 框架
图表渲染Draw.io iframe预览和编辑
XML 压缩pako (deflate)减少传输体积
AI 模型大语言模型生成 XML
PromptPrompt Engineering控制输出格式

后续优化方向

短期优化

  1. 样式丰富化: 支持更多图形样式和主题配色
  2. 布局算法: 引入 dagre/ELK 等自动布局算法
  3. 错误恢复: XML 解析失败时自动重试生成

中期优化

  1. 模板库: 预设流程图、架构图、时序图等模板
  2. 导出功能: 支持导出 PNG、SVG、PDF 格式
  3. 历史记录: 保存生成历史,支持回溯

长期优化

  1. 多人协作: 基于 WebSocket 的实时协作
  2. 版本对比: 图表版本对比和合并
  3. AI 优化: 根据用户反馈优化生成效果

在线体验

想体验的小伙伴可以访问:https://ai.ikunyyds.top

总结

本文详细介绍了如何实现一个 AI 驱动的 Draw.io 智能绘图功能,包括:

  1. 技术方案选型:对比了 XML 模式和 MCP 模式的优缺点
  2. 系统架构设计:前后端分离,SSE 流式响应
  3. Prompt 工程:从失败到成功的迭代优化过程
  4. 核心代码实现:策略模式、MCP 客户端、XML 解析
  5. 前端渲染:iframe 嵌入 + pako 压缩
  6. 踩坑总结:6 个典型问题及解决方案
  7. 安全与性能:XSS/XXE 防护、限流、缓存

希望这篇文章能帮助到想实现类似功能的小伙伴,如有问题欢迎在评论区交流!


如果这篇文章对你有帮助,欢迎点赞收藏~ 有问题也可以在评论区交流!