存储与前端对接
本文面向两类集成方:想把存储层接到自己数据库的,以及自研前端要接入的。
存储接口契约
签名与路径以代码为准:server/internal/store/store.go、server/internal/{issue,change,override}/、server/cmd/server/main.go。
两种存储实现
文件存储是默认实现,零依赖:所有状态落在数据目录里,JSON 文件加原子写。
PostgreSQL 可选:设置 VIEWER_PG_DSN 即切换。三张表由各自的 pgstore.go 在构造时自动建立。
两处不对称要知道:
- 模型注册表不进 PG。模型元数据和上传文件始终是文件存储,PG 只承接 issue、change、override 三个网关侧数据。
- Issue 截图不进 PG。PG 模式下 PNG 仍落盘,库里只存相对路径。
数据目录布局
Go server 和两个 Python 服务必须共享同一个数据目录,配错会 404 或改错文件。
{VIEWER_DATA_DIR}/
├── uploads/
│ └── {id}.ifc|.dxf # 当前模型态(script/run 或 save 时原子替换)
└── models/{id}/
├── model.json # 模型状态:name/size/status/error(Go store,原子写)
├── model.xkt # converter 产物(GET /v1/models/{id}/model.xkt 服务)
├── metadata.json # converter 产物(xeokit 元模型)
├── issues.json # issue 列表(仅文件存储模式)
├── issues/{issueId}.png # issue 截图(文件/PG 模式均在此)
├── changes.json # 修改记录(仅文件存储模式)
├── overrides.json # 属性 override(仅文件存储模式)
├── edit-history.json # services/ifc 持久化编辑历史
├── pending.json # script-run 回放簿记(内部)
├── script_staging.json # 暂存脚本链
├── bootstrap.ifc # 首次暂存脚本时保留的上传原件
├── current.map.json # 当前 ScriptMap 发布信封
├── scripts/ # 大版本脚本快照:v{n}.py + v{n}.map.json 全留
├── versions/ # 大版本产物:v{n}.ifc|.dxf 只留最新
└── ifc_cache/ # diff/下载时按需重建的历史版本 + .map.json sidecar写入方分两组:uploads、model.json、XKT、metadata 和 issues/changes/overrides 由 Go server 与 converter 写;其余由 Python 服务直接读写,不经过 Go store。Go 和 Python 共享的是目录契约,不是同一个 store 实例。
第三方整合路径
按耦合从高到低三条路:
实现 Go store 接口,用自己的数据库承接网关侧状态。三个接口分别在
issue.go、change.go、override.go:接口 方法 职责 issue.StoreList/Create/Update/Delete/DeleteModel/SaveScreenshotissue 增删改查、按模型清理、截图落盘 change.StoreList/Append/DeleteModel修改记录追加与按模型清理 override.StoreGetAll/Set/DeleteModel属性 override 读写与按模型清理 注意模型注册表
store.Store是具体类型不是接口,替换它要改 server 内部装配。复用本仓 PG schema:把三个
pgstore.go里的建表语句跑到自己的 PG 实例,设VIEWER_PG_DSN指向它即可。只做文件级对接:直接读写数据目录,耦合最低,适合只读消费 XKT、metadata 或旁路分析。写入方必须遵守同样的并发纪律:先写临时文件再 rename,禁止原地截断写。
data/ 是运行时目录,不要手工修改。
前端对接契约
envelope、错误码和鉴权行为以 server/internal/api/api.go 与 auth.go 为准。
web 是参考实现
平台本体是 API,web/ 只是一个可整体替换的参考实现。对接面是 Go 网关的 /api/v1/* 和 /v1/models/*。机器可读契约见 REST API 的机器可消费一节。
协议基线
除静态文件端点外,所有响应统一 envelope,code=0 表示成功:
{"code": 0, "message": "ok", "data": {...}}错误码一览:
| code | 含义 |
|---|---|
40001 | 参数或校验错误,编辑服务的 422 经代理也映射为此 |
40002 | 超限,如上传大小 |
40100 | 鉴权失败 |
40400 | 模型或资源不存在 |
40900 | 冲突 |
50000 | 服务器内部错误 |
50200 | 编辑服务不可达等代理错误 |
50400 | 编辑服务超时 |
鉴权与 CORS:
- 默认关闭,设
VIEWER_API_TOKEN后,除 OPTIONS 预检和三个只读文件端点外全部要求Authorization: Bearer <token>。 - 三个豁免端点是 model.xkt、metadata.json 和 issue 截图的 GET,因为 xeokit 和
<img>带不了请求头。 - chat 的 SSE 是唯一放行
?token=查询参数的路径,因为 EventSource 不支持自定义头。 - CORS 白名单用
VIEWER_CORS_ORIGINS配置,逗号分隔。
SSE 事件流:GET /api/v1/chat/sessions/{cid}/events 返回标准 SSE 帧,服务端维护编号缓冲,断线可以用 Last-Event-ID 重放。帧类型见 REST API 的帧表。
编辑流程对接
一切修改落在构建脚本上,原直改端点已退役。典型链路:
BASE=http://127.0.0.1:8090/api/v1
MID=m_0123456789abcdef
# 1. 暂存脚本(整脚本或 params 增量)
curl -X PUT "$BASE/models/$MID/script" \
-H 'Content-Type: application/json' \
-d '{"script": "PARAMS = {...}\n\ndef build(params, out_path):\n ...\n"}'
# 2. 沙箱试运行(预览,无版本;成功后 Go 侧排队重转 XKT)
curl -X POST "$BASE/models/$MID/script/run"
# 3. 保存大版本 v{n}
curl -X POST "$BASE/models/$MID/script/save" \
-H 'Content-Type: application/json' -d '{"note": "v1"}'
# 4. 按 guid 定位脚本调用点
curl "$BASE/models/$MID/script/locate?guid=2O2Fr\$t4X7ZfFPoeewFlqU"
# 5. 版本与 diff
curl "$BASE/models/$MID/edit/versions"
curl -X POST "$BASE/models/$MID/script/diff" \
-H 'Content-Type: application/json' -d '{"base": "v1", "target": "v2"}'
curl -X POST "$BASE/models/$MID/edit/diff" \
-H 'Content-Type: application/json' -d '{"base": "v1", "target": "current"}'配套端点还有 script/undo|redo|discard、script/rollback、script/staging/diff 和 GET .../scripts。保存或回滚后轮询 GET /api/v1/models,状态从 converting 变成 ready 就说明新 XKT 可取了。
显示对接
| 端点 | 说明 |
|---|---|
GET /v1/models/{id}/model.xkt | XKT 几何数据,支持 Range |
GET /v1/models/{id}/metadata.json | xeokit 元模型,schema 见 REST API |
GET /v1/models/{id}/render.json | DXF 渲染数据,仅 dxf 模型 |
自研前端可以用任何渲染器。用 xeokit 时 metadata.json 可以直接作为 XKTLoaderPlugin.load 的输入,实体 id 就是 IFC GlobalId,和编辑 API 的 guid 天然对齐。