数据截至 (上游 commit b084ab075ba2)
Open Design — 架构与原理
30 秒导读: Open Design 是一个跑在你自己机器上的"AI 设计工作台"。它自己不接大模型,而是探测你本机已经装好的编码 agent CLI(
claude、codex、gemini……共 25 种),把它们当成设计引擎:一个本地 daemon 把"设计技能 + 品牌设计系统 + 插件"拼成一份巨型 system prompt,spawn 出 CLI 子进程,边解析它吐出的流式 JSON 事件边把产物文件落盘,最后在浏览器的沙箱 iframe 里把生成的 HTML 渲染出来给你点。
1. 这是什么(零基础也能懂)
一句话定义: 一个本地优先的设计产品,用"你已经装了的编码 agent CLI"当推理引擎,产出可预览、可导出的单页设计产物。
解决什么问题、给谁用。 假设你已经在终端里天天用 Claude Code 或 Codex 写代码,现在想让它给你做一版落地页、一份 pitch deck、一张海报。直接对着 CLI 说"做个落地页",你会得到一堆紫色渐变 + emoji 图标的 AI 味页面;而且它不知道你团队的品牌规范,也没地方给你预览。Open Design 补的就是这中间那一层。
它的三个卖点,对应三件具体的事:
| 卖点 | 具体是什么 | 落在哪 |
|---|---|---|
| 不锁定模型厂商 | 25 个 CLI 适配器,谁装了用谁 | apps/daemon/src/runtimes/defs/ |
| 品牌可复用 | 150 套 DESIGN.md 设计系统当"品牌合同"注入提示词 | design-systems/ |
| 产物能看能改 | HTML 产物落盘,浏览器沙箱 iframe 实时预览 | apps/web/src/runtime/srcdoc.ts |
它能做什么:
- 生成 web / 桌面 / 移动端原型页面(单页 HTML,真 CSS、真字体)。
- 生成演示稿(可翻页、可导 PPTX / PDF)、图片、视频(HyperFrame 动效)。
- 把一个品牌网站蒸馏成
DESIGN.md,之后所有产出都遵守它。 - 反过来做 MCP 服务器:让别的仓库里的 Claude Code / Cursor 直接调用 Open Design 的项目和产物。
用起来什么样。 装完之后 od 起一个本地 daemon(默认 http://127.0.0.1:7456,见 apps/daemon/src/daemon-url.ts:11 DEFAULT_DAEMON_URL),浏览器打开 UI,选一个 skill(比如 artifacts-builder)、选一套设计系统(比如 apple)、敲一句"给我做个 SaaS 落地页",然后就看着页面一块块流出来。想把它接进别的 agent,一行命令:
# 把 Open Design 的 MCP server 装进 Claude Code 的配置
od mcp install claude
一句话直觉: 把 Open Design 想成给编码 agent 套的一层"设计部门 SOP"——agent 是那个手很快但没受过训练的实习生,skill 是作业流程单,DESIGN.md 是品牌手册,沙箱 iframe 是打印出来贴墙上的样稿。
本节到此不涉及任何代码细节。
2. 顶层全景(它大概怎么转)
2.1 四个圈层
这张图从上到下是一次请求的流向,每一层只跟相邻层说话:
┌──────────────────────────────────────────────────────────┐
│ 浏览器 UI (apps/web, Next.js) │
│ 选 skill / 选设计系统 / 输入 brief ← SSE 事件流回来 │
│ 沙箱 iframe 预览产物 HTML │
└───────┬──────────────────────────────────────────────────┘
│ POST /api/runs → 202,再 GET …/events 拉 SSE 流
┌───────▼──────────────────────────────────────────────────┐
│ daemon (apps/daemon, Express + SQLite) ← 全部智力在这 │
│ ① 组装提示词 ② spawn 子进程 ③ 解析事件 ④ 记账落盘 │
└───────┬────────────────────────────┬─────────────────────┘
│ spawn(bin, args) │ 读/写
┌───────▼────────────────┐ ┌───────▼─────────────────────┐
│ 编码 agent CLI 子进程 │ │ 文件系统 │
│ claude / codex / … │ │ skills/ design-systems/ │
│ stdout = 流式 JSON │──▶│ plugins/ 项目产物目录 │
└────────────────────────┘ └─────────────────────────────┘
怎么读这张图: 智力全在 daemon 那一层;CLI 只是一个"会写文件的黑盒",文件系统才是真正的产品数据库。
2.2 部件一句话职责
| 部件 | 干什么 | 在哪 |
|---|---|---|
apps/web | 浏览器 UI,消费 SSE,渲染沙箱预 览 | apps/web/src/ |
apps/daemon | HTTP API + run 生命周期 + 提示词组装 + 子进程管理 | apps/daemon/src/server.ts |
| runtime 适配层 | 25 个 CLI 的 argv / 流格式 / MCP 注入策略声明 | apps/daemon/src/runtimes/defs/ |
| prompt 层 | 把身份、技能、品牌、插件叠成一份 system prompt | apps/daemon/src/prompts/ |
| 资产文件系统 | 157 个 skill、150 套设计系统、插件目录 | skills/ design-systems/ plugins/ |
| critique 层 | 让第二个 agent 当评审,打分到收敛 | apps/daemon/src/critique/ |
apps/desktop / apps/packaged | Electron 壳,把 daemon + web 包成桌面应用 | apps/desktop/src/main/ |
2.3 主线走一遍(高层,不进代码)
一次生成从头到尾是这样:
- 接单。 浏览器
POST /api/runs,daemon 在内存里建一条 run 记录、立刻回 202,前端拿到runId之后自己再开一条 SSE(apps/daemon/src/routes/runs.ts:1206;唯一调用点apps/web/src/providers/daemon.ts:763)。 - 组装人格。 daemon 读项目绑定的 skill、设计系统、插件快照、用户记忆,拼成一份巨型 system prompt(
apps/daemon/src/prompts/system.ts:843composeSystemPrompt)。 - 挑引擎。 按
agentId找到运行时定义,探测二进制是否在 PATH 上,让适配器自己算出这家 CLI 的 argv(apps/daemon/src/runtimes/types.ts:126RuntimeAgentDef)。 - 拍快照。 spawn 之前先给项目目录里所有产物文件做一次指纹快照(
apps/daemon/src/server.ts:10181调snapshotProjectArtifactsAsync)。这是记账旁路,不是产物通道,见 §4.1。 - 开火。
spawn()子进程,提示词默认走 stdin(apps/daemon/src/server.ts:12295)。 - 翻译。 子进程 stdout 的流式 JSON 按
streamFormat分派给对应解析器,统一成一套内部事件,再当 SSE 推给浏览器(apps/daemon/src/runtimes/json-event-stream.ts:918)。 - 结账。 子进程退出,再拍一次快照做 diff,得出"这一轮真的产出/改动了几个文件"(
apps/daemon/src/routes/runs.ts:2458调diffRunArtifacts)。 - 上墙。 浏览器拉产物 HTML,包一层 srcdoc 桥接脚本,塞进
sandbox="allow-scripts"的 iframe(apps/web/src/runtime/srcdoc.ts:381buildSrcdoc)。
别走错门: daemon 另有一条
POST /api/chat(apps/daemon/src/routes/runs.ts:3068),它把create+stream+start三连写在一个 handler 里,在同一个响应里直接回 SSE,不返回 202。apps/web/src全库没有任何调用点(只有 daemon 自己的测试和外部客户端在用),追主线请走/api/runs。
3. 阅读地 图
六章按"由浅入深"排。只想搞懂它怎么转,读 01 和 03 就够;想改代码,按顺序全读。
| 顺序 | 章节 | 一句话 |
|---|---|---|
| 1 | 主线:一次 run 从按下回车到产物落盘 | 端到端追一条 run:建对象、回 202、开 SSE、spawn、看门狗、退出分类 |
| 2 | 适配 25 家 CLI:RuntimeAgentDef 与双向 MCP | 一个声明式结构体怎么吃下 7 种流协议和 4 种 MCP 注入策略 |
| 3 | 提示词工厂:composeSystemPrompt 怎么拼出一份设计师人格 | 34 个可选段落的叠加顺序、优先级与省 token 的门控 |
| 4 | 文件系统即产品:skills、设计系统与插件三类资产 | SKILL.md / DESIGN.md / open-design.json 三种契约怎么被扫描和解析 |
| 5 | 产物与沙箱预览:从流式文本到 iframe 里能点的页面 | 产物怎么被识别、被守卫、被注入桥接脚本渲染出来 |
| 6 | 质量闭环:第二个 agent 当评审、棘轮不许倒退 | anti-slop 静态检查 + 五角色评审面板 + 灰度棘轮 |
推荐路径:01 → 03 → 02 → 04 → 05 → 06。01 给你骨架,03 是这个项目真正的"核心算法",02 是工程量最大的一支。
本页独有的一节: §4.1 的"产物指纹 diff"不在任何一章展开——01 章追的是 run 的事件通道,而指纹 diff 是 run 结束时的记账旁路,两者不在同一条线上。想看它就在这里看完。
4. 巧妙之处(可借鉴的技术)
4.1 用文件系统指纹 diff 代替"解析每家 CLI 的工具调用"
妙在哪: 想统计"这一轮 agent 写了几个文件",最直觉的做法是解析 agent 的 tool-call 流。但 25 家 CLI 的 tool-call 格式各不相同,实测下来只有 Claude Code 那一家的形状能被识别,codex / opencode / gemini / cursor / amr 全部报 artifact_count: 0(apps/daemon/src/run-artifact-fs.ts:1-14 的文件头注释记着这次审计)。
它的解法: 绕开流,直接在 spawn 前后各拍一次项目目录快照,按 size + mtime + sha1 比对。谁跑的、用什么协议报的,一律不关心——文件动了就是动了(apps/daemon/src/run-artifact-fs.ts:146 snapshotProjectArtifacts、:139 diffRunArtifacts)。
两个值得抄的细节:
- 不用"文件数变化"而用逐路径指纹:只改不增的迭代轮次,文件数不变但确实干了活。
- >1MB 的文件跳过哈希(
HASH_MAX_BYTES,run-artifact-fs.ts:71):大媒体是整体重生成的,size 变化足够识别,哈希只为兜住"字节数相同且 mtime 被保留"的病态改写(apps/daemon/src/run-artifact-fs.ts:58ArtifactFingerprint)。
一句必要的边界声明。 这条 diff 的结果只喂给 run_finished 的 artifact_count 等分析字段(apps/daemon/src/routes/runs.ts:2454-2484),它不搬运产物、不产生事件。所以它和 01 章 §2「没有第四条通道」不矛盾:产物依然是子进程用自己的写文件工具落盘、由 chokidar 单独通知前端;指纹 diff 是旁路记账,走的不是通道。
4.2 提示词注入防御钉在最前面
妙在哪: 这份 system prompt 里会拼进用户自定义指令、项目指令、SKILL.md 正文、DESIGN.md 全文——全是可能被污染的第三方文本。
它的解法: 把"工具结果和文件内容是数据不是指令"这段防御,无条件放在 parts 数组第 0 位,任何后续段落都在它之后,所以在优先级上压不过它(apps/daemon/src/prompts/core-slim.ts:23 PROMPT_INJECTION_RESISTANCE)。同理,评审用的品牌文件被包进 <BRAND_SOURCE> 标签当数据引用(apps/daemon/src/prompts/panel.ts:45 renderPanelPrompt)。
4.3 按"会话稳定信号"门控提示词,保住 prompt 缓存
妙在哪: 方向卡片库约 6.7KB、discovery 层约 3000 token,全塞进去既贵又干扰模型。
它的解法: 只在"这一整个会话里都不会变"的信号上做门控——比如"有没有激活设计系统"(有设计系统时方向库就是废话)、"是不是多端项目"(单端不需要设备框目录)。因为门控条件全会话稳定,拼出来的前缀指纹保持可缓存(apps/daemon/src/prompts/system.ts:843 内 activeDesignSystemBody / isMultiTargetProject 分支)。
4.4 anti-slop 是一个"故意很糙"的 grep 检查器
妙在哪: "AI 味"这种主观问题,居然被做成了确定性的静态检查。
它的解法: 不解析 HTML,就是硬 编码 grep:Tailwind 紫色 / 靛蓝色号、emoji 功能图标、无衬线大标题、编造的指标数字。P0 命中就把结果渲染成系统消息回喂给 agent 让它自纠(apps/daemon/src/lint-artifact.ts:120 lintArtifact、:519 renderFindingsForAgent)。
它承认自己会误报,所以每条 finding 都带原文片段让 agent 自己复核——用"廉价 + 可扩展 + 让下游验证"换掉了"精确但做不出来"。
4.5 收敛规则是纯函数,灰度决策也是纯函数
妙在哪: 评审面板打分是否达标(composite >= threshold && mustFix === 0)写成了不碰 I/O 的纯函数(apps/daemon/src/critique/scoreboard.ts:51 decideRound);连"这套评审要不要扩大灰度"也是纯函数:喂一窗口的每日达标率,返回 promote / hold / demote 建议(apps/daemon/src/critique/ratchet.ts:137 evaluateRollout)。
代价换来的东西: 同一个函数既驱动预发布日志行,又驱动 GET /api/critique/conformance,测试可以逐格钉死决策矩阵而不用起 daemon。
4.6 双向 MCP:既当客户端也当服务端
妙在哪: Open Design 和外部 agent 的关系是对称的,两个方向都通。
| 方向 | 谁调谁 | 实现 |
|---|---|---|
| 向外 | daemon 把 od mcp live-artifacts 作为 MCP server 注入给被 spawn 的子进程 | apps/daemon/src/runtimes/mcp.ts:9 |
| 向外 | daemon 把用户配置的外部 MCP 服务器转发进子进程(4 种策略) | apps/daemon/src/server.ts:11286 |
| 向内 | 别的仓库里的 agent 通过 od mcp stdio 反向操作 OD 项目 | apps/daemon/src/mcp.ts:1778 runMcpStdio |
外部 MCP 转发那 4 种策略被做成了 RuntimeAgentDef 上的一个枚举字段(apps/daemon/src/runtimes/types.ts:195 externalMcpInjection),没适配的 CLI 留空——留空不是静默失败,而是被 UI 读出来显式提示"该 agent 不转发外部 MCP"。
4.7 提示词一律走 stdin
妙在哪: 巨型 system prompt 动辄几十 KB,直接塞 argv 会在三个地方炸:Linux MAX_ARG_STRLEN(单条 argv ~128KB)、Windows CreateProcess(命令行 ~32KB)、.cmd shim(~8KB)。
它的解法: stdin 成为默认通道;个别只认文件的 CLI 走 promptViaFile(daemon 建临时文件、退出后清理);Claude Code 更进一步用 --input-format stream-json 让 stdin 保持开着,从而能中途追加消息(apps/daemon/src/runtimes/defs/claude.ts:52)。
5. 代码地图(导航索引)
按符号名 grep 比按行号更抗漂移;行号 as-of 54de349c。
| 主题 | 文件 | 关键符号 |
|---|---|---|
| run 入口(浏览器聊天主入口) | apps/daemon/src/routes/runs.ts:1206 | app.post('/api/runs') |
| run 入口(内联 SSE 变体,web 无调用者) | apps/daemon/src/routes/runs.ts:3068 | app.post('/api/chat') |
| 前端发起 run 的调用点 | apps/web/src/providers/daemon.ts:763 | fetch('/api/runs', …)、consumeDaemonRun |
| run 对象与 SSE 广播 | apps/daemon/src/runtimes/runs.ts:634 | createChatRunService、TERMINAL_RUN_STATUSES |
| spawn 主流程 | apps/daemon/src/server.ts:9658 | startChatRun |
| 实际 spawn 调用 | apps/daemon/src/server.ts:12295 | spawn(invocation.command, …) |
| 提示词组装(daemon 侧入口) | apps/daemon/src/server.ts:8819 | composeDaemonSystemPrompt |
| 提示词组装(核心) | apps/daemon/src/prompts/system.ts:843 | composeSystemPrompt |
| 注入防御段 | apps/daemon/src/prompts/core-slim.ts:23 | PROMPT_INJECTION_RESISTANCE |
| 设计师人格底稿 | apps/daemon/src/prompts/official-system.ts:184 | renderOfficialDesignerPrompt |
| 需求发现 / 方案分叉层 | apps/daemon/src/prompts/discovery.ts:262 | renderDiscoveryAndPhilosophy |
| deck 固定框架(钉在最后) | apps/daemon/src/prompts/deck-framework.ts:355 | DECK_FRAMEWORK_DIRECTIVE |
| 运行时定义结构体 | apps/daemon/src/runtimes/types.ts:126 | RuntimeAgentDef、RuntimeContext |
| 25 个适配器注册表 | apps/daemon/src/runtimes/registry.ts:31 | BASE_AGENT_DEFS、getAgentDef |
| 单个适配器范例 | apps/daemon/src/runtimes/defs/claude.ts:16 | claudeAgentDef、buildArgs |
| CLI 探测 | apps/daemon/src/runtimes/detection.ts:444 | detectAgents、detectAgentsStream |
| 流式 JSON 解析(多家共用) | apps/daemon/src/runtimes/json-event-stream.ts:918 | createJsonEventStreamHandler |
| Claude 专用流解析 | apps/daemon/src/runtimes/claude-stream.ts | createClaudeStreamHandler |
| 向子进程注入 OD 工具 | apps/daemon/src/runtimes/mcp.ts:9 | buildLiveArtifactsMcpServersForAgent |
| 子进程可调的工具端点白名单 | apps/daemon/src/tool-tokens.ts:22 | CHAT_TOOL_ENDPOINTS、CHAT_TOOL_OPERATIONS |
| 反向 MCP server | apps/daemon/src/mcp.ts:1778 | runMcpStdio |
| skill 扫描 与解析 | apps/daemon/src/skills.ts:232 | listSkills、SKILL_ID_ALIASES |
| 产物路径判定 | apps/daemon/src/runtimes/run-artifacts.ts:94 | isArtifactPath、isDesignSystemFile |
| 产物指纹快照 / diff | apps/daemon/src/run-artifact-fs.ts:146 | snapshotProjectArtifacts、diffRunArtifacts、HASH_MAX_BYTES |
| 指纹基线的两个调用点 | apps/daemon/src/server.ts:10186、apps/daemon/src/routes/runs.ts:2454 | runArtifactBaselines.remember / .take |
| 产物 manifest 校验 | apps/daemon/src/artifacts/manifest.ts | ALLOWED_KINDS、ALLOWED_RENDERERS |
| 占位符发布守卫 | apps/daemon/src/artifacts/publication-guard.ts:38 | UNRESOLVED_ARTIFACT_PLACEHOLDERS |
| anti-slop 检查 | apps/daemon/src/lint-artifact.ts:120 | lintArtifact、renderFindingsForAgent |
| 评审编排 | apps/daemon/src/critique/orchestrator.ts:123 | runOrchestrator、OrchestratorParams |
| 评审收敛判定 | apps/daemon/src/critique/scoreboard.ts:51 | decideRound、computeComposite |
| 灰度棘轮 | apps/daemon/src/critique/ratchet.ts:137 | evaluateRollout、ConformanceDay |
| 沙箱预览文档拼装 | apps/web/src/runtime/srcdoc.ts:381 | buildSrcdoc、buildLazySrcdocTransport |
| 预览 iframe 挂载点 | apps/web/src/components/DesignFilesPanel.tsx:2098 | sandbox="allow-scripts allow-downloads" |
| 数据目录布局 | apps/daemon/src/server.ts:1165 | RUNTIME_DATA_DIR、PROJECTS_DIR、SKILL_ROOTS |
| 本地 daemon 地址 | apps/daemon/src/daemon-url.ts:11 | DEFAULT_DAEMON_URL |
资产目录(不是代码,是产品数据):
| 资产 | 目录 | 数量(as-of 本 commit) |
|---|---|---|
| 设计技能 | skills/<id>/SKILL.md | 157 |
| 设计系统 | design-systems/<id>/DESIGN.md | 150 |
| 插件规范与样例 | plugins/spec/SPEC.md、plugins/_official/ | 规范 + 官方样例 |