baoyudesign
JimLiu/baoyu-design · 源码级 2026-06-06 · MIT 基于 skills/ 源码文本

把 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

672
Stars (6 days)
Node.js
运行时
4 Harness
Claude / Cursor / Codex / generic
38 KB
System Prompt
向下探索
01
positioning · the one-liner

核心定位

一句话:把 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 消掉了。

portable

跨 harness

同一套 Skill 跑在 Claude Code / Cursor / Codex / 通用 agent 上,方法论复用、工具实现解耦。

零 API

本地设计引擎

所有能力在本地运行,不访问 claude.ai/design、不需要订阅、不上传任何内容。

self-contained

自包含 HTML

UI mockups / 交互原型 / 线框图 / PPT 全部产出自包含 HTML,可直接打开或部署。

02
layout · the source map

架构总览

技能根目录 skills/baoyu-design/ 把方法论、harness 映射、子技能、编译 agent、设计构件与 vendored Babel 分层组织。

skills/baoyu-design/ 目录树
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 转译
SKILL.md

入口调度

检测 harness 类型并加载对应 reference,再把请求分发给 built-in-skills 子技能。

agents/

三段式管道

compile(写)/ check(读)/ import(同步)三个 .mjs 脚本 + lib/ 三个纯模块。

vendor/

离线 Babel

3.1 MB babel.min.js 内联在 repo 里做 JSX → React.createElement 转译,无 CDN 依赖。

03
harness · one skill, four agents

Harness 抽象层

baoyu-design 最大的工程突破是 harness 抽象层。system-prompt.md 定义的是设计方法论(craft),但它调用的工具(提问、预览、截图、验证)是 harness-specific 的。

架构把这个问题优雅地解决了——方法论在中心,外围每个 reference 文档把抽象的「Web 工具」映射到具体 harness 的真实工具:

system-prompt.md → 按 harness 分流
system-prompt.md(设计方法论)
    ↓
    检测 harness 类型
    ↓
┌─────────────────────────────────────────────┐
│  references/claude.md   → Claude Code        │
│  references/cursor.md   → Cursor             │
│  references/codex.md    → Codex Agent        │
│  (fallback)           → 通用 agent          │
└─────────────────────────────────────────────┘

每个 reference 文档定义了四个能力的精确映射。以 Claude Code 为例:

表 03 · Web 工具 → Claude Code 实现映射
Web 工具Claude Code 实现
questions_v2AskUserQuestion
done / verifierSendUserFile + Claude Preview MCP + fork-verifier-agent
write_fileWrite(去掉 asset review pane)
copy_filesBash cp
eval_jsBash / preview_eval
web_fetchWebFetch / WebSearch

