# Open Design 源码解剖

> 单对象深度档案：一个数据契约如何调度 27 个 agent CLI。2026-09-16 基于 main 分支源码精读（commit 当日）。源码镜像：`工具/open-design-src/mirror/`（568KB，50 文件，sparse 方式获取）。

## 0. TL;DR

Open Design（nexu-io/open-design，96.4k stars，Apache 2.0，TypeScript）是开源的 Claude Design 替代：本地优先桌面应用，把用户已有的 coding agent CLI 当作「设计引擎」调度。它对多 agent 调度问题给出的答案是一份**纯数据契约** `RuntimeAgentDef`：每个 agent CLI 对应一个对象字面量，通用引擎统一完成探测、启动、调用、流解析。接入新 CLI = 新增一个文件。本文解剖这份契约的设计、三个代表性 def 的工程深描、引擎侧的现实坑，以及它留给外部编排器的官方接口。

## 1. 项目概况

| 项 | 值 |
|---|---|
| Stars / Forks | 96,403 / 11,197（2026-09-16，5 个月达成） |
| 许可 / 语言 | Apache-2.0 / TypeScript |
| 形态 | macOS / Windows 桌面应用 + OpenDesign Cloud（充值制模型服务） |
| 定位 | 开源 Claude Design 替代：原型 / deck / 图片 / 文档 / 视频的 agent 工作室 |
| 引擎策略 | 27 个 agent CLI 原生调度 + BYOK proxy 兜底，全部走用户自己的订阅与 key |

核心论点（agent-adapters.md 原文）：代码 agent 领域已收敛出强势实现，**重新实现一个，不如和所有它们对话**。OD 委托整个 agent loop（模型调用、工具使用、上下文管理、权限、恢复、取消）给既有 CLI，自己的 daemon 只做：探测它、喂给它 skill + prompt + 工作目录、把输出流回 Web UI。

## 2. 架构决策：adapter 是数据规约，不是类

传统写法是每个 agent 一个 `class implements AgentRunner { run() cancel() }`。OD 反其道：`RuntimeAgentDef` 是一个 object literal，声明「怎么和这个 CLI 说话」；约 30 个字段，全部是数据或纯函数 arg-builder。探测、启动、调用、取消、流解析全部由通用引擎依据声明执行（`detection.ts` / `launch.ts` / `invocation.ts` / `*-stream.ts`）。

收益：接入新 CLI = 写一个 def 文件 + 注册进 `BASE_AGENT_DEFS` 数组，引擎零改动。只有全新的 wire format 才需要新增一个 `*-stream.ts` 解析器。注册表在 boot 时对重复 id 抛错（fail fast）。

注册表还区分两个清单：`SHIPPED_AGENT_DEFS`（我们发布什么）与 `AGENT_DEFS`（这台机器能跑什么 = shipped + 用户本地 profiles）。注释原话：「a different question from what we ship, and the wrong list for anything that must hold everywhere」——工程品味的细节。

## 3. RuntimeAgentDef 契约精读（`runtimes/types.ts`，~600 行）

### 3.1 识别与探测

- `bin` + `fallbackBins`：主探测目标 + 兼容 fork（claude 的 fallback 是 `openclaude`，argv 兼容的 drop-in fork 自动被识别）。
- `versionPolicy`：`supportedVersions` 精确清单 + `supportedVersionPattern` 正则整条 release line + `requireVersion: true` fail-closed。深因：agent CLI 以 RC 流水线发版，速度远快于 OD 自己的发版，钉死单个版本号等于每个照官方文档装的用户一周后都收到「未测试版本」警告。
- `capabilityFlags`：调 `<bin> --help` 后做子串扫描，flag → capability key 映射。注意 claude 的 `--add-dir` 只在 `-p` 子命令 help 里出现，所以要 `claude -p --help`（issue #430）。
- `hiddenCapabilityFlags`：处理 CLI 存在但 help 里隐藏的 flag（如 `--thinking-display`）。探测法极聪明：用**不可能被接受的值**去调每个 flag——认识该 flag 的 build 拒绝 value，不认识的 build 拒绝 option，两者都在模型调用前失败，探测零成本。

