Files
massive-cloud/server/stepParse.ts

144 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { spawnSync, type SpawnSyncOptions } from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
const DATA_DIR = process.env.DATA_DIR || '/data';
export function stepParseCacheDir(modelId: number, cadFileId: number): string {
return path.join(DATA_DIR, 'models', String(modelId), 'cad', '.step_parse', String(cadFileId));
}
function scriptPath(): string {
const here = path.dirname(fileURLToPath(import.meta.url));
return path.join(here, '..', 'scripts', 'step_parse_mvp.py');
}
/**
* 不依赖 existsSync:挂载/命名空间下路径探测可能不准;子进程 PATH 过短会导致 `python3` ENOENT。
* 顺序尝试可执行文件,仅对 ENOENT 换下一个。
*/
function python3Candidates(): string[] {
const fromEnv = process.env.PYTHON3_PATH?.trim();
const list: string[] = [];
if (fromEnv) list.push(fromEnv);
if (process.platform !== 'win32') {
list.push('/usr/bin/python3', '/usr/local/bin/python3');
}
list.push('python3');
return Array.from(new Set(list));
}
function spawnStepParseSync(script: string, stepAbsolutePath: string, outDir: string) {
const envBase: NodeJS.ProcessEnv = {
...process.env,
PYTHONUNBUFFERED: '1',
// 保证按名查找 python3 时能找到 Debian/Ubuntu 常见路径
PATH: `/usr/bin:/usr/local/bin:/bin:/usr/sbin:${process.env.PATH ?? ''}`,
};
const opts: SpawnSyncOptions = {
encoding: 'utf-8',
maxBuffer: 64 * 1024 * 1024,
timeout: 60 * 60 * 1000,
env: envBase,
};
const args = [script, stepAbsolutePath, outDir];
let lastErr: NodeJS.ErrnoException | undefined;
for (const py of python3Candidates()) {
const r = spawnSync(py, args, opts);
if (!r.error) {
return r;
}
lastErr = r.error as NodeJS.ErrnoException;
if (lastErr.code !== 'ENOENT') {
throw new Error(
`无法启动 Python(${py}):${lastErr.message}。PYTHON3_PATH=${process.env.PYTHON3_PATH ?? '(未设置)'}`,
);
}
}
throw new Error(
`无法启动 Python(已尝试: ${python3Candidates().join(' → ')}):${lastErr?.message ?? 'ENOENT'}。` +
`请确认镜像已安装 python3(docker/Dockerfile.dev),或设置 PYTHON3_PATH 为实际解释器路径。`,
);
}
export type StepManifestPart = { id: string; name: string; stlRelative: string };
export type StepManifest = {
ok: boolean;
parts: StepManifestPart[];
sourceStep?: string;
};
/** cadquery/OCP 可能在 stdout 打印非 JSON 日志,不能只取最后一行 */
function parseJsonResultFromProcessOutput(stdout: string, stderr: string): {
ok?: boolean;
error?: string;
parts?: StepManifestPart[];
} | null {
const text = `${stdout || ''}\n${stderr || ''}`;
let last: { ok?: boolean; error?: string; parts?: StepManifestPart[] } | null = null;
for (const line of text.split('\n')) {
const t = line.trim();
if (!t.startsWith('{') || !t.endsWith('}')) continue;
try {
const o = JSON.parse(t) as { ok?: boolean };
if (typeof o.ok === 'boolean') {
last = o as { ok?: boolean; error?: string; parts?: StepManifestPart[] };
}
} catch {
/* 单行非完整 JSON */
}
}
return last;
}
/**
* 调用 Python cadquery 将 STEP 三角化为每零件 STL,并写入 manifest.json。
* @returns manifest 或抛出带 message 的错误
*/
export function runStepParseMvp(stepAbsolutePath: string, outDir: string): StepManifest {
fs.mkdirSync(outDir, { recursive: true });
const script = scriptPath();
if (!fs.existsSync(script)) {
throw new Error(`未找到 STEP 解析脚本: ${script}`);
}
const r = spawnStepParseSync(script, stepAbsolutePath, outDir);
const out = (v: string | Buffer | null | undefined) =>
v == null ? '' : typeof v === 'string' ? v : v.toString('utf-8');
const combined = `${out(r.stdout)}\n${out(r.stderr)}`;
let parsed = parseJsonResultFromProcessOutput(out(r.stdout), out(r.stderr));
const resultFile = path.join(outDir, '_parse_result.json');
if ((!parsed || typeof parsed.ok !== 'boolean') && fs.existsSync(resultFile)) {
try {
parsed = JSON.parse(fs.readFileSync(resultFile, 'utf-8')) as typeof parsed;
} catch {
/* ignore */
}
}
if (!parsed || typeof parsed.ok !== 'boolean') {
const sig = r.signal != null ? ` signal=${r.signal}` : '';
throw new Error(
`STEP 解析无有效 JSON 结果(exit=${r.status ?? 'null'}${sig})。` +
`请确认 cadquery 可用。输出摘录:\n${combined.trim().slice(-2500)}`,
);
}
if (!parsed.ok) {
throw new Error(parsed.error || 'STEP 解析失败');
}
const manFile = path.join(outDir, 'manifest.json');
if (!fs.existsSync(manFile)) {
throw new Error(`未生成 manifest.json: ${manFile}`);
}
const raw = JSON.parse(fs.readFileSync(manFile, 'utf-8')) as StepManifest;
return raw;
}