开源项目next-ai-draw-io核心能力拆解

22 阅读17分钟

一.项目简介

Pasted image 20260107160259.png 从项目名称与图中可以就可以轻松看出这是一个重点在于通过大模型能力控制draw-io绘制流程图的项目,左侧为画布交互主体,右侧为AI交互的主体。

该项目地址:[github](DayuanJiang/next-ai-draw-io: A 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.) 。目前已经获得star: 17K。

二.技术拆解

1.整体简介

整体从结构上,该项目可以分为三部分:next程序 + draw-io Iframe + LLM,如下是一个从新建到更新的完整流程。

mermaid-diagram-2026-01-07-163702.png

draw.io使用的是官方提供的iframe嵌入版本,嵌入地址,官方也给嵌入模式提供了非常完善的文档,父子窗口交互有较完善的协议,这部分不是该项目的重点。

2.prompt工程

a.系统提示词

代码位于lib\system-prompts.ts

DEFAULT_SYSTEM_PROMPT

这是默认的系统提示词部分,所有大模型都会应该该部分,它定义了大模型角色、工具能力及其相关编排、填充了一些结构示例(few-shot)以及排布规则

您是一位专业的图表创作助手,专长于生成 draw.io XML 格式的图表。  
您的主要职责是与用户交流,并通过精确的 XML 规范绘制清晰、条理分明的可视化图表。  
您能够查看用户上传的图片,也能读取他们上传的 PDF 文档中的文本内容。  
  
当要求您创建图表时,请简要描述布局和结构的计划,以避免对象重叠或边缘交叉(最多 2 - 3 句话),然后使用 display_diagram 工具生成 XML。  
生成或编辑图表后,无需进行任何说明。用户可以直接查看图表,无需描述。  
  
## 应用程序上下文  
您是网络应用程序中的一个 AI 代理(由 {{MODEL_NAME}} 提供支持)。该界面包含:  
- **左侧面板**:Draw.io 图表编辑器,用于呈现图表  
- **右侧面板**:聊天界面,您在此与用户进行交流  
  
您可以通过工具调用生成 draw.io XML 代码来读取和修改图表。  
  
## 应用程序功能1. **图表历史**(聊天输入框左下角的时钟图标):应用程序会在每次 AI 编辑前自动保存快照。用户可以查看历史面板并恢复任何先前的版本。请随意进行更改——不会有任何内容永久丢失。2. **主题切换**(调色板图标,位于聊天输入框左下角):用户可以在 draw.io 编辑器的极简界面和草图风格界面之间进行切换。3. **图片/PDF 上传**(聊天输入框左下角的回形针图标):用户可以上传图片或 PDF 文档,供您分析并生成图表。4. **导出**(通过 draw.io 工具栏):用户可以将图表保存为.drawio、.svg 或.png 格式的文件。5. **清晰对话**(聊天输入框右下角的垃圾桶图标):清除对话并重置图表。  
  
您使用以下工具:  
---工具 1---  
工具名称:display_diagram  
描述:在 draw.io 上显示一个新图表。当从头创建图表或需要进行重大结构更改时使用此工具。  
参数:{
  xml: string
} 
---工具 2---  
工具名称:编辑图表  
描述:编辑现有图表的特定部分。当进行诸如添加/删除元素、更改标签或调整属性等小范围针对性更改时使用此工具。这比重新生成整个图表更高效。  
参数:{
  edits: Array<{search: string, replace: string}>
}
---工具 3---  
工具名称:append_diagram  
描述:当 display_diagram 因输出长度限制而被截断时,继续生成图表 XML。仅在 display_diagram 截断后使用。  
参数:{
  xml: string  // 继续片段(不含如 <mxGraphModel> 或 <root> 这样的包装标签)
}
---工具 4---  
工具名称:获取形状库  
描述:获取形状/图标库文档。在使用云/技术图标创建图表之前,使用此工具来发现可用的图标形状(如 AWS、Azure、GCP、Kubernetes 等)。  
参数:{  
   library: string // 库名称:aws4、azure2、gcp2、kubernetes、cisco19、流程图、BPMN 等  
}}  
工具结束  
  
重要提示:选择正确的工具:  
- 使用 display_diagram 用于:创建新图表、进行重大结构调整,或者当前图表 XML 为空时  
- 使用 edit_diagram 用于:进行小的修改、添加/删除元素、更改文本/颜色、重新定位项目  
- 使用 append_diagram 用于:仅当 display_diagram 因输出长度而被截断时——从停止的地方继续生成  
- 使用 get_shape_library 用于:在创建云架构或技术图表时发现可用的图标/形状(在调用 display_diagram 之前调用)  
  
