使用技巧2026-09-28约 4 分钟阅读

TypeScript 调 PDF 接口:用 @pdf123/sdk 五分钟上手,以及它留给你处理的部分

用 @pdf123/sdk 在 TypeScript 里合并、加水印、读取文件信息,并把多个工具串成一个请求;它在上传之前拦下写错的工具名和参数,服务器的拒绝带 reason 与 hint,重试、取消和批量里的部分失败则要调用方自己处理。

PDF123 · 更新于 2026-09-28

在 Node 里调用 PDF 接口时,@pdf123/sdk 这套软件开发工具包(SDK,Software Development Kit)在文件上传之前拦下写错的工具名和参数。重试、取消、按流读取结果,它都不做。new Pdf123Client() 默认匿名指向 https://pdf123.xyz,文件上传到那边处理,没有本地引擎。

示意图:一次调用依次经过三关:编译期的 tsc 检查、发请求之前的运行时校验、上传之后服务器返回的 400;重试和取消由调用方处理,总大小超过 95 MiB 的输入由客户端自动分块上传

目录里有两个 PDF,就能合并

需要 Node 20.3 或更新版本,或者 Bun。这个包是 ES 模块:代码放在 .mjs 里,或者先运行 npm pkg set type=module。当前目录里放好 a.pdf 和 b.pdf。

npm install @pdf123/sdk
// merge.mjs
import { Pdf123Client } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const merged = await client.run("merge", {
  input: [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
});
console.log(await saveResult(merged, { output: "merged.pdf" }));

node merge.mjs 之后,终端打出你传入的输出路径 merged.pdf,当前目录里也有这个文件。readFileInput 和 saveResult 在 @pdf123/sdk/node 里。根入口只依赖 fetch,输入可以直接给 { name, data },所以不碰文件系统的环境用同一个客户端。结果是 { data, contentType, filename, json },data 是整块的 Uint8Array,要 Buffer 就 Buffer.from(result.data)。

合并只是其中一个工具。

返回文件看 data,返回报告看 json

每个工具都是 client.run(工具名, { input, params })。下面接在上一节的 merge.mjs 之后,client 和 saveResult 已经在作用域里。结果从哪里取,看 getTool 返回的 returns,只有 "file" 和 "json" 两种:前者用 data,后者用 json。

import { getTool } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

// client 来自上一节的 merge.mjs
const marked = await client.run("watermark", {
  input: await readFileInput("a.pdf"),
  params: { watermarkText: "DRAFT", fontSize: 40 },
});
console.log(await saveResult(marked, { output: "marked.pdf" }));

const info = await client.run("get-info", { input: await readFileInput("a.pdf") });
console.log(info.json.FileSize, info.json.Encrypted);
console.log(getTool("get-info")?.returns); // "json"

第一段把加水印的文件写到 marked.pdf,第二段的 info.json 是报告。字段名、默认值和可选值不要记:getTool("watermark")?.fields 就是那一份目录,命令行里的 pdfx describe watermark 读的也是它。TOOLS 里有全部 95 个工具。完整参考在开发者页面里的 SDK 指南。

先合并、再加水印、再压缩,用 pipeline 还是连着三次 run,取决于你要不要中间文件。同样接在上一节的 client 之后。pipeline 把这三步收成一个请求,中间结果留在服务器上,调用方只拿到最后一步,下面把它写成 out.pdf。要每一步的文件,就分开调用。

const result = await client.pipeline(
  [{ tool: "merge" }, { tool: "watermark", params: { watermarkText: "DRAFT" } }, { tool: "compress" }],
  [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
);
console.log(await saveResult(result, { output: "out.pdf" }));

一次最多 8 步。第 9 步得到 HTTP 400: at most 8 pipeline steps allowed。

匿名、带密钥,或指到自己的服务器

设了 PDFX_API_BASE 却仍打到 https://pdf123.xyz,是因为 new Pdf123Client() 不读环境变量,只认构造参数。

匿名调用就用这个构造函数。有密钥时写 new Pdf123Client({ apiKey: process.env.PDFX_API_KEY }),请求头是 X-API-KEY。

指向自己的服务器时,用 @pdf123/sdk/node 里的 clientFromEnv()。它读 PDFX_API_BASE 和 PDFX_API_KEY,也可以直接写 new Pdf123Client({ baseUrl: "http://localhost:8080" })。地址不可达,调用就失败,没有离线模式。自建之后文件留在自己网络里的取舍,见自托管能换来什么。

写错的参数,在上传之前就被拦下

不拦的话,文件先上传,字段名拼错要等服务器返回 400 才知道。每个工具的参数类型从目录生成,三类手误在 tsc 阶段就报错:

写法 编译器的报错(节选)
client.run("rotat", { input }) Argument of type '"rotat"' is not assignable to parameter of type 'ToolId'
params: { watermarkTxt: "DRAFT" } Object literal may only specify known properties, but 'watermarkTxt' does not exist,末尾还有 Did you mean to write 'watermarkText'?
params: { angle: 45 }(旋转只接受 90、180、270) Type '45' is not assignable to type …,后面列出允许的取值

写对的调用,比如 { angle: 90 } 或 { watermarkText: "DRAFT", fontSize: 30 },不报错。数字型字段既接受数字也接受字符串,fontSize: 30 和 fontSize: "30" 等价。不用 TypeScript 的项目,同样的错误在运行时抛出,请求不会发出:第三种写法得到 Field "angle" of "rotate" must be one of: 90, 180, 270,工具名拼错得到 unknown_tool,消息里列出相近的名字。

服务器拒绝时,按 reason 分支

服务器拒绝时,SDK 抛出 Pdf123Error,带 status、code、reason 和 problem.hint。同一个 code 下有多个 reason,先判断 reason,没有再看 code。三种常见失败,用同一份输入在本地跑出来是这样:

输入 status code reason hint(原文是英文)
加密 PDF,没给密码 400 bad_request password_required 提供文档密码,或先解锁这个 PDF
给了错误密码 400 bad_request wrong_password 检查密码后重试
不是 PDF,或文件已损坏 400 invalid_document invalid_pdf 上传有效且未损坏的 PDF,可以先试试修复
import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("locked.pdf");
try {
  await client.run("compress", { input });
} catch (error) {
  if (!(error instanceof Pdf123Error)) throw error;
  console.error(error.status, error.code, error.reason, error.problem?.hint);
  if (error.reason === "password_required") {
    await client.run("compress", { input, password: "secret" });
  }
}

知道密码时,给单个文件传 password,解锁和目标工具在同一个请求里完成。损坏的文件可以交给 修复 再试。

没有结果时要分开看。PDF 转 CSV 在 PDF 里没有表格时,服务器返回 204,SDK 抛 code: "no_content",原因见空表格导出(204)。条件过滤类工具把条件不成立当作正常结果,不抛错,返回 matched: false 和空的 data。

重试、内存和取消,都要调用方处理

网络错误和 5xx 原样抛出,SDK 不自动重试。重试会改变状态的请求时,自己带上 idempotencyKey:同一个键重发,服务器返回第一次的结果,不会重复处理,机制和限制见幂等键。结果整块读进内存,没有流式读取。输入和输出都很大时,要把两者同时占用的内存算进去。

取消和超时是两种 code,一次调用只会报告先发生的那一种。下面的例子把它们拆成两次调用,两个分支才都能跑到:

import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("a.pdf");

const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
try {
  await client.run("compress", { input, signal: controller.signal });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "cancelled") { /* 你主动取消了 */ }
}

try {
  await client.run("compress", { input, timeoutMs: 60_000 });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "timeout") { /* 超过了 timeoutMs,这次没有 signal */ }
}

