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:
Jordan
2026-06-19 20:14:16 +01:00
parent 5dc2dbcf4a
commit 9d11a59fb8

View File

@@ -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 },