From ba8a51886129d3c128d21ee625a8c3a0d16ae5b0 Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Thu, 10 Sep 2026 08:29:04 +0500 Subject: [PATCH] feat: update executor for tools --- web/src/lib/preview/executor/executor.ts | 171 +++++++----------- .../lib/preview/executor/executor.worker.ts | 67 +++++-- web/src/lib/preview/executor/index.ts | 9 +- web/src/lib/registry-new/index.ts | 17 +- 4 files changed, 130 insertions(+), 134 deletions(-) diff --git a/web/src/lib/preview/executor/executor.ts b/web/src/lib/preview/executor/executor.ts index a48009a..ebb7dbb 100644 --- a/web/src/lib/preview/executor/executor.ts +++ b/web/src/lib/preview/executor/executor.ts @@ -1,116 +1,66 @@ import { ToolError } from "$lib/core/errors"; import type { PixelImage } from "$lib/core/types"; +import { sanitizeSchemaParams } from "$lib/registry-schema"; +import type { ToolContext, ToolEntry, ToolResult } from "$lib/registry-new"; -type MaybeRunnable = { - id: string; - domOnly?: boolean; - run?: ( - img: PixelImage, - params: Record, - ) => Promise | PixelImage; - generate?: ( - params: Record, - ) => Promise | PixelImage; +/** Вход единой точки исполнения. `source`/`text` — по контракту `tool.input`. */ +export type ExecuteContext = { + params: Record; + source?: PixelImage; + text?: string; }; -export async function executeStep( - tool: MaybeRunnable, - img: PixelImage, - params: Record, -): Promise { - if (!tool.run) { - throw new Error("errors.noImageRun"); +/** + * Единственная точка исполнения инструмента. Валидирует наличие входа по + * `tool.input`, санитит параметры и запускает `tool.run`. Выбор места + * исполнения (worker/поток) — внутренняя забота executor'а, диспетча методов + * нет: у любого инструмента один `run(ctx)`. + */ +export async function execute( + tool: ToolEntry, + ctx: ExecuteContext, +): Promise { + const params = sanitizeSchemaParams(tool.schema, ctx.params); + if (tool.input === "image" && !ctx.source) { + throw new ToolError("errors.sourceRequired"); } - if (tool.domOnly) { - return await runDirect(tool, img, params); - } - if (typeof Worker === "undefined") { - return await runDirect(tool, img, params); - } - const worker = ensureWorker(); - if (worker === null) { - return await runDirect(tool, img, params); - } - try { - return await runInWorker(worker, tool.id, img, params); - } catch (workerError) { - disableWorker(); - void workerError; - return await runDirect(tool, img, params); + if (tool.input === "text" && !ctx.text?.trim()) { + throw new ToolError("errors.textRequired"); } + return route(tool, { params, source: ctx.source, text: ctx.text }); } /** - * Применение инструмента-генератора (без входного изображения). - * Выполняется напрямую: worker-протокол рассчитан на передачу исходника, - * а генераторам вход не нужен. + * Маршрутизация по способу исполнения. В worker уходит всё, кроме + * domOnly-инструментов: даже текстовые вердикты могут быть тяжёлыми + * (гистограммы, сложный анализ), поэтому ограничивать по типу результата + * неверно. Ограничение одно — доступ к DOM, которого в worker нет. */ -export async function executeGenerate( - tool: MaybeRunnable, - params: Record, -): Promise { - if (!tool.generate) { - throw new Error("errors.noGenerate"); +async function route( + tool: ToolEntry, + ctx: ExecuteContext, +): Promise { + if (tool.domOnly || typeof Worker === "undefined") { + return await runDirect(tool, ctx); } - return await tool.generate(params); -} - -type MaybeTextRunnable = MaybeRunnable & { - runFromText?: ( - text: string, - params: Record, - ) => Promise | PixelImage; - toText?: ( - img: PixelImage, - params: Record, - ) => Promise | string; - textToText?: (text: string) => Promise | string; -}; - -/** Текст → изображение (text-source конвертеры). Выполняется напрямую. */ -export async function executeFromText( - tool: MaybeTextRunnable, - text: string, - params: Record, -): Promise { - if (!tool.runFromText) { - throw new Error("errors.noTextInput"); + const worker = ensureWorker(); + if (worker === null) { + return await runDirect(tool, ctx); } - return await tool.runFromText(text, params); -} - -/** Изображение → текст (текстовые конвертеры и вердикты). */ -export async function executeToText( - tool: MaybeTextRunnable, - img: PixelImage, - params: Record, -): Promise { - if (!tool.toText) { - throw new Error("errors.noTextResult"); + try { + return await runInWorker(worker, tool, ctx); + } catch (workerError) { + disableWorker(); + void workerError; + return await runDirect(tool, ctx); } - return await tool.toText(img, params); -} - -/** Текст → текст (напр. verify-is-png). */ -export async function executeTextToText( - tool: MaybeTextRunnable, - text: string, -): Promise { - if (!tool.textToText) { - throw new Error("errors.noTextResult"); - } - return await tool.textToText(text); } async function runDirect( - tool: MaybeRunnable, - img: PixelImage, - params: Record, -): Promise { - if (!tool.run) { - throw new Error("errors.noImageRun"); - } - return await tool.run(img, params); + tool: ToolEntry, + ctx: ExecuteContext, +): Promise { + return await tool.run(ctx as ToolContext>); } let worker: Worker | null = null; @@ -118,7 +68,7 @@ let workerTried = false; let nextRequestId = 1; const pending = new Map< number, - { resolve: (img: PixelImage) => void; reject: (e: Error) => void } + { resolve: (result: ToolResult) => void; reject: (e: Error) => void } >(); function ensureWorker(): Worker | null { @@ -138,6 +88,7 @@ function ensureWorker(): Worker | null { width?: number; height?: number; data?: Uint8ClampedArray; + text?: string; error?: string; errorKey?: string; errorVars?: Record; @@ -151,6 +102,8 @@ function ensureWorker(): Worker | null { height: payload.height, data: new Uint8ClampedArray(payload.data), }); + } else if (payload.ok && typeof payload.text === "string") { + entry.resolve(payload.text); } else if (payload.errorKey) { entry.reject(new ToolError(payload.errorKey, payload.errorVars)); } else { @@ -180,22 +133,24 @@ function disableWorker(): void { function runInWorker( workerInstance: Worker, - toolId: string, - img: PixelImage, - params: Record, -): Promise { + tool: ToolEntry, + ctx: ExecuteContext, +): Promise { return new Promise((resolve, reject) => { const id = nextRequestId++; pending.set(id, { resolve, reject }); workerInstance.postMessage({ id, - toolId, - image: { - width: img.width, - height: img.height, - data: new Uint8ClampedArray(img.data), - }, - params, + toolId: tool.id, + params: ctx.params, + source: ctx.source + ? { + width: ctx.source.width, + height: ctx.source.height, + data: new Uint8ClampedArray(ctx.source.data), + } + : undefined, + text: ctx.text, }); }); } diff --git a/web/src/lib/preview/executor/executor.worker.ts b/web/src/lib/preview/executor/executor.worker.ts index e67597e..199c75f 100644 --- a/web/src/lib/preview/executor/executor.worker.ts +++ b/web/src/lib/preview/executor/executor.worker.ts @@ -7,10 +7,28 @@ import { sanitizeSchemaParams } from "$lib/registry-schema"; type WorkerRequest = { id: number; toolId: string; - image: { width: number; height: number; data: Uint8ClampedArray }; params: Record; + source?: { width: number; height: number; data: Uint8ClampedArray }; + text?: string; }; +type WorkerResponse = + | { + id: number; + ok: true; + width?: number; + height?: number; + data?: Uint8ClampedArray; + text?: string; + } + | { + id: number; + ok: false; + error?: string; + errorKey?: string; + errorVars?: Record; + }; + self.onmessage = (event: MessageEvent) => { void handle(event.data); }; @@ -21,23 +39,36 @@ async function handle(request: WorkerRequest): Promise { if (!tool?.run) { throw new Error("errors.noImageRun"); } - const image: PixelImage = { - width: request.image.width, - height: request.image.height, - data: new Uint8ClampedArray(request.image.data), - }; - const output = await tool.run( - image, - sanitizeSchemaParams(tool.schema, request.params), + const source: PixelImage | undefined = request.source + ? { + width: request.source.width, + height: request.source.height, + data: new Uint8ClampedArray(request.source.data), + } + : undefined; + const output = await tool.run({ + params: sanitizeSchemaParams(tool.schema, request.params), + source, + text: request.text, + }); + if (typeof output === "string") { + (self as unknown as Worker).postMessage({ + id: request.id, + ok: true, + text: output, + } satisfies WorkerResponse); + return; + } + (self as unknown as Worker).postMessage( + { + id: request.id, + ok: true, + width: output.width, + height: output.height, + data: output.data, + } satisfies WorkerResponse, + [output.data.buffer], ); - const payload = { - id: request.id, - ok: true, - width: output.width, - height: output.height, - data: output.data, - }; - (self as unknown as Worker).postMessage(payload, [output.data.buffer]); } catch (e) { const toolError = e instanceof ToolError ? e : undefined; (self as unknown as Worker).postMessage({ @@ -46,6 +77,6 @@ async function handle(request: WorkerRequest): Promise { error: e instanceof Error ? e.message : String(e), errorKey: toolError?.key, errorVars: toolError?.vars, - }); + } satisfies WorkerResponse); } } diff --git a/web/src/lib/preview/executor/index.ts b/web/src/lib/preview/executor/index.ts index 81e0890..e1714c4 100644 --- a/web/src/lib/preview/executor/index.ts +++ b/web/src/lib/preview/executor/index.ts @@ -1,7 +1,2 @@ -export { - executeFromText, - executeGenerate, - executeStep, - executeTextToText, - executeToText, -} from "./executor"; +export { execute } from "./executor"; +export type { ExecuteContext } from "./executor"; diff --git a/web/src/lib/registry-new/index.ts b/web/src/lib/registry-new/index.ts index 08f1008..2add801 100644 --- a/web/src/lib/registry-new/index.ts +++ b/web/src/lib/registry-new/index.ts @@ -8,7 +8,22 @@ import { colorEntries } from "./color"; import { generateEntries } from "./generate"; import { textEntries } from "./text"; -export type { ToolEntry } from "./types"; +export { + genTool, + imgTool, + textGen, + requireSource, + requireText, + INPUT_MODES, + RESULT_KINDS, +} from "./types"; +export type { + InputMode, + ResultKind, + ToolContext, + ToolEntry, + ToolResult, +} from "./types"; export const TOOLS: ToolEntry[] = [ ...geometryEntries,