koishi-plugin-best-cave 2.0.6 → 2.0.7

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.
@@ -2,7 +2,7 @@ import { Context, Logger } from 'koishi';
2
2
  import { FileManager } from './FileManager';
3
3
  import { Config } from './index';
4
4
  /**
5
- * 数据管理器 (DataManager)
5
+ * @class DataManager
6
6
  * @description
7
7
  * 负责处理回声洞数据的导入和导出功能。
8
8
  */
@@ -12,27 +12,26 @@ export declare class DataManager {
12
12
  private fileManager;
13
13
  private logger;
14
14
  /**
15
- * 创建一个 DataManager 实例。
16
- * @param ctx - Koishi 上下文,用于数据库操作。
17
- * @param config - 插件配置。
18
- * @param fileManager - 文件管理器实例,用于读写导入/导出文件。
19
- * @param logger - 日志记录器实例。
15
+ * @constructor
16
+ * @param ctx Koishi 上下文,用于数据库操作。
17
+ * @param config 插件配置。
18
+ * @param fileManager 文件管理器实例。
19
+ * @param logger 日志记录器实例。
20
20
  */
21
21
  constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger);
22
22
  /**
23
- * 注册与数据导入导出相关的 `.export` 和 `.import` 子命令。
24
- * @param cave - 主 `cave` 命令的实例,用于挂载子命令。
23
+ * @description 注册 `.export` 和 `.import` 子命令。
24
+ * @param cave - 主 `cave` 命令实例。
25
25
  */
26
26
  registerCommands(cave: any): void;
27
27
  /**
28
- * 导出所有状态为 'active' 的回声洞数据。
29
- * 数据将被序列化为 JSON 并保存到 `cave_export.json` 文件中。
30
- * @returns 一个描述导出结果的字符串消息。
28
+ * @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
29
+ * @returns 描述导出结果的消息字符串。
31
30
  */
32
31
  exportData(): Promise<string>;
33
32
  /**
34
- * 从 `cave_import.json` 文件导入回声洞数据。
35
- * @returns 一个描述导入结果的字符串消息。
33
+ * @description 从 `cave_import.json` 文件导入回声洞数据。
34
+ * @returns 描述导入结果的消息字符串。
36
35
  */
37
36
  importData(): Promise<string>;
38
37
  }
@@ -1,11 +1,11 @@
1
1
  import { Logger } from 'koishi';
2
2
  import { Config } from './index';
3
3
  /**
4
- * 文件管理器 (FileManager)
4
+ * @class FileManager
5
5
  * @description
6
- * 封装了对文件(资源)的存储、读取和删除操作。
7
- * 它能够根据插件配置自动选择使用本地文件系统或 AWS S3 作为存储后端。
8
- * 内置了基于 Promise 的文件锁,以防止对本地文件的并发写入冲突。
6
+ * 封装了对文件的存储、读取和删除操作。
7
+ * 能根据配置自动选择使用本地文件系统或 AWS S3 作为存储后端。
8
+ * 内置 Promise 文件锁,防止本地文件的并发写入冲突。
9
9
  */
