@flowblade/sqlduck

中文文档
0.47.0发布于 2026/11/11MITTypeScriptES Module构建来源已验证近 30 天下载 2292
中文使用文档
文档目录13 节
  1. @ flowblade/ sqlduck
  2. 快开始
  3. 创建数据库连接
  4. 将数据添加到数据库
  5. 导出查询
  6. 管理机密
  7. 查询已实现的 Kysely 查询
  8. 交易
  9. 兼容性
  10. 基准
  11. 节点 24.21
  12. 1.4.2号舱
  13. 本地脚本

@ flowblade/ sqlduck

目前试验

  • 来自Zod schemas的 DuckDB 表创建.
  • 容易地摄取来自发电机或动能的数据.

快开始

创建数据库连接

import { DuckDBInstance } from "@duckdb/node-api";
DuckDBInstance.create(undefined, {
  access_mode: "READ_WRITE",
  max_memory: "512M",
});
export const conn = await instance.connect();

将数据添加到数据库

import { DuckDatabaseManager, Table } from "@flowblade/sqlduck";
import { DuckTableLoader } from "@flowblade/sqlduck/loader";
import * as z from "zod";

import { conn } from "./db.config.ts";

const dbManager = new DuckDatabaseManager(conn);
const database = await dbManager.attach({
  type: "memory", // can be 'filesystem', ...
  alias: "mydb",
  options: { COMPRESS: "false" },
});

// Define a zod schema, it will be used to create the table
const userSchema = z.object({
  id: z.int32().min(1).meta({ primaryKey: true }),
  name: z.string(),
});

// Example of a datasource (can be an array, generator, async iterable...)
async function* getUsers(): AsyncIterableIterator<z.input<typeof userSchema>> {
  // database or api call
  yield { id: 1, name: "John" };
  yield { id: 2, name: "Jane" };
}

const loader = new DuckTableLoader({ conn });

// Create a table from the schema and load the rows
const result = await loader.load({
  table: new Table({ name: "user", database: database.alias }),
  schema: userSchema, // The schema to use to create the table
  rows: getUsers(), // The rows to load
  // 👇Optional:
  mode: "createOrReplace", // Default is "create"
  progress: {
    intervalMs: 1000, // Default, or everyRows: 100_000
    onProgress: ({ intervalMs, totalRows, rowsPerSecond }) => {
      console.log(
        `Loaded ${totalRows} rows, est: ${rowsPerSecond} rows/s over ${intervalMs}ms`
      );
    },
  },
});

console.log(`Inserted ${result.totalRows} rows in ${result.timeMs}ms`);
console.log(`Table created with DDL: ${result.createTableDDL}`);

const reader = await conn.runAndReadAll("select * from mydb.user");
const rows = reader.getRowObjectsJS();
// [{id: 1, name: 'John'}, {id: 2, name: 'Jane'}]]

装入策略(默认情况下的DuckDB附件、行附件、准备的有上行的插入)、模式、检查点、进度和调取在 装入器文档。 。 。 .

导出查询

QueryExporter 将参数化的 SQL-tag 查询导出到 DuckDB 的 CSV、 Parquet 或 Excel (XLSX) COPY,所以行从不通过JavaScript. 每个持久性战略都有一个方法:

方法持久性回返
toFile()您选择和拥有的本地文件文件描述( E)path, (中文(简体) ), sizeBytes, (中文(简体) ), rowCount, (中文(简体) ), timeMs. ) (中文(简体) )
toObjectStorage()Azure Blob 存储器、 S3、 GCS 或 R2 中的天体对象描述( A)url, (中文(简体) ), rowCount, (中文(简体) ), timeMs. ) (中文(简体) )
toResponse()临时文件, 响应发送或取消后删除网络 Response 流出下载
import { DuckdbDatasource, sql } from "@flowblade/source-duckdb";
import { QueryExporter } from "@flowblade/sqlduck/export";

const exporter = new QueryExporter({
  datasource: new DuckdbDatasource({ connection: conn }),
});

导出为文件

