koishi-plugin-best-cave 2.0.2 → 2.0.4

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
@@ -25,6 +25,21 @@ export declare function mediaElementToBase64(element: h, fileManager: FileManage
25
25
  * @returns 一个包含 h() 元素和字符串的消息数组。
26
26
  */
27
27
  export declare function buildCaveMessage(cave: CaveObject, config: Config, fileManager: FileManager, logger: Logger): Promise<(string | h)[]>;
28
+ /**
29
+ * 遍历消息元素,将其转换为可存储的格式,并识别需要下载的媒体文件。
30
+ * @param sourceElements - 源消息中的 h() 元素数组。
31
+ * @param newId - 新回声洞的 ID。
32
+ * @param channelId - 频道 ID。
33
+ * @param userId - 用户 ID。
34
+ * @returns 包含待存储元素和待下载媒体列表的对象。
35
+ */
36
+ export declare function prepareElementsForStorage(sourceElements: h[], newId: number, channelId: string, userId: string): Promise<{
37
+ finalElementsForDb: StoredElement[];
38
+ mediaToDownload: {
39
+ url: string;
40
+ fileName: string;
41
+ }[];
42
+ }>;
28
43
  /**
29
44
  * 清理数据库中所有被标记为 'delete' 状态的回声洞及其关联的文件。
30
45
  * @param ctx - Koishi 上下文。
package/lib/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export declare const usage = "\n<div style=\"border-radius: 10px; border: 1px so
9
9
  * @property file - 文件标识符(本地文件名或 S3 Key),用于媒体类型。
10
10
  */
11
11
  export interface StoredElement {
12
- type: 'text' | 'img' | 'video' | 'audio' | 'file';
12
+ type: 'text' | 'image' | 'video' | 'audio' | 'file';
13
13
  content?: string;
14
14
  file?: string;
15
15
  }
package/lib/index.js CHANGED
@@ -37,7 +37,7 @@ __export(index_exports, {
37
37
  usage: () => usage
38
38
  });
39
39
  module.exports = __toCommonJS(index_exports);
40
- var import_koishi3 = require("koishi");
40
+ var import_koishi2 = require("koishi");
41
41
 
42
42
  // src/FileManager.ts
43
43
  var import_client_s3 = require("@aws-sdk/client-s3");
@@ -271,8 +271,7 @@ function storedFormatToHElements(elements) {
271
271
  switch (el.type) {
272
272
  case "text":
273
273
  return import_koishi.h.text(el.content);
274
- case "img":
275
- return (0, import_koishi.h)("image", { src: el.file });
274
+ case "image":
276
275
  case "video":
277
276
  case "audio":
278
277
  case "file":
@@ -292,7 +291,7 @@ async function mediaElementToBase64(element, fileManager, logger2) {
292
291
  return (0, import_koishi.h)(element.type, { ...element.attrs, src: `data:${mimeType};base64,${data.toString("base64")}` });
293
292
  } catch (error) {
294
293
  logger2.warn(`转换本地文件 ${fileName} 为 Base64 失败:`, error);
295
- return (0, import_koishi.h)("p", {}, `[${element.type}]`);
294
+ return import_koishi.h.text(`[${element.type}]`);
296
295
  }
297
296
  }
298
297
  __name(mediaElementToBase64, "mediaElementToBase64");
@@ -327,13 +326,45 @@ async function buildCaveMessage(cave, config, fileManager, logger2) {
327
326
  footerFormat = formatString.substring(separatorIndex + 1);
328
327
  }
329
328
  const headerText = headerFormat.replace("{id}", cave.id.toString()).replace("{name}", cave.userName);
330
- if (headerText.trim()) finalMessage.push((0, import_koishi.h)("p", {}, headerText));
329
+ if (headerText.trim()) finalMessage.push(headerText);
331
330
  finalMessage.push(...processedElements);
332
331
  const footerText = footerFormat.replace("{id}", cave.id.toString()).replace("{name}", cave.userName);
333
- if (footerText.trim()) finalMessage.push((0, import_koishi.h)("p", {}, footerText));
332
+ if (footerText.trim()) finalMessage.push(footerText);
334
333
  return finalMessage;
335
334
  }
336
335
  __name(buildCaveMessage, "buildCaveMessage");
336
+ async function prepareElementsForStorage(sourceElements, newId, channelId, userId) {
337
+ const finalElementsForDb = [];
338
+ const mediaToDownload = [];
339
+ let mediaIndex = 0;
340
+ const stack = [...sourceElements].reverse();
341
+ while (stack.length > 0) {
342
+ const el = stack.pop();
343
+ const elementType = el.type;
344
+ if (el.children) {
345
+ stack.push(...[...el.children].reverse());
346
+ }
347
+ if (["image", "video", "audio", "file"].includes(elementType) && el.attrs.src) {
348
+ const fileIdentifier = el.attrs.src;
349
+ if (fileIdentifier.startsWith("http")) {
350
+ mediaIndex++;
351
+ const originalName = el.attrs.file;
352
+ const defaultExtMap = { "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
353
+ const ext = originalName ? path2.extname(originalName) : "";
354
+ const finalExt = ext || defaultExtMap[elementType] || ".dat";
355
+ const generatedFileName = `${newId}_${mediaIndex}_${channelId}_${userId}${finalExt}`;
356
+ finalElementsForDb.push({ type: elementType, file: generatedFileName });
357
+ mediaToDownload.push({ url: fileIdentifier, fileName: generatedFileName });
358
+ } else {
359
+ finalElementsForDb.push({ type: elementType, file: fileIdentifier });
360
+ }
361
+ } else if (elementType === "text" && el.attrs.content?.trim()) {
362
+ finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
363
+ }
364
+ }
365
+ return { finalElementsForDb, mediaToDownload };
366
+ }
367
+ __name(prepareElementsForStorage, "prepareElementsForStorage");
337
368
  async function cleanupPendingDeletions(ctx, fileManager, logger2) {
338
369
  try {
339
370
  const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
@@ -366,15 +397,6 @@ async function getNextCaveId(ctx, query = {}) {
366
397
  return newId;
367
398
  }
368
399
  __name(getNextCaveId, "getNextCaveId");
369
- async function downloadMedia(ctx, fileManager, url, originalName, type, caveId, index, channelId, userId) {
370
- const defaultExtMap = { "img": ".jpg", "image": ".jpg", "video": ".mp4", "audio": ".mp3", "file": ".dat" };
371
- const ext = originalName ? path2.extname(originalName) : "";
372
- const finalExt = ext || defaultExtMap[type] || ".dat";
373
- const fileName = `${caveId}_${index}_${channelId}_${userId}${finalExt}`;
374
- const response = await ctx.http.get(url, { responseType: "arraybuffer", timeout: 3e4 });
375
- return fileManager.saveFile(fileName, Buffer.from(response));
376
- }
377
- __name(downloadMedia, "downloadMedia");
378
400
  function checkCooldown(session, config, lastUsed) {
379
401
  if (config.coolDown <= 0 || !session.channelId || config.adminUsers.includes(session.userId)) {
380
402
  return null;
@@ -490,7 +512,6 @@ var DataManager = class {
490
512
  };
491
513
 
492
514
  // src/ReviewManager.ts
493
- var import_koishi2 = require("koishi");
494
515
  var ReviewManager = class {
495
516
  /**
496
517
  * 创建一个 ReviewManager 实例。
@@ -574,10 +595,7 @@ ${pendingIds}`;
574
595
  */
575
596
  async buildReviewMessage(cave) {
576
597
  const caveContent = await buildCaveMessage(cave, this.config, this.fileManager, this.logger);
577
- return [
578
- (0, import_koishi2.h)("p", `以下内容待审核:`),
579
- ...caveContent
580
- ];
598
+ return [`待审核`, ...caveContent];
581
599
  }
582
600
  /**
583
601
  * 处理管理员的审核决定(通过或拒绝)。
@@ -601,7 +619,7 @@ ${pendingIds}`;
601
619
  resultMessage = `回声洞(${caveId})已拒绝`;
602
620
  const caveContent = await buildCaveMessage(cave, this.config, this.fileManager, this.logger);
603
621
  broadcastMessage = [
604
- (0, import_koishi2.h)("p", `回声洞(${caveId})已由管理员 "${adminUserName}" 拒绝`),
622
+ `回声洞(${caveId})已由管理员 "${adminUserName}" 拒绝`,
605
623
  ...caveContent
606
624
  ];
607
625
  }
@@ -630,28 +648,28 @@ var usage = `
630
648
  <p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p>
631
649
  </div>
632
650
  `;
633
- var logger = new import_koishi3.Logger("best-cave");
634
- var Config = import_koishi3.Schema.intersect([
635
- import_koishi3.Schema.object({
636
- coolDown: import_koishi3.Schema.number().default(10).description("冷却时间(秒)"),
637
- perChannel: import_koishi3.Schema.boolean().default(false).description("启用分群模式"),
638
- enableProfile: import_koishi3.Schema.boolean().default(false).description("启用自定义昵称"),
639
- enableIO: import_koishi3.Schema.boolean().default(false).description("启用导入导出"),
640
- caveFormat: import_koishi3.Schema.string().default("回声洞 ——({id})|—— {name}").required().description("自定义文本"),
641
- adminUsers: import_koishi3.Schema.array(import_koishi3.Schema.string()).default([]).description("管理员 ID 列表")
651
+ var logger = new import_koishi2.Logger("best-cave");
652
+ var Config = import_koishi2.Schema.intersect([
653
+ import_koishi2.Schema.object({
654
+ coolDown: import_koishi2.Schema.number().default(10).description("冷却时间(秒)"),
655
+ perChannel: import_koishi2.Schema.boolean().default(false).description("启用分群模式"),
656
+ enableProfile: import_koishi2.Schema.boolean().default(false).description("启用自定义昵称"),
657
+ enableIO: import_koishi2.Schema.boolean().default(false).description("启用导入导出"),
658
+ caveFormat: import_koishi2.Schema.string().default("回声洞 ——({id})|—— {name}").description("自定义文本"),
659
+ adminUsers: import_koishi2.Schema.array(import_koishi2.Schema.string()).default([]).description("管理员 ID 列表")
642
660
  }).description("基础配置"),
643
- import_koishi3.Schema.object({
644
- enableReview: import_koishi3.Schema.boolean().default(false).description("启用审核")
661
+ import_koishi2.Schema.object({
662
+ enableReview: import_koishi2.Schema.boolean().default(false).description("启用审核")
645
663
  }).description("审核配置"),
646
- import_koishi3.Schema.object({
647
- localPath: import_koishi3.Schema.string().description("文件映射路径"),
648
- enableS3: import_koishi3.Schema.boolean().default(false).description("启用 S3 存储"),
649
- publicUrl: import_koishi3.Schema.string().description("公共访问 URL").role("link"),
650
- endpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link"),
651
- bucket: import_koishi3.Schema.string().description("存储桶 (Bucket)"),
652
- region: import_koishi3.Schema.string().default("auto").description("区域 (Region)"),
653
- accessKeyId: import_koishi3.Schema.string().description("Access Key ID").role("secret"),
654
- secretAccessKey: import_koishi3.Schema.string().description("Secret Access Key").role("secret")
664
+ import_koishi2.Schema.object({
665
+ localPath: import_koishi2.Schema.string().description("文件映射路径"),
666
+ enableS3: import_koishi2.Schema.boolean().default(false).description("启用 S3 存储"),
667
+ publicUrl: import_koishi2.Schema.string().description("公共访问 URL").role("link"),
668
+ endpoint: import_koishi2.Schema.string().description("端点 (Endpoint)").role("link"),
669
+ bucket: import_koishi2.Schema.string().description("存储桶 (Bucket)"),
670
+ region: import_koishi2.Schema.string().default("auto").description("区域 (Region)"),
671
+ accessKeyId: import_koishi2.Schema.string().description("Access Key ID").role("secret"),
672
+ secretAccessKey: import_koishi2.Schema.string().description("Secret Access Key").role("secret")
655
673
  }).description("存储配置")
656
674
  ]);
657
675
  function apply(ctx, config) {
@@ -702,45 +720,21 @@ function apply(ctx, config) {
702
720
  }
703
721
  });
704
722
  cave.subcommand(".add [content:text]", "添加回声洞").usage("添加一条回声洞。可以直接发送内容,也可以回复或引用一条消息。").action(async ({ session }, content) => {
705
- cleanupPendingDeletions(ctx, fileManager, logger);
706
- const savedFileIdentifiers = [];
707
723
  try {
708
724
  let sourceElements;
709
725
  if (session.quote?.elements) {
710
726
  sourceElements = session.quote.elements;
711
727
  } else if (content?.trim()) {
712
- sourceElements = import_koishi3.h.parse(content);
728
+ sourceElements = import_koishi2.h.parse(content);
713
729
  } else {
714
730
  await session.send("请在一分钟内发送你要添加的内容");
715
731
  const reply = await session.prompt(6e4);
716
732
  if (!reply) return "操作超时,已取消添加";
717
- sourceElements = import_koishi3.h.parse(reply);
733
+ sourceElements = import_koishi2.h.parse(reply);
718
734
  }
719
735
  const scopeQuery = getScopeQuery(session, config);
720
736
  const newId = await getNextCaveId(ctx, scopeQuery);
721
- const finalElementsForDb = [];
722
- let mediaIndex = 1;
723
- async function traverseAndProcess(elements) {
724
- for (const el of elements) {
725
- const elementType = el.type === "image" ? "img" : el.type;
726
- if (["img", "video", "audio", "file"].includes(elementType) && el.attrs.src) {
727
- let fileIdentifier = el.attrs.src;
728
- if (fileIdentifier.startsWith("http")) {
729
- mediaIndex++;
730
- const originalName = el.attrs.file;
731
- const savedId = await downloadMedia(ctx, fileManager, fileIdentifier, originalName, elementType, newId, mediaIndex, session.channelId, session.userId);
732
- savedFileIdentifiers.push(savedId);
733
- fileIdentifier = savedId;
734
- }
735
- finalElementsForDb.push({ type: elementType, file: fileIdentifier });
736
- } else if (elementType === "text" && el.attrs.content?.trim()) {
737
- finalElementsForDb.push({ type: "text", content: el.attrs.content.trim() });
738
- }
739
- if (el.children) await traverseAndProcess(el.children);
740
- }
741
- }
742
- __name(traverseAndProcess, "traverseAndProcess");
743
- await traverseAndProcess(sourceElements);
737
+ const { finalElementsForDb, mediaToDownload } = await prepareElementsForStorage(sourceElements, newId, session.channelId, session.userId);
744
738
  if (finalElementsForDb.length === 0) return "内容为空,已取消添加";
745
739
  let userName = session.username;
746
740
  if (config.enableProfile) {
@@ -756,6 +750,16 @@ function apply(ctx, config) {
756
750
  time: /* @__PURE__ */ new Date()
757
751
  };
758
752
  await ctx.database.create("cave", newCave);
753
+ try {
754
+ const downloadPromises = mediaToDownload.map(async (media) => {
755
+ const response = await ctx.http.get(media.url, { responseType: "arraybuffer", timeout: 3e4 });
756
+ await fileManager.saveFile(media.fileName, Buffer.from(response));
757
+ });
758
+ await Promise.all(downloadPromises);
759
+ } catch (fileError) {
760
+ await ctx.database.remove("cave", { id: newId });
761
+ throw fileError;
762
+ }
759
763
  if (newCave.status === "pending") {
760
764
  reviewManager.sendForReview(newCave);
761
765
  return `提交成功,序号为(${newCave.id})`;
@@ -763,10 +767,6 @@ function apply(ctx, config) {
763
767
  return `添加成功,序号为(${newId})`;
764
768
  } catch (error) {
765
769
  logger.error("添加回声洞失败:", error);
766
- if (savedFileIdentifiers.length > 0) {
767
- logger.info(`添加失败,回滚并删除 ${savedFileIdentifiers.length} 个文件...`);
768
- await Promise.all(savedFileIdentifiers.map((fileId) => fileManager.deleteFile(fileId)));
769
- }
770
770
  return "添加失败,请稍后再试";
771
771
  }
772
772
  });
@@ -797,13 +797,10 @@ function apply(ctx, config) {
797
797
  if (!isOwner && !isAdmin) {
798
798
  return "抱歉,你没有权限删除这条回声洞";
799
799
  }
800
+ const caveMessage = await buildCaveMessage(targetCave, config, fileManager, logger);
800
801
  await ctx.database.upsert("cave", [{ id, status: "delete" }]);
802
+ session.send([`已删除`, ...caveMessage]);
801
803
  cleanupPendingDeletions(ctx, fileManager, logger);
802
- const caveMessage = await buildCaveMessage(targetCave, config, fileManager, logger);
803
- return [
804
- (0, import_koishi3.h)("p", {}, `以下内容已删除`),
805
- ...caveMessage
806
- ];
807
804
  } catch (error) {
808
805
  logger.error(`标记回声洞(${id})失败:`, error);
809
806
  return "删除失败,请稍后再试";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-best-cave",
3
3
  "description": "最强大的回声洞现已重构完成啦!注意数据格式需要使用脚本转换哦~",
4
- "version": "2.0.2",
4
+ "version": "2.0.4",
5
5
  "contributors": [
6
6
  "Yis_Rime <yis_rime@outlook.com>"
7
7
  ],