)
1. 为什么我要用 Codex 重做在线协作画板在线协作画板这个需求看起来简单真做起来坑不少。核心检索词就三个Codex、Canvas、WebSocket 实时同步。它本质上是一个「多人共享一块画布」的 Web 应用适合谁适合想练手实时协作的前端、想给团队做白板工具的全栈、以及想体验 AI 驱动开发流程的独立开发者。我之前的做法是手写 Canvas 绘图逻辑再单独接一套 WebSocket结果两边状态对不上本地画完一笔远端延迟半秒才出现断线重连后画布直接空白撤销只撤自己那部分别人看到的还是旧的。这些问题的根因不是代码写错而是「绘图状态」和「同步状态」没有统一抽象。这次我换了个思路把 Codex 当成一个能听懂架构描述的结对工程师先让它把绘图引擎、历史管理、WebSocket 客户端三块拆清楚再逐块生成。实测下来只要提示词里把「图层用离屏 Canvas」「操作记录用于撤销和同步」这两点讲明白Codex 生成的骨架基本能直接跑。下面这套流程从项目初始化到双端联调每一步都有可复制的命令和配置。你跟着做大概两三个小时能跑通一个能多人同时画线的版本。2. TaoToken 前置给 Codex 配一个稳定的模型入口Codex 本身是命令行工具但它背后要调模型。如果你直接用官方额度跑这种多轮生成的项目很容易中途限流。我的做法是先把模型入口统一到 TaoToken这样 Codex、Claude Code、以及后面可能接的 Agent 都走同一个 Key省得来回切。TaoToken 在这里的角色是「模型 API 聚合入口」不是编辑器替代品。你仍然在本地用 Codex 写代码只是把请求发到 TaoToken 的兼容端点。官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 注意 API 地址不带 UTM 参数。操作顺序是这样先注册账号进控制台创建 API Key然后把 Key 配到环境变量里。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 管理页是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。如果你后面要长期跑编码任务可以看下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。配置环境变量Linux/macOS 用export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/apiWindows PowerShell$env:TAOTOKEN_API_KEYsk-你的key $env:TAOTOKEN_BASE_URLhttps://taotoken.net/api配完之后Codex 的请求就会走这个入口。如果你用的是 Claude Code 那套接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite ClaudeCodeAnthropic 相关配置在 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 。想先验证模型通不通可以直接用模型对话页试一句https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。注意环境变量配好后新开一个终端窗口再跑 Codex否则读不到。3. 可复制配置项目骨架与 Codex 提示词3.1 环境检查与项目初始化先确认本地环境。Node 要 20 以上pnpm 8 以上Codex 用最新版node -v pnpm -v codex --version然后建项目。我习惯前后端分目录前端 Vue 3 Vite后端 Express wsmkdir collaborative-whiteboard cd collaborative-whiteboard pnpm create vite client --template vue cd client pnpm add element-plus element-plus/icons-vue pinia axios pnpm add -D sass cd .. mkdir server cd server pnpm init pnpm add express ws uuid cors pnpm add -D nodemon cd ..目录结构大概长这样collaborative-whiteboard/ ├── client/ │ ├── src/ │ │ ├── components/ │ │ │ ├── ToolBar.vue │ │ │ ├── LayerPanel.vue │ │ │ └── Canvas.vue │ │ ├── composables/ │ │ │ ├── useCanvas.js │ │ │ ├── useHistory.js │ │ │ └── useWebSocket.js │ │ ├── App.vue │ │ └── main.js │ └── package.json ├── server/ │ ├── app.js │ ├── ws-handler.js │ └── package.json └── README.md3.2 给 Codex 的绘图引擎提示词这一步是整个项目的核心。提示词要讲清楚三件事图层怎么管、笔迹怎么平滑、操作记录怎么产生。请实现一个 Canvas 绘图引擎类 DrawingEngine。要求 1. 管理多个图层每个图层一个离屏 Canvas 2. 支持画笔自由绘制用二次贝塞尔曲线平滑 3. 支持矩形、圆形、直线、箭头 4. 支持文字插入 5. 支持橡皮擦用 destination-out 模式 6. 每次绘制产生一个操作记录用于撤销和 WebSocket 同步 7. 用 requestAnimationFrame 优化渲染Codex 生成的核心结构是这样的构造函数里维护layers数组每个 layer 包含一个离屏 canvas 和它的 ctx_bindEvents绑定鼠标和触摸事件_onPointerDown开始绘制_onPointerMove过程中用quadraticCurveTo平滑_onPointerUp结束时生成 operation 对象并触发onOperation回调。关键代码片段_drawSmoothLine(point) { const ctx this.activeLayer.ctx const points this.currentPath const len points.length if (len 3) { ctx.lineTo(point.x, point.y) ctx.stroke() return } const p2 points[len - 2] const midX (p2.x point.x) / 2 const midY (p2.y point.y) / 2 ctx.quadraticCurveTo(p2.x, p2.y, midX, midY) ctx.stroke() }onOperation这个回调是后面 WebSocket 同步的入口一定要留出来。3.3 WebSocket 消息协议配置前后端要约定好消息格式否则联调时对不上。我用的是这套消息类型方向字段说明join客户端→服务端roomId, nickname加入房间joined服务端→客户端userId, users, operations加入成功返回历史操作draw双向operation绘制操作广播cursor双向cursor光标位置同步clear双向userId清空画布user_joined服务端→客户端userId, nickname, users有人加入user_left服务端→客户端userId, users有人离开服务端用ws库按房间管理连接。核心逻辑是RoomManager类维护roomsMap每个房间有users和operations。新用户加入时把最近 100 条操作发给他这样画布状态能追上。joinRoom(roomId, userId, ws, nickname) { const room this.getOrCreateRoom(roomId) room.users.set(userId, { ws, nickname, cursor: { x: 0, y: 0 } }) return room }心跳检测每 30 秒一次用ws.ping()超时就terminate()避免僵尸连接占着房间。3.4 前端 WebSocket 客户端与断线重连前端用useWebSocket组合式函数封装。断线重连用指数退避第一次 2 秒第二次 4 秒最多 30 秒重试 10 次后停止。function attemptReconnect() { if (reconnectAttempts maxReconnectAttempts) return reconnectAttempts const delay Math.min(1000 * Math.pow(2, reconnectAttempts), 30000) reconnectTimer setTimeout(() connect(), delay) }重连成功后要重新joinRoom否则服务端不知道你是谁。这一点很容易漏我踩过坑断线重连后画布能画但别人看不到就是因为没重新加入房间。4. 验证请求双端联调与同步延迟测试4.1 启动服务后端先跑起来cd server npx nodemon app.js看到在线画板后端已启动: http://localhost:3002和WebSocket 地址: ws://localhost:3002/ws就对了。前端另开一个终端cd client pnpm dev浏览器打开http://localhost:5173状态栏显示「已连接」说明 WebSocket 通了。4.2 双端联调动作开两个浏览器窗口都访问http://localhost:5173在工具栏输入同一个房间号room-001分别点「加入」。然后第一个窗口画一笔第二个窗口应该立刻出现同样的笔迹。第二个窗口移动鼠标第一个窗口能看到一个带昵称标签的彩色光标在动。第一个窗口点清空第二个窗口画布同步清空。如果笔迹出现但光标不动检查cursor消息有没有发出去如果光标动了但笔迹不同步检查draw消息里的 operation 结构是否一致。4.3 同步延迟验证打开浏览器开发者工具的 Network 面板筛选 WS看draw消息的时间戳。本地环境下从发送到接收一般在 10 到 30 毫秒。如果超过 100 毫秒检查是不是每次mousemove都发了消息——那样会刷屏。正确做法是用rafThrottle把光标同步限制到每帧一次。export function rafThrottle(fn) { let rafId null return function (...args) { if (rafId) return rafId requestAnimationFrame(() { fn.apply(this, args) rafId null }) } }5. 本篇常见错排查5.1 画布空白或笔迹不显示最常见的原因是_composeLayers没被调用。每次绘制结束、图层切换、可见性变化后都要调一次。另外检查离屏 canvas 的宽高是否和主 canvas 一致不一致会导致绘制偏移。5.2 WebSocket 连接失败先看后端有没有启动再看端口是不是 3002。如果前端报WebSocket connection to ws://localhost:3002/ws failed检查app.js里WebSocketServer的path配置是不是/ws。Nginx 部署时还要加proxy_set_header Upgrade $http_upgrade和Connection upgrade否则握手失败。5.3 断线重连后画布不同步重连后必须重新发join消息服务端才会把你加回房间并返回历史操作。如果只重连不重新加入你画的东西服务端不知道发给谁。5.4 撤销只影响本地撤销是基于图层快照的HistoryManager保存的是每个图层的 DataURL。如果撤销后远端没变化说明撤销操作没有通过 WebSocket 广播。简单做法是撤销后发一条clear加全量重放复杂做法是发undo消息让远端也执行撤销。我建议先用全量重放稳定后再优化。5.5 多人同时画同一区域冲突Canvas 本身没有冲突检测后画的覆盖先画的。如果业务需要可以在 operation 里加layerId和timestamp服务端按时间排序后重放。但大多数协作白板场景不需要这么严格先跑通再说。6. 继续往下走从能跑到好用跑通上面这套之后你手里已经有一个能多人实时画线的版本了。接下来可以做的加图层面板拖拽排序、加导出 PNG/JPG、加房间列表、加操作历史回放。这些都可以继续用 Codex 生成提示词里把「基于现有 DrawingEngine 扩展」讲清楚就行。如果你在接入或排障时卡住优先看 API Keys 和接入文档https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 和 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。想先验证模型响应是否正常用模型对话页最快https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。长期跑编码和 Agent 任务的话Coding Plan 更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。最后一个实用技巧Codex 生成大段代码后别急着全盘接受。先跑一遍把报错贴回去让它修通常两三轮就能稳定。我试过把_composeLayers里棋盘格背景去掉性能立刻好一截——这种小优化AI 不会主动提得你自己判断。