317 lines
11 KiB
Markdown
317 lines
11 KiB
Markdown
# 算法集成规范(Route B · 通用接口接入)
|
||
|
||
本文是宏量点云处理平台对**多类算法批量接入**的对外规范,面向算法团队 +
|
||
验收方。一句话总结:
|
||
|
||
> 算法侧只交付 **`一个 CLI` + `一份 result.json`**,平台用统一插件机制
|
||
> 接入、并发执行、按 manifest 渲染表单、按 result.json 渲染结果。
|
||
|
||
满足工信部验收"算法在软件中集成运行"且 20 分钟内出结果的要求。
|
||
|
||
---
|
||
|
||
## 1. 总体架构
|
||
|
||
```
|
||
前端 (React)
|
||
└── AlgorithmRunPanel ← 由 manifest 自动生成插件列表 + 多选并发
|
||
└── GenericResultView ← 按 result.json 统一渲染(误差/测量/图片/3D 叠加)
|
||
└── PointCloudViewer ← pluginOverlay prop 接收插件生成的热力 / PLY
|
||
|
||
后端 (Node + tRPC)
|
||
└── algorithm.list ← 列出已注册插件
|
||
└── algorithm.run ← 单插件执行(入并发池)
|
||
└── algorithm.runBatch ← 多插件并发执行(入并发池)
|
||
└── /api/files/read/analysis/:taskId/:resultId/:filename ← 通用产物路由
|
||
|
||
算法层(按算法各自交付)
|
||
└── manifest/<id>.json ← 元数据 + UI 表单 + runtime
|
||
└── adapters/<id>_run.py ← 平台薄适配层
|
||
└── fixtures/<id>.json ← 未就绪时的 mock 预设
|
||
```
|
||
|
||
并发池:默认 4 路(环境变量 `ALGORITHM_JOB_CONCURRENCY` 可覆盖),
|
||
单算法超时默认 20 分钟(manifest 内 `runtime.timeoutMs` 可覆盖)。
|
||
|
||
---
|
||
|
||
## 2. Manifest JSON 协议
|
||
|
||
文件位置:`server/plugins/manifests/<plugin-id>.json`。完整示例见
|
||
`coupler-panel.json` 等。
|
||
|
||
```jsonc
|
||
{
|
||
"id": "coupler-panel", // 全局唯一 kebab-case ID
|
||
"name": "车钩面板检测", // UI 显示中文名
|
||
"description": "...",
|
||
"version": "1.0.0", // 算法版本
|
||
"schemaVersion": 1, // result.json schema 版本
|
||
"category": "几何测量", // UI 分组(可选)
|
||
"requiresPointCloud": true, // 默认 true
|
||
"requiresCad": false, // 默认 false;true 时需车型下有 CAD
|
||
"inputs": [
|
||
{
|
||
"name": "profile",
|
||
"label": "调参档位",
|
||
"type": "select", // number | integer | string | boolean | select
|
||
"default": "auto",
|
||
"options": [
|
||
{ "value": "auto", "label": "自动" },
|
||
{ "value": "high_density", "label": "高密度" }
|
||
],
|
||
"description": "辅助说明,渲染在字段下方"
|
||
}
|
||
],
|
||
"runtime": {
|
||
"kind": "process", // 真实算法
|
||
"cmd": ["{pythonBin}", "{adapterDir}/coupler_panel_run.py", "{inputJson}"],
|
||
"envExtra": {
|
||
"PYTHONPATH": "{algRoot}/挠度-偏转角/delivery/coupler_panel_release"
|
||
},
|
||
"timeoutMs": 1200000 // 默认 20 分钟
|
||
}
|
||
}
|
||
```
|
||
|
||
或 mock 模式(用于占位未就绪算法):
|
||
|
||
```jsonc
|
||
"runtime": {
|
||
"kind": "mock",
|
||
"fixture": "flatness-suite.json", // 相对 fixtures/ 的文件名
|
||
"simulateMs": 1500 // 模拟耗时,UI 演示进度条
|
||
}
|
||
```
|
||
|
||
### 2.1 cmd 占位符
|
||
|
||
| 占位符 | 含义 |
|
||
|--------|------|
|
||
| `{pythonBin}` | 容器内 Python 解释器(`$PYTHON3_PATH` 或 `python3`) |
|
||
| `{inputJson}` | runner 写到 workDir 的 `input.json` 绝对路径 |
|
||
| `{workDir}` | 任务工作目录(也是 outDir) |
|
||
| `{adapterDir}` | `server/plugins/adapters/` 绝对路径 |
|
||
| `{algRoot}` | 算法源码根目录(`$ALG_ROOT` 或 `/home/lipan/massive-cloud/alg`) |
|
||
|
||
---
|
||
|
||
## 3. 输入契约 — `input.json`
|
||
|
||
runner 在 workDir 写出 `input.json` 后调用算法 CLI,固定路径:
|
||
`/data/tasks/<taskId>/analysis/<resultId>/input.json`。
|
||
|
||
```jsonc
|
||
{
|
||
"schemaVersion": 1,
|
||
"pluginId": "coupler-panel",
|
||
"pluginVersion": "1.0.0",
|
||
"resultId": 123,
|
||
"taskId": 88,
|
||
"workDir": "/data/tasks/88/analysis/123",
|
||
"outDir": "/data/tasks/88/analysis/123", // 与 workDir 相同
|
||
"inputs": {
|
||
"pointCloudPaths": ["/data/tasks/88/pointcloud/scan.ply"],
|
||
"cadPath": null, // 或 "/data/models/55/cad/panel.stl"
|
||
"config": { // 来自 UI 表单
|
||
"profile": "auto",
|
||
"rightAxisSign": "1"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
约束:
|
||
- 算法**不读全局环境**,所有产物落 `outDir`;
|
||
- 不修改 algorithm package 自身目录;
|
||
- CLI 必须**非交互**:禁用 GUI / 弹窗。
|
||
|
||
---
|
||
|
||
## 4. 输出契约 — `result.json`
|
||
|
||
算法在 `outDir/result.json` 写最终结果:
|
||
|
||
```jsonc
|
||
{
|
||
"schemaVersion": 1,
|
||
"status": "completed", // 或 "error"
|
||
"message": "可选简短说明",
|
||
"measurements": [
|
||
{ "name": "右偏转角", "value": 0.83, "unit": "°", "tolerance": 1.0, "status": "pass" },
|
||
{ "name": "最大 X 偏差", "value": 0.42, "unit": "mm", "tolerance": 1.0, "status": "pass" },
|
||
{ "name": "拟合内点率", "value": 0.92, "status": "info" }
|
||
],
|
||
"errorStats": { // 可选;用于 GenericResultView 误差卡片
|
||
"min": -1.83, "max": 1.28, "mean": 0.04, "stdDev": 0.32,
|
||
"totalPoints": 1500000,
|
||
"withinTolerance": 1450000, "outOfTolerance": 50000
|
||
},
|
||
"overlay3d": { // 可选;前端通过 PointCloudViewer.pluginOverlay 消费
|
||
"kind": "vertexColors", // 或 "ply" / "none"
|
||
"file": "heatmap.bin", // 相对 outDir
|
||
"vertexCount": 123456,
|
||
"dispMin": -0.5, "dispMax": 0.5
|
||
},
|
||
"images": ["plot.png", "images/diagnostic.png"], // 相对 outDir
|
||
"extra": { "...": "算法专属字段,前端折叠展示" }
|
||
}
|
||
```
|
||
|
||
`measurements[*].status` ∈ {`pass` | `warning` | `fail` | `info`},前端按 badge
|
||
颜色渲染。
|
||
|
||
---
|
||
|
||
## 5. 进度上报协议
|
||
|
||
子进程通过 **stdout 单行 JSON** 上报进度,runner 解析后写入数据库:
|
||
|
||
```text
|
||
{"type":"progress","percent":0.12,"message":"去噪"}
|
||
{"type":"progress","percent":0.45,"message":"平面拟合"}
|
||
{"type":"progress","percent":0.80,"message":"轮廓度计算"}
|
||
```
|
||
|
||
字段说明:
|
||
- `percent`:允许 0~1 或 0~100,runner 自动归一化为 0~1;
|
||
- `message`:当前阶段名,会写入 `resultData.stage`。
|
||
|
||
stderr 仅作日志,runner 会原样转发到平台日志。
|
||
|
||
---
|
||
|
||
## 6. 错误约定
|
||
|
||
| 情况 | 算法行为 | 平台行为 |
|
||
|------|----------|----------|
|
||
| 数据不合规 / 计算失败 | 写 `result.json` 且 `status="error"`,`message` 中文+英文摘要 | DB.status="error",前端红字提示 |
|
||
| 进程异常退出(无 result.json) | exit code 非 0 | runner 写一条带 `exitCode` 的 error 记录 |
|
||
| 超时 | runner 发 SIGKILL | DB.status="error",错误信息为「超时 N ms」 |
|
||
|
||
---
|
||
|
||
## 7. tRPC 接口
|
||
|
||
```ts
|
||
// 列出所有插件
|
||
trpc.algorithm.list.useQuery()
|
||
// → { plugins: PluginSummary[], scheduler: { running, queued, limit } }
|
||
|
||
// 单个算法
|
||
trpc.algorithm.run.useMutation()
|
||
// .mutate({ taskId, pluginId, params })
|
||
// → { id, pluginId, schemaVersion }
|
||
|
||
// 并发批量
|
||
trpc.algorithm.runBatch.useMutation()
|
||
// .mutate({ taskId, jobs: [{ pluginId, params }] })
|
||
// → { jobs: [{ id, pluginId }], scheduler }
|
||
```
|
||
|
||
每次 run 都会在 `task_analysis_results` 表插入一条 `type='plugin'` 的记录。
|
||
前端通过原有 `trpc.analysisResult.list` 轮询,按 `pluginId` 区分。
|
||
|
||
---
|
||
|
||
## 8. 数据库变更
|
||
|
||
迁移文件:`drizzle/0007_task_analysis_plugin_columns.sql`。
|
||
|
||
```sql
|
||
ALTER TABLE task_analysis_results
|
||
MODIFY COLUMN type ENUM(..., 'plugin') NOT NULL;
|
||
ALTER TABLE task_analysis_results
|
||
ADD COLUMN pluginId VARCHAR(64) NULL AFTER type,
|
||
ADD COLUMN schemaVersion INT NULL AFTER pluginId;
|
||
CREATE INDEX idx_task_analysis_plugin_id ON task_analysis_results (pluginId);
|
||
```
|
||
|
||
升级流程:
|
||
1. 应用 SQL 迁移(`pnpm db:push` 或手动执行)。
|
||
2. 重启 Node 服务。
|
||
3. 新增算法只需新增 manifest + adapter,无需再变更数据库。
|
||
|
||
---
|
||
|
||
## 9. 文件路由(产物)
|
||
|
||
```
|
||
GET /api/files/read/analysis/:taskId/:resultId/:filename
|
||
```
|
||
|
||
返回 outDir 下任意相对路径的文件(含 `images/foo.png` 等)。前端通过
|
||
`result.json.images` / `overlay3d.file` 直接拼接 URL。
|
||
|
||
> 与现存 `/read/analysis/heatmap/...`、`/read/analysis/assembly-vertex/...`
|
||
> 共存;具体路由优先匹配,最后由通用路由兜底。
|
||
|
||
---
|
||
|
||
## 10. 已接入的算法
|
||
|
||
全部 4 类算法都是真实 `process` 运行时,没有任何 mock:
|
||
|
||
| 插件 ID | 算法 | 来源 | 依赖 |
|
||
|---------|------|------|------|
|
||
| `coupler-panel` | 车钩面板检测 | `alg/挠度-偏转角/delivery/coupler_panel_release` | numpy / scipy / pyyaml |
|
||
| `underframe-deflection` | 构架挠度测量 | `alg/挠度-偏转角/delivery/underframe_deflection` | numpy / scipy |
|
||
| `train-measurement` | 列车点云测距 | `alg/260505列车点云测距v1/.../train_measurement_system` | numpy / pyyaml / open3d / openpyxl |
|
||
| `flatness-suite` | 平整度 / 平面度 / 轮廓 / 曲率 | `alg/平整度、平面度、轮廓测量/` 下的脚本 | numpy / open3d(contour 还需 torch + trimesh) |
|
||
|
||
> flatness-suite 的 contour 子模块依赖较重(~2GB torch)。adapter 在 torch
|
||
> 缺失时会自动跳过 contour,**plane / curvature 仍能正常出结果**。
|
||
|
||
### 10.1 容器内依赖安装
|
||
|
||
镜像构建阶段:
|
||
```bash
|
||
# 通用部分 — 已经在 Dockerfile 里自动执行
|
||
python3 -m pip install --break-system-packages \
|
||
--default-timeout=300 --retries 10 \
|
||
-i https://pypi.tuna.tsinghua.edu.cn/simple \
|
||
-r requirements-geometry.txt
|
||
|
||
# 可选:启用 flatness-suite 的 contour 子模块(CPU 版,~2GB)
|
||
python3 -m pip install --break-system-packages \
|
||
-r requirements-flatness-contour.txt
|
||
```
|
||
|
||
### 10.2 算法源码挂载
|
||
|
||
`docker-compose.yml` 已配置:
|
||
|
||
```yaml
|
||
app:
|
||
environment:
|
||
- ALG_ROOT=/alg
|
||
volumes:
|
||
- ${LOCAL_ALG_DIR:-./alg}:/alg:ro # 只读挂载,安全
|
||
```
|
||
|
||
宿主机上的 `alg/` 目录(与本仓库 `alg/` 结构一致)只读挂载到容器内 `/alg`。
|
||
manifest 里所有 `{algRoot}` 占位符在运行时都解析为该路径。算法源码通过卷挂载
|
||
而非打入镜像,避免 13.5GB 的构建上下文。
|
||
|
||
---
|
||
|
||
## 11. 验收检查清单
|
||
|
||
- [x] 系统中存在「算法运行(多选并发)」面板,能列出 4 类算法
|
||
- [x] 多选执行后所有算法**并行**调度(默认 4 路并发池)
|
||
- [x] 单次运行有进度条,20 分钟内出结果(manifest 默认超时 1200s)
|
||
- [x] 结果统一渲染:误差统计 / 测量值表 / 图片 / 3D 叠加按钮
|
||
- [x] 数据库记录算法 ID + schemaVersion,可回放、可追溯
|
||
- [x] 算法层与 UI 层完全解耦:新增算法不改前端 / 不改 tRPC / 不改数据库
|
||
|
||
---
|
||
|
||
## 12. 后续接入新算法的步骤
|
||
|
||
1. 把算法封装成一个 Python 入口:`python -m my_algo.run <input.json>`,
|
||
保证:读 `inputs.pointCloudPaths[0]` → 计算 → 写 `outDir/result.json`。
|
||
2. 复制一份 manifest(如 `coupler-panel.json`)改 `id`、`cmd`、`envExtra`、`inputs`。
|
||
3. (可选)写一个 adapter `server/plugins/adapters/my_algo_run.py` 做参数翻译,
|
||
或者算法 CLI 直接吃 input.json。
|
||
4. 重启服务,前端自动刷出新条目,**不需要任何前端 / SQL 改动**。
|