analyze/upload vía /api/image-bytes + MCP HTTP (vscode) forzado a test

Imágenes:
- analyze_image y upload resuelven los bytes por el endpoint Python
  /api/image-bytes (pythonGetBinary). analyze_image enruta los dominios
  forge (env ACAI_FORGE_DOMAIN) al endpoint en vez de fetch directo (que
  daba ECONNREFUSED 127.0.0.1 dentro del container).

Aislamiento de entorno (vscode = solo test):
- resolveCurrentModeOverride(): sesión MCP HTTP (mcpSessionId presente) →
  "local"; stdio (chat/cron) → ACAI_MODE_OVERRIDE de entorno. Lo usan los
  builders de headers (pythonServerClient, files/helpers) → toda tool del
  MCP HTTP manda X-Acai-Mode: local.
- httpServer.resolveProjectCredentials fuerza forceMode:"local" al resolver
  project-info → la sesión obtiene web_url/api_web_url forge-local y opera
  siempre contra test, nunca producción.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jordan
2026-06-19 19:11:50 +01:00
parent 5883473e92
commit 5dc2dbcf4a
7 changed files with 120 additions and 118 deletions

View File

@@ -5,7 +5,7 @@ import { withAuth, getSessionCredentials } from "../../auth/index.js";
import { handleToolError, validateRequired, handleApiResponse } from "../helpers/errorHandler.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { AcaiHttpClient } from "../helpers/acaiHttpClient.js";
import { pythonPost } from "../helpers/pythonServerClient.js";
import { pythonPost, pythonGetBinary } from "../helpers/pythonServerClient.js";
import { resolveCurrentProjectDir } from "../files/helpers.js";
/**
@@ -34,68 +34,32 @@ async function mcpPost(target, actionWs, payload, token, tokenHash) {
* 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"];
const LOCAL_HOSTS = ["localhost", "127.0.0.1", "acai-app", "acai-web", "web", "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;
// URL http(s) con host NO local → es una URL pública real: usar tal cual (null).
if (typeof imageUrl === "string" && /^https?:\/\//i.test(imageUrl)) {
let parsed;
try { parsed = new URL(imageUrl); } catch { return null; }
if (!LOCAL_HOSTS.includes(parsed.hostname)) return null;
}
// Caso 2: URL HTTP — verificar si es local
let parsed;
// Ruta del proyecto, host local o chat-preview → los bytes los resuelve el
// server Python (/api/image-bytes): disco para standalone, fetch de producción
// para imágenes Acai cuyo fichero local es un stub.
const project = path.basename(resolveCurrentProjectDir() || "");
if (!project) return null;
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 = resolveCurrentProjectDir();
if (projectDir && parsed.pathname) {
const { buffer } = await pythonGetBinary("/api/image-bytes", { project, ref: imageUrl });
let fileName = "image.jpg";
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}`);
}
const p = imageUrl.startsWith("/") ? imageUrl : new URL(imageUrl).pathname;
fileName = (p.split("?")[0].split("/").pop()) || "image.jpg";
} catch { /* keep default */ }
return { fileBase64: buffer.toString("base64"), fileName };
} catch (error) {
console.error(`[upload] /api/image-bytes falló para ${imageUrl}: ${error.message}`);
return null;
}
return null;
}
export function registerUploadRecordImageTool(server) {