<!-- Source: https://deepseek-browser-use.io.nexus/22 -->

# 源码教程：文件上传、截图、PDF 与 OCR

这一组命令连接浏览器对象与服务端文件系统。路径属于服务端，不能把客户端路径直接当成服务器可以打开的文件。文件传输协议见[第 12 篇](<https://deepseek-browser-use.io.nexus/09>)，OCR 使用方式见[第 16 篇](<https://deepseek-browser-use.io.nexus/11>)。

## 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_file` | `contentBase64`、`url`、`index`、`selector`、`filename`、`contentType`、`timeoutMs`、`frame`、`path`* | `uploadFile` / `uploadFileInline` |
| `screenshot` | `path`、`fullPage`、`index`、`selector`、`clipX`、`clipY`、`clipWidth`、`clipHeight`、`inline` | `screenshot` |
| `get_element_screenshot` | `index`、`selector`、`path`、`inline`、`frame` | `getElementScreenshot` |
| `pdf` | `path` | `pdf` |
| `ocr_image` | `path`、`index`、`selector`、`frame`、`language` | `ocrImage` |

<!-- current-source-tutorial -->
## 当前源码：命令注册与执行

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

### `upload_file`

::: details 展开参数注册

源码：`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"));
    });
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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`

::: details 展开参数注册

源码：`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")));
```

:::

::: details 展开 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);
  }
```

:::
