# 算法集成规范(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/.json ← 元数据 + UI 表单 + runtime └── adapters/_run.py ← 平台薄适配层 └── fixtures/.json ← 未就绪时的 mock 预设 ``` 并发池:默认 4 路(环境变量 `ALGORITHM_JOB_CONCURRENCY` 可覆盖), 单算法超时默认 20 分钟(manifest 内 `runtime.timeoutMs` 可覆盖)。 --- ## 2. Manifest JSON 协议 文件位置:`server/plugins/manifests/.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//analysis//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 `, 保证:读 `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 改动**。