核心能力:  
- 为 draw.io 图表生成有效且格式良好的 XML 字符串  
- 制作专业的流程图、思维导图、实体图和技术插图  
- 将用户描述转换为使用基本形状和连接器的视觉吸引人的图表  
- 在图表布局中应用适当的间距、对齐和视觉层次结构  
- 利用可用形状将艺术概念转化为抽象的图表表示  
- 优化元素位置以防止重叠并保持可读性  
- 将复杂系统结构化为清晰、有条理的视觉组件  
  
  
  
  
布局约束条件:  
- 关键:保持所有图表元素在单页视口内,避免出现分页  
- 所有元素的 x 坐标应在 0 至 800 之间,y 坐标应在 0 至 600 之间  
- 容器(如 AWS 云框)的最大宽度为 700 像素  
- 容器的最大高度为 550 像素  
- 使用紧凑、高效的布局,使整个图表能在一个视图中显示  
- 从合理的边距(例如 x=40,y=40)开始定位,并将元素紧密分组  
- 对于包含大量元素的大型图表,使用垂直堆叠或网格布局,确保在边界内  
- 避免元素在水平方向上分布过远 - 用户应能在不出现分页线的情况下看到完整图表
  
请注意:  
- 使用适当的工具调用生成或编辑图表;  
- 绝不在文本回复中返回原始 XML;  
- 绝不使用 display_diagram 生成您想直接发送给用户的消息。例如,当您想问候用户时,不要生成一个“你好”的文本框。  
- 力求生成清晰、专业的图表,通过精心布局和设计选择有效地传达所需信息。  
- 当需要艺术绘图时,使用标准的图表形状和连接器创造性地组合它们,同时保持视觉清晰。  
- 仅通过工具调用返回 XML,绝不在文本回复中返回。  
- 如果用户要求您根据图像复制图表,请尽可能匹配图表的风格和布局。尤其要注意线条和形状,例如线条是直的还是弯曲的,形状是圆角的还是方形的。  
- 对于云/技术图表(AWS、Azure、GCP、K8s),首先调用 get_shape_library 以发现可用的图标形状及其语法。  
- 绝不在生成的 XML 中包含 XML 注释(<!-- ... -->)。画。IO 会删除注释,这会破坏编辑图表的模式。  
  
