@00akshatsinha00/convex-cascading-delete

中文文档
0.1.0发布于 2026/02/09Apache-2.0TypeScriptES Module零依赖近 30 天下载 8028

一个用于管理串联的 Convex 组件,用原子和分批删除模式在相关文档中删除.

中文使用文档
文档目录36 节
  1. 为什么使用此组件 ?
  2. 预先要求: Convex
  3. 安装
  4. 步骤1:安装软件包
  5. 步骤2: 将组件添加到您的 Convex 应用程序
  6. 第3步:配置级联规则和即时处理
  7. 快速启动
  8. API 参考
  9. defineCascadeRules(config)
  10. CascadingDelete 类
  11. makeBatchDeleteHandler(internalMutationBuilder, componentRe…
  12. 反应钩
  13. 导出类型
  14. 方案要求
  15. 实例
  16. 多层次等级
  17. 分枝带
  18. 循环依赖
  19. 使用补丁Db作为安全警卫
  20. 最佳做法
  21. 结构概览
  22. 一致性保障
  23. 内置模式( I) deleteWithCascade (中文(简体) )
  24. 挂载模式( E) deleteWithCascadeBatched (中文(简体) )
  25. 性能特征
  26. 测试
  27. 运行示例
  28. 解决问题
  29. “ Index 不存在” 错误
  30. 批次删除已卡住
  31. 交易限额超过
  32. 带有表格名称的错误类型
  33. 现场直播
  34. 找到窃听器了吗? 特质请求?
  35. 许可证
  36. 已建为

Convex 积分删除

npm version

用于管理串联的 Convex 组件删除相关文档。 通过现有的索引配置关系,然后安全地删除知道所有相关记录的文档将自动清理,并有明确的一致性保障.

为什么使用此组件 ?

  • 现有计划 - 不向特殊计划定义迁移;使用您现有的 defineTable 和指数
  • 明确配置 - 在一个地方界定的级联关系的明确、申报性规则
  • 两种删除模式 - 内置(原子、单项交易)小项,分批(排定)大树
  • 进展跟踪 - 实时批次删除进度的反钩并作被动更新
  • 警卫 - 可选 patchDb 帮助防止意外直接 db.delete 电话
  • 索引验证 - 启动时抓取配置错误,而不是删除时
  • 循环处理 - 通过访问的一组自动处理循环和钻石依赖
  • 完全可观察性 - 按表格列出的返回删除摘要
  • 非侵入性 - 滴入组件,该组件不会替换您的系统构建器,或需要修改代码,超出删除调用范围

预先要求: Convex

你需要一个已有的 Convex 项目来使用这个组件. Convex是一个主机后端平台,包括一个数据库,没有服务器的功能,还有一堆你还可以了解的 这里。 。 。 .

运行 npm create convex 或遵循其中任何一条 Convex 快速启动 来设置一个.

安装

步骤1:安装软件包

npm install @00akshatsinha00/convex-cascading-delete

步骤2: 将组件添加到您的 Convex 应用程序

// convex/convex.config.ts
import { defineApp } from "convex/server";
import convexCascadingDelete from "@00akshatsinha00/convex-cascading-delete/convex.config";

const app = defineApp();
app.use(convexCascadingDelete);

export default app;

第3步:配置级联规则和即时处理

// convex/cascading.ts
import {
  CascadingDelete,
  defineCascadeRules,
  makeBatchDeleteHandler
} from "@00akshatsinha00/convex-cascading-delete";
import { components } from "./_generated/api";
import { internalMutation } from "./_generated/server";

export const cascadeRules = defineCascadeRules({
  users: [
    { to: "posts", via: "byAuthorId", field: "authorId" },
    { to: "comments", via: "byAuthorId", field: "authorId" }
  ],
  posts: [
    { to: "comments", via: "byPostId", field: "postId" }
  ]
});

export const cd = new CascadingDelete(components.convexCascadingDelete, {
  rules: cascadeRules
});

// Required for batched mode - exports an internal mutation that processes deletion batches
export const _cascadeBatchHandler = makeBatchDeleteHandler(
  internalMutation,
  components.convexCascadingDelete
);

快速启动

使用配置 cd 在您的突变中:

// convex/users.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { cd } from "./cascading";

export const deleteUser = mutation({
  args: { userId: v.id("users") },
  handler: async (ctx, { userId }) => {
    // Deletes user + all their posts + all comments on those posts
    const summary = await cd.deleteWithCascade(ctx, "users", userId);
    console.log("Deleted:", summary);
    // Returns: { users: 1, posts: 5, comments: 23 }
  }
});

对于大删除树,使用分批模式:

// convex/organizations.ts
import { mutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
import { cd } from "./cascading";

export const deleteOrganization = mutation({
  args: { orgId: v.id("organizations") },
  handler: async (ctx, { orgId }) => {
    const result = await cd.deleteWithCascadeBatched(
      ctx,
      "organizations",
      orgId,
      {
        batchHandlerRef: internal.cascading._cascadeBatchHandler,
        batchSize: 2000
      }
    );
    // result.jobId can be used to track progress via useDeletionJobStatus hook
    // result.initialSummary contains counts from the first inline batch
    return result;
  }
});

API 参考

defineCascadeRules(config)

定义和验证表格之间的级联关系。 返回被冻结的配置对象.

const rules = defineCascadeRules({
  [sourceTable: string]: [
    {
      to: string,       // Target table name to cascade to
      via: string,       // Index name on target table
      field: string      // Field in index used for equality matching (holds parent ID)
    }
  ]
});

所需经费:

  • 指定的索引 via 目标表中必须存在
  • 索引必须包含 field
  • 这个 field 必须包含来自源表格的ID

进行验证:

  • 所有属性( A)to, (中文(简体) ), via, (中文(简体) ), field)必须是在场的和弦
  • 复制规则(相同的) to:via:field 组合)被拒绝
  • 配置必须是非无效对象