SKILL.md 里的入口逻辑就是简单的 harness 检测:

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/)
解耦了什么:方法论(craft)与工具实现(tooling)被彻底分离——四套不同的 agent 共享同一套 system-prompt.md,只在 references/*.md 这一层切换具体工具名。新增一个 harness 只需新增一份 reference 文档。
04
pipeline · compile / check / import

设计系统管道

baoyu-design 不只是输出设计物,它还支持完整的设计系统消费和创作管道——受 Claude Design 网站产品启发,但实现了完整的开源版本。

图 04 · 三段式管道
compile-design-system.mjs
写磁盘
build
check-design-system.mjs
只读 subagent
sync
import-design-system.mjs
消费者侧同步

第一段: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 是否一致
为什么要 read-only:这个检查器被设计成可以在一个隔离的 Agent 里运行,不写任何磁盘——这对于 Claude Code 的 subagent 机制很重要(subagent 不应该污染主工作区)。

第三段:import-design-system.mjs(消费者侧同步)。当用户在设计项目里想绑定一个已有设计系统时,这个脚本把编译好的 DS 复制到 <project>/_ds/<slug>/ 下作为自包含副本(版本钉住),并生成 _ds_prompt.md(绑定提示词)和 _d_meta.json(元数据)。

设计系统绑定到项目而非全局:每个设计项目有自己独立的 DS 副本,设计系统更新不会意外影响已有项目。
05
ds-core · the read-only model

ds-core 解析器

源码:agents/lib/ds-core.mjs。整个设计系统管道的数据核心,实现了对设计系统源码的完整解析——但不写任何磁盘。这是刻意为之的设计决策。

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、版本比较)成为可能——这些能力在没有这个约束的情况下很难实现。

ds-core 是复用核心:buildModel() 同时被 compile(建模后产出 bundle)、check(验证)、import(决定复制集)三条链路复用,是「一次解析、多处消费」的唯一真相源。
06
asset-store · versioned deliverables

资产版本化

asset 记录系统是 baoyu-design 元数据管理的另一条线。record-asset.mjs 负责把每个设计交付物(HTML 文件)记录到 _d_meta.json。

_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 建立版本链——这是典型的版本控制系统思路,但用于非代码资产。

07
starter-components · atomic building blocks

Starter 构件

源码:starter-components/。设计交付物的原子构件,使用 React + Babel in-browser 转译。

design-canvas.jsx

多方案对比画布

把多个 prototype 并排展示,用 DCArtboard 组织。

deck-stage.js

PPT 播放引擎

slide 位置存在 URL hash 里,支持键盘 / 点击导航。

tweaks-panel.jsx

实时调参面板

设计师可直接在原型上调整参数,参数存在 localStorage 里。

*-frame.jsx

设备外壳

ios-frame / android-frame / macos-window / browser-window 把设计包裹进对应窗口。

animations.jsx

视频级动画

读取 built-in-skills/animated-video.md 的规范驱动动画。

确定性构建:精确的 script 标签版本(react@18.3.1、babel@7.29.0)硬编码在 system-prompt.md 里,确保每次构建的确定性。CSS 推荐用自定义属性(CSS variables)而非 per-component style 对象,避免多文件间命名冲突。
08
fork-verifier · quality gate

验证子 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
共性问题闭环:这个验证流程解决了多 harness 环境下「设计物到底能不能跑」的共性问题——无论在哪个 harness 里,最终的 HTML 交付物都必须能正确加载。探针协议细节见 验证探针协议 →
09
lifecycle · author & consume

创作消费全流程

从设计系统作者写组件,到设计 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
10
compile · the full call chain

编译调用链全解

以下基于真实源码逐函数拆解。agents/compile-design-system.mjs 的 main 段落只有约 40 行,全部重活分在两个层次:ds-core.mjs 的 buildModel()(纯读,建模)和编译器自身的三个 build 函数(纯写,产出)。

node compile-design-system.mjs <projectDir>
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 当作源码处理,产生自我污染:

ds-core.mjs — GENERATED 三件套
// 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 漏掉。

11
bundle · dual scope & fail-soft

_ds_bundle.js 双作用域

buildBundle() 生成的 IIFE 里有两个刻意分开的对象:__ds_ns(挂到 window.<Namespace> 的公开命名空间)和 __ds_scope(仅 bundle 内部可见的共享作用域,兄弟组件互相引用走这里)。

每个源文件被包成一个独立的 try/catch 块:

compile-design-system.mjs — buildBundle() 输出骨架
/* @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:

buildBundle() — 转译失败也走 fail-soft
// 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));
sourceHashes 钩子:bundle 首行的 @ds-bundle JSON 注释里带着每个源文件的 sourceHashes(sha256 前 12 位),任何工具不解析 bundle 体就能 diff 出哪些源文件已过期——这是给增量工作流留的钩子,同时也是 resolveNamespace() 的第二级数据源。
12
makeStripPlugin · two author styles

Babel 剥离插件

设计系统组件有两种写法:参考源用全局风格(const React = window.React,零 import),用户自己写的组件却往往是标准 ESM(import React from 'react' + 相对路径引兄弟组件)。编译器用一个自定义 Babel 插件统一两者。

React 系 import 直接删除,相对路径 import 改写成从 __ds_scope 解构:

compile-design-system.mjs — makeStripPlugin 的 ImportDeclaration visitor
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:createRequire 加载 vendored babel
// 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');
13
tokens · annotation first, value fallback

Token 分类器

设计 token 要进 manifest 和 lint 配置,必须先知道每个 --token 是 color、spacing 还是 shadow。ds-core 的策略是「标注优先、按值兜底」。

token 提取用一条正则同时抓名字、值和可选的 /* @kind */ 行尾注解:

