Skip to content

源码教程:文件上传、截图、PDF 与 OCR ​

这一组命令连接浏览器对象与服务端文件系统。路径属于服务端,不能把客户端路径直接当成服务器可以打开的文件。文件传输协议见第 12 篇,OCR 使用方式见第 16 篇。

1. upload_file 的两个入口分支 ​

注册层先检查 contentBase64 或 url:存在时调用 uploadFileInline,先落盘,再进入页面上传;否则要求 path,调用 uploadFile。因此一次调用应明确选择一种来源,不要同时传互相矛盾的文件来源。

页面上传最终定位 input[type=file] 并使用浏览器文件输入能力设置文件。file input 常被隐藏,不能套用普通按钮的“只挑可见元素”策略。selector 与 index 至少选择一个,iframe 中的 selector 还需要 frame。

json
{"id":"1001","method":"upload_file","params":{"selector":"input[type=file]","path":"tmp/sample-logo.png"}}

path 需要按上传存储模块的解析规则使用;通过上传接口获得的相对路径按服务端上传目录解析,使用绝对路径时则指向服务端已有文件。不要假定所有命令的相对路径基准相同,OCR 文件路径的基准是进程工作目录。

实现不能只检查 setInputFiles 是否返回:框架可能消费文件后重置 input,甚至替换整个节点。回读应解释 filesLength、监听信息、consumed 和页面变化,避免因新 input 为空误判失败。上传后出现裁剪窗口时,需完成裁剪,不能直接再次上传。

2. screenshot 的目标选择 ​

screenshot 优先处理 index/selector 指定的元素;没有元素定位时使用 Page.screenshot。只有 clipX、clipY、clipWidth、clipHeight 全部存在时才设置裁剪区域。fullPage 控制整页截图,不应同时把“元素截图”和“整页截图”当成两张产物。

默认不返回 Base64。未指定 path 且没有要求 inline 时,会生成任务目录中的 shot-N.png;指定 inline 才把图片编码放入结果。图像路径在任务 data 目录内时可生成 image URL,任意本地路径不会自动变成公开下载地址。

截图期间实现会隐藏交互高亮层,再恢复它,避免彩色编号挡住二维码或表单。截图失败应返回错误或降级说明,不凭页面文本声称已经取得图片。

json
{"id":"1001","method":"screenshot","params":{"fullPage":false,"inline":false}}

3. get_element_screenshot 与 pdf ​

get_element_screenshot 复用元素截图辅助逻辑,支持 frame。它通过浏览器截图获取像素,不是将跨域图片画到 canvas 后读取,所以不受同一种 canvas 污染限制。返回路径、URL 或显式请求的 Base64。

pdf 使用当前 Page 的 PDF 能力并保存文件,不是先做 OCR 再生成文档。该能力受浏览器引擎支持范围约束;不能向所有引擎承诺可用。导出前应等待字体、图片和业务内容完成加载。

4. ocr_image 的源码链 ​

text
path 分支:解析文件路径并检查存在
元素分支:解析任务与 Frame → 元素截图
 → WindowsOcr.read
 → 将内置脚本写临时文件(处理 UTF-8 BOM)
 → Windows PowerShell 调用 Windows.Media.Ocr
 → 从输出文件读取文字与错误标识
 → data.ok/text/language/lineCount 等字段
 → 清理临时脚本和输出文件

PlaywrightService.ocrImage 即使识别失败,也可能以外层成功响应返回内部 OCR 结果。客户端必须检查 data.ok。语言缺失、平台不支持、文件不存在和文字识别为空是不同情况。

请求 language 不等于实际引擎语言;当前脚本会尝试用户语言回退,但未返回实际 engineLanguage。阅读源码时应核对返回语句,不能只照抄注释。

5. 验证方法 ​

使用无个人信息的本地示例图片测试上传、节点替换、隐藏 input 和裁剪流程;验证默认截图只返回文件位置,inline 才返回 Base64,路径与文件实际一致。OCR 使用人为制作的示例文字,并分别检查语言包可用与不可用的返回。截图和 OCR 结果均应按敏感资料管理,默认留档不等于自动脱敏。

注册参数与 Java 入口 ​

以下按 CommandTable 实际读取参数整理。* 表示注册层使用必填读取器;其余字段省略后由服务决定默认行为。带条件的入口仍需满足正文说明,例如上传文件来源、元素定位二选一。外层 id 不重复列出。

命令params 字段Java 入口
upload_filecontentBase64、url、index、selector、filename、contentType、timeoutMs、frame、path*uploadFile / uploadFileInline
screenshotpath、fullPage、index、selector、clipX、clipY、clipWidth、clipHeight、inlinescreenshot
get_element_screenshotindex、selector、path、inline、framegetElementScreenshot
pdfpathpdf
ocr_imagepath、index、selector、frame、languageocrImage

当前源码:命令注册与执行 ​