10
10
  export declare class FileManager {
11
11
  private logger;
@@ -14,51 +14,36 @@ export declare class FileManager {
14
14
  private s3Client?;
15
15
  private s3Bucket?;
16
16
  /**
17
- * 创建一个 FileManager 实例。
18
- * @param baseDir - Koishi 应用的基础数据目录 (ctx.baseDir)。
19
- * @param config - 插件的完整配置对象。
20
- * @param logger - 日志记录器实例。
17
+ * @constructor
18
+ * @param baseDir Koishi 应用的基础数据目录 (ctx.baseDir)。
19
+ * @param config 插件的配置对象。
20
+ * @param logger 日志记录器实例。
21
21
  */
22
22
  constructor(baseDir: string, config: Config, logger: Logger);
23
23
  /**
24
- * 确保本地资源目录存在。如果目录不存在,则会递归创建。
25
- * 这是一个幂等操作。
26
- * @private
27
- */
28
- private ensureDirectory;
29
- /**
30
- * 获取给定文件名的完整本地路径。
31
- * @param fileName - 文件名。
32
- * @returns 文件的绝对路径。
33
- * @private
34
- */
35
- private getFullPath;
36
- /**
37
- * 使用文件锁来安全地执行一个异步文件操作。
38
- * 这可以防止对同一文件的并发读写造成数据损坏。
39
- * @template T - 异步操作的返回类型。
40
- * @param fileName - 需要加锁的文件名。
41
- * @param operation - 要执行的异步函数。
24
+ * @description 使用文件锁安全地执行异步文件操作,防止并发读写冲突。
25
+ * @template T 异步操作的返回类型。
26
+ * @param fullPath 需要加锁的文件的完整路径。
27
+ * @param operation 要执行的异步函数。
42
28
  * @returns 返回异步操作的结果。
43
- * @private
44
29
  */
45
30
  private withLock;
46
31
  /**
47
- * 保存文件,自动选择 S3 或本地存储。
48
- * @param fileName - 文件名,将用作 S3 中的 Key 或本地文件名。
49
- * @param data - 要写入的 Buffer 数据。
32
+ * @description 保存文件,自动选择 S3 或本地存储。
33
+ * @param fileName 用作 S3 Key 或本地文件名。
34
+ * @param data 要写入的 Buffer 数据。
50
35
  * @returns 返回保存时使用的文件名/标识符。
51
36
  */
52
37
  saveFile(fileName: string, data: Buffer): Promise<string>;
53
38
  /**
54
- * 读取文件,自动从 S3 或本地存储读取。
55
- * @param fileName - 要读取的文件名/标识符。
39
+ * @description 读取文件,自动从 S3 或本地存储读取。
40
+ * @param fileName 要读取的文件名/标识符。
56
41
  * @returns 文件的 Buffer 数据。
57
42
  */
58
43
  readFile(fileName: string): Promise<Buffer>;
59
44
  /**
60
- * 删除文件,自动从 S3 或本地删除。
61
- * @param fileIdentifier - 要删除的文件名/标识符。
45
+ * @description 删除文件,自动从 S3 或本地删除。
46
+ * @param fileIdentifier 要删除的文件名/标识符。
62
47
  */
63
48
  deleteFile(fileIdentifier: string): Promise<void>;
64
49
  }
@@ -1,8 +1,8 @@
1
1
  import { Context } from 'koishi';
2
2
  /**
3
- * 数据库中 `cave_user` 表的记录结构。
4
- * @property userId - 用户的唯一 ID,作为主键。
5
- * @property nickname - 用户设置的自定义昵称。
3
+ * @description 数据库 `cave_user` 表的结构定义。
4
+ * @property userId 用户唯一ID,作为主键。
5
+ * @property nickname 用户自定义的昵称。
6
6
  */
7
7
  export interface UserProfile {
8
8
  userId: string;
@@ -14,38 +14,37 @@ declare module 'koishi' {
14
14
  }
15
15
  }
16
16
  /**
17
- * 个人资料管理器 (ProfileManager)
17
+ * @class ProfileManager
18
18
  * @description
19
- * 负责管理用户在回声洞插件中的自定义昵称。
20
- * 提供设置、获取和清除昵称的数据库操作和相关命令。
21
- * 此类仅在插件配置中启用了 `enableProfile` 时才会被实例化。
19
+ * 负责管理用户在回声洞中的自定义昵称。
20
+ * 当插件配置 `enableProfile` 为 true 时实例化。
22
21
  */
23
22
  export declare class ProfileManager {
24
23
  private ctx;
25
24
  /**
26
- * 创建一个 ProfileManager 实例。
25
+ * @constructor
27
26
  * @param ctx - Koishi 上下文,用于初始化数据库模型。
28
27
  */
29
28
  constructor(ctx: Context);
30
29
  /**
31
- * 注册与用户昵称相关的 `.profile` 子命令。
32
- * @param cave - 主 `cave` 命令的实例,用于挂载子命令。
30
+ * @description 注册 `.profile` 子命令,用于管理用户昵称。
31
+ * @param cave - 主 `cave` 命令实例。
33
32
  */
34
33
  registerCommands(cave: any): void;
35
34
  /**
36
- * 设置或更新指定用户的昵称。
35
+ * @description 设置或更新指定用户的昵称。
37
36
  * @param userId - 目标用户的 ID。
38
37
  * @param nickname - 要设置的新昵称。
39
38
  */
40
39
  setNickname(userId: string, nickname: string): Promise<void>;
41
40
  /**
42
- * 获取指定用户的昵称。
41
+ * @description 获取指定用户的昵称。
43
42
  * @param userId - 目标用户的 ID。
44
- * @returns 返回用户的昵称字符串。如果用户未设置昵称,则返回 null。
43
+ * @returns 返回用户的昵称字符串,如果未设置则返回 null。
45
44
  */
46
45
  getNickname(userId: string): Promise<string | null>;
47
46
  /**
48
- * 清除指定用户的昵称设置。
47
+ * @description 清除指定用户的昵称设置。
49
48
  * @param userId - 目标用户的 ID。
50
49
  */
51
50
  clearNickname(userId: string): Promise<void>;
@@ -2,11 +2,10 @@ import { Context, h, Logger } from 'koishi';
2
2
  import { CaveObject, Config } from './index';
3
3
  import { FileManager } from './FileManager';
4
4
  /**
5
- * 审核管理器 (ReviewManager)
5
+ * @class ReviewManager
6
6
  * @description
7
- * 负责处理回声洞的审核流程。当 `enableReview` 配置项开启时,
8
- * 此管理器将被激活,用于处理新回声洞的提交、向管理员发送审核通知
9
- * 以及处理管理员的审核操作(通过/拒绝)。
7
+ * 负责处理回声洞的审核流程。当 `enableReview` 配置开启时,
8
+ * 此管理器将被激活,处理新洞的提交、审核通知和审核操作。
10
9
  */
11
10
  export declare class ReviewManager {
12
11
  private ctx;
@@ -14,35 +13,28 @@ export declare class ReviewManager {
14
13
  private fileManager;
15
14
  private logger;
16
15
  /**
17
- * 创建一个 ReviewManager 实例。
18
- * @param ctx - Koishi 上下文。
19
- * @param config - 插件配置。
20
- * @param fileManager - 文件管理器实例。
21
- * @param logger - 日志记录器实例。
16
+ * @constructor
17
+ * @param ctx Koishi 上下文。
18
+ * @param config 插件配置。
19
+ * @param fileManager 文件管理器实例。
20
+ * @param logger 日志记录器实例。
22
21
  */
23
22
  constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger);
24
23
  /**
25
- * 注册与审核相关的 `.review` 子命令。
26
- * @param cave - 主 `cave` 命令的实例,用于挂载子命令。
24
+ * @description 注册与审核相关的 `.review` 子命令。
25
+ * @param cave - 主 `cave` 命令实例。
27
26
  */
28
27
  registerCommands(cave: any): void;
29
28
  /**
30
- * 将一条新的回声洞提交给所有管理员进行审核。
31
- * @param cave - 新创建的、状态为 'pending' 的回声洞对象。
29
+ * @description 将新回声洞提交给管理员审核。
30
+ * @param cave 新创建的、状态为 'pending' 的回声洞对象。
32
31
  */
33
32
  sendForReview(cave: CaveObject): Promise<void>;
34
33
  /**
35
- * 构建一条用于发送给管理员的、包含审核信息的消息。
36
- * @param cave - 待审核的回声洞对象。
37
- * @returns 一个可直接发送的消息数组。
38
- * @private
39
- */
40
- private buildReviewMessage;
41
- /**
42
- * 处理管理员的审核决定(通过或拒绝)。
43
- * @param action - 'approve' (通过) 或 'reject' (拒绝)。
44
- * @param caveId - 被审核的回声洞 ID。
45
- * @param adminUserName - 执行操作的管理员的昵称。
34
+ * @description 处理管理员的审核决定(通过或拒绝)。
35
+ * @param action 'approve' (通过) 或 'reject' (拒绝)。
36
+ * @param caveId 被审核的回声洞 ID。
37
+ * @param adminUserName 操作管理员的昵称。
46
38
  * @returns 返回给操作者的确认消息。
47
39
  */
48
40
  processReview(action: 'approve' | 'reject', caveId: number, adminUserName: string): Promise<string | (string | h)[]>;
package/lib/Utils.d.ts CHANGED
@@ -2,78 +2,54 @@ import { Context, h, Logger, Session } from 'koishi';
2
2
  import { CaveObject, Config, StoredElement } from './index';
3
3
  import { FileManager } from './FileManager';
4
4
  /**
5
- * 将数据库中存储的 StoredElement[] 数组转换为 Koishi h() 元素数组。
6
- * @param elements - 从数据库读取的元素对象数组。
7
- * @returns 转换后的 h() 元素数组,用于消息发送。
5
+ * @description 将数据库存储的 StoredElement[] 转换为 Koishi h() 元素数组。
6
+ * @param elements 从数据库读取的元素数组。
7
+ * @returns 转换后的 h() 元素数组。
8
8
  */
9
9
  export declare function storedFormatToHElements(elements: StoredElement[]): h[];
10
10
  /**
11
- * 将指向本地媒体文件的 h() 元素转换为内联 Base64 格式。
12
- * @param element - 包含本地文件路径的 h() 媒体元素。
13
- * @param fileManager - FileManager 实例,用于读取文件。
14
- * @param logger - Logger 实例,用于记录错误。
15
- * @returns 转换后的 h() 元素,其 src 属性为 Base64 数据 URI。
16
- */
17
- export declare function mediaElementToBase64(element: h, fileManager: FileManager, logger: Logger): Promise<h>;
18
- /**
19
- * 构建一条包含回声洞内容的完整消息,准备发送。
11
+ * @description 构建一条用于发送的完整回声洞消息。
20
12
  * 此函数会处理 S3 URL、文件映射路径或本地文件到 Base64 的转换。
21
- * @param cave - 要展示的回声洞对象。
22
- * @param config - 插件配置。
23
- * @param fileManager - FileManager 实例。
24
- * @param logger - Logger 实例。
25
- * @returns 一个包含 h() 元素和字符串的消息数组。
13
+ * @param cave 要展示的回声洞对象。
14
+ * @param config 插件配置。
15
+ * @param fileManager FileManager 实例。
16
+ * @param logger Logger 实例。
17
+ * @returns 包含 h() 元素和字符串的消息数组。
26
18
  */
27
19
  export declare function buildCaveMessage(cave: CaveObject, config: Config, fileManager: FileManager, logger: Logger): Promise<(string | h)[]>;
28
20
  /**
29
- * 清理数据库中所有被标记为 'delete' 状态的回声洞及其关联的文件。
30
- * @param ctx - Koishi 上下文。
31
- * @param fileManager - FileManager 实例,用于删除文件。
32
- * @param logger - Logger 实例。
21
+ * @description 清理数据库中所有被标记为 'delete' 状态的回声洞及其关联文件。
22
+ * @param ctx Koishi 上下文。
23
+ * @param fileManager FileManager 实例。
24
+ * @param logger Logger 实例。
33
25
  */
34
26
  export declare function cleanupPendingDeletions(ctx: Context, fileManager: FileManager, logger: Logger): Promise<void>;
35
27
  /**
36
- * 根据插件配置(是否分群)和当前会话,生成数据库查询所需的范围条件。
37
- * @param session - 当前会话对象。
38
- * @param config - 插件配置。
39
- * @returns 一个用于数据库查询的条件对象。
28
+ * @description 根据配置(是否分群)和当前会话,生成数据库查询的范围条件。
29
+ * @param session 当前会话对象。
30
+ * @param config 插件配置。
31
+ * @returns 用于数据库查询的条件对象。
40
32
  */
41
33
  export declare function getScopeQuery(session: Session, config: Config): object;
42
34
  /**
43
- * 获取下一个可用的回声洞 ID
44
- * 策略是找到当前已存在的 ID 中最小的未使用正整数。
45
- * @param ctx - Koishi 上下文。
46
- * @param query - 查询回声洞的范围条件,用于分群模式。
47
- * @returns 一个可用的新 ID。
48
- * @performance 对于非常大的数据集,此函数可能会有性能瓶颈,因为它需要获取所有现有 ID。
35
+ * @description 获取下一个可用的回声洞 ID(最小的未使用正整数)。
36
+ * @param ctx Koishi 上下文。
37
+ * @param query 查询范围条件,用于分群模式。
38
+ * @returns 可用的新 ID。
39
+ * @performance 在大数据集下,此函数可能存在性能瓶颈,因为它需要获取所有现有ID。
49
40
  */
50
41
  export declare function getNextCaveId(ctx: Context, query?: object): Promise<number>;
51
42
  /**
52
- * 下载网络媒体资源并保存到文件存储中(本地或 S3)。
53
- * @param ctx - Koishi 上下文。
54
- * @param fileManager - FileManager 实例。
55
- * @param url - 媒体资源的 URL。
56
- * @param originalName - 原始文件名,用于获取扩展名。
57
- * @param type - 媒体类型 ('image', 'video', 'audio', 'file')。
58
- * @param caveId - 新建回声洞的 ID。
59
- * @param index - 媒体在消息中的索引。
60
- * @param channelId - 频道 ID。
61
- * @param userId - 用户 ID。
43
+ * @description 下载网络媒体资源并保存到文件存储中。
62
44
  * @returns 保存后的文件名/标识符。
63
45
  */
64
46
  export declare function downloadMedia(ctx: Context, fileManager: FileManager, url: string, originalName: string, type: string, caveId: number, index: number, channelId: string, userId: string): Promise<string>;
65
47
  /**
66
- * 检查用户在当前频道是否处于指令冷却状态。
67
- * @param session - 当前会话对象。
68
- * @param config - 插件配置。
69
- * @param lastUsed - 存储各频道最后使用时间的 Map。
70
- * @returns 如果处于冷却中,返回提示信息字符串;否则返回 null。
48
+ * @description 检查用户是否处于指令冷却中。
49
+ * @returns 若在冷却中则返回提示字符串,否则返回 null。
71
50
  */
72
51
  export declare function checkCooldown(session: Session, config: Config, lastUsed: Map<string, number>): string | null;
73
52
  /**
74
- * 更新指定频道的指令使用时间戳。
75
- * @param session - 当前会话对象。
76
- * @param config - 插件配置。
77
- * @param lastUsed - 存储各频道最后使用时间的 Map。
53
+ * @description 更新指定频道的指令使用时间戳。
78
54
  */
79
55
  export declare function updateCooldownTimestamp(session: Session, config: Config, lastUsed: Map<string, number>): void;
package/lib/index.d.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  import { Context, Schema } from 'koishi';
2
2
  export declare const name = "best-cave";
3
3
  export declare const inject: string[];
4
- export declare const usage = "\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83D\uDCCC \u63D2\u4EF6\u8BF4\u660E</h2>\n <p>\uD83D\uDCD6 <strong>\u4F7F\u7528\u6587\u6863</strong>\uFF1A\u8BF7\u70B9\u51FB\u5DE6\u4E0A\u89D2\u7684 <strong>\u63D2\u4EF6\u4E3B\u9875</strong> \u67E5\u770B\u63D2\u4EF6\u4F7F\u7528\u6587\u6863</p>\n <p>\uD83D\uDD0D <strong>\u66F4\u591A\u63D2\u4EF6</strong>\uFF1A\u53EF\u8BBF\u95EE <a href=\"https://github.com/YisRime\" style=\"color:#4a6ee0;text-decoration:none;\">\u82E1\u6DDE\u7684 GitHub</a> \u67E5\u770B\u672C\u4EBA\u7684\u6240\u6709\u63D2\u4EF6</p>\n</div>\n\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #e0574a;\">\u2764\uFE0F \u652F\u6301\u4E0E\u53CD\u9988</h2>\n <p>\uD83C\uDF1F \u559C\u6B22\u8FD9\u4E2A\u63D2\u4EF6\uFF1F\u8BF7\u5728 <a href=\"https://github.com/YisRime\" style=\"color:#e0574a;text-decoration:none;\">GitHub</a> \u4E0A\u7ED9\u6211\u4E00\u4E2A Star\uFF01</p>\n <p>\uD83D\uDC1B \u9047\u5230\u95EE\u9898\uFF1F\u8BF7\u901A\u8FC7 <strong>Issues</strong> \u63D0\u4EA4\u53CD\u9988\uFF0C\u6216\u52A0\u5165 QQ \u7FA4 <a href=\"https://qm.qq.com/q/PdLMx9Jowq\" style=\"color:#e0574a;text-decoration:none;\"><strong>855571375</strong></a> \u8FDB\u884C\u4EA4\u6D41</p>\n</div>\n";
4
+ export declare const usage = "\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83D\uDCCC \u63D2\u4EF6\u8BF4\u660E</h2>\n <p>\uD83D\uDCD6 <strong>\u4F7F\u7528\u6587\u6863</strong>\uFF1A\u8BF7\u70B9\u51FB\u5DE6\u4E0A\u89D2\u7684 <strong>\u63D2\u4EF6\u4E3B\u9875</strong> \u67E5\u770B\u63D2\u4EF6\u4F7F\u7528\u6587\u6863</p>\n <p>\uD83D\uDD0D <strong>\u66F4\u591A\u63D2\u4EF6</strong>\uFF1A\u53EF\u8BBF\u95EE <a href=\"https://github.com/YisRime\" style=\"color:#4a6ee0;text-decoration:none;\">\u82E1\u6DDE\u7684 GitHub</a> \u67E5\u770B\u672C\u4EBA\u7684\u6240\u6709\u63D2\u4EF6</p>\n</div>\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #e0574a;\">\u2764\uFE0F \u652F\u6301\u4E0E\u53CD\u9988</h2>\n <p>\uD83C\uDF1F \u559C\u6B22\u8FD9\u4E2A\u63D2\u4EF6\uFF1F\u8BF7\u5728 <a href=\"https://github.com/YisRime\" style=\"color:#e0574a;text-decoration:none;\">GitHub</a> \u4E0A\u7ED9\u6211\u4E00\u4E2A Star\uFF01</p>\n <p>\uD83D\uDC1B \u9047\u5230\u95EE\u9898\uFF1F\u8BF7\u901A\u8FC7 <strong>Issues</strong> \u63D0\u4EA4\u53CD\u9988\uFF0C\u6216\u52A0\u5165 QQ \u7FA4 <a href=\"https://qm.qq.com/q/PdLMx9Jowq\" style=\"color:#e0574a;text-decoration:none;\"><strong>855571375</strong></a> \u8FDB\u884C\u4EA4\u6D41</p>\n</div>\n";
5
5
  /**
6
- * 存储在数据库中的单个消息元素。
7
- * @property type - 元素类型。
8
- * @property content - 文本内容,仅用于 'text' 类型。
9
- * @property file - 文件标识符(本地文件名或 S3 Key),用于媒体类型。
6
+ * @description 存储在数据库中的单个消息元素。
10
7
  */
11
8
  export interface StoredElement {
12
9
  type: 'text' | 'image' | 'video' | 'audio' | 'file';
@@ -14,14 +11,7 @@ export interface StoredElement {
14
11
  file?: string;
15
12
  }
16
13
  /**
17
- * 数据库中 `cave` 表的完整对象模型。
18
- * @property id - 回声洞的唯一数字 ID。
19
- * @property elements - 构成回声洞内容的元素数组。
20
- * @property channelId - 提交回声洞的频道 ID,若为私聊则为 null。
21
- * @property userId - 提交用户的 ID。
22
- * @property userName - 提交用户的昵称。
23
- * @property status - 回声洞状态: 'active' (活跃), 'delete' (待删除), 'pending' (待审核)。
24
- * @property time - 提交时间。
14
+ * @description 数据库 `cave` 表的完整对象模型。
25
15
  */
26
16
  export interface CaveObject {
27
17
  id: number;
@@ -37,9 +27,6 @@ declare module 'koishi' {
37
27
  cave: CaveObject;
38
28
  }
39
29
  }
40
- /**
41
- * 插件的配置接口。
42
- */
43
30
  export interface Config {
44
31
  coolDown: number;
45
32
  perChannel: boolean;
@@ -57,13 +44,5 @@ export interface Config {
57
44
  bucket?: string;
58
45
  publicUrl?: string;
59
46
  }
60
- /**
61
- * 使用 Koishi Schema 定义插件的配置项,用于生成配置界面。
62
- */
63
47
  export declare const Config: Schema<Config>;
64
- /**
65
- * 插件的入口函数。
66
- * @param ctx - Koishi 上下文。
67
- * @param config - 用户提供的插件配置。
68
- */
69
48
  export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  });
39
39
  module.exports = __toCommonJS(index_exports);
40
40
  var import_koishi2 = require("koishi");
41
+ var path3 = __toESM(require("path"));
41
42
 
42
43
  // src/FileManager.ts
43
44
  var import_client_s3 = require("@aws-sdk/client-s3");
@@ -45,10 +46,10 @@ var fs = __toESM(require("fs/promises"));
45
46
  var path = __toESM(require("path"));
46
47
  var FileManager = class {
47
48
  /**
48
- * 创建一个 FileManager 实例。
49
- * @param baseDir - Koishi 应用的基础数据目录 (ctx.baseDir)。
50
- * @param config - 插件的完整配置对象。
51
- * @param logger - 日志记录器实例。
49
+ * @constructor
50
+ * @param baseDir Koishi 应用的基础数据目录 (ctx.baseDir)。
51
+ * @param config 插件的配置对象。
52
+ * @param logger 日志记录器实例。
52
53
  */
53
54
  constructor(baseDir, config, logger2) {
54
55
  this.logger = logger2;
@@ -68,47 +69,18 @@ var FileManager = class {
68
69
  static {
69
70
  __name(this, "FileManager");
70
71
  }
71
- // 本地资源存储目录的绝对路径。
72
72
  resourceDir;
73
- // 本地文件锁,键为文件绝对路径,值为一个 Promise,用于防止对同一文件的并发访问。
74
73
  locks = /* @__PURE__ */ new Map();
75
- // S3 客户端实例,仅在启用 S3 时初始化。
76
74
  s3Client;
77
- // S3 存储桶名称。
78
75
  s3Bucket;
79
76
  /**
80
- * 确保本地资源目录存在。如果目录不存在,则会递归创建。
81
- * 这是一个幂等操作。
82
- * @private
83
- */
84
- async ensureDirectory() {
85
- try {
86
- await fs.mkdir(this.resourceDir, { recursive: true });
87
- } catch (error) {
88
- this.logger.error(`创建资源目录失败 ${this.resourceDir}:`, error);
89
- throw error;
90
- }
91
- }
92
- /**
93
- * 获取给定文件名的完整本地路径。
94
- * @param fileName - 文件名。
95
- * @returns 文件的绝对路径。
96
- * @private
97
- */
98
- getFullPath(fileName) {
99
- return path.join(this.resourceDir, fileName);
100
- }
101
- /**
102
- * 使用文件锁来安全地执行一个异步文件操作。
103
- * 这可以防止对同一文件的并发读写造成数据损坏。
104
- * @template T - 异步操作的返回类型。
105
- * @param fileName - 需要加锁的文件名。
106
- * @param operation - 要执行的异步函数。
77
+ * @description 使用文件锁安全地执行异步文件操作,防止并发读写冲突。
78
+ * @template T 异步操作的返回类型。
79
+ * @param fullPath 需要加锁的文件的完整路径。
80
+ * @param operation 要执行的异步函数。
107
81
  * @returns 返回异步操作的结果。
108
- * @private
109
82
  */
110
- async withLock(fileName, operation) {
111
- const fullPath = this.getFullPath(fileName);
83
+ async withLock(fullPath, operation) {
112
84
  while (this.locks.has(fullPath)) {
113
85
  await this.locks.get(fullPath);
114
86
  }
@@ -119,9 +91,9 @@ var FileManager = class {
119
91
  return promise;
120
92
  }
121
93
  /**
122
- * 保存文件,自动选择 S3 或本地存储。
123
- * @param fileName - 文件名,将用作 S3 中的 Key 或本地文件名。
124
- * @param data - 要写入的 Buffer 数据。
94
+ * @description 保存文件,自动选择 S3 或本地存储。
95
+ * @param fileName 用作 S3 Key 或本地文件名。
96
+ * @param data 要写入的 Buffer 数据。
125
97
  * @returns 返回保存时使用的文件名/标识符。
126
98
  */
127
99
  async saveFile(fileName, data) {
@@ -131,57 +103,52 @@ var FileManager = class {
131
103
  Key: fileName,
132
104
  Body: data,
133
105
  ACL: "public-read"
134
- // 默认将文件权限设置为公开可读,方便通过 URL 访问。
106
+ // 默认为公开可读
135
107
  });
136
108
  await this.s3Client.send(command);
137
- return fileName;
138
109
  } else {
139
- await this.ensureDirectory();
140
- const filePath = this.getFullPath(fileName);
141
- await this.withLock(fileName, () => fs.writeFile(filePath, data));
142
- return fileName;
110
+ await fs.mkdir(this.resourceDir, { recursive: true }).catch((error) => {
111
+ this.logger.error(`创建资源目录失败 ${this.resourceDir}:`, error);
112
+ throw error;
113
+ });
114
+ const filePath = path.join(this.resourceDir, fileName);
115
+ await this.withLock(filePath, () => fs.writeFile(filePath, data));
143
116
  }
117
+ return fileName;
144
118
  }
145
119
  /**
146
- * 读取文件,自动从 S3 或本地存储读取。
147
- * @param fileName - 要读取的文件名/标识符。
120
+ * @description 读取文件,自动从 S3 或本地存储读取。
121
+ * @param fileName 要读取的文件名/标识符。
148
122
  * @returns 文件的 Buffer 数据。
149
123
  */
150
124
  async readFile(fileName) {
151
125
  if (this.s3Client) {
152
- const command = new import_client_s3.GetObjectCommand({
153
- Bucket: this.s3Bucket,
154
- Key: fileName
155
- });
126
+ const command = new import_client_s3.GetObjectCommand({ Bucket: this.s3Bucket, Key: fileName });
156
127
  const response = await this.s3Client.send(command);
157
- const byteArray = await response.Body.transformToByteArray();
158
- return Buffer.from(byteArray);
128
+ return Buffer.from(await response.Body.transformToByteArray());
159
129
  } else {
160
- const filePath = this.getFullPath(fileName);
161
- return this.withLock(fileName, () => fs.readFile(filePath));
130
+ const filePath = path.join(this.resourceDir, fileName);
131
+ return this.withLock(filePath, () => fs.readFile(filePath));
162
132
  }
163
133
  }
164
134
  /**
165
- * 删除文件,自动从 S3 或本地删除。
166
- * @param fileIdentifier - 要删除的文件名/标识符。
135
+ * @description 删除文件,自动从 S3 或本地删除。
136
+ * @param fileIdentifier 要删除的文件名/标识符。
167
137
  */
168
138
  async deleteFile(fileIdentifier) {
169
139
  if (this.s3Client) {
170
- const command = new import_client_s3.DeleteObjectCommand({
171
- Bucket: this.s3Bucket,
172
- Key: fileIdentifier
173
- });
140
+ const command = new import_client_s3.DeleteObjectCommand({ Bucket: this.s3Bucket, Key: fileIdentifier });
174
141
  await this.s3Client.send(command).catch((err) => {
175
- this.logger.warn(`删除文件 ${fileIdentifier} 失败:`, err);
142
+ this.logger.warn(`删除 S3 文件 ${fileIdentifier} 失败:`, err);
176
143
  });
177
144
  } else {
178
- const filePath = this.getFullPath(fileIdentifier);
179
- await this.withLock(fileIdentifier, async () => {
145
+ const filePath = path.join(this.resourceDir, fileIdentifier);
146
+ await this.withLock(filePath, async () => {
180
147
  try {
181
148
  await fs.unlink(filePath);
182
149
  } catch (error) {
183
150
  if (error.code !== "ENOENT") {
184
- this.logger.warn(`删除文件 ${filePath} 失败:`, error);
151
+ this.logger.warn(`删除本地文件 ${filePath} 失败:`, error);
185
152
  }
186
153
  }
187
154
  });
@@ -192,7 +159,7 @@ var FileManager = class {
192
159
  // src/ProfileManager.ts
193
160
  var ProfileManager = class {
194
161
  /**
195
- * 创建一个 ProfileManager 实例。
162
+ * @constructor
196
163
  * @param ctx - Koishi 上下文,用于初始化数据库模型。
197
164
  */
198
165
  constructor(ctx) {
@@ -204,49 +171,47 @@ var ProfileManager = class {
204
171
  // 用户自定义昵称
205
172
  }, {
206
173
  primary: "userId"
207
- // 使用 userId 作为主键,确保每个用户只有一条昵称记录。
174
+ // 保证每个用户只有一条昵称记录。
208
175
  });
209
176
  }
210
177
  static {
211
178
  __name(this, "ProfileManager");
212
179
  }
213
180
  /**
214
- * 注册与用户昵称相关的 `.profile` 子命令。
215
- * @param cave - 主 `cave` 命令的实例,用于挂载子命令。
181
+ * @description 注册 `.profile` 子命令,用于管理用户昵称。
182
+ * @param cave - 主 `cave` 命令实例。
216
183
  */
217
184
  registerCommands(cave) {
218
- cave.subcommand(".profile [nickname:text]", "设置显示昵称").usage("设置你在回声洞中显示的昵称。不提供昵称则清除记录。").action(async ({ session }, nickname) => {
185
+ cave.subcommand(".profile [nickname:text]", "设置显示昵称").usage("设置你在回声洞中显示的昵称。若不提供昵称,则清除现有昵称。").action(async ({ session }, nickname) => {
219
186
  const trimmedNickname = nickname?.trim();
220
- if (!trimmedNickname) {
187
+ if (trimmedNickname) {
188
+ await this.setNickname(session.userId, trimmedNickname);
189
+ return `昵称已更新为:${trimmedNickname}`;
190
+ } else {
221
191
  await this.clearNickname(session.userId);
222
192
  return "昵称已清除";
223
193
  }
224
- await this.setNickname(session.userId, trimmedNickname);
225
- return `昵称已更新为:${trimmedNickname}`;
226
194
  });
227
195
  }
228
196
  /**
229
- * 设置或更新指定用户的昵称。
197
+ * @description 设置或更新指定用户的昵称。
230
198
  * @param userId - 目标用户的 ID。
231
199
  * @param nickname - 要设置的新昵称。
232
200
  */
233
201
  async setNickname(userId, nickname) {
234
- await this.ctx.database.upsert("cave_user", [{
235
- userId,
236
- nickname
237
- }]);
202
+ await this.ctx.database.upsert("cave_user", [{ userId, nickname }]);
238
203
  }
239
204
  /**
240
- * 获取指定用户的昵称。
205
+ * @description 获取指定用户的昵称。
241
206
  * @param userId - 目标用户的 ID。
242
- * @returns 返回用户的昵称字符串。如果用户未设置昵称,则返回 null。
207
+ * @returns 返回用户的昵称字符串,如果未设置则返回 null。
243
208
  */
244
209
  async getNickname(userId) {
245
- const profiles = await this.ctx.database.get("cave_user", { userId });
246
- return profiles[0]?.nickname || null;
210
+ const profile = await this.ctx.database.get("cave_user", { userId });
211
+ return profile[0]?.nickname ?? null;
247
212
  }
248
213
  /**
249
- * 清除指定用户的昵称设置。
214
+ * @description 清除指定用户的昵称设置。
250
215
  * @param userId - 目标用户的 ID。
251
216
  */
252
217
  async clearNickname(userId) {
@@ -257,78 +222,46 @@ var ProfileManager = class {
257
222
  // src/Utils.ts
258
223
  var import_koishi = require("koishi");
259
224
  var path2 = __toESM(require("path"));
260
- var mimeTypeMap = {
261
- ".png": "image/png",
262
- ".jpg": "image/jpeg",
263
- ".jpeg": "image/jpeg",
264
- ".gif": "image/gif",
265
- ".mp4": "video/mp4",
266
- ".mp3": "audio/mpeg",
267
- ".webp": "image/webp"
268
- };
225
+ var mimeTypeMap = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".webp": "image/webp" };
269
226
  function storedFormatToHElements(elements) {
270
227
  return elements.map((el) => {
271
- switch (el.type) {
272
- case "text":
273
- return import_koishi.h.text(el.content);
274
- case "image":
275
- case "video":
276
- case "audio":
277
- case "file":
278
- return (0, import_koishi.h)(el.type, { src: el.file });
279
- default:
280
- return null;
281
- }
228
+ if (el.type === "text") return import_koishi.h.text(el.content);
229
+ if (["image", "video", "audio", "file"].includes(el.type)) return (0, import_koishi.h)(el.type, { src: el.file });
230
+ return null;
282
231
  }).filter(Boolean);
283
232
  }
284
233
  __name(storedFormatToHElements, "storedFormatToHElements");
285
- async function mediaElementToBase64(element, fileManager, logger2) {
286
- const fileName = element.attrs.src;
287
- try {
288
- const data = await fileManager.readFile(fileName);
289
- const ext = path2.extname(fileName).toLowerCase();
290
- const mimeType = mimeTypeMap[ext] || "application/octet-stream";
291
- return (0, import_koishi.h)(element.type, { ...element.attrs, src: `data:${mimeType};base64,${data.toString("base64")}` });
292
- } catch (error) {
293
- logger2.warn(`转换本地文件 ${fileName} 为 Base64 失败:`, error);
294
- return (0, import_koishi.h)("p", {}, `[${element.type}]`);
295
- }
296
- }
297
- __name(mediaElementToBase64, "mediaElementToBase64");
298
234
  async function buildCaveMessage(cave, config, fileManager, logger2) {
299
235
  const caveHElements = storedFormatToHElements(cave.elements);
300
- const processedElements = await Promise.all(caveHElements.map((element) => {
236
+ const processedElements = await Promise.all(caveHElements.map(async (element) => {
301
237
  const isMedia = ["image", "video", "audio", "file"].includes(element.type);
302
238
  const fileName = element.attrs.src;
303
- if (!isMedia || !fileName) {
304
- return Promise.resolve(element);
305
- }
239
+ if (!isMedia || !fileName) return element;
306
240
  if (config.enableS3 && config.publicUrl) {
307
241
  const fullUrl = config.publicUrl.endsWith("/") ? `${config.publicUrl}${fileName}` : `${config.publicUrl}/${fileName}`;
308
- return Promise.resolve((0, import_koishi.h)(element.type, { ...element.attrs, src: fullUrl }));
242
+ return (0, import_koishi.h)(element.type, { ...element.attrs, src: fullUrl });
309
243
  }
310
244
  if (config.localPath) {
311
- const mappedPath = path2.join(config.localPath, fileName);
312
- const fileUri = `file://${mappedPath}`;
313
- return Promise.resolve((0, import_koishi.h)(element.type, { ...element.attrs, src: fileUri }));
245
+ const fileUri = `file://${path2.join(config.localPath, fileName)}`;
246
+ return (0, import_koishi.h)(element.type, { ...element.attrs, src: fileUri });
247
+ }
248
+ try {
249
+ const data = await fileManager.readFile(fileName);
250
+ const ext = path2.extname(fileName).toLowerCase();
251
+ const mimeType = mimeTypeMap[ext] || "application/octet-stream";
252
+ return (0, import_koishi.h)(element.type, { ...element.attrs, src: `data:${mimeType};base64,${data.toString("base64")}` });
253
+ } catch (error) {
254
+ logger2.warn(`转换文件 ${fileName} 为 Base64 失败:`, error);
255
+ return (0, import_koishi.h)("p", {}, `[${element.type}]`);
314
256
  }
315
- return mediaElementToBase64(element, fileManager, logger2);
316
257
  }));
317
258
  const finalMessage = [];
318
- const formatString = config.caveFormat;
319
- const separatorIndex = formatString.indexOf("|");
320
- let headerFormat, footerFormat;
321
- if (separatorIndex === -1) {
322
- headerFormat = formatString;
323
- footerFormat = "";
324
- } else {
325
- headerFormat = formatString.substring(0, separatorIndex);
326
- footerFormat = formatString.substring(separatorIndex + 1);
327
- }
328
- const headerText = headerFormat.replace("{id}", cave.id.toString()).replace("{name}", cave.userName);
259
+ const [headerFormat, footerFormat = ""] = config.caveFormat.split("|");
260
+ const replacements = { id: cave.id.toString(), name: cave.userName };
261
+ const headerText = headerFormat.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]);
329
262
  if (headerText.trim()) finalMessage.push(headerText);
330
263
  finalMessage.push(...processedElements);
331
- const footerText = footerFormat.replace("{id}", cave.id.toString()).replace("{name}", cave.userName);
264
+ const footerText = footerFormat.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]);
332
265
  if (footerText.trim()) finalMessage.push(footerText);
333
266
  return finalMessage;
334
267
  }
@@ -336,7 +269,7 @@ __name(buildCaveMessage, "buildCaveMessage");
336
269
  async function cleanupPendingDeletions(ctx, fileManager, logger2) {
337
270
  try {
338
271
  const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
339
- if (cavesToDelete.length === 0) return;
272
+ if (!cavesToDelete.length) return;
340
273
  for (const cave of cavesToDelete) {
341
274
  const deletePromises = cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file));
342
275
  await Promise.all(deletePromises);
@@ -349,15 +282,12 @@ async function cleanupPendingDeletions(ctx, fileManager, logger2) {
349
282
  __name(cleanupPendingDeletions, "cleanupPendingDeletions");
350
283
  function getScopeQuery(session, config) {
351
284
  const baseQuery = { status: "active" };
352
- if (config.perChannel && session.channelId) {
353
- return { ...baseQuery, channelId: session.channelId };
354
- }
355
- return baseQuery;
285
+ return config.perChannel && session.channelId ? { ...baseQuery, channelId: session.channelId } : baseQuery;
356
286
  }
357
287
  __name(getScopeQuery, "getScopeQuery");
358
288
  async function getNextCaveId(ctx, query = {}) {
359
- const allCaves = await ctx.database.get("cave", query, { fields: ["id"] });
360
- const existingIds = new Set(allCaves.map((c) => c.id));
289
+ const allCaveIds = (await ctx.database.get("cave", query, { fields: ["id"] })).map((c) => c.id);
290
+ const existingIds = new Set(allCaveIds);
361
291
  let newId = 1;
362
292
  while (existingIds.has(newId)) {
363
293
  newId++;
@@ -365,19 +295,8 @@ async function getNextCaveId(ctx, query = {}) {
365
295
  return newId;
366
296
  }
367
297
  __name(getNextCaveId, "getNextCaveId");
368
- async function downloadMedia(ctx, fileManager, url, originalName, type, caveId, index, channelId, userId) {
369
- const defaultExtMap = { "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
370
- const ext = originalName ? path2.extname(originalName) : "";
371
- const finalExt = ext || defaultExtMap[type] || ".dat";
372
- const fileName = `${caveId}_${index}_${channelId}_${userId}${finalExt}`;
373
- const response = await ctx.http.get(url, { responseType: "arraybuffer", timeout: 3e4 });
374
- return fileManager.saveFile(fileName, Buffer.from(response));
375
- }
376
- __name(downloadMedia, "downloadMedia");
377
298
  function checkCooldown(session, config, lastUsed) {
378
- if (config.coolDown <= 0 || !session.channelId || config.adminUsers.includes(session.userId)) {
379
- return null;
380
- }
299
+ if (config.coolDown <= 0 || !session.channelId || config.adminUsers.includes(session.userId)) return null;
381
300
  const now = Date.now();
382
301
  const lastTime = lastUsed.get(session.channelId) || 0;
383
302
  if (now - lastTime < config.coolDown * 1e3) {
@@ -397,11 +316,11 @@ __name(updateCooldownTimestamp, "updateCooldownTimestamp");
397
316
  // src/DataManager.ts
398
317
  var DataManager = class {
399
318
  /**
400
- * 创建一个 DataManager 实例。
401
- * @param ctx - Koishi 上下文,用于数据库操作。
402
- * @param config - 插件配置。
403
- * @param fileManager - 文件管理器实例,用于读写导入/导出文件。
404
- * @param logger - 日志记录器实例。
319
+ * @constructor
320
+ * @param ctx Koishi 上下文,用于数据库操作。
321
+ * @param config 插件配置。
322
+ * @param fileManager 文件管理器实例。
323
+ * @param logger 日志记录器实例。
405
324
  */
406
325
  constructor(ctx, config, fileManager, logger2) {
407
326
  this.ctx = ctx;
@@ -413,16 +332,15 @@ var DataManager = class {
413
332
  __name(this, "DataManager");
414
333
  }
415
334
  /**
416
- * 注册与数据导入导出相关的 `.export` 和 `.import` 子命令。
417
- * @param cave - 主 `cave` 命令的实例,用于挂载子命令。
335
+ * @description 注册 `.export` 和 `.import` 子命令。
336
+ * @param cave - 主 `cave` 命令实例。
418
337
  */
419
338
  registerCommands(cave) {
420
339
  cave.subcommand(".export", "导出回声洞数据").usage("将所有回声洞数据导出到 cave_export.json。").action(async ({ session }) => {
421
340
  if (!this.config.adminUsers.includes(session.userId)) return "抱歉,你没有权限导出数据";
422
341
  try {
423
342
  await session.send("正在导出数据,请稍候...");
424
- const resultMessage = await this.exportData();
425
- return resultMessage;
343
+ return await this.exportData();
426
344
  } catch (error) {
427
345
  this.logger.error("导出数据时发生错误:", error);
428
346
  return `导出失败: ${error.message}`;
@@ -432,8 +350,7 @@ var DataManager = class {
432
350
  if (!this.config.adminUsers.includes(session.userId)) return "抱歉,你没有权限导入数据";
433
351
  try {
434
352
  await session.send("正在导入数据,请稍候...");
435
- const resultMessage = await this.importData();
436
- return resultMessage;
353
+ return await this.importData();
437
354
  } catch (error) {
438
355
  this.logger.error("导入数据时发生错误:", error);
439
356
  return `导入失败: ${error.message}`;
@@ -441,9 +358,8 @@ var DataManager = class {
441
358
  });
442
359
  }
443
360
  /**
444
- * 导出所有状态为 'active' 的回声洞数据。
445
- * 数据将被序列化为 JSON 并保存到 `cave_export.json` 文件中。
446
- * @returns 一个描述导出结果的字符串消息。
361
+ * @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
362
+ * @returns 描述导出结果的消息字符串。
447
363
  */
448
364
  async exportData() {
449
365
  const fileName = "cave_export.json";
@@ -454,8 +370,8 @@ var DataManager = class {
454
370
  return `成功导出 ${portableCaves.length} 条数据`;
455
371
  }
456
372
  /**
457
- * 从 `cave_import.json` 文件导入回声洞数据。
458
- * @returns 一个描述导入结果的字符串消息。
373
+ * @description 从 `cave_import.json` 文件导入回声洞数据。
374
+ * @returns 描述导入结果的消息字符串。
459
375
  */
460
376
  async importData() {
461
377
  const fileName = "cave_import.json";
@@ -463,9 +379,7 @@ var DataManager = class {
463
379
  try {
464
380
  const fileContent = await this.fileManager.readFile(fileName);
465
381
  importedCaves = JSON.parse(fileContent.toString("utf-8"));
466
- if (!Array.isArray(importedCaves)) {
467
- throw new Error("导入文件格式无效");
468
- }
382
+ if (!Array.isArray(importedCaves)) throw new Error("导入文件格式无效");
469
383
  } catch (error) {
470
384
  this.logger.error(`读取导入文件失败:`, error);
471
385
  return `读取导入文件失败: ${error.message || "未知错误"}`;
@@ -477,9 +391,9 @@ var DataManager = class {
477
391
  ...cave,
478
392
  id: newId,
479
393
  channelId: cave.channelId || null,
480
- // 确保 channelId 存在,若无则为 null。
394
+ // 保证 channelId 存在
481
395
  status: "active"
482
- // 导入的数据直接设为 active 状态。
396
+ // 导入的数据直接设为活跃状态
483
397
  };
484
398
  await this.ctx.database.create("cave", newCave);
485
399
  successCount++;
@@ -491,11 +405,11 @@ var DataManager = class {
491
405
  // src/ReviewManager.ts
492
406
  var ReviewManager = class {
493
407
  /**
494
- * 创建一个 ReviewManager 实例。
495
- * @param ctx - Koishi 上下文。
496
- * @param config - 插件配置。
497
- * @param fileManager - 文件管理器实例。
498
- * @param logger - 日志记录器实例。
408
+ * @constructor
409
+ * @param ctx Koishi 上下文。
410
+ * @param config 插件配置。
411
+ * @param fileManager 文件管理器实例。
412
+ * @param logger 日志记录器实例。
499
413
  */
500
414
  constructor(ctx, config, fileManager, logger2) {
501
415
  this.ctx = ctx;
@@ -507,49 +421,36 @@ var ReviewManager = class {
507
421
  __name(this, "ReviewManager");
508
422
  }
509
423
  /**
510
- * 注册与审核相关的 `.review` 子命令。
511
- * @param cave - 主 `cave` 命令的实例,用于挂载子命令。
424
+ * @description 注册与审核相关的 `.review` 子命令。
425
+ * @param cave - 主 `cave` 命令实例。
512
426
  */
513
427
  registerCommands(cave) {
514
428
  cave.subcommand(".review [id:posint] [action:string]", "审核回声洞").usage("查看或审核回声洞,使用 <Y/N> 进行审核。").action(async ({ session }, id, action) => {
515
- if (!this.config.adminUsers.includes(session.userId)) {
516
- return "抱歉,你没有权限执行审核";
517
- }
429
+ if (!this.config.adminUsers.includes(session.userId)) return "抱歉,你没有权限执行审核";
518
430
  if (!id) {
519
431
  const pendingCaves = await this.ctx.database.get("cave", { status: "pending" });
520
- if (pendingCaves.length === 0) {
521
- return "当前没有需要审核的回声洞";
522
- }
523
- const pendingIds = pendingCaves.map((c) => c.id).join(", ");
432
+ if (pendingCaves.length === 0) return "当前没有需要审核的回声洞";
524
433
  return `当前共有 ${pendingCaves.length} 条待审核回声洞,序号为:
525
- ${pendingIds}`;
434
+ ${pendingCaves.map((c) => c.id).join(", ")}`;
526
435
  }
527
436
  const [targetCave] = await this.ctx.database.get("cave", { id });
528
- if (!targetCave) {
529
- return `回声洞(${id})不存在`;
530
- }
531
- if (targetCave.status !== "pending") {
532
- return `回声洞(${id})无需审核`;
533
- }
437
+ if (!targetCave) return `回声洞(${id})不存在`;
438
+ if (targetCave.status !== "pending") return `回声洞(${id})无需审核`;
534
439
  if (id && !action) {
535
- return this.buildReviewMessage(targetCave);
440
+ return [`待审核:`, ...await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger)];
536
441
  }
537
442
  const normalizedAction = action.toLowerCase();
538
443
  let reviewAction;
539
- if (["y", "yes", "ok", "pass", "approve"].includes(normalizedAction)) {
540
- reviewAction = "approve";
541
- } else if (["n", "no", "deny", "reject"].includes(normalizedAction)) {
542
- reviewAction = "reject";
543
- } else {
544
- return `无效操作: "${action}"
444
+ if (["y", "yes", "ok", "pass", "approve"].includes(normalizedAction)) reviewAction = "approve";
445
+ else if (["n", "no", "deny", "reject"].includes(normalizedAction)) reviewAction = "reject";
446
+ else return `无效操作: "${action}"
545
447
  请使用 "Y" (通过) 或 "N" (拒绝)`;
546
- }
547
448
  return this.processReview(reviewAction, id, session.username);
548
449
  });
549
450
  }
550
451
  /**
551
- * 将一条新的回声洞提交给所有管理员进行审核。
552
- * @param cave - 新创建的、状态为 'pending' 的回声洞对象。
452
+ * @description 将新回声洞提交给管理员审核。
453
+ * @param cave 新创建的、状态为 'pending' 的回声洞对象。
553
454
  */
554
455
  async sendForReview(cave) {
555
456
  if (!this.config.adminUsers?.length) {
@@ -557,7 +458,7 @@ ${pendingIds}`;
557
458
  await this.ctx.database.upsert("cave", [{ id: cave.id, status: "active" }]);
558
459
  return;
559
460
  }
560
- const reviewMessage = await this.buildReviewMessage(cave);
461
+ const reviewMessage = [`待审核:`, ...await buildCaveMessage(cave, this.config, this.fileManager, this.logger)];
561
462
  try {
562
463
  await this.ctx.broadcast(this.config.adminUsers, reviewMessage);
563
464
  } catch (error) {
@@ -565,29 +466,15 @@ ${pendingIds}`;
565
466
  }
566
467
  }
567
468
  /**
568
- * 构建一条用于发送给管理员的、包含审核信息的消息。
569
- * @param cave - 待审核的回声洞对象。
570
- * @returns 一个可直接发送的消息数组。
571
- * @private
572
- */
573
- async buildReviewMessage(cave) {
574
- const caveContent = await buildCaveMessage(cave, this.config, this.fileManager, this.logger);
575
- return [
576
- `以下内容待审核:`,
577
- ...caveContent
578
- ];
579
- }
580
- /**
581
- * 处理管理员的审核决定(通过或拒绝)。
582
- * @param action - 'approve' (通过) 或 'reject' (拒绝)。
583
- * @param caveId - 被审核的回声洞 ID。
584
- * @param adminUserName - 执行操作的管理员的昵称。
469
+ * @description 处理管理员的审核决定(通过或拒绝)。
470
+ * @param action 'approve' (通过) 或 'reject' (拒绝)。
471
+ * @param caveId 被审核的回声洞 ID。
472
+ * @param adminUserName 操作管理员的昵称。
585
473
  * @returns 返回给操作者的确认消息。
586
474
  */
587
475
  async processReview(action, caveId, adminUserName) {
588
- const [cave] = await this.ctx.database.get("cave", { id: caveId });
589
- if (!cave) return `回声洞(${caveId})不存在`;
590
- if (cave.status !== "pending") return `回声洞(${caveId})无需审核`;
476
+ const [cave] = await this.ctx.database.get("cave", { id: caveId, status: "pending" });
477
+ if (!cave) return `回声洞(${caveId})不存在或无需审核`;
591
478
  let resultMessage;
592
479
  let broadcastMessage;
593
480
  if (action === "approve") {
@@ -596,19 +483,12 @@ ${pendingIds}`;
596
483
  broadcastMessage = `回声洞(${caveId})已由管理员 "${adminUserName}" 通过`;
597
484
  } else {
598
485
  await this.ctx.database.upsert("cave", [{ id: caveId, status: "delete" }]);
599
- const caveContent = await buildCaveMessage(cave, this.config, this.fileManager, this.logger);
600
- resultMessage = [
601
- `回声洞(${caveId})已拒绝`,
602
- ...caveContent
603
- ];
604
- broadcastMessage = [
605
- `回声洞(${caveId})已由管理员 "${adminUserName}" 拒绝`,
606
- ...caveContent
607
- ];
486
+ resultMessage = `回声洞(${caveId})已拒绝`;
487
+ broadcastMessage = `回声洞(${caveId})已由管理员 "${adminUserName}" 拒绝`;
608
488
  cleanupPendingDeletions(this.ctx, this.fileManager, this.logger);
609
489
  }
610
- if (broadcastMessage && this.config.adminUsers?.length) {
611
- await this.ctx.broadcast(this.config.adminUsers, broadcastMessage).catch((err) => {
490
+ if (this.config.adminUsers?.length) {
491
+ this.ctx.broadcast(this.config.adminUsers, broadcastMessage).catch((err) => {
612
492
  this.logger.error(`广播回声洞(${cave.id})审核结果失败:`, err);
613
493
  });
614
494
  }
@@ -625,7 +505,6 @@ var usage = `
625
505
  <p>📖 <strong>使用文档</strong>:请点击左上角的 <strong>插件主页</strong> 查看插件使用文档</p>
626
506
  <p>🔍 <strong>更多插件</strong>:可访问 <a href="https://github.com/YisRime" style="color:#4a6ee0;text-decoration:none;">苡淞的 GitHub</a> 查看本人的所有插件</p>
627
507
  </div>
628
-
629
508
  <div style="border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
630
509
  <h2 style="margin-top: 0; color: #e0574a;">❤️ 支持与反馈</h2>
631
510
  <p>🌟 喜欢这个插件?请在 <a href="https://github.com/YisRime" style="color:#e0574a;text-decoration:none;">GitHub</a> 上给我一个 Star!</p>
@@ -659,23 +538,13 @@ var Config = import_koishi2.Schema.intersect([
659
538
  function apply(ctx, config) {
660
539
  ctx.model.extend("cave", {
661
540
  id: "unsigned",
662
- // 无符号整数,作为主键。
663
541
  elements: "json",
664
- // 存储为 JSON 字符串的元素数组。
665
542
  channelId: "string",
666
- // 频道 ID。
667
543
  userId: "string",
668
- // 用户 ID。
669
544
  userName: "string",
670
- // 用户昵称。
671
545
  status: "string",
672
- // 回声洞状态。
673
546
  time: "timestamp"
674
- // 提交时间。
675
- }, {
676
- primary: "id"
677
- // 将 'id' 字段设置为主键。
678
- });
547
+ }, { primary: "id" });
679
548
  const fileManager = new FileManager(ctx.baseDir, config, logger);
680
549
  const lastUsed = /* @__PURE__ */ new Map();
681
550
  let profileManager;
@@ -704,7 +573,6 @@ function apply(ctx, config) {
704
573
  }
705
574
  });
706
575
  cave.subcommand(".add [content:text]", "添加回声洞").usage("添加一条回声洞。可以直接发送内容,也可以回复或引用一条消息。").action(async ({ session }, content) => {
707
- const savedFileIdentifiers = [];
708
576
  try {
709
577
  let sourceElements;
710
578
  if (session.quote?.elements) {
@@ -717,46 +585,73 @@ function apply(ctx, config) {
717
585
  if (!reply) return "操作超时,已取消添加";
718
586
  sourceElements = import_koishi2.h.parse(reply);
719
587
  }
720
- const scopeQuery = getScopeQuery(session, config);
721
- const newId = await getNextCaveId(ctx, scopeQuery);
588
+ const idScopeQuery = {};
589
+ if (config.perChannel && session.channelId) {
590
+ idScopeQuery["channelId"] = session.channelId;
591
+ }
592
+ const newId = await getNextCaveId(ctx, idScopeQuery);
722
593
  const finalElementsForDb = [];
594
+ const mediaToSave = [];
723
595
  let mediaIndex = 0;
596
+ const typeMap = {
597
+ "img": "image",
598
+ "image": "image",
599
+ "video": "video",
600
+ "audio": "audio",
601
+ "file": "file",
602
+ "text": "text"
603
+ };
724
604
  async function traverseAndProcess(elements) {
725
605
  for (const el of elements) {
726
- const elementType = el.type;
727
- if (["image", "video", "audio", "file"].includes(elementType) && el.attrs.src) {
606
+ const normalizedType = typeMap[el.type];
607
+ if (!normalizedType) {
608
+ if (el.children) await traverseAndProcess(el.children);
609
+ continue;
610
+ }
611
+ if (["image", "video", "audio", "file"].includes(normalizedType) && el.attrs.src) {
728
612
  let fileIdentifier = el.attrs.src;
729
613
  if (fileIdentifier.startsWith("http")) {
730
614
  mediaIndex++;
731
- const originalName = el.attrs.file;
732
- const savedId = await downloadMedia(ctx, fileManager, fileIdentifier, originalName, elementType, newId, mediaIndex, session.channelId, session.userId);
733
- savedFileIdentifiers.push(savedId);
734
- fileIdentifier = savedId;
615
+ const defaultExtMap = { "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
616
+ const ext = el.attrs.file && path3.extname(el.attrs.file) ? path3.extname(el.attrs.file) : defaultExtMap[normalizedType] || ".dat";
617
+ const channelIdentifier = session.channelId || "private";
618
+ const fileName = `${newId}_${mediaIndex}_${channelIdentifier}_${session.userId}${ext}`;
619
+ mediaToSave.push({ sourceUrl: fileIdentifier, fileName });
620
+ fileIdentifier = fileName;
735
621
  }
736
- finalElementsForDb.push({ type: elementType, file: fileIdentifier });
737
- } else if (elementType === "text" && el.attrs.content?.trim()) {
622
+ finalElementsForDb.push({ type: normalizedType, file: fileIdentifier });
623
+ } else if (normalizedType === "text" && el.attrs.content?.trim()) {
738
624
  finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
739
625
  }
740
- if (el.children) await traverseAndProcess(el.children);
626
+ if (el.children) {
627
+ await traverseAndProcess(el.children);
628
+ }
741
629
  }
742
630
  }
743
631
  __name(traverseAndProcess, "traverseAndProcess");
744
632
  await traverseAndProcess(sourceElements);
745
633
  if (finalElementsForDb.length === 0) return "内容为空,已取消添加";
746
- let userName = session.username;
747
- if (config.enableProfile) {
748
- userName = await profileManager.getNickname(session.userId) || userName;
749
- }
634
+ const customNickname = config.enableProfile ? await profileManager.getNickname(session.userId) : null;
750
635
  const newCave = {
751
636
  id: newId,
752
637
  elements: finalElementsForDb,
753
638
  channelId: session.channelId,
754
639
  userId: session.userId,
755
- userName,
640
+ userName: customNickname || session.username,
756
641
  status: config.enableReview ? "pending" : "active",
757
642
  time: /* @__PURE__ */ new Date()
758
643
  };
759
644
  await ctx.database.create("cave", newCave);
645
+ try {
646
+ await Promise.all(mediaToSave.map(async (media) => {
647
+ const response = await ctx.http.get(media.sourceUrl, { responseType: "arraybuffer", timeout: 3e4 });
648
+ await fileManager.saveFile(media.fileName, Buffer.from(response));
649
+ }));
650
+ } catch (fileSaveError) {
651
+ logger.error(`文件保存失败:`, fileSaveError);
652
+ await ctx.database.remove("cave", { id: newId });
653
+ throw fileSaveError;
654
+ }
760
655
  if (newCave.status === "pending") {
761
656
  reviewManager.sendForReview(newCave);
762
657
  return `提交成功,序号为(${newCave.id})`;
@@ -764,10 +659,6 @@ function apply(ctx, config) {
764
659
  return `添加成功,序号为(${newId})`;
765
660
  } catch (error) {
766
661
  logger.error("添加回声洞失败:", error);
767
- if (savedFileIdentifiers.length > 0) {
768
- logger.info(`添加失败,回滚并删除 ${savedFileIdentifiers.length} 个文件...`);
769
- await Promise.all(savedFileIdentifiers.map((fileId) => fileManager.deleteFile(fileId)));
770
- }
771
662
  return "添加失败,请稍后再试";
772
663
  }
773
664
  });
@@ -778,9 +669,7 @@ function apply(ctx, config) {
778
669
  try {
779
670
  const query = { ...getScopeQuery(session, config), id };
780
671
  const [targetCave] = await ctx.database.get("cave", query);
781
- if (!targetCave) {
782
- return `回声洞(${id})不存在`;
783
- }
672
+ if (!targetCave) return `回声洞(${id})不存在`;
784
673
  updateCooldownTimestamp(session, config, lastUsed);
785
674
  return buildCaveMessage(targetCave, config, fileManager, logger);
786
675
  } catch (error) {
@@ -793,18 +682,13 @@ function apply(ctx, config) {
793
682
  try {
794
683
  const [targetCave] = await ctx.database.get("cave", { id, status: "active" });
795
684
  if (!targetCave) return `回声洞(${id})不存在`;
796
- const isOwner = targetCave.userId === session.userId;
797
- const isAdmin = config.adminUsers.includes(session.userId);
798
- if (!isOwner && !isAdmin) {
685
+ if (targetCave.userId !== session.userId && !config.adminUsers.includes(session.userId)) {
799
686
  return "抱歉,你没有权限删除这条回声洞";
800
687
  }
801
688
  await ctx.database.upsert("cave", [{ id, status: "delete" }]);
802
689
  const caveMessage = await buildCaveMessage(targetCave, config, fileManager, logger);
803
690
  cleanupPendingDeletions(ctx, fileManager, logger);
804
- return [
805
- `以下内容已删除`,
806
- ...caveMessage
807
- ];
691
+ return [`已删除`, ...caveMessage];
808
692
  } catch (error) {
809
693
  logger.error(`标记回声洞(${id})失败:`, error);
810
694
  return "删除失败,请稍后再试";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-best-cave",
3
3
  "description": "最强大的回声洞现已重构完成啦!注意数据格式需要使用脚本转换哦~",
4
- "version": "2.0.6",
4
+ "version": "2.0.7",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],