CascadingDelete 类

用于删除操作的主要接口.

构造器

const cd = new CascadingDelete(components.convexCascadingDelete, { rules });
参数类型说明
componentComponentApi构成部分参考来源 components.convexCascadingDelete
options.rulesCascadeConfig规则从 defineCascadeRules()

deleteWithCascade(ctx, table, id)

删除单项交易中的文档及其所有连带依赖。 使用深度第一的后序转录(在父母之前删除儿童),并使用访问器进行循环检测.

const summary: DeletionSummary = await cd.deleteWithCascade(ctx, "users", userId);
// Returns: { users: 1, posts: 5, comments: 23 }
参数类型说明
ctxMutationCtxConvex 变异上下文
tablestring源表格名称
idstring要删除的文档 ID
回返DeletionSummary删除文档计数的表格名称地图

最佳服务: 中小删除树(4 000多份文件)

一致性 : 完全原子 - 单个 Convex 交易中所有删除成功或全部失败

deleteWithCascadeBatched(ctx, table, id, options)

删除文档及其依存于多个批次交易中。 首先通过只读的 traversal 收集所有目标,删除第一批内入,再通过组件的工作系统将剩余批次排出.

const result = await cd.deleteWithCascadeBatched(
  ctx,
  "organizations",
  orgId,
  {
    batchHandlerRef: internal.cascading._cascadeBatchHandler,
    batchSize: 2000  // Optional, defaults to 2000
  }
);
// Returns: { jobId: "j57a...", initialSummary: { organizations: 1, teams: 3 } }
// jobId is null if all targets fit in the first batch
参数类型说明
ctxMutationCtxConvex 变异上下文
tablestring源表格名称
idstring要删除的文档 ID
options.batchHandlerRefFunctionReference<"mutation">引用您的导出批量处理器( 从 makeBatchDeleteHandler(中文(简体) )
options.batchSizenumber (可选)每批文件,默认为2000年
回返{ jobId: string | null, initialSummary: DeletionSummary }用于跟踪的任务ID( 如果全部被删除的话) 和第一批摘要

最佳服务: 大删除树( 大小)

一致性 : 每批原子, 间批最终。 每批都是单独的Convex交易.

进展跟踪: 传回 jobId 页:1 useDeletionJobStatus 反应钩

validateRules(ctx)

通过测试查询对每个索引进行测试,验证所有配置的索引是否存在。 应用程序初始化时或仅通过 dev 检查时, 应该呼叫一次 .

await cd.validateRules(ctx);
// Throws descriptive error if any index is missing or misconfigured
参数类型说明
ctxQueryCtxConvex 查询或变异上下文

patchDb(db)

返回直接投放的代理数据库写入器 .delete() ,强制所有删除都通过 deleteWithCascade作为关键突变中的安全警卫很有用.

export const safeDeleteUser = mutation({
  handler: async (ctx, args) => {
    const safeDb = cd.patchDb(ctx.db);
    // safeDb.delete(id)  --> throws "Direct db.delete() is disabled"
    // safeDb.query(...)   --> works normally
    // safeDb.insert(...)  --> works normally
    // safeDb.patch(...)   --> works normally
  }
});

makeBatchDeleteHandler(internalMutationBuilder, componentRef)

工厂功能为处理删除批次而创建app-side内部突变. 此函数必须从您的凸起码导出, 这样组件的调度器就可以通过函数控件引用它 .

import { makeBatchDeleteHandler } from "@00akshatsinha00/convex-cascading-delete";
import { components } from "./_generated/api";
import { internalMutation } from "./_generated/server";

export const _cascadeBatchHandler = makeBatchDeleteHandler(
  internalMutation,
  components.convexCascadingDelete
);
参数类型说明
internalMutationBuilderInternalMutation你的应用软件 internalMutation 来自 _generated/server
componentRefComponentApi构成部分参考来源 components.convexCascadingDelete
回返FunctionReference<"mutation">内部变异以通过 batchHandlerRef

如何运作: 返回的突变收到一批 { table, id } 目标,通过 ctx.db.delete(id),然后通过 reportBatchComplete。该组件的调度器调用此函数处理每个批次.

反应钩

useDeletionJobStatus(api, jobId)

监视带有反应性更新的批量删除进度 。 环绕组件的 getJobStatus 查询.

import { useDeletionJobStatus } from "@00akshatsinha00/convex-cascading-delete/react";
import { api } from "../convex/_generated/api";

function DeletionProgress({ jobId }: { jobId: string | null }) {
  const status = useDeletionJobStatus(api, jobId);

  if (!status) return null;

  const progress = (status.completedCount / status.totalTargetCount) * 100;

  return (
    <div>
      <progress value={progress} max={100} />
      <p>{status.status}: {status.completedCount} / {status.totalTargetCount}</p>
      {status.status === "completed" && (
        <pre>{JSON.stringify(JSON.parse(status.completedSummary), null, 2)}</pre>
      )}
    </div>
  );
}
参数类型说明
apiany你的应用软件 api 对象从 _generated/api
jobIdstring | null工作编号从 deleteWithCascadeBatched,或无法跳过
回返BatchJobStatus | null目前的工作状况,如果没有工作/找不到工作,则无效

BatchJobStatus 形状 :

外地类型说明
status"pending" | "processing" | "completed" | "failed"当前工作状态
totalTargetCountnumber要删除的文件共计
completedCountnumber迄今删除的文件
completedSummarystringJSON 字符串映射表名以删除计数
errorstring | undefined如果任务失败, 错误消息

导出类型

所有类型均可从主要入口导入:

import type {
  CascadeRule,       // { to: string; via: string; field: string }
  CascadeConfig,     // { [sourceTable: string]: CascadeRule[] }
  DeletionSummary,   // { [tableName: string]: number }
  DeletionTarget,    // { table: string; id: string }
  BatchJobStatus,    // { status, totalTargetCount, completedCount, completedSummary, error? }
} from "@00akshatsinha00/convex-cascading-delete";

方案要求

你的计划一定有与你的级联规则相匹配的索引 每个规则 via 必须对应 to 表格,还有 field 必须是该索引的第一个字段.

// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
  }),

  posts: defineTable({
    authorId: v.id("users"),
    title: v.string(),
    content: v.string(),
  }).index("byAuthorId", ["authorId"]),  // Required for cascade from users

  comments: defineTable({
    authorId: v.id("users"),
    postId: v.id("posts"),
    text: v.string(),
  })
    .index("byAuthorId", ["authorId"])   // For user → comments cascade
    .index("byPostId", ["postId"]),      // For post → comments cascade
});