const file = await exporter.toFile({
  sql: sql`SELECT id, name FROM user WHERE active = ${true}`,
  format: { type: "parquet", compression: "zstd" },
  file: "/data/exports/users.parquet",
});

console.log(`${file.path}: ${file.rowCount} rows, ${file.sizeBytes} bytes`);

目录 file 必须存在。 一个已存在的文件被替换, 但只有在导出完成后: 一个失败的导出没有留下部分文件, 并且保留了先前的文件 .

导出为对象存储

toObjectStorage 将导出直接上传到 Azure Blob 存储器(az://, (中文(简体) ), abfss://页:1s3://),谷歌云存储器(gs://)或 Cloudflare R2(r2://) (中文(简体) ). DuckDB 写给存储器本身: 本地没有写出任何内容 。 其他urls和本地路径被拒绝.

import { DuckSecretsManager } from "@flowblade/sqlduck";

await new DuckSecretsManager(conn).create({
  name: "exports_azure",
  type: "azure",
  connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
  scope: "az://exports/",
});

const exported = await exporter.toObjectStorage({
  sql: sql`SELECT id, name FROM user WHERE active = ${true}`,
  format: { type: "parquet" },
  url: "az://exports/2026/users.parquet",
});

console.log(
  `${exported.url}: ${exported.rowCount} rows in ${exported.timeMs}ms`
);

出口商不取证书:DuckDB使用其范围与url相匹配的秘密(见 管理机密) (中文(简体) ). 容器或桶必须存在,并替换一个已存在的物体。 所需的 DuckDB 扩展名(azure 或者说 httpfs)被安装并加载在第一个导出上,这可能需要网络访问. 在 Azure 上, 失败的导出不会留下局部的blob .

导出为响应

toResponse 返回网络标准 Response 与 Content-Type, (中文(简体) ), Content-Length 财务报告和审定财务报表 Content-Disposition 信头设定 。 从Hono、Next.js 路线处理器、 Bun 或 Deno 也可以返回:

return await exporter.toResponse({
  sql: sql`SELECT id, name FROM user WHERE active = ${true}`,
  format: { type: "csv", header: true },
  filename: "users.csv", // optional, defaults to export.<extension>
  signal: request.signal,
});

与 node:http 或 Express, 把它插入服务器响应中 :

import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";

const response = await exporter.toResponse({ sql, format });
res.writeHead(response.status, Object.fromEntries(response.headers));
await pipeline(Readable.fromWeb(response.body), res);

首先将查询导出为临时文件, 因此在发送任何字节前失败的查询拒绝 。 当主体读取完毕后, 临时文件被删除, 取消( 客户端断开) , 中止通过 signal 或者失败。 作为一个安全网,一个其身体无人阅读的反应 idleTimeoutMs,在其第一次读取或两次读取之前,失败,其临时文件也被删除.

选项默认说明
filenameexport.<extension>下载文件名 Content-Disposition 标题
directoryos.tmpdir()创建临时文件的现有目录
headers附加响应头, 即: Cache-Control
idleTimeoutMs60_000最长可等待阅读的时间; Infinity 关闭它
signal取消查询,然后将答复流出,是否读取

压缩 CSV 作为压缩文件发送 (application/gzip 或者说 application/zstd, (中文(简体) ), export.csv.gz),无 Content-Encoding。 。 。 .

导出 Kysely 查询

DuckDB上运行的导出,但行可以通过一个 Kysely 查询从另一个数据库(msql, postgres...)中来. 说吧 source 改为 sql:它的行被流到一个临时 DuckDB 表格中,导出,然后不管结果如何,该表格都会被丢出. 和我们一起工作 toFile, (中文(简体) ), toObjectStorage 财务报告和审定财务报表 toResponse。 。 。 .

import { KyselyQueryWithZodSchema } from "@flowblade/sqlduck/kysely";
import * as z from "zod";

const file = await exporter.toFile({
  source: new KyselyQueryWithZodSchema({
    query: db.selectFrom("user").select(["id", "name"]),
    // Creates the temporary table, must match the selected columns
    schema: z.strictObject({ id: z.int32(), name: z.string() }),
  }),
  format: { type: "parquet" },
  file: "/data/exports/users.parquet",
  // Optional: where the temporary table is created
  materialization: { database: "scratch" }, // e.g. ATTACH 'scratch.duckdb' AS scratch
});

mapQuery 财务报告和审定财务报表 selectColumns 适用于已实现行: mapQuery 收到一个 SELECT 页:1 各行一次通过JavaScript,从源数据库到DuckDB;到CSV,Parquet或Excel的写入会留在DuckDB内部. materialization.database 财务报告和审定财务报表 materialization.schema 必须是有效的、非保留的 DuckDB 标识符,而且 计划必须存在.

格式

这个 format 选项是受歧视的结合:

格式支持的选项
csvheader, (中文(简体) ), delimiter, (中文(简体) ), quote, (中文(简体) ), escape, (中文(简体) ), nullString, (中文(简体) ), dateFormat, (中文(简体) ), timestampFormat, (中文(简体) ), compression (单位:千美元)none, (中文(简体) ), gzip, (中文(简体) ), zstd(中文(简体) )
parquetcompression (单位:千美元)uncompressed, (中文(简体) ), snappy, (中文(简体) ), gzip, (中文(简体) ), zstd, (中文(简体) ), brotli, (中文(简体) ), lz4, (中文(简体) ), lz4_raw, (中文(简体) ), rowGroupSize, (中文(简体) ), preserveOrder
excelheader, (中文(简体) ), sheet, (中文(简体) ), sheetRowLimit

Excel使用DuckDB的Excel扩展,安装并加载在第一个Excel导出上,这可能需要网络访问. 出口商本身要求 DuckdbDatasource;通过 Kysely 输出查询其他数据库 source。 。 。 .

映射导出查询

DuckDB内部有两种选择,每种方法都有:

  • mapQuery 重写查询,一般通过将查询包入过滤、排序或添加计算出列来进行.
  • selectColumns 选择、重新排序和重命名导出列。 密钥是查询的列名( 以 mapQuery页:1 列按照按键的顺序排列.
const file = await exporter.toFile({
  sql: sql`SELECT id, name, password FROM user`,
  format: { type: "csv", header: true },
  file: "/data/exports/users.csv",
  mapQuery: (query) =>
    sql`SELECT *, upper(name) AS name_uc FROM (${query}) ORDER BY id`,
  selectColumns: { id: "User ID", name_uc: "Name" },
});

日志

QueryExporter 日志为 日志 下划线 ["flowblade", "sqlduck"] 类别,或与 logger 给构造者:

职等活动属性
info导出完成format, (中文(简体) ), queryTimeMs, (中文(简体) ), timeMs附加时 path, (中文(简体) ), sizeBytes, (中文(简体) ), rowCount (档案), url, (中文(简体) ), rowCount (对象存储)或 filename, (中文(简体) ), path, (中文(简体) ), sizeBytes (答复)
warning通过它导出已中止 signalformat, (中文(简体) ), timeMs
error导出失败format, (中文(简体) ), sql (查询或Kysely源的文本,没有参数值), timeMs, (中文(简体) ), errorMessage
error无法删除部分文件或临时文件path, (中文(简体) ), errorMessage
debugDuckDB 扩展已装入( D)excel, (中文(简体) ), azure, (中文(简体) ), httpfs(中文(简体) )extension, (中文(简体) ), timeMs
debug实现 Kysely 源table, (中文(简体) ), rowCount, (中文(简体) ), timeMs
debug完全读取或取消答复,删除临时文件filename, (中文(简体) ), outcome, (中文(简体) ), path, (中文(简体) ), streamTimeMs
warning响应中止或未读( Q)outcome数字 : aborted, (中文(简体) ), timeout(中文(简体) )filename, (中文(简体) ), outcome, (中文(简体) ), path, (中文(简体) ), streamTimeMs
error无法读取响应文件( N)outcome数字 : failed(中文(简体) )filename, (中文(简体) ), outcome, (中文(简体) ), path, (中文(简体) ), streamTimeMs

管理机密

DuckSecretsManager 创建 DuckDB 秘密,允许访问对象存储(Azure,S3,GCS,R2)或HTTP端点. 它跟随其他管理器:它是由与logtape的连接和日志创建的.

import { DuckSecretsManager } from "@flowblade/sqlduck";

const secrets = new DuckSecretsManager(conn);

await secrets.create({
  name: "exports_azure",
  type: "azure",
  connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
  scope: "az://exports/",
});

// Without keys: managed identity, cli, env...
await secrets.create({
  name: "lake_azure",
  type: "azure",
  provider: "credential_chain",
  accountName: "mylake",
});
方法说明
create(secret, { persistent, onConflict })创造出一个秘密. onConflict 实值 error (违约), replace 或者说 ignore
drop(name, { ifExists })丢出一个秘密
list({ name, type })列出由 DuckDB 编辑的敏感值的秘密
which(path, type)秘密的 DuckDB 会用于路径,或者 null
withSecret(secret, fn)运行 fn 暂时保密,完成或抛出时放下
  • 秘密值被绑定为查询参数,因此它们从未出现在已登录的语句或出错中.
  • 秘密默认是暂时的:他们一生生活在DuckDB实例的记忆中. persistent: true 在 DuckDB 的秘密目录中保存它们.
  • 秘密属于 DuckDB 实例: 它的所有关系都能看到它们,包括 a withSecret 在函数运行时隐藏。 使用 scope 以限制一个秘密所适用的路径.
  • azure, (中文(简体) ), s3, (中文(简体) ), r2, (中文(简体) ), gcs 财务报告和审定财务报表 http 秘密被输入。 其他选项进入 options (骆驼案件密钥),其他类型 { type: "custom", customType }。 。 。 .
  • 创建扩展类型的秘密( N)azure, (中文(简体) ), s3...)使 DuckDB 安装并加载扩展,这可能需要网络第一次访问.

查询已实现的 Kysely 查询

⚠️ 实验:下面的API可能会改变一个小的发布.

withMaterializedKyselyQuery 将 Kysely 查询的行( 例如从 mssql 或 postgres) 流入一个临时 DuckDB 表格, 将您的查询运行在它上, 然后丢弃表格, 无论查询成功、 失败或丢弃 .

import { DuckDBInstance } from "@duckdb/node-api";
import {
  createKyselyMssqlDialect,
  TediousConnUtils,
} from "@flowblade/source-kysely";
import { sql } from "@flowblade/sql-tag";
import {
  KyselyQueryWithZodSchema,
  withMaterializedKyselyQuery,
} from "@flowblade/sqlduck/kysely";
import { Kysely } from "kysely";
import * as z from "zod";

// Source database: any Kysely instance (here mssql)
type DB = {
  user: { id: number; name: string };
};
const db = new Kysely<DB>({
  dialect: createKyselyMssqlDialect({
    tediousConfig: TediousConnUtils.fromJdbcDsn(
      "sqlserver://localhost:1433;database=db;user=sa;password=pwd"
    ),
  }),
});

// Target duckdb connection (a DuckDBConnection or a DuckdbDatasource)
const instance = await DuckDBInstance.create();
const conn = await instance.connect();

const table = new KyselyQueryWithZodSchema({
  query: db.selectFrom("user").select(["id", "name"]),
  // used to create the temporary table, must match the selected columns
  schema: z.strictObject({ id: z.int32(), name: z.string() }),
});

const result = await withMaterializedKyselyQuery({
  duckConn: conn,
  table,
  // 👇Optional: where the temporary table is created.
  // Defaults to the connection's current database and schema.
  database: "scratch", // e.g. ATTACH 'scratch.duckdb' AS scratch
  schema: "staging", // must already exist
  query: ({ dsDuck, table }) =>
    dsDuck.query(
      sql<{ id: number; name: string }>`
                SELECT id, name FROM ${sql.raw(table.getFullName())} WHERE id < 1000
            `
    ),
});

if (result.error) {
  // materialization or query error
} else {
  console.log(result.data);
  // the first span holds the materialization info (ddl, affectedRows, timeMs, tableName)
  console.log(result.meta.getSpansByType("materialization"));
}

database 财务报告和审定财务报表 schema 必须是有效的、非保留的 DuckDB 标识符,否则返回出错结果,查询不运行。 以所附文件数据库为目标,可使大型材料化不包含在内.

交易

斯克尔达克参加 DuckdbDatasource 同一连接的交易(参见 @ flowblade/ source-duckdb (英语):

  • 交易等待运行的负载( e)toTable()或管理者查询, 他们等待一个运行的交易,
  • 在一次交易召回中,他们在交易中运行:失败的负载会阻止承诺.
const ds = new DuckdbDatasource({ connection: conn });
await ds.transactionOrThrow(async (tx) => {
  await tx.queryOrThrow(sql`DELETE FROM product`);
  // rolled back with the delete if anything fails
  await sqlDuck.toTable({ table, schema, rowStream, autoCheckpoint: false });
});

说明: DuckDB在交易中无法检查,在交易回调中运行的负载的检查点被跳过( DuckDB) :autoCheckpoint, (中文(简体) ), checkpointChunksFrequency,则 checkpoint 选项 DuckTableLoader) (中文(简体) ). 使用 isInConnectionTransaction(conn) 从 @flowblade/source-duckdb 在你的代码里做同样的事情.

兼容性

职等中情局说明
节点✅要求22.18+,CI为22x,24x和26x
阿宝✅Bun 1.4+,用最新的Bun对CI进行单元测试
德诺✅Deno LTS,与Deno 2一起对CI进行单元测试
DuckDB 函数✅@duckdb/node-api 2.x (单位:千美元)^2.0.0-r.1),1.x 不再支持
字体✅TS 5.0+ + / (单位:千美元) 是- 类型- 错误 检查CI.
ES2022 (英语)✅检查文件 检查

