feat: update executor for tools

This commit is contained in:
2026-09-10 12:01:42 +05:00
parent 292e53c9c7
commit ba8a518861
4 changed files with 130 additions and 134 deletions
+63 -108
View File
@@ -1,116 +1,66 @@
import { ToolError } from "$lib/core/errors"; import { ToolError } from "$lib/core/errors";
import type { PixelImage } from "$lib/core/types"; import type { PixelImage } from "$lib/core/types";
import { sanitizeSchemaParams } from "$lib/registry-schema";
import type { ToolContext, ToolEntry, ToolResult } from "$lib/registry-new";
type MaybeRunnable = { /** Вход единой точки исполнения. `source`/`text` — по контракту `tool.input`. */
id: string; export type ExecuteContext = {
domOnly?: boolean; params: Record<string, unknown>;
run?: ( source?: PixelImage;
img: PixelImage, text?: string;
params: Record<string, unknown>,
) => Promise<PixelImage> | PixelImage;
generate?: (
params: Record<string, unknown>,
) => Promise<PixelImage> | PixelImage;
}; };
export async function executeStep( /**
tool: MaybeRunnable, * Единственная точка исполнения инструмента. Валидирует наличие входа по
img: PixelImage, * `tool.input`, санитит параметры и запускает `tool.run`. Выбор места
params: Record<string, unknown>, * исполнения (worker/поток) — внутренняя забота executor'а, диспетча методов
): Promise<PixelImage> { * нет: у любого инструмента один `run(ctx)`.
if (!tool.run) { */
throw new Error("errors.noImageRun"); export async function execute(
tool: ToolEntry,
ctx: ExecuteContext,
): Promise<ToolResult> {
const params = sanitizeSchemaParams(tool.schema, ctx.params);
if (tool.input === "image" && !ctx.source) {
throw new ToolError("errors.sourceRequired");
} }
if (tool.domOnly) { if (tool.input === "text" && !ctx.text?.trim()) {
return await runDirect(tool, img, params); throw new ToolError("errors.textRequired");
}
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);
} }
return route(tool, { params, source: ctx.source, text: ctx.text });
} }
/** /**
* Применение инструмента-генератора (без входного изображения). * Маршрутизация по способу исполнения. В worker уходит всё, кроме
* Выполняется напрямую: worker-протокол рассчитан на передачу исходника, * domOnly-инструментов: даже текстовые вердикты могут быть тяжёлыми
* а генераторам вход не нужен. * (гистограммы, сложный анализ), поэтому ограничивать по типу результата
* неверно. Ограничение одно — доступ к DOM, которого в worker нет.
*/ */
export async function executeGenerate( async function route(
tool: MaybeRunnable, tool: ToolEntry,
params: Record<string, unknown>, ctx: ExecuteContext,
): Promise<PixelImage> { ): Promise<ToolResult> {
if (!tool.generate) { if (tool.domOnly || typeof Worker === "undefined") {
throw new Error("errors.noGenerate"); return await runDirect(tool, ctx);
} }
return await tool.generate(params); const worker = ensureWorker();
} if (worker === null) {
return await runDirect(tool, ctx);
type MaybeTextRunnable = MaybeRunnable & {
runFromText?: (
text: string,
params: Record<string, unknown>,
) => Promise<PixelImage> | PixelImage;
toText?: (
img: PixelImage,
params: Record<string, unknown>,
) => Promise<string> | string;
textToText?: (text: string) => Promise<string> | string;
};
/** Текст → изображение (text-source конвертеры). Выполняется напрямую. */
export async function executeFromText(
tool: MaybeTextRunnable,
text: string,
params: Record<string, unknown>,
): Promise<PixelImage> {
if (!tool.runFromText) {
throw new Error("errors.noTextInput");
} }
return await tool.runFromText(text, params); try {
} return await runInWorker(worker, tool, ctx);
} catch (workerError) {
/** Изображение → текст (текстовые конвертеры и вердикты). */ disableWorker();
export async function executeToText( void workerError;
tool: MaybeTextRunnable, return await runDirect(tool, ctx);
img: PixelImage,
params: Record<string, unknown>,
): Promise<string> {
if (!tool.toText) {
throw new Error("errors.noTextResult");
} }
return await tool.toText(img, params);
}
/** Текст → текст (напр. verify-is-png). */
export async function executeTextToText(
tool: MaybeTextRunnable,
text: string,
): Promise<string> {
if (!tool.textToText) {
throw new Error("errors.noTextResult");
}
return await tool.textToText(text);
} }
async function runDirect( async function runDirect(
tool: MaybeRunnable, tool: ToolEntry,
img: PixelImage, ctx: ExecuteContext,
params: Record<string, unknown>, ): Promise<ToolResult> {
): Promise<PixelImage> { return await tool.run(ctx as ToolContext<Record<string, unknown>>);
if (!tool.run) {
throw new Error("errors.noImageRun");
}
return await tool.run(img, params);
} }
let worker: Worker | null = null; let worker: Worker | null = null;
@@ -118,7 +68,7 @@ let workerTried = false;
let nextRequestId = 1; let nextRequestId = 1;
const pending = new Map< const pending = new Map<
number, number,
{ resolve: (img: PixelImage) => void; reject: (e: Error) => void } { resolve: (result: ToolResult) => void; reject: (e: Error) => void }
>(); >();
function ensureWorker(): Worker | null { function ensureWorker(): Worker | null {
@@ -138,6 +88,7 @@ function ensureWorker(): Worker | null {
width?: number; width?: number;
height?: number; height?: number;
data?: Uint8ClampedArray; data?: Uint8ClampedArray;
text?: string;
error?: string; error?: string;
errorKey?: string; errorKey?: string;
errorVars?: Record<string, string | number>; errorVars?: Record<string, string | number>;
@@ -151,6 +102,8 @@ function ensureWorker(): Worker | null {
height: payload.height, height: payload.height,
data: new Uint8ClampedArray(payload.data), data: new Uint8ClampedArray(payload.data),
}); });
} else if (payload.ok && typeof payload.text === "string") {
entry.resolve(payload.text);
} else if (payload.errorKey) { } else if (payload.errorKey) {
entry.reject(new ToolError(payload.errorKey, payload.errorVars)); entry.reject(new ToolError(payload.errorKey, payload.errorVars));
} else { } else {
@@ -180,22 +133,24 @@ function disableWorker(): void {
function runInWorker( function runInWorker(
workerInstance: Worker, workerInstance: Worker,
toolId: string, tool: ToolEntry,
img: PixelImage, ctx: ExecuteContext,
params: Record<string, unknown>, ): Promise<ToolResult> {
): Promise<PixelImage> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const id = nextRequestId++; const id = nextRequestId++;
pending.set(id, { resolve, reject }); pending.set(id, { resolve, reject });
workerInstance.postMessage({ workerInstance.postMessage({
id, id,
toolId, toolId: tool.id,
image: { params: ctx.params,
width: img.width, source: ctx.source
height: img.height, ? {
data: new Uint8ClampedArray(img.data), width: ctx.source.width,
}, height: ctx.source.height,
params, data: new Uint8ClampedArray(ctx.source.data),
}
: undefined,
text: ctx.text,
}); });
}); });
} }
+49 -18
View File
@@ -7,10 +7,28 @@ import { sanitizeSchemaParams } from "$lib/registry-schema";
type WorkerRequest = { type WorkerRequest = {
id: number; id: number;
toolId: string; toolId: string;
image: { width: number; height: number; data: Uint8ClampedArray };
params: Record<string, unknown>; params: Record<string, unknown>;
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<string, string | number>;
};
self.onmessage = (event: MessageEvent<WorkerRequest>) => { self.onmessage = (event: MessageEvent<WorkerRequest>) => {
void handle(event.data); void handle(event.data);
}; };
@@ -21,23 +39,36 @@ async function handle(request: WorkerRequest): Promise<void> {
if (!tool?.run) { if (!tool?.run) {
throw new Error("errors.noImageRun"); throw new Error("errors.noImageRun");
} }
const image: PixelImage = { const source: PixelImage | undefined = request.source
width: request.image.width, ? {
height: request.image.height, width: request.source.width,
data: new Uint8ClampedArray(request.image.data), height: request.source.height,
}; data: new Uint8ClampedArray(request.source.data),
const output = await tool.run( }
image, : undefined;
sanitizeSchemaParams(tool.schema, request.params), 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) { } catch (e) {
const toolError = e instanceof ToolError ? e : undefined; const toolError = e instanceof ToolError ? e : undefined;
(self as unknown as Worker).postMessage({ (self as unknown as Worker).postMessage({
@@ -46,6 +77,6 @@ async function handle(request: WorkerRequest): Promise<void> {
error: e instanceof Error ? e.message : String(e), error: e instanceof Error ? e.message : String(e),
errorKey: toolError?.key, errorKey: toolError?.key,
errorVars: toolError?.vars, errorVars: toolError?.vars,
}); } satisfies WorkerResponse);
} }
} }
+2 -7
View File
@@ -1,7 +1,2 @@
export { export { execute } from "./executor";
executeFromText, export type { ExecuteContext } from "./executor";
executeGenerate,
executeStep,
executeTextToText,
executeToText,
} from "./executor";
+16 -1
View File
@@ -8,7 +8,22 @@ import { colorEntries } from "./color";
import { generateEntries } from "./generate"; import { generateEntries } from "./generate";
import { textEntries } from "./text"; 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[] = [ export const TOOLS: ToolEntry[] = [
...geometryEntries, ...geometryEntries,