### 3.2 调用与 prompt 投递

- `buildArgs(prompt, imagePaths, extraDirs, options, context) -> argv`：唯一的「代码」，也是纯函数。
- prompt 投递三通道：`promptViaStdin`（规避 Linux E2BIG：argv 单条 ~128KB 上限；Windows ENAMETOOLONG：CreateProcess ~32KB，.cmd shim 更是 ~8KB）、`promptViaFile`（daemon 建临时文件，用后即删）、argv 直传（不推荐，antigravity 被迫用）。
- `promptInputFormat: 'stream-json'`：stdin 写入单条 JSONL user 消息且**不关闭**，daemon 可在 turn 中途继续注入后续 user 消息，避免重复 spawn。

### 3.3 流回传：8 种 wire format 兜住 27 个 CLI

| streamFormat | runtime ids |
|---|---|
| `claude-stream-json` | claude, amp, codebuddy |
| `json-event-stream` | codex, cursor-agent, opencode, mimo, byok-opencode |
| `copilot-stream-json` | copilot |
| `qoder-stream-json` | qoder |
| `acp-json-rpc` | amr(Vela), devin, hermes, kimi, kiro, kilo, reasonix, trae-cli, vibe |
| `pi-rpc` | pi |
| `dsh-profile-jsonl` | deepseek-harness |
| `plain` | aider, antigravity, atomcode, deepseek, grok-build, qwen |

两个趋势信号：**ACP（Agent Client Protocol，JSON-RPC over stdio）一个协议占 9 席**，agent 侧接口正在标准化，是新增 runtime 的推荐路径（docs/new-agent-runtime-acp.md 专文）；Gemini 本地生成 runtime 已退役（连 96k star 的项目都维护不动某些 CLI，适配是有现实成本的）。

解析器 emit 的统一事件集：thinking / tool-call / tool-result / text-delta / file-write / error / done。事件集归解析器管，不归 def 管。

### 3.4 会话恢复：三种风格 + 一种放弃

1. **specify-style**（`resumesSessionViaCli` + `newSessionId`）：daemon 铸造 UUID，CLI 被告知用它。Claude：`--session-id <uuid>` 新建、`--resume <id>` 续聊。daemon 持有的 id 即它生成的 id。
2. **capture-style**（`capturesSessionIdFromStream`）：CLI 自己生成会话 id 并在流里上报（codex 的 `thread.started.thread_id`），daemon 从解析流捕获并持久化**它**作为 resume handle，`newSessionId` 不传给 CLI。
3. **ACP load**（`resumesSessionViaAcpLoad`）：通过 ACP `session/load` + `getDurableSessionId()`，resume 失败映射到 reseed 路径。
4. **plain 无状态**：没有上游会话存储的 CLI（qwen、deepseek、antigravity），每轮 spawn 全量重灌 daemon 渲染的 transcript（`## user` / `## assistant` 块），靠对话文本本身延续记忆。这是兼容性的下限兜底。

### 3.5 能力声明与集成

- `externalMcpInjection`（外部 MCP 服务器注入四策略）：`claude-mcp-json`（写 `.mcp.json` 进项目 cwd，Claude Code 自动加载）/ `acp-merge`（并进 ACP launch 描述符的 mcpServers）/ `opencode-env-content`（序列化进 `OPENCODE_CONFIG_CONTENT` 环境变量）/ `mimo-env-content`。没有原生 MCP 通道的 agent（codex、cursor-agent、copilot、qoder、pi）留空，设置 UI 显式提示「请在 agent 自己的配置里设置」，替代 issue #2142 之前的静默失败。
- `authProbe`：声明式认证探测（如 `claude auth status`、`cursor-agent status`），副作用免费。没声明的 agent 不主动探测，auth 状态只能从真实聊天失败的错误文本反推。
- 模型发现三通道：静态 `fallbackModels`、`listModels`（跑一条命令解析 stdout）、`fetchModels`（异步，如 claude 从 mmd/MMS 路由拉取 proxy 兼容模型）。
- 超时两把尺：`inactivityTimeoutMs`（不活跃看门狗，Copilot deck 生成的思考期会长时间静默，需要更长）与 `firstOutputTimeoutMs`（宣布等待模型输出到首个实质事件的硬顶，心跳不续命）。

