ARCH - TASKER BIM: серверный CAD-шлюз и независимый просмотр из Ops

This commit is contained in:
DCCONSTRUCTIONS
2026-07-30 07:35:23 +03:00
parent 24481ea432
commit ad04910209
10 changed files with 1252 additions and 499 deletions
@@ -9,7 +9,6 @@ import { API_BASE_URL } from "@plane/constants";
// plane types
import { getFileMetaDataForUpload, generateFileUploadPayload } from "@plane/services";
import type {
TFileMetaDataLite,
TIssueAttachment,
TIssueAttachmentUploadResponse,
TIssueServiceType,
@@ -17,17 +16,11 @@ import type {
import { EIssueServiceType } from "@plane/types";
// services
import {
deleteBeamModelAsset,
deleteBeamModelVersion,
getBeamModelVersionRecords,
getBeamModelMimeType,
getBeamViewerAttachment,
getBeamViewerVersionNumber,
isBeamModelFile,
mergeBeamModelVersionRecords,
type TBeamModelVersionRecord,
type TBeamViewerAttachment,
uploadBeamModelFile,
type TBeamConversionStatus,
type TBeamVersionHistoryResponse,
} from "@/helpers/beam-viewer";
import { APIService } from "@/services/api.service";
import { FileUploadService } from "@/services/file-upload.service";
@@ -49,6 +42,15 @@ export class IssueAttachmentService extends APIService {
this.serviceType = serviceType;
}
private attachmentBasePath(workspaceSlug: string, projectId: string, issueId: string): string {
return `/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/attachments`;
}
private toBeamRequestError(error: unknown, fallback: string): Error {
const responseData = (error as { response?: { data?: { message?: string; error?: string } } })?.response?.data;
return new Error(responseData?.message || responseData?.error || fallback);
}
private async updateIssueAttachmentUploadStatus(
workspaceSlug: string,
projectId: string,
@@ -64,43 +66,6 @@ export class IssueAttachmentService extends APIService {
});
}
private async createBeamIssueAttachmentReference(
workspaceSlug: string,
projectId: string,
issueId: string,
payload: TFileMetaDataLite & { beamViewer: TBeamViewerAttachment }
): Promise<TIssueAttachment> {
return this.post(
`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/attachments/`,
payload
)
.then((response) => {
const createResponse: TIssueAttachmentCreateResponse = response?.data;
return createResponse.attachment;
})
.catch((error) => {
throw error?.response?.data ?? error;
});
}
async updateBeamIssueAttachmentReference(
workspaceSlug: string,
projectId: string,
issueId: string,
attachmentId: string,
beamViewer: TBeamViewerAttachment,
attributes: Record<string, unknown> = {}
): Promise<TIssueAttachment> {
return this.patch(
`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/attachments/${attachmentId}/`,
{ ...attributes, beamViewer }
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data ?? error;
});
}
async uploadBeamIssueAttachmentVersion(
workspaceSlug: string,
projectId: string,
@@ -108,60 +73,26 @@ export class IssueAttachmentService extends APIService {
attachment: TIssueAttachment,
file: File,
uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"],
uploadedBy?: string
_uploadedBy?: string
): Promise<TIssueAttachment> {
const previousBeamViewer = getBeamViewerAttachment(attachment);
if (!previousBeamViewer) {
if (!getBeamViewerAttachment(attachment)) {
throw new Error("Для этого вложения нет BIM-метаданных.");
}
if (!isBeamModelFile(file.name)) {
throw new Error("Формат модели не поддерживается BIM Viewer.");
}
const previousVersions = getBeamModelVersionRecords(previousBeamViewer, attachment);
const currentVersion = Math.max(
getBeamViewerVersionNumber(previousBeamViewer, attachment),
...previousVersions.map((version) => version.version)
);
const nextVersion = currentVersion + 1;
const assetId = previousBeamViewer.assetId || attachment.id;
const nextBeamViewer = await uploadBeamModelFile(file, {
assetId,
issueId,
onUploadProgress: uploadProgressHandler,
projectId,
uploadedBy,
version: nextVersion,
workspaceSlug,
});
const nextVersions = mergeBeamModelVersionRecords(
{ ...previousBeamViewer, assetId, versions: previousVersions },
nextBeamViewer,
attachment
);
return this.updateBeamIssueAttachmentReference(
workspaceSlug,
projectId,
issueId,
attachment.id,
{
...nextBeamViewer,
assetId,
projectId: nextBeamViewer.projectId || previousBeamViewer.projectId,
previewAvailable: nextBeamViewer.previewAvailable,
uploadedBy,
version: nextVersion,
versions: nextVersions,
viewerUrl: nextBeamViewer.viewerUrl ?? null,
},
{
name: file.name,
size: file.size,
type: getBeamModelMimeType(file),
version: nextVersion,
}
);
const formData = new FormData();
formData.append("file", file);
return this.post(
`${this.attachmentBasePath(workspaceSlug, projectId, issueId)}/${attachment.id}/bim-versions/`,
formData,
{ onUploadProgress: uploadProgressHandler }
)
.then((response) => response?.data)
.catch((error) => {
throw this.toBeamRequestError(error, "Не удалось загрузить новую версию BIM-модели.");
});
}
async deleteBeamIssueAttachmentVersion(
@@ -171,36 +102,69 @@ export class IssueAttachmentService extends APIService {
attachment: TIssueAttachment,
versionToDelete: TBeamModelVersionRecord
): Promise<TIssueAttachment> {
const beamViewer = getBeamViewerAttachment(attachment);
if (!beamViewer) {
if (!getBeamViewerAttachment(attachment)) {
throw new Error("Для этого вложения нет BIM-метаданных.");
}
const versionKey = encodeURIComponent(versionToDelete.versionId || String(versionToDelete.version));
return this.delete(
`${this.attachmentBasePath(workspaceSlug, projectId, issueId)}/${attachment.id}/bim-versions/${versionKey}/`
)
.then((response) => response?.data)
.catch((error) => {
throw this.toBeamRequestError(error, "Не удалось удалить версию BIM-модели.");
});
}
const currentVersion = getBeamViewerVersionNumber(beamViewer, attachment);
const isCurrentVersion = versionToDelete.versionId
? beamViewer.versionId === versionToDelete.versionId
: currentVersion === versionToDelete.version;
if (isCurrentVersion) {
throw new Error("Текущую версию нельзя удалить. Сначала переключите модель на другую версию.");
}
async getBeamIssueAttachmentStatus(
workspaceSlug: string,
projectId: string,
issueId: string,
attachmentId: string
): Promise<TBeamConversionStatus> {
return this.get(
`${this.attachmentBasePath(workspaceSlug, projectId, issueId)}/${attachmentId}/bim-status/`
)
.then((response) => response?.data)
.catch((error) => {
throw this.toBeamRequestError(error, "Не удалось получить статус BIM-модели.");
});
}
const versions = getBeamModelVersionRecords(beamViewer, attachment);
const nextVersions = versions.filter((version) =>
versionToDelete.versionId
? version.versionId !== versionToDelete.versionId
: version.version !== versionToDelete.version
);
async getBeamIssueAttachmentVersions(
workspaceSlug: string,
projectId: string,
issueId: string,
attachmentId: string
): Promise<TBeamVersionHistoryResponse> {
return this.get(
`${this.attachmentBasePath(workspaceSlug, projectId, issueId)}/${attachmentId}/bim-versions/`
)
.then((response) => response?.data)
.catch((error) => {
throw this.toBeamRequestError(error, "Не удалось получить историю BIM-модели.");
});
}
if (nextVersions.length === versions.length) {
throw new Error("Версия не найдена.");
}
await deleteBeamModelVersion(versionToDelete);
return this.updateBeamIssueAttachmentReference(workspaceSlug, projectId, issueId, attachment.id, {
...beamViewer,
versions: nextVersions,
});
async getBeamIssueAttachmentViewerUrl(
workspaceSlug: string,
projectId: string,
issueId: string,
attachmentId: string,
versionId?: string
): Promise<string> {
return this.post(
`${this.attachmentBasePath(workspaceSlug, projectId, issueId)}/${attachmentId}/bim-viewer/`,
versionId ? { versionId } : {}
)
.then((response) => {
const viewerUrl = response?.data?.viewerUrl;
if (!viewerUrl) throw new Error("Ops не вернул ссылку запуска BIM Viewer.");
return viewerUrl as string;
})
.catch((error) => {
if (error instanceof Error && !("response" in error)) throw error;
throw this.toBeamRequestError(error, "Не удалось открыть BIM-модель.");
});
}
async uploadIssueAttachment(
@@ -211,19 +175,20 @@ export class IssueAttachmentService extends APIService {
uploadProgressHandler?: AxiosRequestConfig["onUploadProgress"]
): Promise<TIssueAttachment> {
if (isBeamModelFile(file.name)) {
const beamViewer = await uploadBeamModelFile(file, {
issueId,
onUploadProgress: uploadProgressHandler,
projectId,
workspaceSlug,
});
return this.createBeamIssueAttachmentReference(workspaceSlug, projectId, issueId, {
beamViewer,
name: file.name,
size: file.size,
type: getBeamModelMimeType(file),
});
const formData = new FormData();
formData.append("file", file);
return this.post(
`${this.attachmentBasePath(workspaceSlug, projectId, issueId)}/bim-upload/`,
formData,
{ onUploadProgress: uploadProgressHandler }
)
.then((response) => {
const createResponse: TIssueAttachmentCreateResponse = response?.data;
return createResponse.attachment;
})
.catch((error) => {
throw this.toBeamRequestError(error, "Не удалось загрузить модель через Ops.");
});
}
const fileMetaData = await getFileMetaDataForUpload(file);
@@ -278,15 +243,8 @@ export class IssueAttachmentService extends APIService {
projectId: string,
issueId: string,
assetId: string,
attachment?: TIssueAttachment
_attachment?: TIssueAttachment
): Promise<TIssueAttachment> {
const beamViewer = getBeamViewerAttachment(attachment);
if (beamViewer) {
await deleteBeamModelAsset(beamViewer).catch((error) => {
console.warn("BIM storage cleanup failed; deleting OPS attachment reference anyway.", error);
});
}
return this.delete(
`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/${this.serviceType}/${issueId}/attachments/${assetId}/`
)