ds-core.mjs — DECL_RE
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:

ds-core.mjs — classifyByValue()
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
}
数据质量前移:分类不出来的 token 不会被静默吞掉,而是聚合成 checker 输出里的「headline issue」(带前 10 个名字的列表),提示作者补 /* @kind color|spacing|radius|shadow|font */ 注解或改名——把数据质量问题前移到创作时解决,而不是留给消费侧猜。
14
adherence · prop allowlist from .d.ts

Adherence Lint

_adherence.oxlintrc.json 是给消费侧的「设计系统遵循度」检查。除了两条全局规则(裸 hex 颜色、裸 px 值都应换成 var() token),它为每个组件生成 JSX 级别的 prop 白名单。

数据来源是该组件 .d.ts 里的第一个 exported interface:interface 名去掉尾部 Props 即元素名,成员即合法 prop:

compile-design-system.mjs — buildAdherence() selector
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。

已知边界(advisory):源码注释坦白了推导的边界——对集合型组件(Dropdown / RadioGroup / TabList),文件里第一个 interface 是 item 形状(DropdownOption 等),所以规则落在 item 元素而非容器上。「This is advisory lint config」,不是硬校验。
15
namespace · stability above all

命名空间解析

所有组件都挂在 window.<Namespace> 下,而 @dsCard 卡片 HTML 里写死了 const { X } = window.<Namespace>——一旦重编译改变命名空间,所有卡片同时失效。所以 resolveNamespace() 把「沿用旧值」放在最高优先级。

ds-core.mjs — resolveNamespace(root, projectName)
// 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,又保证重复编译可复现。

stale readme 启发式:配套还有一个检查——buildModel() 扫描 README 里的 window.Xxx_abcdef 引用,与当前命名空间不一致就报 issue。连文档漂移都纳入了 checker 的检查面。
16
import · copy set & metadata merge

Import 数据流

agents/import-design-system.mjs 是消费侧唯一入口。它复用 buildModel()(零写)解析 DS 源目录,然后构建一个去重的「精确复制集」,按三层收集。

node import-design-system.mjs <dsDir> <projectDir> [--primary]
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 赢」。

17
ds-prompt · the executable contract

_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 unresolved var() 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 里,完整文件留在源树——副本保持最小,信息却不丢
18
probes · two in-page diagnostics

验证探针协议

agents/fork-verifier-agent.md 不只是「截图看看」的检查清单,它内置了两段可直接 eval 的诊断 JS。

第一段定位 overflow / 对齐问题的根因——同时取出元素和父元素的盒模型约束,强制主 agent 修根因(box-sizing、flex 的 min-height:auto、父级未解析的百分比高度)而不是修像素:

fork-verifier-agent.md — 盒约束探针
// 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 护栏在交付端闭环:

fork-verifier-agent.md — 自定义属性收集器
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]);
协议三条硬规则:验证 agent 绝对只读(upstream 的 write_file / str_replace_edit / run_script 全部 off-limits,发现问题只报告、由主 agent 修复后重跑);needs_work 只留给真问题(broken layout、console error、缺内容、未解析 token),「Not nitpicks」;以及「The verdict is the only exit」——最终消息必须以 done / needs_work 收尾,没有 verdict 的纯文字回复被定义为 dead end。
19
contrast · vs pi-dynamic-workflows

