feat: deliver in chosen format (not forced JPEG); wire output_ext through jobs/Telegram/MCP
- delivery.ts: upload the stored result in its chosen format (output_ext, global or per-recipe) instead of always converting non-alpha to JPEG. - jobs POST froze the snapshot without output_ext -> per-conversion/per-recipe format choice was silently dropped. Now carried through. - telegram.ts: recipe snapshots carry output_ext + delivery_target_id (custom formats already flow via output_format). - mcp/klarbild-mcp.mjs: process_images gains tasks, output_ext, orientation, crop_mode, contour_mm, picdrop_gallery; job_status shows finished_at + errors. - Docs: llms.txt, mcp/README, changelog updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
+26
-6
@@ -49,13 +49,20 @@ const TOOLS = [
|
||||
description: 'Bilder an Klarbild übergeben und verarbeiten. Dateien als lokale Pfade ODER data:-URLs. Entweder recipeId ODER mode+Optionen angeben.',
|
||||
inputSchema: { type: 'object', properties: {
|
||||
files: { type: 'array', items: { type: 'string' }, description: 'Lokale Pfade oder data:-URLs' },
|
||||
recipeId: { type: 'string', description: 'Optional: Preset-ID (aus list_recipes)' },
|
||||
recipeId: { type: 'string', description: 'Optional: Preset-ID (aus list_recipes) — überschreibt die Einzeloptionen' },
|
||||
mode: { type: 'string', enum: ['each', 'compose', 'generate'], description: 'Falls kein recipeId' },
|
||||
tasks: { type: 'array', items: { type: 'string', enum: ['clean', 'cutout', 'format', 'contour'] },
|
||||
description: 'Nur bei mode=each. Standard: ["format"] wenn output_format gesetzt.' },
|
||||
prompt_text: { type: 'string', description: 'Beschreibung für compose/generate' },
|
||||
output_format: { type: 'string', description: 'z. B. 30x40, theframe, keep' },
|
||||
output_format: { type: 'string', description: 'Fest: 30x40, A4, theframe, hochformat, keep … ODER frei: "25x35" (=25×35 cm) bzw. "sticker5" (=5×5 cm).' },
|
||||
output_ext: { type: 'string', enum: ['png', 'jpg'], description: 'Dateiformat. Weglassen = globale Standard-Einstellung. JPG nur ohne Transparenz.' },
|
||||
orientation: { type: 'string', enum: ['portrait', 'landscape'] },
|
||||
crop_mode: { type: 'string', enum: ['crop', 'extend'] },
|
||||
contour_mm: { type: 'number', description: 'Stickerrand in mm (nur mit tasks=cutout+contour).' },
|
||||
picdrop_gallery: { type: 'string', description: 'Ziel-Galerie/Unterordner (bei delivery picdrop/both).' },
|
||||
delivery: { type: 'string', enum: ['library', 'picdrop', 'both'] },
|
||||
}, required: ['files'] } },
|
||||
{ name: 'job_status', description: 'Status eines Auftrags abfragen.',
|
||||
{ name: 'job_status', description: 'Status eines Auftrags abfragen (inkl. Endzeit und Fehlermeldungen).',
|
||||
inputSchema: { type: 'object', properties: { jobId: { type: 'string' } }, required: ['jobId'] } },
|
||||
];
|
||||
|
||||
@@ -73,11 +80,21 @@ server.setRequestHandler({ method: 'tools/call' }, async (req) => {
|
||||
const files = a.files || [];
|
||||
const mode = a.mode || 'each';
|
||||
const sources = (mode === 'generate') ? [] : await Promise.all(files.map((f) => uploadFile(f)));
|
||||
const wantsFormat = a.output_format && a.output_format !== 'keep';
|
||||
const tasks = Array.isArray(a.tasks) && a.tasks.length ? a.tasks : (wantsFormat ? ['format'] : []);
|
||||
const body = a.recipeId
|
||||
? { recipeId: a.recipeId, mode, sources, prompt_text: a.prompt_text }
|
||||
: { mode, prompt_text: a.prompt_text, sources,
|
||||
recipe: { tasks: a.output_format && a.output_format !== 'keep' ? ['format'] : [],
|
||||
output_format: a.output_format || 'keep', delivery: a.delivery || 'library' },
|
||||
recipe: {
|
||||
tasks: mode === 'each' ? tasks : (wantsFormat ? ['format'] : []),
|
||||
output_format: a.output_format || 'keep',
|
||||
output_ext: a.output_ext || null,
|
||||
orientation: a.orientation || 'portrait',
|
||||
crop_mode: a.crop_mode || 'crop',
|
||||
contour_mm: a.contour_mm ?? null,
|
||||
picdrop_gallery: a.picdrop_gallery || null,
|
||||
delivery: a.delivery || 'library',
|
||||
},
|
||||
delivery: a.delivery || 'library' };
|
||||
const j = await (await fetch(api('/api/jobs'), { method: 'POST', headers: { ...authHeaders, 'Content-Type': 'application/json' }, body: JSON.stringify(body) })).json();
|
||||
if (!j.jobId) throw new Error(j.error || 'Auftrag fehlgeschlagen');
|
||||
@@ -86,7 +103,10 @@ server.setRequestHandler({ method: 'tools/call' }, async (req) => {
|
||||
if (name === 'job_status') {
|
||||
const j = await (await fetch(api(`/api/jobs/${a.jobId}`), { headers: authHeaders })).json();
|
||||
const job = j.job || j;
|
||||
return { content: [{ type: 'text', text: `Status: ${job.status} — ${job.done_count}/${job.total} fertig${job.failed_count ? `, ${job.failed_count} fehlgeschlagen` : ''}` }] };
|
||||
const errs = (j.items || []).map((it) => it.error_message).filter(Boolean);
|
||||
const fin = job.finished_at ? ` · beendet ${job.finished_at}` : '';
|
||||
const errTxt = errs.length ? `\nFehler: ${errs.join('; ')}` : '';
|
||||
return { content: [{ type: 'text', text: `Status: ${job.status} — ${job.done_count}/${job.total} fertig${job.failed_count ? `, ${job.failed_count} fehlgeschlagen` : ''}${fin}${errTxt}` }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: `Unbekanntes Tool: ${name}` }], isError: true };
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user