ARCH - TASKER BIM: серверный CAD-шлюз и независимый просмотр из Ops
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import type { TIssueAttachment } from "@plane/types";
|
||||
import {
|
||||
DEFAULT_BEAM_VIEWER_BASE_URL,
|
||||
@@ -43,12 +42,13 @@ export type TBeamModelVersionRecord = {
|
||||
|
||||
export type TBeamViewerAttachment = {
|
||||
assetId?: string;
|
||||
backend: "beam-viewer-local" | "beam-viewer";
|
||||
backend: "beam-viewer-local" | "beam-viewer-ops" | "beam-viewer";
|
||||
conversion?: TBeamViewerConversion;
|
||||
downloadUrl: string;
|
||||
originalFilename: string;
|
||||
previewAvailable: boolean;
|
||||
projectId?: string;
|
||||
registryOwnerId?: string;
|
||||
sha256?: string;
|
||||
src: string;
|
||||
type: string;
|
||||
@@ -115,30 +115,13 @@ const MIME_TYPE_BY_EXTENSION: Record<string, string> = {
|
||||
const normalizeBaseUrl = (url: string | undefined): string =>
|
||||
(url && url.trim() ? url.trim() : DEFAULT_BEAM_VIEWER_BASE_URL).replace(/\/+$/, "");
|
||||
|
||||
const getUploadGroupId = (parts: Array<string | undefined>): string => {
|
||||
const value = parts
|
||||
.filter(Boolean)
|
||||
.join("_")
|
||||
.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||
return value || "tasker";
|
||||
};
|
||||
|
||||
const toBeamRelativeUploadSrc = (src: string): string => {
|
||||
try {
|
||||
const url = new URL(src);
|
||||
return url.pathname.replace(/^\/+/, "");
|
||||
} catch (_error) {
|
||||
return src.replace(/^\/+/, "");
|
||||
}
|
||||
};
|
||||
|
||||
const toBeamAbsoluteUrl = (src: string | undefined, cacheKey?: string): string | undefined => {
|
||||
if (!src) return undefined;
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(src);
|
||||
} catch (_error) {
|
||||
url = new URL(src.startsWith("/") ? src : `/${src}`, `${getBeamApiBaseUrl()}/`);
|
||||
url = new URL(src.startsWith("/") ? src : `/${src}`, `${getBeamViewerBaseUrl()}/`);
|
||||
}
|
||||
if (cacheKey) url.searchParams.set("v", cacheKey);
|
||||
return url.toString();
|
||||
@@ -146,9 +129,6 @@ const toBeamAbsoluteUrl = (src: string | undefined, cacheKey?: string): string |
|
||||
|
||||
export const getBeamViewerBaseUrl = (): string => normalizeBaseUrl(process.env.VITE_BEAM_VIEWER_BASE_URL);
|
||||
|
||||
export const getBeamApiBaseUrl = (): string =>
|
||||
normalizeBaseUrl(process.env.VITE_BEAM_API_BASE_URL || process.env.VITE_BEAM_VIEWER_BASE_URL);
|
||||
|
||||
export const getBeamModelTypeFromName = (name: string | undefined): string | null => {
|
||||
return getBeamModelTypeFromFileName(name);
|
||||
};
|
||||
@@ -317,188 +297,3 @@ export const getBeamVersionViewerUrl = (version: TBeamModelVersionRecord): strin
|
||||
type: artifactType,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchBeamConversionStatus = async (beamViewer: TBeamViewerAttachment): Promise<TBeamConversionStatus> => {
|
||||
const statusUrl = new URL("/api/conversions/status", `${getBeamApiBaseUrl()}/`);
|
||||
statusUrl.searchParams.set("src", toBeamRelativeUploadSrc(beamViewer.src));
|
||||
|
||||
const response = await fetch(statusUrl.toString(), { credentials: "include" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`BIM conversion status failed: HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as TBeamConversionStatus;
|
||||
return {
|
||||
...payload,
|
||||
artifactUrl: toBeamAbsoluteUrl(payload.artifactSrc, payload.updatedAt),
|
||||
metadataUrl: toBeamAbsoluteUrl(payload.metadataSrc),
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchBeamModelVersions = async (
|
||||
beamViewer: TBeamViewerAttachment
|
||||
): Promise<TBeamVersionHistoryResponse> => {
|
||||
const versionsUrl = new URL("/api/uploads/versions", `${getBeamApiBaseUrl()}/`);
|
||||
if (beamViewer.projectId && beamViewer.assetId) {
|
||||
versionsUrl.searchParams.set("projectId", beamViewer.projectId);
|
||||
versionsUrl.searchParams.set("assetId", beamViewer.assetId);
|
||||
} else {
|
||||
versionsUrl.searchParams.set("src", toBeamRelativeUploadSrc(beamViewer.src));
|
||||
}
|
||||
|
||||
const response = await fetch(versionsUrl.toString(), { credentials: "include" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`BIM version history failed: HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as TBeamVersionHistoryResponse;
|
||||
};
|
||||
|
||||
export const uploadBeamModelFile = (
|
||||
file: File,
|
||||
options: {
|
||||
assetId?: string;
|
||||
issueId?: string;
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
|
||||
projectId?: string;
|
||||
uploadedBy?: string;
|
||||
version?: number;
|
||||
workspaceSlug?: string;
|
||||
} = {}
|
||||
): Promise<TBeamViewerAttachment> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const type = getBeamModelTypeFromName(file.name);
|
||||
if (!type) {
|
||||
reject(new Error("Формат модели не поддерживается BIM Viewer"));
|
||||
return;
|
||||
}
|
||||
const directViewerType = getBeamDirectModelTypeFromName(file.name);
|
||||
|
||||
const apiBaseUrl = getBeamApiBaseUrl();
|
||||
const uploadUrl = new URL("/api/uploads", `${apiBaseUrl}/`);
|
||||
uploadUrl.searchParams.set("filename", file.name);
|
||||
uploadUrl.searchParams.set(
|
||||
"projectId",
|
||||
getUploadGroupId([options.workspaceSlug, options.projectId, options.issueId])
|
||||
);
|
||||
if (options.assetId) uploadUrl.searchParams.set("assetId", options.assetId);
|
||||
if (options.version) uploadUrl.searchParams.set("version", String(options.version));
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", uploadUrl.toString());
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
||||
xhr.upload.addEventListener("progress", (event) => {
|
||||
if (!event.lengthComputable || !options.onUploadProgress) return;
|
||||
options.onUploadProgress({
|
||||
loaded: event.loaded,
|
||||
progress: event.loaded / event.total,
|
||||
total: event.total,
|
||||
} as AxiosProgressEvent);
|
||||
});
|
||||
xhr.addEventListener("error", () => reject(new Error("Не удалось загрузить модель в BIM Viewer")));
|
||||
xhr.addEventListener("load", () => {
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
reject(new Error(xhr.responseText || `BIM Viewer upload failed: HTTP ${xhr.status}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText) as {
|
||||
assetId?: string;
|
||||
conversion?: TBeamViewerAttachment["conversion"];
|
||||
originalFilename?: string;
|
||||
projectId?: string;
|
||||
sha256?: string;
|
||||
src?: string;
|
||||
uploadedAt?: string;
|
||||
version?: number;
|
||||
versionId?: string;
|
||||
};
|
||||
if (!response.src) {
|
||||
reject(new Error("BIM Viewer не вернул путь к загруженной модели"));
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadUrl = new URL(response.src.startsWith("/") ? response.src : `/${response.src}`, `${apiBaseUrl}/`);
|
||||
const baseAttachment: TBeamViewerAttachment = {
|
||||
assetId: response.assetId ?? options.assetId,
|
||||
backend: "beam-viewer-local",
|
||||
downloadUrl: downloadUrl.toString(),
|
||||
originalFilename: response.originalFilename || file.name,
|
||||
previewAvailable: !!directViewerType,
|
||||
projectId: response.projectId,
|
||||
sha256: response.sha256,
|
||||
src: downloadUrl.toString(),
|
||||
type,
|
||||
uploadedBy: options.uploadedBy,
|
||||
uploadedAt: response.uploadedAt || new Date().toISOString(),
|
||||
version: response.version ?? options.version ?? 1,
|
||||
versionId: response.versionId,
|
||||
};
|
||||
|
||||
if (!directViewerType) {
|
||||
const nextAttachment: TBeamViewerAttachment = {
|
||||
...baseAttachment,
|
||||
conversion: {
|
||||
...response.conversion,
|
||||
componentTreeRequired: true,
|
||||
message:
|
||||
response.conversion?.message ??
|
||||
"Оригинальный STEP загружен. Просмотр появится после подготовки модели и дерева компонентов.",
|
||||
sourceFormat: type,
|
||||
status: response.conversion?.status ?? "conversion_required",
|
||||
targetFormat: response.conversion?.targetFormat ?? "xkt",
|
||||
},
|
||||
};
|
||||
resolve({
|
||||
...nextAttachment,
|
||||
versions: [createBeamModelVersionRecord(nextAttachment, { size: file.size })],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const nextAttachment: TBeamViewerAttachment = {
|
||||
...baseAttachment,
|
||||
viewerUrl: buildBeamViewerUrl({
|
||||
name: file.name,
|
||||
settingsSrc: downloadUrl.toString(),
|
||||
src: downloadUrl.toString(),
|
||||
type: directViewerType,
|
||||
}),
|
||||
};
|
||||
|
||||
resolve({
|
||||
...nextAttachment,
|
||||
versions: [createBeamModelVersionRecord(nextAttachment, { size: file.size })],
|
||||
});
|
||||
} catch (_error) {
|
||||
reject(new Error("BIM Viewer вернул некорректный ответ"));
|
||||
}
|
||||
});
|
||||
xhr.send(file);
|
||||
});
|
||||
|
||||
const deleteBeamStorageResource = async (
|
||||
endpoint: "/api/uploads/asset" | "/api/uploads/version",
|
||||
payload: unknown
|
||||
): Promise<void> => {
|
||||
const response = await fetch(new URL(endpoint, `${getBeamApiBaseUrl()}/`).toString(), {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error((await response.text()) || `BIM storage delete failed: HTTP ${response.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteBeamModelAsset = async (beamViewer: TBeamViewerAttachment): Promise<void> =>
|
||||
deleteBeamStorageResource("/api/uploads/asset", beamViewer);
|
||||
|
||||
export const deleteBeamModelVersion = async (version: TBeamModelVersionRecord): Promise<void> =>
|
||||
deleteBeamStorageResource("/api/uploads/version", version);
|
||||
|
||||
Reference in New Issue
Block a user