跳到主要内容

数据截至 (上游 commit b084ab075ba2)

Open Design — 架构与原理

30 秒导读: Open Design 是一个跑在你自己机器上的"AI 设计工作台"。它自己不接大模型,而是探测你本机已经装好的编码 agent CLI(claudecodexgemini……共 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/daemonHTTP API + run 生命周期 + 提示词组装 + 子进程管理apps/daemon/src/server.ts
runtime 适配层25 个 CLI 的 argv / 流格式 / MCP 注入策略声明apps/daemon/src/runtimes/defs/
prompt 层把身份、技能、品牌、插件叠成一份 system promptapps/daemon/src/prompts/
资产文件系统157 个 skill、150 套设计系统、插件目录skills/ design-systems/ plugins/
critique 层让第二个 agent 当评审,打分到收敛apps/daemon/src/critique/
apps/desktop / apps/packagedElectron 壳,把 daemon + web 包成桌面应用apps/desktop/src/main/

2.3 主线走一遍(高层,不进代码)

一次生成从头到尾是这样:

  1. 接单。 浏览器 POST /api/runs,daemon 在内存里建一条 run 记录、立刻回 202,前端拿到 runId 之后自己再开一条 SSE(apps/daemon/src/routes/runs.ts:1206;唯一调用点 apps/web/src/providers/daemon.ts:763)。
  2. 组装人格。 daemon 读项目绑定的 skill、设计系统、插件快照、用户记忆,拼成一份巨型 system prompt(apps/daemon/src/prompts/system.ts:843 composeSystemPrompt)。
  3. 挑引擎。agentId 找到运行时定义,探测二进制是否在 PATH 上,让适配器自己算出这家 CLI 的 argv(apps/daemon/src/runtimes/types.ts:126 RuntimeAgentDef)。
  4. 拍快照。 spawn 之前先给项目目录里所有产物文件做一次指纹快照(apps/daemon/src/server.ts:10181snapshotProjectArtifactsAsync)。这是记账旁路,不是产物通道,见 §4.1。
  5. 开火。 spawn() 子进程,提示词默认走 stdin(apps/daemon/src/server.ts:12295)。
  6. 翻译。 子进程 stdout 的流式 JSON 按 streamFormat 分派给对应解析器,统一成一套内部事件,再当 SSE 推给浏览器(apps/daemon/src/runtimes/json-event-stream.ts:918)。
  7. 结账。 子进程退出,再拍一次快照做 diff,得出"这一轮真的产出/改动了几个文件"(apps/daemon/src/routes/runs.ts:2458diffRunArtifacts)。
  8. 上墙。 浏览器拉产物 HTML,包一层 srcdoc 桥接脚本,塞进 sandbox="allow-scripts" 的 iframe(apps/web/src/runtime/srcdoc.ts:381 buildSrcdoc)。

别走错门: daemon 另有一条 POST /api/chatapps/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: 0apps/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_BYTESrun-artifact-fs.ts:71):大媒体是整体重生成的,size 变化足够识别,哈希只为兜住"字节数相同且 mtime 被保留"的病态改写(apps/daemon/src/run-artifact-fs.ts:58 ArtifactFingerprint)。

