内容
<p align="center">
<img src="assets/icon-flat.svg" alt="partsmith" width="112" height="112">
</p>
# partsmith
**用于 AI 代理的参数化 CAD 服务器。** partsmith 在一个小型 REST API 和模型上下文协议(MCP)端点的基础上封装了 [build123d](https://github.com/gumyr/build123d) (OpenCascade),因此像 Claude 这样的 LLM 客户端可以通过编写 build123d Python 代码来设计真实的、可打印的 3D 零件,然后在单个 HTTP 界面上渲染、测量、验证、版本化和导出它们。
它是一个无头、单文件、简单的设计,旨在与身份验证反向代理一起使用,运行在小型 Linux 主机上。容器本身不执行任何身份验证;周边是安全边界(请参见 [`SECURITY.md`](SECURITY.md))。
## 它能做什么
- **使用 build123d Python(`result = Box(30, 20, 10)`)编写零件**,并自动注入一个包含常用 3D 打印模式(螺丝孔、槽、圆角、安装模式)的库。
- **查看设计内部**:3D 阴影视图、2D 正交投影视图、带有尺寸的多视图复合图和 **任何平面上的横截面** - 所有这些都作为内联 PNG 返回,无需离开聊天界面。
- **验证可打印性**:在打开切片器之前进行防水/流形/壁厚检查。
- **将设计** 持久化并版本化到磁盘:每次保存都是一个新版本,具有一个 `diff`,该 `diff` 报告源代码更改和任何两个版本之间的几何增量(体积、表面积、边界框)。
- **导出到 STL / STEP / 3MF**。
所有操作都可以通过 REST 和 MCP 相同的方式进行;两者共享同一个进程内引擎,因此通过一种协议编写的模型可以被另一种协议访问。
## 安装
### 选项 A — ZimaOS 自定义安装(推荐用于 homelab)
partsmith 附带一个 `docker-compose.yml` 文件,其中包含 [`JLay2026/zimaboard-services`](https://github.com/JLay2026/zimaboard-services/tree/main/partsmith) 中的 `x-casaos` 元数据。将它粘贴到 ZimaOS 仪表板的自定义安装中,填写表单,点击安装。有关完整的操作指南(包括一次性绑定挂载 chown 步骤),请参见 [`docs/partsmith-setup.md`](https://github.com/JLay2026/zimaboard-services/blob/main/docs/partsmith-setup.md)。
### 选项 B — 从发布镜像运行 `docker run`
镜像发布到 GHCR 上的每个版本:
```bash
# 预创建绑定挂载目录为 uid 1000(容器的用户)。
# 跳过此步骤将导致每个导出/渲染返回 500 禁止访问。
sudo mkdir -p /srv/partsmith/workspace /srv/partsmith/renders
sudo chown -R 1000:1000 /srv/partsmith/workspace /srv/partsmith/renders
docker run -d --name partsmith \
-p 127.0.0.1:8123:8123 \
-v /srv/partsmith/workspace:/workspace \
-v /srv/partsmith/renders:/renders \
ghcr.io/jlay2026/partsmith:latest
curl http://127.0.0.1:8123/health
# {"status":"ok","version":"0.3.1"}
```
使用 `:0.3.1` 而不是 `:latest` 来固定特定版本。可用标签:<https://github.com/JLay2026/partsmith/pkgs/container/partsmith>
### 选项 C — 本地开发(从源代码构建)
```bash
git clone https://github.com/JLay2026/partsmith
cd partsmith
pip install -e .
python -m uvicorn src.server:app --host 0.0.0.0 --port 8123
curl http://localhost:8123/health
```
或者在本地构建容器:`docker compose up -d --build`(预先创建并将 `workspace/` 和 `renders/` 作为 uid 1000)。
## 连接 MCP 客户端
partsmith 在 `/mcp/` 处公开了一个可流式传输的 HTTP MCP 服务器。基于 URL 的 MCP 客户端(Cowork 的托管 MCP UI、Claude Code 等)直接注册它 - 不需要 shim 进程。
| 字段 | 值 |
|---|---|
| URL | `<your-base-url>/mcp/`(需要尾部斜杠) |
| 传输 | `streamable-http` |
| 身份验证 | 反向代理强制执行的操作(通过客户端的 Headers 字段发送头部) |
连接后,客户端会看到完整的 `partsmith_*` 工具表面(截至 v0.3.1 版本共有 17 个工具)。
## build123d 助手
每次 `create_model` / `save_design` 调用都会在执行命名空间中加载 `from build123d import *` 和 `import numpy as np`,并额外加载七个常用模式的助手:
| 助手 | 用途 |
|---|---|
| `through_hole(diameter, depth)` | 普通圆柱孔 |
| `screw_hole(diameter, depth, countersink=True, ...)` | 机螺钉孔,M 系列默认值 |
| `hex_hole(across_flats, depth)` | 螺母袋(M3=5.5,M4=7,M5=8,M6=10 mm) |
| `slot(length, width, depth)` | 用于可调节安装的体育场形槽 |
| `chamfer_edges(part, radius, edges='all'\|'top'\|'bottom'\|'side')` | 带边缘选择的倒角 |
| `fillet_top_edges(part, radius)` | 圆润所有顶部面边缘 |
| `screw_pattern(positions, hole_func)` | 在每个(x,y)处应用孔 |
```python
# 壁挂件:4 个沉头螺丝孔,顶部边缘圆角
body = Box(100, 50, 10)
holes = screw_pattern(
[(10, 10), (90, 10), (10, 40), (90, 40)],
lambda: screw_hole(3.2, 10),
).moved(Location((0, 0, 10)))
result = fillet_top_edges(body - holes, radius=1.5)
```
## 设计存储 + 版本控制
模型存储在内存中,重启容器后会被清除。**设计** 存储在磁盘上的 `{workspace}/designs/{name}/` 目录下,重启容器后仍然保留,每个版本都有一个 `vN.py` 和 `vN.json` 文件对:
```
designs/bracket/
v1.py v1.json
v2.py v2.json
v3.py v3.json
```
- `save_design(name, code)` 会追加下一个版本(或传递 `version=N` 来覆盖一个插槽)。
- `load_design(name)` 加载最新的(或特定的)版本并将其注册为模型。
- `diff_designs(name, v1, v2)` 返回一个统一的源代码差异以及体积/表面积/边界框的增量 - “v3 是否比 v2 在我关心的方面确实更好?”一眼就能回答。
## REST 工具表面
| 端点 | 方法 | 用途 | 启用版本 |
|---|---|---|---|
| `/health` | GET | `{"status":"ok","version":"X"}` | v0.1 |
| `/model/create` | POST | 执行 build123d 代码,存储为命名模型,返回几何图形 + base64 预览 | v0.1 |
| `/model/modify` | POST | 重新执行现有名称 | v0.1 |
| `/model/list` | GET | 列出加载的模型 | v0.1 |
| `/model/{name}/measure` | GET | 边界框,体积,面积,拓扑计数 | v0.1 |
| `/render/3d` | POST | 3D 阴影视图(Lambertian),base64 PNG | v0.1 |
| `/render/2d` | POST | 2D 正交视图 + 可选尺寸 | v0.1 |
| `/render/multiview` | POST | 合成:正面 + 右侧 + 顶部 + 等距 | v0.1 |
| `/render/section` | POST | **2D 横截面** 在 XY/XZ/YZ 上,偏移 | v0.2.6 |
| `/render/all` | POST | 将每个标准视图渲染到磁盘 | v0.1 |
| `/export` | POST | 以二进制格式导出 STL / STEP / 3MF | v0.1 |
| `/analyze/printability` | POST | 防水 / 流形 / 壁厚检查 | v0.1 |
| `/workspace/{filename}` | GET | 流式传输先前导出的文件(MCP 大文件后备) | v0.2 |
| `/design/save` | POST | 保存设计版本(也作为模型执行) | v0.2.4 |
| `/design/list` | GET | 列出已保存的设计(每个最新版本) | v0.2.4 |
| `/design/{name}` | GET | 加载源代码 + 元数据(`?version=N`) | v0.2.4 |
| `/design/{name}` | DELETE | 删除版本,或全部删除(`?version=N`) | v0.2.4 |
| `/design/{name}/load` | POST | 加载 + 执行设计为模型(`?version=N`) | v0.2.4 |
| `/design/{name}/versions` | GET | 列出版本号 | v0.2.7 |
| `/design/{name}/diff` | GET | 差异两个版本(`?v1=&v2=`) | v0.2.7 |
## MCP 工具
所有前缀均为 `partsmith_` 以避免多服务器设置中的冲突。
| 工具 | 用途 | 启用版本 |
|---|---|---|
| `partsmith_health` | 状态 + 版本 + 传输 | v0.2 |
| `partsmith_create_model` | 执行 build123d 代码,注册为模型,返回几何图形 + 预览 | v0.2 |
| `partsmith_modify_model` | 针对现有名称重新执行 | v0.2 |
| `partsmith_list_models` | 列出加载的模型 | v0.2 |
| `partsmith_measure_model` | 边界框,体积,面积,计数 | v0.2 |
| `partsmith_render_3d` | 3D 阴影视图 | v0.2 |
| `partsmith_render_2d` | 2D 正交投影 | v0.2 |
| `partsmith_render_multiview` | 2×2 复合图 | v0.2 |
| `partsmith_render_section` | **横截面** 通过平面 | v0.2.6 |
| `partsmith_export` | 导出 STL / STEP / 3MF | v0.2 |
| `partsmith_analyze_printability` | 防水 / 流形 / 壁厚检查 | v0.2 |
| `partsmith_save_design` | 保存设计版本 | v0.2.4 |
| `partsmith_load_design` | 加载 + 执行已保存的设计 | v0.2.4 |
| `partsmith_list_designs` | 列出已保存的设计 | v0.2.4 |
| `partsmith_delete_design` | 删除设计 / 版本 | v0.2.4 |
| `partsmith_list_versions` | 列出设计版本 | v0.2.7 |
| `partsmith_diff_designs` | 差异两个版本(源代码 + 几何增量) | v0.2.7 |
### 文件交接:内联 vs URL
返回文件的工具(`partsmith_render_*`,`partsmith_export`)在 ≤ `PARTSMITH_INLINE_MAX_BYTES`(默认为 **8 MiB**)时,将字节内联为 base64。当有效负载较大时,持久化到工作区并返回一个 `url_path`,客户端通过 `GET /workspace/<filename>` 使用相同的身份验证标头获取。
| 响应形状 | 何时 | 客户端操作 |
|---|---|---|
| `{"inline": true, "data_b64": "..."}` | ≤ 8 MiB | base64 解码并使用 |
| `{"inline": false, "url_path": "/workspace/foo.stl"}` | > 8 MiB | GET base_url + url_path |
## 配置
| 环境变量 | 默认值 | 用途 |
|---|---|---|
| `PARTSMITH_WORKSPACE` | `/workspace` | 导出 + 保存设计的位置 |
| `PARTSMITH_RENDERS` | `/renders` | 渲染的 PNG 缓存位置 |
| `PARTSMITH_HOST` | `0.0.0.0` | 服务器绑定地址 |
| `PARTSMITH_PORT` | `8123` | 服务器绑定端口 |
| `PARTSMITH_MAX_BODY_BYTES` | `1048576`(1 MiB) | 拒绝大于此值的 REST 请求。 `/mcp` 豁免。 |
| `PARTSMITH_INLINE_MAX_BYTES` | `8388608`(8 MiB) | 大于此值的 MCP 文件有效负载返回 `url_path` 而不是内联。 |
## 操作说明
- **模型存储在内存中;设计存储在磁盘上。** 容器重启会清除模型注册表(LRU 缓存,限制 32 个),但绝不会删除设计存储。使用 `save_design` 持久化您想要保留的任何内容。
- **跨协议状态共享** - REST 和 MCP 包装相同的 `CADEngine`。
- **默认单个 uvicorn 工作进程** - 并发渲染序列化。对于单个用户来说足够;为并行处理添加 `--workers N` 到 `entrypoint.sh` 以增加(~+500 MB 驻留内存,每个工作进程)。
- **绑定挂载权限** - 容器以 uid 1000 运行;`workspace/` 和 `renders/` 必须可被该 uid 写入。 v0.1.3+ 以 500 错误显示权限失败,并指定确切的路径 + 预期 uid。
## 测试
两个 GitHub Actions 工作流程在每次 PR + 推送到 main 时运行:
- **`ci.yml`** - `ruff` 校验 + 一个轻量级 `pytest` 作业(`tests/test_versioning.py`,纯标准库,~13 秒)。
- **`integration.yml`** - 构建容器,启动它,并在 `/health` 和 `/mcp/` 上运行 `tests/integration/`。单个 MCP 握手测试防止 v0.2.x 部署错误类(生命周期崩溃,双前缀 URL,DNS 重新绑定 421,状态长期轮询挂起)。在本地运行:
```bash
docker run -d --rm -p 127.0.0.1:8123:8123 --name p-test \
ghcr.io/jlay2026/partsmith:latest
PARTSMITH_URL=http://127.0.0.1:8123 pytest tests/integration/ -v
docker stop p-test
```
## 部署
生产部署位于 [**JLay2026/zimaboard-services**](https://github.com/JLay2026/zimaboard-services)(`partsmith/docker-compose.yml` + `docs/partsmith-setup.md`),由 Caddy 以 LAN/Tailscale 仅限匹配器前端。partsmith 在 `https://cad.<your-internal-domain>/` 处可访问,其 MCP 端点在 `/mcp/` 处。
## 项目文档
- [`ROADMAP.md`](ROADMAP.md) - 主题,状态,发布顺序
- [`CHANGELOG.md`](CHANGELOG.md) - 每个版本的更改记录
- [`SECURITY.md`](SECURITY.md) - 威胁模型(“周界是边界”)
- [`NOTICE.md`](NOTICE.md) - 致先前工作的致谢
## 为什么存在
开始时已有几个现有的 build123d-MCP 包装器。由于其中最突出的项目的主文件中有一个 `SyntaxError` 存在了四个月,没有人注意到,(b)包装的表面很小(~1,400 LOC),因此拥有它完全是自己的成本低于维护一个修补后的分支。该项目遵循“小而精”原则 - 每个功能都必须证明其行数是合理的,并且新的模式只有在实际设计需要时才会添加,而不是凭空臆测。
参见 [`NOTICE.md`](NOTICE.md) 以获取对先前工作的致谢。
## 许可证
MIT - 请参见 [`LICENSE`](LICENSE)。