# @epistola\.app/epistola-client 中文文档

规范地址：https://npmcn.com/package/%40epistola.app/epistola-client/

版本：2\.0\.0
npm 元数据更新时间：2026-11-12T10:56:14.539Z
中文译文更新时间：2026-11-13T21:38:41.908Z
软件包许可：EUPL-1\.2

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40epistola.app/epistola-client
原文对照：https://npmcn.com/package/%40epistola.app/epistola-client/?lang=original
项目主页：https://github.com/epistola-app/epistola-contract
源码仓库：https://github.com/epistola-app/epistola-contract

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：14.3 MB，967 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22\.12\.0
- 运行依赖：0 个
- 首次发布：2026-09-09T09:45:33.656Z
- 最新版本发布：2026-11-12T10:56:12.589Z
- 近 30 天下载量：3038（2026-10-14 至 2026-11-12，npm 官方下载统计）
- 已发布版本数：17（近 12 个月 17 个）

## 中文简介

Epistola文档生成API的Node.js客户端,在平台的自取. 由Epistola OpenAPI合同产生,有身份头,RFC 9457问题-细节错误处理,API-key和自签的JWT auth,NDJSON结果收集,

## 安装

```sh
npm install @epistola.app/epistola-client
```

也可以使用：`pnpm add @epistola.app/epistola-client`、`yarn add @epistola.app/epistola-client`、`bun add @epistola.app/epistola-client`

## README

# Epistola Node.js 客户端