先在本章上半部分理解行为,再按命令展开实现。注册代码说明 JSON 参数如何传给 Java;服务方法展示实际浏览器操作。方法依赖共享类中的字段和辅助函数,不应脱离原类直接粘贴编译。

upload_file ​

展开参数注册

源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
    put("upload_file", (svc, id, a) -> {
      if (optStr(a, "contentBase64") != null || optStr(a, "url") != null) {
        return svc.uploadFileInline(id, a.getInteger("index"), optStr(a, "selector"), optStr(a, "filename"),
            optStr(a, "contentType"), optStr(a, "contentBase64"), optStr(a, "url"), a.getInteger("timeoutMs"),
            optStr(a, "frame"));
      }
      return svc.uploadFile(id, a.getInteger("index"), optStr(a, "selector"), reqStr(a, "path"),
          a.getInteger("timeoutMs"), optStr(a, "frame"));
    });
展开 uploadFileInline 实现

源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
  public RespBodyVo uploadFileInline(Long browserId, Integer index, String selector, String filename,
      String contentType, String contentBase64, String url, Integer timeoutMs, String frame) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return RespBodyVo.fail("没有找到对应的浏览器实例:" + browserId);
    }
    if ((contentBase64 == null || contentBase64.isBlank()) && (url == null || url.isBlank())) {
      return RespBodyVo.fail("upload_file 需要 path,或 contentBase64 / url 之一");
    }
    byte[] data;
    String resolvedContentType = contentType;
    try {
      if (contentBase64 != null && !contentBase64.isBlank()) {
        String payload = contentBase64.trim();
        // 允许直接贴 data URI
        int comma = payload.indexOf(',');
        if (payload.startsWith("data:") && comma > 0) {
          String header = payload.substring(5, comma);
          int semicolon = header.indexOf(';');
          if (resolvedContentType == null) {
            resolvedContentType = semicolon > 0 ? header.substring(0, semicolon) : header;
          }
          payload = payload.substring(comma + 1);
        }
        data = Base64.getDecoder().decode(payload.replaceAll("\\s+", ""));
      } else {
        data = download(url, resolvedContentType);
        if (resolvedContentType == null) {
          resolvedContentType = contentTypeOf(url);
        }
      }
    } catch (IllegalArgumentException e) {
      return RespBodyVo.fail("upload_file 失败:contentBase64 不是合法的 base64(" + e.getMessage() + ")");
    } catch (IOException e) {
      return RespBodyVo.fail("upload_file 失败:下载 " + url + " 出错(" + briefMessage(e.getMessage()) + ")");
    }
    Kv saved;
    try {
      saved = UploadStore.save(filename, resolvedContentType, data);
    } catch (IOException e) {
      return RespBodyVo.fail("upload_file 失败:写入服务端暂存目录出错(" + briefMessage(e.getMessage()) + ")");
    }
    RespBodyVo uploaded = uploadFile(browserId, index, selector, saved.getStr("path"), timeoutMs, frame);
    if (!uploaded.isOk()) {
      return uploaded;
    }
    Kv data2 = uploaded.getData() instanceof Kv ? (Kv) uploaded.getData() : new Kv();
    data2.set(saved);
    data2.set("source", contentBase64 != null && !contentBase64.isBlank() ? "contentBase64" : "url");
    uploaded.setData(data2);
    return uploaded;
  }

screenshot ​

展开参数注册

源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
    put("screenshot",
        (svc, id, a) -> svc.screenshot(id, optStr(a, "path"), a.getBoolean("fullPage"), a.getInteger("index"),
            optStr(a, "selector"), a.getDouble("clipX"), a.getDouble("clipY"), a.getDouble("clipWidth"),
            a.getDouble("clipHeight"), a.getBoolean("inline")));
展开 screenshot 实现

源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
  public RespBodyVo screenshot(Long browserId, String path, Boolean fullPage, Integer index, String selector,
      Double clipX, Double clipY, Double clipWidth, Double clipHeight, Boolean inline) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    if (index != null || (selector != null && !selector.isEmpty())) {
      return elementScreenshot(inst, index, selector, path, inline);
    }
    boolean wantInline = inline != null && inline;
    String target = path;
    if ((target == null || target.isEmpty()) && !wantInline) {
      target = defaultShotPath(inst);
    }
    Page.ScreenshotOptions options = new Page.ScreenshotOptions().setFullPage(fullPage != null && fullPage);
    if (clipX != null && clipY != null && clipWidth != null && clipHeight != null) {
      options.setClip(clipX, clipY, clipWidth, clipHeight);
    }
    if (target != null && !target.isEmpty()) {
      ensureParent(target);
      options.setPath(Paths.get(target));
    }
    try {
      byte[] bytes = spuriousRetry(() -> withHighlightHidden(inst, () -> inst.page.screenshot(options)));
      Kv data = Kv.by("size", bytes.length);
      if (target != null && !target.isEmpty()) {
        data.set("path", target);
        data.set("url", shotUrl(inst, target));
      }
      if (wantInline) {
        data.set("base64", Base64.getEncoder().encodeToString(bytes));
        data.set("inline", true);
      } else {
        data.set("inline", false).set("base64Omitted", true).set("note",
            "默认不返回 base64(会把整张图塞进上下文):要看图请 GET data.url,确实需要内联再传 inline=true");
      }
      return RespBodyVo.ok(data);
    } catch (PlaywrightException e) {
      return RespBodyVo.fail("screenshot 失败:" + briefMessage(e.getMessage()));
    }
  }

