feat(api): section-prefix factory + batch routes via apiToolPath (+section validation)

This commit is contained in:
SnapOtter
2026-06-20 11:44:36 +08:00
parent 0691457f94
commit 68dd7dabaa
3 changed files with 61 additions and 51 deletions
+13 -6
View File
@@ -1,7 +1,7 @@
/**
* Batch processing route.
*
* POST /api/v1/tools/:toolId/batch
* POST /api/v1/tools/:section/:toolId/batch
*
* Accepts multipart with multiple files + settings JSON.
* Each file is enqueued as a batch-child BullMQ job; a batch-finalize
@@ -12,7 +12,7 @@ import { randomUUID } from "node:crypto";
import { mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getBundleForTool, TOOL_BUNDLE_MAP, TOOLS } from "@snapotter/shared";
import { getBundleForTool, TOOL_BUNDLE_MAP, TOOLS, toolSection } from "@snapotter/shared";
import archiver from "archiver";
import type { FlowJob } from "bullmq";
import { eq } from "drizzle-orm";
@@ -56,10 +56,17 @@ function injectTraceContextIntoFlow(node: FlowJob): void {
export async function registerBatchRoutes(app: FastifyInstance): Promise<void> {
app.post(
"/api/v1/tools/:toolId/batch",
"/api/v1/tools/:section/:toolId/batch",
{ config: { rateLimit: { max: 20, timeWindow: "1 minute" } } },
async (request: FastifyRequest<{ Params: { toolId: string } }>, reply: FastifyReply) => {
const { toolId } = request.params;
async (
request: FastifyRequest<{ Params: { section: string; toolId: string } }>,
reply: FastifyReply,
) => {
const { section, toolId } = request.params;
const tool = TOOLS.find((t) => t.id === toolId);
if (!tool || toolSection(tool) !== section) {
return reply.status(404).send({ error: "Not found", code: "NOT_FOUND" });
}
// Batch processing (especially with AI) can take tens of minutes.
// Disable the Node.js HTTP socket timeout so the connection is not
@@ -182,7 +189,7 @@ export async function registerBatchRoutes(app: FastifyInstance): Promise<void> {
// Resolve the tool's modality so non-image files (audio/video/document)
// validate through their own handler instead of the image validator.
const modality = TOOLS.find((t) => t.id === toolId)?.modality ?? "image";
const modality = tool.modality;
const batchScratch = join(tmpdir(), "snapotter-scratch", `batch-${parentId}`);
await mkdir(batchScratch, { recursive: true });
+9 -3
View File
@@ -2,7 +2,13 @@ import { randomUUID } from "node:crypto";
import { mkdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { extname, join } from "node:path";
import { ANALYTICS_EVENTS, getBundleForTool, TOOL_BUNDLE_MAP, TOOLS } from "@snapotter/shared";
import {
ANALYTICS_EVENTS,
apiToolPath,
getBundleForTool,
TOOL_BUNDLE_MAP,
TOOLS,
} from "@snapotter/shared";
import { and, inArray, sql } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import type { z } from "zod";
@@ -186,7 +192,7 @@ export function registerToolProcessFn(config: AnyToolRouteConfig): void {
}
/**
* Factory that registers a POST /api/v1/tools/:toolId route.
* Factory that registers a POST /api/v1/tools/:section/:toolId route.
*
* The route accepts multipart with:
* - A file part (the image to process)
@@ -211,7 +217,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
toolRegistry.set(config.toolId, resolved);
app.post(
`/api/v1/tools/${config.toolId}`,
apiToolPath(config.toolId),
{ config: { rateLimit: { max: 60, timeWindow: "1 minute" } } },
async (request: FastifyRequest, reply: FastifyReply) => {
// Check per-tool access before processing uploads