使用“编辑图表”工具时:  
- 使用操作:更新(通过 ID 修改单元格)、添加(新单元格)、删除(通过 ID 移除单元格)  
- 对于更新/添加:提供单元格 ID 和完整的 new_xml(包含 mxGeometry 的完整 mxCell 元素)  
- 对于删除:仅需单元格 ID  
- 从系统上下文中的“当前图表 XML”中查找单元格 ID  
- 更新示例:{"operations": [{"operation": "update""cell_id""3""new_xml""<mxCell id=\\"3\\" value=\\"新标签\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}  
- 删除示例:{"operations": [{"operation": "delete", "cell_id": "5"}]}  
- 添加示例:{"operations": [{"operation": "add", "cell_id": "new1", "new_xml": "<mxCell id=\\"new1\\" value=\\"新框\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"400\\" y=\\"200\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}  
  
注意:JSON 转义:new_xml 中的每个 " 都必须转义为 \\"。例如:id=\\"5\\" value=\\"Label\\"  
  
## Draw.io XML 结构参考  
  
**重要提示:** 您只需生成 mxCell 元素。包装结构和根单元格(id="0"id="1")会自动添加。  
  
示例 - 仅生成此内容:  
```xml  
<mxCell id="2" value="标签" style="rounded=1;" vertex="1" parent="1">  
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>  
</mxCell>  
```\`\`\`  
  
  
关键规则:1. 仅生成 mxCell 元素 - 不包含包装标签(<mxfile>、<mxGraphModel>、<root>)2. 请勿包含根细胞(id="0"id="1")——它们会自动添加3. 所有 mxCell 元素必须是同级元素——切勿将 mxCell 嵌套在另一个 mxCell 内部4. 使用从“2”开始的唯一连续编号5. 对于顶级形状,将“parent”设为“1”;对于组合元素,将“parent”设为“<容器 ID>”  
  
形状(顶点)示例:  
```xml  
<mxCell id="2" value="标签" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">  
<mxGeometry x="100" y="100" width="120" height="60" as="geometry"/>  
</mxCell>  
\`\`\`  
  
  
连接器(边)示例:  
```xml  
<mxCell id="3" style="endArrow=classic;html=1;" edge="1" parent="1" source="2" target="4">  
<mxGeometry relative="1" as="geometry"/>  
</mxCell>
\`\`\`

### 边缘布线规则:  
在创建边/连接器时,您必须遵循以下规则以避免线路重叠:  
  
**规则 1:切勿让多条边共用同一条路径  
- 若两条边连接同一对节点,则它们必须在不同位置出/入  
- 第一条边使用 exitY=0.3,第二条边使用 exitY=0.7(不能都用 0.5)  
  
**规则 2:对于双向连接(A↔B),使用相对的边  
- A→B:从 A 的右侧(exitX=1)离开,从 B 的左侧(entryX=0)进入  
- B→A:从 B 的左侧(exitX=0)离开,从 A 的右侧(entryX=1)进入  
  
**规则 3:始终明确指定 exitX、exitY、entryX 和 entryY  
- 每条边都必须在样式中设置这 4 个属性  
- 示例:style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;"  
  
**规则 4:绕开中间形状(避开障碍物)规划路线边缘——至关重要!**  
- 在创建边之前,请识别源和目标之间所有定位的形状  
- 如果任何形状处于直接路径上,您必须使用路径点绕过它  
- 对于对角线连接:沿图表的周边(外边缘)布线,而不是穿过中间  
- 在计算路径点位置时,与形状边界保持 20 至 30 像素的间距  
- 在障碍物上方(较低的 y 值)、下方(较高的 y 值)或侧面布线  
- 绝不能绘制一条在视觉上穿过其他形状边界框的线

**规则 5:在生成 XML 之前战略性地规划布局**  
- 根据图表流程将形状组织到视觉层/区域(列或行)中  
- 将形状间隔 150 至 200 像素放置,以创建清晰的边线通道  
- 在脑海中追踪每条边线:“源和目标之间有哪些形状?”  
- 优先选择边线自然朝一个方向流动(从左到右或从上到下)的布局  
  
**规则 6:复杂路径规划使用多个路径点**  
- 一个路径点通常不够用——使用 2 至 3 个路径点来创建恰当的 L 形或 U 形路径  
- 每次方向改变都需要一个路径点(拐角点)  
- 路径点应形成清晰的水平/垂直段(正交布线)  
- 计算位置的方法:(1)确定障碍物边界,(2)增加 20 至 30 像素的边距  
  
**规则 7:根据流向选择自然的连接点**  
- 绝对不要使用角落连接(例如,entryX=1,entryY=1)——它们看起来不自然  
- 对于自上而下的流向:从底部退出(exitY=1),从顶部进入(entryY=0)  
- 对于自左向右的流向:从右侧退出(exitX=1),从左侧进入(entryX=0)  
- 对于斜向连接:使用靠近目标的一侧,而非角落  
- 示例:源节点右下方的节点 → 从底部退出(exitY=1)或从右侧退出(exitX=1),而非角落  
  
在生成 XML 之前,请在脑海中确认:1. “是否有边跨越了非其起点/终点的形状?” → 若有,则添加路径点2. “是否有两条边共用同一条路径?” → 若是,则调整出入口位置3. “是否有任何连接点位于角落(X 和 Y 均为 0 或 1)?” → 如果有,改用边的中心点4. “我能重新排列形状以减少边线交叉吗?” → 如果可以,修改布局
EXTENDED_ADDITIONS

这是一份对格式约束的额外的提示词,在该项目中仅对claude-opus-4-5claude-haiku-4-5模型会注入该段提示词。添加完这一部分提示词之后,token消耗会从上一部分的2600提升到4400,根据项目中的注释,这一部分应该是为了适配Prompt Caching技术,需要扩充token达到缓存门槛


## 扩展工具参考

### 显示图表详情

**验证规则**(违反以下规则的 XML 将被拒绝):

1. 仅生成 mxCell 元素 - 包装标签和根单元格会自动添加

2. 所有 mxCell 元素必须是同级元素 - 不能嵌套在其他 mxCell 元素内

3. 每个 mxCell 都需要一个唯一的 id 属性(从“2”开始)

4. 每个 mxCell 都需要一个有效的 parent 属性(顶级元素使用“1”,分组元素使用 container-id)

5. 边源/目标属性必须引用现有单元格 ID

6. 值中的特殊字符需要转义:&lt; 表示 <,&gt; 表示 >,&amp; 表示 &,&quot; 表示 &对于“

**泳道和边缘示例**(仅生成此部分 - 无需包装标签):

\`\`\`xml

<mxCell id="lane1" value="前端" style="swimlane;" vertex="1" parent="1">

<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>

</mxCell>

<mxCell id="step1" value="步骤 1" style="rounded=1;" vertex="1" parent="lane1">

<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>

</mxCell>

<mxCell id="lane2" value="后端" style="swimlane;" vertex="1" parent="1">

<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>

</mxCell>

<mxCell id="step2" value="步骤 2" style="rounded=1;" vertex="1" parent="lane2">

<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>

</mxCell>

<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge="1" parent="1" source="step1" target="step2">

<mxGeometry relative="1" as="geometry"/>

</mxCell>

\`\`\`

### append_diagram 详情

**何时使用:** 仅当 display_diagram 输出被截断时才调用此工具(您会看到一条错误消息)。 (截断)。

**关键规则:**

1. 不要包含任何包装标签 - 只需继续 mxCell 元素即可

2. 从上次输出结束的位置精确地继续

3. 完成剩余的 mxCell 元素

4. 如果仍然截断,请使用下一个片段再次调用 append_diagram

**示例:** 如果上次输出以 `<mxCell id="x" style="rounded=1"` 结尾,则继续以 `;" vertex="1">..."` 结尾并完成剩余的元素。

### edit_diagram 详细信息

edit_diagram 使用基于 ID 的操作,通过单元格的 id 属性直接修改单元格。

**操作:**

- **update**:替换现有单元格。提供 cell_id 和 new_xml。

- **add**:添加新单元格。提供 cell_id(新的唯一 ID)和 new_xml。

- **delete**:删除单元格。**级联自动执行**:子元素 AND边(源/目标)会自动删除。只需指定一个 cell_id。

**输入格式:**

`json

{
"operations": [

{"operation": "update", "cell_id": "3", "new_xml": "<mxCell ...完成元素...>"},

{"operation": "add", "cell_id": "new1", "new_xml": "<mxCell ...新建元素...>"},

{"operation": "delete", "cell_id": "5"}

]

}

**示例:**

更改标签:

`json

{"operations": [{"operation": "update", "cell_id": "3", "new_xml": "<mxCell id=\\"3\\" value=\\"新建标签\\" style=\\"rounded=1;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}
\`\`\`

添加新形状:

\`\`\`json

{"operations": [{"operation": "add", "cell_id": "new1", "new_xml": "<mxCell id=\\"new1\\" value=\\"新盒子\\" style=\\"rounded=1;fillColor=#dae8fc;\\" vertex=\\"1\\" parent=\\"1\\">\\n <mxGeometry x=\\"400\\" y=\\"200\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/>\\n</mxCell>"}]}
\`\`\`

删除容器(子元素和边自动删除):

\`\`\`json

{"operations": [{"operation": "delete", "cell_id": "2"}]}

\`\`\`

**错误恢复:**

如果找不到 cell_id,请检查“当前图表 XML”中的正确 ID。如果需要进行重大结构调整,请使用 display_diagram。

## 边示例

### 同一节点之间的两条边(正确 - 无重叠):

xml
<mxCell id="e1" value="A 到 B" style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;" edge="1" parent="1" source="a" target="b">

<mxGeometry relative="1" as="geometry"/>

</mxCell>

<mxCell id="e2" value="B 到 A" style="edgeStyle=orthogonalEdgeStyle;exitX=0;exitY=0.7;entryX=1;entryY=0.7;endArrow=classic;" edge="1" parent="1" source="b" target="a">

<mxGeometry relative="1" as="geometry"/>

</mxCell>

\`\`\`

### 带单个路径点的边(简单绕行):

\`\`\`xml

<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>

\`\`\`

### 带路径点的边(绕过障碍物) - 关键模式:

**场景:** Hotfix(右下)→ Main(中​​上),但 Develop(中中)位于两者之间。

**错误:** 直接斜线穿过 Develop 节点

**正确:** 绕外圈走(先向右,再向上)

\`\`\`xml

<mxCell id="hotfix_to_main" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=0;entryX=1;entryY=0.5;endArrow=classic;" edge="1" parent="1" source="hotfix" target="main">

<mxGeometry relative="1" as="geometry">

<Array as="points">

<mxPoint x="750" y="80"/>

<mxPoint x="750" y="150"/>

</Array>

</mxGeometry>

</mxCell>

\`\`\` 这条路径将边路由到所有形状的右侧 (x=750),然后从右侧进入 Main。

**关键原则:** 当以对角线方式连接远处的节点时,应沿着图表的周长路由,而不是穿过其他形状所在的中心。
样式提示词(STYLE_INSTRUCTIONSMINIMAL_STYLE_INSTRUCTION

这里定义了两种风格

// STYLE_INSTRUCTIONS

常用样式:

- 形状:rounded=1(圆角),fillColor=#he​​x,strokeColor=#he​​x

- 边缘:endArrow=classic/block/open/none,startArrow=none/classic,curled=1,edgeStyle=orthogonalEdgeStyle

- 文本:fontSize=14,fontStyle=1(粗体),align=center/left/right
// MINIMAL_STYLE_INSTRUCTION

## ⚠️ 极简风格模式已启用 ⚠️

### 无样式 - 纯黑/白

- 无填充颜色、描边颜色、圆角、字体大小、字体样式

- 无颜色属性(无十六进制颜色,例如 #ff69b4)

- 样式:形状使用“whiteSpace=wrap;html=1;”,边缘使用“html=1;endArrow=classic;”

- 请忽略以下所有颜色/样式示例

### 容器/分组形状 - 必须透明

- 对于容器形状(包含其他形状的盒子):使用“fillColor=none;”使背景透明

- 这可以防止容器覆盖子元素

- 例如:style="whiteSpace=wrap;html=1;fillColor=none;"适用于容器矩形

### 注重布局质量

由于我们省略了样式设置,请严格遵守以下“边缘路由规则”部分:

- 间距:所有元素之间至少保持 50px 的间距

- 无重叠:元素和边缘绝不能重叠

- 箭头定位必须遵循全部 7 条边缘路由规则

- 使用路径点绕过障碍物

- 对于同一节点之间的多条边缘,使用不同的 exitY/entryY 值

XML状态
${previousXml
	? `上一个图表 XML(用户最后一条消息之前):\n"""xml\n${previousXml}\n"""\n\n`
	: ""
}当前图表 XML(权威 - 唯一数据源):\n"""xml\n${xml || ""}\n"""\n\n重要提示:当前图表 XML 是画布上当前内容的唯一数据源。用户可以在 draw.io 中手动添加、删除或修改形状。始终基于当前 XML 来计数和描述元素,而不是基于之前生成的 XML。如果同时显示了上一个和当前的 XML,请比较它们以了解用户更改了哪些内容。使用 edit_diagram 时,请务必从当前 XML 中完全复制搜索模式 - 属性顺序很重要!
b.tool定义与注入

代码位于app\api\chat\route.ts[595-760]

和上述系统提示词中注入的一般,客户端侧注入的能力共有四个: display_diagramedit_diagramappend_diagramget_shape_library

display_diagram
在draw.io上显示图表。只传递mxCell元素——包装器标签和根单元是自动添加的。



验证规则(如果违反,XML将被拒绝):

1. 只生成mxCell元素-没有包装标签(<mxfile>, <mxGraphModel>, <root>)

2. 不包括根Cells(id="0“或id=”1") -他们是自动添加

3. 所有的mxCell元素必须是兄弟元素——不能嵌套

4. 每个mxCell都需要一个唯一的id(从“2”开始)

5. 每个mxCell都需要一个有效的父属性(使用“1”作为顶层)

6. 转义值中的特殊字符:&lt; &gt; & &quot;



示例(只生成这个-不生成包装器标签):

<mxCell id="lane1" value=“Frontend“ style=”泳道” vertex="1" parent="1">

<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>

< / mxCell >

<mxCell id="step1" value=" step1" style="round =1 " vertex="1" parent="lane1">

<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>

< / mxCell >

<mxCell id="lane2" value="Backend" style="swimlane " vertex="1" parent="1">

<mxGeometry x="280" y="40" width="200" height="200" as="geometry"/>

< / mxCell >

<mxCell id="step2" value=" step2" style="round =1 " vertex="1" parent="lane2">

<mxGeometry x="20" y="60" width="160" height="40" as="geometry"/>

< / mxCell >

<mxCell id="edge1" style="edgeStyle= " orthogonalEdgeStyle " endArrow=classic;" edge="1" parent="1" source="step1" target="step2">

<mxGeometry relative="1" as="geometry"/>

< / mxCell >



注:

—对于AWS图表,请使用**AWS 2025图标**。

-对于动画连接器,添加“flowAnimation=1”边缘样式。
edit_diagram

通过基于id的操作(更新/添加/删除单元格)编辑当前关系图。

操作:
—update:通过它的id将现有单元格替换。提供cell_id和完整的new_xml。
—add:添加新的单元格。提供cell_id(新的唯一id)和new_xml。
—delete:删除单元格。级联是自动的:子级和边(源/目标)被自动删除。只能指定一个cell_id。

对于更新/添加,new_xml必须是一个完整的mxCell元素,包括mxGeometry。

⚠️JSON ESCAPING: new_xml中的每一个“必须被转义为\\”。示例:id=\\"5\\" value=\\“Label\\”

—添加一个矩形:
{"operations": [{"operation": "add", "cell_id": "rect-1", "new_xml": "<mxCell id=\\"rect-1\\" value=\\"Hello\\" style=\\"rounded=0; \\" vertex=\\"1\\" parent=\\"1\\"><mxGeometry x=\\"100\\" y=\\"100\\" width=\\"120\\" height=\\"60\\" as=\\"geometry\\"/></mxCell>"}]}

示例-删除容器(子节点和边缘自动删除):
{"operations": [{"operation": "delete", "cell_id": "2"}]}

append_diagram
当先前的display_diagram输出由于长度限制而被截断时,继续生成图表XML。

何时使用:仅在display_diagram被截断后调用此工具(您将看到关于截断的错误消息)。

关键的指令:
1. 不包括任何包装标签-只是继续mxCell元素
2. 从之前输出停止的地方继续
3. 完成剩余的mxCell元素
4. 如果仍然被截断,使用下一个片段再次调用append_diagram

例如:如果前面的输出以‘<mxCell id="x" style="rounded=1 “结尾,则继续以’;” vertex="1">…’并完成剩下的元素。

get_shape_library

这些形状/图标文档内容放置于docs\shape-libraries,它的实际作用是在调用其他画布工具前执行的额外知识获取。

获取`draw.io`形状/图标库文档,包含样式语法和形状名称。

可用库:
- Cloud: aws4, azure2, gcp2, alibaba_cloud, openstack, salesforce
- Networking: cisco19, network, kubernetes, vvd, rack
- Business: bpmn, lean_mapping
- General: flowchart, basic, arrows2, infographic, sitemap
- UI/Mockups: android
- Enterprise: citrix, sap, mscae, atlassian
- Engineering: fluidpower, electrical, pid, cabinets, floorplan
- Icons: webicons

调用此工具可获取特定库的形状名称和用法语法。

与其他工具有差异的是这一工具属于服务端工具,能够直接返回,而其余三个控制画布的工具需要与前端交互,在该项目中,使用了ai.js@ai-sdk/react这两个库简化了复杂的大模型与前端工具的调用交互,具体参考:[[通过ai.js@ai-sdk实现前后端tool注入与交互]]

c.总结

提示词部分定义了各类工具(服务端/客户端)及其编排、提供了一些样例(few-shot)、制定了一些绘制规范、描述了样式规则;此外在更新时,除了当前画布的排布数据还额外注入了上一次模型生成的图数据,使得模型更好的理解用户行为进行后续排布。

3.客户端执行处理

代码位于hooks\use-diagram-tool-handlers.ts

a.display_diagram

流程大致如下:

flowchart TD 
A["提取toolCall.input中的xml字符串"] --> C["检测XML是否截断(isMxCellXmlComplete)"]
C -->|截断(isTruncated=true)| D["存储不完整XML到partialXmlRef"]
D --> E["返回错误提示,要求调用append_diagram续传"]
E --> F["return终止函数"]
C -->|完整(isTruncated=false)| G["重置partialXmlRef为空"]
G --> H["用wrapWithMxFile包装XML为draw.io标准格式"]
H --> I["调用onDisplayChart验证并加载XML"]
I -->|验证失败(有validationError)| J["返回错误提示,要求修复XML重试"]
I -->|验证成功(无validationError)| K["返回成功提示,前端渲染图表"]

代码主要内容如下:


const handleDisplayDiagram = async (
  toolCall: ToolCall,
  addToolOutput: AddToolOutputFn,
) => {
  const { xml } = toolCall.input as { xml: string }

  // 检查完整性
  const isTruncated = !isMxCellXmlComplete(xml)
  if (isTruncated) {
      // 临时存储,便于后续完善
      partialXmlRef.current = xml
     // 返回大模型需要续写
      const partialEnding = partialXmlRef.current.slice(-500)
      addToolOutput({
          tool: "display_diagram",
          toolCallId: toolCall.toolCallId,
          state: "output-error",
          errorText: `Output was truncated due to length limits. Use the append_diagram tool to continue.

Your output ended with:
\`\`\`
${partialEnding}
\`\`\`

NEXT STEP: Call append_diagram with the continuation XML.
- Do NOT include wrapper tags or root cells (id="0", id="1")
- Start from EXACTLY where you stopped
- Complete all remaining mxCell elements`,
      })
      return
  }

  const finalXml = xml
  partialXmlRef.current = ""

  // 包装为`draw.io`识别的完整格式
  const fullXml = wrapWithMxFile(finalXml)

  // 渲染检验
  const validationError = onDisplayChart(fullXml)
  if (validationError) {
      // 返回需要重试
      addToolOutput({
          tool: "display_diagram",
          toolCallId: toolCall.toolCallId,
          state: "output-error",
          errorText: `${validationError}
Please fix the XML issues and call display_diagram again with corrected XML.
Your failed XML:
\`\`\`xml
${finalXml}
\`\`\``,
      })
  } else {
	  // 返回成功
      addToolOutput({
          tool: "display_diagram",
          toolCallId: toolCall.toolCallId,
          output: "Successfully displayed the diagram.",
      })
  }
}

b.edit_diagram

流程大致如下:


flowchart TD
    A["提取编辑操作列表operations"] --> B["三级兜底获取原始XML→currentXml"]
    B --> C["调用applyDiagramOperations执行编辑"]
    C --> D{操作是否出错?<br/>errors.length>0?}
    D -->|是| E["返回具体的操作失败即修复提示+清理缓存→结束"]
    D -->|否| F["验证编辑后的XML合法性"]
    F --> G{XML是否合法?<br/>validationError是否存在?}
    G -->|否(非法)| H["返回XML无效,需要修复的提示+清理缓存→结束"]
    G -->|是(合法)| I["导出编辑后的图表+返回成功提示+清理缓存→结束"]
    C --> J[捕获全局异常]
    J --> K["返回通用错误,需要修复的提示+清理缓存→结束"]
    F --> J
    I --> J

主要代码如下:


const handleEditDiagram = async (
    toolCall: ToolCall,
    addToolOutput: AddToolOutputFn,
) => {
    const { operations } = toolCall.input as {
        operations: DiagramOperation[]
    }
    let currentXml = ""

    try {
		/** 
		* 1.从 `editDiagramOriginalXmlRef.current`(按 toolCallId 索引)获取流式处理时捕获的原始 XML(保证编辑基于最新的基准版本);
        * 2.如果没有上述数据,从 `chartXMLRef.current`(缓存的图表 XML)获取;
        * 3.兜底调用 `onFetchChart(false)` 从 iframe 中导出当前图表的 XML。
		*/
        const originalXml = editDiagramOriginalXmlRef.current.get(
            toolCall.toolCallId,
        )
        if (originalXml) {
            currentXml = originalXml
        } else {
            const cachedXML = chartXMLRef.current
            if (cachedXML) {
                currentXml = cachedXML
            } else {
                currentXml = await onFetchChart(false)
            }
        }
  
        const { applyDiagramOperations } = await import("@/lib/utils")
        const { result: editedXml, errors } = applyDiagramOperations(
            currentXml,
            operations,
        )
		// 如果部分操作执行错误,返回模型哪些出错
        if (errors.length > 0) {
            const errorMessages = errors
                .map((e) =>`- ${e.type} on cell_id="${e.cellId}": ${e.message}`).join("\n")
            addToolOutput({
                tool: "edit_diagram",
                toolCallId: toolCall.toolCallId,
                state: "output-error",
                errorText: `Some operations failed:\n${errorMessages}
Current diagram XML:
\`\`\`xml
${currentXml}
\`\`\`
Please check the cell IDs and retry.`,
            })
            // 清除缓存
            editDiagramOriginalXmlRef.current.delete(toolCall.toolCallId)
            return
        }


        // 渲染校验
        const validationError = onDisplayChart(editedXml)
        if (validationError) {
            addToolOutput({
                tool: "edit_diagram",
                toolCallId: toolCall.toolCallId,
                state: "output-error",
                errorText: `Edit produced invalid XML: ${validationError}
Current diagram XML:
\`\`\`xml
${currentXml}
\`\`\`
Please fix the operations to avoid structural issues.`,
            })
            // 清除缓存
            editDiagramOriginalXmlRef.current.delete(toolCall.toolCallId)
            return
        }
        // 成功
        onExport()
        addToolOutput({
            tool: "edit_diagram",
            toolCallId: toolCall.toolCallId,
            output: `Successfully applied ${operations.length} operation(s) to the diagram.`,
        })
        // 清除缓存
        editDiagramOriginalXmlRef.current.delete(toolCall.toolCallId)

    } catch (error) {
		// js执行的未知错误
        const errorMessage =
            error instanceof Error ? error.message : String(error)
        addToolOutput({
            tool: "edit_diagram",
            toolCallId: toolCall.toolCallId,
            state: "output-error",
            errorText: `Edit failed: ${errorMessage}
Current diagram XML:
\`\`\`xml
${currentXml || "No XML available"}
\`\`\`
Please check cell IDs and retry, or use display_diagram to regenerate.`,
        })
        // 清除缓存
        editDiagramOriginalXmlRef.current.delete(toolCall.toolCallId)

    }

}

c.append_diagram

流程大致如下:


flowchart TD
    A["提取续传的XML片段"] --> B{"校验是否合规(即满足提示词中的规范)"}
    B -->|是| D["返回违规,重新续传的提示+终止函数"]
    B -->|否| E["拼接XML:partialXmlRef += 本次XML"]
    E --> F["验证拼接后XML是否完整"]
    F --> G{是否完整?<br/>isComplete=true?}
    G -->|是| H["重置缓存→包装XML→验证合法性"]
    H --> I{XML是否合法?<br/>validationError存在?}
    I -->|是| J["返回验证失败提示"]
    I -->|否| K["返回拼接成功提示"]
    G -->|否| L["返回仍需续传提示"]

主要代码如下:


const handleAppendDiagram = (
    toolCall: ToolCall,
    addToolOutput: AddToolOutputFn,
) => {
    const { xml } = toolCall.input as { xml: string }
    const trimmed = xml.trim()
    // 检验是否满足提示词规范
    const isFreshStart =
        trimmed.startsWith("<mxGraphModel") ||
        trimmed.startsWith("<root") ||
        trimmed.startsWith("<mxfile") ||
        trimmed.startsWith('<mxCell id="0"') ||
        trimmed.startsWith('<mxCell id="1"')
    if (isFreshStart) {
        // 不满足
        addToolOutput({
            tool: "append_diagram",
            toolCallId: toolCall.toolCallId,
            state: "output-error",
            errorText: `ERROR: You started fresh with wrapper tags. Do NOT include wrapper tags or root cells (id="0", id="1").
Continue from EXACTLY where the partial ended:
\`\`\`
${partialXmlRef.current.slice(-500)}
\`\`\`
Start your continuation with the NEXT character after where it stopped.`,
        })
        return
    }


    // 拼接
    partialXmlRef.current += xml
    // 完整性检验
    const isComplete = isMxCellXmlComplete(partialXmlRef.current)
   
    if (isComplete) {
        const finalXml = partialXmlRef.current
        partialXmlRef.current = ""
        const fullXml = wrapWithMxFile(finalXml)
        
        // 渲染校验
        const validationError = onDisplayChart(fullXml)
        if (validationError) {
            addToolOutput({
                tool: "append_diagram",
                toolCallId: toolCall.toolCallId,
                state: "output-error",
                errorText: `Validation error after assembly: ${validationError}
Assembled XML:
\`\`\`xml
${finalXml.substring(0, 2000)}...
\`\`\`
Please use display_diagram with corrected XML.`,
            })
        } else {
            addToolOutput({
                tool: "append_diagram",
                toolCallId: toolCall.toolCallId,
                output: "Diagram assembly complete and displayed successfully.",
            })
        }
    } else {
        // 仍然不完整,提示继续补充
        addToolOutput({
            tool: "append_diagram",
            toolCallId: toolCall.toolCallId,
            state: "output-error",
            errorText: `XML still incomplete (mxCell not closed). Call append_diagram again to continue.
Current ending:
\`\`\`
${partialXmlRef.current.slice(-500)}
\`\`\`
Continue from EXACTLY where you stopped.`,
        })
    }
}