一句必要的边界声明。 这条 diff 的结果只喂给 run_finishedartifact_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:843activeDesignSystemBody / 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:1206app.post('/api/runs')
run 入口(内联 SSE 变体,web 无调用者)apps/daemon/src/routes/runs.ts:3068app.post('/api/chat')
前端发起 run 的调用点apps/web/src/providers/daemon.ts:763fetch('/api/runs', …)consumeDaemonRun
run 对象与 SSE 广播apps/daemon/src/runtimes/runs.ts:634createChatRunServiceTERMINAL_RUN_STATUSES
spawn 主流程apps/daemon/src/server.ts:9658startChatRun
实际 spawn 调用apps/daemon/src/server.ts:12295spawn(invocation.command, …)
提示词组装(daemon 侧入口)apps/daemon/src/server.ts:8819composeDaemonSystemPrompt
提示词组装(核心)apps/daemon/src/prompts/system.ts:843composeSystemPrompt
注入防御段apps/daemon/src/prompts/core-slim.ts:23PROMPT_INJECTION_RESISTANCE
设计师人格底稿apps/daemon/src/prompts/official-system.ts:184renderOfficialDesignerPrompt
需求发现 / 方案分叉层apps/daemon/src/prompts/discovery.ts:262renderDiscoveryAndPhilosophy
deck 固定框架(钉在最后)apps/daemon/src/prompts/deck-framework.ts:355DECK_FRAMEWORK_DIRECTIVE
运行时定义结构体apps/daemon/src/runtimes/types.ts:126RuntimeAgentDefRuntimeContext
25 个适配器注册表apps/daemon/src/runtimes/registry.ts:31BASE_AGENT_DEFSgetAgentDef
单个适配器范例apps/daemon/src/runtimes/defs/claude.ts:16claudeAgentDefbuildArgs
CLI 探测apps/daemon/src/runtimes/detection.ts:444detectAgentsdetectAgentsStream
流式 JSON 解析(多家共用)apps/daemon/src/runtimes/json-event-stream.ts:918createJsonEventStreamHandler
Claude 专用流解析apps/daemon/src/runtimes/claude-stream.tscreateClaudeStreamHandler
向子进程注入 OD 工具apps/daemon/src/runtimes/mcp.ts:9buildLiveArtifactsMcpServersForAgent
子进程可调的工具端点白名单apps/daemon/src/tool-tokens.ts:22CHAT_TOOL_ENDPOINTSCHAT_TOOL_OPERATIONS
反向 MCP serverapps/daemon/src/mcp.ts:1778runMcpStdio
skill 扫描与解析apps/daemon/src/skills.ts:232listSkillsSKILL_ID_ALIASES
产物路径判定apps/daemon/src/runtimes/run-artifacts.ts:94isArtifactPathisDesignSystemFile
产物指纹快照 / diffapps/daemon/src/run-artifact-fs.ts:146snapshotProjectArtifactsdiffRunArtifactsHASH_MAX_BYTES
指纹基线的两个调用点apps/daemon/src/server.ts:10186apps/daemon/src/routes/runs.ts:2454runArtifactBaselines.remember / .take
产物 manifest 校验apps/daemon/src/artifacts/manifest.tsALLOWED_KINDSALLOWED_RENDERERS
占位符发布守卫apps/daemon/src/artifacts/publication-guard.ts:38UNRESOLVED_ARTIFACT_PLACEHOLDERS
anti-slop 检查apps/daemon/src/lint-artifact.ts:120lintArtifactrenderFindingsForAgent
评审编排apps/daemon/src/critique/orchestrator.ts:123runOrchestratorOrchestratorParams
评审收敛判定apps/daemon/src/critique/scoreboard.ts:51decideRoundcomputeComposite
灰度棘轮apps/daemon/src/critique/ratchet.ts:137evaluateRolloutConformanceDay
沙箱预览文档拼装apps/web/src/runtime/srcdoc.ts:381buildSrcdocbuildLazySrcdocTransport
预览 iframe 挂载点apps/web/src/components/DesignFilesPanel.tsx:2098sandbox="allow-scripts allow-downloads"
数据目录布局apps/daemon/src/server.ts:1165RUNTIME_DATA_DIRPROJECTS_DIRSKILL_ROOTS
本地 daemon 地址apps/daemon/src/daemon-url.ts:11DEFAULT_DAEMON_URL

资产目录(不是代码,是产品数据):

资产目录数量(as-of 本 commit)
设计技能skills/<id>/SKILL.md157
设计系统design-systems/<id>/DESIGN.md150
插件规范与样例plugins/spec/SPEC.mdplugins/_official/规范 + 官方样例