dsh-draw2code BDD 验收说明

August 22, 2026 · View on GitHub

features/draw2code.feature 面向插件的真实协作契约:workspace 门禁、draw2code_create 的自适应产品深挖与可执行项目简报、项目草稿与版本、确认后独立画板、agent 工具、冲突确认、场景持久化、客户端挂载同步、画板切换,以及 draw2code_generate 从页面范围选择到真实预览验收的完整产品流程。它不把 Excalidraw 的坐标或 React 内部 ref 当作用户行为。

已实现并通过真实宿主验收 — draw2code_generate 产品流程

完整决策见 GENERATE_PRODUCT_FLOW.md。本节既是产品契约,也是当前实现的验收口径。

职责边界

  • generate 只负责把用户选定的原型页面转换成经过预览验收的单文件 HTML Demo;不搭建正式前端工程,也不接管生成后的长期修改。
  • create 不是强制前置条件:有简报则继承,没有简报则从画板建立生成简报。
  • 每次生成都展示全部可识别页面让用户选择;新页面来自语义化 rectangle 页面外框,旧命名 Frame 继续兼容。系统智能推荐并将推荐项置顶、显式标记和解释原因。宿主支持预选时可默认勾选;当前 DSH 不支持预选,因此不能伪装成已选择,用户始终拥有最终范围控制权。
  • 首次生成选择整体视觉方向,后续默认继承;不把颜色、字体、圆角和阴影拆成参数问卷。
  • 用户只选择一个整体视觉方向,工具在内部把它展开为气质、背景、主操作、语义色、密度、字体层级、布局策略、动效和视觉焦点,不增加额外问答。
  • 移动端或桌面端优先从统一页面边界尺寸推断,只有混合布局等真实歧义才询问。

原型门禁与确认

  • 页面无法理解、核心流程断开、关键操作或 mock 数据缺失属于阻断问题,必须先回画板补齐。
  • Agent 自动补齐也要先更新画板并让用户看到,不能只在 HTML 中增加产品内容。
  • 次要图标、动画和非核心状态属于非阻断提醒,只在最终简报说明默认值。
  • 页面范围、视觉方向和原型检查完成后,只展示一次生成简报并确认,不逐项复述。最终确认使用可点击的“确认生成 / 修改页面范围 / 修改视觉方向”选项,不要求用户手动输入“确认”;修改时只返回对应选择,不重问其他已完成项。

输出和完成标准

  • 始终输出统一入口的单文件 HTML,不询问技术栈,也不创建 generate 版本历史。
  • 画板定义页面、信息层级、文案、mock 数据、组件语义和交互关系;前端使用内容流、CSS Grid/Flex 与响应式约束重新排版,不复制 Excalidraw 绝对坐标。
  • 可以补充让已有交互可用的通用反馈,但不能自行发明页面、模块、流程或重大业务规则。
  • 生成后自动打开预览、逐页截图、检查目标视口、控制台、DOM、溢出、裁切、按钮居中和底部导航,并实际走通核心流程。
  • action=complete 必须提交结构化 verificationEvidence;工具会用 outputSha256 把本地预览绑定到当前生成入口,用 captureId 关联同次截图和 DOM 快照,并读取 workspace 内带 SHA-256 的 PNG/DOM 产物,核对视口尺寸与原型关键文案。缺少逐页证据或多页面切换证据、控制台存在 error/warning、布局或交互检查失败时不得进入 completed。
  • 生成页面必须保留稳定的 d2c-page 起止注释;重新生成时工具直接比较未选页面块哈希,选择全部页面时不要求不存在的“未选页面”证据。
  • 实现问题由 Agent 自动修复和复验,产品变化才询问用户。
  • 中断后恢复已完成的选择,不交付未验收的半成品。
  • 验收通过后 generate 结束,普通修改回到日常协作;只有用户明确要求重新生成时才开启新一轮。

产品级实施顺序

  1. 准备与选择:事实继承、全页面多选、智能推荐与视觉方向。
  2. 原型门禁:阻断/提醒分级、回画板补齐和流程恢复。
  3. 统一确认:生成简报和已有内容保护。
  4. 生成与验收:单文件产物、自动预览、核心流程验证和自动修复。
  5. 中断与收口:恢复准备状态、禁止半成品冒充完成、验收后结束 generate。

