mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
feat: update executor for tools
This commit is contained in:
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,2 @@
|
|||||||
export {
|
export { execute } from "./executor";
|
||||||
executeFromText,
|
export type { ExecuteContext } from "./executor";
|
||||||
executeGenerate,
|
|
||||||
executeStep,
|
|
||||||
executeTextToText,
|
|
||||||
executeToText,
|
|
||||||
} from "./executor";
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user