相应的级联规则是:

const rules = defineCascadeRules({
  users: [
    { to: "posts", via: "byAuthorId", field: "authorId" },
    { to: "comments", via: "byAuthorId", field: "authorId" }
  ],
  posts: [
    { to: "comments", via: "byPostId", field: "postId" }
  ]
});

实例

多层次等级

const rules = defineCascadeRules({
  organizations: [
    { to: "teams", via: "byOrganizationId", field: "organizationId" }
  ],
  teams: [
    { to: "members", via: "byTeamId", field: "teamId" },
    { to: "projects", via: "byTeamId", field: "teamId" }
  ],
  projects: [
    { to: "tasks", via: "byProjectId", field: "projectId" }
  ],
  tasks: [
    { to: "comments", via: "byTaskId", field: "taskId" }
  ]
});

// Deleting an organization cascades through 5 levels
const summary = await cd.deleteWithCascade(ctx, "organizations", orgId);
// Returns: { organizations: 1, teams: 5, members: 23, projects: 12, tasks: 67, comments: 234 }

分枝带

单亲表可连入多个依赖表:

const rules = defineCascadeRules({
  users: [
    { to: "posts", via: "byAuthorId", field: "authorId" },
    { to: "comments", via: "byAuthorId", field: "authorId" },
    { to: "likes", via: "byUserId", field: "userId" },
    { to: "follows", via: "byFollowerId", field: "followerId" }
  ]
});

