feat(web): complete Blender route runtime execution
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-09-07 00:12:32 +08:00
parent e2e81c7c00
commit e9bfef0c5a
35 changed files with 57527 additions and 75 deletions

View File

@@ -1,5 +1,5 @@
import type { ErrorCode } from "./error";
import type { BlenderOperatorPropertyValue } from "./blender-operator";
import type { BlenderOperatorDescriptor, BlenderOperatorPropertyValue } from "./blender-operator";
export const SERVER_OPERATOR_JOB_SCHEMA = 1 as const;
export const SERVER_OPERATOR_JOB_ROUTE = "SERVER_NATIVE" as const;
@@ -14,6 +14,7 @@ export const SERVER_OPERATOR_JOB_BUDGET = {
maxTimeoutMs: 10 * 60 * 1000,
minMemoryBytes: 64 * 1024 * 1024,
maxMemoryBytes: 4 * 1024 * 1024 * 1024,
maxEnvelopeBytes: 512 * 1024 * 1024 + 1024 * 1024,
} as const;
export const SERVER_OPERATOR_JOB_STATUSES = ["QUEUED", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED"] as const;
export type ServerOperatorJobStatus = typeof SERVER_OPERATOR_JOB_STATUSES[number];
@@ -23,6 +24,13 @@ export interface ServerOperatorBuildIdentityIR {
version: string;
buildSha256: string;
}
export interface ServerOperatorSchemaResultIR {
schemaVersion: typeof SERVER_OPERATOR_JOB_SCHEMA;
route: typeof SERVER_OPERATOR_JOB_ROUTE;
operator: BlenderOperatorDescriptor;
blenderBuild: ServerOperatorBuildIdentityIR;
}
export interface ServerOperatorJobRequestIR {
schemaVersion: typeof SERVER_OPERATOR_JOB_SCHEMA;
route: typeof SERVER_OPERATOR_JOB_ROUTE;
@@ -52,11 +60,57 @@ export interface ServerOperatorJobResultIR {
propertiesSha256: string;
requestSha256: string;
resultSha256: string;
resultBlendSha256?: string;
resultBlendByteLength?: number;
resultBlendBase64?: string;
result?: unknown;
errorCode?: ErrorCode;
errorMessage?: string;
}
const SERVER_OPERATOR_ENVELOPE_MAGIC = "WBJ1";
const SERVER_OPERATOR_ENVELOPE_HEADER_BYTES = 16;
export function canonicalServerOperatorProperties(properties: Record<string, BlenderOperatorPropertyValue>): string {
return stableJSON(properties);
}
export function encodeServerOperatorJobEnvelope(request: ServerOperatorJobRequestIR, sourceBlend: ArrayBuffer | Uint8Array): ArrayBuffer {
const validated = parseServerOperatorJobRequest(request);
const source = sourceBlend instanceof Uint8Array ? sourceBlend : new Uint8Array(sourceBlend);
if (source.byteLength !== validated.sourceBlendByteLength) throw new ServerOperatorJobValidationError("source blend byte length does not match request");
const requestBytes = new TextEncoder().encode(JSON.stringify(validated));
const total = SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes.byteLength + source.byteLength;
if (total > SERVER_OPERATOR_JOB_BUDGET.maxEnvelopeBytes) throw new ServerOperatorJobValidationError("binary envelope exceeds the budget");
const envelope = new Uint8Array(total);
envelope.set(new TextEncoder().encode(SERVER_OPERATOR_ENVELOPE_MAGIC), 0);
const view = new DataView(envelope.buffer);
view.setUint32(4, SERVER_OPERATOR_JOB_SCHEMA, true);
view.setUint32(8, requestBytes.byteLength, true);
view.setUint32(12, source.byteLength, true);
envelope.set(requestBytes, SERVER_OPERATOR_ENVELOPE_HEADER_BYTES);
envelope.set(source, SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes.byteLength);
return envelope.buffer;
}
export function decodeServerOperatorJobEnvelope(value: ArrayBuffer | Uint8Array): { request: ServerOperatorJobRequestIR; sourceBlend: Uint8Array } {
const envelope = value instanceof Uint8Array ? value : new Uint8Array(value);
if (envelope.byteLength < SERVER_OPERATOR_ENVELOPE_HEADER_BYTES || new TextDecoder().decode(envelope.slice(0, 4)) !== SERVER_OPERATOR_ENVELOPE_MAGIC) {
throw new ServerOperatorJobValidationError("binary envelope header is invalid");
}
const view = new DataView(envelope.buffer, envelope.byteOffset, envelope.byteLength);
if (view.getUint32(4, true) !== SERVER_OPERATOR_JOB_SCHEMA) throw new ServerOperatorJobValidationError("binary envelope schema is invalid");
const requestBytes = view.getUint32(8, true);
const sourceBytes = view.getUint32(12, true);
if (requestBytes < 1 || sourceBytes < 1 || SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes + sourceBytes !== envelope.byteLength || envelope.byteLength > SERVER_OPERATOR_JOB_BUDGET.maxEnvelopeBytes) {
throw new ServerOperatorJobValidationError("binary envelope lengths are invalid");
}
const request = parseServerOperatorJobRequest(JSON.parse(new TextDecoder().decode(envelope.slice(SERVER_OPERATOR_ENVELOPE_HEADER_BYTES, SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes))));
const sourceBlend = envelope.slice(SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes);
if (sourceBlend.byteLength !== request.sourceBlendByteLength) throw new ServerOperatorJobValidationError("binary envelope source length is invalid");
return { request, sourceBlend };
}
export class ServerOperatorJobValidationError extends Error {
readonly code: ErrorCode;