koishi-plugin-best-cave 2.0.9 → 2.1.0
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/Utils.d.ts +0 -5
- package/lib/index.js +107 -166
- package/package.json +1 -1
- package/lib/DataManager.d.ts +0 -36
- package/lib/FileManager.d.ts +0 -48
- package/lib/ProfileManager.d.ts +0 -50
- package/lib/ReviewManager.d.ts +0 -38
- package/lib/index.d.ts +0 -48
package/lib/Utils.d.ts
CHANGED
|
@@ -39,11 +39,6 @@ export declare function getScopeQuery(session: Session, config: Config): object;
|
|
|
39
39
|
* @performance 在大数据集下,此函数可能存在性能瓶颈,因为它需要获取所有现有ID。
|
|
40
40
|
*/
|
|
41
41
|
export declare function getNextCaveId(ctx: Context, query?: object): Promise<number>;
|
|
42
|
-
/**
|
|
43
|
-
* @description 下载网络媒体资源并保存到文件存储中。
|
|
44
|
-
* @returns 保存后的文件名/标识符。
|
|
45
|
-
*/
|
|
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>;
|
|
47
42
|
/**
|
|
48
43
|
* @description 检查用户是否处于指令冷却中。
|
|
49
44
|
* @returns 若在冷却中则返回提示字符串,否则返回 null。
|
package/lib/index.js
CHANGED
|
@@ -102,7 +102,6 @@ var FileManager = class {
|
|
|
102
102
|
Key: fileName,
|
|
103
103
|
Body: data,
|
|
104
104
|
ACL: "public-read"
|
|
105
|
-
// 默认为公开可读
|
|
106
105
|
});
|
|
107
106
|
await this.s3Client.send(command);
|
|
108
107
|
} else {
|
|
@@ -135,22 +134,18 @@ var FileManager = class {
|
|
|
135
134
|
* @param fileIdentifier 要删除的文件名/标识符。
|
|
136
135
|
*/
|
|
137
136
|
async deleteFile(fileIdentifier) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
this.logger.warn(`删除本地文件 ${filePath} 失败:`, error);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
});
|
|
137
|
+
try {
|
|
138
|
+
if (this.s3Client) {
|
|
139
|
+
const command = new import_client_s3.DeleteObjectCommand({ Bucket: this.s3Bucket, Key: fileIdentifier });
|
|
140
|
+
await this.s3Client.send(command);
|
|
141
|
+
} else {
|
|
142
|
+
const filePath = path.join(this.resourceDir, fileIdentifier);
|
|
143
|
+
await this.withLock(filePath, () => fs.unlink(filePath));
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error.code !== "ENOENT" && error.name !== "NoSuchKey") {
|
|
147
|
+
this.logger.warn(`删除文件 ${fileIdentifier} 失败:`, error);
|
|
148
|
+
}
|
|
154
149
|
}
|
|
155
150
|
}
|
|
156
151
|
};
|
|
@@ -186,10 +181,9 @@ var ProfileManager = class {
|
|
|
186
181
|
if (trimmedNickname) {
|
|
187
182
|
await this.setNickname(session.userId, trimmedNickname);
|
|
188
183
|
return `昵称已更新为:${trimmedNickname}`;
|
|
189
|
-
} else {
|
|
190
|
-
await this.clearNickname(session.userId);
|
|
191
|
-
return "昵称已清除";
|
|
192
184
|
}
|
|
185
|
+
await this.clearNickname(session.userId);
|
|
186
|
+
return "昵称已清除";
|
|
193
187
|
});
|
|
194
188
|
}
|
|
195
189
|
/**
|
|
@@ -206,8 +200,8 @@ var ProfileManager = class {
|
|
|
206
200
|
* @returns 返回用户的昵称字符串,如果未设置则返回 null。
|
|
207
201
|
*/
|
|
208
202
|
async getNickname(userId) {
|
|
209
|
-
const profile = await this.ctx.database.get("cave_user", { userId });
|
|
210
|
-
return profile
|
|
203
|
+
const [profile] = await this.ctx.database.get("cave_user", { userId }, { fields: ["nickname"] });
|
|
204
|
+
return profile?.nickname ?? null;
|
|
211
205
|
}
|
|
212
206
|
/**
|
|
213
207
|
* @description 清除指定用户的昵称设置。
|
|
@@ -237,35 +231,28 @@ async function buildCaveMessage(cave, config, fileManager, logger2) {
|
|
|
237
231
|
const fileName = element.attrs.src;
|
|
238
232
|
if (!isMedia || !fileName) return element;
|
|
239
233
|
if (config.enableS3 && config.publicUrl) {
|
|
240
|
-
const fullUrl =
|
|
234
|
+
const fullUrl = new URL(fileName, config.publicUrl).href;
|
|
241
235
|
return (0, import_koishi.h)(element.type, { ...element.attrs, src: fullUrl });
|
|
242
236
|
}
|
|
243
237
|
if (config.localPath) {
|
|
244
|
-
|
|
245
|
-
return (0, import_koishi.h)(element.type, { ...element.attrs, src: fileUri });
|
|
238
|
+
return (0, import_koishi.h)(element.type, { ...element.attrs, src: `file://${path2.join(config.localPath, fileName)}` });
|
|
246
239
|
}
|
|
247
240
|
try {
|
|
248
241
|
const data = await fileManager.readFile(fileName);
|
|
249
|
-
const
|
|
250
|
-
const mimeType = mimeTypeMap[ext] || "application/octet-stream";
|
|
242
|
+
const mimeType = mimeTypeMap[path2.extname(fileName).toLowerCase()] || "application/octet-stream";
|
|
251
243
|
return (0, import_koishi.h)(element.type, { ...element.attrs, src: `data:${mimeType};base64,${data.toString("base64")}` });
|
|
252
244
|
} catch (error) {
|
|
253
245
|
logger2.warn(`转换文件 ${fileName} 为 Base64 失败:`, error);
|
|
254
246
|
return (0, import_koishi.h)("p", {}, `[${element.type}]`);
|
|
255
247
|
}
|
|
256
248
|
}));
|
|
257
|
-
const finalMessage = [];
|
|
258
|
-
const [headerFormat, footerFormat = ""] = config.caveFormat.split("|");
|
|
259
249
|
const replacements = { id: cave.id.toString(), name: cave.userName };
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
250
|
+
const formatPart = /* @__PURE__ */ __name((part) => part.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]).trim(), "formatPart");
|
|
251
|
+
const [header, footer] = config.caveFormat.split("|", 2).map(formatPart);
|
|
252
|
+
const finalMessage = [];
|
|
253
|
+
if (header) finalMessage.push(header + "\n");
|
|
264
254
|
finalMessage.push(...processedElements);
|
|
265
|
-
|
|
266
|
-
if (footerText.trim()) {
|
|
267
|
-
finalMessage.push("\n" + footerText.trim());
|
|
268
|
-
}
|
|
255
|
+
if (footer) finalMessage.push("\n" + footer);
|
|
269
256
|
return finalMessage;
|
|
270
257
|
}
|
|
271
258
|
__name(buildCaveMessage, "buildCaveMessage");
|
|
@@ -300,11 +287,10 @@ async function getNextCaveId(ctx, query = {}) {
|
|
|
300
287
|
__name(getNextCaveId, "getNextCaveId");
|
|
301
288
|
function checkCooldown(session, config, lastUsed) {
|
|
302
289
|
if (config.coolDown <= 0 || !session.channelId) return null;
|
|
303
|
-
const now = Date.now();
|
|
304
290
|
const lastTime = lastUsed.get(session.channelId) || 0;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return `指令冷却中,请在 ${
|
|
291
|
+
const remainingTime = lastTime + config.coolDown * 1e3 - Date.now();
|
|
292
|
+
if (remainingTime > 0) {
|
|
293
|
+
return `指令冷却中,请在 ${Math.ceil(remainingTime / 1e3)} 秒后重试`;
|
|
308
294
|
}
|
|
309
295
|
return null;
|
|
310
296
|
}
|
|
@@ -327,31 +313,25 @@ async function processMessageElements(sourceElements, newId, channelId, userId)
|
|
|
327
313
|
"file": "file",
|
|
328
314
|
"text": "text"
|
|
329
315
|
};
|
|
316
|
+
const defaultExtMap = { "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
|
|
330
317
|
async function traverse(elements) {
|
|
331
318
|
for (const el of elements) {
|
|
332
319
|
const normalizedType = typeMap[el.type];
|
|
333
|
-
if (
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
mediaToSave.push({ sourceUrl: fileIdentifier, fileName });
|
|
346
|
-
fileIdentifier = fileName;
|
|
320
|
+
if (normalizedType) {
|
|
321
|
+
if (["image", "video", "audio", "file"].includes(normalizedType) && el.attrs.src) {
|
|
322
|
+
let fileIdentifier = el.attrs.src;
|
|
323
|
+
if (fileIdentifier.startsWith("http")) {
|
|
324
|
+
const ext = path2.extname(el.attrs.file || "") || defaultExtMap[normalizedType];
|
|
325
|
+
const fileName = `${newId}_${++mediaIndex}_${channelId || "private"}_${userId}${ext}`;
|
|
326
|
+
mediaToSave.push({ sourceUrl: fileIdentifier, fileName });
|
|
327
|
+
fileIdentifier = fileName;
|
|
328
|
+
}
|
|
329
|
+
finalElementsForDb.push({ type: normalizedType, file: fileIdentifier });
|
|
330
|
+
} else if (normalizedType === "text" && el.attrs.content?.trim()) {
|
|
331
|
+
finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
|
|
347
332
|
}
|
|
348
|
-
finalElementsForDb.push({ type: normalizedType, file: fileIdentifier });
|
|
349
|
-
} else if (normalizedType === "text" && el.attrs.content?.trim()) {
|
|
350
|
-
finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
|
|
351
|
-
}
|
|
352
|
-
if (el.children) {
|
|
353
|
-
await traverse(el.children);
|
|
354
333
|
}
|
|
334
|
+
if (el.children) await traverse(el.children);
|
|
355
335
|
}
|
|
356
336
|
}
|
|
357
337
|
__name(traverse, "traverse");
|
|
@@ -361,10 +341,11 @@ async function processMessageElements(sourceElements, newId, channelId, userId)
|
|
|
361
341
|
__name(processMessageElements, "processMessageElements");
|
|
362
342
|
async function handleFileUploads(ctx, config, fileManager, logger2, reviewManager, cave, mediaToSave) {
|
|
363
343
|
try {
|
|
364
|
-
|
|
344
|
+
const uploadPromises = mediaToSave.map(async (media) => {
|
|
365
345
|
const response = await ctx.http.get(media.sourceUrl, { responseType: "arraybuffer", timeout: 3e4 });
|
|
366
346
|
await fileManager.saveFile(media.fileName, Buffer.from(response));
|
|
367
|
-
})
|
|
347
|
+
});
|
|
348
|
+
await Promise.all(uploadPromises);
|
|
368
349
|
const finalStatus = config.enableReview ? "pending" : "active";
|
|
369
350
|
await ctx.database.upsert("cave", [{ id: cave.id, status: finalStatus }]);
|
|
370
351
|
if (finalStatus === "pending" && reviewManager) {
|
|
@@ -374,7 +355,7 @@ async function handleFileUploads(ctx, config, fileManager, logger2, reviewManage
|
|
|
374
355
|
} catch (fileSaveError) {
|
|
375
356
|
logger2.error(`回声洞(${cave.id})文件保存失败:`, fileSaveError);
|
|
376
357
|
await ctx.database.upsert("cave", [{ id: cave.id, status: "delete" }]);
|
|
377
|
-
|
|
358
|
+
cleanupPendingDeletions(ctx, fileManager, logger2);
|
|
378
359
|
}
|
|
379
360
|
}
|
|
380
361
|
__name(handleFileUploads, "handleFileUploads");
|
|
@@ -402,28 +383,19 @@ var DataManager = class {
|
|
|
402
383
|
* @param cave - 主 `cave` 命令实例。
|
|
403
384
|
*/
|
|
404
385
|
registerCommands(cave) {
|
|
405
|
-
|
|
406
|
-
const adminChannelId = this.config.adminChannel
|
|
407
|
-
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
408
|
-
try {
|
|
409
|
-
await session.send("正在导出数据,请稍候...");
|
|
410
|
-
return await this.exportData();
|
|
411
|
-
} catch (error) {
|
|
412
|
-
this.logger.error("导出数据时发生错误:", error);
|
|
413
|
-
return `导出失败: ${error.message}`;
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
cave.subcommand(".import", "导入回声洞数据").usage("从 cave_import.json 中导入回声洞数据。").action(async ({ session }) => {
|
|
417
|
-
const adminChannelId = this.config.adminChannel ? this.config.adminChannel.split(":")[1] : null;
|
|
386
|
+
const requireAdmin = /* @__PURE__ */ __name((action) => async ({ session }) => {
|
|
387
|
+
const adminChannelId = this.config.adminChannel?.split(":")[1];
|
|
418
388
|
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
419
389
|
try {
|
|
420
|
-
await session.send("
|
|
421
|
-
return await
|
|
390
|
+
await session.send("正在处理,请稍候...");
|
|
391
|
+
return await action();
|
|
422
392
|
} catch (error) {
|
|
423
|
-
this.logger.error("
|
|
424
|
-
return
|
|
393
|
+
this.logger.error("数据操作时发生错误:", error);
|
|
394
|
+
return `操作失败: ${error.message || "未知错误"}`;
|
|
425
395
|
}
|
|
426
|
-
});
|
|
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()));
|
|
427
399
|
}
|
|
428
400
|
/**
|
|
429
401
|
* @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
|
|
@@ -450,17 +422,12 @@ var DataManager = class {
|
|
|
450
422
|
if (!Array.isArray(importedCaves)) throw new Error("导入文件格式无效");
|
|
451
423
|
} catch (error) {
|
|
452
424
|
this.logger.error(`读取导入文件失败:`, error);
|
|
453
|
-
|
|
425
|
+
throw new Error(`读取导入文件失败: ${error.message || "未知错误"}`);
|
|
454
426
|
}
|
|
455
427
|
let successCount = 0;
|
|
456
428
|
for (const cave of importedCaves) {
|
|
457
429
|
const newId = await getNextCaveId(this.ctx, {});
|
|
458
|
-
const newCave = {
|
|
459
|
-
...cave,
|
|
460
|
-
id: newId,
|
|
461
|
-
channelId: cave.channelId,
|
|
462
|
-
status: "active"
|
|
463
|
-
};
|
|
430
|
+
const newCave = { ...cave, id: newId, status: "active" };
|
|
464
431
|
await this.ctx.database.create("cave", newCave);
|
|
465
432
|
successCount++;
|
|
466
433
|
}
|
|
@@ -493,27 +460,29 @@ var ReviewManager = class {
|
|
|
493
460
|
*/
|
|
494
461
|
registerCommands(cave) {
|
|
495
462
|
cave.subcommand(".review [id:posint] [action:string]", "审核回声洞").usage("查看或审核回声洞,使用 <Y/N> 进行审核。").action(async ({ session }, id, action) => {
|
|
496
|
-
const adminChannelId = this.config.adminChannel
|
|
463
|
+
const adminChannelId = this.config.adminChannel?.split(":")[1];
|
|
497
464
|
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
498
465
|
if (!id) {
|
|
499
|
-
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" });
|
|
500
|
-
if (pendingCaves.length
|
|
466
|
+
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" }, { fields: ["id"] });
|
|
467
|
+
if (!pendingCaves.length) return "当前没有需要审核的回声洞";
|
|
501
468
|
return `当前共有 ${pendingCaves.length} 条待审核回声洞,序号为:
|
|
502
469
|
${pendingCaves.map((c) => c.id).join(", ")}`;
|
|
503
470
|
}
|
|
504
471
|
const [targetCave] = await this.ctx.database.get("cave", { id });
|
|
505
472
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
506
473
|
if (targetCave.status !== "pending") return `回声洞(${id})无需审核`;
|
|
507
|
-
if (
|
|
474
|
+
if (!action) {
|
|
508
475
|
return [`待审核:`, ...await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger)];
|
|
509
476
|
}
|
|
510
477
|
const normalizedAction = action.toLowerCase();
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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}"
|
|
515
485
|
请使用 "Y" (通过) 或 "N" (拒绝)`;
|
|
516
|
-
return this.processReview(reviewAction, id);
|
|
517
486
|
});
|
|
518
487
|
}
|
|
519
488
|
/**
|
|
@@ -521,14 +490,13 @@ ${pendingCaves.map((c) => c.id).join(", ")}`;
|
|
|
521
490
|
* @param cave 新创建的、状态为 'pending' 的回声洞对象。
|
|
522
491
|
*/
|
|
523
492
|
async sendForReview(cave) {
|
|
524
|
-
|
|
525
|
-
if (!channelParts || channelParts.length < 2 || !channelParts[1]) {
|
|
493
|
+
if (!this.config.adminChannel?.includes(":")) {
|
|
526
494
|
this.logger.warn(`管理群组配置无效,已自动通过回声洞(${cave.id})`);
|
|
527
495
|
await this.ctx.database.upsert("cave", [{ id: cave.id, status: "active" }]);
|
|
528
496
|
return;
|
|
529
497
|
}
|
|
530
|
-
const reviewMessage = [`待审核:`, ...await buildCaveMessage(cave, this.config, this.fileManager, this.logger)];
|
|
531
498
|
try {
|
|
499
|
+
const reviewMessage = [`待审核:`, ...await buildCaveMessage(cave, this.config, this.fileManager, this.logger)];
|
|
532
500
|
await this.ctx.broadcast([this.config.adminChannel], import_koishi2.h.normalize(reviewMessage));
|
|
533
501
|
} catch (error) {
|
|
534
502
|
this.logger.error(`发送回声洞(${cave.id})审核消息失败:`, error);
|
|
@@ -543,16 +511,14 @@ ${pendingCaves.map((c) => c.id).join(", ")}`;
|
|
|
543
511
|
async processReview(action, caveId) {
|
|
544
512
|
const [cave] = await this.ctx.database.get("cave", { id: caveId, status: "pending" });
|
|
545
513
|
if (!cave) return `回声洞(${caveId})无需审核`;
|
|
546
|
-
let resultMessage;
|
|
547
514
|
if (action === "approve") {
|
|
548
515
|
await this.ctx.database.upsert("cave", [{ id: caveId, status: "active" }]);
|
|
549
|
-
|
|
516
|
+
return `回声洞(${caveId})已通过`;
|
|
550
517
|
} else {
|
|
551
518
|
await this.ctx.database.upsert("cave", [{ id: caveId, status: "delete" }]);
|
|
552
|
-
resultMessage = `回声洞(${caveId})已拒绝`;
|
|
553
519
|
cleanupPendingDeletions(this.ctx, this.fileManager, this.logger);
|
|
520
|
+
return `回声洞(${caveId})已拒绝`;
|
|
554
521
|
}
|
|
555
|
-
return resultMessage;
|
|
556
522
|
}
|
|
557
523
|
};
|
|
558
524
|
|
|
@@ -607,10 +573,10 @@ function apply(ctx, config) {
|
|
|
607
573
|
}, { primary: "id" });
|
|
608
574
|
const fileManager = new FileManager(ctx.baseDir, config, logger);
|
|
609
575
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const cave = ctx.command("cave", "回声洞").option("add", "-a <content:text>
|
|
576
|
+
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 }) => {
|
|
614
580
|
if (options.add) return session.execute(`cave.add ${options.add}`);
|
|
615
581
|
if (options.view) return session.execute(`cave.view ${options.view}`);
|
|
616
582
|
if (options.delete) return session.execute(`cave.del ${options.delete}`);
|
|
@@ -620,7 +586,7 @@ function apply(ctx, config) {
|
|
|
620
586
|
try {
|
|
621
587
|
const query = getScopeQuery(session, config);
|
|
622
588
|
const candidates = await ctx.database.get("cave", query, { fields: ["id"] });
|
|
623
|
-
if (candidates.length
|
|
589
|
+
if (!candidates.length) {
|
|
624
590
|
return `当前${config.perChannel && session.channelId ? "本群" : ""}还没有任何回声洞`;
|
|
625
591
|
}
|
|
626
592
|
const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
|
|
@@ -634,21 +600,17 @@ function apply(ctx, config) {
|
|
|
634
600
|
});
|
|
635
601
|
cave.subcommand(".add [content:text]", "添加回声洞").usage("添加一条回声洞。可以直接发送内容,也可以回复或引用一条消息。").action(async ({ session }, content) => {
|
|
636
602
|
try {
|
|
637
|
-
let sourceElements;
|
|
638
|
-
if (
|
|
639
|
-
sourceElements = session.quote.elements;
|
|
640
|
-
} else if (content?.trim()) {
|
|
603
|
+
let sourceElements = session.quote?.elements;
|
|
604
|
+
if (!sourceElements && content?.trim()) {
|
|
641
605
|
sourceElements = import_koishi3.h.parse(content);
|
|
642
|
-
}
|
|
606
|
+
}
|
|
607
|
+
if (!sourceElements) {
|
|
643
608
|
await session.send("请在一分钟内发送你要添加的内容");
|
|
644
609
|
const reply = await session.prompt(6e4);
|
|
645
610
|
if (!reply) return "操作超时,已取消添加";
|
|
646
611
|
sourceElements = import_koishi3.h.parse(reply);
|
|
647
612
|
}
|
|
648
|
-
const idScopeQuery = {};
|
|
649
|
-
if (config.perChannel && session.channelId) {
|
|
650
|
-
idScopeQuery["channelId"] = session.channelId;
|
|
651
|
-
}
|
|
613
|
+
const idScopeQuery = config.perChannel && session.channelId ? { channelId: session.channelId } : {};
|
|
652
614
|
const newId = await getNextCaveId(ctx, idScopeQuery);
|
|
653
615
|
const { finalElementsForDb, mediaToSave } = await processMessageElements(
|
|
654
616
|
sourceElements,
|
|
@@ -657,39 +619,25 @@ function apply(ctx, config) {
|
|
|
657
619
|
session.userId
|
|
658
620
|
);
|
|
659
621
|
if (finalElementsForDb.length === 0) return "内容为空,已取消添加";
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
622
|
+
const userName = (config.enableProfile ? await profileManager.getNickname(session.userId) : null) || session.username;
|
|
623
|
+
const hasMedia = mediaToSave.length > 0;
|
|
624
|
+
const initialStatus = hasMedia ? "preload" : config.enableReview ? "pending" : "active";
|
|
625
|
+
const newCave = {
|
|
626
|
+
id: newId,
|
|
627
|
+
elements: finalElementsForDb,
|
|
628
|
+
channelId: session.channelId,
|
|
629
|
+
userId: session.userId,
|
|
630
|
+
userName,
|
|
631
|
+
status: initialStatus,
|
|
632
|
+
time: /* @__PURE__ */ new Date()
|
|
633
|
+
};
|
|
634
|
+
await ctx.database.create("cave", newCave);
|
|
635
|
+
if (hasMedia) {
|
|
673
636
|
handleFileUploads(ctx, config, fileManager, logger, reviewManager, newCave, mediaToSave);
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const finalStatus = config.enableReview ? "pending" : "active";
|
|
677
|
-
const newCave = {
|
|
678
|
-
id: newId,
|
|
679
|
-
elements: finalElementsForDb,
|
|
680
|
-
channelId: session.channelId,
|
|
681
|
-
userId: session.userId,
|
|
682
|
-
userName,
|
|
683
|
-
status: finalStatus,
|
|
684
|
-
time: /* @__PURE__ */ new Date()
|
|
685
|
-
};
|
|
686
|
-
await ctx.database.create("cave", newCave);
|
|
687
|
-
if (finalStatus === "pending") {
|
|
688
|
-
reviewManager.sendForReview(newCave);
|
|
689
|
-
return `提交成功,序号为(${newId})`;
|
|
690
|
-
}
|
|
691
|
-
return `添加成功,序号为(${newId})`;
|
|
637
|
+
} else if (initialStatus === "pending") {
|
|
638
|
+
reviewManager.sendForReview(newCave);
|
|
692
639
|
}
|
|
640
|
+
return initialStatus === "pending" || initialStatus === "preload" && config.enableReview ? `提交成功,序号为(${newId})` : `添加成功,序号为(${newId})`;
|
|
693
641
|
} catch (error) {
|
|
694
642
|
logger.error("添加回声洞失败:", error);
|
|
695
643
|
return "添加失败,请稍后再试";
|
|
@@ -715,8 +663,10 @@ function apply(ctx, config) {
|
|
|
715
663
|
try {
|
|
716
664
|
const [targetCave] = await ctx.database.get("cave", { id, status: "active" });
|
|
717
665
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
718
|
-
const adminChannelId = config.adminChannel
|
|
719
|
-
|
|
666
|
+
const adminChannelId = config.adminChannel?.split(":")[1];
|
|
667
|
+
const isAuthor = targetCave.userId === session.userId;
|
|
668
|
+
const isAdmin = session.channelId === adminChannelId;
|
|
669
|
+
if (!isAuthor && !isAdmin) {
|
|
720
670
|
return "你没有权限删除这条回声洞";
|
|
721
671
|
}
|
|
722
672
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
@@ -731,8 +681,8 @@ function apply(ctx, config) {
|
|
|
731
681
|
cave.subcommand(".list", "查询我的投稿").usage("查询并列出你所有投稿的回声洞序号。").action(async ({ session }) => {
|
|
732
682
|
try {
|
|
733
683
|
const query = { ...getScopeQuery(session, config), userId: session.userId };
|
|
734
|
-
const userCaves = await ctx.database.get("cave", query);
|
|
735
|
-
if (userCaves.length
|
|
684
|
+
const userCaves = await ctx.database.get("cave", query, { fields: ["id"] });
|
|
685
|
+
if (!userCaves.length) return "你还没有投稿过回声洞";
|
|
736
686
|
const caveIds = userCaves.map((c) => c.id).sort((a, b) => a - b).join(", ");
|
|
737
687
|
return `你已投稿 ${userCaves.length} 条回声洞,序号为:
|
|
738
688
|
${caveIds}`;
|
|
@@ -741,18 +691,9 @@ ${caveIds}`;
|
|
|
741
691
|
return "查询失败,请稍后再试";
|
|
742
692
|
}
|
|
743
693
|
});
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
if (config.enableIO) {
|
|
749
|
-
dataManager = new DataManager(ctx, config, fileManager, logger);
|
|
750
|
-
dataManager.registerCommands(cave);
|
|
751
|
-
}
|
|
752
|
-
if (config.enableReview) {
|
|
753
|
-
reviewManager = new ReviewManager(ctx, config, fileManager, logger);
|
|
754
|
-
reviewManager.registerCommands(cave);
|
|
755
|
-
}
|
|
694
|
+
if (profileManager) profileManager.registerCommands(cave);
|
|
695
|
+
if (dataManager) dataManager.registerCommands(cave);
|
|
696
|
+
if (reviewManager) reviewManager.registerCommands(cave);
|
|
756
697
|
}
|
|
757
698
|
__name(apply, "apply");
|
|
758
699
|
// Annotate the CommonJS export names for ESM import in node:
|
package/package.json
CHANGED
package/lib/DataManager.d.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { Context, Logger } from 'koishi';
|
|
2
|
-
import { FileManager } from './FileManager';
|
|
3
|
-
import { Config } from './index';
|
|
4
|
-
/**
|
|
5
|
-
* @class DataManager
|
|
6
|
-
* @description 负责处理回声洞数据的导入和导出功能。
|
|
7
|
-
*/
|
|
8
|
-
export declare class DataManager {
|
|
9
|
-
private ctx;
|
|
10
|
-
private config;
|
|
11
|
-
private fileManager;
|
|
12
|
-
private logger;
|
|
13
|
-
/**
|
|
14
|
-
* @constructor
|
|
15
|
-
* @param ctx Koishi 上下文,用于数据库操作。
|
|
16
|
-
* @param config 插件配置。
|
|
17
|
-
* @param fileManager 文件管理器实例。
|
|
18
|
-
* @param logger 日志记录器实例。
|
|
19
|
-
*/
|
|
20
|
-
constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger);
|
|
21
|
-
/**
|
|
22
|
-
* @description 注册 `.export` 和 `.import` 子命令。
|
|
23
|
-
* @param cave - 主 `cave` 命令实例。
|
|
24
|
-
*/
|
|
25
|
-
registerCommands(cave: any): void;
|
|
26
|
-
/**
|
|
27
|
-
* @description 导出所有 'active' 状态的回声洞数据到 `cave_export.json`。
|
|
28
|
-
* @returns 描述导出结果的消息字符串。
|
|
29
|
-
*/
|
|
30
|
-
exportData(): Promise<string>;
|
|
31
|
-
/**
|
|
32
|
-
* @description 从 `cave_import.json` 文件导入回声洞数据。
|
|
33
|
-
* @returns 描述导入结果的消息字符串。
|
|
34
|
-
*/
|
|
35
|
-
importData(): Promise<string>;
|
|
36
|
-
}
|
package/lib/FileManager.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Logger } from 'koishi';
|
|
2
|
-
import { Config } from './index';
|
|
3
|
-
/**
|
|
4
|
-
* @class FileManager
|
|
5
|
-
* @description 封装了对文件的存储、读取和删除操作。
|
|
6
|
-
* 能根据配置自动选择使用本地文件系统或 AWS S3 作为存储后端。
|
|
7
|
-
* 内置 Promise 文件锁,防止本地文件的并发写入冲突。
|
|
8
|
-
*/
|
|
9
|
-
export declare class FileManager {
|
|
10
|
-
private logger;
|
|
11
|
-
private resourceDir;
|
|
12
|
-
private locks;
|
|
13
|
-
private s3Client?;
|
|
14
|
-
private s3Bucket?;
|
|
15
|
-
/**
|
|
16
|
-
* @constructor
|
|
17
|
-
* @param baseDir Koishi 应用的基础数据目录 (ctx.baseDir)。
|
|
18
|
-
* @param config 插件的配置对象。
|
|
19
|
-
* @param logger 日志记录器实例。
|
|
20
|
-
*/
|
|
21
|
-
constructor(baseDir: string, config: Config, logger: Logger);
|
|
22
|
-
/**
|
|
23
|
-
* @description 使用文件锁安全地执行异步文件操作,防止并发读写冲突。
|
|
24
|
-
* @template T 异步操作的返回类型。
|
|
25
|
-
* @param fullPath 需要加锁的文件的完整路径。
|
|
26
|
-
* @param operation 要执行的异步函数。
|
|
27
|
-
* @returns 返回异步操作的结果。
|
|
28
|
-
*/
|
|
29
|
-
private withLock;
|
|
30
|
-
/**
|
|
31
|
-
* @description 保存文件,自动选择 S3 或本地存储。
|
|
32
|
-
* @param fileName 用作 S3 Key 或本地文件名。
|
|
33
|
-
* @param data 要写入的 Buffer 数据。
|
|
34
|
-
* @returns 返回保存时使用的文件名/标识符。
|
|
35
|
-
*/
|
|
36
|
-
saveFile(fileName: string, data: Buffer): Promise<string>;
|
|
37
|
-
/**
|
|
38
|
-
* @description 读取文件,自动从 S3 或本地存储读取。
|
|
39
|
-
* @param fileName 要读取的文件名/标识符。
|
|
40
|
-
* @returns 文件的 Buffer 数据。
|
|
41
|
-
*/
|
|
42
|
-
readFile(fileName: string): Promise<Buffer>;
|
|
43
|
-
/**
|
|
44
|
-
* @description 删除文件,自动从 S3 或本地删除。
|
|
45
|
-
* @param fileIdentifier 要删除的文件名/标识符。
|
|
46
|
-
*/
|
|
47
|
-
deleteFile(fileIdentifier: string): Promise<void>;
|
|
48
|
-
}
|
package/lib/ProfileManager.d.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Context } from 'koishi';
|
|
2
|
-
/**
|
|
3
|
-
* @description 数据库 `cave_user` 表的结构定义。
|
|
4
|
-
* @property userId 用户唯一ID,作为主键。
|
|
5
|
-
* @property nickname 用户自定义的昵称。
|
|
6
|
-
*/
|
|
7
|
-
export interface UserProfile {
|
|
8
|
-
userId: string;
|
|
9
|
-
nickname: string;
|
|
10
|
-
}
|
|
11
|
-
declare module 'koishi' {
|
|
12
|
-
interface Tables {
|
|
13
|
-
cave_user: UserProfile;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* @class ProfileManager
|
|
18
|
-
* @description 负责管理用户在回声洞中的自定义昵称。
|
|
19
|
-
* 当插件配置 `enableProfile` 为 true 时实例化。
|
|
20
|
-
*/
|
|
21
|
-
export declare class ProfileManager {
|
|
22
|
-
private ctx;
|
|
23
|
-
/**
|
|
24
|
-
* @constructor
|
|
25
|
-
* @param ctx - Koishi 上下文,用于初始化数据库模型。
|
|
26
|
-
*/
|
|
27
|
-
constructor(ctx: Context);
|
|
28
|
-
/**
|
|
29
|
-
* @description 注册 `.profile` 子命令,用于管理用户昵称。
|
|
30
|
-
* @param cave - 主 `cave` 命令实例。
|
|
31
|
-
*/
|
|
32
|
-
registerCommands(cave: any): void;
|
|
33
|
-
/**
|
|
34
|
-
* @description 设置或更新指定用户的昵称。
|
|
35
|
-
* @param userId - 目标用户的 ID。
|
|
36
|
-
* @param nickname - 要设置的新昵称。
|
|
37
|
-
*/
|
|
38
|
-
setNickname(userId: string, nickname: string): Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
* @description 获取指定用户的昵称。
|
|
41
|
-
* @param userId - 目标用户的 ID。
|
|
42
|
-
* @returns 返回用户的昵称字符串,如果未设置则返回 null。
|
|
43
|
-
*/
|
|
44
|
-
getNickname(userId: string): Promise<string | null>;
|
|
45
|
-
/**
|
|
46
|
-
* @description 清除指定用户的昵称设置。
|
|
47
|
-
* @param userId - 目标用户的 ID。
|
|
48
|
-
*/
|
|
49
|
-
clearNickname(userId: string): Promise<void>;
|
|
50
|
-
}
|
package/lib/ReviewManager.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
/**
|
|
14
|
-
* @constructor
|
|
15
|
-
* @param ctx Koishi 上下文。
|
|
16
|
-
* @param config 插件配置。
|
|
17
|
-
* @param fileManager 文件管理器实例。
|
|
18
|
-
* @param logger 日志记录器实例。
|
|
19
|
-
*/
|
|
20
|
-
constructor(ctx: Context, config: Config, fileManager: FileManager, logger: Logger);
|
|
21
|
-
/**
|
|
22
|
-
* @description 注册与审核相关的 `.review` 子命令。
|
|
23
|
-
* @param cave - 主 `cave` 命令实例。
|
|
24
|
-
*/
|
|
25
|
-
registerCommands(cave: any): void;
|
|
26
|
-
/**
|
|
27
|
-
* @description 将新回声洞提交到管理群组以供审核。
|
|
28
|
-
* @param cave 新创建的、状态为 'pending' 的回声洞对象。
|
|
29
|
-
*/
|
|
30
|
-
sendForReview(cave: CaveObject): Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
* @description 处理管理员的审核决定(通过或拒绝)。
|
|
33
|
-
* @param action 'approve' (通过) 或 'reject' (拒绝)。
|
|
34
|
-
* @param caveId 被审核的回声洞 ID。
|
|
35
|
-
* @returns 返回给操作者的确认消息。
|
|
36
|
-
*/
|
|
37
|
-
processReview(action: 'approve' | 'reject', caveId: number): Promise<string>;
|
|
38
|
-
}
|
package/lib/index.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Context, Schema } from 'koishi';
|
|
2
|
-
export declare const name = "best-cave";
|
|
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<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
|
-
/**
|
|
6
|
-
* @description 存储在数据库中的单个消息元素。
|
|
7
|
-
*/
|
|
8
|
-
export interface StoredElement {
|
|
9
|
-
type: 'text' | 'image' | 'video' | 'audio' | 'file';
|
|
10
|
-
content?: string;
|
|
11
|
-
file?: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* @description 数据库 `cave` 表的完整对象模型。
|
|
15
|
-
*/
|
|
16
|
-
export interface CaveObject {
|
|
17
|
-
id: number;
|
|
18
|
-
elements: StoredElement[];
|
|
19
|
-
channelId: string;
|
|
20
|
-
userId: string;
|
|
21
|
-
userName: string;
|
|
22
|
-
status: 'active' | 'delete' | 'pending' | 'preload';
|
|
23
|
-
time: Date;
|
|
24
|
-
}
|
|
25
|
-
declare module 'koishi' {
|
|
26
|
-
interface Tables {
|
|
27
|
-
cave: CaveObject;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export interface Config {
|
|
31
|
-
coolDown: number;
|
|
32
|
-
perChannel: boolean;
|
|
33
|
-
adminChannel: string;
|
|
34
|
-
enableProfile: boolean;
|
|
35
|
-
enableIO: boolean;
|
|
36
|
-
enableReview: boolean;
|
|
37
|
-
caveFormat: string;
|
|
38
|
-
localPath?: string;
|
|
39
|
-
enableS3: boolean;
|
|
40
|
-
endpoint?: string;
|
|
41
|
-
region?: string;
|
|
42
|
-
accessKeyId?: string;
|
|
43
|
-
secretAccessKey?: string;
|
|
44
|
-
bucket?: string;
|
|
45
|
-
publicUrl?: string;
|
|
46
|
-
}
|
|
47
|
-
export declare const Config: Schema<Config>;
|
|
48
|
-
export declare function apply(ctx: Context, config: Config): void;
|