基准

节点 24.21

 RUN  v5.0.1 /home/sebastien/github/flowblade/packages/sqlduck

 ✓  bench  bench/appender.bench.ts (1 test) 7533ms
   ✓ appender benches 7530ms
     name                                                             hz     min     max    mean     p75     p99    p995    p999      rme  samples
     duckdb appender, count: 100000, chunk size 1024              5.1751  180.11  231.60  194.56  192.86  229.70  230.65  231.41  ±10.12%        6   fastest
     duckdb appender memory, count: 100000, chunk size 2048       5.0295  172.58  244.98  202.55  226.73  244.25  244.62  244.91  ±19.34%        5
     duckdb appender file no wal, count: 100000, chunk size 1024  4.2547  210.67  317.28  240.39  237.64  314.09  315.69  316.96  ±22.80%        5
     duckdb appender file no wal, count: 100000, chunk size 2048  3.8371  200.49  323.42  268.76  295.48  322.31  322.87  323.31  ±30.51%        4
     duckdb appender file, count: 100000, chunk size 2048         3.7207  257.80  287.20  269.21  272.99  286.63  286.91  287.14   ±7.53%        4   slowest
 ✓  bench  bench/stream.bench.ts (1 test) 3986ms
   ✓ Bench rowsToColumnsChunks 3985ms
     name                                                                         hz      min      max     mean      p75      p99     p995     p999      rme  samples
     rowToColumnsChunk with chunkSize 2048 (count: 100000)                   16.4982  53.5190  78.3487  61.5746  70.6733  78.0419  78.1953  78.3180   ±7.00%       17   fastest
     rowToColumnsChunk with transformer with chunkSize 2048 (count: 100000)  13.4073  63.2078   105.13  76.1133  81.6002   102.92   104.03   104.91   ±8.98%       14
     mapFakeRowStream with chunkSize 2048 (count: 100000)                    10.3533  82.5579   120.58  98.5407   113.23   120.25   120.42   120.54  ±10.12%       11   slowest
 ✓  bench  bench/table-create.bench.ts (1 test) 1286ms
   ✓ Bench getTableCreateFromZod 1284ms
     name                          hz     min      max    mean     p75     p99    p995    p999     rme  samples
     getTableCreateFromZod  20,993.53  0.0343  10.9287  0.0597  0.0619  0.2081  0.3518  0.6929  ±4.00%    16755

