脚本即事实源:Script 编辑与版本对比
面向设计师的编辑与版本模型,一句话概括:Python 构建脚本是 IFC 的唯一对应表示,IFC 是脚本受限执行的产物。人和 AI 的修改都落在脚本上,平台只在版本之间和暂存步之间做对比。
design JSON 的定位
design JSON 只是 AI 起草阶段的草稿,不是模型的完整表示,不进版本,不参与 diff。与 IFC 一一对应的只有构建脚本。AI 生成复杂模型时可以先写一份 design JSON 辅助构思,见 AI Skill,但交付物永远是脚本。
工作流
plan / design 草稿(可选,AI 构思辅助,不落版本)
→ 构建脚本 v{n}.py(唯一事实源:PARAMS + build())
→ 沙箱执行 → IFC v{n}(派生物,可随脚本重建)三个核心概念
构建脚本
每个模型对应一个完整 Python 脚本,要遵循脚本契约:
- 头部
PARAMS = {...}是顶层字面量 dict,所有可调参数集中在这里。 - 构件的 GlobalId 由 key 确定性派生,并写入
Pset_AIIFC.designKey。同一脚本跑多少次 id 都不变,跨版本 diff 才能对齐。 - 审查可见的构件必须经契约工厂
script_lib.create_entity创建,工厂自动写确定性身份并记录调用点。禁止绕过工厂直接建实体。 - 需要 web 表单可编辑的参数必须是标量字面量或 PARAMS 引用,不能是任意表达式,否则定向改写会拒绝。
- 入口是
build(params, out_path),产物必须过ifcopenshell.validate。
暂存区
脚本编辑先进暂存区,最多 10 步,原子落盘,重启恢复,可撤销重做。暂存后三个去向:放弃,什么都不留;试运行,沙箱执行预览产物但不产生版本;保存,晋升为大版本。
大版本与回退
大版本是用户或 AI 主动保存的点。脚本和定位 map 全量保留,编号只增不改。产物文件只物化最新一份,历史版本 diff 或下载时从脚本重建,结果进缓存。回滚就是恢复某版脚本重新执行,脚本和产物永远一致,不存在改了产物没改脚本的分叉。
AI 下次介入时拿到的输入是当前脚本加两层 diff 摘要,因此它的修改是增量式的,不是推倒重写。
差异引擎
对比分两个粒度。大版本之间做完整对比;暂存链相邻步之间做轻量行内对比。
| 层 | 对象 | 受众 |
|---|---|---|
| 脚本文本 diff 加 PARAMS 键级变更 | v{n-1}.py 对 v{n}.py,以及暂存步间 | AI 的下一轮上下文,用户看脚本 diff 视图 |
| IFC 语义 diff,属性级 GlobalId 对齐 | 两个版本的 IFC | 用户在 Diff Viewer 看,见编辑工作流 |
| 外部上传模型 | 无脚本时按 GlobalId 做属性级对比 | 用户 |
前端界面
Design 面板解析脚本的 PARAMS 块自动生成参数表单,不执行脚本;下钻进脚本编辑器直接改代码。表单提交或脚本保存算暂存一步,保存版本产生大版本。版本对比面板可以选两个大版本看脚本 diff 和语义 diff,也可以看暂存步之间的小 diff。
执行安全
编辑服务在服务端沙箱里执行构建脚本,共享实现见沙箱执行环境。要点:bwrap 按需只读挂载加断网,没有 bwrap 直接拒绝执行;超时 60 秒,连孙进程一起终止;脚本可以经 PEP 723 声明依赖,由宿主机的 uv 构建隔离环境后沙箱内执行。
网络安全取决于部署:编辑服务自身无鉴权,务必只监听 127.0.0.1,外部一律经 Go server 代理。
API
经 Go server 代理的端点:
| 端点 | 语义 |
|---|---|
GET /api/v1/models/{id}/script | 当前脚本,暂存态或最近保存 |
PUT /api/v1/models/{id}/script | 暂存一次编辑。plain 模型首次暂存自动保留原件为 bootstrap.ifc |
GET /api/v1/models/{id}/script/params | 当前 PARAMS,ast 提取不执行 |
POST /api/v1/models/{id}/script/undo|redo|discard | 暂存导航与放弃 |
POST /api/v1/models/{id}/script/run | 沙箱试运行,无版本 |
POST /api/v1/models/{id}/script/save | 晋升大版本;有 bootstrap 时响应带 alignment |
GET /api/v1/models/{id}/scripts | 大版本列表 |
POST /api/v1/models/{id}/script/rollback | 恢复某版脚本并重跑 |
POST /api/v1/models/{id}/script/diff | 两个大版本的脚本 diff |
GET /api/v1/models/{id}/script/staging/diff | 暂存步间小 diff |
GET /api/v1/models/{id}/script/locate?guid= | guid 定位调用点;查不到返回 found:false,暂存与 map 分叉返回 stale:true |
仅直连可用的端点:
| 端点 | 语义 |
|---|---|
POST /models/{id}/script/edit-call | libcst 重写一个标量参数后沙箱验证,成功等同一次暂存;参数不合法返回 422 零副作用 |
定位链路
每次沙箱执行时,契约工厂记下每个构件的调用点,落成 map sidecar,按脚本哈希绑定发布。map 的行号只对生成它的那份脚本有效,暂存了新脚本但没运行时两者会分叉:edit-call 对分叉直接拒绝,locate 降级返回 stale:true,前端提示先运行脚本。
ScriptMap = dict[designKey, {"line": int, "col": int, "snippet": str,
"origin": "literal" | "params" | "traced"}]
# 发布信封(current.map.json / v{n}.map.json):
# {"scriptHash": sha256(脚本全文), "map": ScriptMap}origin 决定前端改写策略,见编辑工作流。
bootstrap:上传 IFC 转脚本
plain 模型经 AI 复现转为 script-backed:AI 通过 MCP 读上传的 IFC,写复现脚本,首次暂存时平台保留原件为 bootstrap.ifc,验证后保存 v1。首次保存响应里的 alignment 计数是原件和生成结果的差异摘要,用它检验复现质量;对齐计算失败不影响保存本身。
与直改链路的关系
直改链路已退役,脚本生成的模型全部走本页模型:选中、定位、改写、沙箱、暂存、大版本。外部上传的 IFC 无编辑入口,仅查看和审查,对比走属性级语义 diff。