koishi-plugin-best-cave 2.4.2 → 2.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ import { Context, Logger } from 'koishi';
2
+ import { FileManager } from './FileManager';
3
+ import { Config } from './index';
4
+ /**
5
+ * @class DataManager
6
+ * @description 负责处理回声洞数据的导入和导出功能。
7
+ */
8
+ export declare class DataManager {
9
+ private ctx;
10
+ private config;
11
+ private fileManager;
12
+ private logger;
13
+ /**
14
+ * @constructor
15
+ * @param ctx Koishi 上下文,用于数据库操作。
16
+ * @param config 插件配置。
17
+ * @param fileManager 文件管理器实例。
18
+ * @param logger 日志记录器实例。
19
+ */
20
+ constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger);
21
+ /**
22
+ * @description 注册 `.export` 和 `.import` 子命令。
23
+ * @param cave - 主 `cave` 命令实例。
24
+ */
25
+ registerCommands(cave: any): void;
26
+ /**
27
+ * @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
28
+ * @returns 描述导出结果的消息字符串。
29
+ */
30
+ exportData(): Promise<string>;
31
+ /**
32
+ * @description 从 `cave_import.json` 文件导入回声洞数据。
33
+ * @returns 描述导入结果的消息字符串。
34
+ */
35
+ importData(): Promise<string>;
36
+ }
@@ -0,0 +1,48 @@
1
+ import { Logger } from 'koishi';
2
+ import { Config } from './index';
3
+ /**
4
+ * @class FileManager
5
+ * @description 封装了对文件的存储、读取和删除操作。
6
+ * 能根据配置自动选择使用本地文件系统或 AWS S3 作为存储后端。
7
+ * 内置 Promise 文件锁,防止本地文件的并发写入冲突。
8
+ */
9
+ export declare class FileManager {
10
+ private logger;
11
+ private resourceDir;
12
+ private locks;
13
+ private s3Client?;
14
+ private s3Bucket?;
15
+ /**
16
+ * @constructor
17
+ * @param baseDir Koishi 应用的基础数据目录 (ctx.baseDir)。
18
+ * @param config 插件的配置对象。
19
+ * @param logger 日志记录器实例。
20
+ */
21
+ constructor(baseDir: string, config: Config, logger: Logger);
22
+ /**
23
+ * @description 使用文件锁安全地执行异步文件操作,防止并发读写冲突。
24
+ * @template T 异步操作的返回类型。
25
+ * @param fullPath 需要加锁的文件的完整路径。
26
+ * @param operation 要执行的异步函数。
27
+ * @returns 异步操作的结果。
28
+ */
29
+ private withLock;
30
+ /**
31
+ * @description 保存文件,自动选择 S3 或本地存储。
32
+ * @param fileName 用作 S3 Key 或本地文件名。
33
+ * @param data 要写入的 Buffer 数据。
34
+ * @returns 保存时使用的文件名。
35
+ */
36
+ saveFile(fileName: string, data: Buffer): Promise<string>;
37
+ /**
38
+ * @description 读取文件,自动从 S3 或本地存储读取。
39
+ * @param fileName 要读取的文件名/标识符。
40
+ * @returns 文件的 Buffer 数据。
41
+ */
42
+ readFile(fileName: string): Promise<Buffer>;
43
+ /**
44
+ * @description 删除文件,自动从 S3 或本地删除。
45
+ * @param fileIdentifier 要删除的文件名/标识符。
46
+ */
47
+ deleteFile(fileIdentifier: string): Promise<void>;
48
+ }
@@ -0,0 +1,102 @@
1
+ import { Context, Logger } from 'koishi';
2
+ import { Config, CaveObject } from './index';
3
+ import { FileManager } from './FileManager';
4
+ /**
5
+ * @description 数据库 `cave_hash` 表的完整对象模型。
6
+ */
7
+ export interface CaveHashObject {
8
+ cave: number;
9
+ hash: string;
10
+ type: 'simhash' | 'phash_g' | 'phash_q1' | 'phash_q2' | 'phash_q3' | 'phash_q4';
11
+ }
12
+ /**
13
+ * @class HashManager
14
+ * @description 负责生成、存储和比较文本与图片的哈希值。
15
+ * 实现了基于 Simhash 的文本查重和基于 DCT 感知哈希 (pHash) 的图片查重方案。
16
+ */
17
+ export declare class HashManager {
18
+ private ctx;
19
+ private config;
20
+ private logger;
21
+ private fileManager;
22
+ /**
23
+ * @constructor
24
+ * @param ctx - Koishi 上下文,用于数据库操作。
25
+ * @param config - 插件配置,用于获取相似度阈值等。
26
+ * @param logger - 日志记录器实例。
27
+ * @param fileManager - 文件管理器实例,用于读取图片文件。
28
+ */
29
+ constructor(ctx: Context, config: Config, logger: Logger, fileManager: FileManager);
30
+ /**
31
+ * @description 注册与哈希功能相关的 `.hash` 和 `.check` 子命令。
32
+ * @param cave - 主 `cave` 命令实例。
33
+ */
34
+ registerCommands(cave: any): void;
35
+ /**
36
+ * @description 检查数据库中所有回声洞,为没有哈希记录的历史数据生成哈希。
37
+ * @returns 一个包含操作结果的报告字符串。
38
+ */
39
+ generateHashesForHistoricalCaves(): Promise<string>;
40
+ /**
41
+ * @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
42
+ * @param cave - 回声洞对象。
43
+ * @returns 生成的哈希对象数组。
44
+ */
45
+ generateAllHashesForCave(cave: Pick<CaveObject, 'id' | 'elements'>): Promise<CaveHashObject[]>;
46
+ /**
47
+ * @description 对数据库中所有哈希进行两两比较,找出相似度过高的内容。
48
+ * @param options 包含临时阈值的可选对象。
49
+ * @returns 一个包含检查结果的报告字符串。
50
+ */
51
+ checkForSimilarCaves(options?: {
52
+ textThreshold?: number;
53
+ imageThreshold?: number;
54
+ }): Promise<string>;
55
+ /**
56
+ * @description 为单个图片Buffer生成全局pHash和四个象限的局部pHash。
57
+ * @param imageBuffer - 图片的Buffer数据。
58
+ * @returns 包含全局哈希和四象限哈希的对象。
59
+ */
60
+ generateAllImageHashes(imageBuffer: Buffer): Promise<{
61
+ globalHash: string;
62
+ quadrantHashes: {
63
+ q1: string;
64
+ q2: string;
65
+ q3: string;
66
+ q4: string;
67
+ };
68
+ }>;
69
+ /**
70
+ * @description 执行二维离散余弦变换 (DCT-II)。
71
+ * @param matrix - 输入的 N x N 像素亮度矩阵。
72
+ * @returns DCT变换后的 N x N 系数矩阵。
73
+ */
74
+ private _dct2D;
75
+ /**
76
+ * @description pHash 算法核心实现。
77
+ * @param imageBuffer - 图片的Buffer。
78
+ * @param size - 期望的哈希位数 (必须是完全平方数, 如 64 或 256)。
79
+ * @returns 十六进制pHash字符串。
80
+ */
81
+ private _generatePHash;
82
+ /**
83
+ * @description 计算两个十六进制哈希字符串之间的汉明距离 (不同位的数量)。
84
+ * @param hex1 - 第一个哈希。
85
+ * @param hex2 - 第二个哈希。
86
+ * @returns 汉明距离。
87
+ */
88
+ calculateHammingDistance(hex1: string, hex2: string): number;
89
+ /**
90
+ * @description 根据汉明距离计算相似度百分比。
91
+ * @param hex1 - 第一个哈希。
92
+ * @param hex2 - 第二个哈希。
93
+ * @returns 相似度 (0-100)。
94
+ */
95
+ calculateSimilarity(hex1: string, hex2: string): number;
96
+ /**
97
+ * @description 为文本生成 64 位 Simhash 字符串。
98
+ * @param text - 需要处理的文本。
99
+ * @returns 16位十六进制 Simhash 字符串。
100
+ */
101
+ generateTextSimhash(text: string): string;
102
+ }
@@ -0,0 +1,45 @@
1
+ import { Context } from 'koishi';
2
+ /** 数据库 `cave_user` 表的结构。 */
3
+ export interface UserName {
4
+ userId: string;
5
+ nickname: string;
6
+ }
7
+ declare module 'koishi' {
8
+ interface Tables {
9
+ cave_user: UserName;
10
+ }
11
+ }
12
+ /**
13
+ * @class NameManager
14
+ * @description 负责管理用户在回声洞中的自定义昵称。
15
+ */
16
+ export declare class NameManager {
17
+ private ctx;
18
+ /**
19
+ * @constructor
20
+ * @param ctx - Koishi 上下文,用于初始化数据库模型。
21
+ */
22
+ constructor(ctx: Context);
23
+ /**
24
+ * @description 注册 `.name` 子命令,用于管理用户昵称。
25
+ * @param cave - 主 `cave` 命令实例。
26
+ */
27
+ registerCommands(cave: any): void;
28
+ /**
29
+ * @description 设置或更新指定用户的昵称。
30
+ * @param userId - 目标用户的 ID。
31
+ * @param nickname - 要设置的新昵称。
32
+ */
33
+ setNickname(userId: string, nickname: string): Promise<void>;
34
+ /**
35
+ * @description 获取指定用户的昵称。
36
+ * @param userId - 目标用户的 ID。
37
+ * @returns 用户的昵称字符串或 null。
38
+ */
39
+ getNickname(userId: string): Promise<string | null>;
40
+ /**
41
+ * @description 清除指定用户的昵称设置。
42
+ * @param userId - 目标用户的 ID。
43
+ */
44
+ clearNickname(userId: string): Promise<void>;
45
+ }
@@ -0,0 +1,32 @@
1
+ import { Context, Logger } from 'koishi';
2
+ import { CaveObject, Config } from './index';
3
+ import { FileManager } from './FileManager';
4
+ /**
5
+ * @class PendManager
6
+ * @description 负责处理回声洞的审核流程,处理新洞的提交、审核通知和审核操作。
7
+ */
8
+ export declare class PendManager {
9
+ private ctx;
10
+ private config;
11
+ private fileManager;
12
+ private logger;
13
+ private reusableIds;
14
+ /**
15
+ * @param ctx Koishi 上下文。
16
+ * @param config 插件配置。
17
+ * @param fileManager 文件管理器实例。
18
+ * @param logger 日志记录器实例。
19
+ * @param reusableIds 可复用 ID 的内存缓存。
20
+ */
21
+ constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reusableIds: Set<number>);
22
+ /**
23
+ * @description 注册与审核相关的子命令。
24
+ * @param cave - 主 `cave` 命令实例。
25
+ */
26
+ registerCommands(cave: any): void;
27
+ /**
28
+ * @description 将新回声洞提交到管理群组以供审核。
29
+ * @param cave 新创建的、状态为 'pending' 的回声洞对象。
30
+ */
31
+ sendForPend(cave: CaveObject): Promise<void>;
32
+ }
package/lib/Utils.d.ts CHANGED
@@ -37,15 +37,6 @@ export declare function getScopeQuery(session: Session, config: Config, includeS
37
37
  * @returns 可用的新 ID。
38
38
  */
39
39
  export declare function getNextCaveId(ctx: Context, reusableIds: Set<number>): Promise<number>;
40
- /**
41
- * @description 检查用户是否处于指令冷却中。
42
- * @returns 若在冷却中则提示字符串,否则 null。
43
- */
44
- export declare function checkCooldown(session: Session, config: Config, lastUsed: Map<string, number>): string | null;
45
- /**
46
- * @description 更新指定频道的指令使用时间戳。
47
- */
48
- export declare function updateCooldownTimestamp(session: Session, config: Config, lastUsed: Map<string, number>): void;
49
40
  /**
50
41
  * @description 解析消息元素,分离出文本和待下载的媒体文件。
51
42
  * @param sourceElements 原始的 Koishi 消息元素数组。
package/lib/index.d.ts CHANGED
@@ -38,7 +38,6 @@ declare module 'koishi' {
38
38
  }
39
39
  }
40
40
  export interface Config {
41
- coolDown: number;
42
41
  perChannel: boolean;
43
42
  adminChannel: string;
44
43
  enableName: boolean;
package/lib/index.js CHANGED
@@ -239,8 +239,8 @@ var DataManager = class {
239
239
  return `操作失败: ${error.message}`;
240
240
  }
241
241
  }, "requireAdmin");
242
- cave.subcommand(".export", "导出回声洞数据").action(requireAdmin(() => this.exportData()));
243
- cave.subcommand(".import", "导入回声洞数据").action(requireAdmin(() => this.importData()));
242
+ cave.subcommand(".export", "导出回声洞数据").usage("将所有回声洞数据导出到 cave_export.json 中。").action(requireAdmin(() => this.exportData()));
243
+ cave.subcommand(".import", "导入回声洞数据").usage("从 cave_import.json 中导入回声洞数据。").action(requireAdmin(() => this.importData()));
244
244
  }
245
245
  /**
246
246
  * @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
@@ -399,20 +399,6 @@ async function getNextCaveId(ctx, reusableIds) {
399
399
  return newId;
400
400
  }
401
401
  __name(getNextCaveId, "getNextCaveId");
402
- function checkCooldown(session, config, lastUsed) {
403
- const adminChannelId = config.adminChannel?.split(":")[1];
404
- if (adminChannelId && session.channelId === adminChannelId) return null;
405
- if (config.coolDown <= 0 || !session.channelId) return null;
406
- const lastTime = lastUsed.get(session.channelId) || 0;
407
- const remainingTime = lastTime + config.coolDown * 1e3 - Date.now();
408
- if (remainingTime > 0) return `指令冷却中,请在 ${Math.ceil(remainingTime / 1e3)} 秒后重试`;
409
- return null;
410
- }
411
- __name(checkCooldown, "checkCooldown");
412
- function updateCooldownTimestamp(session, config, lastUsed) {
413
- if (config.coolDown > 0 && session.channelId) lastUsed.set(session.channelId, Date.now());
414
- }
415
- __name(updateCooldownTimestamp, "updateCooldownTimestamp");
416
402
  async function processMessageElements(sourceElements, newId, session, config, logger2) {
417
403
  const mediaToSave = [];
418
404
  let mediaIndex = 0;
@@ -553,7 +539,7 @@ var PendManager = class {
553
539
  if (session.channelId !== this.config.adminChannel?.split(":")[1]) return "此指令仅限在管理群组中使用";
554
540
  return null;
555
541
  }, "requireAdmin");
556
- const pend = cave.subcommand(".pend [id:posint]", "审核回声洞").action(async ({ session }, id) => {
542
+ const pend = cave.subcommand(".pend [id:posint]", "审核回声洞").usage("查询待审核的回声洞列表,或指定 ID 查看对应待审核的回声洞。").action(async ({ session }, id) => {
557
543
  const adminError = requireAdmin(session);
558
544
  if (adminError) return adminError;
559
545
  if (id) {
@@ -569,31 +555,34 @@ var PendManager = class {
569
555
  return `当前共有 ${pendingCaves.length} 条待审核回声洞,序号为:
570
556
  ${pendingCaves.map((c) => c.id).join("|")}`;
571
557
  });
572
- const createPendAction = /* @__PURE__ */ __name((actionType) => async ({ session }, id) => {
558
+ const createPendAction = /* @__PURE__ */ __name((actionType) => async ({ session }, ...ids) => {
573
559
  const adminError = requireAdmin(session);
574
560
  if (adminError) return adminError;
561
+ let idsToProcess = ids;
562
+ if (idsToProcess.length === 0) {
563
+ const pendingCaves = await this.ctx.database.get("cave", { status: "pending" }, { fields: ["id"] });
564
+ if (!pendingCaves.length) return "当前没有需要审核的回声洞";
565
+ idsToProcess = pendingCaves.map((c) => c.id);
566
+ }
575
567
  try {
576
568
  const targetStatus = actionType === "approve" ? "active" : "delete";
577
569
  const actionText = actionType === "approve" ? "通过" : "拒绝";
578
- if (!id) {
579
- const pendingCaves = await this.ctx.database.get("cave", { status: "pending" });
580
- if (!pendingCaves.length) return `当前没有需要${actionText}的回声洞`;
581
- await this.ctx.database.upsert("cave", pendingCaves.map((c) => ({ id: c.id, status: targetStatus })));
582
- if (targetStatus === "delete") cleanupPendingDeletions(this.ctx, this.fileManager, this.logger, this.reusableIds);
583
- return `已批量${actionText} ${pendingCaves.length} 条回声洞`;
584
- }
585
- const [cave2] = await this.ctx.database.get("cave", { id, status: "pending" });
586
- if (!cave2) return `回声洞(${id})无需审核`;
587
- await this.ctx.database.upsert("cave", [{ id, status: targetStatus }]);
570
+ const cavesToProcess = await this.ctx.database.get("cave", {
571
+ id: { $in: idsToProcess },
572
+ status: "pending"
573
+ });
574
+ if (cavesToProcess.length === 0) return `回声洞(${idsToProcess.join("|")})无需审核或不存在`;
575
+ const processedIds = cavesToProcess.map((cave2) => cave2.id);
576
+ await this.ctx.database.upsert("cave", processedIds.map((id) => ({ id, status: targetStatus })));
588
577
  if (targetStatus === "delete") cleanupPendingDeletions(this.ctx, this.fileManager, this.logger, this.reusableIds);
589
- return `回声洞(${id})已${actionText}`;
578
+ return `已${actionText}回声洞(${processedIds.join("|")})`;
590
579
  } catch (error) {
591
580
  this.logger.error(`审核操作失败:`, error);
592
581
  return `操作失败: ${error.message}`;
593
582
  }
594
583
  }, "createPendAction");
595
- pend.subcommand(".Y [id:posint]", "通过审核").action(createPendAction("approve"));
596
- pend.subcommand(".N [id:posint]", "拒绝审核").action(createPendAction("reject"));
584
+ pend.subcommand(".Y [...ids:posint]", "通过审核").usage("通过一个或多个指定 ID 的回声洞审核。若不指定 ID,则通过所有待审核的回声洞。").action(createPendAction("approve"));
585
+ pend.subcommand(".N [...ids:posint]", "拒绝审核").usage("拒绝一个或多个指定 ID 的回声洞审核。若不指定 ID,则拒绝所有待审核的回声洞。").action(createPendAction("reject"));
597
586
  }
598
587
  /**
599
588
  * @description 将新回声洞提交到管理群组以供审核。
@@ -980,7 +969,6 @@ var usage = `
980
969
  var logger = new import_koishi3.Logger("best-cave");
981
970
  var Config = import_koishi3.Schema.intersect([
982
971
  import_koishi3.Schema.object({
983
- coolDown: import_koishi3.Schema.number().default(10).description("冷却时间(秒)"),
984
972
  perChannel: import_koishi3.Schema.boolean().default(false).description("启用分群模式"),
985
973
  enableName: import_koishi3.Schema.boolean().default(false).description("启用自定义昵称"),
986
974
  enableIO: import_koishi3.Schema.boolean().default(false).description("启用导入导出"),
@@ -1015,7 +1003,6 @@ function apply(ctx, config) {
1015
1003
  time: "timestamp"
1016
1004
  }, { primary: "id" });
1017
1005
  const fileManager = new FileManager(ctx.baseDir, config, logger);
1018
- const lastUsed = /* @__PURE__ */ new Map();
1019
1006
  const reusableIds = /* @__PURE__ */ new Set();
1020
1007
  const profileManager = config.enableName ? new NameManager(ctx) : null;
1021
1008
  const reviewManager = config.enablePend ? new PendManager(ctx, config, fileManager, logger, reusableIds) : null;
@@ -1026,15 +1013,12 @@ function apply(ctx, config) {
1026
1013
  if (options.view) return session.execute(`cave.view ${options.view}`);
1027
1014
  if (options.delete) return session.execute(`cave.del ${options.delete}`);
1028
1015
  if (options.list) return session.execute("cave.list");
1029
- const cdMessage = checkCooldown(session, config, lastUsed);
1030
- if (cdMessage) return cdMessage;
1031
1016
  try {
1032
1017
  const query = getScopeQuery(session, config);
1033
1018
  const candidates = await ctx.database.get("cave", query, { fields: ["id"] });
1034
1019
  if (!candidates.length) return `当前${config.perChannel && session.channelId ? "本群" : ""}还没有任何回声洞`;
1035
1020
  const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
1036
1021
  const [randomCave] = await ctx.database.get("cave", { ...query, id: randomId });
1037
- updateCooldownTimestamp(session, config, lastUsed);
1038
1022
  const messages = await buildCaveMessage(randomCave, config, fileManager, logger, session.platform);
1039
1023
  for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1040
1024
  } catch (error) {
@@ -1100,12 +1084,9 @@ function apply(ctx, config) {
1100
1084
  });
1101
1085
  cave.subcommand(".view <id:posint>", "查看指定回声洞").action(async ({ session }, id) => {
1102
1086
  if (!id) return "请输入要查看的回声洞序号";
1103
- const cdMessage = checkCooldown(session, config, lastUsed);
1104
- if (cdMessage) return cdMessage;
1105
1087
  try {
1106
1088
  const [targetCave] = await ctx.database.get("cave", { ...getScopeQuery(session, config), id });
1107
1089
  if (!targetCave) return `回声洞(${id})不存在`;
1108
- updateCooldownTimestamp(session, config, lastUsed);
1109
1090
  const messages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform);
1110
1091
  for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
1111
1092
  } catch (error) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-best-cave",
3
3
  "description": "功能强大、高度可定制的回声洞。支持丰富的媒体类型、内容查重、人工审核、用户昵称、数据迁移以及本地/S3 双重文件存储后端。",
4
- "version": "2.4.2",
4
+ "version": "2.4.4",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],
package/readme.md CHANGED
@@ -40,8 +40,8 @@
40
40
  | `cave.name [昵称]` | `enableName: true` | 设置你在回声洞中显示的昵称。若不提供昵称,则清除现有设置。 |
41
41
  | `cave.pend` | `enablePend: true` | **(仅限管理群组)** 列出所有待审核的回声洞ID。 |
42
42
  | `cave.pend <序号>` | `enablePend: true` | **(仅限管理群组)** 查看指定待审核内容的详情。 |
43
- | `cave.pend.Y [序号]` | `enablePend: true` | **(仅限管理群组)** 通过审核。若不提供序号,则通过所有待审核内容。 |
44
- | `cave.pend.N [序号]` | `enablePend: true` | **(仅限管理群组)** 拒绝审核。若不提供序号,则拒绝所有待审核内容。 |
43
+ | `cave.pend.Y [...序号]` | `enablePend: true` | **(仅限管理群组)** 通过审核。若不提供序号,则通过所有待审核内容。 |
44
+ | `cave.pend.N [...序号]` | `enablePend: true` | **(仅限管理群组)** 拒绝审核。若不提供序号,则拒绝所有待审核内容。 |
45
45
  | `cave.export` | `enableIO: true` | **(仅限管理群组)** 将所有`active`状态的回声洞导出到 `cave_export.json`。 |
46
46
  | `cave.import` | `enableIO: true` | **(仅限管理群组)** 从 `cave_import.json` 文件中导入数据。 |
47
47
  | `cave.hash` | `enableSimilarity: true` | **(仅限管理群组)** 校验所有历史数据,为缺失哈希的回声洞补全记录。 |
@@ -53,7 +53,6 @@
53
53
 
54
54
  | 配置项 | 类型 | 默认值 | 说明 |
55
55
  | :--- | :--- | :--- | :--- |
56
- | `coolDown` | `number` | `10` | `cave` 和 `cave.view` 指令的冷却时间(秒)。在管理群组中无冷却。 |
57
56
  | `perChannel` | `boolean` | `false` | 是否启用分群模式。`true` 表示各群的回声洞独立。 |
58
57
  | `enableName` | `boolean` | `false` | 是否启用自定义昵称功能 (`cave.name` 指令)。 |
59
58
  | `enableIO` | `boolean` | `false` | 是否启用数据导入/导出功能 (`cave.export` / `.import` 指令)。 |