Files
ttstd-infinite-canvas/canvas-agent/src/dreamina-generation.ts

519 lines
28 KiB
TypeScript

import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import {
dreaminaCliInputSchema,
type DreaminaGenerationInput,
} from "./dreamina-cli-contract.js";
import type { DreaminaPublicGenerationTask } from "./dreamina-cli-runtime.js";
import type { DreaminaTaskContext } from "./dreamina-task-contract.js";
import type { DreaminaModelDescriptor, DreaminaModelOperation } from "./local-runtime-contract.js";
const MAX_REFERENCE_BYTES = 20 * 1024 * 1024;
const MAX_REFERENCE_IMAGES = 30;
const MAX_REFERENCE_VIDEOS = 10;
const MAX_REFERENCE_AUDIOS = 10;
const DEFAULT_CONCURRENCY = 5;
const MAX_RESULT_FILES = 4;
const MAX_RESULT_BYTES = 256 * 1024 * 1024;
const imageExtensions = new Map([[".png", "image/png"], [".jpg", "image/jpeg"], [".jpeg", "image/jpeg"], [".webp", "image/webp"]]);
const videoExtensions = new Map([[".mp4", "video/mp4"], [".mov", "video/quicktime"], [".webm", "video/webm"]]);
export type DreaminaGenerationRequest = {
idempotencyKey: string;
clientOperationId: string;
context: DreaminaTaskContext;
operation: DreaminaModelOperation;
model: string;
prompt: string;
settings: { aspect?: string; resolution?: string; duration?: number; count?: number };
references: unknown[];
};
export type DreaminaGenerationResult = {
mode: "image" | "video";
images?: Array<{ dataUrl: string; mimeType: string; bytes: number }>;
video?: { dataUrl: string; mimeType: string; bytes: number };
};
export class LocalDreaminaGenerationError extends Error {
constructor(readonly code: string, message: string, readonly statusCode = 500) {
super(message);
this.name = "LocalDreaminaGenerationError";
}
}
type DreaminaGenerationAdapterOptions = {
root: string;
models: readonly DreaminaModelDescriptor[];
coordinator?: DreaminaGenerationCoordinator;
runtime: {
generateToResult(input: DreaminaGenerationInput, options?: { signal?: AbortSignal; requestFingerprint?: string; clientOperationId?: string; taskContext?: DreaminaTaskContext }): Promise<DreaminaGenerationResult>;
enqueue?(input: DreaminaGenerationInput, options?: { signal?: AbortSignal; requestFingerprint?: string; clientOperationId?: string; taskContext?: DreaminaTaskContext }): Promise<DreaminaPublicGenerationTask>;
waitForTask?(idempotencyKey: string, mode: "image" | "video", options?: { signal?: AbortSignal }): Promise<DreaminaGenerationResult>;
};
};
type InFlightGeneration = {
fingerprint: string;
controller: AbortController;
promise: Promise<DreaminaGenerationResult>;
waiters: number;
};
// A bounded process-local queue prevents simultaneous CLI execution bursts without changing durable receipts.
export class DreaminaGenerationCoordinator {
private active = 0;
private readonly waiting: Array<() => void> = [];
constructor(private readonly limit = DEFAULT_CONCURRENCY) {
if (!Number.isInteger(limit) || limit < 1 || limit > 32) throw new Error("Dreamina generation concurrency is invalid");
}
async run<T>(action: () => Promise<T>, signal?: AbortSignal): Promise<T> {
throwIfCancelled(signal);
await this.acquire(signal);
try {
throwIfCancelled(signal);
return await action();
} finally {
this.active -= 1;
this.drain();
}
}
private acquire(signal?: AbortSignal): Promise<void> {
if (this.active < this.limit) {
this.active += 1;
return Promise.resolve();
}
return new Promise<void>((resolve, reject) => {
const start = () => {
signal?.removeEventListener("abort", cancel);
this.active += 1;
resolve();
};
const cancel = () => {
const index = this.waiting.indexOf(start);
if (index >= 0) this.waiting.splice(index, 1);
reject(cancelled());
};
signal?.addEventListener("abort", cancel, { once: true });
this.waiting.push(start);
});
}
private drain() {
while (this.active < this.limit && this.waiting.length) this.waiting.shift()!();
}
}
const processCoordinator = new DreaminaGenerationCoordinator();
export class DreaminaGenerationAdapter {
private readonly root: string;
private readonly models: DreaminaModelDescriptor[];
private readonly runtime: DreaminaGenerationAdapterOptions["runtime"];
private readonly coordinator: DreaminaGenerationCoordinator;
private readonly inFlight = new Map<string, InFlightGeneration>();
constructor(options: DreaminaGenerationAdapterOptions) {
if (!path.isAbsolute(options.root) || options.root.includes("\0")) throw new Error("Dreamina generation root is invalid");
this.root = path.resolve(options.root);
this.models = options.models.map((model) => ({ ...model, operations: [...model.operations], settings: { ...model.settings } }));
this.runtime = options.runtime;
this.coordinator = options.coordinator ?? processCoordinator;
}
run(value: unknown, options: { signal?: AbortSignal } = {}): Promise<DreaminaGenerationResult> {
try {
throwIfCancelled(options.signal);
const input = parseRequest(value);
const references = parseInlineReferences(input.references);
validateRequestAgainstCatalog(input, references, this.models);
const fingerprint = crypto.createHash("sha256").update(JSON.stringify(input)).digest("hex");
const existing = this.inFlight.get(input.idempotencyKey);
if (existing) {
if (existing.fingerprint !== fingerprint) return Promise.reject(idempotencyConflict());
return subscribe(existing, options.signal);
}
const controller = new AbortController();
const record: InFlightGeneration = {
fingerprint,
controller,
waiters: 0,
promise: this.runtime.enqueue && this.runtime.waitForTask
? this.execute(input, references, fingerprint, controller.signal)
: this.coordinator.run(() => this.execute(input, references, fingerprint, controller.signal), controller.signal),
};
this.inFlight.set(input.idempotencyKey, record);
void record.promise.finally(() => {
if (this.inFlight.get(input.idempotencyKey) === record) this.inFlight.delete(input.idempotencyKey);
}).catch(() => undefined);
return subscribe(record, options.signal);
} catch (error) {
return Promise.reject(error);
}
}
async submit(value: unknown, options: { signal?: AbortSignal } = {}): Promise<DreaminaPublicGenerationTask> {
throwIfCancelled(options.signal);
if (!this.runtime.enqueue) throw new LocalDreaminaGenerationError("local_generation_async_unavailable", "Dreamina async generation is unavailable", 503);
const input = parseRequest(value);
const references = parseInlineReferences(input.references);
validateRequestAgainstCatalog(input, references, this.models);
const fingerprint = crypto.createHash("sha256").update(JSON.stringify(input)).digest("hex");
await fs.mkdir(this.root, { recursive: true, mode: 0o700 });
const runRoot = await fs.mkdtemp(path.join(this.root, "run-"));
const inputRoot = path.join(runRoot, "input");
await fs.mkdir(inputRoot, { mode: 0o700 });
try {
const referencePaths = await writeInlineReferences(references, inputRoot);
return await this.runtime.enqueue(toDreaminaInput(input, referencePaths), {
signal: options.signal,
requestFingerprint: fingerprint,
clientOperationId: input.clientOperationId,
taskContext: input.context,
});
} finally {
await fs.rm(runRoot, { recursive: true, force: true });
}
}
private async execute(
input: DreaminaGenerationRequest,
references: InlineReference[],
requestFingerprint: string,
signal: AbortSignal,
) {
await fs.mkdir(this.root, { recursive: true, mode: 0o700 });
const runRoot = await fs.mkdtemp(path.join(this.root, "run-"));
const inputRoot = path.join(runRoot, "input");
await fs.mkdir(inputRoot, { mode: 0o700 });
try {
const referencePaths = await writeInlineReferences(references, inputRoot);
const cliInput = toDreaminaInput(input, referencePaths);
if (this.runtime.enqueue && this.runtime.waitForTask) {
await this.runtime.enqueue(cliInput, { signal, requestFingerprint, clientOperationId: input.clientOperationId, taskContext: input.context });
return await this.runtime.waitForTask(input.idempotencyKey, input.operation.endsWith("image") ? "image" : "video", { signal });
}
return await this.runtime.generateToResult(cliInput, { signal, requestFingerprint, clientOperationId: input.clientOperationId, taskContext: input.context });
} finally {
await fs.rm(runRoot, { recursive: true, force: true });
}
}
}
function subscribe(record: InFlightGeneration, signal?: AbortSignal): Promise<DreaminaGenerationResult> {
try { throwIfCancelled(signal); } catch (error) { return Promise.reject(error); }
record.waiters += 1;
return new Promise((resolve, reject) => {
let settled = false;
const finish = () => {
if (settled) return false;
settled = true;
signal?.removeEventListener("abort", cancel);
record.waiters -= 1;
return true;
};
const cancel = () => {
if (!finish()) return;
if (record.waiters === 0) record.controller.abort();
reject(cancelled());
};
signal?.addEventListener("abort", cancel, { once: true });
record.promise.then(
(result) => { if (finish()) resolve(cloneResult(result)); },
(error) => { if (finish()) reject(error); },
);
if (signal?.aborted) cancel();
});
}
function parseRequest(value: unknown): DreaminaGenerationRequest {
const source = record(value);
const allowed = new Set(["idempotencyKey", "clientOperationId", "context", "operation", "model", "prompt", "settings", "references"]);
if (Object.keys(source).some((key) => !allowed.has(key))) throw requestInvalid();
const settings = record(source.settings);
const settingKeys = new Set(["aspect", "resolution", "duration", "count"]);
if (Object.keys(settings).some((key) => !settingKeys.has(key))) throw requestInvalid();
const operation = safeString(source.operation, /^(?:text-to-image|image-to-image|text-to-video|image-to-video|reference-to-video)$/) as DreaminaModelOperation;
const duration = settings.duration === undefined ? undefined : boundedInteger(settings.duration, 1, 60);
const count = settings.count === undefined ? undefined : boundedInteger(settings.count, 1, 4);
if (operation.endsWith("video") && count !== undefined && count !== 1) throw requestInvalid();
if (operation.endsWith("image") && duration !== undefined) throw requestInvalid();
const idempotencyKey = safeString(source.idempotencyKey, /^[A-Za-z0-9._:-]{16,120}$/);
return {
idempotencyKey,
clientOperationId: source.clientOperationId === undefined
? idempotencyKey
: safeString(source.clientOperationId, /^[A-Za-z0-9._:-]{16,120}$/),
context: source.context === undefined ? { scope: "legacy_unscoped" } : parseTaskContext(source.context),
operation,
model: safeString(source.model, /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/),
prompt: nonBlankPrompt(source.prompt),
settings: {
...(settings.aspect === undefined ? {} : { aspect: safeString(settings.aspect, /^[1-9][0-9]{0,2}:[1-9][0-9]{0,2}$/) }),
...(settings.resolution === undefined ? {} : { resolution: safeString(settings.resolution, /^(?:auto|1k|2k|4k|480|720|1080|2160|480p|720p|1080p)$/) }),
...(duration === undefined ? {} : { duration }),
...(count === undefined ? {} : { count }),
},
references: Array.isArray(source.references) ? source.references : [],
};
}
type InlineReference =
| { kind: "image"; mimeType: "image/png" | "image/jpeg" | "image/webp"; contentBase64: string; metadata?: Record<string, string | number> }
| { kind: "video"; mimeType: "video/mp4" | "video/quicktime" | "video/webm"; contentBase64: string; metadata?: Record<string, string | number> }
| { kind: "audio"; mimeType: "audio/mpeg" | "audio/wav" | "audio/mp4" | "audio/aac" | "audio/flac"; contentBase64: string; metadata?: Record<string, string | number> };
function parseInlineReferences(value: unknown): InlineReference[] {
if (!Array.isArray(value) || value.length > MAX_REFERENCE_IMAGES + MAX_REFERENCE_VIDEOS + MAX_REFERENCE_AUDIOS) throw requestInvalid();
let bytes = 0;
const counts = { image: 0, video: 0, audio: 0 };
return value.map((item) => {
const source = record(item);
const kind = safeString(source.kind, /^(?:image|video|audio)$/) as InlineReference["kind"];
counts[kind] += 1;
if (counts.image > MAX_REFERENCE_IMAGES || counts.video > MAX_REFERENCE_VIDEOS || counts.audio > MAX_REFERENCE_AUDIOS) throw requestInvalid();
const allowedMimeTypes = kind === "image"
? ["image/png", "image/jpeg", "image/webp"]
: kind === "video"
? ["video/mp4", "video/quicktime", "video/webm"]
: ["audio/mpeg", "audio/wav", "audio/mp4", "audio/aac", "audio/flac"];
if (Object.keys(source).some((key) => !["kind", "mimeType", "contentBase64", "metadata"].includes(key))
|| !allowedMimeTypes.includes(String(source.mimeType))
|| typeof source.contentBase64 !== "string"
|| !/^[A-Za-z0-9+/]+={0,2}$/.test(source.contentBase64)) throw requestInvalid();
const buffer = Buffer.from(source.contentBase64, "base64");
if (!buffer.byteLength || buffer.toString("base64") !== source.contentBase64
|| !hasExpectedMediaSignature(buffer, String(source.mimeType))) throw requestInvalid();
bytes += buffer.byteLength;
if (bytes > MAX_REFERENCE_BYTES) throw requestInvalid();
const metadata = parseReferenceMetadata(source.metadata, kind);
return {
kind,
mimeType: source.mimeType,
contentBase64: source.contentBase64,
...(metadata ? { metadata } : {}),
} as InlineReference;
});
}
function validateRequestAgainstCatalog(
input: DreaminaGenerationRequest,
references: InlineReference[],
models: readonly DreaminaModelDescriptor[],
) {
const imageCount = references.filter((reference) => reference.kind === "image").length;
const referenceCount = references.length;
const model = models.find((candidate) => candidate.id === input.model);
const expectedModality = input.operation === "text-to-image" || input.operation === "image-to-image" ? "image" : "video";
if (!model || model.modality !== expectedModality || !model.operations.includes(input.operation)) throw modelUnavailable();
if (input.settings.aspect && !model.settings.aspects.includes(input.settings.aspect)) throw modelUnavailable();
if (imageCount > model.settings.maxReferenceImages) throw modelUnavailable();
if ((input.operation === "text-to-image" || input.operation === "text-to-video") && referenceCount !== 0) throw requestInvalid();
if (input.operation !== "text-to-image" && input.operation !== "text-to-video" && referenceCount < 1) throw requestInvalid();
if (input.settings.duration !== undefined && (
model.settings.minDuration === undefined
|| model.settings.maxDuration === undefined
|| input.settings.duration < model.settings.minDuration
|| input.settings.duration > model.settings.maxDuration
)) throw modelUnavailable();
}
type StagedInlineReferences = { images: string[]; videos: string[]; audios: string[] };
function toDreaminaInput(input: DreaminaGenerationRequest, referencePaths: StagedInlineReferences): DreaminaGenerationInput {
const common = { idempotencyKey: input.idempotencyKey, prompt: input.prompt, modelVersion: input.model };
const candidate: unknown = input.operation === "text-to-image"
? { operation: "text2image", ...common, ratio: input.settings.aspect, resolutionType: imageResolution(input.settings.resolution), generateNum: input.settings.count }
: input.operation === "image-to-image"
? { operation: "image2image", ...common, ratio: input.settings.aspect, resolutionType: imageResolution(input.settings.resolution), generateNum: input.settings.count, referenceImages: referencePaths.images }
: input.operation === "text-to-video"
? { operation: "text2video", ...common, ratio: input.settings.aspect, videoResolution: videoResolution(input.settings.resolution), duration: input.settings.duration }
: input.operation === "image-to-video" && referencePaths.images.length <= 2 && !referencePaths.videos.length && !referencePaths.audios.length
? { operation: referencePaths.images.length === 2 ? "frames2video" : "image2video", ...common, videoResolution: videoResolution(input.settings.resolution), duration: input.settings.duration, referenceImages: referencePaths.images }
: {
operation: "multimodal2video",
...common,
ratio: input.settings.aspect,
videoResolution: videoResolution(input.settings.resolution),
duration: input.settings.duration,
referenceImages: referencePaths.images,
referenceVideos: referencePaths.videos,
referenceAudios: referencePaths.audios,
};
const parsed = dreaminaCliInputSchema.safeParse(removeUndefined(candidate));
if (!parsed.success || parsed.data.operation === "query_result") throw requestInvalid();
return parsed.data;
}
function parseTaskContext(value: unknown): DreaminaTaskContext {
const context = record(value);
if (context.scope === "legacy_unscoped") {
if (Object.keys(context).some((key) => key !== "scope")) throw requestInvalid();
return { scope: "legacy_unscoped" };
}
if (context.scope !== "scoped") throw requestInvalid();
const allowed = ["scope", "projectId", "nodeId", "conversationId", "messageId", "batchIndex", "batchCount", "retryOf", "attemptGroupId"];
if (Object.keys(context).some((key) => !allowed.includes(key))) throw requestInvalid();
const result: Extract<DreaminaTaskContext, { scope: "scoped" }> = { scope: "scoped" };
for (const key of ["projectId", "nodeId", "conversationId", "messageId", "retryOf", "attemptGroupId"] as const) {
if (context[key] === undefined) continue;
result[key] = safeString(context[key], /^[^\u0000-\u001f\u007f]{1,200}$/);
}
if (context.batchIndex !== undefined) result.batchIndex = boundedInteger(context.batchIndex, 0, 1_000_000);
if (context.batchCount !== undefined) result.batchCount = boundedInteger(context.batchCount, 1, 1_000_000);
return result;
}
function parseReferenceMetadata(value: unknown, kind: InlineReference["kind"]) {
if (value === undefined) return undefined;
const metadata = record(value);
const allowed = kind === "image" ? ["name", "width", "height"]
: kind === "video" ? ["name", "width", "height", "durationMs"]
: ["name", "durationMs"];
if (Object.keys(metadata).some((key) => !allowed.includes(key))) throw requestInvalid();
const result: Record<string, string | number> = {};
if (metadata.name !== undefined) result.name = safeString(metadata.name, /^[^\u0000-\u001f\u007f]{1,200}$/);
for (const key of ["width", "height", "durationMs"]) {
if (metadata[key] !== undefined) result[key] = boundedInteger(metadata[key], 1, 2_147_483_647);
}
return result;
}
function imageResolution(value: string | undefined) {
return value === undefined || value === "auto" ? "2k" : value;
}
function videoResolution(value: string | undefined) {
if (value === undefined) return "720p";
return ({
"480": "480p",
"720": "720p",
"1080": "1080p",
"2160": "4k",
} as const)[value as "480" | "720" | "1080" | "2160"] ?? value;
}
async function writeInlineReferences(references: InlineReference[], root: string): Promise<StagedInlineReferences> {
const files: StagedInlineReferences = { images: [], videos: [], audios: [] };
const counters = { image: 0, video: 0, audio: 0 };
for (const reference of references) {
const index = ++counters[reference.kind];
const extension = referenceExtension(reference.mimeType);
const file = path.join(root, `${reference.kind}-${String(index).padStart(2, "0")}${extension}`);
await fs.writeFile(file, Buffer.from(reference.contentBase64, "base64"), { flag: "wx", mode: 0o600 });
files[reference.kind === "image" ? "images" : reference.kind === "video" ? "videos" : "audios"].push(file);
}
return files;
}
function referenceExtension(mimeType: InlineReference["mimeType"]) {
return ({
"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp",
"video/mp4": ".mp4", "video/quicktime": ".mov", "video/webm": ".webm",
"audio/mpeg": ".mp3", "audio/wav": ".wav", "audio/mp4": ".m4a", "audio/aac": ".aac", "audio/flac": ".flac",
} as const)[mimeType];
}
function cloneResult(result: DreaminaGenerationResult): DreaminaGenerationResult {
return result.mode === "image"
? { mode: "image", images: result.images?.map((image) => ({ ...image })) }
: { mode: "video", video: result.video ? { ...result.video } : undefined };
}
export async function collectDreaminaGenerationResult(root: string, mode: "image" | "video"): Promise<DreaminaGenerationResult> {
if (!path.isAbsolute(root) || root.includes("\0")) throw resultInvalid();
const resolvedRoot = path.resolve(root);
const extensions = mode === "image" ? imageExtensions : videoExtensions;
const files = (await collectRegularMediaFiles(resolvedRoot, resolvedRoot))
.filter((file) => extensions.has(path.extname(file).toLowerCase()));
if (!files.length || files.length > MAX_RESULT_FILES || (mode === "video" && files.length !== 1)) throw resultInvalid();
const media = await Promise.all(files.map(async (file) => {
const stat = await fs.stat(file);
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_RESULT_BYTES) throw resultInvalid();
const buffer = await fs.readFile(file);
const mimeType = extensions.get(path.extname(file).toLowerCase())!;
if (!hasExpectedMediaSignature(buffer, mimeType)) throw resultInvalid();
return { dataUrl: `data:${mimeType};base64,${buffer.toString("base64")}`, mimeType, bytes: buffer.byteLength };
}));
if (media.reduce((sum, item) => sum + item.bytes, 0) > MAX_RESULT_BYTES) throw resultInvalid();
return mode === "image" ? { mode, images: media } : { mode, video: media[0] };
}
async function collectRegularMediaFiles(root: string, directory: string, depth = 0): Promise<string[]> {
if (depth > 4) throw resultInvalid();
const entries = await fs.readdir(directory, { withFileTypes: true });
const result: string[] = [];
for (const entry of entries) {
if (entry.isSymbolicLink()) throw resultInvalid();
const child = safeChild(root, path.join(directory, entry.name));
if (entry.isDirectory()) result.push(...await collectRegularMediaFiles(root, child, depth + 1));
else if (entry.isFile()) result.push(child);
if (result.length > MAX_RESULT_FILES * 4) throw resultInvalid();
}
return result.sort();
}
function safeChild(root: string, child: string) {
const resolved = path.resolve(child);
const relative = path.relative(root, resolved);
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) throw resultInvalid();
return resolved;
}
function hasExpectedMediaSignature(buffer: Buffer, mimeType: string) {
if (mimeType === "image/png") return buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
if (mimeType === "image/jpeg") return buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
if (mimeType === "image/webp") return buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP";
if (mimeType === "video/mp4" || mimeType === "video/quicktime") return buffer.length >= 12 && buffer.toString("ascii", 4, 8) === "ftyp";
if (mimeType === "video/webm") return buffer.length >= 4 && buffer.subarray(0, 4).equals(Buffer.from([0x1a, 0x45, 0xdf, 0xa3]));
if (mimeType === "audio/mpeg") return buffer.length >= 3 && (buffer.toString("ascii", 0, 3) === "ID3" || (buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0));
if (mimeType === "audio/wav") return buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WAVE";
if (mimeType === "audio/mp4") return buffer.length >= 12 && buffer.toString("ascii", 4, 8) === "ftyp";
if (mimeType === "audio/aac") return buffer.length >= 2 && buffer[0] === 0xff && (buffer[1] & 0xf0) === 0xf0;
if (mimeType === "audio/flac") return buffer.length >= 4 && buffer.toString("ascii", 0, 4) === "fLaC";
return false;
}
function record(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw requestInvalid();
return value as Record<string, unknown>;
}
function safeString(value: unknown, pattern: RegExp) {
if (typeof value !== "string" || !pattern.test(value)) throw requestInvalid();
return value;
}
function nonBlankPrompt(value: unknown) {
const prompt = safeString(value, /^[\s\S]{1,20000}$/).trim();
if (!prompt) throw requestInvalid();
return prompt;
}
function boundedInteger(value: unknown, min: number, max: number) {
if (!Number.isInteger(value) || typeof value !== "number" || value < min || value > max) throw requestInvalid();
return value;
}
function removeUndefined(value: unknown): unknown {
if (Array.isArray(value)) return value.map(removeUndefined);
if (!value || typeof value !== "object") return value;
return Object.fromEntries(Object.entries(value).flatMap(([key, child]) => child === undefined ? [] : [[key, removeUndefined(child)]]));
}
function throwIfCancelled(signal?: AbortSignal) {
if (signal?.aborted) throw cancelled();
}
function cancelled() { return new LocalDreaminaGenerationError("local_generation_cancelled", "本机生成已取消", 499); }
function idempotencyConflict() { return new LocalDreaminaGenerationError("local_generation_idempotency_conflict", "同一幂等键不能用于不同本机生成请求", 409); }
function modelUnavailable() { return new LocalDreaminaGenerationError("local_generation_model_unavailable", "所选本机模型或操作不可用", 409); }
function requestInvalid() { return new LocalDreaminaGenerationError("local_generation_request_invalid", "本机生成请求无效", 400); }
export function unknownGenerationResult() { return new LocalDreaminaGenerationError("local_generation_unknown", "本机生成结果未确认,已禁止自动重试", 409); }
function resultInvalid() { return new LocalDreaminaGenerationError("local_generation_result_invalid", "本机生成结果无法识别", 502); }