节点Js 客户端库 [爱皮斯托拉](https://github.com/epistola-app/epistola-contract) epistola OpenAPI 合同生成的文档生成 API [OpenAPI 生成器](https://openapi-generator.tech/) (单位:千美元)`typescript-fetch`,在月台上独自 `fetch`)和手写胶水,用于身份、认证、错误处理、结果收集,以及 客户端验证。 包含类型脚本声明.

除了股票产生的客户端之外

-   **客户身份** 页眉 (`User-Agent` + 键 `X-EP-Node-Id`每一份请求都需要.
-   **RFC 9457 问题详情** 错误处理 - 输入 `ProblemDetailException` 带有 `typeSlug` 歧视者和产生者 `KnownProblemSlugs` 常数.
-   **静态 API- key** 通过 `Authorization: ApiKey <key>`,支持的方法.
-   **自签 JWT** 认证( e)`JwtSigner`, (中文(简体) ), **实验**{\\fn黑体\\fs22\\bord1\\shad0\\3ahbe\\4ah00\\fscx67\\fscy66\\2chffffff\\3ch808080}用新鲜的短命 每个请求的代号 `node:crypto` 一个人.
-   **这个 `Accept` 每个操作都用页眉声明.** 生成的课从不设置一个 节点的 `fetch` 发送 `*/*` 默认的,所以没有这个请求,它永远不会说接受 `application/problem+json`,文档中每个错误处理器都会被构建为解析.
-   **NDJSON 结果收集** (单位:千美元)`ResultCollector`具有适应性投票、压缩和 分区意识路由帮助器.
-   **模板数据验证** (单位:千美元)`TemplateSchemaValidator`),询问服务器,带有可插件 `TemplateDataValidator` 用于想要在流程中进行检查的消费者 `validate<Model>` 帮助解决合同本身的限制.

软件包版本跟踪Epistola合同版本(`info.version`并锁定释放 与克特林,雅加达EE,.NET和Python客户端. 它需要22.12节或以后的节点和船舶 一个ES模块(节点的) `require()` 也可以装入).

它已经 **没有运行时间依赖性,也没有可选的**: HTTP 是该平台的 `fetch`,JWT签名 实值 `node:crypto`,减压是 `node:zlib`。模板数据验证询问服务器而不是 在此编译计划, 因此不再安装 JSON Schema 编译器( 见 [模板数据验证](https://npmcn.com/package/%40epistola.app/epistola-client/#readme-template-data-validation)) (中文(简体) ).

发布历史 : [变动情况](https://npmcn.com/package/%40epistola.app/epistola-client/CHANGELOG.md)。 。 。 .

## 安装

```bash
npm install @epistola.app/epistola-client
```

## 快开始

```ts
import { ClientIdentity, EpistolaClient, TemplatesApi } from '@epistola.app/epistola-client'

const identity = ClientIdentity.builder()
  .nodeId('my-pod-123')                       // defaults to the hostname
  .product('my-app', '1.0.0')                 // appended to User-Agent
  .build()

const client = EpistolaClient.builder('https://epistola.example.com/api')
  .identity(identity)                         // User-Agent + X-EP-Node-Id
  .apiKey('epk_...')                          // Authorization: ApiKey <key>
  .build()

const templates = new TemplatesApi(client)
const template = await templates.getTemplate({ tenantKey: 'acme', registry: '~', namespace: 'acme', catalogKey: 'invoices', release: '1.4.0', templateKey: 'invoice' })
```

其目录涉及 `registry`, (中文(简体) ), `namespace` 财务报告和审定财务报表 `catalogKey`。 。 。 . `registry: '~'` 地址: 服务器的默认注册; 任何其他注册都以全名命名(`registry: 'acme.com'`) (中文(简体) ).

`EpistolaClient.builder(baseUrl, apiKey)` 是同一事物的简称。 一些Epistola套间 部署完全禁用 API- key 认证; 开关 `e.typeSlug === KnownProblemSlugs.API_KEY_AUTH_DISABLED` 并引导呼叫者前往JWT auth.

自签名 JWT 认证为 **实验**: 爱皮斯托拉 套房可能尚未执行 流动仍然会改变。 试试看,通过 `.jwtSigner(signer)` 改为 `.apiKey(...)`数字 :

```ts
import { JwtSigner } from '@epistola.app/epistola-client'

const signer = JwtSigner.builder()
  .consumerId('invoice-service')
  .privateKey(JwtSigner.loadPrivateKey('private.pem'))
  .build()
```

`EpistolaClient` _实值_ a 生成 `Configuration`,因此每个生成的API类直接取用. 一个构建者可以生产一个以上的客户端:呼叫 `build()` 更改后再次 `requestTimeoutMs(...)` 长期消费者通常需要对照 同一后端——投票、渲染和大额转账不超时(缺省),有限度 为了其他一切.

### 部分更新

生成的模型是平版Script接口,生成的序列器会丢出一个属性 您发送一个您设定到 `null` 作为 `null`在 API 上 `PATCH` 操作 a 省略或无成员保留其值,因为大多数生成的服务器无法区分两者: `updateConsumer({ ..., updateConsumerRequest: { name: 'Billing' } })` 重新命名消费者 别碰别的东西,也一样 `{ name: 'Billing', description: null }`。要删除一个值,请发送 可选文本成员空字符串(`{ description: '' }`,空数组或对象 将列表或地图替换为一无所有,或成员的明确旗帜(`{ clearExpiresAt: true }`, (中文(简体) ), `clearDefaultTheme`, (中文(简体) ), `clearCodeListBinding`) (中文(简体) ).

### 二进制下载

合同宣布的每一项业务 `format: binary` —— 说 `downloadDocument`, (中文(简体) ), `previewDocument`, (中文(简体) ), `downloadImageContent` - 决定 `Blob`· ; `Buffer.from(await blob.arrayBuffer())` 给你的字节.

### 上传文件

多部分业务,例如: `uploadImage` 财务报告和审定财务报表 `importCatalog` 带一个 `Blob`通过 a `File`,哪个 带有名称和类型 :

```ts
import { readFile } from 'node:fs/promises'

const file = new File([await readFile('logo.png')], 'logo.png', { type: 'image/png' })
const image = await new ImagesApi(client).uploadImage({ tenantKey: 'acme-corp', registry: '~', namespace: 'acme-corp', catalogKey: 'main', release: 'k7m2x9qa', file, name: 'Logo' })
```

一个平地 `Blob` 总是随文件名一起发送 `blob`,然后作为 `application/octet-stream` 除非这 被赋予了一种类型。 服务器然后命名图像 `blob`。 。 。 .

## 更新服务器

合同的细微版本可以添加响应成员和enum值("兼容性在 API描述中的 2.x",因此这个客户端继续针对后2.x服务器工作:一个响应 它不知道的成员被忽略, 而它不知道的enum值被保存为字符串 收到,尽管生成的类型没有列出。 保持一个 `default` 切换时的分支 um.

客户端忽略的是什么,它不能发回. 根据答复提出的要求只涉及成员 这个客户知道。 这与书写取代所读内容有关: `getVariantContent` 并写回 `updateVariantContent` 失去每个加入的更新目录的线条版本. 编辑至少与服务器一样新客户端的模板文档,并更改消费者的 通过只发送您更改的资源,从不发送您读取的对象.

## 处理错误

接着 `ProblemDetailException` 打开 `typeSlug` 反对 `KnownProblemSlugs`。弹片列表是 **打开** ——API可以在没有客户端发布的情况下引入新的问题类型——所以始终保持一个 `default` 分支并返回未识别类型的 HTTP 状态.

```ts
import { KnownProblemSlugs, ProblemDetailException, ResponseError, TenantsApi } from '@epistola.app/epistola-client'

try {
  await new TenantsApi(client).getTenant({ tenantKey: 'acme' })
} catch (e) {
  if (e instanceof ProblemDetailException) {
    switch (e.typeSlug) {
      case KnownProblemSlugs.NOT_FOUND:
        log.warn(`tenant not found: ${e.detail}`)
        break
      case KnownProblemSlugs.VALIDATION_ERROR:
        for (const err of e.errors) log.warn(`${err.field}: ${err.message}`)     // field-level ValidationProblemDetail
        break
      case KnownProblemSlugs.DATA_MODEL_VALIDATION_ERROR:
        for (const [example, failures] of Object.entries(e.validationErrors)) {  // 422 data-model failures
          for (const f of failures) log.warn(`${example} ${f.path}: ${f.message}`)
        }
        break
      case KnownProblemSlugs.RELEASE_DEPENDENCIES_MISSING:
        for (const d of e.missingDependencies) {                                 // 409 from deployCatalogRelease
          log.warn(`${d.registry}/${d.namespace}/${d.catalogKey}@${d.version}: ${d.reason}`)
        }
        break
      case KnownProblemSlugs.RELEASE_DEPENDENCIES_UNRELEASED:
        for (const d of e.unreleasedDependencies) {                              // 409 from releaseCatalogDraft
          log.warn(`${d.registry}/${d.namespace}/${d.catalogKey}: draft ${d.releaseName} (${d.releaseId})`)
        }
        break
      case KnownProblemSlugs.CATALOG_READ_ONLY:
        // any write to a catalog that cannot be changed here
        log.warn(`catalog cannot be changed here: ${e.readOnlyReason}`)
        break
      case KnownProblemSlugs.MERGE_CONFLICTS_UNRESOLVED:
        for (const conflict of e.unresolvedConflicts) log.warn(`needs ours or theirs: ${conflict}`)  // mergeIntoCatalogDraft (alpha)
        break
      case KnownProblemSlugs.MERGE_PREVIEW_OUTDATED:
        log.warn('the draft or the release changed since the preview: preview again')  // expectedToken (alpha)
        break
      case KnownProblemSlugs.MERGE_BASE_MISSING:
        // the merge base is not installed here: install it first, then merge again
        log.warn(`install ${e.missingBaseVersion} (${e.missingBaseFingerprint}) first`)
        break
      case KnownProblemSlugs.CATALOG_VERSION_BEHIND:
        log.warn(`the draft is behind ${e.behind.join(', ')}`)                  // releaseCatalogDraft: name them in acknowledgedBehind
        break
      case KnownProblemSlugs.CATALOG_DRAFT_IN_USE:
        // deleteCatalogRelease: drafts of other catalogs name the draft, or it alone holds resources
        // something outside it uses (resourceType/resourceKey); change those first
        log.warn(`named by ${e.dependents.map((d) => `${d.catalogKey}: ${d.releaseName}`).join(', ')}; holds ${e.heldResources.join(', ')}`)
        break
      case KnownProblemSlugs.AMBIGUOUS_VARIANT:
        log.warn(`several variants match: ${e.tiedVariants.join(', ')}`)        // generateDocument, previewDocument: name one by variantKey
        break
      case KnownProblemSlugs.FORBIDDEN:
        // any refused authorization: a permission in a tenant, access to a tenant, a platform role,
        // or a policy (refusalReason), each read from this problem only
        log.warn(`refused: missing ${e.requiredPermission} in ${e.refusedTenantKey}`)
        break
      case KnownProblemSlugs.CATALOG_RENDERING_DEFAULTS_TOO_NEW:
        // importCatalog / installCatalogRelease: the release renders with rendering defaults this
        // installation does not have
        log.warn(`upgrade Epistola: the release needs ${e.extensions.renderingDefaultsVersion}, this has up to ${e.extensions.supportedVersion}`)
        break
      case KnownProblemSlugs.RELEASE_NUMBER_TAKEN:
        // publishReleaseToRegistry (alpha): the registry has this version with other content;
        // importCatalog, installCatalogRelease, installRegistryRelease: the tenant has this version
        // with another fingerprint, or this release id as another release
        log.warn('release the changes under a new version')
        break
      case KnownProblemSlugs.CATALOG_SOURCE_UNAVAILABLE:
        // installCatalogRelease / listAvailableCatalogReleases: 503 when the catalog's source could
        // not be reached (Retry-After may say when), 502 when it answered badly
        log.warn(`catalog source unavailable (${e.problemStatus}): ${e.detail}`)
        break
      default:
        throw e                                                                  // a type this client does not know: handle it by e.problemStatus
    }
  } else if (e instanceof ResponseError) {
    // an error response without a problem document
  }
}
```

每个字段出错 `code` 说为什么球场失败(`min-length`, (中文(简体) ), `stencil-recursion`, (中文(简体) ), `catalog-resources-not-ready`,或为 `undefined` 当服务器发送无。 比较一下 `KnownValidationCodes`生成自合同登记处,并保留 `default` 分支: 新建 代码可能会以小版本出现.

`ProblemDetailException` 扩展生成的 `ResponseError`,因此已经存在 `instanceof ResponseError` 网站继续工作。 它会暴露 `type`, (中文(简体) ), `typeSlug`, (中文(简体) ), `title`, (中文(简体) ), `problemStatus`, (中文(简体) ), `detail`, (中文(简体) ), `instance`, (中文(简体) ), `help` (该类型带有此请求上下文的页面,例如 `?operation=getTemplate`,或 `undefined`:在类型上切换,而不是在这个上,打开它以示提示), `errors`, (中文(简体) ), `validationErrors`, (中文(简体) ), `missingFields`, (中文(简体) ), `invalidFields`, (中文(简体) ), `missingDependencies`, (中文(简体) ), `unreleasedDependencies`, (中文(简体) ), `readOnlyReason`, (中文(简体) ), `unresolvedConflicts`, (中文(简体) ), `isMergeConflictsUnresolvedProblem`, (中文(简体) ), `missingBaseVersion`, (中文(简体) ), `missingBaseFingerprint`, (中文(简体) ), `isMergeBaseMissingProblem`, (中文(简体) ), `behind`, (中文(简体) ), `isCatalogVersionBehindProblem`, (中文(简体) ), `environments`, (中文(简体) ), `pinnedBy`, (中文(简体) ), `isReleaseInUseProblem`, (中文(简体) ), `dependents`, (中文(简体) ), `heldResources`, (中文(简体) ), `isCatalogDraftInUseProblem`, (中文(简体) ), `tiedVariants`, (中文(简体) ), `isAmbiguousVariantProblem`, (中文(简体) ), `requiredPermission`, (中文(简体) ), `refusedTenantKey`, (中文(简体) ), `requiredPlatformRole`, (中文(简体) ), `refusalReason`, (中文(简体) ), `isForbiddenProblem`, (中文(简体) ), `extensions` (每个成员) 在基地成员之外,RFC 9457的五和 `help`, (中文(简体) ), `statusCode`, (中文(简体) ), `rawBody`, (中文(简体) ), `isValidationProblem`, (中文(简体) ), `isDataModelValidationProblem`, (中文(简体) ), `isTemplateDataProblem`, (中文(简体) ), `isReleaseDependenciesMissingProblem`, (中文(简体) ), `isReleaseDependenciesUnreleasedProblem`,以及 `isCatalogReadOnlyProblem`。 。 。 .

A 类 `release-dependencies-unreleased` 问题(409) `releaseCatalogDraft` 被拒绝的原因是 声明草案列出其他目录的草稿,并只列出一个发布针 释放. `unreleasedDependencies` 列出每一份此类草案(`registry`, (中文(简体) ), `namespace`, (中文(简体) ), `catalogKey`, (中文(简体) ), `releaseId`, (中文(简体) ), `releaseName`; 先释放, 或发送 `releaseDependencies: true` 释放他们 与草案.

A 类 `catalog-draft-in-use` 问题(409) `deleteCatalogRelease` 被拒绝是因为什么 在草案之外,它仍然依赖它. `dependents` 列出其他目录的草稿 扶养声明名称`registry`, (中文(简体) ), `namespace`, (中文(简体) ), `catalogKey`, (中文(简体) ), `releaseId`, (中文(简体) ), `releaseName`, (中文(简体) ), 财务报告和审定财务报表 `heldResources` 资源只有它认为它以外的东西 仍然使用,因为 `resourceType/resourceKey` (单位:千美元)`theme/house-style`先换个吧.

A 类 `release-dependencies-missing` 问题(409) `deployCatalogRelease` 由于释放而被拒绝 在其他目录中,解禁针不能取出:租户没有,他们不能取出 用于获取或核实. `missingDependencies` 各列表,并附有 `reason` 保留为字符串 服务器已发送( 比较它与 `MissingReleaseDependencyReason`) (中文(简体) ). 使用依赖释放 默示; 通过 `includeDependencies: true` 明确部署:

```ts
await new EnvironmentsApi(client).deployCatalogRelease({
  tenantKey: 'acme', environmentKey: 'production',
  registry: '~', namespace: 'acme', catalogKey: 'invoices', release: '1.4.0',
  includeDependencies: true,
})
```

## 结果收集

```ts
import { ResultCollector } from '@epistola.app/epistola-client'

const collector = ResultCollector.builder()
  .client(client)
  .tenantKey('acme')
  .handler(async (result) => console.log(result.requestId, result.status))
  .build()

await collector.start()   // adaptive polling; resolves after collector.stop()
```

一次一个结果流,批次只有在结果逐一结果之后才予以确认 处理: 丢出批量的处理器未被确认, 因此它被重新交付。 gzip 总是 已解码;zstd提供并解码到Node的zlib拥有它的地方(22.15+). `partitionFor`, (中文(简体) ), `isMyPartition` 财务报告和审定财务报表 `routingKeyToMe` 从任务中计算服务器报告的一个路由密钥 他们的结果会回到这个节点.

## 模板数据验证

模板数据由 **爱皮斯托拉**,通过 `validateTemplateData`这里没有编译, 因此,没有计划编译器可以安装,判决不能不同意哪一代人会 do ——服务器在提交任务时验证相同数据,并且它也知道哪个可选 字段所显示的释放实际上已读取.

```ts
import { ServerTemplateDataValidator, TemplateSchemaValidator, ValidatingGenerationApi, validateCreateTenantRequest } from '@epistola.app/epistola-client'

const validator = new TemplateSchemaValidator(templatesApi)
// tenant, catalog registry (`~` for the server's default, or in full: 'acme.com'), namespace, catalog key, template
await validator.validate('acme', '~', 'acme', 'invoices', 'invoice', data)   // throws TemplateDataValidationException

// Against a particular release: a version range, an environment, or both together
const pinned = new TemplateSchemaValidator(
  new ServerTemplateDataValidator(templatesApi, { versionRange: '^1.4.0', environmentKey: 'production' }),
)

// Or around generation, which turns the server's `template-data-invalid` problem into the same error:
const generation = new ValidatingGenerationApi(new GenerationApi(client), templatesApi)

// A request model against the constraints the contract declares on it (key patterns, lengths, ranges):
validateCreateTenantRequest({ key: 'Acme Corp', name: 'Acme' })  // throws ModelValidationException
```

每一次失败都带有 **JSON 指针** 输入数据(`/customer/email`JSON斯克马 `keyword` 失败了,一个人能读 `message` ——所以一种形式可以标记错误的字段.

### 正在验证处理中

避免每次检查的请求——如在飞行前进行大量检查——执行 `TemplateDataValidator` 在您所选择的编译器之上, 而不是通过 API :

```ts
const validator = new TemplateSchemaValidator(new MyAjvValidator(templatesApi, '1.4.0'))
const generation = new ValidatingGenerationApi(new GenerationApi(client), new MyAjvValidator(templatesApi, '1.4.0'))
```

解答进程内声明的验证器 `preflightsGeneration = true` (默认),故 `ValidatingGenerationApi` 在提交和报告每批物品之前进行核对 路径前缀 `items[<index>]`。 。 。 . `TtlSchemaCache` 仍然导出用于缓存取来的计划; 不需要"本地"验证员 就需要每通电话的往返费 模版在发布 他们的目录,所以一个本地验证器从您命名的发布中获取其计划( ) `1.4.0` 名称: 您将发布, 因为服务器验证 相对于该发布, 而不是反对草稿.

有两件事,一个是执行的,因为呼吁者都提到 `TemplateDataValidator`: 一个空 数组表示数据是可以接受的,而且 `path` 是JSON指针 编纂者不同意这一点 最后一点——Ajv's `instancePath` 已经是一个指针,但是要单独列出一个丢失的财产, 而其他引擎则会发出JSONPath——因此转换是适配器的工作. 一个有效的Ajv适配器, 大约四十行,在 `test/validation/local/ajvTemplateDataValidator.ts`; 拷贝.

注意当地的判决是自己的: `format` 语法、方言覆盖范围和未知关键词 处理是编译器的选择,因此它可以接受数据 Epistola 会拒绝,或者拒绝数据 埃皮斯托拉会变的.

## 发展

客户端由捆绑的光谱生成. 从寄存器根 :

```bash
make bundle                       # produce openapi.yaml
cd contracts/api/clients/nodejs-fetch
pnpm install --frozen-lockfile
./generate.sh                     # stock client + derived sources into src/generated/
pnpm build                        # dist/
pnpm test                         # node:test, against the compiled tree
```

`src/generated/` (股票客户端 `api/`,并生成 `contractVersion`, (中文(简体) ), `contractIdentity`, (中文(简体) ), `contractMediaTypes`, (中文(简体) ), `contractOperations`, (中文(简体) ), `knownProblemSlugs` 财务报告和审定财务报表 `modelValidation`)被从光谱中分出并重建出每次. 跨客户的合规性 套件驱动此客户端 `make conformance-node`。 。 。 .

## 常见问题

### @epistola\.app/epistola-client 是什么？

Epistola文档生成API的Node\.js客户端,在平台的自取\. 由Epistola OpenAPI合同产生,有身份头,RFC 9457问题-细节错误处理,API-key和自签的JWT auth,NDJSON结果收集,

### 如何安装 @epistola\.app/epistola-client？

在项目目录运行 npm install @epistola\.app/epistola-client，或使用 pnpm add / yarn add / bun add。

### @epistola\.app/epistola-client 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @epistola\.app/epistola-client 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @epistola\.app/epistola-client 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=22\.12\.0。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### @epistola\.app/epistola-client 可以商用吗？

@epistola\.app/epistola-client 使用 EUPL-1\.2 许可。声明的许可为 EUPL-1\.2，请阅读许可原文确认使用条件。以上为概括，不构成法律意见，请以许可原文为准。

### @epistola\.app/epistola-client 的下载量有多少？

根据 npm 官方下载统计，@epistola\.app/epistola-client 在 2026/10/14 – 2026/11/12 这 30 天内被下载 3,038 次，日均约 101 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @epistola\.app/epistola-client 还在维护吗？

最新版本 2\.0\.0 发布于 2026/11/12（1 天前）；至今共发布 17 个版本，其中过去 12 个月发布了 17 个；首次发布于 2026/09/09。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

