From 0a8756c3086b8fd2a9cbc16dbd8f3a98c77648df Mon Sep 17 00:00:00 2001 From: Jordan Diaz Date: Fri, 10 Apr 2026 16:13:35 +0000 Subject: [PATCH] =?UTF-8?q?A=C3=B1adido=20imagenes=20en=20records=20nuevos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mcp-server/tools/media/generateImage.js | 14 +++- mcp-server/tools/media/upload.js | 96 ++++++++++++++++++++++++- src/config.py | 2 +- src/models/agent.py | 2 +- src/orchestrator/agents/base.py | 2 +- 5 files changed, 108 insertions(+), 8 deletions(-) diff --git a/mcp-server/tools/media/generateImage.js b/mcp-server/tools/media/generateImage.js index c469a8f..ee384c1 100644 --- a/mcp-server/tools/media/generateImage.js +++ b/mcp-server/tools/media/generateImage.js @@ -186,7 +186,15 @@ export function registerGenerateImageTool(server) { const credentials = await getSessionCredentials(extra.sessionId); const fullUrl = credentials.web_url ? `${credentials.web_url}/${relativePath}` : dockerUrl; - const uploadUrl = credentials.web_url ? fullUrl : dockerUrl; + + // En modo produccion el archivo solo existe en el filesystem local del + // container agentic, no en el servidor real. Devolvemos el path absoluto + // como uploadUrl para que upload_record_image lo lea del disco (via base64). + // En local/forge el container web puede servirlo desde web:80, asi que + // seguimos devolviendo la URL HTTP. + const uploadUrl = credentials.mode === "production" + ? filePath // absolute filesystem path + : (credentials.web_url ? fullUrl : dockerUrl); return { content: [{ @@ -201,7 +209,9 @@ export function registerGenerateImageTool(server) { fullUrl, uploadUrl, size: `${Math.round(imageBuffer.length / 1024)}KB`, - note: `Image saved. To assign it with upload_record_image, use imageUrl="${uploadUrl}". dockerUrl is mainly for local preview/debugging.`, + note: credentials.mode === "production" + ? `Image saved locally. Use imageUrl="${uploadUrl}" with upload_record_image — it is a filesystem path that will be read and uploaded as base64 to production.` + : `Image saved. To assign it with upload_record_image, use imageUrl="${uploadUrl}". dockerUrl is mainly for local preview/debugging.`, }, null, 2), }], }; diff --git a/mcp-server/tools/media/upload.js b/mcp-server/tools/media/upload.js index f7b86bd..6d98be9 100644 --- a/mcp-server/tools/media/upload.js +++ b/mcp-server/tools/media/upload.js @@ -1,3 +1,5 @@ +import fs from "node:fs"; +import path from "node:path"; import { z } from "zod"; import { withAuth, getSessionCredentials } from "../../auth/index.js"; import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js"; @@ -19,6 +21,81 @@ async function mcpPost(target, actionWs, payload, token, tokenHash) { ); } +/** + * Si imageUrl apunta a un host local (localhost, 127.0.0.1, acai-app), + * descarga el archivo y lo retorna como base64 para incluirlo en el payload. + * Esto permite subir imagenes locales a un servidor remoto (modo produccion), + * ya que el servidor remoto no tiene acceso a nuestras URLs locales. + * + * @param {string} imageUrl + * @returns {Promise<{fileBase64: string, fileName: string} | null>} + * null si la URL no es local (usar imageUrl directamente) + */ +async function resolveLocalImageAsBase64(imageUrl) { + const LOCAL_HOSTS = ["localhost", "127.0.0.1", "acai-app", "host.docker.internal"]; + + // Caso 1: Path absoluto del filesystem (e.g. /opt/acai/webs/.../cms/uploads/x.jpg) + if (typeof imageUrl === "string" && imageUrl.startsWith("/") && !imageUrl.startsWith("//")) { + try { + if (fs.existsSync(imageUrl) && fs.statSync(imageUrl).isFile()) { + const buffer = fs.readFileSync(imageUrl); + return { + fileBase64: buffer.toString("base64"), + fileName: path.basename(imageUrl), + }; + } + } catch (error) { + console.error(`[upload] Failed to read filesystem path ${imageUrl}: ${error.message}`); + } + return null; + } + + // Caso 2: URL HTTP — verificar si es local + let parsed; + try { + parsed = new URL(imageUrl); + } catch { + return null; + } + if (!LOCAL_HOSTS.includes(parsed.hostname)) { + return null; + } + + // Intento A: descargar via HTTP (funciona cuando el host local es alcanzable) + try { + const axios = (await import("axios")).default; + const response = await axios.get(imageUrl, { + responseType: "arraybuffer", + timeout: 30000, + }); + const fileBase64 = Buffer.from(response.data).toString("base64"); + const pathname = parsed.pathname || "/image.jpg"; + const fileName = pathname.split("/").pop() || "image.jpg"; + return { fileBase64, fileName }; + } catch (httpError) { + console.error(`[upload] HTTP fetch failed for ${imageUrl}: ${httpError.message}. Trying filesystem fallback.`); + } + + // Intento B: resolver el pathname contra ACAI_PROJECT_DIR y leer del disco + const projectDir = process.env.ACAI_PROJECT_DIR || ""; + if (projectDir && parsed.pathname) { + try { + const localPath = path.join(projectDir, parsed.pathname); + if (fs.existsSync(localPath) && fs.statSync(localPath).isFile()) { + const buffer = fs.readFileSync(localPath); + return { + fileBase64: buffer.toString("base64"), + fileName: path.basename(localPath), + }; + } + } catch (error) { + console.error(`[upload] Filesystem fallback failed for ${imageUrl}: ${error.message}`); + } + } + + return null; +} + export function registerUploadRecordImageTool(server) { server.tool( "upload_record_image", @@ -42,11 +119,17 @@ export function registerUploadRecordImageTool(server) { const credentials = await getSessionCredentials(extra.sessionId); - // Upload via mcp_respond.php uploadRecordImage (sends imageUrl, PHP downloads it) + // Si la URL es local, descargar y enviar base64 (el servidor remoto no + // puede descargarla en modo produccion). + const localFile = await resolveLocalImageAsBase64(imageUrl); + const uploadPayload = localFile + ? { tableName, recordId, fieldName, alt, fileBase64: localFile.fileBase64, fileName: localFile.fileName } + : { tableName, recordId, fieldName, imageUrl, alt }; + const response = await mcpPost( credentials, "uploadRecordImage", - { tableName, recordId, fieldName, imageUrl, alt }, + uploadPayload, credentials.token, credentials.tokenHash ); @@ -170,10 +253,17 @@ export function registerUploadRecordImageTool(server) { ); // Step 2: Upload new image + // Si la URL es local, descargar y enviar base64 (el servidor remoto no + // puede descargarla en modo produccion). + const localFile = await resolveLocalImageAsBase64(imageUrl); + const uploadPayload = localFile + ? { tableName, recordId, fieldName, alt, fileBase64: localFile.fileBase64, fileName: localFile.fileName } + : { tableName, recordId, fieldName, imageUrl, alt }; + const response = await mcpPost( credentials, "uploadRecordImage", - { tableName, recordId, fieldName, imageUrl, alt }, + uploadPayload, credentials.token, credentials.tokenHash ); diff --git a/src/config.py b/src/config.py index 7dc2f73..ab13ba5 100644 --- a/src/config.py +++ b/src/config.py @@ -65,7 +65,7 @@ class Settings(BaseSettings): # --- Orchestrator --- max_execution_steps: int = 25 - subagent_max_steps: int = 10 + subagent_max_steps: int = 30 max_execution_timeout_seconds: float = 300.0 # 5 min global timeout # --- SSE --- diff --git a/src/models/agent.py b/src/models/agent.py index 175a304..79a3d7b 100644 --- a/src/models/agent.py +++ b/src/models/agent.py @@ -39,6 +39,6 @@ class SubAgentDefinition(BaseModel): profile: AgentProfile input_schema: dict[str, Any] = Field(default_factory=dict) output_schema: dict[str, Any] = Field(default_factory=dict) - max_steps: int = 10 + max_steps: int = 30 requires_approval: bool = False description: str = "" diff --git a/src/orchestrator/agents/base.py b/src/orchestrator/agents/base.py index d781537..235c22f 100644 --- a/src/orchestrator/agents/base.py +++ b/src/orchestrator/agents/base.py @@ -45,7 +45,7 @@ class BaseAgent: async def execute( self, session: SessionState, - max_steps: int = 10, + max_steps: int = 30, ) -> dict[str, Any]: """Run the agent's execution loop.