本轮新增 — draw2code_create

工具职责

  • draw2code_create 是新项目意图的入口,不是 grilling 完成后的收尾工具。
  • action=start 创建 draw2code/.projects/<projectId>.json 草稿并返回 discovery:已明确事实、待解决维度、历史决策和最多 10 题的剩余预算;不会创建画板。
  • 信息不足时 Agent 用 action=propose_question 提交产品专属 insight、一个决策问题、2–4 个有真实取舍的选项、推荐方向、决策影响和依赖;模型再调用宿主 ask_user_question 原样展示。禁止固定依次询问平台、用户、目标、流程、模块和页面。
  • action=answer 记录一个选择后返回 discovery;action=skip 把当前题保留为待验证假设。信息足够或用户要求停止时,Agent 用 action=synthesize 提交结构化 PrototypeBrief,即使当前题尚未回答也不会卡住。
  • 工具校验 PrototypeBrief 后确定性生成完整 briefMarkdownpageBlueprintspageMockData,三者来自同一事实来源。ready 阶段必须完整展示 Markdown,只做一次统一确认。
  • 选择“其他”后直接记录自由文字并继续下一题;ready 项目简报是唯一统一确认点,不再逐项复述用户原话。
  • action=confirm 只在项目简报 ready 且用户确认后创建独立空画板,并把 active-board 指针切到新画板;随后模型必须调用 draw2code_update
  • 项目简报和画板是两种事实:简报记录产品意图,画板记录当前可见原型;确认前不写画板,用户删除的画板内容不能由简报自动恢复。

草稿状态与可靠性

  • draft:用户中断或沉默时保持不变,不猜测暂停/取消。
  • ready:核心问题已完成,等待用户确认简报。
  • confirmed:需求已确认,独立画板已经创建,等待 draw2code_update
  • abandoned / archived:只有用户明确选择对应动作才进入。
  • 每个项目有 revision;旧 revision 返回 stale_revision,不会覆盖最新草稿。
  • 同一 mutation 重试返回缓存的幂等结果,不重复追加回答或推进问题。
  • 项目草稿写入前会归档旧版本到 draw2code/.projects/.versions/<projectId>/

已实现的自适应 SOP 与项目简报

每轮从触发场景、现有替代、核心结果、独特机制、使用闭环、关键风险和首版验证中选择当前影响最大的一项;用户已经说清楚的信息不重复问,模块和页面由产品判断推导后在最终简报统一确认。简单产品通常 3–5 题,复杂产品最多 10 题;预算按完整问题历史计算,修改答案不会刷新额度。“还没想好”或跳过记录为待验证假设,不理解为暂停或取消;原生问题卡片始终提供“直接整理项目简报”,避免输入框被卡片隐藏后无法停止。视觉风格不在原型阶段应用,主动提供的 styleNote 延迟给 draw2code_generate

PrototypeBrief 必须完整包含产品定义、首版包含与排除范围、原型布局、逐页目标与具体结构、每页至少 3 条真实 mock 数据或表单字段、关键状态、页面关系、原型表达原则、验收方式和默认假设。工具据此生成可直接给用户阅读的 Markdown,以及 draw2code_update 使用的页面蓝图和 mock 数据蓝图;列表、聊天、图表、详情和状态组件不能以空白方框、Lorem ipsum、“用户A”“标题”“内容”等占位。

