Skill - 把无限画布装进 Codex:Cowart 的架构拆解与实践指南

文章目录

当 Coding Agent 开始处理图像与视觉设计,纯文字对话就成了瓶颈。Cowart 用一块 tldraw 画布,把「指哪打哪」还给了人类。

一、从「许愿池」到「工作台」

用 Codex、Claude Code 这类 Coding Agent 干活,有一种熟悉的别扭感:你把需求写成一段话,扔进对话框,然后祈祷它理解得八九不离十。文字处理代码时问题不大------函数名、文件路径、行号都是精确的坐标。但一旦任务进入视觉领域,坐标就消失了。

试着用纯文字描述这样一个修改:

「把右上角那个价格标签往左移一点,字号调小,颜色换成和下面按钮一致的那种蓝,另外中间那块留白太大了,把插图放大填一填。」

每一个「那个」「一点」「那种」都是一次信息损耗。人和人沟通时靠的是手指------指着屏幕说「这里」。而在对话框里,手指被剥夺了。

Cowart 解决的正是这件事。它是一个面向 Codex 的原生无限画布 widget 插件,基于 tldraw 构建,让 Codex 不只读文字提示词,还能读到你画在图上的箭头、圈选和批注文字。项目 2026 年 6 月开源,MIT 许可,短时间内在 GitHub 收获五千余星,作者是钟二信(ZHONG XIN),一位有 Sketch 插件与设计工具背景的产品经理。

本文面向的读者是:想理解 Agent 与图形界面如何耦合的开发者,正在做 AI 工具前端的工程师,以及想把 Codex 用作日常视觉工作台的重度用户。我们会从「为什么需要画布」讲到代码结构、数据模型、MCP widget 机制,再到实际使用中的取舍与局限。


二、核心洞察:Agent 缺的不是能力,是指称手段

先把问题定义清楚。当前的图像生成模型(GPT Image 系列及同类)在生成质量上已经足够,真正卡住普通用户的是两个环节:

第一,prompt 的冷启动。 看到一张好图,你不知道该怎么描述它才能复现。

第二,迭代时的指称(reference)。 生成之后要改,改的往往是局部------某个位置、某个元素、某种比例关系。语言在描述空间关系时天生笨拙,而这恰恰是设计工作的主战场。

这两点合起来,导致一个荒谬的现象:模型能力足够强,但人类的表达带宽不够,于是产出停留在「差不多能看」,很难推进到「就是我要的」。

Cowart 的思路不是加强 prompt 工程,而是换一种输入模态。画布上的一个箭头,等价于一段冗长的方位描述;一个圈,等价于「我说的是这块区域」;框的尺寸本身,等价于「按这个比例生成」。空间信息用空间方式传递,几乎零损耗。

更进一步,这套交互还带来了「留痕」的副作用:原图、标注、修订图并排放在画布上,一次迭代的完整推理链条可见可回溯。这是对话式界面很难提供的------聊天记录是线性的,而设计过程是并行发散的。


三、整体架构:四层结构

Cowart 的仓库结构相当克制,顶层只有几个目录:

复制代码
cowart/
├── .codex-plugin/      # Codex 插件声明
├── .mcp.json           # MCP server 配置
├── mcp/
│   ├── server.mjs      # MCP 服务主入口
│   └── lib/
│       ├── plugin-root.mjs
│       ├── widget-resource.mjs
│       ├── cowart-static-widget.mjs
│       └── canvas-storage.mjs
├── skills/             # 三个 Agent skill 定义
├── src/                # tldraw 画布前端(React)
├── scripts/            # start-mcp / probe-mcp / vite-build-once / start-canvas.sh
├── public/
├── index.html
├── vite.config.js
└── package.json

映射成运行时的四层:

职责 关键实现
插件声明层 向 Codex 注册 skill 与 MCP server .codex-plugin/.mcp.json
MCP 服务层 暴露工具、托管 widget 资源、读写画布数据 mcp/server.mjsmcp/lib/*
画布前端层 无限画布、AI 框、标注、演示 src/(tldraw + React 19)
项目数据层 画布 JSON 与图片/HTML 资源 用户项目下的 canvas/ 目录

这个分层里最值得注意的一点是:前三层住在插件仓库,第四层住在用户项目。数据与代码彻底分离,后面会看到这个决定的分量。

依赖清单里的信息量

package.json 是最诚实的架构文档:

json 复制代码
{
  "name": "cowart-canvas",
  "version": "0.1.20",
  "type": "module",
  "dependencies": {
    "@modelcontextprotocol/ext-apps": "^1.7.4",
    "@modelcontextprotocol/sdk": "^1.29.0",
    "tldraw": "^5.1.1",
    "@tldraw/assets": "^5.1.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "vite": "^7.0.0",
    "html2canvas": "^1.4.1",
    "fractional-indexing": "^3.2.0",
    "lucide-react": "^1.24.0",
    "zod": "^4.4.3"
  }
}

逐条读下来,实现路径几乎不用猜:

  • @modelcontextprotocol/sdk + @modelcontextprotocol/ext-apps:不只是普通 MCP server,而是用了 MCP 的应用扩展能力,把 UI 作为资源交付给宿主渲染。这是「原生 widget」而非「本地网页」的技术前提。
  • tldraw 5.x:画布引擎。选它意味着形状系统、选择状态、相机变换、撤销栈这些重活全部复用,Cowart 只需扩展自定义 shape(AI 图片框、AI HTML 框、Slides)。
  • html2canvas :把标注后的画布区域导出为位图。这是「按标注改图」链路的关键一环------模型看到的是一张包含箭头和文字的合成图,而不是一串坐标。
  • fractional-indexing:分数索引排序,常用于维护形状层级(z-order)或 Slides 页序,插入元素时无需重排整个序列。
  • zod:MCP 工具入参校验。工具边界上的类型安全,能显著降低 Agent 传错参数导致的静默失败。
  • vite 7 + React 19 :现代构建链,npm run build 走的是自写的 scripts/vite-build-once.mjs,把产物打成可被 MCP 托管的静态 widget。

仓库还提供了一条聚合质量校验命令:

bash 复制代码
npm run quality   # = npm run check && npm run build && npm run probe:mcp

checknode --check 对每个 .mjs 做语法校验,probe:mcp 主动探活 MCP server。对于一个插件项目,这套自检比接一堆测试框架更务实------插件失效的主要形态就是「MCP 起不来」和「widget 资源加载不到」,正好被这两步覆盖。


四、关键演进:从本地网页服务到原生 widget

Cowart 早期版本的形态是:Codex 启动一个本地 Vite 服务(默认端口 43217),用户在浏览器或 in-app browser 里打开 http://127.0.0.1:43217/。这条路走得通,但代价不小------需要切窗口,端口可能冲突,服务生命周期要人管,画布和对话在两个上下文里。

现在的主线换成了 MCP widget:Codex 调用 render_cowart_canvas_widget,画布直接在 Codex 内部渲染scripts/start-canvas.sh 只作为本地开发的 fallback 保留。

这次调整背后有一个通用结论值得记下来:Agent 的图形界面应该由宿主渲染,而不是由插件另开一个世界。 原因有三:

  1. 上下文同源。widget 与对话在同一容器里,选择状态、剪贴板、截图能通过 widget bridge 双向流动,不需要走 HTTP 端口做进程间通信。
  2. 权限与安全边界清晰。本地起服务意味着开一个监听端口,在企业环境里是需要解释的行为;widget 资源由 MCP 通道交付,边界收敛。
  3. 心智负担低。用户不需要理解「插件其实是个网页」这件事。

mcp/lib/ 下的两个文件名把这条链路写得很直白:widget-resource.mjs 负责把构建产物声明为 MCP 资源,cowart-static-widget.mjs 负责将其组装成宿主可直接渲染的静态 widget。plugin-root.mjs 解决的是另一个经典麻烦------插件被安装到任意路径后,如何稳定定位自身根目录以读取产物和资源。


五、数据模型:为什么画布必须存在用户项目里

画布数据的落盘路径是这样的:

复制代码
<你的项目目录>/
└── canvas/
    └── pages/
        └── <page-id>/
            ├── cowart-canvas.json   # 画布本体
            └── assets/              # 图片、生成的 HTML 等资源

默认写入当前用户项目,而不是插件仓库。三个环境变量控制这套行为:

变量 作用 默认值
COWART_PORT 本地开发服务端口 43217
COWART_PROJECT_DIR 画布数据归属的项目目录 当前项目
COWART_CANVAS_DIR 画布数据目录 $COWART_PROJECT_DIR/canvas

这个设计看着朴素,实际影响很大:

画布随项目走。 A 项目的画布和 B 项目的画布天然隔离,不会互相污染。你在做官网改版时的所有草图,就躺在官网仓库里。

画布进版本控制。 cowart-canvas.json 是纯文本,能 commit、能 diff、能 review、能跟着分支切换。设计过程第一次和代码变更处在同一条时间线上。这一点对团队协作的意义比听起来更大------设计稿不再是 Figma 里的一个孤立链接,而是仓库的一部分。

资源本地化。 生成的图片和 HTML 落在 assets/,不依赖任何云端存储,离线可用,也不会因为服务下线而失效。

卸载或更新插件不丢数据。 代码和数据分离,插件目录随时可以删了重装。

如果要给「Agent 工具的数据应该放哪」下一条经验:放在用户的工作目录,用纯文本格式,让版本控制接管历史。 不要放在插件自己家里,也不要急着上云。


六、四条核心工作流

6.1 打开画布

在 Codex 里用自然语言说明意图即可:

复制代码
Open the Cowart canvas for this project.

Codex 通过 render_cowart_canvas_widget 打开原生 widget。首次使用建议在安装后新开一个对话,让新注册的 skill 和 MCP 工具完整加载------这是插件类工具的通用坑,工具清单通常在会话初始化时快照。

6.2 生成图片:让框的尺寸成为 prompt 的一部分

流程是三步:

  1. 在画布上创建并选中一个 AI 图片 框;
  2. 在弹出的生成面板里写 prompt,可选择一张或多张画布上已有的图作为参考图;
  3. 发送。

关键在于发送出去的载荷不只是文字。Cowart 会把 prompt + 参考图 + 选中框的位置与尺寸信息 一起交给 Codex。Codex 按这个框的比例生成图片,然后把占位框替换成普通图片形状。

这里有两个容易被忽略的巧思:

  • 尺寸即约束。你不需要在 prompt 里写「16:9」「竖版海报」,把框拉成什么形状,图就按什么比例出。空间约束用空间方式表达。
  • 参考图来自画布本身。画布上任何一张图都能被指定为参考,风格延续变成一次点选,而不是一段「保持和上一张一致的插画风格、同样的配色、同样的线条粗细......」的祈祷文。

6.3 按标注改图:这是整个项目的灵魂

步骤:

  1. 在画布上对图片做标注------箭头、圈选、文字说明,用 tldraw 原生的绘图工具;
  2. 选中被标注的图片,点击 按标注修改
  3. Cowart 导出一张包含原图、箭头和标注文字的合成截图,通过 widget bridge 发给 Codex。

Codex 读取截图里的标注意图,生成一张去掉标注痕迹的新图,放在原图旁边。原图和标注不会被删除或移动。

值得展开的是这条链路的技术选择。要把「改这里」传达给模型,理论上有几种方案:

  • 坐标 JSON:把标注序列化成结构化数据(框选区域、箭头起止点、附加文字),随 prompt 一起送出。精确,但需要模型在文字空间里重建视觉布局,且要求模型严格遵循自定义 schema。
  • mask + inpainting:生成蒙版做局部重绘。技术上最「正统」,但依赖特定的图像编辑接口,且对「把这个元素往左移」这类结构性修改无能为力。
  • 合成截图:把标注烧进像素,让多模态模型直接看图理解。

Cowart 选了第三种,用 html2canvas 完成导出。这个选择的合理性在于:现代多模态模型本来就擅长看图,让它看一张画满批注的图,与让人类设计师看同一张图,理解路径是一致的。不需要定义中间协议,不需要模型学习任何私有格式,能力天花板直接对齐视觉模型本身的水平。

代价是精度上限受模型视觉理解能力约束,且需要「重新生成整图」而非局部修补,细节可能漂移。但对绝大多数「构思---迭代」场景,这个权衡是划算的。

还有一个体贴的细节:修订图放在原图旁边,而不是覆盖原图。设计迭代天然是分叉的,保留分支比追求整洁更重要。同时这也支持一种手工用法------你自己截一张带标注的 Cowart 图发给 Codex,走的是同一条修订流程。

6.4 AI HTML 与 AI Slides:把画布变成产出容器

这是 Cowart 从「改图工具」向「工作台」跨出的一步。

AI HTML :创建并选中一个 AI HTML 框(默认 1024 × 576,16:9),输入 prompt 与参考图,Codex 生成完整可运行的单文件 HTML ,直接嵌入这个框。生成的 HTML 作为画布中的嵌入页面存放在当前 page 的 assets/ 目录。选中后可以下载渲染图、直接编辑文本,也可以继续用画布标注来迭代 HTML,或者反过来根据 HTML 与标注生成图片。

「单文件 HTML」这个约束选得很准:无外部依赖、可直接 <iframe> 嵌入、可单独打开、可 commit 进仓库。它同时是产物、是素材、也是可编辑的中间态。

AI Slides :外框默认 1048 × 600,对应一页 1024 × 576 内容加四周各 12px 留白。用法有两种:

  • 组装:把画布上已有的图片或 HTML 拖入 Slides,或复制图片后选中 Slides 粘贴进去,内容自动按顺序横向排列;
  • 生成:选中空 Slides,在生成面板里写整套演示的描述、添加参考图,选择 3、5、10 页或自定义页数(默认 5 页),Codex 生成一组视觉与叙事连贯的独立 16:9 HTML 页面依次加入。

注意一个交互规则:Slides 已有内容时不再显示生成面板。这是一条防误伤的设计------避免在已有成果上意外触发批量覆盖。

演示模式支持左侧缩略图预览与切换、全屏播放、方向键/空格/点击翻页,并且保留 HTML 自身的按钮、链接和表单交互,播放控制栏固定在顶部。也就是说,做出来的不是静态幻灯片,而是一串可交互页面。


七、Skill 机制:Agent 侧的行为契约

Cowart 注册了三个 skill:

Skill 触发场景 行为
cowart:cowart-open-canvas 用户要求打开画布 渲染原生画布 widget
cowart:cowart-image-gen 需要生成、填充、替换画布上的图片 接收画布内 prompt 与参考图,生成图片替换选中的 AI 图片 框;无选中框时插入到当前页面
cowart:cowart-image-edit 用户提供 Cowart 标注截图 依据标注生成修订图

Skill 和 MCP 工具的分工,是理解这类插件的关键:

  • MCP 工具是能力------读取选择状态、保存画布、插入图片或 HTML、写入页面资源目录。它们是确定性的、可校验的原子操作(入参由 zod 把关)。
  • Skill 是判断力------什么时候该调哪个工具、参数怎么组织、多步操作如何编排、边界情况如何降级(比如「没有选中框时怎么办」)。

把「能力」和「判断」分开写,好处是两侧可以独立演进:加一个新的画布形状,只需扩工具;改变 Agent 的行为倾向,只需改 skill 文本。这也是 Agent 插件相较传统插件的结构性差异------一部分逻辑是用自然语言写的,可以被非工程师维护。


八、上手实践

安装

两条路:让 Codex 自动安装(在对话里直接要求它按仓库说明装好),或者手动安装。手动路径推荐把插件 clone 到 Codex personal marketplace 默认引用的位置,并确认 ~/.agents/plugins/marketplace.json 中存在 Cowart 条目:

json 复制代码
{
  "name": "personal",
  "interface": {
    "displayName": "Personal"
  },
  "plugins": [
    {
      "name": "cowart",
      "source": {
        "source": "local",
        "path": "./plugins/cowart"
      },
      "policy": {
        "installation": "AVAILABLE",
        "authentication": "ON_INSTALL"
      },
      "category": "Productivity"
    }
  ]
}

然后先注册 personal marketplace,再安装插件。安装完成后开一个新对话再使用。
| ⚠️

安装类命令请以仓库 README 的当前版本为准。给 Agent 授予「自动安装插件」权限时,值得先自己读一遍要执行的命令------这是所有 Agent 插件生态的共同风险面,与具体项目无关。

本地开发

开发时仍可直接启动 Vite 画布服务,并通过 COWART_PROJECT_DIR 指定画布数据所属的用户项目目录。改完代码后跑一遍聚合校验:

bash 复制代码
npm run check        # 语法自检所有 .mjs
npm run build        # 构建 widget 静态产物
npm run probe:mcp    # 探活 MCP server
npm run quality      # 以上三步串联

几条实用建议

  • 先把框拉对比例再写 prompt。框的尺寸是硬约束,比在文字里描述比例可靠得多。
  • 标注要「说人话」。箭头指向位置,文字写清动作(「删掉」「换成蓝色」「放大 1.5 倍」)。把它当成给外包设计师的批注来写,而不是给机器写指令。
  • 把画布当版本库用。一轮迭代留一列,横向铺开,比反复覆盖同一张图更容易看出走向。
  • canvas/ 目录记得纳入 git ,但注意 assets/ 里的位图会让仓库变胖,长期项目考虑 Git LFS 或定期归档。
  • Slides 生成前先备好参考图,风格一致性主要靠参考图而非文字描述维持。

九、局限与思考

客观地讲几条限制:

强依赖宿主。 Cowart 是为 Codex 写的插件,skill 命名、widget 渲染、图片生成能力都绑定在这套宿主上。想迁移到别的 Agent,MCP 服务层和前端可以复用,但 skill 层和 widget 交付方式要重做。

生成质量取决于底层模型。 Cowart 是交互层的创新,不改变生成能力本身。标注被理解到什么程度、修订图细节保真度如何,最终由多模态模型决定。

整图重生成而非局部修补。 「按标注改图」得到的是新生成的一张图,未被标注的区域也可能发生细微变化。对精修阶段的工作不够可靠。

项目仍在早期。 版本号停在 0.1.x,贡献者集中在作者本人,接口和数据格式都有变动可能。用于严肃生产前建议锁定版本。

协作是单机的。 画布存在本地文件里,多人实时协作不在当前范围内------虽然通过 git 做异步协作反而顺畅。

这些局限不影响它的价值。Cowart 真正的贡献不在功能清单,而在示范了一种范式:Agent 不必被困在对话框里,它可以拥有为特定任务定制的图形界面,而这个界面的产物直接落在用户的文件系统中。


十、可迁移的五条设计经验

如果你也在做 Agent 侧的工具或界面,Cowart 有几条经验值得直接抄:

1. 为任务选对输入模态。 空间问题用空间输入,时间问题用时间轴输入,别一律塞进文本框。表达带宽的提升,往往比 prompt 调优的收益大一个量级。

2. 让界面成为 prompt 的一部分。 选中框的尺寸、标注的位置、拖拽的顺序,都是零成本的高质量输入信号。用户已经在操作了,顺手把语义收集起来就好。

3. 数据放用户家里,用纯文本,交给 git。 可 diff、可回滚、可分支、可 review,插件生死不影响数据存续。

4. 用宿主原生的 UI 交付方式。 别自己另开端口起一个网页世界。MCP widget 这类机制在上下文共享、权限边界和心智负担上全面占优。

5. 能力与判断分层。 确定性操作做成带 schema 校验的工具,编排与降级策略写进 skill 的自然语言里。两层各自演进,改一处不牵动全局。


结语

Cowart 的代码量不算大,思路也不复杂:拿一个成熟的画布引擎,接一套 MCP 工具,把数据落在用户项目里,再用三个 skill 教 Agent 怎么配合。但它触到了一个真问题------当 AI 的能力越过某个门槛,瓶颈就从模型转移到了人机接口。

过去两年,我们习惯了通过打磨 prompt 来榨取模型能力。Cowart 提供了另一个方向的答案:换一种更符合人类直觉的表达方式,让「指哪打哪」重新成为可能。一个箭头胜过一百个形容词,这在人与人之间早就是常识,现在轮到人与 Agent 了。

对开发者,它是一份关于 MCP widget、tldraw 扩展与 Agent 插件分层的活样本;对使用者,它是让 Codex 从「代码助手」变成「视觉工作台」的一块拼图。无论哪一种身份,都值得 clone 下来跑一遍。


参考

  • 项目仓库:zhongerxin/Cowart(MIT,2026 年 6 月开源)
  • 画布引擎:tldraw/tldraw
相关推荐
roman_日积跬步-终至千里1 小时前
【从零开始学架构】DDIA 精要:一套关于“数据系统架构”的分层理论
架构
亲爱的马哥2 小时前
Vue3 + Element Plus 低代码表单设计器架构拆解与私有化落地实践
低代码·架构·敏捷流程
LONGZETECH3 小时前
工业实训仿真设计实践:电机拆装软件的 DAG 流程建模、工具精度分级与数据体系搭建
大数据·算法·unity·架构·汽车
张忠琳5 小时前
【NVIDIA】k8s-device-plugin v0.19.3 — CDI / MIG / vGPU 模块超深度代码分析之五
云原生·容器·架构·kubernetes·nvidia
StarkCoder5 小时前
AI 会做多、看少、不收尾:七种失效和拦住它们的办法
人工智能·架构
冷莫溪5 小时前
Docker——3.Harbor 核心架构与原理详解
docker·容器·架构
beibeix20155 小时前
3D Slicer 架构分析
架构·slicer
snow@li6 小时前
命名规范:企业级微服务模块前缀命名规范全景梳理
微服务·云原生·架构
roman_日积跬步-终至千里6 小时前
【从零开始学架构】系统设计的价值:从业务目标到可演进系统
架构