## 4. 三个代表性 def 深描

### 4.1 claude.ts —— 参考实现

`claude -p --input-format stream-json --output-format stream-json --verbose`。要点：

- 能力全部按探测结果 gate（`--include-partial-messages` 旧版不认识会 exit 1 杀死会话，必须先探测再加）。
- `--thinking-display summarized`：headless 下 extended thinking 文本被扣住（`redact-thinking` beta 仍然在 API 请求上，每个 `thinking_delta` 的 `thinking` 都是空串），显式要 summarized 才能看见推理。
- `--permission-mode bypassPermissions`：daemon 管理的 print 模式没有交互审批通道。
- `--agents <json>`：把 daemon 绑定的 build package 塞进 Claude Code 原生 subagent 系统，prompt 写死「只执行父 agent 给的任务，不许再 spawn subagent」。
- `--add-dir` 传额外目录（旧版本也 gate，对齐对称性）。

### 4.2 deepseek-harness.ts —— 一等公民 runtime

`dsh --profile open-design --stdio`，走 `dsh-profile-jsonl` 专用协议（结构化流、模型发现、取消、会话恢复俱全）。`resumesSessionViaProfileStdio: true` + `capturesSessionIdFromStream: true`。模型动态发现：`--models` 返回目录解析成 `provider/id` + reasoning options。

版本策略注释堪称运维小论文：dsh 的 RC 相互 peer-require，安装器给出的版本随上游漂移；把 pattern 收窄到 `0.1.0-rc.N` 两天后上游发了 `0.1.1-rc.1`，所有用户又见警告。最终方案是 per-patch-line 的正则，且 prerelease 接受范围永远不能超过 `packages/dsh-runtime` peer 依赖能装的范围（否则警告消失但伴随依赖解析失败，比警告更糟且无声）。e2e 测试把两处绑定，一次上游新 patch line 需要两处同改或同不改。

### 4.3 antigravity.ts —— 与不合作 CLI 搏斗的编年史

一行流格式 `plain`，def 文件 11KB，其中大部分是「驯服 agy」的机制代码。五连坑：

1. **无模型 flag**（agy v1.0.3，上游 issue #35）：TUI 的 Switch-Model 会写 `~/.gemini/antigravity-cli/settings.json`，而 `agy -p` 每次启动读它。解法：daemon 在 spawn 前直接写 settings.json 选模型。8 个模型 label 硬编码（镜像 agy TUI consumer-tier 列表，2026-05-28 时点），`supportsCustomModel: false`，因为 label 是服务端 enum，认不出的 id 会变成静默缓存 miss + 空输出，对用户表现为「空响应」。
2. **全局文件竞态**：settings.json 是进程全局的，并发 run A 写模型 A、B 写模型 B，A 的 agy 可能跑在 B 的模型上。解法：promise 锁链串行化所有非默认模型的 spawn；release 条件是**轮询 agy 的 --log-file 直到出现 `Propagating selected model override to backend: label="X"`**（model_config_manager.go 的日志 = settings 已被读走）。注释强调：poll 返回 false 意味着「放弃轮询」，绝不意味着「agy 一定没读」，锁只能在真观察到信号或子进程退出时释放。
3. **print mode 静默失败**：认证缺失与配额耗尽在 stdout/stderr 全部无输出（PR #3157 实证），唯一区分手段是 `--log-file` 事后 grep。flag 顺序 load-bearing：`--log-file` 必须在 `-p` 之前。
4. **stdin 投递被上游改坏**：旧版 `agy -p -` 读 stdin，1.1.13 把 `-` 当字面 prompt 忽略 stdin（issue #7161），只能把整个 prompt 塞进 argv。
5. **`-c` resume flag 故意不用**：实测会激活 agy 内部 agentic loop（多步模型重试、工具调用、工具报错回退缓存响应），OD 的 system prompt OVERRIDE 无法约束，turn 1 的工具重试路径返回缓存的 form 后，turn 2 字节级复现同一 form。解法：当无状态 adapter 用，每轮全量 transcript，且上一轮 assistant 输出要清洗 `<question-form>` 标记（防弱模型如 Gemini 3.5 Flash 模式匹配重发）。

