Ctrl K
ComunidadGuías…

lib/project-image.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
var fs = require("fs");
var path = require("path");
var crypto = require("crypto");
var execFileSync = require("child_process").execFileSync;

/**
 * Attach image handling to a project context.
 *
 * ctx fields:
 *   cwd, slug
 */
function attachImage(ctx) {
  var cwd = ctx.cwd;
  var slug = ctx.slug;
  var fsAsUser = ctx.fsAsUser;

  // --- Chat image storage ---
  var _imgConfig = require("./config");
  var _imgUtils = require("./utils");
  var _imagesBaseDir = path.join(_imgConfig.CONFIG_DIR, "images");
  var _imagesEncodedCwd = _imgUtils.encodeCwd(cwd);
  var imagesDir = ctx.imagesDir || path.join(_imagesBaseDir, _imagesEncodedCwd);

  function isWithin(base, candidate) {
    return candidate === base || candidate.indexOf(base + path.sep) === 0;
  }

  function saveGeneratedImageToProject(fileName, requestedPath, overwrite, osUserInfo) {
    if (typeof fileName !== "string" || !/^\d+-[a-f0-9]+\.\w+$/.test(fileName)) {
      return { ok: false, status: 400, error: "Invalid generated image" };
    }
    if (typeof requestedPath !== "string" || !requestedPath.trim() || requestedPath.length > 500) {
      return { ok: false, status: 400, error: "Enter a project path" };
    }

    var sourcePath = path.join(imagesDir, fileName);
    var sourceExt = path.extname(fileName).toLowerCase();
    var targetPath = requestedPath.trim();
    if (path.isAbsolute(targetPath)) {
      return { ok: false, status: 400, error: "Use a project-relative path" };
    }
    var targetExt = path.extname(targetPath).toLowerCase();
    if (targetExt !== sourceExt) {
      return { ok: false, status: 400, error: "Keep the " + sourceExt + " file extension" };
    }

    var projectRoot;
    var targetAbs;
    try {
      projectRoot = fs.realpathSync(cwd);
      targetAbs = path.resolve(projectRoot, targetPath);
    } catch (e) {
      return { ok: false, status: 400, error: "Project path is not available" };
    }
    if (targetAbs === projectRoot || !isWithin(projectRoot, targetAbs)) {
      return { ok: false, status: 403, error: "Path must stay inside the project" };
    }

    try {
      var sourceReal = fs.realpathSync(sourcePath);
      var imagesRoot = fs.realpathSync(imagesDir);
      if (!isWithin(imagesRoot, sourceReal) || !fs.statSync(sourceReal).isFile()) {
        return { ok: false, status: 404, error: "Generated image not found" };
      }

      var parentDir = path.dirname(targetAbs);
      var existingParent = parentDir;
      while (!fs.existsSync(existingParent) && existingParent !== projectRoot) {
        existingParent = path.dirname(existingParent);
      }
      var realExistingParent = fs.realpathSync(existingParent);
      if (!isWithin(projectRoot, realExistingParent)) {
        return { ok: false, status: 403, error: "Path must stay inside the project" };
      }
      if (osUserInfo && typeof fsAsUser === "function") {
        fsAsUser("mkdir", { dir: parentDir }, osUserInfo);
      } else {
        fs.mkdirSync(parentDir, { recursive: true });
      }
      var realParent = fs.realpathSync(parentDir);
      if (!isWithin(projectRoot, realParent)) {
        return { ok: false, status: 403, error: "Path must stay inside the project" };
      }
      if (fs.existsSync(targetAbs)) {
        if (fs.lstatSync(targetAbs).isSymbolicLink()) {
          return { ok: false, status: 403, error: "Symbolic link targets cannot be replaced" };
        }
        if (!overwrite) {
          return { ok: false, status: 409, error: "A file already exists at this path" };
        }
      }

      if (osUserInfo && typeof fsAsUser === "function") {
        fsAsUser("copy", { source: sourceReal, file: targetAbs, overwrite: !!overwrite }, osUserInfo);
      } else {
        fs.copyFileSync(sourceReal, targetAbs, overwrite ? 0 : fs.constants.COPYFILE_EXCL);
      }
      try { fs.chmodSync(targetAbs, 0o644); } catch (e2) {}
      return { ok: true, path: path.relative(projectRoot, targetAbs).split(path.sep).join("/") };
    } catch (e) {
      if (e && e.code === "EEXIST") {
        return { ok: false, status: 409, error: "A file already exists at this path" };
      }
      return { ok: false, status: 500, error: "Could not save the generated image" };
    }
  }

  // Convert imageRefs in history entries to images with URLs for the client
  function hydrateImageRefs(entry) {
    if (!entry) return entry;
    // Hydrate context_preview: convert screenshotFile to screenshotUrl
    if (entry.type === "context_preview" && entry.tab && entry.tab.screenshotFile) {
      var hydrated = {};
      for (var k in entry) hydrated[k] = entry[k];
      hydrated.tab = {};
      for (var tk in entry.tab) hydrated.tab[tk] = entry.tab[tk];
      hydrated.tab.screenshotUrl = "/p/" + slug + "/images/" + entry.tab.screenshotFile;
      delete hydrated.tab.screenshotFile;
      return hydrated;
    }
    if (!entry.imageRefs) return entry;
    if (entry.type !== "user_message" && entry.type !== "mention_user" && entry.type !== "user_mention" && entry.type !== "generated_image") return entry;
    var images = [];
    for (var ri = 0; ri < entry.imageRefs.length; ri++) {
      var ref = entry.imageRefs[ri];
      images.push({ mediaType: ref.mediaType, url: "/p/" + slug + "/images/" + ref.file });
    }
    var hydrated = {};
    for (var k2 in entry) {
      if (k2 !== "imageRefs") hydrated[k2] = entry[k2];
    }
    hydrated.images = images;
    return hydrated;
  }

  function saveImageFile(mediaType, base64data, ownerLinuxUser) {
    try { fs.mkdirSync(imagesDir, { recursive: true }); } catch (e) {}
    var ext = mediaType === "image/png" ? ".png" : mediaType === "image/gif" ? ".gif" : mediaType === "image/webp" ? ".webp" : ".jpg";
    var hash = crypto.createHash("sha256").update(base64data).digest("hex").substring(0, 16);
    var fileName = Date.now() + "-" + hash + ext;
    var filePath = path.join(imagesDir, fileName);
    try {
      fs.writeFileSync(filePath, Buffer.from(base64data, "base64"));
      if (process.platform !== "win32") {
        // 644 so all local users can read (needed for git, copy, etc.)
        try { fs.chmodSync(filePath, 0o644); } catch (e) {}
        // In OS-user mode the daemon runs as root, so chown the file
        // (and parent dirs) to the session owner to avoid permission issues.
        if (ownerLinuxUser) {
          try {
            var osUsersMod = require("./os-users");
            var uid = osUsersMod.getLinuxUserUid(ownerLinuxUser);
            if (uid != null) {
              execFileSync("chown", [String(uid), filePath]);
              // Also fix parent dirs if root-owned
              try {
                var dirStat = fs.statSync(imagesDir);
                if (dirStat.uid !== uid) {
                  execFileSync("chown", [String(uid), imagesDir]);
                }
              } catch (e2) {}
            }
          } catch (e) {}
        }
      }
      return fileName;
    } catch (e) {
      console.error("[images] Failed to save image:", e.message);
      return null;
    }
  }

  return {
    imagesDir: imagesDir,
    hydrateImageRefs: hydrateImageRefs,
    saveImageFile: saveImageFile,
    saveGeneratedImageToProject: saveGeneratedImageToProject,
  };
}

module.exports = { attachImage: attachImage };