koishi-plugin-best-cave 2.1.0 → 2.1.2
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 +38 -0
- package/lib/ReviewManager.d.ts +40 -0
- package/lib/Utils.d.ts +8 -5
- package/lib/index.js +168 -106
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context, Logger } from 'koishi';
|
|
2
|
+
import { FileManager } from './FileManager';
|
|
3
|
+
import { Config } from './index';
|
|
4
|
+
/**
|
|
5
|
+
* @class DataManager
|
|
6
|
+
* @description 负责处理回声洞数据的导入和导出功能。
|
|
7
|
+
*/
|
|
8
|
+
export declare class DataManager {
|
|
9
|
+
private ctx;
|
|
10
|
+
private config;
|
|
11
|
+
private fileManager;
|
|
12
|
+
private logger;
|
|
13
|
+
private reusableIds;
|
|
14
|
+
/**
|
|
15
|
+
* @constructor
|
|
16
|
+
* @param ctx Koishi 上下文,用于数据库操作。
|
|
17
|
+
* @param config 插件配置。
|
|
18
|
+
* @param fileManager 文件管理器实例。
|
|
19
|
+
* @param logger 日志记录器实例。
|
|
20
|
+
* @param reusableIds 可复用 ID 的内存缓存。
|
|
21
|
+
*/
|
|
22
|
+
constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reusableIds: Set<number>);
|
|
23
|
+
/**
|
|
24
|
+
* @description 注册 `.export` 和 `.import` 子命令。
|
|
25
|
+
* @param cave - 主 `cave` 命令实例。
|
|
26
|
+
*/
|
|
27
|
+
registerCommands(cave: any): void;
|
|
28
|
+
/**
|
|
29
|
+
* @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
|
|
30
|
+
* @returns 描述导出结果的消息字符串。
|
|
31
|
+
*/
|
|
32
|
+
exportData(): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* @description 从 `cave_import.json` 文件导入回声洞数据。
|
|
35
|
+
* @returns 描述导入结果的消息字符串。
|
|
36
|
+
*/
|
|
37
|
+
importData(): Promise<string>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Context, Logger } from 'koishi';
|
|
2
|
+
import { CaveObject, Config } from './index';
|
|
3
|
+
import { FileManager } from './FileManager';
|
|
4
|
+
/**
|
|
5
|
+
* @class ReviewManager
|
|
6
|
+
* @description 负责处理回声洞的审核流程,处理新洞的提交、审核通知和审核操作。
|
|
7
|
+
*/
|
|
8
|
+
export declare class ReviewManager {
|
|
9
|
+
private ctx;
|
|
10
|
+
private config;
|
|
11
|
+
private fileManager;
|
|
12
|
+
private logger;
|
|
13
|
+
private reusableIds;
|
|
14
|
+
/**
|
|
15
|
+
* @constructor
|
|
16
|
+
* @param ctx Koishi 上下文。
|
|
17
|
+
* @param config 插件配置。
|
|
18
|
+
* @param fileManager 文件管理器实例。
|
|
19
|
+
* @param logger 日志记录器实例。
|
|
20
|
+
* @param reusableIds 可复用 ID 的内存缓存。
|
|
21
|
+
*/
|
|
22
|
+
constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reusableIds: Set<number>);
|
|
23
|
+
/**
|
|
24
|
+
* @description 注册与审核相关的子命令。
|
|
25
|
+
* @param cave - 主 `cave` 命令实例。
|
|
26
|
+
*/
|
|
27
|
+
registerCommands(cave: any): void;
|
|
28
|
+
/**
|
|
29
|
+
* @description 将新回声洞提交到管理群组以供审核。
|
|
30
|
+
* @param cave 新创建的、状态为 'pending' 的回声洞对象。
|
|
31
|
+
*/
|
|
32
|
+
sendForReview(cave: CaveObject): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* @description 处理管理员的审核决定(通过或拒绝)。
|
|
35
|
+
* @param action 'approve' (通过) 或 'reject' (拒绝)。
|
|
36
|
+
* @param caveId 被审核的回声洞 ID。
|
|
37
|
+
* @returns 返回给操作者的确认消息。
|
|
38
|
+
*/
|
|
39
|
+
processReview(action: 'approve' | 'reject', caveId: number): Promise<string>;
|
|
40
|
+
}
|
package/lib/Utils.d.ts
CHANGED
|
@@ -22,8 +22,9 @@ export declare function buildCaveMessage(cave: CaveObject, config: Config, fileM
|
|
|
22
22
|
* @param ctx Koishi 上下文。
|
|
23
23
|
* @param fileManager FileManager 实例。
|
|
24
24
|
* @param logger Logger 实例。
|
|
25
|
+
* @param reusableIds 可复用 ID 的内存缓存。
|
|
25
26
|
*/
|
|
26
|
-
export declare function cleanupPendingDeletions(ctx: Context, fileManager: FileManager, logger: Logger): Promise<void>;
|
|
27
|
+
export declare function cleanupPendingDeletions(ctx: Context, fileManager: FileManager, logger: Logger, reusableIds: Set<number>): Promise<void>;
|
|
27
28
|
/**
|
|
28
29
|
* @description 根据配置(是否分群)和当前会话,生成数据库查询的范围条件。
|
|
29
30
|
* @param session 当前会话对象。
|
|
@@ -32,13 +33,14 @@ export declare function cleanupPendingDeletions(ctx: Context, fileManager: FileM
|
|
|
32
33
|
*/
|
|
33
34
|
export declare function getScopeQuery(session: Session, config: Config): object;
|
|
34
35
|
/**
|
|
35
|
-
* @description 获取下一个可用的回声洞 ID
|
|
36
|
+
* @description 获取下一个可用的回声洞 ID。
|
|
37
|
+
* 实现了三阶段逻辑:优先使用回收ID -> 扫描空闲ID -> 获取最大ID+1。
|
|
36
38
|
* @param ctx Koishi 上下文。
|
|
37
39
|
* @param query 查询范围条件,用于分群模式。
|
|
40
|
+
* @param reusableIds 可复用 ID 的内存缓存。
|
|
38
41
|
* @returns 可用的新 ID。
|
|
39
|
-
* @performance 在大数据集下,此函数可能存在性能瓶颈,因为它需要获取所有现有ID。
|
|
40
42
|
*/
|
|
41
|
-
export declare function getNextCaveId(ctx: Context, query
|
|
43
|
+
export declare function getNextCaveId(ctx: Context, query: object, reusableIds: Set<number>): Promise<number>;
|
|
42
44
|
/**
|
|
43
45
|
* @description 检查用户是否处于指令冷却中。
|
|
44
46
|
* @returns 若在冷却中则返回提示字符串,否则返回 null。
|
|
@@ -72,8 +74,9 @@ export declare function processMessageElements(sourceElements: h[], newId: numbe
|
|
|
72
74
|
* @param reviewManager - 审核管理器实例 (可能为 null)。
|
|
73
75
|
* @param cave - 已创建的、状态为 'preload' 的回声洞对象。
|
|
74
76
|
* @param mediaToSave - 需要下载和保存的媒体文件列表。
|
|
77
|
+
* @param reusableIds 可复用 ID 的内存缓存。
|
|
75
78
|
*/
|
|
76
79
|
export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reviewManager: any, cave: CaveObject, mediaToSave: {
|
|
77
80
|
sourceUrl: string;
|
|
78
81
|
fileName: string;
|
|
79
|
-
}[]): Promise<void>;
|
|
82
|
+
}[], reusableIds: Set<number>): Promise<void>;
|
package/lib/index.js
CHANGED
|
@@ -212,10 +212,98 @@ var ProfileManager = class {
|
|
|
212
212
|
}
|
|
213
213
|
};
|
|
214
214
|
|
|
215
|
+
// src/DataManager.ts
|
|
216
|
+
var DataManager = class {
|
|
217
|
+
/**
|
|
218
|
+
* @constructor
|
|
219
|
+
* @param ctx Koishi 上下文,用于数据库操作。
|
|
220
|
+
* @param config 插件配置。
|
|
221
|
+
* @param fileManager 文件管理器实例。
|
|
222
|
+
* @param logger 日志记录器实例。
|
|
223
|
+
* @param reusableIds 可复用 ID 的内存缓存。
|
|
224
|
+
*/
|
|
225
|
+
constructor(ctx, config, fileManager, logger2, reusableIds) {
|
|
226
|
+
this.ctx = ctx;
|
|
227
|
+
this.config = config;
|
|
228
|
+
this.fileManager = fileManager;
|
|
229
|
+
this.logger = logger2;
|
|
230
|
+
this.reusableIds = reusableIds;
|
|
231
|
+
}
|
|
232
|
+
static {
|
|
233
|
+
__name(this, "DataManager");
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* @description 注册 `.export` 和 `.import` 子命令。
|
|
237
|
+
* @param cave - 主 `cave` 命令实例。
|
|
238
|
+
*/
|
|
239
|
+
registerCommands(cave) {
|
|
240
|
+
const requireAdmin = /* @__PURE__ */ __name((action) => async ({ session }) => {
|
|
241
|
+
const adminChannelId = this.config.adminChannel?.split(":")[1];
|
|
242
|
+
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
243
|
+
try {
|
|
244
|
+
await session.send("正在处理,请稍候...");
|
|
245
|
+
return await action();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.logger.error("数据操作时发生错误:", error);
|
|
248
|
+
return `操作失败: ${error.message}`;
|
|
249
|
+
}
|
|
250
|
+
}, "requireAdmin");
|
|
251
|
+
cave.subcommand(".export", "导出回声洞数据").usage("将所有回声洞数据导出到 cave_export.json。").action(requireAdmin(() => this.exportData()));
|
|
252
|
+
cave.subcommand(".import", "导入回声洞数据").usage("从 cave_import.json 中导入回声洞数据。").action(requireAdmin(() => this.importData()));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
|
|
256
|
+
* @returns 描述导出结果的消息字符串。
|
|
257
|
+
*/
|
|
258
|
+
async exportData() {
|
|
259
|
+
const fileName = "cave_export.json";
|
|
260
|
+
const cavesToExport = await this.ctx.database.get("cave", { status: "active" });
|
|
261
|
+
const portableCaves = cavesToExport.map(({ id, ...rest }) => rest);
|
|
262
|
+
const data = JSON.stringify(portableCaves, null, 2);
|
|
263
|
+
await this.fileManager.saveFile(fileName, Buffer.from(data));
|
|
264
|
+
return `成功导出 ${portableCaves.length} 条数据`;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* @description 从 `cave_import.json` 文件导入回声洞数据。
|
|
268
|
+
* @returns 描述导入结果的消息字符串。
|
|
269
|
+
*/
|
|
270
|
+
async importData() {
|
|
271
|
+
const fileName = "cave_import.json";
|
|
272
|
+
let importedCaves;
|
|
273
|
+
try {
|
|
274
|
+
const fileContent = await this.fileManager.readFile(fileName);
|
|
275
|
+
importedCaves = JSON.parse(fileContent.toString("utf-8"));
|
|
276
|
+
if (!Array.isArray(importedCaves)) throw new Error("文件格式无效");
|
|
277
|
+
if (!importedCaves.length) throw new Error("导入文件为空");
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.logger.error(`读取导入文件失败:`, error);
|
|
280
|
+
throw new Error(`读取导入文件失败: ${error.message}`);
|
|
281
|
+
}
|
|
282
|
+
const [lastCave] = await this.ctx.database.get("cave", {}, {
|
|
283
|
+
sort: { id: "desc" },
|
|
284
|
+
limit: 1,
|
|
285
|
+
fields: ["id"]
|
|
286
|
+
});
|
|
287
|
+
let startId = (lastCave?.id || 0) + 1;
|
|
288
|
+
const newCavesToInsert = importedCaves.map((cave, index) => ({
|
|
289
|
+
...cave,
|
|
290
|
+
id: startId + index,
|
|
291
|
+
status: "active"
|
|
292
|
+
}));
|
|
293
|
+
await this.ctx.database.upsert("cave", newCavesToInsert);
|
|
294
|
+
this.reusableIds.clear();
|
|
295
|
+
return `成功导入 ${newCavesToInsert.length} 条数据`;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/ReviewManager.ts
|
|
300
|
+
var import_koishi2 = require("koishi");
|
|
301
|
+
|
|
215
302
|
// src/Utils.ts
|
|
216
303
|
var import_koishi = require("koishi");
|
|
217
304
|
var path2 = __toESM(require("path"));
|
|
218
305
|
var mimeTypeMap = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".webp": "image/webp" };
|
|
306
|
+
var MAX_ID_FLAG = 0;
|
|
219
307
|
function storedFormatToHElements(elements) {
|
|
220
308
|
return elements.map((el) => {
|
|
221
309
|
if (el.type === "text") return import_koishi.h.text(el.content);
|
|
@@ -256,13 +344,15 @@ async function buildCaveMessage(cave, config, fileManager, logger2) {
|
|
|
256
344
|
return finalMessage;
|
|
257
345
|
}
|
|
258
346
|
__name(buildCaveMessage, "buildCaveMessage");
|
|
259
|
-
async function cleanupPendingDeletions(ctx, fileManager, logger2) {
|
|
347
|
+
async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
|
|
260
348
|
try {
|
|
261
349
|
const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
|
|
262
350
|
if (!cavesToDelete.length) return;
|
|
263
351
|
for (const cave of cavesToDelete) {
|
|
264
352
|
const deletePromises = cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file));
|
|
265
353
|
await Promise.all(deletePromises);
|
|
354
|
+
reusableIds.add(cave.id);
|
|
355
|
+
reusableIds.delete(MAX_ID_FLAG);
|
|
266
356
|
await ctx.database.remove("cave", { id: cave.id });
|
|
267
357
|
}
|
|
268
358
|
} catch (error) {
|
|
@@ -275,13 +365,34 @@ function getScopeQuery(session, config) {
|
|
|
275
365
|
return config.perChannel && session.channelId ? { ...baseQuery, channelId: session.channelId } : baseQuery;
|
|
276
366
|
}
|
|
277
367
|
__name(getScopeQuery, "getScopeQuery");
|
|
278
|
-
async function getNextCaveId(ctx, query = {}) {
|
|
368
|
+
async function getNextCaveId(ctx, query = {}, reusableIds) {
|
|
369
|
+
for (const id of reusableIds) {
|
|
370
|
+
if (id > MAX_ID_FLAG) {
|
|
371
|
+
reusableIds.delete(id);
|
|
372
|
+
return id;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (reusableIds.has(MAX_ID_FLAG)) {
|
|
376
|
+
reusableIds.delete(MAX_ID_FLAG);
|
|
377
|
+
const [lastCave] = await ctx.database.get("cave", query, {
|
|
378
|
+
fields: ["id"],
|
|
379
|
+
sort: { id: "desc" },
|
|
380
|
+
limit: 1
|
|
381
|
+
});
|
|
382
|
+
const newId2 = (lastCave?.id || 0) + 1;
|
|
383
|
+
reusableIds.add(MAX_ID_FLAG);
|
|
384
|
+
return newId2;
|
|
385
|
+
}
|
|
279
386
|
const allCaveIds = (await ctx.database.get("cave", query, { fields: ["id"] })).map((c) => c.id);
|
|
280
387
|
const existingIds = new Set(allCaveIds);
|
|
281
388
|
let newId = 1;
|
|
282
389
|
while (existingIds.has(newId)) {
|
|
283
390
|
newId++;
|
|
284
391
|
}
|
|
392
|
+
const maxIdInDb = allCaveIds.length > 0 ? Math.max(...allCaveIds) : 0;
|
|
393
|
+
if (existingIds.size === maxIdInDb) {
|
|
394
|
+
reusableIds.add(MAX_ID_FLAG);
|
|
395
|
+
}
|
|
285
396
|
return newId;
|
|
286
397
|
}
|
|
287
398
|
__name(getNextCaveId, "getNextCaveId");
|
|
@@ -339,7 +450,7 @@ async function processMessageElements(sourceElements, newId, channelId, userId)
|
|
|
339
450
|
return { finalElementsForDb, mediaToSave };
|
|
340
451
|
}
|
|
341
452
|
__name(processMessageElements, "processMessageElements");
|
|
342
|
-
async function handleFileUploads(ctx, config, fileManager, logger2, reviewManager, cave, mediaToSave) {
|
|
453
|
+
async function handleFileUploads(ctx, config, fileManager, logger2, reviewManager, cave, mediaToSave, reusableIds) {
|
|
343
454
|
try {
|
|
344
455
|
const uploadPromises = mediaToSave.map(async (media) => {
|
|
345
456
|
const response = await ctx.http.get(media.sourceUrl, { responseType: "arraybuffer", timeout: 3e4 });
|
|
@@ -355,88 +466,12 @@ async function handleFileUploads(ctx, config, fileManager, logger2, reviewManage
|
|
|
355
466
|
} catch (fileSaveError) {
|
|
356
467
|
logger2.error(`回声洞(${cave.id})文件保存失败:`, fileSaveError);
|
|
357
468
|
await ctx.database.upsert("cave", [{ id: cave.id, status: "delete" }]);
|
|
358
|
-
cleanupPendingDeletions(ctx, fileManager, logger2);
|
|
469
|
+
cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds);
|
|
359
470
|
}
|
|
360
471
|
}
|
|
361
472
|
__name(handleFileUploads, "handleFileUploads");
|
|
362
473
|
|
|
363
|
-
// src/DataManager.ts
|
|
364
|
-
var DataManager = class {
|
|
365
|
-
/**
|
|
366
|
-
* @constructor
|
|
367
|
-
* @param ctx Koishi 上下文,用于数据库操作。
|
|
368
|
-
* @param config 插件配置。
|
|
369
|
-
* @param fileManager 文件管理器实例。
|
|
370
|
-
* @param logger 日志记录器实例。
|
|
371
|
-
*/
|
|
372
|
-
constructor(ctx, config, fileManager, logger2) {
|
|
373
|
-
this.ctx = ctx;
|
|
374
|
-
this.config = config;
|
|
375
|
-
this.fileManager = fileManager;
|
|
376
|
-
this.logger = logger2;
|
|
377
|
-
}
|
|
378
|
-
static {
|
|
379
|
-
__name(this, "DataManager");
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* @description 注册 `.export` 和 `.import` 子命令。
|
|
383
|
-
* @param cave - 主 `cave` 命令实例。
|
|
384
|
-
*/
|
|
385
|
-
registerCommands(cave) {
|
|
386
|
-
const requireAdmin = /* @__PURE__ */ __name((action) => async ({ session }) => {
|
|
387
|
-
const adminChannelId = this.config.adminChannel?.split(":")[1];
|
|
388
|
-
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
389
|
-
try {
|
|
390
|
-
await session.send("正在处理,请稍候...");
|
|
391
|
-
return await action();
|
|
392
|
-
} catch (error) {
|
|
393
|
-
this.logger.error("数据操作时发生错误:", error);
|
|
394
|
-
return `操作失败: ${error.message || "未知错误"}`;
|
|
395
|
-
}
|
|
396
|
-
}, "requireAdmin");
|
|
397
|
-
cave.subcommand(".export", "导出回声洞数据").usage("将所有回声洞数据导出到 cave_export.json。").action(requireAdmin(() => this.exportData()));
|
|
398
|
-
cave.subcommand(".import", "导入回声洞数据").usage("从 cave_import.json 中导入回声洞数据。").action(requireAdmin(() => this.importData()));
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
|
|
402
|
-
* @returns 描述导出结果的消息字符串。
|
|
403
|
-
*/
|
|
404
|
-
async exportData() {
|
|
405
|
-
const fileName = "cave_export.json";
|
|
406
|
-
const cavesToExport = await this.ctx.database.get("cave", { status: "active" });
|
|
407
|
-
const portableCaves = cavesToExport.map(({ id, ...rest }) => rest);
|
|
408
|
-
const data = JSON.stringify(portableCaves, null, 2);
|
|
409
|
-
await this.fileManager.saveFile(fileName, Buffer.from(data));
|
|
410
|
-
return `成功导出 ${portableCaves.length} 条数据`;
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* @description 从 `cave_import.json` 文件导入回声洞数据。
|
|
414
|
-
* @returns 描述导入结果的消息字符串。
|
|
415
|
-
*/
|
|
416
|
-
async importData() {
|
|
417
|
-
const fileName = "cave_import.json";
|
|
418
|
-
let importedCaves;
|
|
419
|
-
try {
|
|
420
|
-
const fileContent = await this.fileManager.readFile(fileName);
|
|
421
|
-
importedCaves = JSON.parse(fileContent.toString("utf-8"));
|
|
422
|
-
if (!Array.isArray(importedCaves)) throw new Error("导入文件格式无效");
|
|
423
|
-
} catch (error) {
|
|
424
|
-
this.logger.error(`读取导入文件失败:`, error);
|
|
425
|
-
throw new Error(`读取导入文件失败: ${error.message || "未知错误"}`);
|
|
426
|
-
}
|
|
427
|
-
let successCount = 0;
|
|
428
|
-
for (const cave of importedCaves) {
|
|
429
|
-
const newId = await getNextCaveId(this.ctx, {});
|
|
430
|
-
const newCave = { ...cave, id: newId, status: "active" };
|
|
431
|
-
await this.ctx.database.create("cave", newCave);
|
|
432
|
-
successCount++;
|
|
433
|
-
}
|
|
434
|
-
return `成功导入 ${successCount} 条回声洞数据`;
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
474
|
// src/ReviewManager.ts
|
|
439
|
-
var import_koishi2 = require("koishi");
|
|
440
475
|
var ReviewManager = class {
|
|
441
476
|
/**
|
|
442
477
|
* @constructor
|
|
@@ -444,46 +479,69 @@ var ReviewManager = class {
|
|
|
444
479
|
* @param config 插件配置。
|
|
445
480
|
* @param fileManager 文件管理器实例。
|
|
446
481
|
* @param logger 日志记录器实例。
|
|
482
|
+
* @param reusableIds 可复用 ID 的内存缓存。
|
|
447
483
|
*/
|
|
448
|
-
constructor(ctx, config, fileManager, logger2) {
|
|
484
|
+
constructor(ctx, config, fileManager, logger2, reusableIds) {
|
|
449
485
|
this.ctx = ctx;
|
|
450
486
|
this.config = config;
|
|
451
487
|
this.fileManager = fileManager;
|
|
452
488
|
this.logger = logger2;
|
|
489
|
+
this.reusableIds = reusableIds;
|
|
453
490
|
}
|
|
454
491
|
static {
|
|
455
492
|
__name(this, "ReviewManager");
|
|
456
493
|
}
|
|
457
494
|
/**
|
|
458
|
-
* @description
|
|
495
|
+
* @description 注册与审核相关的子命令。
|
|
459
496
|
* @param cave - 主 `cave` 命令实例。
|
|
460
497
|
*/
|
|
461
498
|
registerCommands(cave) {
|
|
462
|
-
|
|
499
|
+
const requireAdmin = /* @__PURE__ */ __name((session) => {
|
|
463
500
|
const adminChannelId = this.config.adminChannel?.split(":")[1];
|
|
464
|
-
if (session.channelId !== adminChannelId)
|
|
501
|
+
if (session.channelId !== adminChannelId) {
|
|
502
|
+
return "此指令仅限在管理群组中使用";
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}, "requireAdmin");
|
|
506
|
+
const review = cave.subcommand(".review [id:posint]", "审核回声洞").usage("查看所有待审核回声洞,或查看指定待审核回声洞。").action(async ({ session }, id) => {
|
|
507
|
+
const adminError = requireAdmin(session);
|
|
508
|
+
if (adminError) return adminError;
|
|
465
509
|
if (!id) {
|
|
466
510
|
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" }, { fields: ["id"] });
|
|
467
511
|
if (!pendingCaves.length) return "当前没有需要审核的回声洞";
|
|
468
512
|
return `当前共有 ${pendingCaves.length} 条待审核回声洞,序号为:
|
|
469
|
-
${pendingCaves.map((c) => c.id).join("
|
|
513
|
+
${pendingCaves.map((c) => c.id).join("|")}`;
|
|
470
514
|
}
|
|
471
515
|
const [targetCave] = await this.ctx.database.get("cave", { id });
|
|
472
516
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
473
517
|
if (targetCave.status !== "pending") return `回声洞(${id})无需审核`;
|
|
474
|
-
|
|
475
|
-
return [`待审核:`, ...await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger)];
|
|
476
|
-
}
|
|
477
|
-
const normalizedAction = action.toLowerCase();
|
|
478
|
-
if (["y", "yes", "pass", "approve"].includes(normalizedAction)) {
|
|
479
|
-
return this.processReview("approve", id);
|
|
480
|
-
}
|
|
481
|
-
if (["n", "no", "deny", "reject"].includes(normalizedAction)) {
|
|
482
|
-
return this.processReview("reject", id);
|
|
483
|
-
}
|
|
484
|
-
return `无效操作: "${action}"
|
|
485
|
-
请使用 "Y" (通过) 或 "N" (拒绝)`;
|
|
518
|
+
return [`待审核:`, ...await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger)];
|
|
486
519
|
});
|
|
520
|
+
const createReviewAction = /* @__PURE__ */ __name((actionType) => async ({ session }, id) => {
|
|
521
|
+
const adminError = requireAdmin(session);
|
|
522
|
+
if (adminError) return adminError;
|
|
523
|
+
await session.send("正在处理,请稍候...");
|
|
524
|
+
try {
|
|
525
|
+
if (!id) {
|
|
526
|
+
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" });
|
|
527
|
+
if (!pendingCaves.length) return `当前没有需要${actionType === "approve" ? "通过" : "拒绝"}的回声洞`;
|
|
528
|
+
if (actionType === "approve") {
|
|
529
|
+
await this.ctx.database.upsert("cave", pendingCaves.map((c) => ({ id: c.id, status: "active" })));
|
|
530
|
+
return `已通过 ${pendingCaves.length} 条回声洞`;
|
|
531
|
+
} else {
|
|
532
|
+
await this.ctx.database.upsert("cave", pendingCaves.map((c) => ({ id: c.id, status: "delete" })));
|
|
533
|
+
cleanupPendingDeletions(this.ctx, this.fileManager, this.logger, this.reusableIds);
|
|
534
|
+
return `已拒绝 ${pendingCaves.length} 条回声洞`;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return this.processReview(actionType, id);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
this.logger.error(`审核操作失败:`, error);
|
|
540
|
+
return `操作失败: ${error.message}`;
|
|
541
|
+
}
|
|
542
|
+
}, "createReviewAction");
|
|
543
|
+
review.subcommand(".Y [id:posint]", "通过审核").usage("通过回声洞审核,可批量操作。").action(createReviewAction("approve"));
|
|
544
|
+
review.subcommand(".N [id:posint]", "拒绝审核").usage("拒绝回声洞审核,可批量操作。").action(createReviewAction("reject"));
|
|
487
545
|
}
|
|
488
546
|
/**
|
|
489
547
|
* @description 将新回声洞提交到管理群组以供审核。
|
|
@@ -516,7 +574,7 @@ ${pendingCaves.map((c) => c.id).join(", ")}`;
|
|
|
516
574
|
return `回声洞(${caveId})已通过`;
|
|
517
575
|
} else {
|
|
518
576
|
await this.ctx.database.upsert("cave", [{ id: caveId, status: "delete" }]);
|
|
519
|
-
cleanupPendingDeletions(this.ctx, this.fileManager, this.logger);
|
|
577
|
+
cleanupPendingDeletions(this.ctx, this.fileManager, this.logger, this.reusableIds);
|
|
520
578
|
return `回声洞(${caveId})已拒绝`;
|
|
521
579
|
}
|
|
522
580
|
}
|
|
@@ -573,10 +631,11 @@ function apply(ctx, config) {
|
|
|
573
631
|
}, { primary: "id" });
|
|
574
632
|
const fileManager = new FileManager(ctx.baseDir, config, logger);
|
|
575
633
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
634
|
+
const reusableIds = /* @__PURE__ */ new Set();
|
|
576
635
|
const profileManager = config.enableProfile ? new ProfileManager(ctx) : null;
|
|
577
|
-
const dataManager = config.enableIO ? new DataManager(ctx, config, fileManager, logger) : null;
|
|
578
|
-
const reviewManager = config.enableReview ? new ReviewManager(ctx, config, fileManager, logger) : null;
|
|
579
|
-
const cave = ctx.command("cave", "回声洞").option("add", "-a <content:text>").option("view", "-g <id:posint>").option("delete", "-r <id:posint>").option("list", "-l").usage("随机抽取一条已添加的回声洞。").action(async ({ session, options }) => {
|
|
636
|
+
const dataManager = config.enableIO ? new DataManager(ctx, config, fileManager, logger, reusableIds) : null;
|
|
637
|
+
const reviewManager = config.enableReview ? new ReviewManager(ctx, config, fileManager, logger, reusableIds) : null;
|
|
638
|
+
const cave = ctx.command("cave", "回声洞").option("add", "-a <content:text> 添加回声洞").option("view", "-g <id:posint> 查看指定回声洞").option("delete", "-r <id:posint> 删除指定回声洞").option("list", "-l 查询投稿统计").usage("随机抽取一条已添加的回声洞。").action(async ({ session, options }) => {
|
|
580
639
|
if (options.add) return session.execute(`cave.add ${options.add}`);
|
|
581
640
|
if (options.view) return session.execute(`cave.view ${options.view}`);
|
|
582
641
|
if (options.delete) return session.execute(`cave.del ${options.delete}`);
|
|
@@ -611,14 +670,16 @@ function apply(ctx, config) {
|
|
|
611
670
|
sourceElements = import_koishi3.h.parse(reply);
|
|
612
671
|
}
|
|
613
672
|
const idScopeQuery = config.perChannel && session.channelId ? { channelId: session.channelId } : {};
|
|
614
|
-
const newId = await getNextCaveId(ctx, idScopeQuery);
|
|
673
|
+
const newId = await getNextCaveId(ctx, idScopeQuery, reusableIds);
|
|
615
674
|
const { finalElementsForDb, mediaToSave } = await processMessageElements(
|
|
616
675
|
sourceElements,
|
|
617
676
|
newId,
|
|
618
677
|
session.channelId,
|
|
619
678
|
session.userId
|
|
620
679
|
);
|
|
621
|
-
if (finalElementsForDb.length === 0)
|
|
680
|
+
if (finalElementsForDb.length === 0) {
|
|
681
|
+
return "内容为空,已取消添加";
|
|
682
|
+
}
|
|
622
683
|
const userName = (config.enableProfile ? await profileManager.getNickname(session.userId) : null) || session.username;
|
|
623
684
|
const hasMedia = mediaToSave.length > 0;
|
|
624
685
|
const initialStatus = hasMedia ? "preload" : config.enableReview ? "pending" : "active";
|
|
@@ -633,11 +694,12 @@ function apply(ctx, config) {
|
|
|
633
694
|
};
|
|
634
695
|
await ctx.database.create("cave", newCave);
|
|
635
696
|
if (hasMedia) {
|
|
636
|
-
handleFileUploads(ctx, config, fileManager, logger, reviewManager, newCave, mediaToSave);
|
|
697
|
+
handleFileUploads(ctx, config, fileManager, logger, reviewManager, newCave, mediaToSave, reusableIds);
|
|
637
698
|
} else if (initialStatus === "pending") {
|
|
638
699
|
reviewManager.sendForReview(newCave);
|
|
639
700
|
}
|
|
640
|
-
|
|
701
|
+
const responseMessage = initialStatus === "pending" || initialStatus === "preload" && config.enableReview ? `提交成功,序号为(${newId})` : `添加成功,序号为(${newId})`;
|
|
702
|
+
return responseMessage;
|
|
641
703
|
} catch (error) {
|
|
642
704
|
logger.error("添加回声洞失败:", error);
|
|
643
705
|
return "添加失败,请稍后再试";
|
|
@@ -671,7 +733,7 @@ function apply(ctx, config) {
|
|
|
671
733
|
}
|
|
672
734
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
673
735
|
const caveMessage = await buildCaveMessage(targetCave, config, fileManager, logger);
|
|
674
|
-
cleanupPendingDeletions(ctx, fileManager, logger);
|
|
736
|
+
cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
|
|
675
737
|
return [`已删除`, ...caveMessage];
|
|
676
738
|
} catch (error) {
|
|
677
739
|
logger.error(`标记回声洞(${id})失败:`, error);
|