## 5. 引擎侧的工程现实

- **detection**：所有 def 并行探测，启动预热；每个 probe 故障隔离（一个坏的可执行文件不清空整个 picker）；无持久化缓存，每次要新鲜数据就重新探。resolved 路径与实际 spawn 路径强制同源（probe 到的是 shim、跑的是另一个二进制 = 假阳性）。
- **launch 的 PATH 深坑**（launch.ts）：GUI 启动的 daemon PATH 极简，Pi 这类 `#!/usr/bin/env bun` 脚本 resolve 成功但 spawn exit 127。解法：Node bin 目录置于 PATH 最前（Windows npm .cmd shim 找裸 node），用户工具链目录（Homebrew、~/.bun/bin、版本管理器）追加末尾。Windows 的 `Path` vs `PATH` 用大小写不敏感查找后**就地更新**，避免重复 key。Codex 特殊：npm wrapper 脚本在 GUI 环境可能起不来，向上遍历目录找 `@openai/codex-<platform>-<arch>/vendor/<target-triple>/codex` 原生二进制；嗅探 shebang 只读前 64KB（原生 Codex.app 是 ~190MB，不能整读）。
- **invocation 的中性 cwd**（invocation.ts）：所有探测类调用（version/help/models/auth）默认 cwd = OS 临时目录而非 daemon 进程 cwd。事故：OpenCode 是 bun 写的，启动时在 cwd 跑 `bun install` 装本地插件，daemon 若从 pnpm workspace 启动，一次 `opencode models` 探测会在仓库里丢下 `bun.lock` + `node_modules/.bun`，毁掉 pnpm store 打断 dev server。`killSignal: 'SIGKILL'`：execFile 的 timeout 只发信号，promise 要等子进程真正退出才 settle，trap SIGTERM 的 CLI 会让探测永远 pending；只读探测无物可 flush，直接 SIGKILL。

## 6. 编排工作区契约：被上游编排的官方接口

`docs/orchestrator-workspaces.md` 定义了「外部编排器准备的 scratch workspace」契约，存储与权属分离：

- **Storage**：`od-owned`（OD 自己的项目目录）/ `folder-backed`（外部文件系统根）。
- **Provenance**：`user-local`（用户选的本地文件夹，原地编辑）/ `orchestrator-scratch`（外部编排器准备的一次性工作区，`writeback: "external"`）。

边界刻意窄：OD 读写给定 workspace、产出可检视的设计输出、报告 provenance；源码 checkout、PR 创建、部署、写回策略全部留在 OD 之外。稳定输出边界是 **result package**：run 身份与终态、workspace 元数据、文件与 artifact manifest、事件日志位置、错误/退出码/信号/取消状态、可选 diff。

这意味着任何上游编排系统（如自建的多 agent 流水线）可以把 OD 当作一个「设计工人」接入：准备 scratch 目录 → 调用 → 收 result package → 自己决定写回与否。被编排的接口是官方一等公民。

## 7. 与 Trae Work Design 的对比

