把 Claude Design 装进本地 coding agent
把 claude.ai/design 的设计引擎封装成跨 harness 的 portable Agent Skill——Claude Code / Cursor / Codex / OpenCode 都能在本地跑,生成 UI mockups、交互原型、线框图、PPT,输出物全是自包含 HTML,零 API、零上传。
# JimLiu/baoyu-design · Node.js / MJS · vendored Babel
核心定位
一句话:把 Claude Design 的能力包装成一个 portable Agent Skill,让 Claude Code、Cursor、Codex(甚至通用的文件读写 agent)都能在本地运行完整的设计引擎,输出物是自包含 HTML——不需要访问 claude.ai/design,不需要订阅,不需要任何远程 API。
作者 JimLiu 在 6 天内(2026-06-06 创建)把这个项目从 0 做到 672 stars,增长速度极快。关键在于它解决了一个真实痛点:coding agent 越来越强,但设计师和开发者想把 AI 生成的设计直接嵌入自己的工作流,需要在「网站」和「编辑器」之间来回切换。baoyu-design 把这个 gap 消掉了。
跨 harness
同一套 Skill 跑在 Claude Code / Cursor / Codex / 通用 agent 上,方法论复用、工具实现解耦。
本地设计引擎
所有能力在本地运行,不访问 claude.ai/design、不需要订阅、不上传任何内容。
自包含 HTML
UI mockups / 交互原型 / 线框图 / PPT 全部产出自包含 HTML,可直接打开或部署。
架构总览
技能根目录 skills/baoyu-design/ 把方法论、harness 映射、子技能、编译 agent、设计构件与 vendored Babel 分层组织。
skills/baoyu-design/
├── SKILL.md ← 入口:harness 检测 + 技能调度
├── system-prompt.md ← 核心设计方法论(38 KB)
├── references/
│ ├── claude.md ← Claude Code 工具映射
│ ├── cursor.md ← Cursor 工具映射
│ └── codex.md ← Codex Agent 工具映射
├── built-in-skills/ ← 设计类型专用子技能
│ ├── hi-fi-design.md ← 默认:高质量 mockup
│ ├── interactive-prototype.md
│ ├── wireframe.md
│ ├── make-a-deck.md
│ ├── mobile-prototype.md
│ ├── create-design-system.md
│ ├── use-design-system.md
│ ├── design-system-authoring-guide.md
│ ├── save-as-standalone-html.md
│ ├── export-as-pptx-editable.md
│ └── ...(20+ 技能文件)
├── agents/
│ ├── compile-design-system.mjs ← 写:编译 DS 为 bundle
│ ├── check-design-system.mjs ← 读:验证 DS 完整性
│ ├── import-design-system.mjs ← 消费者侧同步 DS
│ ├── record-asset.mjs ← 记录 deliverable
│ ├── fork-verifier-agent.md ← 验证子 agent
│ ├── design-system-checker.md
│ └── lib/
│ ├── ds-core.mjs ← 读:DS 解析器(不写磁盘)
│ ├── ds-prompt.mjs ← 生成 _ds_prompt.md
│ └── asset-store.mjs ← _d_meta.json 读写
├── starter-components/ ← 设计构件(React + Babel)
│ ├── design-canvas.jsx ← 多方案对比画布
│ ├── deck-stage.js ← PPT 播放引擎
│ ├── tweaks-panel.jsx ← 实时调参面板
│ ├── animations.jsx
│ ├── ios-frame.jsx / android-frame.jsx / macos-window.jsx
│ └── browser-window.jsx
└── vendor/
└── babel.min.js ← 3.1 MB,JSX 转译
入口调度
检测 harness 类型并加载对应 reference,再把请求分发给 built-in-skills 子技能。
三段式管道
compile(写)/ check(读)/ import(同步)三个 .mjs 脚本 + lib/ 三个纯模块。
离线 Babel
3.1 MB babel.min.js 内联在 repo 里做 JSX → React.createElement 转译,无 CDN 依赖。
Harness 抽象层
baoyu-design 最大的工程突破是 harness 抽象层。system-prompt.md 定义的是设计方法论(craft),但它调用的工具(提问、预览、截图、验证)是 harness-specific 的。
架构把这个问题优雅地解决了——方法论在中心,外围每个 reference 文档把抽象的「Web 工具」映射到具体 harness 的真实工具:
system-prompt.md(设计方法论)
↓
检测 harness 类型
↓
┌─────────────────────────────────────────────┐
│ references/claude.md → Claude Code │
│ references/cursor.md → Cursor │
│ references/codex.md → Codex Agent │
│ (fallback) → 通用 agent │
└─────────────────────────────────────────────┘
每个 reference 文档定义了四个能力的精确映射。以 Claude Code 为例:
| Web 工具 | Claude Code 实现 |
|---|---|
questions_v2 | AskUserQuestion |
done / verifier | SendUserFile + Claude Preview MCP + fork-verifier-agent |
write_file | Write(去掉 asset review pane) |
copy_files | Bash cp |
eval_js | Bash / preview_eval |
web_fetch | WebFetch / WebSearch |
SKILL.md 里的入口逻辑就是简单的 harness 检测:
// SKILL.md 中的检测伪代码
if (has AskUserQuestion + Claude Preview MCP) → read references/claude.md
else if (has AskQuestion + cursor-ide-browser MCP) → read references/cursor.md
else if (has functions.* + tool_search) → read references/codex.md
else → generic workflow(ask in chat, write files, HTTP serve designs/)
设计系统管道
baoyu-design 不只是输出设计物,它还支持完整的设计系统消费和创作管道——受 Claude Design 网站产品启发,但实现了完整的开源版本。
第一段:compile-design-system.mjs(写磁盘)。设计系统的源码(React 组件 + CSS tokens)经过这个编译器,产出三个产物:
_ds_bundle.js— IIFE 格式,定义window.<Namespace>.<Component>,可直接在浏览器里<script src>加载_ds_manifest.json— 命名空间、组件列表、卡片信息、起始点、tokens、字体_adherence.oxlintrc.json— lint 规则(原生 hex/px 值 + per-component prop 白名单)
关键实现细节:它用 vendored Babel(3.1 MB 的 babel.min.js)做 JSX → React.createElement 转译,Babel 以 UMD 加载,用 createRequire(import.meta.url) 在 ESM 模块里引用 UMD——一个很巧妙的兼容性处理。
第二段:check-design-system.mjs(只读,零写磁盘)。这个检查器由 ds-core.mjs(read-only 解析器)驱动,可以作为 subagent 运行,检查内容包括:
- 组件是否正确导出到 window 命名空间
- CSS token 是否存在且无循环依赖
- @dsCard / @startingPoint 标注是否合法
- manifest 和 bundle 是否一致
第三段:import-design-system.mjs(消费者侧同步)。当用户在设计项目里想绑定一个已有设计系统时,这个脚本把编译好的 DS 复制到 <project>/_ds/<slug>/ 下作为自包含副本(版本钉住),并生成 _ds_prompt.md(绑定提示词)和 _d_meta.json(元数据)。
ds-core 解析器
源码:agents/lib/ds-core.mjs。整个设计系统管道的数据核心,实现了对设计系统源码的完整解析——但不写任何磁盘。这是刻意为之的设计决策。
walk(root) → 遍历目录,跳过 node_modules/.git/vendor
buildModel(root) → 构建完整项目 model
├─ global CSS entry 查找(8 个已知文件名优先级)
├─ @import closure 解析(post-order,收集所有依赖)
├─ 组件模块解析(.jsx/.tsx → 命名导出)
├─ @dsCard 卡片提取(设计系统元信息)
├─ @startingPoint 起始点提取(模板)
├─ 品牌字体解析
├─ 命名空间提取
└─ 问题列表(never writes,only reports)
这个 read-only 的约束让 checker 能够作为 subagent 安全运行,也让设计系统的静态分析(linter、diff、版本比较)成为可能——这些能力在没有这个约束的情况下很难实现。
buildModel() 同时被 compile(建模后产出 bundle)、check(验证)、import(决定复制集)三条链路复用,是「一次解析、多处消费」的唯一真相源。资产版本化
asset 记录系统是 baoyu-design 元数据管理的另一条线。record-asset.mjs 负责把每个设计交付物(HTML 文件)记录到 _d_meta.json。
{
"designSystems": [{"slug": "...", "path": "_ds/...", "primary": true}],
"assets": [
{
"name": "Reader Mac App",
"path": "designs/reader-nods/Reader App.html",
"status": "approved" | "changes-requested",
"versions": [{"path": "...", "created": ISO8601}],
"inheritFrom": null
}
]
}
status 字段(approved / changes-requested)实现了设计交付的 review 闭环。record-asset 支持 --status approved 翻转状态,--remove 删除记录,--inherit-from 建立版本链——这是典型的版本控制系统思路,但用于非代码资产。
Starter 构件
源码:starter-components/。设计交付物的原子构件,使用 React + Babel in-browser 转译。
多方案对比画布
把多个 prototype 并排展示,用 DCArtboard 组织。
PPT 播放引擎
slide 位置存在 URL hash 里,支持键盘 / 点击导航。
实时调参面板
设计师可直接在原型上调整参数,参数存在 localStorage 里。
设备外壳
ios-frame / android-frame / macos-window / browser-window 把设计包裹进对应窗口。
视频级动画
读取 built-in-skills/animated-video.md 的规范驱动动画。
react@18.3.1、babel@7.29.0)硬编码在 system-prompt.md 里,确保每次构建的确定性。CSS 推荐用自定义属性(CSS variables)而非 per-component style 对象,避免多文件间命名冲突。验证子 agent
源码:agents/fork-verifier-agent.md。verification 是设计交付物质量保证的关键,它定义了一个专门的验证子 agent。
它的职责是:
- 加载 served 的 HTML(通过 localhost HTTP,不是 file://)
- 捕获 JS console errors
- 截图检查布局
- 探测 overflow root causes 和未解析的
var(--*)token - 返回单一 verdict:
done/needs-work
创作消费全流程
从设计系统作者写组件,到设计 agent 消费、验证、交付——两条链路在 import 处汇合。
【设计系统创作】
设计系统作者
↓
写组件 .jsx + tokens.css
↓
node compile-design-system.mjs → _ds_bundle.js + manifest + lint config
↓
(可选)node check-design-system.mjs 验证完整性
↓
设计系统发布
【设计系统消费】
设计 agent 收到项目请求
↓
glob designs/*/_ds_manifest.json 发现已有 DS
↓
node import-design-system.mjs _ds/<slug>/ designs/<proj>/
↓
_ds/<slug>/_ds_prompt.md 生成(绑定视觉风格)
↓
读取 _ds_prompt.md → 设计时遵循 DS token/component
↓
record-asset.mjs 记录每个交付物
↓
fork-verifier-agent 验证
↓
交付 + 记录 approved/changes-requested
编译调用链全解
以下基于真实源码逐函数拆解。agents/compile-design-system.mjs 的 main 段落只有约 40 行,全部重活分在两个层次:ds-core.mjs 的 buildModel()(纯读,建模)和编译器自身的三个 build 函数(纯写,产出)。
node compile-design-system.mjs <projectDir>
↓
buildModel(projectDir) ← ds-core.mjs(零写磁盘)
├─ walk(root) 跳过 SKIP_DIRS(node_modules/.git/dist/vendor…)
│ + GENERATED 三件套(防止产物被当源码重新摄入)
├─ findGlobalCssEntry() 8 个已知入口名 × 最浅路径优先
│ └─ miss → manifestCssEntry() 旧 _ds_manifest.json 的 globalCssPaths 兜底
├─ resolveCssClosure() post-order @import 闭包(依赖先于入口)
├─ extractTokens() DECL_RE 抓 --token + /* @kind */ 行尾注解
├─ detectFonts() @font-face 家族 vs --fontFamily* 品牌字体
├─ collectExports() .jsx/.tsx 顶层导出(function/class/const/export{})
├─ findTagComment() @dsCard / @startingPoint HTML 注释(限定文件前几行)
└─ resolveNamespace() manifest → bundle header → sha256 三级解析
↓
buildBundle(model) → _ds_bundle.js(IIFE,逐文件 try/catch)
buildManifest(model) → _ds_manifest.json(model 字段直通)
buildAdherence(model) → _adherence.oxlintrc.json(.d.ts 派生 lint 规则)
↓
fs.writeFileSync ×3 ← 整条管道唯一写磁盘的位置(main 段落里的 w() helper)
一个容易被忽略但很关键的细节:walk() 在遍历时把编译产物从源文件集合里剔除,否则第二次编译会把上一次的 _ds_bundle.js 当作源码处理,产生自我污染:
// generated artifacts the compiler emits, never treated as source
const GENERATED = new Set([
'_ds_bundle.js', '_ds_manifest.json', '_adherence.oxlintrc.json',
]);
CSS 入口查找也有一层兜底设计:文件名启发式(styles.css / index.css / globals.css / main.css …共 8 个)miss 时,回退到已编译 manifest 里记录的 globalCssPaths——因为闭包是 post-order 的,数组最后一项一定是入口文件本身。源码注释明确写了动机:从 SPA 提取出来的设计系统可能用 colors_and_type.css 这种非常规命名,「without hardcoding every possible entry name」。兜底拿到入口后仍然重新计算闭包,所以重编译时新增的 @import 不会被旧 manifest 漏掉。
_ds_bundle.js 双作用域
buildBundle() 生成的 IIFE 里有两个刻意分开的对象:__ds_ns(挂到 window.<Namespace> 的公开命名空间)和 __ds_scope(仅 bundle 内部可见的共享作用域,兄弟组件互相引用走这里)。
每个源文件被包成一个独立的 try/catch 块:
/* @ds-bundle: {"format":3,"namespace":"...","sourceHashes":{...}} */
(() => {
const __ds_ns = (window.Namespace = window.Namespace || {});
const __ds_scope = {};
(__ds_ns.__errors = __ds_ns.__errors || []);
// components/button.jsx
try { (() => {
/* transpiled body */
Object.assign(__ds_scope, { Button });
})(); } catch (e) { __ds_ns.__errors.push({ path: "components/button.jsx",
error: String((e && e.message) || e) }); }
__ds_ns.Button = __ds_scope.Button;
})();
这是一个 fail-soft 契约:单个组件抛错只会变成 __ds_ns.__errors 里的一条记录,不会让整个 bundle 加载失败。连转译失败也被纳入同一契约——Babel 抛异常时,该文件的 body 被替换成一条运行时 throw,错误顺着 per-file catch 流进 __errors:
// a file that fails to transpile becomes a runtime error entry,
// matching the bundle's per-file try/catch contract
try {
body = transpile(raw, s.path);
} catch (e) {
body = `throw new Error(${JSON.stringify('transpile failed: ' + ((e && e.message) || e))});`;
}
命名冲突也有明确的优先级规则:有 .d.ts 背书的「模块组件」永远赢得命名空间槽位,非模块文件里的同名导出不允许写入 __ds_scope:
// a non-module file never writes a .d.ts-backed component's name into
// __ds_scope — the module implementation must win the namespace slot
const assignNames = s.isModule ? s.exports : s.exports.filter((n) => !exposed.has(n));
@ds-bundle JSON 注释里带着每个源文件的 sourceHashes(sha256 前 12 位),任何工具不解析 bundle 体就能 diff 出哪些源文件已过期——这是给增量工作流留的钩子,同时也是 resolveNamespace() 的第二级数据源。Babel 剥离插件
设计系统组件有两种写法:参考源用全局风格(const React = window.React,零 import),用户自己写的组件却往往是标准 ESM(import React from 'react' + 相对路径引兄弟组件)。编译器用一个自定义 Babel 插件统一两者。
React 系 import 直接删除,相对路径 import 改写成从 __ds_scope 解构:
ImportDeclaration(p) {
const src = p.node.source.value;
if (src === 'react' || src === 'react-dom' || src === 'react-dom/client') {
p.remove();
return;
}
if (src.startsWith('.') || src.startsWith('/')) {
// relative sibling import → pull the names out of __ds_scope
// import { Icon } from './icon' ⇒ const { Icon } = __ds_scope;
p.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(t.objectPattern(names), t.identifier('__ds_scope')),
]),
);
}
// bare external imports (e.g. a CDN lib) are left as-is for the author to resolve
}
export 系列节点同样被剥掉(ExportNamedDeclaration 保留 declaration 本体,ExportDefaultDeclaration 只保留具名 function/class,匿名 default 直接丢弃)。转译预设是 ['react', { runtime: 'classic' }]——classic runtime 意味着产物调用 React.createElement,所以页面必须先加载 React UMD;.ts/.tsx 再追加 typescript preset。而 vendored Babel 本身是 UMD 格式,在 ESM 模块里加载它用了一个不常见但很干净的技巧:
// UMD → CJS:在 .mjs 里用 createRequire 加载 vendored babel.min.js
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
Babel = require('./vendor/babel.min.js');
Token 分类器
设计 token 要进 manifest 和 lint 配置,必须先知道每个 --token 是 color、spacing 还是 shadow。ds-core 的策略是「标注优先、按值兜底」。
token 提取用一条正则同时抓名字、值和可选的 /* @kind */ 行尾注解:
const DECL_RE =
/(--[A-Za-z0-9-]+)\s*:\s*([^;]+);[ \t]*(?:\/\*\s*@kind\s+([A-Za-z]+)\s*\*\/)?/g;
没有注解的 token 走 classifyByValue()。关键在于分类前先用 makeResolver() 把 var() 引用链解析到底(深度上限 10 防循环引用,并处理 var(--x, fallback) 的兜底值),所以 --btn-bg: var(--brand) 能正确归为 color:
function classifyByValue(raw, resolve) {
const v = String(resolve(raw)).trim();
const hasColor =
/#[0-9a-fA-F]{3,8}\b/.test(v) ||
/\b(rgba?|hsla?|okl?ch|oklab|lab|lch|hwb|color)\(/.test(v) ||
NAMED_COLORS.has(v.toLowerCase());
const hasLen =
/(^|[\s,(])-?\d*\.?\d+(px|rem|em|vh|vw|vmin|vmax|%|pt)\b/.test(v);
if (hasColor && hasLen) return 'shadow'; // 颜色 + 长度并存 ⇒ 阴影
if (hasColor) return 'color';
if (hasLen) return 'spacing';
return null; // unclassifiable by value
}
/* @kind color|spacing|radius|shadow|font */ 注解或改名——把数据质量问题前移到创作时解决,而不是留给消费侧猜。Adherence Lint
_adherence.oxlintrc.json 是给消费侧的「设计系统遵循度」检查。除了两条全局规则(裸 hex 颜色、裸 px 值都应换成 var() token),它为每个组件生成 JSX 级别的 prop 白名单。
数据来源是该组件 .d.ts 里的第一个 exported interface:interface 名去掉尾部 Props 即元素名,成员即合法 prop:
syntax.push({
selector: `JSXOpeningElement[name.name='${name}'] > JSXAttribute > JSXIdentifier[name!=/${allowedRe}/]`,
message: `<${name}> doesn't accept that prop. Declared props: ...`,
});
// 纯字符串 union 类型的 prop 还会得到「值级别」的规则:
syntax.push({
selector: `JSXOpeningElement[name.name='${name}'] > JSXAttribute[name.name='${p.name}'] > Literal[value!=/${valRe}/]`,
message: `<${name}> ${p.name} must be one of 'a' | 'b'.`,
});
key / ref / className / style / children(源码里的 ALWAYS_PROPS 常量)永远在白名单里。parseFirstInterface() 解析时还做了一个细节处理:把 interface body 按 ; 拆行再跑行锚定的 prop 正则,既能匹配单行 interface 的每个成员,又不会把 onChange?: (value: string) => void 这类函数类型误拆出假 prop。
命名空间解析
所有组件都挂在 window.<Namespace> 下,而 @dsCard 卡片 HTML 里写死了 const { X } = window.<Namespace>——一旦重编译改变命名空间,所有卡片同时失效。所以 resolveNamespace() 把「沿用旧值」放在最高优先级。
// 1. existing manifest(_ds_manifest.json 的 namespace 字段)
// 2. existing bundle header(首行 @ds-bundle: {...} JSON 注释里的 namespace)
// 3. derive fresh, deterministic from the project name:
const hash = crypto.createHash('sha256').update(projectName).digest('hex').slice(0, 6);
return `${pascalCase(projectName)}_${hash}`;
第三级的派生是确定性的(同名项目永远得到同一个 6 位 hash 后缀),既避免两个都叫 MyKit 的设计系统在同一页面里互相覆盖 window.MyKit,又保证重复编译可复现。
buildModel() 扫描 README 里的 window.Xxx_abcdef 引用,与当前命名空间不一致就报 issue。连文档漂移都纳入了 checker 的检查面。Import 数据流
agents/import-design-system.mjs 是消费侧唯一入口。它复用 buildModel()(零写)解析 DS 源目录,然后构建一个去重的「精确复制集」,按三层收集。
node import-design-system.mjs <dsDir> <projectDir> [--primary]
↓
前置校验:<dsDir> 根必须有 _ds_manifest.json(没编译过 → 直接报错退出)
↓
buildModel(dsDir) ← 复用 ds-core,决定精确复制集
↓
copy set(copyOnce() 按相对路径去重):
① model.globalCssPaths @import 闭包里的全部 CSS(post-order)
② cssAssetTargets(css) 每个 CSS 引用的本地 url() 资产
│ (字体二进制/图片;剥掉 ?v=… 与 #iefix;
│ relToDs.startsWith('..') ⇒ 越界路径直接跳过)
③ _ds_bundle.js + _ds_manifest.json + _adherence.oxlintrc.json
+ README.md + SKILL.md + assets/ 整目录
↓
_d_meta.json merge:designSystems[] 按 slug upsert;
primaryDesignSystem 首次 import 自动认领,--primary 显式覆盖
↓
renderDsPrompt(...) → _ds/<slug>/_ds_prompt.md(绑定提示词)
↓
stdout 报告:精确 wiring 顺序(React UMD → 每个 <link> 按闭包序 → bundle)
+ @startingPoint 种子清单 + warnings(model.issues 全量透传)
两个值得注意的细节:其一,url() 资产解析对 .. 越界路径直接 skip——一个简单但必要的 path-traversal 防护,保证 _ds/<slug>/ 副本真正自包含;其二,多设计系统共存的 token 冲突没有引入新机制,而是直接用 CSS 层叠顺序解决——报告里明确指示「PRIMARY 系统的 <link> 最后加载,所以它的 token 赢」。
_ds_prompt.md 渲染器
agents/lib/ds-prompt.mjs 是纯字符串装配(注释自述:reads and writes nothing),但它生成的 _ds_prompt.md 是整条消费链路的「执行合同」。
几个工程上很讲究的点:
- 版本钉死到 integrity hash:React 18.3.1 / Babel 7.29.0 的
<script>标签连同integrity="sha384-…"一起硬编码在REACT_UMD_TAGS/BABEL_TAG常量里,注释要求与 system-prompt.md 同步——prompt 必须自包含,「whoever reopens the project」不需要再翻技能文档 - 代码围栏自愈:
extractPromptExcerpt()截取每个组件*.prompt.md的前 5 行作为用法摘要,截断后若围栏数为奇数就自动补一个闭合围栏,防止未闭合的 code fence 吞掉后面整个 prompt - 示例名挑选:
sampleComponentNames()只挑「PascalCase 且含小写字母」的导出做解构示例——避免ICON_NAMES这类常量导出被当成组件用法演示 - token allowlist 是硬护栏:把全部
--*名字排序后列进 prompt,并明确警告「Never guess a token name — an unresolvedvar()silently falls back to the browser default」——这是对 LLM 幻觉 token 名的针对性防御 - Scope 免疫声明:prompt 开头注入 SCOPE 段,声明设计系统只是视觉风格参考,guide 里的示例品牌/人物「never treat … as a fact about the user」——防止 DS 文档内容污染对话语境
- per-component 摘要随 prompt 走:组件的
*.prompt.md文件刻意不复制进_ds/,它们的首行摘要以<ds-prompt-excerpts>块的形式内联在 _ds_prompt.md 里,完整文件留在源树——副本保持最小,信息却不丢
验证探针协议
agents/fork-verifier-agent.md 不只是「截图看看」的检查清单,它内置了两段可直接 eval 的诊断 JS。
第一段定位 overflow / 对齐问题的根因——同时取出元素和父元素的盒模型约束,强制主 agent 修根因(box-sizing、flex 的 min-height:auto、父级未解析的百分比高度)而不是修像素:
// diagnose the constraint before reporting
const el = document.querySelector('...'); const p = el.parentElement;
const pick = (e, cs) => ({rect: e.getBoundingClientRect(), boxSizing: cs.boxSizing,
display: cs.display, position: cs.position, width: cs.width, height: cs.height,
minHeight: cs.minHeight, flexDirection: cs.flexDirection});
JSON.stringify({el: pick(el, getComputedStyle(el)), parent: pick(p, getComputedStyle(p))});
第二段收集页面所有样式表里实际定义过的 CSS 自定义属性(递归进 @layer / @media,不止 :root),再 grep 源文件里引用的 var(--…),集合做差即得「未解析 token」清单——和 _ds_prompt.md 的 allowlist 护栏在交付端闭环:
const defined = new Set();
const walk = rs => { for (const r of rs||[]) { if (r.style) for (const p of r.style)
if (p.startsWith('--')) defined.add(p);
try { walk(r.cssRules || r.styleSheet?.cssRules); } catch {} } };
for (const ss of document.styleSheets) try { walk(ss.cssRules); } catch {}
JSON.stringify([...defined]);
needs_work 只留给真问题(broken layout、console error、缺内容、未解析 token),「Not nitpicks」;以及「The verdict is the only exit」——最终消息必须以 done / needs_work 收尾,没有 verdict 的纯文字回复被定义为 dead end。对比 pi-workflows
虽然 baoyu-design 和 pi-dynamic-workflows 都服务于 coding agent,但解决的问题域完全不同。
| 维度 | pi-dynamic-workflows | baoyu-design |
|---|---|---|
| 解决的问题 | 模型编排:子 agent 的 fan-out / fan-in | 设计生成:UI mockup / prototype / deck |
| 核心模块 | assertDeterministicAst + vm.Script | compile-design-system.mjs + Babel |
| 用户可见输出 | workflow 执行结果(结构化 JSON) | HTML 设计物(mockup/prototype/deck) |
| AST 校验 | 严格:拒绝 Date/Math.random/原型链 | N/A(设计系统编译,非脚本执行) |
| 版本管理 | 无(每次 workflow 全新) | _d_meta.json + 版本链 + status |
| 多 harness | 单 harness(Claude Code) | 4 harness(Claude Code/Cursor/Codex/generic) |
| 沙箱 | node:vm + 冻结 process/console | 浏览器 sandbox(HTTP served) |
技术亮点
七个工程决策共同构成了 baoyu-design 的差异化竞争力。
一套方法论,四套工具
同一套 SKILL.md + system-prompt.md,通过 references/*.md 注入不同 harness 的工具实现,四套代码复用同一套方法论。
创作 / 消费解耦
compile(写)/ check(读-only subagent)/ import(同步)三段式,解耦了「创作工具」和「消费工具」。
可作隔离 subagent
ds-core.mjs 不写磁盘,让 checker 可以作为隔离 subagent 安全运行。
非代码资产的 VCS
_d_meta.json 追踪每个设计物的版本链和 review 状态,实现非代码资产的版本控制。
离线可用
把 3.1 MB Babel 内联在 repo 里,避免 CDN 依赖,确保离线可用。
20+ built-in skills
hi-fi mockup、interactive prototype、wireframe、deck、PPT export——覆盖设计输出的主要形态。
局限性
几个为换取核心价值而做出的必要权衡。
- 设计系统创作(compile-design-system.mjs)需要开发者理解 React + CSS tokens,对非前端背景的用户有门槛
- Babel vendor(3.1 MB)占 repo 体积的绝大部分,但这是为了离线可用性的必要权衡
- starter-components 依赖 React 18.3.1 + Babel 7.29.0 的精确版本锁定,升级需要手动更新 system-prompt.md 里的 integrity hash
- design-system 的「起始点」(starting points)功能依赖 @startingPoint 标注的一致性,标注不规范会导致功能失效
工程陷阱
references/claude.md 和各 .mjs 的注释里记录了大量「踩过才知道」的坑,对在 Claude Code 里实际跑这套技能的人尤其有用。
打不到 React 的 onClick
React 18 createRoot 把事件委托在根容器上,Claude Preview MCP 的合成点击到不了 handler。workaround 是用 preview_eval 找到节点的 __reactProps$* key,手动调 el[propKey].onClick({stopPropagation(){},preventDefault(){}})——真实浏览器点击没问题,这是 harness-only 的坑。
反而可用
全局 keydown 监听能用 window.dispatchEvent(new KeyboardEvent('keydown',{key:'k',metaKey:true,bubbles:true})) 触发——测 ⌘K / Esc 快捷键走这条路。
resize 兜底
页面内 location.reload() 或反复自定义 resize 之后,截图窗口会缩在角落。修复手段是 preview_resize 切到预设尺寸再切回来;文档直接建议用 location.href = … 代替 reload()。
是死路
多文件原型(HTML 加载 <script type="text/babel" src="…jsx">)在 file:// 下被浏览器的 cross-origin 本地脚本读取限制拦死,必须 HTTP。官方姿势是单个 server 服务整个 designs/ 目录(python3 -m http.server 4311 --directory designs),所有项目复用同一个。
损坏时宁可崩
asset-store.mjs 的 readMeta() 对「文件存在但 JSON 非法」选择 throw 而不是返回空对象——注释写明动机:「better than silently clobbering the orchestrator-written fields on the next write」。静默重建会丢掉 title / prompt / designSystems 等编排器写入的字段。
查 __errors
组件转译失败不会让编译报错退出,而是变成 bundle 里的运行时 throw 进 __errors。debug「组件怎么没渲染」时,第一步应该是在控制台查 window.<Namespace>.__errors。
会被 checker 抓
@dsCard 卡片若直接引用组件 .jsx 而不是走 _ds_bundle.js,ds-core 会报 issue——绕过 bundle 意味着绕过命名空间和 fail-soft 契约;但页面加载自己本地的非模块 .jsx 是正常 multi-file-prototype 模式,源码注释里专门说明「is NOT flagged」。
只看第一个 interface
adherence 推导对一文件多 interface 的组件(Card → CardProps + CardHeaderProps)只取第一个;集合组件的规则会落在 item 元素上。这是 advisory lint 的已知边界,不要把它当成完整类型检查。