feat(web): classify and route complete Blender inventory
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-06 21:10:31 +08:00
parent 66d3f4e0f4
commit e2e81c7c00
41 changed files with 116672 additions and 37 deletions

View File

@@ -40,6 +40,7 @@ import {
type LibraryAppendClosureIR,
} from "../../../protocol/library-main-append";
import { createWasmModuleLoader, type WasmModuleLoader } from "../engine-client/wasm-module-loader";
import { parseServerOperatorJobRequest } from "../../../protocol/server-operator-job";
type WasmModule = {
_malloc: (size: number) => number;
@@ -189,6 +190,73 @@ function validateOperatorInvocation(value: unknown): WebEngineOperatorInvocation
};
}
function validateMacroInvocation(value: unknown): void {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw report("INVALID_ARGUMENT", "invokeBlenderMacro requires an object params payload", true);
}
const params = value as Record<string, unknown>;
if (Object.keys(params).some((key) => !["macro", "properties", "baseRevision", "serverJob"].includes(key))) {
throw report("INVALID_ARGUMENT", "invokeBlenderMacro received an unknown parameter", true);
}
if (typeof params.macro !== "string" || params.macro.length === 0 || params.macro.length > 128) {
throw report("INVALID_ARGUMENT", "invokeBlenderMacro requires a bounded macro identifier", true);
}
if (params.properties !== undefined &&
(typeof params.properties !== "object" || params.properties === null || Array.isArray(params.properties) || !validOperatorJson(params.properties))) {
throw report("INVALID_ARGUMENT", "invokeBlenderMacro properties must be typed JSON", true);
}
if (params.serverJob !== undefined &&
(typeof params.serverJob !== "object" || params.serverJob === null || Array.isArray(params.serverJob) || !validOperatorJson(params.serverJob))) {
throw report("INVALID_ARGUMENT", "invokeBlenderMacro serverJob must be typed JSON", true);
}
if (params.baseRevision !== undefined && (!Number.isSafeInteger(params.baseRevision) || params.baseRevision < 0)) {
throw report("REVISION_CONFLICT", "invokeBlenderMacro baseRevision must be a non-negative integer", true);
}
}
function validateFeatureRouteInvocation(value: unknown): void {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw report("INVALID_ARGUMENT", "invokeFeatureRoute requires an object params payload", true);
}
const params = value as Record<string, unknown>;
if (Object.keys(params).some((key) => !["route", "properties", "baseRevision", "serverJob"].includes(key))) {
throw report("INVALID_ARGUMENT", "invokeFeatureRoute received an unknown parameter", true);
}
if (typeof params.route !== "object" || params.route === null || Array.isArray(params.route)) {
throw report("INVALID_ARGUMENT", "invokeFeatureRoute requires route metadata", true);
}
const route = params.route as Record<string, unknown>;
if (!["WEB_MAIN", "SERVER_NATIVE"].includes(route.route as string) || typeof route.identifier !== "string" || route.identifier.length === 0 || route.identifier.length > 256) {
throw report("INVALID_ARGUMENT", "invokeFeatureRoute only accepts typed native or server-job routes", true);
}
if (params.properties !== undefined && (typeof params.properties !== "object" || params.properties === null || Array.isArray(params.properties) || !validOperatorJson(params.properties))) {
throw report("INVALID_ARGUMENT", "invokeFeatureRoute properties must be typed JSON", true);
}
if (params.baseRevision !== undefined && (!Number.isSafeInteger(params.baseRevision) || params.baseRevision < 0)) {
throw report("REVISION_CONFLICT", "invokeFeatureRoute baseRevision must be a non-negative integer", true);
}
if (route.route === "SERVER_NATIVE") {
if (typeof params.serverJob !== "object" || params.serverJob === null || Array.isArray(params.serverJob)) {
throw report("INVALID_ARGUMENT", "SERVER_NATIVE routes require a typed serverJob payload", true);
}
try { parseServerOperatorJobRequest(params.serverJob); }
catch (error) { reportProtocolError(error, "INVALID_ARGUMENT"); }
}
}
function validateServerOperatorJobParams(command: string, value: unknown): void {
if (command === "submitServerOperatorJob") {
try { parseServerOperatorJobRequest(value); }
catch (error) { reportProtocolError(error, "INVALID_ARGUMENT"); }
return;
}
if (typeof value !== "object" || value === null || Array.isArray(value)) throw report("INVALID_ARGUMENT", `${command} requires an object params payload`, true);
const params = value as Record<string, unknown>;
if (Object.keys(params).some((key) => !["jobId", "requestSha256"].includes(key)) || typeof params.jobId !== "string" || params.jobId.length === 0 || params.jobId.length > 128 || typeof params.requestSha256 !== "string" || !/^[a-f0-9]{64}$/.test(params.requestSha256)) {
throw report("INVALID_ARGUMENT", `${command} requires a bounded jobId and requestSha256`, true);
}
}
function reportProtocolError(error: unknown, fallbackCode: ErrorReport["code"]): never {
if (typeof error === "object" && error !== null && "code" in error && typeof (error as { code?: unknown }).code === "string") {
const candidate = error as { code: ErrorReport["code"]; message?: unknown };
@@ -1299,14 +1367,26 @@ function callUnifiedJson<T>(cmd: string, params: Record<string, unknown> = {}, t
}
}
type WebEngineRouteJsonCommand = WebEngineJsonCommand | "importScene" | "exportScene";
const READ_ONLY_JSON_COMMANDS: ReadonlySet<WebEngineRouteJsonCommand> = new Set(["snapshot", "delta", "depsgraph", "memoryStats", "listBlenderOperators", "exportScene", "listRNADataBlocks", "readRNAProperties", "listRNACollections"]);
const STATEFUL_JSON_COMMANDS: ReadonlySet<WebEngineRouteJsonCommand> = new Set(["applyCommand", "undo", "redo", "invokeBlenderOperator", "importScene", "assetMark", "assetCatalog", "libraryLink", "libraryOverride", "libraryReload", "setRNAProperty"]);
type WebEngineRouteJsonCommand = WebEngineJsonCommand | "importScene" | "exportScene" | "submitServerOperatorJob" | "cancelServerOperatorJob" | "readServerOperatorResult";
const READ_ONLY_JSON_COMMANDS: ReadonlySet<WebEngineRouteJsonCommand> = new Set(["snapshot", "delta", "depsgraph", "memoryStats", "listBlenderOperators", "listBlenderMacros", "listFeatureRoutes", "exportScene", "listRNADataBlocks", "readRNAProperties", "listRNACollections", "readRNAPropertySchema", "readRNACollection", "submitServerOperatorJob", "cancelServerOperatorJob", "readServerOperatorResult"]);
const STATEFUL_JSON_COMMANDS: ReadonlySet<WebEngineRouteJsonCommand> = new Set(["applyCommand", "undo", "redo", "invokeBlenderOperator", "invokeBlenderMacro", "invokeFeatureRoute", "importScene", "assetMark", "assetCatalog", "libraryLink", "libraryOverride", "libraryReload", "setRNAProperty", "setRNAArrayProperty", "setRNAEnumProperty"]);
function validateRNAParams(command: WebEngineRouteJsonCommand, value: unknown): void {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw report("INVALID_ARGUMENT", `${command} requires an object params payload`, true);
const params = value as Record<string, unknown>;
const allowed = command === "listRNADataBlocks" ? ["type", "dataBlockType", "editableOnly", "limit"] : command === "setRNAProperty" ? ["type", "dataBlockType", "name", "dataBlockName", "property", "propertyIdentifier", "value", "baseRevision"] : ["type", "dataBlockType", "name", "dataBlockName", "limit"];
const readSchema = command === "readRNAPropertySchema";
const readCollection = command === "readRNACollection";
const arrayWrite = command === "setRNAArrayProperty";
const enumWrite = command === "setRNAEnumProperty";
const allowed = command === "listRNADataBlocks"
? ["type", "dataBlockType", "editableOnly", "limit"]
: command === "setRNAProperty" || arrayWrite || enumWrite
? ["type", "dataBlockType", "name", "dataBlockName", "property", "propertyIdentifier", "value", "baseRevision"]
: readSchema
? ["type", "dataBlockType", "name", "dataBlockName", "property", "propertyIdentifier"]
: readCollection
? ["type", "dataBlockType", "name", "dataBlockName", "property", "propertyIdentifier", "path", "limit"]
: ["type", "dataBlockType", "name", "dataBlockName", "limit"];
if (Object.keys(params).some((key) => !allowed.includes(key))) throw report("INVALID_ARGUMENT", `${command} received an unknown parameter`, true);
const type = params.type ?? params.dataBlockType;
const name = params.name ?? params.dataBlockName;
@@ -1316,7 +1396,7 @@ function validateRNAParams(command: WebEngineRouteJsonCommand, value: unknown):
if (property !== undefined && (typeof property !== "string" || property.length === 0 || property.length > 128)) throw report("INVALID_ARGUMENT", `${command} requires a bounded RNA property identifier`, true);
if (params.limit !== undefined && (!Number.isSafeInteger(params.limit) || (params.limit as number) < 1 || (params.limit as number) > 1024)) throw report("INVALID_ARGUMENT", `${command} limit is outside the bounded reflection budget`, true);
if (params.editableOnly !== undefined && typeof params.editableOnly !== "boolean") throw report("INVALID_ARGUMENT", `${command} editableOnly must be boolean`, true);
if (command === "setRNAProperty" && (property === undefined || !Object.prototype.hasOwnProperty.call(params, "value"))) throw report("INVALID_ARGUMENT", "setRNAProperty requires property and value", true);
if ((command === "setRNAProperty" || arrayWrite || enumWrite) && (property === undefined || !Object.prototype.hasOwnProperty.call(params, "value"))) throw report("INVALID_ARGUMENT", `${command} requires property and value`, true);
if (params.baseRevision !== undefined && (!Number.isSafeInteger(params.baseRevision) || (params.baseRevision as number) < 0)) throw report("REVISION_CONFLICT", "setRNAProperty baseRevision must be non-negative", true);
const valueIsSafe = (candidate: unknown, depth = 0): boolean => {
if (depth > 4 || candidate === null || typeof candidate === "string" || typeof candidate === "boolean") return candidate === null || typeof candidate !== "string" || candidate.length <= 4096;
@@ -1324,7 +1404,19 @@ function validateRNAParams(command: WebEngineRouteJsonCommand, value: unknown):
if (Array.isArray(candidate)) return candidate.length <= 64 && candidate.every((item) => valueIsSafe(item, depth + 1));
return false;
};
if (command === "setRNAProperty" && !valueIsSafe(params.value)) throw report("INVALID_ARGUMENT", "setRNAProperty value must be bounded scalar JSON", true);
if ((command === "setRNAProperty" || arrayWrite || enumWrite) && !valueIsSafe(params.value)) throw report("INVALID_ARGUMENT", `${command} value must be bounded JSON`, true);
if (readCollection) {
const path = params.path ?? property;
if (typeof path !== "string" || path.length === 0 || path.length > 256 || path.split(".").some((part) => !/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(part))) {
throw report("INVALID_ARGUMENT", "readRNACollection requires a bounded RNA collection path", true);
}
}
if (arrayWrite && (!Array.isArray(params.value) || params.value.length === 0 || params.value.length > 64)) {
throw report("INVALID_ARGUMENT", "setRNAArrayProperty requires a bounded non-empty array", true);
}
if (enumWrite && !(typeof params.value === "string" || (typeof params.value === "number" && Number.isSafeInteger(params.value)))) {
throw report("INVALID_ARGUMENT", "setRNAEnumProperty requires an enum identifier or integer value", true);
}
}
function readSnapshot(): ReturnType<typeof parseSceneSnapshotIR> {
@@ -1819,7 +1911,10 @@ async function handleRequest(event: MessageEvent<WebEngineRequest>): Promise<voi
throw report("INVALID_ARGUMENT", "cmd json applyCommand requires params.command", true);
}
if (commandName === "invokeBlenderOperator") validateOperatorInvocation(request.command.params);
if (["listRNADataBlocks", "readRNAProperties", "setRNAProperty", "listRNACollections"].includes(commandName)) validateRNAParams(commandName, request.command.params ?? {});
if (commandName === "invokeBlenderMacro") validateMacroInvocation(request.command.params);
if (commandName === "invokeFeatureRoute") validateFeatureRouteInvocation(request.command.params);
if (["submitServerOperatorJob", "cancelServerOperatorJob", "readServerOperatorResult"].includes(commandName)) validateServerOperatorJobParams(commandName, request.command.params ?? {});
if (["listRNADataBlocks", "readRNAProperties", "setRNAProperty", "listRNACollections", "readRNAPropertySchema", "setRNAArrayProperty", "setRNAEnumProperty", "readRNACollection"].includes(commandName)) validateRNAParams(commandName, request.command.params ?? {});
if (["assetMark", "assetCatalog", "libraryLink", "libraryOverride", "libraryReload"].includes(commandName)) {
try {
parseAssetLibraryCommand(commandName, request.command.params ?? {});