| 维度 | Open Design | TRAE Work Design（2026-06-24 发布） |
|---|---|---|
| 形态 | 独立桌面应用，本地优先 | Trae IDE/Work 内置模式，云工作台 |
| 开源 | Apache-2.0 全开源，可自托管解剖 | 闭源 |
| 引擎 | 27 个 agent CLI 任选（BYOK，用户自有订阅/key），可混合 | 字节自家模型栈，单引擎 |
| 品牌约束 | `DESIGN.md` 文件契约（git 可版本管理，任何 agent 可读） | 内置品牌模板 + Figma 自动解析，AI 结构化归纳样式 |
| 产出 | 单页 artifact：真实 CSS/字体/组件，导出 HTML/PDF/PPTX/MP4 | 画布原型 + 可手改 + 直接产出代码（打通生产流程） |
| 编辑体验 | artifact 流（对话 + 文件 + iframe 预览） | 画布交互（生成后在画布上手改） |
| 扩展 | 30+ skills 文件系统 + 插件 + MCP + Agent Skills 标准 | Trae skill 生态 |
| 成本 | 免费 + 用户已有 agent 订阅 | 免费额度（评测口径） |

结论：两者重叠面在设计生成，分歧在哲学。Trae Design 的优势是一体化与画布体验（需求→设计→代码一站式、上手快）；OD 的优势是引擎自由、契约文件化、产出物是真实工程文件。OD 同时是研究多 agent 调度架构的最佳开源样本。二者不互斥：把 OD 的 `DESIGN.md` 思路搬进任何工具（含 Trae）都能提升品牌一致性，因为「设计系统是文件契约而不是人脑审美」。

## 8. 可复用的设计模式清单

1. **配置与引擎分离**：能力用数据声明，行为归通用引擎；新集成 = 新数据文件。适用于任何「多后端统一调度」场景。
2. **能力探测三件套**：help 子串扫描 + hidden flag 值拒绝法 + authProbe 声明式探测，全部零副作用、零成本。
3. **会话恢复的风格学**：谁铸造 id（调用方 vs 被调方）决定 resume handle 的归属；无法 resume 时降级为全量 transcript 重灌。
4. **「确认已读」锁**：共享配置文件的写-读竞态，释放锁的信号必须来自消费方真实读取的证据（日志行），而不是写入方的时间猜测。
5. **探测与执行同源**：probe 到的路径必须就是将要 spawn 的路径，否则可用性判断是假的。
6. **中性 cwd 原则**：探测类子进程永远不继承宿主 cwd，防其副作用污染宿主项目。
7. **版本策略与依赖范围联动**：接受的上游版本不能超过伴随组件实际能装的范围内，否则警告消失、故障无声。
8. **result package 边界**：被编排组件只描述自己产出了什么，不替编排者执行写回；权属（provenance）与存储（storage）正交声明。

## 9. 文件地图（源码索引）

```
apps/daemon/src/
├── runtimes/
│   ├── types.ts          # RuntimeAgentDef 契约（约 600 行，含大量决策注释）
│   ├── registry.ts       # 27 个 shipped def + 本地 profiles，boot 去重
│   ├── detection.ts      # 并行探测，故障隔离
│   ├── launch.ts         # 可执行文件解析 + PATH 修复 + Codex 原生二进制
│   ├── invocation.ts     # 探测类调用（中性 cwd + SIGKILL）
│   ├── capabilities.ts   # 运行期能力注册表
│   ├── claude-stream.ts  # claude-stream-json 解析器（52KB）
│   ├── plain-stream.ts   # plain 解析器
│   └── defs/             # 27 个 agent def + shared.ts
│       ├── claude.ts         # 参考实现
│       ├── deepseek-harness.ts  # profile-stdio 一等公民
│       ├── antigravity.ts    # 不合作 CLI 驯化史
│       └── ...
├── agent-protocol/
│   ├── acp/              # ACP 协议实现（stdio-mcp 版本 withholding 等）
│   └── dsh-profile/      # DeepSeek Harness profile 协议
docs/
├── agent-adapters.md     # adapter 契约总文档（41KB，必读）
├── new-agent-runtime-acp.md  # 新增 ACP runtime 指南
├── orchestrator-workspaces.md # 外部编排器 workspace 契约
└── architecture.md
```

## 10. 更新记录

- 2026-08-14 收录（skills线索.md / 网站推荐.md，85k stars）
- 2026-09-16 源码解剖 + 条目更新（96.4k stars；DeepSeek Harness 一等公民支持；Gemini 本地 runtime 退役；本文）
