koishi-plugin-best-cave 2.0.5 → 2.0.6
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.
- package/lib/DataManager.d.ts +8 -5
- package/lib/FileManager.d.ts +18 -13
- package/lib/ProfileManager.d.ts +11 -9
- package/lib/ReviewManager.d.ts +13 -16
- package/lib/Utils.d.ts +46 -40
- package/lib/index.d.ts +16 -12
- package/lib/index.js +301 -268
- package/package.json +1 -1
package/lib/DataManager.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { Context, Logger } from 'koishi';
|
|
|
2
2
|
import { FileManager } from './FileManager';
|
|
3
3
|
import { Config } from './index';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @description
|
|
5
|
+
* 数据管理器 (DataManager)
|
|
6
|
+
* @description
|
|
7
|
+
* 负责处理回声洞数据的导入和导出功能。
|
|
7
8
|
*/
|
|
8
9
|
export declare class DataManager {
|
|
9
10
|
private ctx;
|
|
@@ -11,19 +12,21 @@ export declare class DataManager {
|
|
|
11
12
|
private fileManager;
|
|
12
13
|
private logger;
|
|
13
14
|
/**
|
|
15
|
+
* 创建一个 DataManager 实例。
|
|
14
16
|
* @param ctx - Koishi 上下文,用于数据库操作。
|
|
15
|
-
* @param config -
|
|
17
|
+
* @param config - 插件配置。
|
|
16
18
|
* @param fileManager - 文件管理器实例,用于读写导入/导出文件。
|
|
17
19
|
* @param logger - 日志记录器实例。
|
|
18
20
|
*/
|
|
19
21
|
constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger);
|
|
20
22
|
/**
|
|
21
23
|
* 注册与数据导入导出相关的 `.export` 和 `.import` 子命令。
|
|
22
|
-
* @param cave - 主 `cave`
|
|
24
|
+
* @param cave - 主 `cave` 命令的实例,用于挂载子命令。
|
|
23
25
|
*/
|
|
24
26
|
registerCommands(cave: any): void;
|
|
25
27
|
/**
|
|
26
|
-
* 导出所有状态为 'active'
|
|
28
|
+
* 导出所有状态为 'active' 的回声洞数据。
|
|
29
|
+
* 数据将被序列化为 JSON 并保存到 `cave_export.json` 文件中。
|
|
27
30
|
* @returns 一个描述导出结果的字符串消息。
|
|
28
31
|
*/
|
|
29
32
|
exportData(): Promise<string>;
|
package/lib/FileManager.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Logger } from 'koishi';
|
|
2
2
|
import { Config } from './index';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @description
|
|
4
|
+
* 文件管理器 (FileManager)
|
|
5
|
+
* @description
|
|
6
|
+
* 封装了对文件(资源)的存储、读取和删除操作。
|
|
7
|
+
* 它能够根据插件配置自动选择使用本地文件系统或 AWS S3 作为存储后端。
|
|
6
8
|
* 内置了基于 Promise 的文件锁,以防止对本地文件的并发写入冲突。
|
|
7
9
|
*/
|
|
8
10
|
export declare class FileManager {
|
|
@@ -12,13 +14,15 @@ export declare class FileManager {
|
|
|
12
14
|
private s3Client?;
|
|
13
15
|
private s3Bucket?;
|
|
14
16
|
/**
|
|
17
|
+
* 创建一个 FileManager 实例。
|
|
15
18
|
* @param baseDir - Koishi 应用的基础数据目录 (ctx.baseDir)。
|
|
16
|
-
* @param config -
|
|
19
|
+
* @param config - 插件的完整配置对象。
|
|
17
20
|
* @param logger - 日志记录器实例。
|
|
18
21
|
*/
|
|
19
22
|
constructor(baseDir: string, config: Config, logger: Logger);
|
|
20
23
|
/**
|
|
21
|
-
*
|
|
24
|
+
* 确保本地资源目录存在。如果目录不存在,则会递归创建。
|
|
25
|
+
* 这是一个幂等操作。
|
|
22
26
|
* @private
|
|
23
27
|
*/
|
|
24
28
|
private ensureDirectory;
|
|
@@ -30,7 +34,8 @@ export declare class FileManager {
|
|
|
30
34
|
*/
|
|
31
35
|
private getFullPath;
|
|
32
36
|
/**
|
|
33
|
-
*
|
|
37
|
+
* 使用文件锁来安全地执行一个异步文件操作。
|
|
38
|
+
* 这可以防止对同一文件的并发读写造成数据损坏。
|
|
34
39
|
* @template T - 异步操作的返回类型。
|
|
35
40
|
* @param fileName - 需要加锁的文件名。
|
|
36
41
|
* @param operation - 要执行的异步函数。
|
|
@@ -39,21 +44,21 @@ export declare class FileManager {
|
|
|
39
44
|
*/
|
|
40
45
|
private withLock;
|
|
41
46
|
/**
|
|
42
|
-
*
|
|
43
|
-
* @param fileName - 文件名,将用作 S3 Key 或本地文件名。
|
|
47
|
+
* 保存文件,自动选择 S3 或本地存储。
|
|
48
|
+
* @param fileName - 文件名,将用作 S3 中的 Key 或本地文件名。
|
|
44
49
|
* @param data - 要写入的 Buffer 数据。
|
|
45
|
-
* @returns
|
|
50
|
+
* @returns 返回保存时使用的文件名/标识符。
|
|
46
51
|
*/
|
|
47
52
|
saveFile(fileName: string, data: Buffer): Promise<string>;
|
|
48
53
|
/**
|
|
49
|
-
* 读取文件,自动从 S3
|
|
50
|
-
* @param fileName -
|
|
54
|
+
* 读取文件,自动从 S3 或本地存储读取。
|
|
55
|
+
* @param fileName - 要读取的文件名/标识符。
|
|
51
56
|
* @returns 文件的 Buffer 数据。
|
|
52
57
|
*/
|
|
53
58
|
readFile(fileName: string): Promise<Buffer>;
|
|
54
59
|
/**
|
|
55
|
-
* 删除文件,自动从 S3
|
|
56
|
-
* @param
|
|
60
|
+
* 删除文件,自动从 S3 或本地删除。
|
|
61
|
+
* @param fileIdentifier - 要删除的文件名/标识符。
|
|
57
62
|
*/
|
|
58
|
-
deleteFile(
|
|
63
|
+
deleteFile(fileIdentifier: string): Promise<void>;
|
|
59
64
|
}
|
package/lib/ProfileManager.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Context } from 'koishi';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* @
|
|
5
|
-
* @property
|
|
6
|
-
* @property {string} nickname - 用户在回声洞中显示的自定义昵称。
|
|
3
|
+
* 数据库中 `cave_user` 表的记录结构。
|
|
4
|
+
* @property userId - 用户的唯一 ID,作为主键。
|
|
5
|
+
* @property nickname - 用户设置的自定义昵称。
|
|
7
6
|
*/
|
|
8
7
|
export interface UserProfile {
|
|
9
8
|
userId: string;
|
|
@@ -15,19 +14,22 @@ declare module 'koishi' {
|
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @description
|
|
17
|
+
* 个人资料管理器 (ProfileManager)
|
|
18
|
+
* @description
|
|
19
|
+
* 负责管理用户在回声洞插件中的自定义昵称。
|
|
20
20
|
* 提供设置、获取和清除昵称的数据库操作和相关命令。
|
|
21
|
+
* 此类仅在插件配置中启用了 `enableProfile` 时才会被实例化。
|
|
21
22
|
*/
|
|
22
23
|
export declare class ProfileManager {
|
|
23
24
|
private ctx;
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
+
* 创建一个 ProfileManager 实例。
|
|
27
|
+
* @param ctx - Koishi 上下文,用于初始化数据库模型。
|
|
26
28
|
*/
|
|
27
29
|
constructor(ctx: Context);
|
|
28
30
|
/**
|
|
29
31
|
* 注册与用户昵称相关的 `.profile` 子命令。
|
|
30
|
-
* @param cave - 主 `cave`
|
|
32
|
+
* @param cave - 主 `cave` 命令的实例,用于挂载子命令。
|
|
31
33
|
*/
|
|
32
34
|
registerCommands(cave: any): void;
|
|
33
35
|
/**
|
|
@@ -39,7 +41,7 @@ export declare class ProfileManager {
|
|
|
39
41
|
/**
|
|
40
42
|
* 获取指定用户的昵称。
|
|
41
43
|
* @param userId - 目标用户的 ID。
|
|
42
|
-
* @returns
|
|
44
|
+
* @returns 返回用户的昵称字符串。如果用户未设置昵称,则返回 null。
|
|
43
45
|
*/
|
|
44
46
|
getNickname(userId: string): Promise<string | null>;
|
|
45
47
|
/**
|
package/lib/ReviewManager.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { Context, h, Logger } from 'koishi';
|
|
|
2
2
|
import { CaveObject, Config } from './index';
|
|
3
3
|
import { FileManager } from './FileManager';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @description
|
|
7
|
-
*
|
|
5
|
+
* 审核管理器 (ReviewManager)
|
|
6
|
+
* @description
|
|
7
|
+
* 负责处理回声洞的审核流程。当 `enableReview` 配置项开启时,
|
|
8
|
+
* 此管理器将被激活,用于处理新回声洞的提交、向管理员发送审核通知
|
|
9
|
+
* 以及处理管理员的审核操作(通过/拒绝)。
|
|
8
10
|
*/
|
|
9
11
|
export declare class ReviewManager {
|
|
10
12
|
private ctx;
|
|
@@ -12,30 +14,25 @@ export declare class ReviewManager {
|
|
|
12
14
|
private fileManager;
|
|
13
15
|
private logger;
|
|
14
16
|
/**
|
|
17
|
+
* 创建一个 ReviewManager 实例。
|
|
15
18
|
* @param ctx - Koishi 上下文。
|
|
16
|
-
* @param config -
|
|
19
|
+
* @param config - 插件配置。
|
|
17
20
|
* @param fileManager - 文件管理器实例。
|
|
18
21
|
* @param logger - 日志记录器实例。
|
|
19
22
|
*/
|
|
20
23
|
constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger);
|
|
21
24
|
/**
|
|
22
25
|
* 注册与审核相关的 `.review` 子命令。
|
|
23
|
-
* @param cave - 主 `cave`
|
|
26
|
+
* @param cave - 主 `cave` 命令的实例,用于挂载子命令。
|
|
24
27
|
*/
|
|
25
28
|
registerCommands(cave: any): void;
|
|
26
29
|
/**
|
|
27
|
-
*
|
|
28
|
-
* @private
|
|
29
|
-
*/
|
|
30
|
-
private listPendingCaves;
|
|
31
|
-
/**
|
|
32
|
-
* 将一条新回声洞提交给管理员进行审核。
|
|
33
|
-
* 如果没有配置管理员,将自动通过审核。
|
|
30
|
+
* 将一条新的回声洞提交给所有管理员进行审核。
|
|
34
31
|
* @param cave - 新创建的、状态为 'pending' 的回声洞对象。
|
|
35
32
|
*/
|
|
36
33
|
sendForReview(cave: CaveObject): Promise<void>;
|
|
37
34
|
/**
|
|
38
|
-
*
|
|
35
|
+
* 构建一条用于发送给管理员的、包含审核信息的消息。
|
|
39
36
|
* @param cave - 待审核的回声洞对象。
|
|
40
37
|
* @returns 一个可直接发送的消息数组。
|
|
41
38
|
* @private
|
|
@@ -44,9 +41,9 @@ export declare class ReviewManager {
|
|
|
44
41
|
/**
|
|
45
42
|
* 处理管理员的审核决定(通过或拒绝)。
|
|
46
43
|
* @param action - 'approve' (通过) 或 'reject' (拒绝)。
|
|
47
|
-
* @param
|
|
48
|
-
* @param adminUserName -
|
|
44
|
+
* @param caveId - 被审核的回声洞 ID。
|
|
45
|
+
* @param adminUserName - 执行操作的管理员的昵称。
|
|
49
46
|
* @returns 返回给操作者的确认消息。
|
|
50
47
|
*/
|
|
51
|
-
processReview(action: 'approve' | 'reject',
|
|
48
|
+
processReview(action: 'approve' | 'reject', caveId: number, adminUserName: string): Promise<string | (string | h)[]>;
|
|
52
49
|
}
|
package/lib/Utils.d.ts
CHANGED
|
@@ -2,72 +2,78 @@ import { Context, h, Logger, Session } from 'koishi';
|
|
|
2
2
|
import { CaveObject, Config, StoredElement } from './index';
|
|
3
3
|
import { FileManager } from './FileManager';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param elements -
|
|
7
|
-
* @returns
|
|
5
|
+
* 将数据库中存储的 StoredElement[] 数组转换为 Koishi h() 元素数组。
|
|
6
|
+
* @param elements - 从数据库读取的元素对象数组。
|
|
7
|
+
* @returns 转换后的 h() 元素数组,用于消息发送。
|
|
8
8
|
*/
|
|
9
9
|
export declare function storedFormatToHElements(elements: StoredElement[]): h[];
|
|
10
10
|
/**
|
|
11
|
-
* 将指向本地媒体文件的 h
|
|
12
|
-
* @param element -
|
|
11
|
+
* 将指向本地媒体文件的 h() 元素转换为内联 Base64 格式。
|
|
12
|
+
* @param element - 包含本地文件路径的 h() 媒体元素。
|
|
13
13
|
* @param fileManager - FileManager 实例,用于读取文件。
|
|
14
|
-
* @param logger - Logger
|
|
15
|
-
* @returns 转换后的 h
|
|
14
|
+
* @param logger - Logger 实例,用于记录错误。
|
|
15
|
+
* @returns 转换后的 h() 元素,其 src 属性为 Base64 数据 URI。
|
|
16
16
|
*/
|
|
17
17
|
export declare function mediaElementToBase64(element: h, fileManager: FileManager, logger: Logger): Promise<h>;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* 构建一条包含回声洞内容的完整消息,准备发送。
|
|
20
|
+
* 此函数会处理 S3 URL、文件映射路径或本地文件到 Base64 的转换。
|
|
20
21
|
* @param cave - 要展示的回声洞对象。
|
|
21
|
-
* @param config -
|
|
22
|
+
* @param config - 插件配置。
|
|
22
23
|
* @param fileManager - FileManager 实例。
|
|
23
24
|
* @param logger - Logger 实例。
|
|
24
|
-
* @returns
|
|
25
|
+
* @returns 一个包含 h() 元素和字符串的消息数组。
|
|
25
26
|
*/
|
|
26
27
|
export declare function buildCaveMessage(cave: CaveObject, config: Config, fileManager: FileManager, logger: Logger): Promise<(string | h)[]>;
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
-
* @param sourceElements - 源消息中的 h-element 数组。
|
|
30
|
-
* @param newId - 新回声洞的 ID。
|
|
31
|
-
* @param channelId - 频道 ID。
|
|
32
|
-
* @param userId - 用户 ID。
|
|
33
|
-
* @returns 一个包含待存储元素和待下载媒体列表的对象。
|
|
34
|
-
*/
|
|
35
|
-
export declare function prepareElementsForStorage(sourceElements: h[], newId: number, channelId: string, userId: string): {
|
|
36
|
-
finalElementsForDb: StoredElement[];
|
|
37
|
-
mediaToDownload: {
|
|
38
|
-
url: string;
|
|
39
|
-
fileName: string;
|
|
40
|
-
}[];
|
|
41
|
-
};
|
|
42
|
-
/**
|
|
43
|
-
* 清理数据库中所有标记为 'delete' 状态的回声洞及其关联的文件。
|
|
29
|
+
* 清理数据库中所有被标记为 'delete' 状态的回声洞及其关联的文件。
|
|
44
30
|
* @param ctx - Koishi 上下文。
|
|
45
31
|
* @param fileManager - FileManager 实例,用于删除文件。
|
|
46
32
|
* @param logger - Logger 实例。
|
|
47
33
|
*/
|
|
48
34
|
export declare function cleanupPendingDeletions(ctx: Context, fileManager: FileManager, logger: Logger): Promise<void>;
|
|
49
35
|
/**
|
|
50
|
-
*
|
|
51
|
-
* @param session -
|
|
52
|
-
* @param config -
|
|
53
|
-
* @returns
|
|
36
|
+
* 根据插件配置(是否分群)和当前会话,生成数据库查询所需的范围条件。
|
|
37
|
+
* @param session - 当前会话对象。
|
|
38
|
+
* @param config - 插件配置。
|
|
39
|
+
* @returns 一个用于数据库查询的条件对象。
|
|
40
|
+
*/
|
|
41
|
+
export declare function getScopeQuery(session: Session, config: Config): object;
|
|
42
|
+
/**
|
|
43
|
+
* 获取下一个可用的回声洞 ID。
|
|
44
|
+
* 策略是找到当前已存在的 ID 中最小的未使用正整数。
|
|
45
|
+
* @param ctx - Koishi 上下文。
|
|
46
|
+
* @param query - 查询回声洞的范围条件,用于分群模式。
|
|
47
|
+
* @returns 一个可用的新 ID。
|
|
48
|
+
* @performance 对于非常大的数据集,此函数可能会有性能瓶颈,因为它需要获取所有现有 ID。
|
|
49
|
+
*/
|
|
50
|
+
export declare function getNextCaveId(ctx: Context, query?: object): Promise<number>;
|
|
51
|
+
/**
|
|
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。
|
|
62
|
+
* @returns 保存后的文件名/标识符。
|
|
54
63
|
*/
|
|
55
|
-
export declare function
|
|
56
|
-
status: 'active';
|
|
57
|
-
channelId?: string;
|
|
58
|
-
};
|
|
64
|
+
export declare function downloadMedia(ctx: Context, fileManager: FileManager, url: string, originalName: string, type: string, caveId: number, index: number, channelId: string, userId: string): Promise<string>;
|
|
59
65
|
/**
|
|
60
|
-
*
|
|
61
|
-
* @param session -
|
|
62
|
-
* @param config -
|
|
66
|
+
* 检查用户在当前频道是否处于指令冷却状态。
|
|
67
|
+
* @param session - 当前会话对象。
|
|
68
|
+
* @param config - 插件配置。
|
|
63
69
|
* @param lastUsed - 存储各频道最后使用时间的 Map。
|
|
64
|
-
* @returns
|
|
70
|
+
* @returns 如果处于冷却中,返回提示信息字符串;否则返回 null。
|
|
65
71
|
*/
|
|
66
72
|
export declare function checkCooldown(session: Session, config: Config, lastUsed: Map<string, number>): string | null;
|
|
67
73
|
/**
|
|
68
74
|
* 更新指定频道的指令使用时间戳。
|
|
69
|
-
* @param session -
|
|
70
|
-
* @param config -
|
|
75
|
+
* @param session - 当前会话对象。
|
|
76
|
+
* @param config - 插件配置。
|
|
71
77
|
* @param lastUsed - 存储各频道最后使用时间的 Map。
|
|
72
78
|
*/
|
|
73
79
|
export declare function updateCooldownTimestamp(session: Session, config: Config, lastUsed: Map<string, number>): void;
|
package/lib/index.d.ts
CHANGED
|
@@ -3,11 +3,10 @@ export declare const name = "best-cave";
|
|
|
3
3
|
export declare const inject: string[];
|
|
4
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";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* @
|
|
8
|
-
* @property type - 元素类型: 'text', 'image', 'video', 'audio', 'file'。
|
|
6
|
+
* 存储在数据库中的单个消息元素。
|
|
7
|
+
* @property type - 元素类型。
|
|
9
8
|
* @property content - 文本内容,仅用于 'text' 类型。
|
|
10
|
-
* @property file -
|
|
9
|
+
* @property file - 文件标识符(本地文件名或 S3 Key),用于媒体类型。
|
|
11
10
|
*/
|
|
12
11
|
export interface StoredElement {
|
|
13
12
|
type: 'text' | 'image' | 'video' | 'audio' | 'file';
|
|
@@ -15,15 +14,14 @@ export interface StoredElement {
|
|
|
15
14
|
file?: string;
|
|
16
15
|
}
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
* @
|
|
20
|
-
* @property
|
|
21
|
-
* @property elements - 构成回声洞内容的 StoredElement 数组。
|
|
17
|
+
* 数据库中 `cave` 表的完整对象模型。
|
|
18
|
+
* @property id - 回声洞的唯一数字 ID。
|
|
19
|
+
* @property elements - 构成回声洞内容的元素数组。
|
|
22
20
|
* @property channelId - 提交回声洞的频道 ID,若为私聊则为 null。
|
|
23
21
|
* @property userId - 提交用户的 ID。
|
|
24
22
|
* @property userName - 提交用户的昵称。
|
|
25
23
|
* @property status - 回声洞状态: 'active' (活跃), 'delete' (待删除), 'pending' (待审核)。
|
|
26
|
-
* @property time -
|
|
24
|
+
* @property time - 提交时间。
|
|
27
25
|
*/
|
|
28
26
|
export interface CaveObject {
|
|
29
27
|
id: number;
|
|
@@ -39,6 +37,9 @@ declare module 'koishi' {
|
|
|
39
37
|
cave: CaveObject;
|
|
40
38
|
}
|
|
41
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* 插件的配置接口。
|
|
42
|
+
*/
|
|
42
43
|
export interface Config {
|
|
43
44
|
coolDown: number;
|
|
44
45
|
perChannel: boolean;
|
|
@@ -49,16 +50,19 @@ export interface Config {
|
|
|
49
50
|
caveFormat: string;
|
|
50
51
|
localPath?: string;
|
|
51
52
|
enableS3: boolean;
|
|
52
|
-
publicUrl?: string;
|
|
53
53
|
endpoint?: string;
|
|
54
|
-
bucket?: string;
|
|
55
54
|
region?: string;
|
|
56
55
|
accessKeyId?: string;
|
|
57
56
|
secretAccessKey?: string;
|
|
57
|
+
bucket?: string;
|
|
58
|
+
publicUrl?: string;
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* 使用 Koishi Schema 定义插件的配置项,用于生成配置界面。
|
|
62
|
+
*/
|
|
59
63
|
export declare const Config: Schema<Config>;
|
|
60
64
|
/**
|
|
61
|
-
*
|
|
65
|
+
* 插件的入口函数。
|
|
62
66
|
* @param ctx - Koishi 上下文。
|
|
63
67
|
* @param config - 用户提供的插件配置。
|
|
64
68
|
*/
|
package/lib/index.js
CHANGED
|
@@ -45,8 +45,9 @@ var fs = __toESM(require("fs/promises"));
|
|
|
45
45
|
var path = __toESM(require("path"));
|
|
46
46
|
var FileManager = class {
|
|
47
47
|
/**
|
|
48
|
+
* 创建一个 FileManager 实例。
|
|
48
49
|
* @param baseDir - Koishi 应用的基础数据目录 (ctx.baseDir)。
|
|
49
|
-
* @param config -
|
|
50
|
+
* @param config - 插件的完整配置对象。
|
|
50
51
|
* @param logger - 日志记录器实例。
|
|
51
52
|
*/
|
|
52
53
|
constructor(baseDir, config, logger2) {
|
|
@@ -67,19 +68,24 @@ var FileManager = class {
|
|
|
67
68
|
static {
|
|
68
69
|
__name(this, "FileManager");
|
|
69
70
|
}
|
|
71
|
+
// 本地资源存储目录的绝对路径。
|
|
70
72
|
resourceDir;
|
|
73
|
+
// 本地文件锁,键为文件绝对路径,值为一个 Promise,用于防止对同一文件的并发访问。
|
|
71
74
|
locks = /* @__PURE__ */ new Map();
|
|
75
|
+
// S3 客户端实例,仅在启用 S3 时初始化。
|
|
72
76
|
s3Client;
|
|
77
|
+
// S3 存储桶名称。
|
|
73
78
|
s3Bucket;
|
|
74
79
|
/**
|
|
75
|
-
*
|
|
80
|
+
* 确保本地资源目录存在。如果目录不存在,则会递归创建。
|
|
81
|
+
* 这是一个幂等操作。
|
|
76
82
|
* @private
|
|
77
83
|
*/
|
|
78
84
|
async ensureDirectory() {
|
|
79
85
|
try {
|
|
80
86
|
await fs.mkdir(this.resourceDir, { recursive: true });
|
|
81
87
|
} catch (error) {
|
|
82
|
-
this.logger.error(
|
|
88
|
+
this.logger.error(`创建资源目录失败 ${this.resourceDir}:`, error);
|
|
83
89
|
throw error;
|
|
84
90
|
}
|
|
85
91
|
}
|
|
@@ -93,7 +99,8 @@ var FileManager = class {
|
|
|
93
99
|
return path.join(this.resourceDir, fileName);
|
|
94
100
|
}
|
|
95
101
|
/**
|
|
96
|
-
*
|
|
102
|
+
* 使用文件锁来安全地执行一个异步文件操作。
|
|
103
|
+
* 这可以防止对同一文件的并发读写造成数据损坏。
|
|
97
104
|
* @template T - 异步操作的返回类型。
|
|
98
105
|
* @param fileName - 需要加锁的文件名。
|
|
99
106
|
* @param operation - 要执行的异步函数。
|
|
@@ -103,24 +110,19 @@ var FileManager = class {
|
|
|
103
110
|
async withLock(fileName, operation) {
|
|
104
111
|
const fullPath = this.getFullPath(fileName);
|
|
105
112
|
while (this.locks.has(fullPath)) {
|
|
106
|
-
await this.locks.get(fullPath)
|
|
107
|
-
});
|
|
113
|
+
await this.locks.get(fullPath);
|
|
108
114
|
}
|
|
109
|
-
const promise = operation()
|
|
115
|
+
const promise = operation().finally(() => {
|
|
116
|
+
this.locks.delete(fullPath);
|
|
117
|
+
});
|
|
110
118
|
this.locks.set(fullPath, promise);
|
|
111
|
-
|
|
112
|
-
return await promise;
|
|
113
|
-
} finally {
|
|
114
|
-
if (this.locks.get(fullPath) === promise) {
|
|
115
|
-
this.locks.delete(fullPath);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
119
|
+
return promise;
|
|
118
120
|
}
|
|
119
121
|
/**
|
|
120
|
-
*
|
|
121
|
-
* @param fileName - 文件名,将用作 S3 Key 或本地文件名。
|
|
122
|
+
* 保存文件,自动选择 S3 或本地存储。
|
|
123
|
+
* @param fileName - 文件名,将用作 S3 中的 Key 或本地文件名。
|
|
122
124
|
* @param data - 要写入的 Buffer 数据。
|
|
123
|
-
* @returns
|
|
125
|
+
* @returns 返回保存时使用的文件名/标识符。
|
|
124
126
|
*/
|
|
125
127
|
async saveFile(fileName, data) {
|
|
126
128
|
if (this.s3Client) {
|
|
@@ -129,18 +131,20 @@ var FileManager = class {
|
|
|
129
131
|
Key: fileName,
|
|
130
132
|
Body: data,
|
|
131
133
|
ACL: "public-read"
|
|
134
|
+
// 默认将文件权限设置为公开可读,方便通过 URL 访问。
|
|
132
135
|
});
|
|
133
136
|
await this.s3Client.send(command);
|
|
137
|
+
return fileName;
|
|
134
138
|
} else {
|
|
135
139
|
await this.ensureDirectory();
|
|
136
140
|
const filePath = this.getFullPath(fileName);
|
|
137
141
|
await this.withLock(fileName, () => fs.writeFile(filePath, data));
|
|
142
|
+
return fileName;
|
|
138
143
|
}
|
|
139
|
-
return fileName;
|
|
140
144
|
}
|
|
141
145
|
/**
|
|
142
|
-
* 读取文件,自动从 S3
|
|
143
|
-
* @param fileName -
|
|
146
|
+
* 读取文件,自动从 S3 或本地存储读取。
|
|
147
|
+
* @param fileName - 要读取的文件名/标识符。
|
|
144
148
|
* @returns 文件的 Buffer 数据。
|
|
145
149
|
*/
|
|
146
150
|
async readFile(fileName) {
|
|
@@ -158,26 +162,26 @@ var FileManager = class {
|
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
/**
|
|
161
|
-
* 删除文件,自动从 S3
|
|
162
|
-
* @param
|
|
165
|
+
* 删除文件,自动从 S3 或本地删除。
|
|
166
|
+
* @param fileIdentifier - 要删除的文件名/标识符。
|
|
163
167
|
*/
|
|
164
|
-
async deleteFile(
|
|
168
|
+
async deleteFile(fileIdentifier) {
|
|
165
169
|
if (this.s3Client) {
|
|
166
170
|
const command = new import_client_s3.DeleteObjectCommand({
|
|
167
171
|
Bucket: this.s3Bucket,
|
|
168
|
-
Key:
|
|
172
|
+
Key: fileIdentifier
|
|
169
173
|
});
|
|
170
174
|
await this.s3Client.send(command).catch((err) => {
|
|
171
|
-
this.logger.warn(`删除文件 ${
|
|
175
|
+
this.logger.warn(`删除文件 ${fileIdentifier} 失败:`, err);
|
|
172
176
|
});
|
|
173
177
|
} else {
|
|
174
|
-
const filePath = this.getFullPath(
|
|
175
|
-
await this.withLock(
|
|
178
|
+
const filePath = this.getFullPath(fileIdentifier);
|
|
179
|
+
await this.withLock(fileIdentifier, async () => {
|
|
176
180
|
try {
|
|
177
181
|
await fs.unlink(filePath);
|
|
178
182
|
} catch (error) {
|
|
179
183
|
if (error.code !== "ENOENT") {
|
|
180
|
-
this.logger.warn(`删除文件 ${
|
|
184
|
+
this.logger.warn(`删除文件 ${filePath} 失败:`, error);
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
});
|
|
@@ -188,15 +192,19 @@ var FileManager = class {
|
|
|
188
192
|
// src/ProfileManager.ts
|
|
189
193
|
var ProfileManager = class {
|
|
190
194
|
/**
|
|
191
|
-
*
|
|
195
|
+
* 创建一个 ProfileManager 实例。
|
|
196
|
+
* @param ctx - Koishi 上下文,用于初始化数据库模型。
|
|
192
197
|
*/
|
|
193
198
|
constructor(ctx) {
|
|
194
199
|
this.ctx = ctx;
|
|
195
200
|
this.ctx.model.extend("cave_user", {
|
|
196
201
|
userId: "string",
|
|
202
|
+
// 用户 ID
|
|
197
203
|
nickname: "string"
|
|
204
|
+
// 用户自定义昵称
|
|
198
205
|
}, {
|
|
199
206
|
primary: "userId"
|
|
207
|
+
// 使用 userId 作为主键,确保每个用户只有一条昵称记录。
|
|
200
208
|
});
|
|
201
209
|
}
|
|
202
210
|
static {
|
|
@@ -204,7 +212,7 @@ var ProfileManager = class {
|
|
|
204
212
|
}
|
|
205
213
|
/**
|
|
206
214
|
* 注册与用户昵称相关的 `.profile` 子命令。
|
|
207
|
-
* @param cave - 主 `cave`
|
|
215
|
+
* @param cave - 主 `cave` 命令的实例,用于挂载子命令。
|
|
208
216
|
*/
|
|
209
217
|
registerCommands(cave) {
|
|
210
218
|
cave.subcommand(".profile [nickname:text]", "设置显示昵称").usage("设置你在回声洞中显示的昵称。不提供昵称则清除记录。").action(async ({ session }, nickname) => {
|
|
@@ -223,16 +231,19 @@ var ProfileManager = class {
|
|
|
223
231
|
* @param nickname - 要设置的新昵称。
|
|
224
232
|
*/
|
|
225
233
|
async setNickname(userId, nickname) {
|
|
226
|
-
await this.ctx.database.upsert("cave_user", [{
|
|
234
|
+
await this.ctx.database.upsert("cave_user", [{
|
|
235
|
+
userId,
|
|
236
|
+
nickname
|
|
237
|
+
}]);
|
|
227
238
|
}
|
|
228
239
|
/**
|
|
229
240
|
* 获取指定用户的昵称。
|
|
230
241
|
* @param userId - 目标用户的 ID。
|
|
231
|
-
* @returns
|
|
242
|
+
* @returns 返回用户的昵称字符串。如果用户未设置昵称,则返回 null。
|
|
232
243
|
*/
|
|
233
244
|
async getNickname(userId) {
|
|
234
|
-
const
|
|
235
|
-
return
|
|
245
|
+
const profiles = await this.ctx.database.get("cave_user", { userId });
|
|
246
|
+
return profiles[0]?.nickname || null;
|
|
236
247
|
}
|
|
237
248
|
/**
|
|
238
249
|
* 清除指定用户的昵称设置。
|
|
@@ -243,102 +254,6 @@ var ProfileManager = class {
|
|
|
243
254
|
}
|
|
244
255
|
};
|
|
245
256
|
|
|
246
|
-
// src/DataManager.ts
|
|
247
|
-
var DataManager = class {
|
|
248
|
-
/**
|
|
249
|
-
* @param ctx - Koishi 上下文,用于数据库操作。
|
|
250
|
-
* @param config - 插件配置对象。
|
|
251
|
-
* @param fileManager - 文件管理器实例,用于读写导入/导出文件。
|
|
252
|
-
* @param logger - 日志记录器实例。
|
|
253
|
-
*/
|
|
254
|
-
constructor(ctx, config, fileManager, logger2) {
|
|
255
|
-
this.ctx = ctx;
|
|
256
|
-
this.config = config;
|
|
257
|
-
this.fileManager = fileManager;
|
|
258
|
-
this.logger = logger2;
|
|
259
|
-
}
|
|
260
|
-
static {
|
|
261
|
-
__name(this, "DataManager");
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* 注册与数据导入导出相关的 `.export` 和 `.import` 子命令。
|
|
265
|
-
* @param cave - 主 `cave` 命令实例,用于挂载子命令。
|
|
266
|
-
*/
|
|
267
|
-
registerCommands(cave) {
|
|
268
|
-
cave.subcommand(".export", "导出回声洞数据").usage("将所有回声洞数据导出到 cave_export.json。").action(async ({ session }) => {
|
|
269
|
-
if (!this.config.adminUsers.includes(session.userId)) return "抱歉,你没有权限导出数据";
|
|
270
|
-
try {
|
|
271
|
-
await session.send("正在导出数据,请稍候...");
|
|
272
|
-
return await this.exportData();
|
|
273
|
-
} catch (error) {
|
|
274
|
-
this.logger.error("导出数据时发生错误:", error);
|
|
275
|
-
return `导出失败: ${error.message}`;
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
cave.subcommand(".import", "导入回声洞数据").usage("从 cave_import.json 中导入回声洞数据。").action(async ({ session }) => {
|
|
279
|
-
if (!this.config.adminUsers.includes(session.userId)) return "抱歉,你没有权限导入数据";
|
|
280
|
-
try {
|
|
281
|
-
await session.send("正在导入数据,请稍候...");
|
|
282
|
-
return await this.importData();
|
|
283
|
-
} catch (error) {
|
|
284
|
-
this.logger.error("导入数据时发生错误:", error);
|
|
285
|
-
return `导入失败: ${error.message}`;
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* 导出所有状态为 'active' 的回声洞数据到 `cave_export.json` 文件。
|
|
291
|
-
* @returns 一个描述导出结果的字符串消息。
|
|
292
|
-
*/
|
|
293
|
-
async exportData() {
|
|
294
|
-
const fileName = "cave_export.json";
|
|
295
|
-
const cavesToExport = await this.ctx.database.get("cave", { status: "active" });
|
|
296
|
-
const portableCaves = cavesToExport.map(({ id, ...rest }) => rest);
|
|
297
|
-
const data = JSON.stringify(portableCaves, null, 2);
|
|
298
|
-
await this.fileManager.saveFile(fileName, Buffer.from(data));
|
|
299
|
-
return `成功导出 ${portableCaves.length} 条数据`;
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* 从 `cave_import.json` 文件导入回声洞数据。
|
|
303
|
-
* @returns 一个描述导入结果的字符串消息。
|
|
304
|
-
*/
|
|
305
|
-
async importData() {
|
|
306
|
-
const fileName = "cave_import.json";
|
|
307
|
-
let importedData;
|
|
308
|
-
try {
|
|
309
|
-
const fileContent = await this.fileManager.readFile(fileName);
|
|
310
|
-
importedData = JSON.parse(fileContent.toString("utf-8"));
|
|
311
|
-
if (!Array.isArray(importedData)) {
|
|
312
|
-
throw new Error("导入文件格式非 JSON 数组");
|
|
313
|
-
}
|
|
314
|
-
} catch (error) {
|
|
315
|
-
return `读取导入文件失败: ${error.message}`;
|
|
316
|
-
}
|
|
317
|
-
const allCaves = await this.ctx.database.get("cave", {}, { fields: ["id"] });
|
|
318
|
-
const existingIds = new Set(allCaves.map((c) => c.id));
|
|
319
|
-
let nextId = 1;
|
|
320
|
-
const cavesToCreate = [];
|
|
321
|
-
for (const caveData of importedData) {
|
|
322
|
-
while (existingIds.has(nextId)) {
|
|
323
|
-
nextId++;
|
|
324
|
-
}
|
|
325
|
-
const newId = nextId;
|
|
326
|
-
const newCave = {
|
|
327
|
-
...caveData,
|
|
328
|
-
id: newId,
|
|
329
|
-
channelId: caveData.channelId,
|
|
330
|
-
status: "active"
|
|
331
|
-
};
|
|
332
|
-
cavesToCreate.push(newCave);
|
|
333
|
-
existingIds.add(newId);
|
|
334
|
-
}
|
|
335
|
-
if (cavesToCreate.length > 0) {
|
|
336
|
-
await this.ctx.database.upsert("cave", cavesToCreate);
|
|
337
|
-
}
|
|
338
|
-
return `成功导入 ${cavesToCreate.length} 条回声洞数据`;
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
|
|
342
257
|
// src/Utils.ts
|
|
343
258
|
var import_koishi = require("koishi");
|
|
344
259
|
var path2 = __toESM(require("path"));
|
|
@@ -376,7 +291,7 @@ async function mediaElementToBase64(element, fileManager, logger2) {
|
|
|
376
291
|
return (0, import_koishi.h)(element.type, { ...element.attrs, src: `data:${mimeType};base64,${data.toString("base64")}` });
|
|
377
292
|
} catch (error) {
|
|
378
293
|
logger2.warn(`转换本地文件 ${fileName} 为 Base64 失败:`, error);
|
|
379
|
-
return import_koishi.h
|
|
294
|
+
return (0, import_koishi.h)("p", {}, `[${element.type}]`);
|
|
380
295
|
}
|
|
381
296
|
}
|
|
382
297
|
__name(mediaElementToBase64, "mediaElementToBase64");
|
|
@@ -386,68 +301,47 @@ async function buildCaveMessage(cave, config, fileManager, logger2) {
|
|
|
386
301
|
const isMedia = ["image", "video", "audio", "file"].includes(element.type);
|
|
387
302
|
const fileName = element.attrs.src;
|
|
388
303
|
if (!isMedia || !fileName) {
|
|
389
|
-
return element;
|
|
304
|
+
return Promise.resolve(element);
|
|
390
305
|
}
|
|
391
306
|
if (config.enableS3 && config.publicUrl) {
|
|
392
|
-
const fullUrl =
|
|
393
|
-
return (0, import_koishi.h)(element.type, { ...element.attrs, src: fullUrl });
|
|
307
|
+
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 }));
|
|
394
309
|
}
|
|
395
310
|
if (config.localPath) {
|
|
396
|
-
const
|
|
397
|
-
|
|
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 }));
|
|
398
314
|
}
|
|
399
315
|
return mediaElementToBase64(element, fileManager, logger2);
|
|
400
316
|
}));
|
|
401
317
|
const finalMessage = [];
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
|
|
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);
|
|
329
|
+
if (headerText.trim()) finalMessage.push(headerText);
|
|
405
330
|
finalMessage.push(...processedElements);
|
|
406
|
-
|
|
331
|
+
const footerText = footerFormat.replace("{id}", cave.id.toString()).replace("{name}", cave.userName);
|
|
332
|
+
if (footerText.trim()) finalMessage.push(footerText);
|
|
407
333
|
return finalMessage;
|
|
408
334
|
}
|
|
409
335
|
__name(buildCaveMessage, "buildCaveMessage");
|
|
410
|
-
function prepareElementsForStorage(sourceElements, newId, channelId, userId) {
|
|
411
|
-
const finalElementsForDb = [];
|
|
412
|
-
const mediaToDownload = [];
|
|
413
|
-
let mediaIndex = 0;
|
|
414
|
-
const processElement = /* @__PURE__ */ __name((el) => {
|
|
415
|
-
const elementType = el.type;
|
|
416
|
-
if (["image", "video", "audio", "file"].includes(elementType) && el.attrs.src) {
|
|
417
|
-
const fileIdentifier = el.attrs.src;
|
|
418
|
-
if (fileIdentifier.startsWith("http")) {
|
|
419
|
-
mediaIndex++;
|
|
420
|
-
const originalName = el.attrs.file;
|
|
421
|
-
const defaultExtMap = { "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
|
|
422
|
-
const ext = originalName ? path2.extname(originalName) : "";
|
|
423
|
-
const finalExt = ext || defaultExtMap[elementType] || ".dat";
|
|
424
|
-
const generatedFileName = `${newId}_${mediaIndex}_${channelId}_${userId}${finalExt}`;
|
|
425
|
-
finalElementsForDb.push({ type: elementType, file: generatedFileName });
|
|
426
|
-
mediaToDownload.push({ url: fileIdentifier, fileName: generatedFileName });
|
|
427
|
-
} else {
|
|
428
|
-
finalElementsForDb.push({ type: elementType, file: fileIdentifier });
|
|
429
|
-
}
|
|
430
|
-
} else if (elementType === "text" && el.attrs.content?.trim()) {
|
|
431
|
-
finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
|
|
432
|
-
}
|
|
433
|
-
if (el.children) {
|
|
434
|
-
el.children.forEach(processElement);
|
|
435
|
-
}
|
|
436
|
-
}, "processElement");
|
|
437
|
-
sourceElements.forEach(processElement);
|
|
438
|
-
return { finalElementsForDb, mediaToDownload };
|
|
439
|
-
}
|
|
440
|
-
__name(prepareElementsForStorage, "prepareElementsForStorage");
|
|
441
336
|
async function cleanupPendingDeletions(ctx, fileManager, logger2) {
|
|
442
337
|
try {
|
|
443
338
|
const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
|
|
444
339
|
if (cavesToDelete.length === 0) return;
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
await ctx.database.remove("cave", { id: { $in: idsToRemove } });
|
|
340
|
+
for (const cave of cavesToDelete) {
|
|
341
|
+
const deletePromises = cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file));
|
|
342
|
+
await Promise.all(deletePromises);
|
|
343
|
+
await ctx.database.remove("cave", { id: cave.id });
|
|
344
|
+
}
|
|
451
345
|
} catch (error) {
|
|
452
346
|
logger2.error("清理回声洞时发生错误:", error);
|
|
453
347
|
}
|
|
@@ -461,6 +355,25 @@ function getScopeQuery(session, config) {
|
|
|
461
355
|
return baseQuery;
|
|
462
356
|
}
|
|
463
357
|
__name(getScopeQuery, "getScopeQuery");
|
|
358
|
+
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));
|
|
361
|
+
let newId = 1;
|
|
362
|
+
while (existingIds.has(newId)) {
|
|
363
|
+
newId++;
|
|
364
|
+
}
|
|
365
|
+
return newId;
|
|
366
|
+
}
|
|
367
|
+
__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");
|
|
464
377
|
function checkCooldown(session, config, lastUsed) {
|
|
465
378
|
if (config.coolDown <= 0 || !session.channelId || config.adminUsers.includes(session.userId)) {
|
|
466
379
|
return null;
|
|
@@ -481,13 +394,106 @@ function updateCooldownTimestamp(session, config, lastUsed) {
|
|
|
481
394
|
}
|
|
482
395
|
__name(updateCooldownTimestamp, "updateCooldownTimestamp");
|
|
483
396
|
|
|
397
|
+
// src/DataManager.ts
|
|
398
|
+
var DataManager = class {
|
|
399
|
+
/**
|
|
400
|
+
* 创建一个 DataManager 实例。
|
|
401
|
+
* @param ctx - Koishi 上下文,用于数据库操作。
|
|
402
|
+
* @param config - 插件配置。
|
|
403
|
+
* @param fileManager - 文件管理器实例,用于读写导入/导出文件。
|
|
404
|
+
* @param logger - 日志记录器实例。
|
|
405
|
+
*/
|
|
406
|
+
constructor(ctx, config, fileManager, logger2) {
|
|
407
|
+
this.ctx = ctx;
|
|
408
|
+
this.config = config;
|
|
409
|
+
this.fileManager = fileManager;
|
|
410
|
+
this.logger = logger2;
|
|
411
|
+
}
|
|
412
|
+
static {
|
|
413
|
+
__name(this, "DataManager");
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* 注册与数据导入导出相关的 `.export` 和 `.import` 子命令。
|
|
417
|
+
* @param cave - 主 `cave` 命令的实例,用于挂载子命令。
|
|
418
|
+
*/
|
|
419
|
+
registerCommands(cave) {
|
|
420
|
+
cave.subcommand(".export", "导出回声洞数据").usage("将所有回声洞数据导出到 cave_export.json。").action(async ({ session }) => {
|
|
421
|
+
if (!this.config.adminUsers.includes(session.userId)) return "抱歉,你没有权限导出数据";
|
|
422
|
+
try {
|
|
423
|
+
await session.send("正在导出数据,请稍候...");
|
|
424
|
+
const resultMessage = await this.exportData();
|
|
425
|
+
return resultMessage;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
this.logger.error("导出数据时发生错误:", error);
|
|
428
|
+
return `导出失败: ${error.message}`;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
cave.subcommand(".import", "导入回声洞数据").usage("从 cave_import.json 中导入回声洞数据。").action(async ({ session }) => {
|
|
432
|
+
if (!this.config.adminUsers.includes(session.userId)) return "抱歉,你没有权限导入数据";
|
|
433
|
+
try {
|
|
434
|
+
await session.send("正在导入数据,请稍候...");
|
|
435
|
+
const resultMessage = await this.importData();
|
|
436
|
+
return resultMessage;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
this.logger.error("导入数据时发生错误:", error);
|
|
439
|
+
return `导入失败: ${error.message}`;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* 导出所有状态为 'active' 的回声洞数据。
|
|
445
|
+
* 数据将被序列化为 JSON 并保存到 `cave_export.json` 文件中。
|
|
446
|
+
* @returns 一个描述导出结果的字符串消息。
|
|
447
|
+
*/
|
|
448
|
+
async exportData() {
|
|
449
|
+
const fileName = "cave_export.json";
|
|
450
|
+
const cavesToExport = await this.ctx.database.get("cave", { status: "active" });
|
|
451
|
+
const portableCaves = cavesToExport.map(({ id, ...rest }) => rest);
|
|
452
|
+
const data = JSON.stringify(portableCaves, null, 2);
|
|
453
|
+
await this.fileManager.saveFile(fileName, Buffer.from(data));
|
|
454
|
+
return `成功导出 ${portableCaves.length} 条数据`;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* 从 `cave_import.json` 文件导入回声洞数据。
|
|
458
|
+
* @returns 一个描述导入结果的字符串消息。
|
|
459
|
+
*/
|
|
460
|
+
async importData() {
|
|
461
|
+
const fileName = "cave_import.json";
|
|
462
|
+
let importedCaves;
|
|
463
|
+
try {
|
|
464
|
+
const fileContent = await this.fileManager.readFile(fileName);
|
|
465
|
+
importedCaves = JSON.parse(fileContent.toString("utf-8"));
|
|
466
|
+
if (!Array.isArray(importedCaves)) {
|
|
467
|
+
throw new Error("导入文件格式无效");
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
this.logger.error(`读取导入文件失败:`, error);
|
|
471
|
+
return `读取导入文件失败: ${error.message || "未知错误"}`;
|
|
472
|
+
}
|
|
473
|
+
let successCount = 0;
|
|
474
|
+
for (const cave of importedCaves) {
|
|
475
|
+
const newId = await getNextCaveId(this.ctx, {});
|
|
476
|
+
const newCave = {
|
|
477
|
+
...cave,
|
|
478
|
+
id: newId,
|
|
479
|
+
channelId: cave.channelId || null,
|
|
480
|
+
// 确保 channelId 存在,若无则为 null。
|
|
481
|
+
status: "active"
|
|
482
|
+
// 导入的数据直接设为 active 状态。
|
|
483
|
+
};
|
|
484
|
+
await this.ctx.database.create("cave", newCave);
|
|
485
|
+
successCount++;
|
|
486
|
+
}
|
|
487
|
+
return `成功导入 ${successCount} 条回声洞数据`;
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
484
491
|
// src/ReviewManager.ts
|
|
485
|
-
var APPROVE_ACTIONS = /* @__PURE__ */ new Set(["y", "yes", "pass", "approve"]);
|
|
486
|
-
var REJECT_ACTIONS = /* @__PURE__ */ new Set(["n", "no", "deny", "reject"]);
|
|
487
492
|
var ReviewManager = class {
|
|
488
493
|
/**
|
|
494
|
+
* 创建一个 ReviewManager 实例。
|
|
489
495
|
* @param ctx - Koishi 上下文。
|
|
490
|
-
* @param config -
|
|
496
|
+
* @param config - 插件配置。
|
|
491
497
|
* @param fileManager - 文件管理器实例。
|
|
492
498
|
* @param logger - 日志记录器实例。
|
|
493
499
|
*/
|
|
@@ -502,7 +508,7 @@ var ReviewManager = class {
|
|
|
502
508
|
}
|
|
503
509
|
/**
|
|
504
510
|
* 注册与审核相关的 `.review` 子命令。
|
|
505
|
-
* @param cave - 主 `cave`
|
|
511
|
+
* @param cave - 主 `cave` 命令的实例,用于挂载子命令。
|
|
506
512
|
*/
|
|
507
513
|
registerCommands(cave) {
|
|
508
514
|
cave.subcommand(".review [id:posint] [action:string]", "审核回声洞").usage("查看或审核回声洞,使用 <Y/N> 进行审核。").action(async ({ session }, id, action) => {
|
|
@@ -510,46 +516,44 @@ var ReviewManager = class {
|
|
|
510
516
|
return "抱歉,你没有权限执行审核";
|
|
511
517
|
}
|
|
512
518
|
if (!id) {
|
|
513
|
-
|
|
519
|
+
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(", ");
|
|
524
|
+
return `当前共有 ${pendingCaves.length} 条待审核回声洞,序号为:
|
|
525
|
+
${pendingIds}`;
|
|
514
526
|
}
|
|
515
527
|
const [targetCave] = await this.ctx.database.get("cave", { id });
|
|
516
|
-
if (!targetCave)
|
|
517
|
-
|
|
518
|
-
if (!action) {
|
|
519
|
-
return this.buildReviewMessage(targetCave);
|
|
528
|
+
if (!targetCave) {
|
|
529
|
+
return `回声洞(${id})不存在`;
|
|
520
530
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
return this.processReview("approve", targetCave, session.username);
|
|
531
|
+
if (targetCave.status !== "pending") {
|
|
532
|
+
return `回声洞(${id})无需审核`;
|
|
524
533
|
}
|
|
525
|
-
if (
|
|
526
|
-
return this.
|
|
534
|
+
if (id && !action) {
|
|
535
|
+
return this.buildReviewMessage(targetCave);
|
|
527
536
|
}
|
|
528
|
-
|
|
537
|
+
const normalizedAction = action.toLowerCase();
|
|
538
|
+
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}"
|
|
529
545
|
请使用 "Y" (通过) 或 "N" (拒绝)`;
|
|
546
|
+
}
|
|
547
|
+
return this.processReview(reviewAction, id, session.username);
|
|
530
548
|
});
|
|
531
549
|
}
|
|
532
550
|
/**
|
|
533
|
-
*
|
|
534
|
-
* @private
|
|
535
|
-
*/
|
|
536
|
-
async listPendingCaves() {
|
|
537
|
-
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" });
|
|
538
|
-
if (pendingCaves.length === 0) {
|
|
539
|
-
return "当前没有需要审核的回声洞";
|
|
540
|
-
}
|
|
541
|
-
const pendingIds = pendingCaves.map((c) => c.id).join(", ");
|
|
542
|
-
return `当前共有 ${pendingCaves.length} 条待审核回声洞,序号为:
|
|
543
|
-
${pendingIds}`;
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* 将一条新回声洞提交给管理员进行审核。
|
|
547
|
-
* 如果没有配置管理员,将自动通过审核。
|
|
551
|
+
* 将一条新的回声洞提交给所有管理员进行审核。
|
|
548
552
|
* @param cave - 新创建的、状态为 'pending' 的回声洞对象。
|
|
549
553
|
*/
|
|
550
554
|
async sendForReview(cave) {
|
|
551
555
|
if (!this.config.adminUsers?.length) {
|
|
552
|
-
this.logger.warn(
|
|
556
|
+
this.logger.warn(`未配置管理员,回声洞(${cave.id})已自动通过审核`);
|
|
553
557
|
await this.ctx.database.upsert("cave", [{ id: cave.id, status: "active" }]);
|
|
554
558
|
return;
|
|
555
559
|
}
|
|
@@ -557,45 +561,55 @@ ${pendingIds}`;
|
|
|
557
561
|
try {
|
|
558
562
|
await this.ctx.broadcast(this.config.adminUsers, reviewMessage);
|
|
559
563
|
} catch (error) {
|
|
560
|
-
this.logger.error(
|
|
564
|
+
this.logger.error(`广播回声洞(${cave.id})审核请求失败:`, error);
|
|
561
565
|
}
|
|
562
566
|
}
|
|
563
567
|
/**
|
|
564
|
-
*
|
|
568
|
+
* 构建一条用于发送给管理员的、包含审核信息的消息。
|
|
565
569
|
* @param cave - 待审核的回声洞对象。
|
|
566
570
|
* @returns 一个可直接发送的消息数组。
|
|
567
571
|
* @private
|
|
568
572
|
*/
|
|
569
573
|
async buildReviewMessage(cave) {
|
|
570
574
|
const caveContent = await buildCaveMessage(cave, this.config, this.fileManager, this.logger);
|
|
571
|
-
return [
|
|
575
|
+
return [
|
|
576
|
+
`以下内容待审核:`,
|
|
577
|
+
...caveContent
|
|
578
|
+
];
|
|
572
579
|
}
|
|
573
580
|
/**
|
|
574
581
|
* 处理管理员的审核决定(通过或拒绝)。
|
|
575
582
|
* @param action - 'approve' (通过) 或 'reject' (拒绝)。
|
|
576
|
-
* @param
|
|
577
|
-
* @param adminUserName -
|
|
583
|
+
* @param caveId - 被审核的回声洞 ID。
|
|
584
|
+
* @param adminUserName - 执行操作的管理员的昵称。
|
|
578
585
|
* @returns 返回给操作者的确认消息。
|
|
579
586
|
*/
|
|
580
|
-
async processReview(action,
|
|
587
|
+
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})无需审核`;
|
|
581
591
|
let resultMessage;
|
|
582
592
|
let broadcastMessage;
|
|
583
593
|
if (action === "approve") {
|
|
584
|
-
await this.ctx.database.upsert("cave", [{ id:
|
|
585
|
-
resultMessage = `回声洞(${
|
|
586
|
-
broadcastMessage = `回声洞(${
|
|
594
|
+
await this.ctx.database.upsert("cave", [{ id: caveId, status: "active" }]);
|
|
595
|
+
resultMessage = `回声洞(${caveId})已通过`;
|
|
596
|
+
broadcastMessage = `回声洞(${caveId})已由管理员 "${adminUserName}" 通过`;
|
|
587
597
|
} else {
|
|
588
|
-
await this.ctx.database.upsert("cave", [{ id:
|
|
589
|
-
resultMessage = `回声洞(${cave.id})已拒绝`;
|
|
598
|
+
await this.ctx.database.upsert("cave", [{ id: caveId, status: "delete" }]);
|
|
590
599
|
const caveContent = await buildCaveMessage(cave, this.config, this.fileManager, this.logger);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
600
|
+
resultMessage = [
|
|
601
|
+
`回声洞(${caveId})已拒绝`,
|
|
602
|
+
...caveContent
|
|
603
|
+
];
|
|
604
|
+
broadcastMessage = [
|
|
605
|
+
`回声洞(${caveId})已由管理员 "${adminUserName}" 拒绝`,
|
|
606
|
+
...caveContent
|
|
607
|
+
];
|
|
608
|
+
cleanupPendingDeletions(this.ctx, this.fileManager, this.logger);
|
|
595
609
|
}
|
|
596
|
-
if (this.config.adminUsers?.length) {
|
|
597
|
-
this.ctx.broadcast(this.config.adminUsers, broadcastMessage).catch((err) => {
|
|
598
|
-
this.logger.error(
|
|
610
|
+
if (broadcastMessage && this.config.adminUsers?.length) {
|
|
611
|
+
await this.ctx.broadcast(this.config.adminUsers, broadcastMessage).catch((err) => {
|
|
612
|
+
this.logger.error(`广播回声洞(${cave.id})审核结果失败:`, err);
|
|
599
613
|
});
|
|
600
614
|
}
|
|
601
615
|
return resultMessage;
|
|
@@ -645,14 +659,22 @@ var Config = import_koishi2.Schema.intersect([
|
|
|
645
659
|
function apply(ctx, config) {
|
|
646
660
|
ctx.model.extend("cave", {
|
|
647
661
|
id: "unsigned",
|
|
662
|
+
// 无符号整数,作为主键。
|
|
648
663
|
elements: "json",
|
|
664
|
+
// 存储为 JSON 字符串的元素数组。
|
|
649
665
|
channelId: "string",
|
|
666
|
+
// 频道 ID。
|
|
650
667
|
userId: "string",
|
|
668
|
+
// 用户 ID。
|
|
651
669
|
userName: "string",
|
|
670
|
+
// 用户昵称。
|
|
652
671
|
status: "string",
|
|
672
|
+
// 回声洞状态。
|
|
653
673
|
time: "timestamp"
|
|
674
|
+
// 提交时间。
|
|
654
675
|
}, {
|
|
655
676
|
primary: "id"
|
|
677
|
+
// 将 'id' 字段设置为主键。
|
|
656
678
|
});
|
|
657
679
|
const fileManager = new FileManager(ctx.baseDir, config, logger);
|
|
658
680
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
@@ -674,40 +696,55 @@ function apply(ctx, config) {
|
|
|
674
696
|
}
|
|
675
697
|
const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
|
|
676
698
|
const [randomCave] = await ctx.database.get("cave", { ...query, id: randomId });
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
return buildCaveMessage(randomCave, config, fileManager, logger);
|
|
680
|
-
}
|
|
681
|
-
return "未能获取到回声洞";
|
|
699
|
+
updateCooldownTimestamp(session, config, lastUsed);
|
|
700
|
+
return buildCaveMessage(randomCave, config, fileManager, logger);
|
|
682
701
|
} catch (error) {
|
|
683
702
|
logger.error("随机获取回声洞失败:", error);
|
|
703
|
+
return "随机获取回声洞失败";
|
|
684
704
|
}
|
|
685
705
|
});
|
|
686
706
|
cave.subcommand(".add [content:text]", "添加回声洞").usage("添加一条回声洞。可以直接发送内容,也可以回复或引用一条消息。").action(async ({ session }, content) => {
|
|
707
|
+
const savedFileIdentifiers = [];
|
|
687
708
|
try {
|
|
688
|
-
let sourceElements
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
709
|
+
let sourceElements;
|
|
710
|
+
if (session.quote?.elements) {
|
|
711
|
+
sourceElements = session.quote.elements;
|
|
712
|
+
} else if (content?.trim()) {
|
|
713
|
+
sourceElements = import_koishi2.h.parse(content);
|
|
714
|
+
} else {
|
|
715
|
+
await session.send("请在一分钟内发送你要添加的内容");
|
|
716
|
+
const reply = await session.prompt(6e4);
|
|
717
|
+
if (!reply) return "操作超时,已取消添加";
|
|
718
|
+
sourceElements = import_koishi2.h.parse(reply);
|
|
699
719
|
}
|
|
700
720
|
const scopeQuery = getScopeQuery(session, config);
|
|
701
|
-
const
|
|
702
|
-
const
|
|
703
|
-
let
|
|
704
|
-
|
|
705
|
-
|
|
721
|
+
const newId = await getNextCaveId(ctx, scopeQuery);
|
|
722
|
+
const finalElementsForDb = [];
|
|
723
|
+
let mediaIndex = 0;
|
|
724
|
+
async function traverseAndProcess(elements) {
|
|
725
|
+
for (const el of elements) {
|
|
726
|
+
const elementType = el.type;
|
|
727
|
+
if (["image", "video", "audio", "file"].includes(elementType) && el.attrs.src) {
|
|
728
|
+
let fileIdentifier = el.attrs.src;
|
|
729
|
+
if (fileIdentifier.startsWith("http")) {
|
|
730
|
+
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;
|
|
735
|
+
}
|
|
736
|
+
finalElementsForDb.push({ type: elementType, file: fileIdentifier });
|
|
737
|
+
} else if (elementType === "text" && el.attrs.content?.trim()) {
|
|
738
|
+
finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
|
|
739
|
+
}
|
|
740
|
+
if (el.children) await traverseAndProcess(el.children);
|
|
741
|
+
}
|
|
706
742
|
}
|
|
707
|
-
|
|
743
|
+
__name(traverseAndProcess, "traverseAndProcess");
|
|
744
|
+
await traverseAndProcess(sourceElements);
|
|
708
745
|
if (finalElementsForDb.length === 0) return "内容为空,已取消添加";
|
|
709
746
|
let userName = session.username;
|
|
710
|
-
if (config.enableProfile
|
|
747
|
+
if (config.enableProfile) {
|
|
711
748
|
userName = await profileManager.getNickname(session.userId) || userName;
|
|
712
749
|
}
|
|
713
750
|
const newCave = {
|
|
@@ -720,24 +757,17 @@ function apply(ctx, config) {
|
|
|
720
757
|
time: /* @__PURE__ */ new Date()
|
|
721
758
|
};
|
|
722
759
|
await ctx.database.create("cave", newCave);
|
|
723
|
-
|
|
724
|
-
const downloadPromises = mediaToDownload.map(async (media) => {
|
|
725
|
-
const response = await ctx.http.get(media.url, { responseType: "arraybuffer", timeout: 3e4 });
|
|
726
|
-
await fileManager.saveFile(media.fileName, Buffer.from(response));
|
|
727
|
-
});
|
|
728
|
-
await Promise.all(downloadPromises);
|
|
729
|
-
} catch (fileError) {
|
|
730
|
-
await ctx.database.remove("cave", { id: newId });
|
|
731
|
-
logger.error("媒体文件存储失败:", fileError);
|
|
732
|
-
return "添加失败:媒体文件存储失败";
|
|
733
|
-
}
|
|
734
|
-
if (newCave.status === "pending" && reviewManager) {
|
|
760
|
+
if (newCave.status === "pending") {
|
|
735
761
|
reviewManager.sendForReview(newCave);
|
|
736
762
|
return `提交成功,序号为(${newCave.id})`;
|
|
737
763
|
}
|
|
738
764
|
return `添加成功,序号为(${newId})`;
|
|
739
765
|
} catch (error) {
|
|
740
766
|
logger.error("添加回声洞失败:", error);
|
|
767
|
+
if (savedFileIdentifiers.length > 0) {
|
|
768
|
+
logger.info(`添加失败,回滚并删除 ${savedFileIdentifiers.length} 个文件...`);
|
|
769
|
+
await Promise.all(savedFileIdentifiers.map((fileId) => fileManager.deleteFile(fileId)));
|
|
770
|
+
}
|
|
741
771
|
return "添加失败,请稍后再试";
|
|
742
772
|
}
|
|
743
773
|
});
|
|
@@ -748,7 +778,9 @@ function apply(ctx, config) {
|
|
|
748
778
|
try {
|
|
749
779
|
const query = { ...getScopeQuery(session, config), id };
|
|
750
780
|
const [targetCave] = await ctx.database.get("cave", query);
|
|
751
|
-
if (!targetCave)
|
|
781
|
+
if (!targetCave) {
|
|
782
|
+
return `回声洞(${id})不存在`;
|
|
783
|
+
}
|
|
752
784
|
updateCooldownTimestamp(session, config, lastUsed);
|
|
753
785
|
return buildCaveMessage(targetCave, config, fileManager, logger);
|
|
754
786
|
} catch (error) {
|
|
@@ -766,14 +798,15 @@ function apply(ctx, config) {
|
|
|
766
798
|
if (!isOwner && !isAdmin) {
|
|
767
799
|
return "抱歉,你没有权限删除这条回声洞";
|
|
768
800
|
}
|
|
769
|
-
const caveMessage = await buildCaveMessage(targetCave, config, fileManager, logger);
|
|
770
801
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
771
|
-
|
|
772
|
-
cleanupPendingDeletions(ctx, fileManager, logger)
|
|
773
|
-
|
|
774
|
-
|
|
802
|
+
const caveMessage = await buildCaveMessage(targetCave, config, fileManager, logger);
|
|
803
|
+
cleanupPendingDeletions(ctx, fileManager, logger);
|
|
804
|
+
return [
|
|
805
|
+
`以下内容已删除`,
|
|
806
|
+
...caveMessage
|
|
807
|
+
];
|
|
775
808
|
} catch (error) {
|
|
776
|
-
logger.error(
|
|
809
|
+
logger.error(`标记回声洞(${id})失败:`, error);
|
|
777
810
|
return "删除失败,请稍后再试";
|
|
778
811
|
}
|
|
779
812
|
});
|