循环依赖

该组件通过访问集自动处理循环引用。 无无限循环 :

const rules = defineCascadeRules({
  users: [
    { to: "friendships", via: "byUserId", field: "userId" }
  ],
  friendships: [
    { to: "users", via: "byFriendId", field: "friendId" }
  ]
});

// Safe - visited set prevents re-processing already-seen documents
const summary = await cd.deleteWithCascade(ctx, "users", userId);

使用补丁Db作为安全警卫

import { mutation } from "./_generated/server";
import { cd } from "./cascading";

export const processUser = mutation({
  handler: async (ctx, args) => {
    // Replace ctx.db with a guarded version for this mutation
    const safeCtx = { ...ctx, db: cd.patchDb(ctx.db) };

    // All reads work normally
    const user = await safeCtx.db.get(args.userId);

    // Direct deletes are blocked - forces cascade usage
    // safeCtx.db.delete(args.userId)  --> throws Error

    // Must use cascade delete instead
    await cd.deleteWithCascade(ctx, "users", args.userId);
  }
});

最佳做法

  1. 从内线模式开始 - 使用( I) deleteWithCascade 对于大多数案例来说,它更简单,完全原子化
  2. 切换为大树批次 - 使用( I) deleteWithCascadeBatched 当删除超过4,000个文档以避免交易限制时
  3. 验证启动规则 - 打电话 validateRules() 在一个 dev- 仅初始化函数中及早捕获错误配置的索引
  4. 在关键突变中使用补丁Db - 防止意外直接删除留下孤儿记录
  5. 监测批量进度 - 用那个 useDeletionJobStatus 显示用户实时删除反馈的钩头
  6. 测试级联规则 - 利用测试辅助人员核查生产前的预期关系

结构概览

