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 +15 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +74 -77
- package/package.json +1 -1
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' | '
|
|
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
|
|
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 "
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
634
|
-
var Config =
|
|
635
|
-
|
|
636
|
-
coolDown:
|
|
637
|
-
perChannel:
|
|
638
|
-
enableProfile:
|
|
639
|
-
enableIO:
|
|
640
|
-
caveFormat:
|
|
641
|
-
adminUsers:
|
|
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
|
-
|
|
644
|
-
enableReview:
|
|
661
|
+
import_koishi2.Schema.object({
|
|
662
|
+
enableReview: import_koishi2.Schema.boolean().default(false).description("启用审核")
|
|
645
663
|
}).description("审核配置"),
|
|
646
|
-
|
|
647
|
-
localPath:
|
|
648
|
-
enableS3:
|
|
649
|
-
publicUrl:
|
|
650
|
-
endpoint:
|
|
651
|
-
bucket:
|
|
652
|
-
region:
|
|
653
|
-
accessKeyId:
|
|
654
|
-
secretAccessKey:
|
|
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 =
|
|
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 =
|
|
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 "删除失败,请稍后再试";
|