本轮新增 — 低保真原型可读性与语义修复

  • draw2code_update 把落盘正确性与产品原型质量拆开:兼容字段 verified 与规范字段 writeVerified 只证明写入及回读一致;prototypeQuality 单独报告结构、内容、布局、质量分和 warnings;只有最终 visualReview 覆盖全部页面后才返回 completionReady=true

  • 空白过多、首屏信息不足、文字层级平、主操作不清、状态无强调、边框滥用、点击区域过小、页面边距或重复控件节奏不一致,会以可执行的质量 warning 返回,不再把“写进去了”误当成“画好了”。

  • 首次批量创建 3 个及以上页面会返回 visual-review-required;Agent 必须先画一个代表页、在真实画板检查后再铺开剩余页面,并在最后逐页复核。visualReview 与最近一次 update 的 revrevealRequestId 绑定,且 Canvas 必须已经实际加载并确认同一 board + revision;旧证据不能重放,最终复核必须在写入完成后的独立空 ops 调用中提交。

  • draw2code_update 除规范的 {op:"upsert",element:{...}} 外,也接受直接元素、省略 opelement 包装和字段平铺的 upsert;只对同时具有非空 id + type 的无歧义输入做兼容,不猜测 bare id 是删除还是修改。op=delete 时也兼容 elementIdelement.id,避免 Agent 只因移动 id 字段重发整批修改。

  • 同一批对相同元素 id 的多次操作按最终净结果读回验证:upsert→delete 期望元素不存在,delete→upsert 期望最终元素存在;不会因临时中间态已经被后续操作覆盖而误报失败。

  • frameId 的子元素若原坐标无法落入 frame、而加上 frame 左上角后能完整落入,会被安全识别为 frame 局部坐标并换算为画布绝对坐标;已经正确的绝对坐标保持不变,含义不明确的坐标仍由 layout-invalid 阻止写盘。

  • 读回校验认可工具自身执行的组件语义对齐修复;例如 Chip 标签从 left/top 规范为 center/middle 后仍返回 verified=true,不会在数据已经落盘后误报失败并诱发重复覆盖。

  • draw2code_update 写盘前会检查多行或预计换行的 text 是否有足够高度;失败返回 layout-invalid,不写入半截组件。

  • rectanglediamondellipse 不再允许携带依赖 Excalidraw 形状文字的 text;按钮、卡片和输入框文案必须使用独立的 text 元素。

  • bottom-navigation 必须使用 customData.role=bottom-navigation 的矩形 shell 加独立标签,并位于页面底部安全区;空 shell、互相重叠的栏目和普通一行“底部导航:...”文字都会被拒绝。

  • 页面内组件不得越过页面边界;成功写入后工具仍会返回 layoutWarnings,让模型能发现旧画板中已有的视觉问题。

  • 预检只阻塞本次 Agent 更新涉及的元素;用户已有的旧问题会作为提醒返回,不会阻塞用户继续手工编辑。

  • draw2code_create action=start 要求 Agent 基于完整需求先概括并显式传入语义化 projectName;工具不再用正则、关键词或前 N 字裁剪从原话造名称,只做合法性校验。确认后的画板名直接使用 projectName,不追加“原型”后缀;完整 idea 仍保存在项目草稿和简报中。

  • draw2code_update 会把 text 的 containerId 补成 Excalidraw 完整双向绑定;普通读取、打开画板和客户端写回不会借机改写既有故障样本。

  • Agent 新增绑定文字的组件时必须用 customData.role 声明按钮、选择框、输入框、Chip、卡片等产品语义;缺失时返回 component-role-missing,不再把未知控件静默写成左上对齐。

  • buttonprimary-actionchiptabbottom-navigation-item 等操作型文案不只规范为 center/middle,还会把文字盒缩至真实行高,并按外框几何重新计算垂直中心;inputselectdropdownsearch-field 等表单值保持 left/middle,不会为了修按钮而误改输入内容。

  • 底部导航 shell 内的每个独立标签必须设置 customData.role=bottom-navigation-item;缺失时返回 bottom-navigation-item-role-missing。即使 Agent 错把多个栏目文字都绑定到 shell,update 也会将其修复为独立文字、保留各自槽位并按 shell 垂直居中;空 shell 返回 bottom-navigation-items-missing,栏目重叠返回 bottom-navigation-item-overlap

  • 新页面归属通过 prototype-page rectangle 的几何范围判断,页面子元素保持 frameId=nullcontainerId 只用于形状的唯一绑定标签。旧 Frame 画板仍保留 frameId 兼容,若旧 Agent 错把 text 的 containerId 指向 frame,update 仍会原子修复为 containerId=null 与对应 frameId

  • 原型不询问品牌视觉,但允许形状用 customData.tone 表达 primary、success、warning、danger、info、neutral 六种语义;使用浅底色和对应描边,且不覆盖显式颜色。

  • 完整新页面使用普通 rectangle 外框,设置 customData.role=prototype-pagecustomData.pageNamecustomData.mockDataMin;外框上方独立 text 使用 role=prototype-page-labelpageId。承载示例记录的可见 text 使用 role=mock-data。数量不足或只写无意义占位符时返回 mock-data-insufficient,整批更新不落盘。

  • 新页面组件不成组、不设 frameId,优先保证用户可以直接点选编辑;移动整页需要用户框选页面内容。用户在两个新页面之间手绘 Arrow 时,箭头保持画布级并完整显示,不受页面矩形裁切。

  • draw2code_read 返回规范字段 pageNamespagespageRelations,跨页箭头不混入任一页面 UI 内容;deprecated frameNames 返回相同页面名供旧调用兼容。

  • draw2code_generate 使用 pages 作为规范范围参数,frames 仅作兼容别名;两者同时传入但内容不一致时返回 page-scope-conflict

  • 页面矩形重叠导致普通元素同时落在多个页面时返回 page-membership-ambiguous warning,不移动或重写用户元素。

  • 页面名称重复时读取返回 page-name-duplicate,generate 拒绝按名称猜测页面;用户需要先把页面改成唯一名称。

