# 许可证

> 配置 Exchange 导出授权、查询剩余额度并处理授权错误。

- Human documentation: [https://docs.univer.ai/zh-CN/server/import-export/license](https://docs.univer.ai/zh-CN/server/import-export/license)

- Agent Markdown: [https://docs.univer.ai/zh-CN/server/import-export/license.md](https://docs.univer.ai/zh-CN/server/import-export/license.md)

- Requested language: `zh-CN`

- Content language: `zh-CN`

- Documentation version: `1.3.1`

- Source: [import-export/license.zh-CN.mdx](https://github.com/dream-num/documentation/blob/dev/content/server/import-export/license.zh-CN.mdx)

---

Exchange 在运行 `@univerjs-pro/exchange-node` 的服务端进程中读取许可证。转换用法见 [Office 文件导入导出](https://docs.univer.ai/zh-CN/server/import-export/node.md)。

## 配置授权

推荐通过环境变量 `UNIVER_LICENSE` 与 `UNIVER_LICENSE_KEY` 成对提供授权。无需调用 `configureExchange`；SDK 在首次导出或额度查询时自动读取一次，后续调用复用该配置。未提供环境凭据时使用 Free；已提供但无效的授权会报错，不能用于导出。

也可以使用 `configureExchange({ license, licenseKey })` 显式配置，它返回 `Promise<void>`。显式配置完全忽略环境变量；应等待配置完成后再开始使用新授权的导出。

```ts
import { configureExchange, getExportQuota } from '@univerjs-pro/exchange-node'

await configureExchange({
  license: '<license content>',
  licenseKey: '<licenseKey content>',
})

const quota = await getExportQuota()
```

`license` 与 `licenseKey` 使用授权提供方交付的完整内容，不是文件路径。两项应成对配置，并通过应用的密钥管理系统提供。

| 输入 | 使用方式 |
| --- | --- |
| 不调用 `configureExchange` | 首次使用时成对读取环境变量；均未提供则使用 Free。 |
| `configureExchange({ license, licenseKey })` | 只使用显式凭据，完全忽略环境变量。 |
| `configureExchange({})` | 显式选择 Free，忽略环境变量。 |
| 所选来源只提供一个字段 | 返回 `INVALID_ARGUMENT`，不会从另一来源补齐。 |

完整的环境配置读取后即固定，即使后续操作失败也不会自动重读。更换默认授权应显式调用并等待 `configureExchange`；配置失败时保留此前配置，应处理错误后再开始新的导出。

同一 JavaScript 上下文中的 ESM 与 CommonJS 共用配置。每个 `worker_threads` Worker 都需独立配置或从环境变量初始化。导出使用调用公开导出方法时的配置，后续换证不会改变已发起的导出。

### 为单次操作指定授权

导出 API 和 `getExportQuota` 的选项可传入完整的 `license`、`licenseKey`，仅作用于本次操作，不修改默认配置。例如，使用同一授权查询额度并导出：

```ts
import { UniverInstanceType } from '@univerjs/core'
import { ExchangeFormat, exportToBuffer, getExportQuota } from '@univerjs-pro/exchange-node'

const credentials = { license: '<license content>', licenseKey: '<licenseKey content>' }
const quota = await getExportQuota(credentials)
const output = await exportToBuffer(workbook, {
  type: UniverInstanceType.UNIVER_SHEET,
  format: ExchangeFormat.XLSX,
  ...credentials,
})
```

此方式同样适用于文件导出、Snapshot 导出和可选 PDF 导出。不传这两个字段时使用默认配置；只传一个字段返回 `INVALID_ARGUMENT`。显式传入的完整凭据不会从默认配置或环境变量补齐，空字符串也不会被视为省略；授权无效时返回 `LICENSE_INVALID`，不会重新使用默认商业授权。

### 可选 PDF 支持

安装 PDF 扩展包，并在配置授权前导入 PDF 入口：

```sh
pnpm add @univerjs-pro/exchange-node @univerjs-pro/exchange-pdf-node-binding
```

```ts
import '@univerjs-pro/exchange-node/pdf'
import { configureExchange, getExportQuota } from '@univerjs-pro/exchange-node'

await configureExchange({
  license: '<license content>',
  licenseKey: '<licenseKey content>',
})

const quota = await getExportQuota()
```

该入口注册 PDF 支持，原生组件在实际使用时加载。PDF 仍使用 `importBuffer`、`importFile`、`exportToBuffer`、`exportToFile` 及对应 Snapshot API，指定 `UniverInstanceType.UNIVER_PDF` 和 `ExchangeFormat.PDF`；无需改用另一套转换 API。仅处理 PDF 的应用不需要加载 Office 原生组件。环境变量配置同样适用于 PDF。

## 查询剩余额度

`getExportQuota({ license, licenseKey })` 返回 `Promise<ExportQuota>`，查询该授权在 Office 与 PDF 间共享的额度。不传凭据时查询默认配置。查询不接收类型参数，也不判断授权是否允许导出某个类型；类型授权在实际导出时检查。

查询不扣次、不预留额度，也不检查 SDK 版本、授权是否已生效或导出类型权限。余额为零时仍成功返回当前快照；其他并发请求可能随后使用额度，请同时处理实际导出的错误。

授权尚未生效时，有限额度返回首个授权月的快照，`periodStart` 为未来日期。查询成功或 `licenseStatus` 为 `valid` 均不代表当前允许导出；版本、生效时间、类型权限、可用额度和并发名额在实际导出时检查。

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `licenseStatus` | `'not_configured' \| 'expired' \| 'valid'` | 导出订阅未配置、已到期或未到期；`valid` 不表示已通过导出准入检查。无效授权直接报错。 |
| `expiresAt` | `string \| null` | 导出订阅到期的 UTC ISO 时间；未配置或无到期时间时为 `null`。 |
| `observedAt` | `string` | 本次快照的 UTC ISO 时间。 |
| `limit` | `number \| null` | 当前月额度上限；不限次数时为 `null`。 |
| `used` | `number \| null` | 当前月已成功导出的次数；不限次数时为 `null`。 |
| `reserved` | `number \| null` | 当前月正在处理的导出占用次数；不限次数时为 `null`。 |
| `remaining` | `number \| null` | 当前可用次数，即 `max(0, limit - used - reserved)`；不限次数时为 `null`。 |
| `periodStart` | `string \| null` | 所查询月额度窗口的 UTC ISO 起点，包含该时刻；尚未生效时为首期起点，不限次数时为 `null`。 |
| `periodEnd` | `string \| null` | 所查询月额度窗口的 UTC ISO 终点，不包含该时刻；不限次数时为 `null`。 |

`periodEnd` 表示下一月额度开始的时间，`expiresAt` 表示整个导出订阅到期的时间。订阅有效时始终使用商业额度；本月耗尽不会使用 Free，下个月开始使用新的商业月额度。订阅到期后使用原有 Free 剩余额度，不重新发放或清零。

有限额度快照示例：

```json
{
  "licenseStatus": "valid",
  "expiresAt": "2027-10-19T23:59:59Z",
  "observedAt": "2026-10-21T09:00:00Z",
  "limit": 500,
  "used": 20,
  "reserved": 1,
  "remaining": 479,
  "periodStart": "2026-10-19T00:00:00Z",
  "periodEnd": "2026-11-19T00:00:00Z"
}
```

## 使用额度

Free 每授权月允许 10 次成功导出。商业授权的次数、并发及版本范围以所购授权为准。Free 与商业授权均不设置授权层面的文件大小和单元格数量上限。周期与剩余额度以查询结果为准，未用完的月额度不结转。

导入无需配置导出授权，也不消耗导出次数。同一部署中，Office 与 PDF 使用同一商业授权时共享额度。Office 转换格式见 [Office 文件导入导出](https://docs.univer.ai/zh-CN/server/import-export/node.md)。

每次成功转换计 1 次，返回 Buffer 即视为转换成功。转换成功后的文件写入、上传或下载失败不退回次数；再次成功转换会再次计次。本周期额度耗尽后拒绝新的导出，需等待下一授权周期或换用额度更高的有效授权；有效商业授权不会借用 Free 额度。

## 处理错误

使用结构化 `code` 判断错误，不要解析 `message` 文本。用户界面可根据错误码提供本地化提示；诊断信息请保留供技术支持使用。

```ts
import { UniverInstanceType } from '@univerjs/core'
import { ExchangeError, ExchangeFormat, exportToBuffer } from '@univerjs-pro/exchange-node'

try {
  const output = await exportToBuffer(workbook, {
    type: UniverInstanceType.UNIVER_SHEET,
    format: ExchangeFormat.XLSX,
  })
} catch (error) {
  if (error instanceof ExchangeError) {
    // 根据 error.code 展示对应提示，例如额度不足时为 LICENSE_EXPORT_QUOTA_EXCEEDED
    console.error(error.code)
  } else {
    throw error
  }
}
```

下表区分额度查询（`getExportQuota`）与实际导出的报错时机。两类 API 均返回 Promise，使用 `await` 配合 `try/catch` 处理错误。

| 错误码 | 报错时机 | 含义及处理方式 |
| --- | --- | --- |
| `LICENSE_EXPORT_QUOTA_EXCEEDED` | 仅导出 | 当前周期的可用导出额度不足；查询额度或联系商务调整授权。 |
| `LICENSE_CONCURRENCY_TIMEOUT` | 仅导出 | 等待可用导出并发名额超时；降低并发后再发起导出。 |
| `LICENSE_UNIT_TYPE_REJECTED` | 仅导出 | 当前授权未包含此导出类型或该类型授权已到期；使用包含该类型的有效授权。 |
| `LICENSE_INVALID` | 查询、导出 | 授权无效；检查完整凭据或联系授权提供方更新授权。 |
| `LICENSE_POLICY_INVALID` | 查询、导出 | 授权配置不符合 SDK 要求；联系授权提供方更新授权。 |
| `LICENSE_NOT_YET_EFFECTIVE` | 仅导出 | 授权尚未生效；在生效后使用。 |
| `LICENSE_VERSION_NOT_ALLOWED` | 仅导出 | 当前 SDK 版本不在授权范围内；使用匹配版本或更新授权。 |
| `UNSUPPORTED_FORMAT` | 仅导出 | 不支持指定的格式或格式与类型不匹配；检查导出选项。 |
| `INVALID_FILE` | 仅导出 | 转换输入无效；检查输入内容。 |
| `INCOMPLETE_SNAPSHOT` | 仅导出 | 导出数据不完整；提供完整数据或 Snapshot。 |
| `CONVERSION_FAILED` | 仅导出 | 文件转换失败；保留错误信息并检查输入内容。 |
| `IO_ERROR` | 仅导出 | 文件读写失败；成功转换后的文件写入失败已经消耗 1 次。 |
| `INVALID_ARGUMENT` | 查询、导出 | 参数或凭据不完整；检查传入配置。 |
| `NATIVE_LOAD_FAILED` | 查询、导出 | SDK 原生组件无法加载；检查已安装包及运行环境是否匹配。 |
| `LICENSE_USAGE_STATE_INVALID` | 查询、导出 | 授权用量状态无法验证；保留错误信息并联系技术支持。 |
| `LICENSE_USAGE_STORAGE_ERROR` | 查询、导出 | 授权用量处理失败；保留错误信息并联系技术支持，避免自动重复提交。 |

应用可根据 `licenseStatus` 与 `expiresAt` 自行展示到期提醒；SDK 不额外返回提示列表。

遇到授权用量处理异常时，不应仅根据错误推断本次是否已计次，请保留完整错误并联系技术支持。