对比 pi-workflows

虽然 baoyu-design 和 pi-dynamic-workflows 都服务于 coding agent,但解决的问题域完全不同。

表 19 · 架构对比
维度pi-dynamic-workflowsbaoyu-design
解决的问题模型编排:子 agent 的 fan-out / fan-in设计生成:UI mockup / prototype / deck
核心模块assertDeterministicAst + vm.Scriptcompile-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)
20
highlights · what stands out

技术亮点

七个工程决策共同构成了 baoyu-design 的差异化竞争力。

harness 抽象层

一套方法论,四套工具

同一套 SKILL.md + system-prompt.md,通过 references/*.md 注入不同 harness 的工具实现,四套代码复用同一套方法论。

三段式管道

创作 / 消费解耦

compile(写)/ check(读-only subagent)/ import(同步)三段式,解耦了「创作工具」和「消费工具」。

read-only 解析器

可作隔离 subagent

ds-core.mjs 不写磁盘,让 checker 可以作为隔离 subagent 安全运行。

版本化交付物

非代码资产的 VCS

_d_meta.json 追踪每个设计物的版本链和 review 状态,实现非代码资产的版本控制。

vendored Babel

离线可用

把 3.1 MB Babel 内联在 repo 里,避免 CDN 依赖,确保离线可用。

多格式输出

20+ built-in skills

hi-fi mockup、interactive prototype、wireframe、deck、PPT export——覆盖设计输出的主要形态。

零 API 设计:所有能力在本地运行,不需要访问 claude.ai/design,不需要任何远程服务——这是整个项目的立身之本。
21
limitations · the trade-offs

局限性

几个为换取核心价值而做出的必要权衡。

  • 设计系统创作(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 标注的一致性,标注不规范会导致功能失效
22
pitfalls · self-documented traps

工程陷阱

references/claude.md 和各 .mjs 的注释里记录了大量「踩过才知道」的坑,对在 Claude Code 里实际跑这套技能的人尤其有用。

preview_click

打不到 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()。

file://

是死路

多文件原型(HTML 加载 <script type="text/babel" src="…jsx">)在 file:// 下被浏览器的 cross-origin 本地脚本读取限制拦死,必须 HTTP。官方姿势是单个 server 服务整个 designs/ 目录(python3 -m http.server 4311 --directory designs),所有项目复用同一个。

_d_meta.json

损坏时宁可崩

asset-store.mjs 的 readMeta() 对「文件存在但 JSON 非法」选择 throw 而不是返回空对象——注释写明动机:「better than silently clobbering the orchestrator-written fields on the next write」。静默重建会丢掉 title / prompt / designSystems 等编排器写入的字段。

fail-soft 的另一面

查 __errors

组件转译失败不会让编译报错退出,而是变成 bundle 里的运行时 throw 进 __errors。debug「组件怎么没渲染」时,第一步应该是在控制台查 window.<Namespace>.__errors。

<script src> 组件

会被 checker 抓

@dsCard 卡片若直接引用组件 .jsx 而不是走 _ds_bundle.js,ds-core 会报 issue——绕过 bundle 意味着绕过命名空间和 fail-soft 契约;但页面加载自己本地的非模块 .jsx 是正常 multi-file-prototype 模式,源码注释里专门说明「is NOT flagged」。

prop 白名单

只看第一个 interface

adherence 推导对一文件多 interface 的组件(Card → CardProps + CardHeaderProps)只取第一个;集合组件的规则会落在 item 元素上。这是 advisory lint 的已知边界,不要把它当成完整类型检查。

相关项目:可对比阅读 pi-dynamic-workflows ↗(模型编排层)。
baoyudesign · 源码即文档
JimLiu/baoyu-design · MIT · 2026-06-06
基于 skills/baoyu-design/ 源码文本撰写 · 含真实函数名与代码片段
github.com/JimLiu/baoyu-design ↗