Files
ttstd-infinite-canvas/web/src/services/local-dreamina-generation.ts

817 lines
42 KiB
TypeScript

import type { LocalRuntimeTransport } from "@/services/local-runtime";
import { LocalRuntimeClientError, type LocalRuntimeConnection } from "@/services/local-runtime-session";
import { getLocalRuntimeSessionClient } from "@/stores/use-local-runtime-store";
import type { GenerationTaskEffectClaim, GenerationTaskEffectResult, GenerationTaskEffectStore } from "@/services/generation-task-materializer";
const MAX_RESPONSE_BYTES = 256 * 1024 * 1024;
const MAX_REFERENCE_BYTES = 20 * 1024 * 1024;
const MAX_TASK_LIST_RESPONSE_BYTES = 1024 * 1024;
const MAX_EFFECT_RESPONSE_BYTES = 16 * 1024;
const GENERATION_EFFECT_CONSUMER_ID = "web-generation-materializer";
const MAX_TASK_LIST_ITEMS = 100;
export const LOCAL_DREAMINA_WAIT_STOPPED_CODE = "dreamina_local_wait_stopped";
export const LOCAL_DREAMINA_WAIT_STOPPED_MESSAGE = "仅停止本机等待,官方任务仍可能继续。";
export const LOCAL_DREAMINA_OFFICIAL_INCOMPLETE_CODE = "dreamina_official_incomplete";
export const LOCAL_DREAMINA_OFFICIAL_INCOMPLETE_MESSAGE = "官方任务未完成;可能已在官方取消或生成失败。";
export const LOCAL_DREAMINA_OFFICIAL_FAILED_CODE = "dreamina_official_failed";
export const LOCAL_DREAMINA_FAILED_OR_CANCELLED_MESSAGE = "任务未成功。当前 Dreamina CLI 无法可靠判断是官网取消还是生成失败。";
export const LOCAL_DREAMINA_OFFICIAL_FAILED_MESSAGE = LOCAL_DREAMINA_FAILED_OR_CANCELLED_MESSAGE;
export type LocalDreaminaTaskLifecycle = "QUEUED_LOCAL" | "SUBMITTING" | "SUBMISSION_UNCERTAIN" | "ACCEPTED" | "RUNNING" | "TERMINAL";
export type LocalDreaminaTerminalOutcome = "SUCCEEDED" | "REJECTED" | "FAILED" | "CANCELLED" | "FAILED_OR_CANCELLED";
export type LocalDreaminaTaskSyncState = "SYNC_OK" | "SYNC_RETRY_WAIT" | "SYNC_BLOCKED_ACCOUNT" | "SYNC_UNCERTAIN" | "SYNC_CONFLICT";
export type LocalDreaminaTaskResultState = "NOT_AVAILABLE" | "PENDING_MATERIALIZATION" | "MATERIALIZING" | "READY" | "FAILED_RETRYABLE" | "FAILED_PERMANENT";
export type LocalDreaminaTaskContext =
| {
scope: "scoped";
projectId?: string;
nodeId?: string;
conversationId?: string;
messageId?: string;
batchIndex?: number;
batchCount?: number;
retryOf?: string;
attemptGroupId?: string;
}
| { scope: "legacy_unscoped" };
export type LocalDreaminaTaskOutput = {
outputIndex: number;
mediaType: "image" | "video" | "audio";
providerArtifactRef?: string;
materializedAssetId?: string;
materializationErrorCode?: string;
};
export type LocalDreaminaProviderObservation = {
source: "submit_receipt" | "query_result" | "list_task";
observedAt: string;
accountBinding?: string;
fenceEpoch?: number;
status: "pending" | "processing" | "completed" | "failed" | "cancelled";
};
export type LocalDreaminaProviderCapability = {
adapterSupported: boolean;
cancelSupported: boolean;
pushStatusSupported: boolean;
statusConsistency: "eventual_polling";
accountEntitlement: "yes" | "no" | "unknown";
currentlyObservedAvailable: "yes" | "no" | "unknown";
references: { images: boolean; videos: boolean; audios: boolean; firstLastFrames: boolean };
};
export type LocalDreaminaReference =
| { kind?: "image"; mimeType: "image/png" | "image/jpeg" | "image/webp"; bytes: Uint8Array; metadata?: { name?: string; width?: number; height?: number } }
| { kind: "video"; mimeType: "video/mp4" | "video/quicktime" | "video/webm"; bytes: Uint8Array; metadata?: { name?: string; width?: number; height?: number; durationMs?: number } }
| { kind: "audio"; mimeType: "audio/mpeg" | "audio/wav" | "audio/mp4" | "audio/aac" | "audio/flac"; bytes: Uint8Array; metadata?: { name?: string; durationMs?: number } };
export type LocalDreaminaGenerationInput = {
model: `local:dreamina-cli:${string}`;
mode: "image" | "video";
prompt: string;
settings: { aspect?: string; resolution?: string; duration?: number; count?: number };
references: LocalDreaminaReference[];
resumeOnly?: boolean;
idempotencyKey?: string;
clientOperationId?: string;
context?: LocalDreaminaTaskContext;
};
export type LocalDreaminaGenerationResult = {
mode: "image" | "video";
images?: Array<{ dataUrl: string; mimeType: string; bytes: number }>;
video?: { dataUrl: string; mimeType: string; bytes: number };
};
export type LocalDreaminaGenerationTask = {
id: string;
clientOperationId?: string;
provider: "dreamina-cli";
mode: "image" | "video";
operation: string;
model: string;
status: "queued" | "running" | "succeeded" | "failed" | "cancelled";
stage: "queued" | "submitting" | "submitted" | "generating" | "succeeded" | "failed" | "cancelled" | "submission_unknown";
progress?: number;
receiptRecorded: boolean;
errorCode?: string;
officialStatus?: "pending" | "processing" | "completed" | "failed" | "cancelled";
lifecycle?: LocalDreaminaTaskLifecycle;
terminalOutcome?: LocalDreaminaTerminalOutcome;
syncState?: LocalDreaminaTaskSyncState;
resultState?: LocalDreaminaTaskResultState;
outputs?: LocalDreaminaTaskOutput[];
accountBinding?: string;
context?: LocalDreaminaTaskContext;
providerObservation?: LocalDreaminaProviderObservation;
createdAt: string;
updatedAt: string;
result?: LocalDreaminaGenerationResult;
};
export class LocalDreaminaGenerationClientError extends Error {
constructor(
readonly code: string,
message: string,
readonly status = 500,
) {
super(message);
this.name = "LocalDreaminaGenerationClientError";
}
}
type RuntimeClient = LocalRuntimeTransport & {
connect(signal?: AbortSignal): Promise<LocalRuntimeConnection>;
revokeLocalSession?(): void;
};
type Dependencies = {
client?: RuntimeClient;
idempotencyKey?: () => string;
onTaskUpdate?: (task: LocalDreaminaGenerationTask) => void;
};
type ParsedInput = Omit<LocalDreaminaGenerationInput, "model"> & { model: string };
export function createLocalDreaminaTaskEffectStore(dependencies: Pick<Dependencies, "client"> = {}): GenerationTaskEffectStore {
const client = dependencies.client ?? getLocalRuntimeSessionClient();
type EffectLease = {
taskId: string;
effectKey: string;
leaseToken: string;
expiresAt: string;
fence: number;
};
const leases = new Map<string, EffectLease>();
const leaseKey = (taskId: string, effectKey: string) => `${taskId}\0${effectKey}`;
const call = async (path: string, body: Record<string, unknown>) => {
const request = async () => {
await requireConnection(client);
const response = await client.request(path, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
return { response, value: await readBoundedJson(response, MAX_EFFECT_RESPONSE_BYTES) };
};
let result = await request();
if (result.response.status === 401 || result.response.status === 403) {
client.revokeLocalSession?.();
result = await request();
}
if (!result.response.ok) throw runtimeError(result.response.status, result.value);
const envelope = record(result.value);
const value = record(envelope?.result);
if (envelope?.ok !== true || !value) {
throw new LocalDreaminaGenerationClientError("local_runtime_response_invalid", "本机任务副作用响应无效", 502);
}
return value;
};
return {
async claim(effectKey, taskId): Promise<GenerationTaskEffectClaim> {
const value = await call("/dreamina/generate/effects/claim", {
consumerId: GENERATION_EFFECT_CONSUMER_ID,
taskId,
effectKey,
});
if (value.status === "busy") {
if (typeof value.retryAt !== "string" || !Number.isFinite(Date.parse(value.retryAt))) {
throw new LocalDreaminaGenerationClientError("local_runtime_response_invalid", "本机任务副作用响应无效", 502);
}
return { status: "busy", retryAt: value.retryAt };
}
if (value.status === "completed") {
const result = parseEffectResult(value.result);
return { status: "completed", result };
}
if (value.status !== "claimed" || typeof value.leaseToken !== "string" || typeof value.leaseExpiresAt !== "string" || !Number.isFinite(Date.parse(value.leaseExpiresAt)) || !Number.isSafeInteger(value.fence) || (value.fence as number) < 1) {
throw new LocalDreaminaGenerationClientError("local_runtime_response_invalid", "本机任务副作用响应无效", 502);
}
const lease = {
taskId,
effectKey,
leaseToken: value.leaseToken,
expiresAt: value.leaseExpiresAt,
fence: value.fence as number,
};
leases.set(leaseKey(taskId, effectKey), lease);
return { status: "claimed", fence: lease.fence };
},
async renew(effectKey, taskId) {
const key = leaseKey(taskId, effectKey);
const lease = leases.get(key);
if (!lease) throw new LocalDreaminaGenerationClientError("local_runtime_effect_lease_missing", "本机任务副作用租约缺失", 409);
const value = await call("/dreamina/generate/effects/renew", {
consumerId: GENERATION_EFFECT_CONSUMER_ID,
taskId: lease.taskId,
effectKey: lease.effectKey,
leaseToken: lease.leaseToken,
fence: lease.fence,
});
if (typeof value.leaseExpiresAt !== "string" || !Number.isFinite(Date.parse(value.leaseExpiresAt)) || value.fence !== lease.fence) {
throw new LocalDreaminaGenerationClientError("local_runtime_effect_lease_lost", "本机任务副作用租约已失效", 409);
}
lease.expiresAt = value.leaseExpiresAt;
return { fence: lease.fence };
},
async complete(effectKey, taskId, result) {
const key = leaseKey(taskId, effectKey);
const lease = leases.get(key);
if (!lease) throw new LocalDreaminaGenerationClientError("local_runtime_effect_lease_missing", "本机任务副作用租约缺失", 409);
const value = await call("/dreamina/generate/effects/complete", {
consumerId: GENERATION_EFFECT_CONSUMER_ID,
taskId: lease.taskId,
effectKey: lease.effectKey,
leaseToken: lease.leaseToken,
fence: lease.fence,
result,
});
if (value.completed !== true) throw new LocalDreaminaGenerationClientError("local_runtime_effect_lease_lost", "本机任务副作用租约已失效", 409);
leases.delete(key);
},
async release(effectKey, taskId) {
const key = leaseKey(taskId, effectKey);
const lease = leases.get(key);
if (!lease) throw new LocalDreaminaGenerationClientError("local_runtime_effect_lease_missing", "本机任务副作用租约缺失", 409);
const value = await call("/dreamina/generate/effects/release", {
consumerId: GENERATION_EFFECT_CONSUMER_ID,
taskId: lease.taskId,
effectKey: lease.effectKey,
leaseToken: lease.leaseToken,
fence: lease.fence,
});
if (value.released !== true) {
throw new LocalDreaminaGenerationClientError("local_runtime_effect_lease_lost", "本机任务副作用租约已失效", 409);
}
leases.delete(key);
},
};
}
function parseEffectResult(value: unknown): GenerationTaskEffectResult {
const result = record(value);
if (!result || Object.keys(result).some((key) => key !== "materializedAssetId") || (result.materializedAssetId !== undefined && typeof result.materializedAssetId !== "string")) {
throw new LocalDreaminaGenerationClientError("local_runtime_response_invalid", "本机任务副作用响应无效", 502);
}
return typeof result.materializedAssetId === "string" ? { materializedAssetId: result.materializedAssetId } : {};
}
// Local CLI work goes straight to the signed Runtime; it must never create a backend task.
export async function runLocalDreaminaGenerationTask(input: LocalDreaminaGenerationInput, dependencies: Dependencies = {}, signal?: AbortSignal): Promise<LocalDreaminaGenerationResult> {
const parsed = parseInput(input);
const idempotencyKey = parsed.idempotencyKey ?? dependencies.idempotencyKey?.() ?? crypto.randomUUID();
validateTaskIdentity(idempotencyKey, parsed.mode);
const client = dependencies.client ?? getLocalRuntimeSessionClient();
let task = parsed.resumeOnly ? await queryLocalDreaminaGenerationTask(idempotencyKey, parsed.mode, { client }, signal) : await submitParsedTask(parsed, idempotencyKey, client, signal);
dependencies.onTaskUpdate?.(task);
if (task.status === "queued" || task.status === "running") {
task = await waitForLocalDreaminaGenerationTask(idempotencyKey, parsed.mode, { client }, signal);
dependencies.onTaskUpdate?.(task);
}
if (task.status === "succeeded" && task.result) return task.result;
if (task.status === "cancelled") {
if (task.errorCode === LOCAL_DREAMINA_WAIT_STOPPED_CODE) {
throw new LocalDreaminaGenerationClientError(LOCAL_DREAMINA_WAIT_STOPPED_CODE, LOCAL_DREAMINA_WAIT_STOPPED_MESSAGE, 409);
}
throw new DOMException("Aborted", "AbortError");
}
if (task.errorCode === LOCAL_DREAMINA_OFFICIAL_INCOMPLETE_CODE) {
throw new LocalDreaminaGenerationClientError(task.errorCode, LOCAL_DREAMINA_OFFICIAL_INCOMPLETE_MESSAGE, 502);
}
if (task.errorCode === LOCAL_DREAMINA_OFFICIAL_FAILED_CODE) {
throw new LocalDreaminaGenerationClientError(task.errorCode, LOCAL_DREAMINA_OFFICIAL_FAILED_MESSAGE, 502);
}
throw new LocalDreaminaGenerationClientError(task.errorCode ?? "local_generation_unknown", "本机即梦生成未完成", 502);
}
export async function queryLocalDreaminaGenerationTask(idempotencyKey: string, mode: "image" | "video" | undefined, dependencies: Pick<Dependencies, "client"> = {}, signal?: AbortSignal) {
validateTaskIdentity(idempotencyKey, mode);
return queryWithOneReconnect(dependencies.client ?? getLocalRuntimeSessionClient(), idempotencyKey, mode, signal);
}
export async function waitForLocalDreaminaGenerationTask(idempotencyKey: string, mode: "image" | "video" | undefined, dependencies: Pick<Dependencies, "client"> = {}, signal?: AbortSignal) {
validateTaskIdentity(idempotencyKey, mode);
const client = dependencies.client ?? getLocalRuntimeSessionClient();
const request = async () => {
await requireConnection(client, signal);
const response = await client.request("/dreamina/generate/wait", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ idempotencyKey, ...(mode ? { mode } : {}) }),
signal,
});
return { response, value: await readBoundedJson(response) };
};
let result = await request();
if (result.response.status === 401 || result.response.status === 403) {
client.revokeLocalSession?.();
result = await request();
}
if (!result.response.ok) throw runtimeError(result.response.status, result.value);
return parseTask(result.value, mode);
}
export async function listLocalDreaminaGenerationTaskPage(page: { limit: number; cursor?: string; projectId?: string; activeOnly?: boolean }, dependencies: Pick<Dependencies, "client"> = {}, signal?: AbortSignal) {
const client = dependencies.client ?? getLocalRuntimeSessionClient();
const params = new URLSearchParams({ limit: String(Math.max(1, Math.min(100, Math.trunc(page.limit)))) });
if (page.cursor) params.set("cursor", page.cursor);
if (page.projectId) params.set("projectId", page.projectId);
if (page.activeOnly) params.set("activeOnly", "true");
const request = async () => {
await requireConnection(client, signal);
const response = await client.request(`/dreamina/generate/tasks?${params.toString()}`, { method: "GET", signal });
return { response, value: await readBoundedJson(response, MAX_TASK_LIST_RESPONSE_BYTES) };
};
let result = await request();
if (result.response.status === 401 || result.response.status === 403) {
client.revokeLocalSession?.();
result = await request();
}
if (!result.response.ok) throw runtimeError(result.response.status, result.value);
return parseTaskPage(result.value);
}
export async function listLocalDreaminaGenerationTasks(dependencies: Pick<Dependencies, "client"> = {}, signal?: AbortSignal) {
const tasks: LocalDreaminaGenerationTask[] = [];
const seenCursors = new Set<string>();
let cursor: string | undefined;
do {
const page = await listLocalDreaminaGenerationTaskPage({ limit: 100, ...(cursor ? { cursor } : {}) }, dependencies, signal);
tasks.push(...page.tasks);
if (!page.nextCursor || seenCursors.has(page.nextCursor)) break;
seenCursors.add(page.nextCursor);
cursor = page.nextCursor;
} while (true);
return tasks;
}
export async function cancelLocalDreaminaGenerationTask(idempotencyKey: string, dependencies: Pick<Dependencies, "client"> = {}, signal?: AbortSignal) {
validateTaskIdentity(idempotencyKey, "image");
const client = dependencies.client ?? getLocalRuntimeSessionClient();
await requireConnection(client, signal);
const response = await client.request("/dreamina/generate/cancel", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ idempotencyKey }),
signal,
});
const value = await readBoundedJson(response);
if (!response.ok) throw runtimeError(response.status, value);
return parseTask(value);
}
export async function deleteLocalDreaminaGenerationTask(idempotencyKey: string, dependencies: Pick<Dependencies, "client"> = {}, signal?: AbortSignal) {
validateTaskIdentity(idempotencyKey, "image");
const client = dependencies.client ?? getLocalRuntimeSessionClient();
await requireConnection(client, signal);
const response = await client.request("/dreamina/generate/delete", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ idempotencyKey }),
signal,
});
const value = await readBoundedJson(response);
if (!response.ok) throw runtimeError(response.status, value);
const envelope = record(value);
const result = envelope?.ok === true ? record(envelope.result) : undefined;
if (!result || typeof result !== "object" || Array.isArray(result) || (result as { deleted?: unknown }).deleted !== true) {
throw new LocalDreaminaGenerationClientError("dreamina_response_invalid", "本机即梦任务响应无效", 502);
}
return { deleted: true as const };
}
export async function refreshLocalDreaminaGenerationTask(idempotencyKey: string, dependencies: Pick<Dependencies, "client"> = {}, signal?: AbortSignal) {
validateTaskIdentity(idempotencyKey, "image");
const client = dependencies.client ?? getLocalRuntimeSessionClient();
await requireConnection(client, signal);
const response = await client.request("/dreamina/generate/refresh", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ idempotencyKey }),
signal,
});
const value = await readBoundedJson(response);
if (!response.ok) throw runtimeError(response.status, value);
return parseTask(value);
}
async function submitParsedTask(parsed: ParsedInput, idempotencyKey: string, client: RuntimeClient, signal?: AbortSignal) {
// Mutation retries are forbidden: establish the side-effect-free session before the only paid POST.
await requireConnection(client, signal);
let response: Response;
let value: unknown;
try {
response = await client.request("/dreamina/generate", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
idempotencyKey,
clientOperationId: parsed.clientOperationId ?? idempotencyKey,
operation: operationFor(parsed.mode, parsed.references),
model: parsed.model,
prompt: parsed.prompt,
settings: parsed.settings,
references: parsed.references.map((reference) => ({
kind: reference.kind ?? "image",
mimeType: reference.mimeType,
contentBase64: bytesToBase64(reference.bytes),
...(reference.metadata ? { metadata: reference.metadata } : {}),
})),
context: parsed.context ?? { scope: "scoped" },
}),
signal,
});
value = await readBoundedJson(response);
} catch (error) {
if (signal?.aborted || (error instanceof Error && error.name === "AbortError")) throw submissionUnknown();
throw error;
}
if (!response.ok) throw runtimeError(response.status, value);
return parseTask(value, parsed.mode);
}
async function queryWithOneReconnect(client: RuntimeClient, idempotencyKey: string, mode: "image" | "video" | undefined, signal?: AbortSignal) {
const request = async () => {
await requireConnection(client, signal);
const response = await client.request("/dreamina/generate/query", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ idempotencyKey, ...(mode ? { mode } : {}) }),
signal,
});
return { response, value: await readBoundedJson(response) };
};
let result = await request();
if (result.response.status === 401 || result.response.status === 403) {
client.revokeLocalSession?.();
result = await request();
}
if (!result.response.ok) throw runtimeError(result.response.status, result.value);
return parseTask(result.value, mode);
}
async function requireConnection(client: RuntimeClient, signal?: AbortSignal) {
const connection = await client.connect(signal);
if (connection.state !== "connected") throw new LocalRuntimeClientError("origin_not_trusted", "本机连接需要重新建立", 403);
}
function parseInput(value: LocalDreaminaGenerationInput): ParsedInput {
if (
!value ||
typeof value !== "object" ||
!/^local:dreamina-cli:[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/.test(value.model) ||
(value.mode !== "image" && value.mode !== "video") ||
typeof value.prompt !== "string" ||
!value.prompt.trim() ||
value.prompt.length > 20_000 ||
!Array.isArray(value.references) ||
!validReferenceCounts(value.model, value.references) ||
(value.idempotencyKey !== undefined && !/^[A-Za-z0-9._:-]{16,120}$/.test(value.idempotencyKey)) ||
(value.clientOperationId !== undefined && !/^[A-Za-z0-9._:-]{16,120}$/.test(value.clientOperationId))
)
throw invalidRequest();
const model = value.model.slice("local:dreamina-cli:".length);
const settings = value.settings ?? {};
if (!settings || typeof settings !== "object" || Array.isArray(settings) || Object.keys(settings).some((key) => !["aspect", "resolution", "duration", "count"].includes(key))) throw invalidRequest();
if (settings.duration !== undefined && (!Number.isInteger(settings.duration) || settings.duration < 1 || settings.duration > 60)) throw invalidRequest();
if (settings.count !== undefined && (!Number.isInteger(settings.count) || settings.count < 1 || settings.count > 4)) throw invalidRequest();
if (value.mode === "video" && settings.count !== undefined && settings.count !== 1) throw invalidRequest();
if (value.mode === "image" && settings.duration !== undefined) throw invalidRequest();
if (model === "seedance2.0mini" && ((settings.duration ?? 4) < 4 || (settings.duration ?? 4) > 15)) throw unavailable();
if (value.context !== undefined) parseInputContext(value.context);
let bytes = 0;
for (const reference of value.references) {
const kind = reference?.kind ?? "image";
const validMime =
kind === "image"
? ["image/png", "image/jpeg", "image/webp"].includes(reference?.mimeType)
: kind === "video"
? ["video/mp4", "video/quicktime", "video/webm"].includes(reference?.mimeType)
: kind === "audio" && ["audio/mpeg", "audio/wav", "audio/mp4", "audio/aac", "audio/flac"].includes(reference?.mimeType);
if (!reference || !validMime || !(reference.bytes instanceof Uint8Array) || !reference.bytes.byteLength || !safeReferenceMetadata(reference.metadata, kind)) throw invalidRequest();
bytes += reference.bytes.byteLength;
if (bytes > MAX_REFERENCE_BYTES) throw invalidRequest();
}
return { ...value, model, prompt: value.prompt.trim(), settings };
}
function validReferenceCounts(model: string, references: LocalDreaminaReference[]) {
if (model !== "local:dreamina-cli:seedance2.5") return references.length <= 30;
if (references.length > 50) return false;
const counts = { image: 0, video: 0, audio: 0 };
for (const reference of references) {
const kind = reference?.kind ?? "image";
if (kind === "image" || kind === "video" || kind === "audio") counts[kind] += 1;
}
return counts.image <= 30 && counts.video <= 10 && counts.audio <= 10;
}
function operationFor(mode: "image" | "video", references: LocalDreaminaReference[]) {
if (mode === "image") return references.length ? "image-to-image" : "text-to-image";
const imageCount = references.filter((reference) => (reference.kind ?? "image") === "image").length;
const hasMultimodal = references.some((reference) => reference.kind === "video" || reference.kind === "audio");
if (!references.length) return "text-to-video";
return hasMultimodal || imageCount > 2 ? "reference-to-video" : "image-to-video";
}
function parseInputContext(value: LocalDreaminaTaskContext) {
const context = value as unknown as Record<string, unknown>;
if (!context || (context.scope !== "scoped" && context.scope !== "legacy_unscoped")) throw invalidRequest();
if (context.scope === "legacy_unscoped") {
if (Object.keys(context).some((key) => key !== "scope")) throw invalidRequest();
return;
}
const allowed = ["scope", "projectId", "nodeId", "conversationId", "messageId", "batchIndex", "batchCount", "retryOf", "attemptGroupId"];
if (Object.keys(context).some((key) => !allowed.includes(key))) throw invalidRequest();
for (const key of ["projectId", "nodeId", "conversationId", "messageId", "retryOf", "attemptGroupId"]) {
if (context[key] !== undefined && !safeCorrelationId(context[key])) throw invalidRequest();
}
if (context.batchIndex !== undefined && (!Number.isSafeInteger(context.batchIndex) || (context.batchIndex as number) < 0)) throw invalidRequest();
if (context.batchCount !== undefined && (!Number.isSafeInteger(context.batchCount) || (context.batchCount as number) < 1)) throw invalidRequest();
}
function safeReferenceMetadata(value: unknown, kind: "image" | "video" | "audio") {
if (value === undefined) return true;
const metadata = record(value);
if (!metadata) return false;
const allowed = kind === "image" ? ["name", "width", "height"] : kind === "video" ? ["name", "width", "height", "durationMs"] : ["name", "durationMs"];
if (Object.keys(metadata).some((key) => !allowed.includes(key))) return false;
if (metadata.name !== undefined && (typeof metadata.name !== "string" || metadata.name.length > 200 || /[\u0000-\u001f\u007f]/.test(metadata.name))) return false;
for (const key of ["width", "height", "durationMs"]) {
if (metadata[key] !== undefined && (!Number.isFinite(metadata[key]) || (metadata[key] as number) <= 0)) return false;
}
return true;
}
async function readBoundedJson(response: Response, maxBytes = MAX_RESPONSE_BYTES): Promise<unknown> {
const declared = Number(response.headers.get("content-length"));
if (Number.isFinite(declared) && declared > maxBytes) throw invalidResponse();
if (!response.body) throw invalidResponse();
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
try {
while (true) {
const item = await reader.read();
if (item.done) break;
total += item.value.byteLength;
if (total > maxBytes) {
await reader.cancel();
throw invalidResponse();
}
chunks.push(item.value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
try {
return JSON.parse(new TextDecoder().decode(bytes)) as unknown;
} catch {
throw invalidResponse();
}
}
function parseTask(value: unknown, expectedMode?: "image" | "video", allowMissingResult = false): LocalDreaminaGenerationTask {
const envelope = record(value);
const source = envelope?.ok === true ? record(envelope.result) : undefined;
const status = source?.status;
const stage = source?.stage;
if (
!source ||
typeof source.id !== "string" ||
!/^[A-Za-z0-9._:-]{8,160}$/.test(source.id) ||
source.provider !== "dreamina-cli" ||
(source.mode !== "image" && source.mode !== "video") ||
(expectedMode && source.mode !== expectedMode) ||
typeof source.operation !== "string" ||
typeof source.model !== "string" ||
!["queued", "running", "succeeded", "failed", "cancelled"].includes(String(status)) ||
!["queued", "submitting", "submitted", "generating", "succeeded", "failed", "cancelled", "submission_unknown"].includes(String(stage)) ||
typeof source.receiptRecorded !== "boolean" ||
typeof source.createdAt !== "string" ||
!Number.isFinite(Date.parse(source.createdAt)) ||
typeof source.updatedAt !== "string" ||
!Number.isFinite(Date.parse(source.updatedAt))
)
throw invalidResponse();
const result = source.result === undefined ? undefined : parseResult(source.result, source.mode);
if (status === "succeeded" && !result && !allowMissingResult) throw invalidResponse();
return {
id: source.id,
...(typeof source.clientOperationId === "string" && /^[A-Za-z0-9._:-]{16,120}$/.test(source.clientOperationId) ? { clientOperationId: source.clientOperationId } : {}),
provider: "dreamina-cli",
mode: source.mode,
operation: source.operation,
model: source.model,
status: status as LocalDreaminaGenerationTask["status"],
stage: stage as LocalDreaminaGenerationTask["stage"],
...(typeof source.progress === "number" && source.progress >= 0 && source.progress <= 100 ? { progress: source.progress } : {}),
receiptRecorded: source.receiptRecorded,
...(typeof source.errorCode === "string" ? { errorCode: source.errorCode } : {}),
...(typeof source.officialStatus === "string" && ["pending", "processing", "completed", "failed", "cancelled"].includes(source.officialStatus) ? { officialStatus: source.officialStatus as LocalDreaminaGenerationTask["officialStatus"] } : {}),
...parseTaskContract(source),
createdAt: source.createdAt,
updatedAt: source.updatedAt,
...(result ? { result } : {}),
};
}
function parseTaskContract(source: Record<string, unknown>): Partial<LocalDreaminaGenerationTask> {
const contractKeys = ["lifecycle", "terminalOutcome", "syncState", "resultState", "outputs", "accountBinding", "context", "providerObservation"];
if (!contractKeys.some((key) => source[key] !== undefined)) return {};
const lifecycle = source.lifecycle;
const terminalOutcome = source.terminalOutcome;
const syncState = source.syncState;
const resultState = source.resultState;
if (
!["QUEUED_LOCAL", "SUBMITTING", "SUBMISSION_UNCERTAIN", "ACCEPTED", "RUNNING", "TERMINAL"].includes(String(lifecycle)) ||
!["SYNC_OK", "SYNC_RETRY_WAIT", "SYNC_BLOCKED_ACCOUNT", "SYNC_UNCERTAIN", "SYNC_CONFLICT"].includes(String(syncState)) ||
!["NOT_AVAILABLE", "PENDING_MATERIALIZATION", "MATERIALIZING", "READY", "FAILED_RETRYABLE", "FAILED_PERMANENT"].includes(String(resultState)) ||
(lifecycle === "TERMINAL") !== (typeof terminalOutcome === "string") ||
(terminalOutcome !== undefined && !["SUCCEEDED", "REJECTED", "FAILED", "CANCELLED", "FAILED_OR_CANCELLED"].includes(String(terminalOutcome)))
) {
throw invalidResponse();
}
const accountBinding = source.accountBinding === undefined ? undefined : parseAccountBinding(source.accountBinding);
const context = parseTaskContext(source.context);
const outputs = parseTaskOutputs(source.outputs);
const providerObservation = source.providerObservation === undefined ? undefined : parseProviderObservation(source.providerObservation);
return {
lifecycle: lifecycle as LocalDreaminaTaskLifecycle,
...(terminalOutcome === undefined ? {} : { terminalOutcome: terminalOutcome as LocalDreaminaTerminalOutcome }),
syncState: syncState as LocalDreaminaTaskSyncState,
resultState: resultState as LocalDreaminaTaskResultState,
outputs,
...(accountBinding ? { accountBinding } : {}),
context,
...(providerObservation ? { providerObservation } : {}),
};
}
function parseTaskContext(value: unknown): LocalDreaminaTaskContext {
const context = record(value);
if (!context || (context.scope !== "scoped" && context.scope !== "legacy_unscoped")) throw invalidResponse();
if (context.scope === "legacy_unscoped") {
if (Object.keys(context).some((key) => key !== "scope")) throw invalidResponse();
return { scope: "legacy_unscoped" };
}
const allowed = ["scope", "projectId", "nodeId", "conversationId", "messageId", "batchIndex", "batchCount", "retryOf", "attemptGroupId"];
if (Object.keys(context).some((key) => !allowed.includes(key))) throw invalidResponse();
const parsed: Extract<LocalDreaminaTaskContext, { scope: "scoped" }> = { scope: "scoped" };
for (const key of ["projectId", "nodeId", "conversationId", "messageId", "retryOf", "attemptGroupId"] as const) {
if (context[key] === undefined) continue;
if (!safeCorrelationId(context[key])) throw invalidResponse();
parsed[key] = context[key] as string;
}
if (context.batchIndex !== undefined) {
if (!Number.isSafeInteger(context.batchIndex) || (context.batchIndex as number) < 0) throw invalidResponse();
parsed.batchIndex = context.batchIndex as number;
}
if (context.batchCount !== undefined) {
if (!Number.isSafeInteger(context.batchCount) || (context.batchCount as number) < 1) throw invalidResponse();
parsed.batchCount = context.batchCount as number;
}
return parsed;
}
function parseTaskOutputs(value: unknown): LocalDreaminaTaskOutput[] {
if (!Array.isArray(value) || value.length > 32) throw invalidResponse();
const indexes = new Set<number>();
return value.map((candidate) => {
const output = record(candidate);
if (
!output ||
!Number.isSafeInteger(output.outputIndex) ||
(output.outputIndex as number) < 0 ||
(output.outputIndex as number) > 999 ||
indexes.has(output.outputIndex as number) ||
!["image", "video", "audio"].includes(String(output.mediaType)) ||
(output.providerArtifactRef !== undefined && !safeOpaqueRef(output.providerArtifactRef)) ||
(output.materializedAssetId !== undefined && !safeOpaqueRef(output.materializedAssetId)) ||
(output.materializationErrorCode !== undefined && !safeErrorCode(output.materializationErrorCode))
)
throw invalidResponse();
indexes.add(output.outputIndex as number);
return {
outputIndex: output.outputIndex as number,
mediaType: output.mediaType as LocalDreaminaTaskOutput["mediaType"],
...(typeof output.providerArtifactRef === "string" ? { providerArtifactRef: output.providerArtifactRef } : {}),
...(typeof output.materializedAssetId === "string" ? { materializedAssetId: output.materializedAssetId } : {}),
...(typeof output.materializationErrorCode === "string" ? { materializationErrorCode: output.materializationErrorCode } : {}),
};
});
}
function parseProviderObservation(value: unknown): LocalDreaminaProviderObservation {
const observation = record(value);
if (
!observation ||
!["submit_receipt", "query_result", "list_task"].includes(String(observation.source)) ||
typeof observation.observedAt !== "string" ||
!Number.isFinite(Date.parse(observation.observedAt)) ||
!["pending", "processing", "completed", "failed", "cancelled"].includes(String(observation.status)) ||
(observation.accountBinding !== undefined && !safeAccountBinding(observation.accountBinding)) ||
(observation.fenceEpoch !== undefined && (!Number.isSafeInteger(observation.fenceEpoch) || (observation.fenceEpoch as number) < 0))
) {
throw invalidResponse();
}
return {
source: observation.source as LocalDreaminaProviderObservation["source"],
observedAt: observation.observedAt,
...(typeof observation.accountBinding === "string" ? { accountBinding: observation.accountBinding } : {}),
...(typeof observation.fenceEpoch === "number" ? { fenceEpoch: observation.fenceEpoch } : {}),
status: observation.status as LocalDreaminaProviderObservation["status"],
};
}
function parseAccountBinding(value: unknown) {
if (!safeAccountBinding(value)) throw invalidResponse();
return value as string;
}
function safeAccountBinding(value: unknown): value is string {
return typeof value === "string" && /^[A-Za-z0-9._:-]{8,160}$/.test(value);
}
function safeCorrelationId(value: unknown): value is string {
return typeof value === "string" && value.length >= 1 && value.length <= 200 && !/[\u0000-\u001f\u007f]/.test(value);
}
function safeOpaqueRef(value: unknown): value is string {
return typeof value === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value);
}
function safeErrorCode(value: unknown): value is string {
return typeof value === "string" && /^[a-z][a-z0-9_]{2,80}$/.test(value);
}
function parseTaskPage(value: unknown) {
const envelope = record(value);
const rawResult = envelope?.ok === true ? envelope.result : undefined;
const result = record(rawResult);
const items = Array.isArray(rawResult) ? rawResult : result?.tasks;
const nextCursor = result?.nextCursor;
if (!Array.isArray(items) || items.length > MAX_TASK_LIST_ITEMS || (nextCursor !== undefined && (typeof nextCursor !== "string" || !/^[A-Za-z0-9_-]{1,512}$/.test(nextCursor)))) throw invalidResponse();
return {
tasks: items.map((item) => parseTask({ ok: true, result: item }, undefined, true)),
...(typeof nextCursor === "string" ? { nextCursor } : {}),
};
}
function parseResult(value: unknown, mode: "image" | "video"): LocalDreaminaGenerationResult {
const result = record(value);
if (!result || result.mode !== mode) throw invalidResponse();
if (mode === "video") return { mode, video: parseMedia(result.video, "video/") };
if (!Array.isArray(result.images) || !result.images.length || result.images.length > 4) throw invalidResponse();
return { mode, images: result.images.map((image) => parseMedia(image, "image/")) };
}
function parseMedia(value: unknown, prefix: "image/" | "video/") {
const media = record(value);
const bytes = media?.bytes;
if (!media || typeof media.dataUrl !== "string" || typeof media.mimeType !== "string" || !media.mimeType.startsWith(prefix) || !Number.isInteger(bytes) || (bytes as number) <= 0 || !media.dataUrl.startsWith(`data:${media.mimeType};base64,`))
throw invalidResponse();
return { dataUrl: media.dataUrl, mimeType: media.mimeType, bytes: bytes as number };
}
function runtimeError(status: number, value: unknown) {
const source = record(value);
const code = typeof source?.code === "string" ? source.code : "local_generation_request_failed";
const allowed = /^(?:local_generation|dreamina)_[a-z0-9_]{2,80}$/.test(code);
throw new LocalDreaminaGenerationClientError(allowed ? code : "local_generation_request_failed", "本机即梦生成请求失败", status || 502);
}
function bytesToBase64(bytes: Uint8Array) {
let binary = "";
for (const byte of bytes) binary += String.fromCharCode(byte);
return btoa(binary);
}
function record(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
}
function submissionUnknown() {
return new LocalDreaminaGenerationClientError("dreamina_submission_unknown", "Dreamina submission result is unknown", 502);
}
function validateTaskIdentity(idempotencyKey: string, mode: "image" | "video" | undefined) {
if (!/^[A-Za-z0-9._:-]{16,120}$/.test(idempotencyKey) || (mode !== undefined && mode !== "image" && mode !== "video")) throw invalidRequest();
}
function invalidRequest() {
return new LocalDreaminaGenerationClientError("local_generation_request_invalid", "本机即梦生成参数无效", 400);
}
function unavailable() {
return new LocalDreaminaGenerationClientError("local_generation_model_unavailable", "所选本机即梦模型或操作不可用", 409);
}
function invalidResponse() {
return new LocalDreaminaGenerationClientError("local_generation_response_invalid", "本机即梦生成响应无效", 502);
}