You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js二次请求截图时文件无法打开错误的解决求助

问题描述

我用Node.js做了个服务,能捕获所有显示器截图并合并成一张图。第一次请求正常,但不用重启服务器的第二次请求就会报错,文件打不开。试过先删旧文件再创建新的,结果碰到error eperm operation not permitted unlink错误,只有关了服务器才能删文件,求解决办法。

原代码

const express = require('express');
const app = express();
const port = 3000;
const fs = require('fs');


app.get('/', (req, res) => {
  hello = 'Hello World!';
  console.log(hello);
  res.send(hello);
});

app.get('/screenshot', (req, res) => {
  const screenshot = require('screenshot-desktop');
  const sharp = require('sharp');

  screenshot.listDisplays().then((displays) => {
    let screenshots = [];
    displays.forEach((display, i) => {
      const screenshotPath = `screenshot${i}.png`;
      screenshot({ screen: display.id }).then((imgBuffer) => { 
        fs.writeFile(screenshotPath, imgBuffer, (err) => { 
          if (err) throw err;
          console.log('Screenshot salvo!');
          screenshots.push(screenshotPath);
          if (screenshots.length === displays.length) {
            // Todas as screenshots foram salvas, agora junte-as
            Promise.all([
              sharp(screenshots[1]).metadata(),
              sharp(screenshots[0]).metadata()
            ]).then(([metadata1, metadata2]) => {
              sharp(screenshots[1])
                .extend({
                  top: 0,
                  bottom: 0,
                  left: 0,
                  right: metadata2.width,
                  background: { r: 0, g: 0, b: 0, alpha: 0 }
                })
                .composite([{ input: screenshots[0], left: metadata1.width, top: 0 }])
                .toBuffer()
                .then((buffer) => {
                  let base64Image = `data:image/png;base64,${buffer.toString('base64')}`;
                  res.send(base64Image);
                })
                .catch((err) => console.error(err));
            }).catch(err => console.error(err));
          }
        });
      }, (err) => {
        console.error(err);
      });
    });
  });
});

app.listen(port, () => {
  console.log(`Servidor rodando em http://localhost:${port}`);
});

错误信息

Servidor rodando em http://localhost:3000
Screenshot salvo!
Screenshot salvo!
F:\Andre\sk-access-control-ai\facialRecog\server.js:23
          if (err) throw err;
                   ^

[Error: UNKNOWN: unknown error, open 'facialRecog\screenshot0.png'] {
  errno: -4094,
  code: 'UNKNOWN',
  syscall: 'open',
  path: 'facialRecog\\screenshot0.png'
}

Node.js v20.12.2

解决办法

核心问题是sharp处理文件后未释放文件句柄,导致系统锁定文件,二次请求时无法写入/删除。另外原代码还存在异步流程混乱、重复导入模块的问题,以下是优化后的方案:

最优方案:全程用Buffer处理(彻底避免文件锁定)

直接在内存中处理截图Buffer,完全跳过本地文件写入,从根源解决文件被占用的问题:

const express = require('express');
const app = express();
const port = 3000;
const screenshot = require('screenshot-desktop');
const sharp = require('sharp');

app.get('/', (req, res) => {
  const hello = 'Hello World!';
  console.log(hello);
  res.send(hello);
});