1.4.2号舱

 RUN  v5.0.1 /home/sebastien/github/flowblade/packages/sqlduck

 ✓  bench  bench/appender.bench.ts (1 test) 7353ms
   ✓ appender benches 7351ms
     name                                                             hz     min     max    mean     p75     p99    p995    p999      rme  samples
     duckdb appender memory, count: 100000, chunk size 2048       6.5010  141.93  167.82  154.19  158.35  167.29  167.55  167.77   ±4.92%        7   fastest
     duckdb appender, count: 100000, chunk size 1024              5.8972  158.33  185.98  170.04  174.01  185.43  185.70  185.92   ±6.09%        6
     duckdb appender file no wal, count: 100000, chunk size 2048  5.1876  181.29  250.19  195.34  189.16  247.18  248.69  249.89  ±14.55%        6
     duckdb appender file no wal, count: 100000, chunk size 1024  4.8410  187.20  264.48  209.80  208.05  262.22  263.35  264.26  ±18.63%        5
     duckdb appender file, count: 100000, chunk size 2048         4.1319  229.30  261.16  242.64  253.36  260.85  261.00  261.13   ±7.09%        5   slowest
 ✓  bench  bench/stream.bench.ts (1 test) 3933ms
   ✓ Bench rowsToColumnsChunks 3931ms
     name                                                                         hz      min      max     mean      p75      p99     p995     p999     rme  samples
     rowToColumnsChunk with chunkSize 2048 (count: 100000)                   24.5296  34.3957  51.2149  41.1676  43.4825  50.3109  50.7629  51.1245  ±4.18%       25   fastest
     rowToColumnsChunk with transformer with chunkSize 2048 (count: 100000)  23.5864  36.3807  57.7801  42.8068  43.2094  56.3888  57.0845  57.6410  ±4.56%       24
     mapFakeRowStream with chunkSize 2048 (count: 100000)                    21.5954  42.7406  51.6524  46.4448  48.1176  51.5462  51.5993  51.6418  ±2.52%       22   slowest
 ✓  bench  bench/table-create.bench.ts (1 test) 1310ms
   ✓ Bench getTableCreateFromZod 1308ms
     name                          hz     min      max    mean     p75     p99    p995    p999     rme  samples
     getTableCreateFromZod  37,765.77  0.0185  10.1286  0.0360  0.0346  0.1235  0.1771  2.3683  ±4.27%    27781

