前言
大家好,最近学习了 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 模式为例,完整的数据流转过程如下:

核心实现
项目结构
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 {
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
|
public class DrawioXmlParser { private static final Pattern XML_PATTERN = Pattern.compile("<mxGraphModel[\\s\\S]*?</mxGraphModel>");
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; }
public static String fixXml(String xml) { if (xml == null) return null; xml = xml.replaceAll("&(?!(amp|lt|gt|quot|apos);)", "&"); xml = xml.replace(""", "\"").replace(""", "\""); xml = xml.replace("'", "'").replace("'", "'"); xml = xml.replace("\uFEFF", ""); xml = xml.replace("\r\n", "\n").replace("\r", "\n"); xml = xml.replaceFirst("<\\?xml[^>]*\\?>\\s*", ""); return xml.trim(); }
public static boolean validateXml(String xml) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 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
|
private String processAiResponse(String aiResponse) { String xml = DrawioXmlParser.extractXml(aiResponse); if (xml == null) { log.error("无法从 AI 响应中提取 XML"); throw new DrawioException("AI 未生成有效的图表数据"); } xml = DrawioXmlParser.fixXml(xml); if (!DrawioXmlParser.validateXml(xml)) { log.warn("XML 格式验证失败,尝试进一步修复..."); xml = attemptAggressiveFix(xml); } 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);
const encodedXml = useMemo(() => { try { const encoder = new TextEncoder(); const data = encoder.encode(xml); const compressed = pako.deflateRaw(data, { level: 9 }); 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); return encodeURIComponent(base64); } catch (err) { console.error('XML 编码失败:', err); setPreviewError('图表编码失败,请检查 XML 格式'); return null; } }, [xml]);
const previewUrl = useMemo(() => { if (!encodedXml) return null; return `https://viewer.diagrams.net/?lightbox=1&nav=1&edit=_blank#R${encodedXml}`; }, [encodedXml]);
const openInDrawio = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (encodedXml) { const editUrl = `https://app.diagrams.net/?splash=0#R${encodedXml}`; window.open(editUrl, '_blank', 'noopener,noreferrer'); } };
const copyXml = async () => { try { await navigator.clipboard.writeText(xml); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('复制失败:', err); } };
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; 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(); } 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);)", "&");
|
坑 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 不一致
解决方案:
- 在 Prompt 中强调 ID 一致性
- 后端添加 ID 校验逻辑
1 2 3 4 5 6 7
| 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) 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
| location /api/ai/ { proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_buffering off; }
|
1 2 3 4 5 6 7 8 9 10
| @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { configurer.setDefaultTimeout(300000); } }; }
|
性能优化
1. AI 响应缓存
对于相同或相似的绘图请求,可以缓存 AI 响应:
1 2 3 4
| @Cacheable(value = "drawio", key = "#request.hashCode()") public String generateDiagram(DrawioRequest request) { }
|
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 + TypeScript | UI 框架 |
| 图表渲染 | Draw.io iframe | 预览和编辑 |
| XML 压缩 | pako (deflate) | 减少传输体积 |
| AI 模型 | 大语言模型 | 生成 XML |
| Prompt | Prompt Engineering | 控制输出格式 |
后续优化方向
短期优化
- 样式丰富化: 支持更多图形样式和主题配色
- 布局算法: 引入 dagre/ELK 等自动布局算法
- 错误恢复: XML 解析失败时自动重试生成
中期优化
- 模板库: 预设流程图、架构图、时序图等模板
- 导出功能: 支持导出 PNG、SVG、PDF 格式
- 历史记录: 保存生成历史,支持回溯
长期优化
- 多人协作: 基于 WebSocket 的实时协作
- 版本对比: 图表版本对比和合并
- AI 优化: 根据用户反馈优化生成效果
在线体验
想体验的小伙伴可以访问:https://ai.ikunyyds.top
总结
本文详细介绍了如何实现一个 AI 驱动的 Draw.io 智能绘图功能,包括:
- 技术方案选型:对比了 XML 模式和 MCP 模式的优缺点
- 系统架构设计:前后端分离,SSE 流式响应
- Prompt 工程:从失败到成功的迭代优化过程
- 核心代码实现:策略模式、MCP 客户端、XML 解析
- 前端渲染:iframe 嵌入 + pako 压缩
- 踩坑总结:6 个典型问题及解决方案
- 安全与性能:XSS/XXE 防护、限流、缓存
希望这篇文章能帮助到想实现类似功能的小伙伴,如有问题欢迎在评论区交流!
如果这篇文章对你有帮助,欢迎点赞收藏~ 有问题也可以在评论区交流!