get_element_screenshot ​

展开参数注册

源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
    put("get_element_screenshot", (svc, id, a) -> svc.getElementScreenshot(id, a.getInteger("index"),
        optStr(a, "selector"), optStr(a, "path"), a.getBoolean("inline"), optStr(a, "frame")));
展开 getElementScreenshot 实现

源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
  public RespBodyVo getElementScreenshot(Long browserId, Integer index, String selector, String path,
      Boolean inline, String frame) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    return elementScreenshot(inst, index, selector, path, inline, frame);
  }

pdf ​

展开参数注册

源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
    put("pdf", (svc, id, a) -> svc.pdf(id, optStr(a, "path")));
展开 pdf 实现

源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
  public RespBodyVo pdf(Long browserId, String path) {
    BrowserInstance inst = INSTANCES.get(browserId);
    if (inst == null) {
      return notFound(browserId);
    }
    SharedBrowser browser = sharedBrowser;
    if (browser != null && browser.engine.isFirefox()) {
      // 与其让 Playwright 抛一句英文的「only supported in Chromium」,不如直接说清怎么改
      return RespBodyVo.fail("pdf 失败:PDF 导出只有 Chromium 支持,当前引擎是 firefox"
          + "(browser=firefox 或 browser.engine=firefox),需要 PDF 时请换成 Chromium 系的浏览器再 start:"
          + "browser=chrome / browser=chromium / browser=edge");
    }
    String target = (path == null || path.isEmpty()) ? defaultPdfPath() : path;
    try {
      ensureParent(target);
      inst.page.pdf(new Page.PdfOptions().setPath(Paths.get(target)));
      return RespBodyVo.ok(Kv.by("path", target));
    } catch (PlaywrightException e) {
      return RespBodyVo.fail("pdf 失败:" + briefMessage(e.getMessage()));
    }
  }

ocr_image ​

展开参数注册

源码:playwright-server/src/main/java/nexus/io/ai/browser/actions/registry/CommandTable.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
    put("ocr_image", (svc, id, a) -> svc.ocrImage(id, optStr(a, "path"), a.getInteger("index"),
        optStr(a, "selector"), optStr(a, "frame"), optStr(a, "language")));
展开 ocrImage 实现

源码:playwright-server/src/main/java/nexus/io/ai/browser/service/PlaywrightService.java。以下为当前实现,可放回原类中阅读;依赖同类字段和辅助方法,并非独立编译单元。

java
  public RespBodyVo ocrImage(Long browserId, String path, Integer index, String selector, String frame,
      String language) {
    Path image = null;
    String imageUrl = null;
    String target;
    if (path != null && !path.isBlank()) {
      image = Paths.get(path).toAbsolutePath().normalize();
      target = "path=" + path;
      BrowserInstance inst = INSTANCES.get(browserId);
      if (inst != null) {
        // 落盘在 data/<id>/ 下的图可以直接 GET,贴给用户看
        imageUrl = shotUrl(inst, image.toString());
      }
    } else if (index != null || (selector != null && !selector.isBlank())) {
      BrowserInstance inst = INSTANCES.get(browserId);
      if (inst == null) {
        return notFound(browserId);
      }
      String shotPath = defaultShotPath(inst);
      RespBodyVo shot = elementScreenshot(inst, index, selector, shotPath, Boolean.FALSE, frame);
      if (!shot.isOk()) {
        return shot;
      }
      Kv shotData = shot.getData() instanceof Kv ? (Kv) shot.getData() : new Kv();
      image = shotData.getStr("path") == null ? null : Paths.get(shotData.getStr("path"));
      imageUrl = shotData.getStr("url");
      target = String.valueOf(shotData.get("target"));
    } else {
      return RespBodyVo.fail("ocr_image 需要 path,或 index / selector 之一");
    }
    if (image == null || !Files.isRegularFile(image)) {
      return RespBodyVo.fail("ocr_image 找不到图片:" + image);
    }
    Kv read = WindowsOcr.read(image, language);
    Kv data = new Kv();
    data.putAll(read);
    data.set("target", target).set("imagePath", image.toAbsolutePath().toString());
    if (imageUrl != null) {
      data.set("imageUrl", imageUrl);
    }
    if (!Boolean.TRUE.equals(read.get("ok"))) {
      data.set("ocrSupported", WindowsOcr.available());
    }
    return RespBodyVo.ok(data);
  }