┌─────────────────────────────────────────────────────────────────┐
│  YOUR APP                                                       │
│                                                                 │
│  ┌──────────────────────────────────┐                           │
│  │  Your Mutation                   │                           │
│  │                                  │                           │
│  │  const cd = new CascadingDelete( │                           │
│  │    components.convexCascadingDel,│                           │
│  │    { rules: cascadeRules }       │                           │
│  │  );                              │                           │
│  │                                  │  ctx.db (YOUR tables)     │
│  │  // Inline mode:                 │─────► .query(table)       │
│  │  cd.deleteWithCascade(ctx,       │       .withIndex(idx,...) │
│  │    "teams", teamId)              │       .collect()          │
│  │                                  │       .delete(id)         │
│  │  // Batched mode:                │                           │
│  │  cd.deleteWithCascadeBatched(ctx,│                           │
│  │    "teams", teamId, opts)        │                           │
│  │                                  │                           │
│  └──────────┬───────────────────────┘                           │
│             │                                                   │
│             │ ctx.runMutation(component.lib.createBatchJob, ...)│
│             │ ctx.runQuery(component.lib.getJobStatus, ...)     │
│             ▼                                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  COMPONENT (Isolated — own DB, own transactions)         │   │
│  │                                                          │   │
│  │  Table: deletionJobs                                     │   │
│  │    { status, targets, deleteHandle, batchSize, summary } │   │
│  │                                                          │   │
│  │  Functions:                                              │   │
│  │    createBatchJob(targets, handle, batchSize)            │   │
│  │    processNextBatch(jobId)                               │   │
│  │      ├─ scheduler.runAfter(0, deleteHandle, batch)     ──┼──►│
│  │      └─ scheduler.runAfter(200ms, self, jobId)           │   │
│  │    getJobStatus(jobId) → reactive query                  │   │
│  │    reportBatchComplete(jobId, summary)                   │   │
│  └──────────────────────────────────────────────────────────┘   │
│             │                                                   │
│             │ Function handle callback                          │
│             ▼                                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Your Batch Delete Handler (via makeBatchDeleteHandler)  │   │
│  │                                                          │   │
│  │  handler: async (ctx, { targets, jobId }) => {           │   │
│  │    for (t of targets) await ctx.db.delete(t.id);         │   │
│  │    await ctx.runMutation(component.reportBatchComplete,  │   │
│  │      { jobId, summary });                                │   │
│  │  }                                                       │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

主要建筑制约: Convex 组件无法访问您的应用程序的表格 。 在您的应用程序的突变上下文中, 所有文档的翻转和删除都会运行 ctx.db该部分只管理自己孤立的数据库中的批量工作状态(创建、进步、完成).

一致性保障

内置模式( I)deleteWithCascade(中文(简体) )

  • 完全原子 - 所有删除在单一Convex交易中成功或全部失败
  • ACID 达标 - 汇兑公司内部的交易担保
  • 即时 - 返回完成 DeletionSummary 同步

挂载模式( E)deleteWithCascadeBatched(中文(简体) )

  • 每批原子 - 每批都是单独的Convex交易
  • 最终间批次 - Batches在两百米之间同步处理
  • 第一批内入 - 在调用突变中删除最初批次,以便立即反馈
  • 其余一批已排定 - 通过组件调度器使用函数手柄进行处理
  • 可观察到的进展 - 使用( I) useDeletionJobStatus 钩子或 getJobStatus 实时状态查询

性能特征

特征细节
内置模式限制~4,000个文档(根据Convex每笔交易16K的写入限制)
批量大小可配置, 默认为每批2,000个文件
逆变算法深度第一,顺序后(在父母面前删除儿童)
循环检测O(1) 通过 Set<string>
索引使用量高效 .withIndex() 查询——无表格扫描
批次调度延迟分批200米,防止排程器被淹
遵守对流限制16K写作,32K文档扫描,4,096索引读取,每笔交易1s执行

测试

软件包导出一个测试助手, 用于 convex-test数字 :

import { convexTest } from "convex-test";
import { register } from "@00akshatsinha00/convex-cascading-delete/test";
import schema from "./schema";

const modules = import.meta.glob("./convex/**/*.ts");