取消信号(AbortSignal)一触发,请求以 code: "cancelled" 拒绝;超过超时是 code: "timeout"。每个请求默认超时 5 分钟,可以在创建客户端时改默认值,也可以在单次调用里用 timeoutMs 覆盖。分块上传的每个请求各自带这个超时。取消只关闭这条连接,服务器是否停止处理没有保证。

一个文件失败,其余继续,回调是完成顺序

给单文件工具一批输入,用 runBatch。一个文件失败,其余的继续。onResult 在每个文件完成时立即调用,所以是完成顺序,index 才是它在输入数组里的位置;返回的数组始终按输入顺序排列。字节要在这个回调里写盘:retainData: false 会在回调返回后把 data 清空,不在这里 saveResult,结果就没了。中途被打断时,已经写好的文件还在。

下面假设目录里有三个正常的 PDF:a.pdf、b.pdf、加密的 locked.pdf(密码 secret),再加一个随便写了几个字的坏文件 broken.pdf:

import { Pdf123Client } from "@pdf123/sdk";
import { fileSource, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const results = await client.runBatch("compress", [
  fileSource("a.pdf"), fileSource("broken.pdf"), fileSource("b.pdf"), fileSource("locked.pdf"),
], {
  concurrency: 2,
  passwordFor: (file) => (file.name === "locked.pdf" ? "secret" : undefined),
  onResult: async (entry, index) => {
    console.log(index, entry.input.name, entry.ok ? "ok" : entry.error.reason);
    if (entry.ok) await saveResult(entry.result, { output: "out/" });
  },
  retainData: false,
});

同时处理的文件数默认是 2,用 concurrency 改。fileSource(路径) 让文件轮到处理时才读盘,一大批大文件不会一次全进内存。passwordFor 让混着加锁和未加锁文件的一批能一次跑完。在本地跑上面这四个文件,回调依次收到:

1 broken.pdf invalid_pdf
0 a.pdf ok
2 b.pdf ok
3 locked.pdf ok

完成顺序每次可能不同,这只是一次运行的样子。retainData: false 让返回数组里的 data 变成空的;成功的文件已经由上面的 saveResult 写进 out/。给 runBatch 传 idempotencyKey 时,每个文件实际发出的键是 <键>:<序号>。

总大小超过 95 MiB 才改走分块上传

一次直接请求的请求体上限是 100 MiB,超过会被拒绝,细节见大文件被拒。一次请求的所有文件加起来超过 95 MiB 时,SDK 改用分块上传,client.run 的写法不用变。服务器默认单个上传最大 500 MiB,自托管时用 PDFX_UPLOAD_MAX_BYTES 调整。

同一个操作在 curl、MCP 和命令行里的写法,见同一操作,四种客户端:那边把四种调用并排,这边只展开 SDK。这个包在 npm 上的页面是 @pdf123/sdk。

打开工具
在浏览器里完成处理,不加水印,文件处理完就会删除。
打开工具