app.get('/screenshot', async (req, res) => {
  try {
    // 获取所有显示器列表
    const displays = await screenshot.listDisplays();
    // 并行捕获所有显示器的截图Buffer
    const screenshotBuffers = await Promise.all(
      displays.map(display => screenshot({ screen: display.id }))
    );

    // 获取每个截图的元数据(宽高)
    const metadataList = await Promise.all(
      screenshotBuffers.map(buffer => sharp(buffer).metadata())
    );

    // 计算合并后的总宽度和最大高度
    const totalWidth = metadataList.reduce((sum, meta) => sum + meta.width, 0);
    const maxHeight = Math.max(...metadataList.map(meta => meta.height));

    // 初始化基础画布,以第一张图为起点扩展宽度
    let combinedBuffer = await sharp(screenshotBuffers[0])
      .extend({
        top: 0,
        bottom: maxHeight - metadataList[0].height,
        left: 0,
        right: totalWidth - metadataList[0].width,
        background: { r: 0, g: 0, b: 0, alpha: 0 }
      })
      .toBuffer();

    // 依次合并剩余截图,垂直居中对齐
    let currentX = metadataList[0].width;
    for (let i = 1; i < screenshotBuffers.length; i++) {
      combinedBuffer = await sharp(combinedBuffer)
        .composite([{
          input: screenshotBuffers[i],
          left: currentX,
          top: (maxHeight - metadataList[i].height) / 2
        }])
        .toBuffer();
      currentX += metadataList[i].width;
    }

    // 返回base64格式图片
    const base64Image = `data:image/png;base64,${combinedBuffer.toString('base64')}`;
    res.send(base64Image);
  } catch (err) {
    console.error('截图合并失败:', err);
    res.status(500).send('截图生成失败');
  }
});

app.listen(port, () => {
  console.log(`Servidor rodando em http://localhost:${port}`);
});

修改说明

  • 把require移到顶部,避免每次请求重复加载模块
  • 用async/await替代嵌套Promise,代码逻辑更清晰
  • 全程基于内存Buffer处理,彻底绕开本地文件操作,解决文件锁定问题
  • 用Promise.all并行处理截图和元数据,提升处理效率
  • 增加全局错误捕获,确保请求不会挂起
  • 新增垂直居中对齐逻辑(可根据需求调整)

备选方案:使用临时文件并确保清理

如果必须写入本地文件,需使用唯一命名的临时文件,并在处理完成后强制删除:

const express = require('express');
const app = express();
const port = 3000;
const fs = require('fs/promises');
const path = require('path');
const screenshot = require('screenshot-desktop');
const sharp = require('sharp');

app.get('/', (req, res) => {
  const hello = 'Hello World!';
  console.log(hello);
  res.send(hello);
});

app.get('/screenshot', async (req, res) => {
  const tempFiles = [];
  try {
    const displays = await screenshot.listDisplays();
    // 生成带时间戳的临时文件路径,避免重名
    const screenshotPaths = displays.map((_, i) => 
      path.join(__dirname, `temp_${Date.now()}_screenshot_${i}.png`)
    );
    tempFiles.push(...screenshotPaths);

    // 并行写入临时文件
    await Promise.all(
      displays.map(async (display, i) => {
        const imgBuffer = await screenshot({ screen: display.id });
        await fs.writeFile(screenshotPaths[i], imgBuffer);
      })
    );

    // 合并图片逻辑(和原代码类似)
    const [metadata1, metadata2] = await Promise.all([
      sharp(screenshotPaths[1]).metadata(),
      sharp(screenshotPaths[0]).metadata()
    ]);
    const buffer = await sharp(screenshotPaths[1])
      .extend({
        top: 0,
        bottom: 0,
        left: 0,
        right: metadata2.width,
        background: { r: 0, g: 0, b: 0, alpha: 0 }
      })
      .composite([{ input: screenshotPaths[0], left: metadata1.width, top: 0 }])
      .toBuffer();

    const base64Image = `data:image/png;base64,${buffer.toString('base64')}`;
    res.send(base64Image);
  } catch (err) {
    console.error('处理失败:', err);
    res.status(500).send('截图生成失败');
  } finally {
    // 无论成功失败,都删除临时文件
    await Promise.all(tempFiles.map(file => 
      fs.unlink(file).catch(() => {})
    ));
  }
});

app.listen(port, () => {
  console.log(`Servidor rodando em http://localhost:${port}`);
});

内容的提问来源于stack exchange,提问作者André Pimentel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 04:50:55