
Hi,我是松柏。
前几天我做了个开源的 AI + Excalidraw 的绘图工具,收到了很多小伙伴的好评:

短短几天 Github https://github.com/co-pine/ai-excalidraw 也 100+ star ⭐️ 了:

所以今天我又花了一些时间对这个工具进行了更新,主要增加了多会话管理、选中元素更新、删除元素这些功能,让体验更加流畅!先一起看看效果吧:
选中元素更新:

删除元素:

多会话管理,也就是说,每个对话的画布是相互隔离的,互不影响:

体验地址:https://www.lzkz.top/tool/excalidraw
Github 地址:https://github.com/co-pine/ai-excalidraw
废话不多说,关注点赞,我们看看这次的更新及其实现思路吧!
之前的版本所有会话共享一张画布,很容易互相污染,不好管理。所以这次更新支持了多会话管理,可以同时维护多个独立的画布和对话历史。

实现思路很简单,为每个会话生成唯一的 sessionId,然后在 localStorage 中分别存储:
// 会话数据结构
interface Session {
id: string
name: string
createdAt: number
messages: Message[]
elements: ExcalidrawElement[]
}
// 保存会话
const saveSession = (session: Session) => {
localStorage.setItem(`session-${session.id}`, JSON.stringify(session))
}
// 切换会话
const switchSession = (sessionId: string) => {
const session = JSON.parse(localStorage.getItem(`session-${sessionId}`))
// 恢复对话历史和画布元素
setMessages(session.messages)
excalidrawAPI.updateScene({ elements: session.elements })
}
这样每个会话都是完全独立的,可以随时创建新会话或切换到之前的会话继续编辑,非常方便。所有数据依然是存储在本地,不用担心安全问题:

点击选中元素后,对话框的上方会显示元素的信息,在发消息时会把元素信息也一起发送给 AI,这样 AI 就能基于画布的状态实现精确修改,“指哪改哪”。

这个功能的核心实现是通过 Excalidraw API 获取选中元素的信息:
// 获取当前选中的元素
const getSelectedElements = () => {
const api = excalidrawAPIRef.current
const appState = api.getAppState()
const allElements = api.getSceneElements()
// 过滤出选中的元素
return allElements.filter(el =>
appState.selectedElementIds[el.id]
)
}
// 更新选中的元素
const updateSelectedElements = (updates: Partial<ExcalidrawElement>) => {
const selectedElements = getSelectedElements()
const updatedElements = selectedElements.map(el => ({
...el,
...updates,
}))
// 应用更新
api.updateScene({
elements: allElements.map(el =>
updatedElements.find(updated => updated.id === el.id) || el
)
})
}
这个功能给我的体验非常好,比如我可以直接选中不满意的多个节点,然后描述修改需求,就能进行精确的局部修改了。
为了让这个工具更好用,我还引入了 AI 工具调用能力,让 AI 可以主动获取画布信息和操作元素。

这里主要提供了两个核心工具:
AI 可以主动查询画布上的元素信息,比如:
get_elements() 获取所有元素const tools = {
get_elements: {
description: '获取画布上的所有元素信息',
parameters: {
type: 'object',
properties: {
elementIds: {
type: 'array',
description: '可选,指定要获取的元素 ID 列表'
}
}
},
execute: (args) => {
const api = excalidrawAPIRef.current
const elements = api.getSceneElements()
if (args.elementIds) {
return elements.filter(el => args.elementIds.includes(el.id))
}
return elements
}
}
}
AI 还可以帮你删除不需要的元素:
get_elements() 找到所有红色矩形delete_elements(ids) 删除它们const tools = {
delete_elements: {
description: '删除指定的元素',
parameters: {
type: 'object',
properties: {
elementIds: {
type: 'array',
description: '要删除的元素 ID 列表',
items: { type: 'string' }
}
},
required: ['elementIds']
},
execute: (args) => {
const api = excalidrawAPIRef.current
const elements = api.getSceneElements()
// 标记为删除
const updatedElements = elements.map(el =>
args.elementIds.includes(el.id)
? { ...el, isDeleted: true }
: el
)
api.updateScene({ elements: updatedElements })
}
}
}
我觉得工具调用还是挺方便的,可以很大程度上提高使用的便捷性。
1)因为现在很多模型都是思考模型,而且思考的比较久,所以为了让用户体验更好一点,这次更新会把模型的思考过程展示给用户,减少纯等待的时间。
实现上很简单,就是在 AI 返回的流式响应中,单独处理 reasoning_content 字段:
const streamAI = async (prompt: string) => {
const response = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ messages, tools })
})
const reader = response.body.getReader()
let reasoning = ''
let content = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = new TextDecoder().decode(value)
// 解析流式数据
if (text.includes('reasoning_content')) {
reasoning += extractReasoning(text)
setThinkingContent(reasoning) // 展示思考过程
} else {
content += text
setResponse(content) // 展示正常回复
}
}
}
2)这次更新没有加没有记忆功能,个人觉得没有太大必要,来回传递画布内容太费 token 了,而且过长的上下文也会影响到 AI 输出的质量。所以从成本和体验上考虑,我选择了不加记忆功能,让 AI 通过工具调用来了解画布状态。
3)上期有一些小伙伴说希望能出对应的 Obsidian 插件,但是我本人很少用 Obsidian,不清楚大家希望这个插件做成什么样、如何交互等等,所以这个就没做,不过也欢迎其他感兴趣的小伙伴在我代码的基础上改造。
以上就是 AI Excalidraw 这个项目的本次更新啦,项目已经开源,欢迎大家点个 Star ⭐、提 issue 和 pr 。开源和体验地址:
如果觉得有用,欢迎关注转发点赞呀,也可以加我微信 co_pine 一起学习交流,下期再见,拜拜👋🏻。