next-ai-draw-io 中 draw.io XML Schema 指南:AI 生成图表的完整 XML 契约与校验实现解析
【免费下载链接】next-ai-draw-ioA next.js web application that integrates AI capabilities with draw.io diagrams. This app allows you to create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.项目地址: https://gitcode.com/GitHub_Trending/ne/next-ai-draw-io
本文以仓库中的app/api/chat/xml_guide.md为主体,系统讲解 draw.io(diagrams.net)XML 文件的层级结构、mxCell/mxGeometry属性语义、样式语法与常用图元模式(分组、泳道、表格),并结合lib/utils.ts、app/api/chat/route.ts和hooks/use-diagram-tool-handlers.ts的源码,说明这份指南中每一条规则在 next-ai-draw-io 的 AI 绘图链路中是如何被实际执行、校验和自动修复的。读完后你既能按规范手工编写合法的 draw.io XML,也能理解项目如何用工具调用契约保证 LLM 输出的 XML 可靠渲染。
一、这份指南在项目中的位置
app/api/chat/xml_guide.md是一份面向"如何程序化创建 draw.io 图表"的 XML Schema 参考文档。在 next-ai-draw-io 中,它定义了 LLM 通过display_diagram工具输出图表 XML 时必须遵守的结构契约。这份契约在多处源码中被逐条重申和强制执行:
- chat 路由 中
display_diagram工具的description直接内嵌了指南的 6 条校验规则("Generate ONLY mxCell elements - NO wrapper tags"、"Do NOT include root cells (id="0" or id="1")" 等); - 系统提示词 中的 "Draw.io XML Structure Reference" 一节是同一契约的浓缩版;
- 客户端工具处理器 在收到模型输出后执行截断检测、结构包装与渲染校验。
换句话说,指南描述的不是"理想格式",而是"违反即被拒绝"的硬约束。下文按指南原有脉络逐节展开,并在每个规则处标注其在源码中的落点。
二、基本结构:四层嵌套的文档骨架
draw.io XML 文件的层级结构如下:
<mxfile> <diagram> <mxGraphModel> <root> <mxCell /> <!-- Cells that make up the diagram --> </root> </mxGraphModel> </diagram> </mxfile><mxfile>:文件根元素。<diagram>:文档中的每一页(page)。<mxGraphModel>:该页的图模型数据。<root>:容器,内含本页所有mxCell。
关键机制:模型不需要生成这四层外壳。指南在<root>一节明确指出:生成图表时只需提供mxCell元素,root 容器与两个根单元(id="0"、id="1")会自动添加。这一点在 wrapWithMxFile 中实现——它按输入形态分三种情况处理:
- 已含
<mxfile>的完整结构:原样返回; - 只有
<mxGraphModel>:补上<mxfile><diagram name="Page-1" id="page-1">外壳; - 裸的
mxCell列表(或带<root>包裹):剥离<root>标签、去掉模型误生成的id="0"/id="1"根单元,然后统一拼接为<mxfile><diagram ...><mxGraphModel><root>{ROOT_CELLS}{content}</root></mxGraphModel></diagram></mxfile>,其中ROOT_CELLS恒为<mxCell id="0"/><mxCell id="1" parent="0"/>。
该函数还会定位最后一个合法的mxCell结束位置(/>或</mxCell>),若其后只剩闭合标签(某些模型会附加多余的 wrapper 闭合),则一并裁剪。单测 tests/unit/utils.test.ts 对空输入、裸 XML、完整 XML 三种路径均有覆盖。
2.1 根元素<mxfile>属性
| 属性 | 含义 |
|---|---|
host | 创建文件的应用(如"app.diagrams.net") |
modified | 最后修改时间戳 |
agent | 浏览器 / User-Agent 信息 |
version | 应用版本 |
type | 文件类型(通常"device"或"google") |
示例:
<mxfile host="app.diagrams.net" modified="2023-07-14T10:20:30.123Z" agent="Mozilla/5.0" version="21.5.2" type="device">2.2 页元素<diagram>属性
每个<diagram>代表文档的一页:
id:图表唯一标识;name:页名。
<diagram id="pWHN0msd4Ud1ZK5cD-Hr" name="Page-1">补充:MCP 服务端的 checkDuplicateIds 对多页文档做了细化——
mxCell的 id 只需页内唯一(每页都合法地复用"0"/"1"根单元),而<diagram>的 id 必须跨页唯一。这与本指南针对单页生成的约定一致。
2.3 图模型<mxGraphModel>属性
包含实际的图数据,完整属性及默认惯例如下:
dx/dy:x/y 方向网格大小(通常为 1)grid:是否启用网格(0 或 1)gridSize:网格单元尺寸(通常 10)guides:是否启用参考线(0 或 1)tooltips:是否启用工具提示(0 或 1)connect:是否允许连接(0 或 1)arrows:是否启用箭头(0 或 1)fold:是否启用折叠(0 或 1)page:是否启用页面视图(0 或 1)pageScale:页面缩放(通常 1)pageWidth:页宽(如 850)pageHeight:页高(如 1100)math:是否启用数学排版(0 或 1)shadow:是否启用阴影(0 或 1)
典型示例:
<mxGraphModel dx="1" dy="1" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">其中pageWidth="850"正是系统提示词中"所有元素 x 坐标保持在 0–800、y 坐标保持在 0–600、容器最大 700×550"等布局约束的由来——元素必须落在单页视口内以避免分页(见 system-prompts.ts 布局约束)。
三、<root>容器与两个保留根单元
<root>包含图中所有单元。其内部有两个始终存在的特殊单元(指南"Special Cells"一节):
- Root Cell(
id="0"):所有单元的父节点; - Default Parent Cell(
id="1",parent="0"):默认图层,绝大多数单元的直接父节点。
<root> <mxCell id="0"/> <!-- Auto-added --> <mxCell id="1" parent="0"/> <!-- Auto-added --> <!-- Your mxCell elements go here (start from id="2") --> </root>生成侧的约定是:用户单元一律从id="2"开始编号。这一约定在工具链两端都有硬性保障:
- 删除操作会显式保护根单元:applyDiagramOperations 中
delete操作遇到cell_id为"0"或"1"时直接报错Cannot delete root cell; append_diagram续写时,handleAppendDiagram 会检测模型是否错误地重新输出了<mxGraphModel、<root、<mxfile或<mxCell id="0"/<mxCell id="1"开头的内容,若是则拒绝并要求从截断处继续。
四、mxCell:图的基本构建块
mxCell表示形状、连接线、文本等一切元素。
4.1 通用属性(所有单元)
id:单元唯一标识;parent:父单元 id(顶层单元通常为"1");value:文本内容;style:样式信息(见第五节)。
4.2 形状单元(vertex)专有属性
vertex="1":标记为形状;connectable:是否可被连接(0 或 1)。
4.3 连接线单元(edge)专有属性
edge="1":标记为连接线;source:源单元 id;target:目标单元 id。
4.4 两个可直接复制的最小示例
矩形形状:
<mxCell id="2" value="Hello World" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxGeometry x="350" y="190" width="120" height="60" as="geometry"/> </mxCell>连接线(带源/目标锚点):
<mxCell id="3" value="" style="endArrow=classic;html=1;rounded=0;" edge="1" parent="1" source="2" target="4"> <mxGeometry width="50" height="50" relative="1" as="geometry"> <mxPoint x="400" y="430" as="sourcePoint"/> <mxPoint x="450" y="380" as="targetPoint"/> </mxGeometry> </mxCell>注意
mxPoint必须带as="sourcePoint"/as="targetPoint"属性:convertToLegalXml 会主动移除mxGeometry内部没有as属性的"孤儿"mxPoint,因为这类节点会导致 draw.io 报 "Could not add object mxPoint" 错误;而位于<Array as="points">内的mxPoint(折线路径点)则会被完整保留。
五、mxGeometry:位置与尺寸
5.1 形状单元
x/y:形状左上角的坐标;width/height:宽高;as="geometry":声明该几何信息是主形状定义。
<mxGeometry x="350" y="190" width="120" height="60" as="geometry"/>5.2 连接线
relative="1":相对几何(端点跟随源/目标单元移动);as="geometry"。
<mxGeometry width="50" height="50" relative="1" as="geometry"> <mxPoint x="400" y="430" as="sourcePoint"/> <mxPoint x="450" y="380" as="targetPoint"/> </mxGeometry>折线拐点通过<Array as="points">表达,系统提示词中的正交路由示例即为此用法:
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;entryX=0.5;entryY=0;endArrow=classic;" edge="1" parent="1" source="a" target="b"> <mxGeometry relative="1" as="geometry"> <Array as="points"> <mxPoint x="300" y="150"/> </Array> </mxGeometry> </mxCell>六、style样式参考:分号分隔的键值对
样式写在mxCell的style属性中,格式为分号分隔的key=value对。
6.1 形状样式(shape=...)
| 形状 | 样式片段 |
|---|---|
| 矩形 | shape=rectangle |
| 椭圆 | shape=ellipse |
| 三角形 | shape=triangle |
| 菱形 | shape=rhombus |
| 六边形 | shape=hexagon |
| 云 | shape=cloud |
| 小人(actor) | shape=actor |
| 圆柱 | shape=cylinder |
| 文档 | shape=document |
| 便签 | shape=note |
| 卡片 | shape=card |
| 平行四边形 | shape=parallelogram |
6.2 连接线样式
| 键 | 说明 |
|---|---|
endArrow=classic | 末端箭头类型(classic、open、oval、diamond、block) |
startArrow=none | 起点箭头类型(none、classic、open、oval、diamond) |
curved=1 | 是否曲线连接(0 或 1) |
edgeStyle=orthogonalEdgeStyle | 正交布线 |
elbow=vertical | 肘形方向(vertical、horizontal) |
jumpStyle=arc | 跨线跳线样式(arc、gap) |
jumpSize=10 | 跳线尺寸 |
通用高频片段(来自 STYLE_INSTRUCTIONS):形状常用rounded=1、fillColor=#hex、strokeColor=#hex;文本常用fontSize=14、fontStyle=1(加粗)、align=center/left/right。
专业图标库(AWS、Azure、GCP、Kubernetes 等)的
shape=语法不在本指南范围内,项目通过get_shape_library工具按需加载 docs/shape-libraries 下的分库文档;route.ts 对该工具做了输入消毒(仅允许字母、数字、下划线和连字符)与路径边界校验,防止路径穿越。
七、生成规则清单(指南 Tips 一节)及其源码落点
指南给出 7 条创建规则,其中每一条都能在代码中找到对应的检查或修复实现:
- 只生成 mxCell 元素——外壳与根单元自动添加。落点:wrapWithMxFile。
- id 从 "2" 开始("0"/"1" 保留)。落点:wrapWithMxFile 会主动剥离模型误生成的根单元。
- id 唯一且连续。落点:validateMxCellStructure 的
checkDuplicateIds报告前 3 个重复 id;自动修复侧 autoFixXml 会给重复 id 追加_dupN后缀(仅限非<mxfile>单页输入,多页文档因各页合法复用 0/1 而改为硬性报错)。 - parent 关系正确(顶层用
parent="1")。落点:结构属性重复检查覆盖parent字段(见下)。 - 用 mxGeometry 定位形状。落点:
convertToLegalXml会丢弃没有mxGeometry子节点的mxCell。 - 连接线必须指定 source/target。落点:
checkDuplicateAttributes把edge、parent、source、target、vertex、connectable列为结构属性,重复即报错(STRUCTURAL_ATTRS)。 - 所有 mxCell 必须是兄弟节点,严禁嵌套。落点:
validateMxCellStructure先用 DOM 解析检查parentElement.tagName === "mxCell",再用checkNestedMxCells正则兜底;autoFixXml甚至能"展平"嵌套单元(Flatten 逻辑)。
validateMxCellStructure 完整检查序列(顺序执行、命中即返回错误信息)值得逐一了解,它定义了"什么样的 XML 会被项目拒绝":
| # | 检查项 | 失败时的典型错误信息 |
|---|---|---|
| 0 | DOMParser 语法解析 + DOM 级嵌套 mxCell 检查 | "The XML contains syntax errors (likely unescaped special characters...)" |
| 1 | CDATA 包裹 | "XML is wrapped in CDATA section" |
| 2 | 结构属性重复 | "Duplicate structural attribute(s): ..." |
| 3 | 属性值中未转义的< | "Unescaped < character in attribute values" |
| 4 | 重复 id | "Found duplicate ID(s): ..." |
| 5 | 标签开闭不匹配 | "Expected closing tag ... but found ..." |
| 6 | 非法字符引用(&#x.../&#...) | "Missing semicolon after hex reference" |
| 7 | 注释内含-- | "Comment contains -- which is not allowed" |
| 8 | 未转义&与非法实体名(仅允许 lt/gt/amp/quot/apos) | "Found unescaped & character(s)" |
| 9 | 空 id 属性 | "Found mxCell element(s) with empty id attribute" |
| 10 | 嵌套 mxCell(正则兜底) | "Cells should be siblings, not nested inside other mxCell elements" |
配套的 autoFixXml 提供约 24 步自动修复:还原 JSON 转义(=\"→")、去 CDATA、去外部标签、修复<Cell>/</mxElement>等标签名拼写错误、补全未闭合标签、删除多余闭合标签、移除尾部垃圾、展平嵌套单元、重命名重复 id、为缺失 id 生成cell_时间戳_N、最后对仍无法解析的"病态"单元逐行丢弃(上限 10 次迭代防止死循环)。validateAndFixXml则组合为"先验证、失败则修复、再验证"的完整闭环。MCP 包内是 独立拷贝(注释说明为避免跨包导入),与lib/utils.ts同源。
八、常用模式(Common Patterns)
8.1 分组(Grouping)
创建父容器单元,子单元通过parent指向它:
<!-- Group container --> <mxCell id="10" value="Group" style="group" vertex="1" connectable="0" parent="1"> <mxGeometry x="200" y="200" width="200" height="100" as="geometry" /> </mxCell> <!-- Elements inside the group --> <mxCell id="11" value="Element 1" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="10"> <mxGeometry width="90" height="40" as="geometry" /> </mxCell> <mxCell id="12" value="Element 2" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="10"> <mxGeometry x="110" width="90" height="40" as="geometry" /> </mxCell>注意容器上connectable="0"使分组框本身不可作为连接端点;子单元的mxGeometry坐标是相对于容器的局部坐标(id="11"未写 x/y 即默认 0,0)。
8.2 泳道(Swimlanes)
使用swimlane样式。指南强调:泳道、步骤与连线必须全部是<root>下的兄弟节点,连线不得嵌套在泳道或步骤内部:
<root> <mxCell id="0"/> <mxCell id="1" parent="0"/> <!-- Swimlane 1 --> <mxCell id="lane1" value="Frontend" style="swimlane;startSize=30;" vertex="1" parent="1"> <mxGeometry x="40" y="40" width="200" height="300" as="geometry"/> </mxCell> <!-- Swimlane 2 --> <mxCell id="lane2" value="Backend" style="swimlane;startSize=30;" vertex="1" parent="1"> <mxGeometry x="280" y="40" width="200" height="300" as="geometry"/> </mxCell> <!-- Step inside lane1 (parent="lane1") --> <mxCell id="step1" value="Send Request" style="rounded=1;" vertex="1" parent="lane1"> <mxGeometry x="20" y="60" width="160" height="40" as="geometry"/> </mxCell> <!-- Step inside lane2 (parent="lane2") --> <mxCell id="step2" value="Process" style="rounded=1;" vertex="1" parent="lane2"> <mxGeometry x="20" y="60" width="160" height="40" as="geometry"/> </mxCell> <!-- Edge connecting step1 to step2 (sibling element, NOT nested inside steps) --> <mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2"> <mxGeometry relative="1" as="geometry"/> </mxCell> </root>要点拆解:startSize=30是泳道标题栏高度;步骤的parent指向泳道 id(lane1/lane2),坐标为泳道内局部坐标;而连线edge1的parent="1"、source/target跨泳道引用步骤 id——这正是"层级关系用 parent 表达、拓扑关系用 source/target 表达"的范例,且所有mxCell平铺在<root>下。
8.3 表格(Tables)
表格由带父子关系的多个单元组成,表头行用shape=table,数据行用shape=tableRow:
<mxCell id="30" value="Table" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" vertex="1" parent="1"> <mxGeometry x="200" y="200" width="180" height="120" as="geometry" /> </mxCell> <mxCell id="31" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[0,0.5,1,0.5];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" vertex="1" parent="30"> <mxGeometry y="30" width="180" height="30" as="geometry" /> </mxCell>关键样式键:container=1使表格成为容器;childLayout=tableLayout启用表格局部布局(单元格自动排列);startSize=30是表头行高;行的mxGeometry y="30"紧随表头之后。
九、进阶特性(Advanced Features)
9.1 自定义属性(Custom Attributes)
draw.io 允许在单元内挂载<Object>元素以保存附加元数据,可被插件和自定义行为读取:
<mxCell id="40" value="Custom Element" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1"> <mxGeometry x="200" y="200" width="120" height="60" as="geometry"/> <Object label="Custom Label" customAttr="value" /> </mxCell>9.2 用户自定义样式
通过组合样式键得到自定义外观,例如带六边形外轮廓、固定尺寸、配色与字体的单元:
<mxCell id="50" value="Custom Styled Cell" style="shape=hexagon;perimeter=hexagonPerimeter2;whiteSpace=wrap;html=1;fixedSize=1;fillColor=#f8cecc;strokeColor=#b85450;strokeWidth=2;fontSize=14;fontStyle=1" vertex="1" parent="1"> <mxGeometry x="300" y="200" width="120" height="80" as="geometry"/> </mxCell>各键语义:perimeter=hexagonPerimeter2决定连接锚点沿六边形哪组边分布;fixedSize=1锁定形状比例;fillColor/strokeColor/strokeWidth控制填充、描边色与线宽;fontSize/fontStyle控制字号与加粗。
9.3 图层(Layers)
通过额外挂接在根单元(id="0")下的单元创建多个图层来组织复杂图表:
<!-- Default layer (always present) --> <mxCell id="1" parent="0"/> <!-- Additional custom layer --> <mxCell id="60" value="Layer 2" style="locked=0;group=" parent="0"/> <!-- Elements in Layer 2 --> <mxCell id="61" value="Element in Layer 2" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="60"> <mxGeometry x="200" y="300" width="120" height="60" as="geometry"/> </mxCell>图层单元parent="0"(而非"1"),其上元素再把parent指向图层 id(60)。
十、指南规则在完整工具链中的执行路径
把前面各节的落点串起来,一次display_diagram调用的完整生命周期是:
- 模型输出:模型按指南契约只输出裸
mxCell序列(id 从 "2" 起、全为兄弟节点、属性转义合法); - 截断检测:handleDisplayDiagram 用 isMxCellXmlComplete 判断输出是否完整——该函数定位最后一个
/>或</mxCell>,其后若只剩闭合标签/空白才视为完整;不完整则暂存部分 XML,向模型返回截断尾部 500 字符并指引其调用append_diagram从断点续写; - 续写组装:
append_diagram追加片段后再次检测完整性,直到最后一个 mxCell 闭合才进入渲染(handleAppendDiagram); - 结构包装:
wrapWithMxFile补齐四层外壳与根单元,得到可加载的完整<mxfile>; - 加载与校验:
onDisplayChart内部经loadDiagram加载,结构层面由validateMxCellStructure十项检查把关,失败时把具体错误信息与完整 XML 回传给模型要求重生成(use-diagram-tool-handlers.ts); - 视觉验证(可选):渲染后截图交给视觉模型校验布局问题(重叠、连线穿框),最多重试 3 次,超限则带警告接受(handleDisplayDiagram,反馈格式化逻辑见 formatValidationFeedback)。
编辑路径同样由 id 契约支撑:edit_diagram的 update/add/delete 操作在 applyDiagramOperations 中执行,其中 update/add 要求new_xml的id与cell_id严格一致(ID mismatch 即报错),delete 会自动级联删除子单元与所有source/target指向它的连线(含连线子节点如标签),且被级联删除的单元再次出现在操作列表中时会被静默跳过而非报错。
十一、相关源码与文档索引
| 内容 | 路径 |
|---|---|
| XML Schema 指南(本文主体) | app/api/chat/xml_guide.md |
| 包装/校验/修复/编辑核心实现 | lib/utils.ts |
| chat API 与工具定义 | app/api/chat/route.ts |
| 客户端工具处理器 | hooks/use-diagram-tool-handlers.ts |
| 视觉校验反馈格式化 | lib/diagram-validator.ts |
| 系统提示词(契约浓缩版) | lib/system-prompts.ts |
| MCP 端校验/修复拷贝 | packages/mcp-server/src/xml-validation.ts |
| 单测(wrapWithMxFile 等) | tests/unit/utils.test.ts |
| 专业形状库文档 | docs/shape-libraries/README.md |
适用前提:以上规则对应当前仓库的实现状态。wrapWithMxFile生成的默认页参数(Page-1/page-1、单页结构)意味着 AI 生成路径面向单页图表;多页文档的 id 作用域差异只在 MCP 服务端的校验逻辑中有特殊处理,编写 XML 时以指南的单页约定为准即可。
【免费下载链接】next-ai-draw-ioA next.js web application that integrates AI capabilities with draw.io diagrams. This app allows you to create, modify, and enhance diagrams through natural language commands and AI-assisted visualization.项目地址: https://gitcode.com/GitHub_Trending/ne/next-ai-draw-io
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考