当前审查结论

已修复 — replace 操作在工具层丢失 scene

src/tools.tsparseUpdateOps() 现在会把 replace.scene 传递到 SceneStore,因此 draw2code_update 的整页替换可以继续进入统一归一化、原子写入和读回验证流程。

修复前的复现证据是:upsert 首次调用返回 verified: true;随后同一画板的 replace 返回:

bad-ops: ops[0] is "replace" but missing its scene

已修复 — 场景写回破坏箭头和链接元数据

normalizeElement() 现在会保留合法的 linkstartBindingendBindingstartArrowheadendArrowheadlastCommittedPoint;缺失或类型不合法的值仍使用安全默认值。浏览器和 agent 继续共享归一化流程,但不会再清空完整 Excalidraw 元素的连接语义。

回归测试现在写入带 startArrowhead: "arrow"endArrowhead: "triangle"、双向 binding 和 https://example.com/prototype 的 arrow,并断言磁盘读回值保持不变。

已修复 — 删除当前活动画板后的旧画面/复写窗口

删除流程现在会阻止新的 debounce save、等待已发出的同板保存完成,再调用 DELETE;删除当前活动画板且没有其他画板时会强制重置 board state,即使 fallback 名称仍是 prototype 也不会因为同名提前 return。建议后续在真实宿主中补一条删除活动画板的 UI 回归场景。

已修复 — 防抖保存使用了错误的版本基线,可能复活删除内容

原实现只把待保存的 name + elements 放进队列,定时器触发时重新读取 revRef.current。如果用户删除后 Agent 恰好先写入,用户的旧场景就可能带着 Agent 的新 revision 直接覆盖回去,表现为“Agent 画完的页面消失”或“刚删掉的模块又出现”。

现在每个防抖编辑 burst 会锁定首次编辑时的 baseRev + baseElements;后续指针事件只更新候选元素,不移动 compare-and-swap 基线。冲突合并已抽到 src/client/sync.ts,并在一次写入发生连续冲突时最多继续重试 3 次,每次都重新读取当前场景后做三方合并。回归覆盖了“用户删除 + Agent 新增页面 + 两次连续冲突”,确认删除内容不会复活、Agent 新增会保留。

已修复 — Agent 成功写入但用户看不到:当前画板不再只存在 localStorage

原实现把当前画板名只存在浏览器 localStorage,host 侧工具不知道用户正停在哪一块画板;工具调用如果默认写 prototype,可以得到磁盘 verified=true,但用户眼前可能正在看“顾客端”。

现在浏览器通过 loopback /api/draw2code/active-board 将当前画板同步到 workspace 的 draw2code/.active-board.jsondraw2code_updatedraw2code_readdraw2code_generate 省略 name 时使用这个共享指针。更新在写入并回读验证成功后会选中目标画板、发布绑定 board + revision 的一次性 reveal request,并由常驻客户端监听器自动打开或激活“画码”;Canvas 实际加载该 revision 后才回执,冲突、布局失败和重复轮询不会触发抢焦点。

已修复 — host 重启后没有快照会放行已有元素覆盖

