Rowboat Spaces Whiteboard:Excalidraw 式实时协同白板的设计、持久化与 Agent 访问
Rowboat Spaces WhiteboardExcalidraw 式实时协同白板的设计、持久化与 Agent 访问【免费下载链接】rowboatOpen-source AI coworker, with memory项目地址: https://gitcode.com/GitHub_Trending/rowb/rowboatRowboat开源 AI 协作者在 Spaces 模块中内置了一块可多人实时绘制的共享白板空间成员在同一个画布上共同绘制光标、选区与图形实时同步风格与 excalidraw.com 一致。本指南以仓库中的设计文档 apps/x/SPACES_WHITEBOARD.md 为主体结合协议、服务端、核心与渲染端的真实源码完整讲解其协同模型per-element last-writer-wins、信令通道设计、持久化快照机制、UI 集成方式以及 Agent 如何通过 MCP 工具读写白板。读完本文你将掌握这套方案为什么这样设计、代码落在哪里、如何在现有架构上复现的完整脉络。1. 背景Harbor 与 Spaces 的现有同步骨架在介绍白板之前先厘清它所在的基础设施。Harbor 对每个组织暴露 REST 每组织一条 WebSocket/v1/live MCP全部是 service.ts 核心的薄投影参见 server.ts。1.1 持久事件永不经过 Socket客户端→服务端的帧只有三种subscribe {spaceId, afterOffset?}、unsubscribe、presence。所有写操作都走 REST客户端对baseVersion提出完整新内容得到applied | merged | conflict三态结果服务端在 merge.ts 做行级三方合并。持久化的SpaceEvent联合类型是change | message | topic | membership | reaction | message_deleted | message_edited等它们共享每个 space 一条追加式 offset 序列——定义见 events.ts。1.2 Presence 是唯一的瞬时通道PresenceState viewing | typing | agent_working | agent_idle | idle服务端只校验成员身份然后通过 hub 广播不持久化、无 offsetservice.ts。该帧没有 payload 槽位——只有{spaceId, memberId, state, topicId?, at}。白板方案的核心洞察正是Harbor 已经具备承载实时流量所需的所有形态只差一种带 payload 的帧种类。2. 为什么现有持久化路径承载不了实时白板流量文档给出了四条独立且任何一条都足以否决的理由合并语义不匹配行级三方合并对序列化后的场景无意义。两人同时添加图形 ≈ 合并 fixture04-both-append-at-end见 fixtures/merge即几乎每次并发编辑都冲突。对二进制内容则永远不合并——任何过期写入都是硬conflict。吞吐量瓶颈交互频率的白板编辑若走proposeChange会排队等待每个 space 唯一的 advisory lock与该空间内所有聊天消息争抢。体积上限文本资产上限 1 MB真实 Excalidraw 场景轻易超过。日志污染白板操作若写入持久日志重连回放将无界增长refreshTick会引发无意义的重新拉取风暴并污染活动流/已读状态。好消息是presence 路径恰好就是成员校验 不持久化 hub 广播的正确形态。Harbor 的 content-blind 哲学与 excalidraw-roomExcalidraw 官方参考中继服务器约 150 行、不存储不解密的 socket.io 中继完全一致。3. 协同模型per-element last-writer-wins而非 CRDT3.1 每个元素的元数据Excalidraw 的协同不是 CRDT而是每元素最后写入者胜出LWW。每个元素携带version——整数每次对该元素的修改都会自增versionNonce——每次修改重新生成的随机整数确定性 tie-breakerversion 相同时更小的 nonce 在所有端胜出isDeleted——删除是像普通编辑一样传播的软删除墓碑tombstoneindex——用于 z-order 的分数索引字符串使并发重排产生不同索引而非冲突的数组位置导出的reconcileElements(localElements, remoteElements, appState)按元素合并两个场景version 更高者整体胜出、保护本地正在进行的编辑正在输入/缩放/绘制的元素不被覆盖、保留双方的未知元素结果按分数索引重新排序。3.2 LWW 无需 CRDT 的原因文档归纳为五点粒度是整个元素用户很少同时协同编辑同一元素每次更新携带完整元素状态分歧在下一次写入时自然弥合tie-break 是确定性的所有端收敛到同一结果每 20 秒一次的全场景周期重播SYNC_FULL_SCENE_INTERVAL_MS自愈任何丢失的消息。Excalidraw 官方写明了这套设计。3.3 两类流量类别内容投递SCENE_UPDATE/SCENE_INIT自上次广播以来 version 有推进的元素的完整 JSON对照Mapid, version做 diff初始化及每 20 秒发送包含墓碑的全场景可靠MOUSE_LOCATION、IDLE_STATUS光标 x/y 按钮 selectedElementIds 用户名节流到 33 ms约 30fps易失背压时允许丢弃新协作者加入时已在场成员广播SCENE_INIT全场景加入者在 5 秒超时或自己是房间第一个人时回退到从持久化加载。3.4 包事实v0.18.12026-08License全 MIT包、monorepo、room server。内置字体一般是 SIL OFL——发布前需核对dist/prod/fonts下的许可文件。Reactpeer-deps^17.0.2 || ^18.2.0 || ^19.0.0——React 19 官方支持0.18。react/react-dom 是唯一peer其余全部打包。ESM-only自 0.18 起UMD 已移除Vite 7 可用TS 需moduleResolution: bundler或node16/nodenext。体积主入口约 1.1 MB min / 353 KB gzip117 个懒加载 chunk 合计约 2.4 MB gzip。必须用React.lazy懒加载。CSSimport excalidraw/excalidraw/index.css——注意与 Tailwind v4 preflight 的冲突。⚠ 字体默认从 CDN 加载打包型 Electron 应用必须把node_modules/excalidraw/excalidraw/dist/prod/fonts复制进 renderer bundle并在挂载前设置window.EXCALIDRAW_ASSET_PATH否则离线应用只能渲染 fallback 字体。这是最可能的打包坑见 §6。3.5 Presence 渲染是免费的向组件喂excalidrawAPI.updateScene({ collaborators: MapsocketId, Collaborator })每条含{pointer, username, selectedElementIds, color, avatarUrl, userState, ...}——远端光标、名字标签、选区高亮和头像条会自动渲染。出向方向把onPointerUpdateprop 接到传输层即可。3.6 持久化与墓碑 GCexcalidraw.com 每个房间存一个 Firestore 文档序列化元素数组含墓碑每 20 秒在先对照已存文档再写入的事务中节流保存——并发保存者合并而非互相覆盖。图片不走 socketblob 以 fileId 为键上传到 Firebase Storage4 MiB 上限图片元素的status: saved标志像普通元素编辑一样同步通知对端懒加载 blob。远端更新用updateScene({ elements, captureUpdate: CaptureUpdateAction.NEVER })应用以免污染本地 undo 历史。被删元素在 24 小时DELETED_ELEMENT_TIMEOUT后停止同步——离线超过该窗口的客户端可能复活删除需要把窗口设得宽松或服务端跟踪删除。4. 架构方案新瞬时帧 客户端协同循环 快照持久化4.1 实时同步新增 whiteboard 帧服务端保持 content-blind协议新增whiteboard帧客户端→服务端与服务端→客户端双向ClientFrame: { kind: whiteboard, spaceId, boardId, payload } ServerFrame: { kind: whiteboard, spaceId, boardId, memberId, at, payload }payload对 Harbor不透明schema 为z.unknown()或宽松信封。服务端校验成员身份后向空间订阅者做 hub 广播——几乎是publishPresence的翻版永不持久化、永不回放、无 offset。payload 内部镜像 Excalidraw 自己的子类型SCENE_UPDATE、SCENE_INIT、MOUSE_LOCATION、IDLE_STATUS。该设计在源码中有完整落地。协议侧events.ts 中ServerFrame联合新增了kind: whiteboard变体注释明确说明payload 对组织有意保持不透明——与白板工具所依赖的中继服务器相同的 content-blind 姿态ClientFrame同样新增对应变体。服务端侧service.ts 的publishWhiteboard先requireMember(ctx, spaceId)再hub.publish(spaceId, {...})与publishPresence同文件紧邻的实现形态一致。跳过了 Excalidraw 的端到端加密房间密钥放 URL fragment、逐消息 AES-GCM——那是为了让公共中继保持 zero-knowledgeHarbor 是受信任的、成员准入的服务端E2E 反而会阻碍未来的服务端特性缩略图、Agent 访问。传输层不做 volatile/reliable 区分单条 WebSocket 在连接期间是 TCP 可靠的丢帧只发生在断线期间20 秒全场景重同步 快照持久化负责兜底。4.2 客户端协同循环Portal/Collab 的再实现渲染端 core 只有几百行全部使用公开导出出向onChange→ 用getSceneVersion(elements) lastBroadcastVersion把关 → 对照MapelementId, version做 diff → 只广播 version 推进的元素。onPointerUpdate→MOUSE_LOCATION节流到 33 ms。每次广播都会调度一次节流到 20 秒的全场景重播syncAll含墓碑。入向restoreElements→reconcileElements(local, remote, appState)→ 记录getSceneVersion(reconciled)避免回声广播→updateScene({ elements, captureUpdate: CaptureUpdateAction.NEVER })。Presence把收到的MOUSE_LOCATION/IDLE_STATUS与空间成员身份折叠进collaboratorsmap →updateScene({ collaborators })。复用现有成员身份与显示名像useSpacePresence一样按 TTL 移除条目。加入加载最新快照见 4.3通过SCENE_INIT请求宣告或等待 ≤20 秒的周期全量同步已在画板上的对端看到新订阅者后会广播全场景。渲染端实现位于 whiteboard-pane.tsx其头部常量精确对应上述节流值CURSOR_SYNC_MS 33约 30fps、FULL_SYNC_MS 20_000周期全场景自愈、SAVE_AFTER_MS 15_000快照节流、HEARTBEAT_MS 20_000、COLLABORATOR_TTL_MS 65_000约 3 次心跳。发送侧core 的 live.ts 提供whiteboard(spaceId, boardId, payload)与presence()姿态一致socket 未开时静默丢弃并ensureConnected()——注释明确协同循环的周期全场景重播和快照对账吸收缺口丢一帧只损失流畅度不损失数据。值得一提渲染端的关键参数是每客户端确定性色相clientColor基于 clientId 哈希保证光标、名字标签和头像条颜色一致。4.3 持久化走现有合约的节流 blob 快照每个画板是一个资产如whiteboards/roadmap.excalidraw其版本是通过现有proposeChange blob 路径进行内容寻址的blob场景 JSON 常规性超过 1 MB 文本上限blob 允许 100 MB保存节流到约 20 秒匹配 excalidraw.com 的 Firebase 节奏并在面板关闭时保存。过期的二进制 propose 一律返回conflict——冲突时拉取当前 blob、本地reconcileElements、重新 propose。这就是 excalidraw.com 的写前对账事务在 Harbor 冲突合约中的表达。由于在线客户端本就通过瞬时通道收敛真正的冲突很少见。只有这些节流快照触碰持久日志——因此画板作为普通带版本资产出现在 Files/activity 中免费获得历史、回收站、溯源却不会淹没日志。附带好处离线编辑大体存活。重连时用本地场景对最新快照做对账——本地更高 version 的元素胜出受 24 小时墓碑窗口约束。v1 中的场景图片2026-09-01 移除v1 画板只有图形 文本。图片工具在编辑器层面被禁用UIOptions.tools.image false同时封禁工具栏按钮、图片粘贴、Excalidraw 内拖放因此没有任何图片能进入场景也就不存在需要维护的上传管线。这取代了两个早期迭代快照内嵌 dataURL、然后whiteboards/images/fileId的资产形式因为上传路径在 dogfood 中不稳定——协同核心在无图片时依然稳固快照保持小体积的 agent 可读文本零旁带字节。未来若回归图片image-as-asset 设计每个 fileId 确定性路径、打开面板间{t:files}通告帧是已记录的方案。4.4 UI 布局头部按钮空间顶栏与 Talk | Read | Split 模式切换器相邻。点击打开空间的默认画板首次使用时创建。画板建模为新的RailSelectionkindlib/spaces-selection.ts在文档面板/全出血模式渲染配一个列出画板的 rail 区块——而不是第四个SpaceMode。这样每个空间免费获得多个命名画板且契合文件模型模式会把哪个表面与哪个文档混为一谈。React.lazy懒加载 Excalidraw 面板使约 350 KB gzip 的主 chunk 直到首次打开才加载。Presence 芯片有活跃白板会话的成员可出现在现有的N here芯片中可选打磨项。5. 工作分解与落地状态5.1 原始计划约 10–14 个工作日#部分文件预估1协议whiteboard Client/ServerFrame 变体events.ts core.ts、index.ts0.5 d2服务端帧处理镜像 presence 路径成员校验 hub 广播测试ws.ts、service.tsws.test.ts1–2 d3客户端 core组织 socket 上收发live.ts0.5 d4IPC 管道新增spaces:whiteboard通道ipc.ts、spaces.ts、apps/main/src/spaces/ipc.ts、spaces-deps.ts、channels.ts1 d5Excalidraw 嵌入面板组件、头部按钮、rail 入口、视图状态/深链、字体自托管whiteboard-pane.tsx、spaces-view.tsx、spaces-selection.ts、space-rail.tsx、App.tsx2–3 d6协同客户端§4.2 循环跨 IPC 桥新 hook/store模式参照 use-space-chat3–4 d7持久化节流快照保存blob 上传 带blob:的 proposeChange、冲突对账重试、打开时加载coreclient.ts、渲染端面板1–2 d8打磨/QA把 whiteboard 帧排除出refreshTick重新拉取、presence 芯片、打包字体/资产验证、双机冒烟测试1–2 d建议顺序5嵌入、单用户、本地文件持久化→ 1–4传输→ 6协同→ 7真实持久化→ 8。第 5 步之后嵌入即可演示。5.2 已落地状态v12026-08-31 交付文档 §9 记录了实际实现与计划相比有两处改进快照文本优先而非 blob 优先。场景序列化为标准单行.excalidrawJSONfiles恒为空图片禁用低于约 900 KB 时作为文本资产存储因此 Agent 通过普通read_asset/propose_changeMCP 工具读写画板——无需为决策 #2 改动任何 MCP 表面blob 版本回退因空files形状而罕见。单行形状是承重设计Harbor 的行合并对单行 JSON永远不会产生损坏的merged内容——非相同并发保存总是冲突fixture 02 语义由对账重试循环处理相同保存合并为相同字节fixture 06。Rowboat 皮肤而非原生 Excalidraw。whiteboard.css 只改编辑器有文档的 CSS 自定义属性映射到应用的 design tokensislands/popups/inputs/radius/shadows 跟随 App.css 的亮暗主题并把标志性的紫色强调换成克制的蓝新元素默认干净描边 无衬线字体currentItemRoughness: 0、FONT_FAMILY.Nunito。Library 触发器外部 excalidraw.com 库被隐藏其右上角槽位改为渲染画板名芯片一个弹层切换器列出空间的画板 新建画板…输入框与实时协作者头像颜色与各自光标同源。帮助对话框保留快捷键表但去掉外链头⌘O/⌘S/主题切换画布动作关闭空间拥有持久化、应用拥有主题自定义WelcomeScreen取代默认字标空状态。升级包时需复查 whiteboard.css 中的变量名是否仍存在。另有三个后续迭代Split 并排停靠2026-09-01Split⌘3打开画板时将其放入文档槽位——聊天在左、实时画板在右之间是可拖拽分隔条共享docWidth≥420px。关键实现细节面板在 full ⇄ split 切换间保持同一棵树位置wrapper flex-order把它排到聊天右侧切换不重挂载实时协同会话——无重加入、无光标闪烁。远程模式无需专属事件中继2026-09-01spaces:events已在共享PUSH_CHANNELS白名单中桌面事件客户端通用转发 payload入站 whiteboard 帧经 child/remote server 模式原样流过。只有发送通道spaces:whiteboard需要双表面处理。快照写入经 whiteboard-saver.ts2026-09-02一次线上画板被清空事故后原面板在保存时从 Excalidraw API 读场景而 Excalidraw 的三种行为把它变成数据丢失竞态——excalidrawAPI回调在 App构造函数中触发此时场景仍为空initialData异步水合且自身触发onChange被当成用户编辑标记一切为脏componentWillUnmount换入全新空场景而 API 仍持续应答。打开画板再关闭卸载冲刷会把{elements:[]}存到真实内容之上。saver 是纯状态机单元测试见 whiteboard-saver.test.ts仅在快照加载后才存在只在getSceneVersion推进超过水合场景版本时接受本地变更清空画布仍会保存——删除通过newElementWith标记isDeleted会推进版本序列化它最后接受的场景而非重读编辑器dispose()保证已死亡的面板永远不能再写。5.3 协议层的 app-side 词汇表Harbor 契约保持 content-blind白板真实帧 schema 定义在 packages/shared/src/spaces.ts与 Excalidraw 升级完全解耦SpacesWhiteboardPayload判别联合{ t: scene; clientId; syncAll; elements }diff 帧只含 version 推进的元素syncAll帧含墓碑的全场景也是scene_request的应答、{ t: scene_request; clientId }、{ t: cursor; clientId; cursor }、{ t: idle; clientId; state: active | idle | away }。关键约定clientId是每次面板随机生成的 id——同一成员可在两个窗口/两台机器打开同一画板且中继把每帧回显给发送者自己的订阅接收端丢弃clientId与自己相同的帧协作者 presence 以 clientId 而非 memberId 为键。路径约定WHITEBOARD_DIR whiteboards、WHITEBOARD_EXT .excalidraw、DEFAULT_WHITEBOARD_PATH whiteboards/board.excalidraw。WHITEBOARD_TEXT_SNAPSHOT_MAX_BYTES 900_000≤此字节数以文本资产存储合约文本上限 1 MB之上回退 blob 版本。所有写入者面板与 Agent 工具共用这一个数字画板不会因最后谁保存而切换传输方式。EMPTY_WHITEBOARD_CONTENT提供新画板的首个快照与面板首次保存字节一致相同 propose 干净合并。辅助函数isWhiteboardPath、whiteboardDisplayNamewhiteboards/roadmap.excalidraw→roadmap、whiteboardPathForName把画板名清洗成合法资产路径/转-。6. 风险与坑字体/资产打包是最可能的坑不设置window.EXCALIDRAW_ASSET_PATH指向打包资产字体就从 unpkg 加载——离线即坏且未在打包app://scheme base: ./下验证过。要尽早验证打包构建。渲染端已设置window.EXCALIDRAW_ASSET_PATH ./excalidraw-assets/见 whiteboard-pane.tsx 顶部vite 配置负责复制/服务这些字体。IPC 跳数增加光标延迟renderer → main → Harbor远程服务器模式是三跳。30fps 节流下在真实 RTT 上应可接受但要预算快路径不让 whiteboard 帧经过 zod 重校验风暴IPC 扇出限定在打开画板的窗口。refreshTick重拉风暴快照变更集是持久事件20 秒节奏没问题但高频路径绝不能落日志并考虑把.excalidraw变更事件排除出成员/资产重拉。无离线队列断线期间发送的瞬时帧被丢弃。可接受——本地场景本身就是 outbox重连对账最新快照恢复离线编辑含 24 小时墓碑复活警告。单节点 hub当前规模足够白板不改现有天花板只是继承它。Undo/redo远端更新必须用CaptureUpdateAction.NEVER应用否则远端编辑进入本地 undo 历史。Zod/link 纪律协议变更需要 Harbor 重建流程npm run protocol与 4.2.1 固定版本白板 PR 同时触碰两个 workspace。限流rate_limited存在于协议错误枚举但无人实现行为异常的客户端可能淹没 hub。建议顺手在服务端加简单的每连接帧预算。7. 关键决策2026-08-31✅每个空间多个命名画板。建模为whiteboards/下的资产头部按钮打开/创建默认画板。基于 rail-selection 设计几乎零额外成本。✅Agent 可访问画板。访问经快照即资产的路径无需新服务端表面Agent 拉取资产 blob 读取当前画板用与人类客户端相同的proposeChange 冲突对账纪律写入新快照来绘制。在线客户端看到由此产生的持久change事件后拉取新快照、reconcileElements进打开场景——Agent 的增补在一个事件往返内出现在所有人的画布上。由此增加的要求Agent 写入的元素必须携带合法的version/versionNonce/分数index字段x/shared或 core 中的小工具应拥有生成合法 Excalidraw 元素白板面板必须对打开画板的持久 change 事件做对账而不仅是瞬时帧。✅瞬时 payload 类型不透明。协议以z.unknown()承载payload——最大化 content-blind匹配 excalidraw-room真实帧 schema 在x/shared应用侧定义。Excalidraw 升级永不触碰 Harbor 合约。✅远程服务器模式从第一天起支持。两套管道表面同时交付本地 apps/main/src/spaces/ipc.ts 与远程 spaces-deps.ts channels.ts。这反映代码库的一条长期原则客户端–服务器分离是横向的每个新特性同时支持本地模式与服务器模式。7.1 本地 vs 远程服务器模式apps/x 正处于客户端–服务器分离中期见 SEPARATION_PLAN.md。同一特性走两条不同管道本地模式当前人人如此运行一切在用户机器上。renderer 无 socket 无 tokenElectronmain 进程宿主x/core持有组织凭证与到 Harbor 的唯一 WebSocket经进程内 Electron IPC 向窗口扇出事件。renderer↔core 腿约 0 ms白板延迟 ≈ 各用户到 Harbor 的 RTT。远程服务器模式迁移终点apps/x/apps/server 是无头 rowboat-server——同一x/core大脑sessions、agents、spaces 栈、凭证作为独立进程运行由 main 派生或在另一台机器常开盒子。桌面应用变为瘦客户端经POST /rpc/{channel} 实时 WebSocket 与其通信。RPC 表面是 strangler-fig 迁移只有 channels.ts 的RPC_CHANNELS中列出的通道存在——未列出即 404——spaces 处理器在 main 与 server 之间刻意重复实现。对白板的后果特性平价是按通道选择加入的。若spaces:whiteboard只在 main 实现应用连 rowboat-server 运行时白板静默不存在。延迟上光标帧走发送方 renderer → 发送方 core → Harbor → 接收方 core → 接收方 renderer远程模式下每段都变成真实网络 RTT——rowboat-server 若与 Harbor 同云则总量接近本地模式若在家用机被远端访问则远程光标明显更卡。这是流畅度问题而非正确性问题——场景同步基于版本且自愈。已知缺口spaces:uploadBlob的文件路径快路径在 core 所在机器读文件spaces-deps.ts——远程模式下是错误机器白板只在罕见超大快照回退时用uploadBlob且经通道发送字节两种模式都能工作。建议两条表面从第一天一起接线。8. Agent 访问2026-09-12决策 #2 说 Agent 经资产路径写快照绘制。实际交付为一对工具 一个技能让模型永不手写 Excalidraw JSON8.1 纯函数半边packages/core/src/spaces/whiteboard.tsparseWhiteboardSnapshot/serializeWhiteboardSnapshot——面板的精确单行形状未变更场景字节一致。summarizeWhiteboard——模型视角的清单带绑定标签的图形、自由文本、from→to 连接线、other自由手绘/帧、整体 bounds。applyWhiteboardOps——在现有场景上执行add/connect/update/delete。它持有手写快照最容易写错的全部不变量每个被触碰元素version自增 新鲜versionNonce分数index追加在现有最高 key 之后实现见nextFractionalIndexbase-62 编码rocicorp/fractional-indexing 格式删除是isDeleted墓碑绝不省略——省略的元素会被任何打开面板的下一次保存复活绑定标签containerIdboundElements、箭头绑定startBinding/endBindingcenter-to-center 线 focus 0端点距各轮廓 5px 间隙绑定图形移动时重路由——见rerouteConnectors文本按 Nunito 估算字号FONT_FAMILY_NUNITO 6、行高 1.35、平均字宽 0.55em。未触碰元素原样透传。全或无批次中任何未知 id 一律不写validate在任何写入前收集全部问题并抛WhiteboardOpError。操作词汇zod 定义WhiteboardOpaddrectangle/ellipse/diamond盒子自带居中标签缺省时按标签自适应尺寸/text自由文本放置可用显式x/y或相对锚点rightOf/leftOf/below/abovegap默认 80样式可选描边/填充颜色名或 hex、虚线、strokeWidth0.5–8、fontSize8–96。connectfrom/to两个元素 idkind默认arrowline无箭头可选label。update改文本/位置/尺寸/样式标签自动重排箭头跟随盒子。delete移除元素标签与箭头绑定随行。重叠启发式warnIfOverlapping会在新增落地到已有内容上时给出带修复建议的警告——sizes are computed from labels, so place elements with rightOf/below/leftOf/above instead of guessed coordinates。8.2 工具半边runtime/tools/domains/whiteboard.tswhiteboard-readpermission: none——把画板渲染成模型可推理的紧凑摘要画板不存在返回成功exists: false 空间现有画板列表 next提示绝不返回错误第一次 dogfood 中错误结果把模型带偏去读技能源码、跑find /Applications想学习格式。whiteboard-drawpermission: prompt——绘制 read → apply → serialize → proposeconflict时在currentContent上重放同一批 ops 再 propose最多 3 次MAX_CONFLICT_RETRIES。ops上限 200MAX_OPS。按WHITEBOARD_TEXT_SNAPSHOT_MAX_BYTES阈值选文本或 blob 传输——与面板同一阈值。两者都基于组织的 agent faceread_asset/propose_changeviacallOrgTool组合而成core-only两个宿主Electron main 与 rowboat-server零 IPC/RPC 管道免费获得。8.3 技能与上下文skills/whiteboard/skill.ts——模型侧规则先读再画、在现有内容旁添加、一个请求一次调用、相对放置spaces 技能把画板请求路由给它并禁止在whiteboards/上直接propose_change。那块画板如何解析composer 的 菜单列出画板hooks/use-space-boards.ts →SpaceMentionRefkindboardSpaces 中打开的画板经userMessageContext.middlePanekindwhiteboard存在任一都会为该轮钉住spaceswhiteboard技能。8.4 为什么不直接用convertToExcalidrawElementsexcalidraw/excalidraw是 renderer-only其 bundle 使用 Vite 容忍而 Node 拒绝的无扩展名roughjs/bin/rough导入且拉入 React——因此元素构建器基于 0.18 元素类型手写。excalidraw/mermaid-to-excalidraw同样是浏览器专用。9. 验证与测试状态Harbor 套件 211 通过含新中继测试core spaces 39含通过 stub 的真实 socket 白板往返renderer 365server app 27类型检查 lint 干净。生产 renderer 构建把 Excalidraw 拆成约 1 MB 懒加载 chunk并把字体复制进dist/excalidraw-assets/。仍需双运行应用的人工 QA双机绘制会话光标、并发编辑、中途加入、打包构建字体渲染Excalifontvs fallback——CDN fallback 绝不能触发、Agent 经 MCP 绘制对whiteboards/*.excalidraw的propose_change携带合法version/versionNonce/index字段并实时出现、远程服务器模式光标延迟体感。10. 延伸阅读仓库内设计文档全文apps/x/SPACES_WHITEBOARD.md含 Excalidraw 协同工作原理的完整调研与外部来源清单协议定义events.tsHarbor 契约CONTRACT.md合并 fixtureapps/harbor/packages/protocol/fixtures/merge/服务端实现service.tspublishWhiteboard、ws.ts客户端 corelive.tswhiteboard()发送器纯函数画板引擎whiteboard.ts共享词汇表spaces.ts渲染端whiteboard-pane.tsx协同循环、whiteboard-saver.ts快照状态机、whiteboard.cssRowboat 皮肤Agent 工具与技能whiteboard.ts、skill.ts【免费下载链接】rowboatOpen-source AI coworker, with memory项目地址: https://gitcode.com/GitHub_Trending/rowb/rowboat创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考