koishi-plugin-best-cave 2.0.9 → 2.1.1

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 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
- if (this.s3Client) {
139
- const command = new import_client_s3.DeleteObjectCommand({ Bucket: this.s3Bucket, Key: fileIdentifier });
140
- await this.s3Client.send(command).catch((err) => {
141
- this.logger.warn(`删除 S3 文件 ${fileIdentifier} 失败:`, err);
142
- });
143
- } else {
144
- const filePath = path.join(this.resourceDir, fileIdentifier);
145
- await this.withLock(filePath, async () => {
146
- try {
147
- await fs.unlink(filePath);
148
- } catch (error) {
149
- if (error.code !== "ENOENT") {
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[0]?.nickname ?? null;
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 = config.publicUrl.endsWith("/") ? `${config.publicUrl}${fileName}` : `${config.publicUrl}/${fileName}`;
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
- const fileUri = `file://${path2.join(config.localPath, fileName)}`;
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 ext = path2.extname(fileName).toLowerCase();
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 headerText = headerFormat.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]);
261
- if (headerText.trim()) {
262
- finalMessage.push(headerText.trim() + "\n");
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
- const footerText = footerFormat.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]);
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
- if (now - lastTime < config.coolDown * 1e3) {
306
- const waitTime = Math.ceil((config.coolDown * 1e3 - (now - lastTime)) / 1e3);
307
- return `指令冷却中,请在 ${waitTime} 秒后重试`;
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 (!normalizedType) {
334
- if (el.children) await traverse(el.children);
335
- continue;
336
- }
337
- if (["image", "video", "audio", "file"].includes(normalizedType) && el.attrs.src) {
338
- let fileIdentifier = el.attrs.src;
339
- if (fileIdentifier.startsWith("http")) {
340
- mediaIndex++;
341
- const defaultExtMap = { "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
342
- const ext = el.attrs.file && path2.extname(el.attrs.file) ? path2.extname(el.attrs.file) : defaultExtMap[normalizedType] || ".dat";
343
- const channelIdentifier = channelId || "private";
344
- const fileName = `${newId}_${mediaIndex}_${channelIdentifier}_${userId}${ext}`;
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
- await Promise.all(mediaToSave.map(async (media) => {
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
- await cleanupPendingDeletions(ctx, fileManager, logger2);
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
- cave.subcommand(".export", "导出回声洞数据").usage("将所有回声洞数据导出到 cave_export.json。").action(async ({ session }) => {
406
- const adminChannelId = this.config.adminChannel ? this.config.adminChannel.split(":")[1] : null;
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 this.importData();
390
+ await session.send("正在处理,请稍候...");
391
+ return await action();
422
392
  } catch (error) {
423
- this.logger.error("导入数据时发生错误:", error);
424
- return `导入失败: ${error.message}`;
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
- return `读取导入文件失败: ${error.message || "未知错误"}`;
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 ? this.config.adminChannel.split(":")[1] : null;
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 === 0) return "当前没有需要审核的回声洞";
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 (id && !action) {
474
+ if (!action) {
508
475
  return [`待审核:`, ...await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger)];
509
476
  }
510
477
  const normalizedAction = action.toLowerCase();
511
- let reviewAction;
512
- if (["y", "yes", "ok", "pass", "approve"].includes(normalizedAction)) reviewAction = "approve";
513
- else if (["n", "no", "deny", "reject"].includes(normalizedAction)) reviewAction = "reject";
514
- else return `无效操作: "${action}"
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
- const channelParts = this.config.adminChannel?.split(":");
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
- resultMessage = `回声洞(${caveId})已通过`;
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,9 +573,9 @@ 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
- let profileManager;
611
- let dataManager;
612
- let reviewManager;
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;
613
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}`);
@@ -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 === 0) {
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 (session.quote?.elements) {
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
- } else {
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 customNickname = config.enableProfile ? await profileManager.getNickname(session.userId) : null;
661
- const userName = customNickname || session.username;
662
- if (mediaToSave.length > 0) {
663
- const newCave = {
664
- id: newId,
665
- elements: finalElementsForDb,
666
- channelId: session.channelId,
667
- userId: session.userId,
668
- userName,
669
- status: "preload",
670
- time: /* @__PURE__ */ new Date()
671
- };
672
- await ctx.database.create("cave", newCave);
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
- return config.enableReview ? `提交成功,序号为(${newId})` : `添加成功,序号为(${newId})`;
675
- } else {
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 ? config.adminChannel.split(":")[1] : null;
719
- if (targetCave.userId !== session.userId && session.channelId !== adminChannelId) {
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 === 0) return "你还没有投稿过回声洞";
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 (config.enableProfile) {
745
- profileManager = new ProfileManager(ctx);
746
- profileManager.registerCommands(cave);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-best-cave",
3
3
  "description": "最强大的回声洞现已重构完成啦!注意数据格式需要使用脚本转换哦~",
4
- "version": "2.0.9",
4
+ "version": "2.1.1",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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;