mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: pipeline only shows compatible tools and displays errors
The pipeline tool picker was showing all tools, but only tools registered via createToolRoute() support pipeline execution. Tools with custom routes (remove-background, upscale, ocr, etc.) would silently fail with "Tool not found" and the empty catch block hid the error from users. Add GET /api/v1/pipeline/tools endpoint that returns the IDs of pipeline-compatible tools. The frontend fetches this list and filters the tool picker accordingly. Also surface pipeline execution errors in the UI instead of swallowing them.
This commit is contained in:
@@ -17,7 +17,7 @@ import { validateImageBuffer } from "../lib/file-validation.js";
|
|||||||
import { sanitizeFilename } from "../lib/filename.js";
|
import { sanitizeFilename } from "../lib/filename.js";
|
||||||
import { createWorkspace } from "../lib/workspace.js";
|
import { createWorkspace } from "../lib/workspace.js";
|
||||||
import { requireAuth } from "../plugins/auth.js";
|
import { requireAuth } from "../plugins/auth.js";
|
||||||
import { getToolConfig } from "./tool-factory.js";
|
import { getRegisteredToolIds, getToolConfig } from "./tool-factory.js";
|
||||||
|
|
||||||
/** Schema for a single pipeline step. */
|
/** Schema for a single pipeline step. */
|
||||||
const pipelineStepSchema = z.object({
|
const pipelineStepSchema = z.object({
|
||||||
@@ -312,5 +312,15 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/pipeline/tools
|
||||||
|
*
|
||||||
|
* Returns the IDs of tools that can be used as pipeline steps.
|
||||||
|
* Only tools registered via createToolRoute() support pipeline execution.
|
||||||
|
*/
|
||||||
|
app.get("/api/v1/pipeline/tools", async (_request: FastifyRequest, reply: FastifyReply) => {
|
||||||
|
return reply.send({ toolIds: getRegisteredToolIds() });
|
||||||
|
});
|
||||||
|
|
||||||
app.log.info("Pipeline routes registered");
|
app.log.info("Pipeline routes registered");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ export function getToolConfig(toolId: string): AnyToolRouteConfig | undefined {
|
|||||||
return toolRegistry.get(toolId);
|
return toolRegistry.get(toolId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the IDs of all tools registered via createToolRoute().
|
||||||
|
* These are the tools that can be used in pipelines and batch processing.
|
||||||
|
*/
|
||||||
|
export function getRegisteredToolIds(): string[] {
|
||||||
|
return [...toolRegistry.keys()];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory that registers a POST /api/v1/tools/:toolId route.
|
* Factory that registers a POST /api/v1/tools/:toolId route.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ interface PipelineBuilderProps {
|
|||||||
processedSize: number;
|
processedSize: number;
|
||||||
stepsCompleted: number;
|
stepsCompleted: number;
|
||||||
} | null;
|
} | null;
|
||||||
|
executionError?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PipelineBuilder({
|
export function PipelineBuilder({
|
||||||
@@ -52,6 +53,7 @@ export function PipelineBuilder({
|
|||||||
saving = false,
|
saving = false,
|
||||||
executing = false,
|
executing = false,
|
||||||
executionResult = null,
|
executionResult = null,
|
||||||
|
executionError = null,
|
||||||
}: PipelineBuilderProps) {
|
}: PipelineBuilderProps) {
|
||||||
const [showToolPicker, setShowToolPicker] = useState(false);
|
const [showToolPicker, setShowToolPicker] = useState(false);
|
||||||
const [expandedStep, setExpandedStep] = useState<string | null>(null);
|
const [expandedStep, setExpandedStep] = useState<string | null>(null);
|
||||||
@@ -61,6 +63,7 @@ export function PipelineBuilder({
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [disabledTools, setDisabledTools] = useState<string[]>([]);
|
const [disabledTools, setDisabledTools] = useState<string[]>([]);
|
||||||
const [experimentalEnabled, setExperimentalEnabled] = useState(false);
|
const [experimentalEnabled, setExperimentalEnabled] = useState(false);
|
||||||
|
const [pipelineToolIds, setPipelineToolIds] = useState<string[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiGet<{ settings: Record<string, string> }>("/v1/settings")
|
apiGet<{ settings: Record<string, string> }>("/v1/settings")
|
||||||
@@ -71,15 +74,22 @@ export function PipelineBuilder({
|
|||||||
setExperimentalEnabled(data.settings.enableExperimentalTools === "true");
|
setExperimentalEnabled(data.settings.enableExperimentalTools === "true");
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
|
// Fetch which tools actually support pipeline execution
|
||||||
|
apiGet<{ toolIds: string[] }>("/v1/pipeline/tools")
|
||||||
|
.then((data) => setPipelineToolIds(data.toolIds))
|
||||||
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const PIPELINE_TOOLS = useMemo(() => {
|
const PIPELINE_TOOLS = useMemo(() => {
|
||||||
return PIPELINE_TOOLS_BASE.filter((t) => {
|
return PIPELINE_TOOLS_BASE.filter((t) => {
|
||||||
if (disabledTools.includes(t.id)) return false;
|
if (disabledTools.includes(t.id)) return false;
|
||||||
if (t.experimental && !experimentalEnabled) return false;
|
if (t.experimental && !experimentalEnabled) return false;
|
||||||
|
// Only show tools that are registered in the pipeline-compatible tool registry
|
||||||
|
if (pipelineToolIds && !pipelineToolIds.includes(t.id)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [disabledTools, experimentalEnabled]);
|
}, [disabledTools, experimentalEnabled, pipelineToolIds]);
|
||||||
|
|
||||||
const addStep = useCallback(
|
const addStep = useCallback(
|
||||||
(toolId: string) => {
|
(toolId: string) => {
|
||||||
@@ -326,6 +336,16 @@ export function PipelineBuilder({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Execution error */}
|
||||||
|
{executionError && (
|
||||||
|
<div className="rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-4">
|
||||||
|
<div className="flex items-center gap-2 text-red-700 dark:text-red-400">
|
||||||
|
<icons.AlertCircle className="h-5 w-5 shrink-0" />
|
||||||
|
<span className="text-sm font-medium">{executionError}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Execution result */}
|
{/* Execution result */}
|
||||||
{executionResult && (
|
{executionResult && (
|
||||||
<div className="rounded-lg border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 p-4 space-y-2">
|
<div className="rounded-lg border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 p-4 space-y-2">
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function AutomatePage() {
|
|||||||
processedSize: number;
|
processedSize: number;
|
||||||
stepsCompleted: number;
|
stepsCompleted: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [executionError, setExecutionError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Load saved pipelines
|
// Load saved pipelines
|
||||||
const loadPipelines = useCallback(async () => {
|
const loadPipelines = useCallback(async () => {
|
||||||
@@ -96,6 +97,7 @@ export function AutomatePage() {
|
|||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
setExecuting(true);
|
setExecuting(true);
|
||||||
setExecutionResult(null);
|
setExecutionResult(null);
|
||||||
|
setExecutionError(null);
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
@@ -123,9 +125,12 @@ export function AutomatePage() {
|
|||||||
processedSize: data.processedSize,
|
processedSize: data.processedSize,
|
||||||
stepsCompleted: data.stepsCompleted,
|
stepsCompleted: data.stepsCompleted,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
setExecutionError(data.error || `Pipeline failed with status ${res.status}`);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Error handling
|
setExecutionError("Connection error. Please try again.");
|
||||||
} finally {
|
} finally {
|
||||||
setExecuting(false);
|
setExecuting(false);
|
||||||
}
|
}
|
||||||
@@ -215,6 +220,7 @@ export function AutomatePage() {
|
|||||||
saving={saving}
|
saving={saving}
|
||||||
executing={executing}
|
executing={executing}
|
||||||
executionResult={executionResult}
|
executionResult={executionResult}
|
||||||
|
executionError={executionError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user