upload_record_image: aceptar ruta relativa del proyecto (sin base64)
Para una imagen local/pegada desde vscode: guardarla en una carpeta sincronizada NO truncada (cms/uploads/chat/ o cms/uploads/generated/), dejar que el sync la suba a test y pasar su RUTA RELATIVA como imageUrl. El server lee los bytes de disco vía resolve_image_source — cero base64 por el contexto del modelo, cero URLs localhost inalcanzables. - Validación relajada: además de http(s) y ruta absoluta, se acepta ruta relativa del proyecto (sin esquema, sin "..", <=512 chars, charset de ruta) → sigue rechazando data-URI/base64 crudo. - Descripciones de upload_record_image / replace_record_image actualizadas con el flujo correcto. - resolve_image_source y el aislamiento de entorno: sin cambios (la ruta relativa la resuelve por modo+stub, igual para chat y vscode). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,12 +65,12 @@ async function resolveLocalImageAsBase64(imageUrl) {
|
|||||||
export function registerUploadRecordImageTool(server) {
|
export function registerUploadRecordImageTool(server) {
|
||||||
server.tool(
|
server.tool(
|
||||||
"upload_record_image",
|
"upload_record_image",
|
||||||
"Upload an image to a specific record field in Acai CMS. MANDATORY: before calling this tool, you MUST call get_table_schema with minimal=true to find the EXACT upload field name. Look for fields with type='upload'. NEVER guess field names. Table names WITHOUT 'cms_' prefix. recordId is 'num', never 'id'. If the URL came from generate_image, prefer uploadUrl (or fullUrl) over dockerUrl.",
|
"Upload an image to a specific record field in Acai CMS. MANDATORY: before calling this tool, you MUST call get_table_schema with minimal=true to find the EXACT upload field name. Look for fields with type='upload'. NEVER guess field names. Table names WITHOUT 'cms_' prefix. recordId is 'num', never 'id'. If the URL came from generate_image, prefer uploadUrl (or fullUrl) over dockerUrl. For a LOCAL or pasted image (a file on your machine, no public URL): save it into the synced project folder cms/uploads/chat/<name>.ext, wait for the sync to push it, then pass its PROJECT-RELATIVE path (e.g. 'cms/uploads/chat/foto.png') as imageUrl. NEVER pass a data-URI/base64 nor spin up a localhost server.",
|
||||||
withAuthParams({
|
withAuthParams({
|
||||||
tableName: z.string().describe("Table name without 'cms_' prefix (e.g., 'productos')"),
|
tableName: z.string().describe("Table name without 'cms_' prefix (e.g., 'productos')"),
|
||||||
recordId: z.string().describe("Record 'num' (primary key)"),
|
recordId: z.string().describe("Record 'num' (primary key)"),
|
||||||
fieldName: z.string().describe("EXACT field name from the schema. MUST match a field with type 'upload' from get_table_schema or get_module_config_vars. Do NOT guess."),
|
fieldName: z.string().describe("EXACT field name from the schema. MUST match a field with type 'upload' from get_table_schema or get_module_config_vars. Do NOT guess."),
|
||||||
imageUrl: z.string().describe("URL of the image to upload"),
|
imageUrl: z.string().describe("Image to upload: an http(s) URL, OR a project-relative path to a file already synced to the project (e.g. 'cms/uploads/chat/foto.png'). For local/pasted images use the relative-path form. NOT a data-URI or base64."),
|
||||||
alt: z.string().optional().describe("Alt text for the image (optional)"),
|
alt: z.string().optional().describe("Alt text for the image (optional)"),
|
||||||
}),
|
}),
|
||||||
{ readOnlyHint: false, destructiveHint: false },
|
{ readOnlyHint: false, destructiveHint: false },
|
||||||
@@ -83,19 +83,33 @@ export function registerUploadRecordImageTool(server) {
|
|||||||
);
|
);
|
||||||
if (validationError) return validationError;
|
if (validationError) return validationError;
|
||||||
|
|
||||||
// Rechazar data-URI / base64 crudo: derivar el nombre de fichero
|
// Aceptamos: URL http(s), ruta absoluta del servidor, o RUTA
|
||||||
// del base64 produce nombres de miles de chars que revientan
|
// RELATIVA del proyecto (p.ej. "cms/uploads/chat/foto.png"). Para
|
||||||
// file_put_contents ("File name too long"). Exigir URL http real.
|
// una imagen local/pegada el flujo correcto es guardarla en una
|
||||||
|
// carpeta sincronizada NO truncada (cms/uploads/chat/ o
|
||||||
|
// cms/uploads/generated/), dejar que el sync la suba a test y pasar
|
||||||
|
// aquí su ruta relativa: el server lee los bytes de disco vía
|
||||||
|
// resolve_image_source (sin base64 por el modelo).
|
||||||
|
// Seguimos rechazando data-URI / base64 crudo: derivar el nombre
|
||||||
|
// de un base64 gigante revienta file_put_contents ("File name too
|
||||||
|
// long"). El tope de longitud + charset de ruta lo descartan.
|
||||||
const trimmedImage = imageUrl.trim();
|
const trimmedImage = imageUrl.trim();
|
||||||
const isHttpUrl = /^https?:\/\//i.test(trimmedImage);
|
const isHttpUrl = /^https?:\/\//i.test(trimmedImage);
|
||||||
const isFsPath = trimmedImage.startsWith("/") && !trimmedImage.startsWith("//");
|
const isAbsPath = trimmedImage.startsWith("/") && !trimmedImage.startsWith("//");
|
||||||
if (!isHttpUrl && !isFsPath) {
|
const isRelPath = !isHttpUrl && !isAbsPath
|
||||||
|
&& !/^[a-z][a-z0-9+.-]*:/i.test(trimmedImage) // sin esquema (data:, file:...)
|
||||||
|
&& !trimmedImage.includes("..")
|
||||||
|
&& trimmedImage.length <= 512
|
||||||
|
&& /^[\w./ -]+$/.test(trimmedImage); // charset de ruta (no base64)
|
||||||
|
if (!isHttpUrl && !isAbsPath && !isRelPath) {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
error: "imageUrl must be an http(s) URL, not a data URI or raw base64. " +
|
error: "imageUrl debe ser una URL http(s) o una ruta relativa del proyecto " +
|
||||||
"First upload the image with the 'upload_image_to_assets' tool and pass the returned imageUrl here."
|
"(p.ej. 'cms/uploads/chat/foto.png'), no un data-URI ni base64 crudo. " +
|
||||||
|
"Para una imagen local/pegada: guárdala en cms/uploads/chat/ (sincronizada a test), " +
|
||||||
|
"espera a que el sync la suba y pasa su ruta relativa."
|
||||||
}, null, 2)
|
}, null, 2)
|
||||||
}],
|
}],
|
||||||
isError: true,
|
isError: true,
|
||||||
@@ -221,7 +235,7 @@ export function registerUploadRecordImageTool(server) {
|
|||||||
recordId: z.string().describe("Record 'num' (primary key)"),
|
recordId: z.string().describe("Record 'num' (primary key)"),
|
||||||
fieldName: z.string().describe("Upload field name"),
|
fieldName: z.string().describe("Upload field name"),
|
||||||
uploadId: z.string().describe("Upload ID to replace (get from list_record_uploads)"),
|
uploadId: z.string().describe("Upload ID to replace (get from list_record_uploads)"),
|
||||||
imageUrl: z.string().describe("URL of the new image to upload"),
|
imageUrl: z.string().describe("New image: an http(s) URL, OR a project-relative path to a file already synced (e.g. 'cms/uploads/chat/foto.png'). For local/pasted images use the relative-path form. NOT a data-URI or base64."),
|
||||||
alt: z.string().optional().describe("Alt text for the image (optional)"),
|
alt: z.string().optional().describe("Alt text for the image (optional)"),
|
||||||
}),
|
}),
|
||||||
{ readOnlyHint: false, destructiveHint: false },
|
{ readOnlyHint: false, destructiveHint: false },
|
||||||
|
|||||||
Reference in New Issue
Block a user