本地脚本

名称说明
pnpm build
pnpm typecheck
pnpm lint检查 lint 错误
pnpm lint --fix尝试运行自动插件
pnpm test-unit运行单元测试
pnpm test-e2e运行单元测试

版本与发布记录

@flowblade/sqlduck 共发布 89 个版本,首次发布于 2025/12/23;过去 12 个月发布了 89 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.47.0latest2026/11/114 天前
0.46.02026/11/105 天前
0.45.22026/11/087 天前
0.45.12026/11/087 天前
0.45.02026/11/069 天前
0.44.12026/11/0610 天前

关于 @flowblade/sqlduck 的常见问题

如何安装 @flowblade/sqlduck?

在项目目录运行 npm install @flowblade/sqlduck,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@duckdb/node-api(^2.0.0-r.1)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@flowblade/sqlduck 支持 TypeScript 吗?

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

@flowblade/sqlduck 支持 ESM 还是 CommonJS?

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

@flowblade/sqlduck 需要什么版本的 Node.js?

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

@flowblade/sqlduck 可以商用吗?

@flowblade/sqlduck 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@flowblade/sqlduck 的下载量有多少?

根据 npm 官方下载统计,@flowblade/sqlduck 在 2026/10/15 – 2026/11/13 这 30 天内被下载 2,292 次,日均约 76 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@flowblade/sqlduck 还在维护吗?

最新版本 0.47.0 发布于 2026/11/11(4 天前);至今共发布 89 个版本,其中过去 12 个月发布了 89 个;首次发布于 2025/12/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。