boardCache 是运行时内存状态,host 重启后为空。旧逻辑在没有快照时会放行对已有 id 的 upsert/delete,无法区分 Agent 自己的旧元素和用户刚改过的元素。现在无快照时只有新增 id 可以直接执行;任何触碰已有 id 的操作都进入确认流程,避免重启把协作保护降级成静默覆盖。

已修复 — Agent 把 frame 标题写入 text 时页面生成找不到 frame

实际 Harness 回归中,Agent 传入的 frame 使用了 text 而不是 Excalidraw 的 name。归一化现在在 name 为空时把已写入的 text 作为 frame name,保持画框可见性的同时让 draw2code_generate(frames=[...]) 能按页面名找到它。

已验证事实

  • npm run typecheck 通过。
  • npm run build 通过,并生成 dist/index.jslib/client.js
  • npm test 通过:当前 100 个 Node 内置回归测试全部通过,覆盖无 Frame 页面外框与外部标题契约、跨页箭头及绑定文案归类、绑定优先级、同页箭头端点归属、用户手绘箭头保护、mock 数据增量删除门禁、重复页面名拒绝、rectangle/legacy Frame 混合画板、pages/frames 兼容冲突、update 参数容错、同批净结果校验、legacy Frame 局部坐标安全换算、按钮文字真实几何居中、底部导航门禁、同步协调、删除不复活、UTF-8 容量限制、当前画板目标解析、自动 reveal、create grilling、原型质量门禁和 generate 恢复/验收流程。
  • 项目包含 Node 内置回归入口和 features/draw2code.feature BDD 契约。
  • DeepSeek Harness 的真实界面已加载 画码 标签页,当前能看到 prototype 画板、新画板 菜单入口、Excalidraw 工具栏和原型文字元素;这证明插件注册与非空画板挂载路径至少可达。
  • 重新启动 dsh web 后,GET /api/draw2code/active-board?root=<workspace>GET /api/draw2code/reveal-request?root=<workspace> 均能从真实 host 返回成功 envelope;仅刷新网页不足以重装 host bundle。
  • Harness 真实删除回归确认:用户删除与 Agent 新增发生并发冲突时,旧内容不会复活,Agent 新增内容仍会保留并显示。
  • Harness 最终 update 回归确认:新画板“更新容错回归三”只调用一次 draw2code_update,14 个 ops 一次应用并返回 verified=true;frame 局部坐标、Chip left/topcenter/middle 的语义修复、嵌套 delete,以及同批 upsert→delete 均未触发重试。活动画板 API 返回该画板,磁盘读回为 12 个元素且 temp-note 不存在,真实画布可见“任务详情页”和三条检查项。
  • 无 Frame 真实协作回归确认:真实 DSH 0.3.0 在“无Frame回归”画板显示两个 prototype-page rectangle,页面内元素均为 frameId=null。用户通过 Excalidraw 箭头工具从“查看详情”按钮手工拖到第二页目标卡片,箭头完整跨过两个页面边界,磁盘读回 frameId=null、双端 binding 完整,draw2code_read.pageRelations 正确返回“任务列表 → 任务详情”。
  • 用户改动保护确认:用户又在真实画布把“评审需求文档 · 14:00”改为“评审需求文档 · 15:00”,随后 Agent 只新增“明天 10:00 复盘”组件;draw2code_update 返回 verified=true,读回确认手工文案、跨页箭头及其 binding 均未被覆盖,真实画布也同时可见三者。
  • Legacy Frame 兼容确认:真实 workspace 的“小猪清单”仍被读取为 5 个 legacy-frame 页面,deprecated frames 参数可以启动 generate 的 page-scope 选择;调用前后原画板 SHA-256 均为 5307fa45aa5193d2d8c82245492261c8ae6fdd9726debda59fcdbb164bae6d36,没有自动迁移或重写。

验收边界

电脑验收与 host 测试承担不同证据:真实 Harness 已覆盖插件加载、普通矩形页面显示、用户手绘跨页箭头、手工文字修改、Agent 增量更新和 legacy Frame generate 兼容;npm test 继续覆盖 replace、元数据保留、冲突合并和 workspace 安全边界。客户端删除活动画板仍建议后续补一条独立 UI 自动化场景。