test("cascading delete works", async () => {
  const t = convexTest(schema, modules);
  register(t, "convexCascadingDelete");

  // ... your test code using the component
});

这个 register 函数在测试实例中记录组件的策略和模块。 第二个参数必须与您中的组件名称匹配 convex.config.ts。 。 。 .

运行示例

这个 example/ 目录包含一个完整的工作应用程序,显示内线和分批删除模式,并具有5级组织层级.

# Clone the repository
git clone https://github.com/akshatsinha0/convex-cascading-delete.git
cd convex-cascading-delete

# Install dependencies
npm install

# Start the dev server (backend + frontend + build watcher)
npm run dev

例如

  • 种子数据按钮 - 创建有团队、成员、项目、任务和评论的抽样组织
  • 内行删除 - 在一次交易中以解剖方式删除一个组织
  • 挂起删除 - 删除一个组织在多个分批交易中实时取得进展
  • 文档计数器 - 参见所有6个表格中的反应性更新数
  • (原始内容存档于2013-10-12) (英语). REST API - HTTP端点在 /api/deletion-job-status?jobId=... 外部职务监测

解决问题

“ Index 不存在” 错误

运行 validateRules() 以识别缺失的索引 :

await cd.validateRules(ctx);
// Error: Cascade validation failed: Index "byAuthorId" with field "authorId"
// does not exist on table "posts". Define it in your schema.
// Source table: "users"

将缺少的索引添加到您的计划中 .index("indexName", ["fieldName"])。 。 。 .

批次删除已卡住

直接检查工作状态 :

const status = await ctx.runQuery(
  components.convexCascadingDelete.lib.getJobStatus,
  { jobId }
);
console.log(status);
// { status: "processing", totalTargetCount: 500, completedCount: 200, ... }

如果一个工作卡在 "processing" 状态,可能是由于批量处理器功能没有被正确导出或部署不匹配.

交易限额超过

如果内置模式出现交易限制出错而失败, 请切换到分批模式 :

// Instead of:
await cd.deleteWithCascade(ctx, "organizations", orgId);

// Use:
await cd.deleteWithCascadeBatched(ctx, "organizations", orgId, {
  batchHandlerRef: internal.cascading._cascadeBatchHandler,
  batchSize: 1000  // Reduce batch size if needed
});

带有表格名称的错误类型

动态表格访问使用类型断言 :

const summary = await cd.deleteWithCascade(ctx, "users", userId as any);

现场直播

尝试交互式演示 : https://convex-cascadding-delete.vercel.app 互联网档案馆的存檔,存档日期2013-12-21

找到窃听器了吗? 特质请求?

放在这。 。 。 .

许可证

阿帕奇 - 2.0

已建为

Convex 组件写入挑战 - 全速投放功能

版本与发布记录

@00akshatsinha00/convex-cascading-delete 共发布 1 个版本,首次发布于 2026/02/09;过去 12 个月发布了 1 个版本。

版本发布日期距今
0.1.0latest2026/02/098 个月前

关于 @00akshatsinha00/convex-cascading-delete 的常见问题

@00akshatsinha00/convex-cascading-delete 是什么?

一个用于管理串联的 Convex 组件,用原子和分批删除模式在相关文档中删除.

如何安装 @00akshatsinha00/convex-cascading-delete?

在项目目录运行 npm install @00akshatsinha00/convex-cascading-delete,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:convex(^1.31.7)、react(^18.3.1 || ^19.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@00akshatsinha00/convex-cascading-delete 支持 TypeScript 吗?

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

@00akshatsinha00/convex-cascading-delete 支持 ESM 还是 CommonJS?

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

@00akshatsinha00/convex-cascading-delete 可以商用吗?

@00akshatsinha00/convex-cascading-delete 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@00akshatsinha00/convex-cascading-delete 的下载量有多少?

根据 npm 官方下载统计,@00akshatsinha00/convex-cascading-delete 在 2026/10/04 – 2026/11/02 这 30 天内被下载 8,028 次,日均约 268 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@00akshatsinha00/convex-cascading-delete 还在维护吗?

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