koishi-plugin-best-cave 1.4.0 → 1.5.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/index.d.ts +4 -3
- package/lib/index.js +675 -423
- package/lib/utils/AuditManage.d.ts +43 -0
- package/lib/utils/ContentHash.d.ts +87 -0
- package/lib/utils/FileHandle.d.ts +63 -0
- package/lib/utils/HashManage.d.ts +108 -0
- package/lib/utils/HashStorage.d.ts +5 -0
- package/lib/utils/IdManage.d.ts +69 -0
- package/package.json +1 -1
- package/readme.md +16 -4
package/lib/index.js
CHANGED
|
@@ -33,14 +33,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
33
33
|
// src/locales/zh-CN.yml
|
|
34
34
|
var require_zh_CN = __commonJS({
|
|
35
35
|
"src/locales/zh-CN.yml"(exports2, module2) {
|
|
36
|
-
module2.exports = { _config: { manager: "管理员", number: "冷却时间(秒)", enableAudit: "启用审核",
|
|
36
|
+
module2.exports = { _config: { manager: "管理员", number: "冷却时间(秒)", enableAudit: "启用审核", enableTextDuplicate: "启用文本查重", textDuplicateThreshold: "文本相似度阈值(0-1)", enableImageDuplicate: "启用图片查重", imageDuplicateThreshold: "图片相似度阈值(0-1)", imageMaxSize: "图片最大大小(MB)", allowVideo: "允许视频上传", videoMaxSize: "视频最大大小(MB)", enablePagination: "启用统计分页", itemsPerPage: "每页显示数目", blacklist: "黑名单(用户)", whitelist: "审核白名单(用户/群组/频道)" }, commands: { cave: { description: "回声洞", usage: "支持添加、抽取、查看、管理回声洞", examples: "使用 cave 随机抽取回声洞\n使用 -a 直接添加或引用添加\n使用 -g 查看指定回声洞\n使用 -r 删除指定回声洞", options: { a: "添加回声洞", g: "查看回声洞", r: "删除回声洞", p: "通过审核(批量)", d: "拒绝审核(批量)", l: "查询投稿统计" }, add: { noContent: "请在一分钟内发送内容", operationTimeout: "操作超时,添加取消", videoDisabled: "不允许上传视频", submitPending: "提交成功,序号为({0})", addSuccess: "添加成功,序号为({0})", mediaSizeExceeded: "{0}文件大小超过限制", localFileNotAllowed: "检测到本地文件路径,无法保存" }, remove: { noPermission: "你无权删除他人添加的回声洞", deletePending: "删除(待审核)", deleted: "已删除" }, list: { pageInfo: "第 {0} / {1} 页", header: "当前共有 {0} 项回声洞:", totalItems: "用户 {0} 共计投稿 {1} 项:", idsLine: "{0}" }, audit: { noPending: "暂无待审核回声洞", pendingNotFound: "未找到待审核回声洞", pendingResult: "{0},剩余 {1} 个待审核回声洞:[{2}]", auditPassed: "已通过", auditRejected: "已拒绝", batchAuditResult: "已{0} {1}/{2} 项回声洞", title: "待审核回声洞:", from: "投稿人:", sendFailed: "发送审核消息失败,无法联系管理员 {0}" }, error: { noContent: "回声洞内容为空", getCave: "获取回声洞失败", noCave: "没有回声洞", invalidId: "请输入有效的回声洞ID", notFound: "未找到该回声洞", exactDuplicateFound: "发现完全相同的", similarDuplicateFound: "发现相似度为 {0}% 的", addFailed: "添加失败,请稍后重试。" }, message: { blacklisted: "你已被列入黑名单", managerOnly: "此操作仅限管理员可用", cooldown: "群聊冷却中...请在 {0} 秒后重试", caveTitle: "回声洞 —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0}文件大小超过限制" } } } };
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// src/locales/en-US.yml
|
|
41
41
|
var require_en_US = __commonJS({
|
|
42
42
|
"src/locales/en-US.yml"(exports2, module2) {
|
|
43
|
-
module2.exports = { _config: { manager: "Administrator", number: "Cooldown time (seconds)", enableAudit: "Enable moderation",
|
|
43
|
+
module2.exports = { _config: { manager: "Administrator", number: "Cooldown time (seconds)", enableAudit: "Enable moderation", enableTextDuplicate: "Enable text duplicate check", textDuplicateThreshold: "Text similarity threshold (0-1)", enableImageDuplicate: "Enable image duplicate check", imageDuplicateThreshold: "Image similarity threshold (0-1)", imageMaxSize: "Maximum image size (MB)", allowVideo: "Allow video upload", videoMaxSize: "Maximum video size (MB)", enablePagination: "Enable statistics pagination", itemsPerPage: "Items per page", blacklist: "Blacklist (users)", whitelist: "Moderation whitelist (users/groups/channels)" }, commands: { cave: { description: "Echo Cave", usage: "Support adding, drawing, viewing, and managing echo caves", examples: "Use cave to randomly draw an echo\nUse -a to add directly or add by reference\nUse -g to view specific echo\nUse -r to delete specific echo", options: { a: "Add echo", g: "View echo", r: "Delete echo", p: "Approve moderation (batch)", d: "Reject moderation (batch)", l: "Query submission statistics" }, add: { noContent: "Please send content within one minute", operationTimeout: "Operation timeout, addition cancelled", videoDisabled: "Video upload not allowed", submitPending: "Submission successful, ID is ({0})", addSuccess: "Added successfully, ID is ({0})", mediaSizeExceeded: "{0} file size exceeds limit", localFileNotAllowed: "Local file path detected, cannot save" }, remove: { noPermission: "You don't have permission to delete others' echos", deletePending: "Delete (pending review)", deleted: "Deleted" }, list: { pageInfo: "Page {0} / {1}", header: "Currently there are {0} echos:", totalItems: "User {0} has submitted {1} items:", idsLine: "{0}" }, audit: { noPending: "No pending echos for review", pendingNotFound: "Pending echo not found", pendingResult: "{0}, {1} pending echos remaining: [{2}]", auditPassed: "Approved", auditRejected: "Rejected", batchAuditResult: "{0} {1}/{2} echos", title: "Pending echos:", from: "Submitted by:", sendFailed: "Failed to send moderation message, cannot contact administrator {0}" }, error: { noContent: "Echo content is empty", getCave: "Failed to get echo", noCave: "No echos available", invalidId: "Please enter a valid echo ID", notFound: "Echo not found", exactDuplicateFound: "Found exactly identical", similarDuplicateFound: "Found {0}% similar", addFailed: "Add failed, please try again later." }, message: { blacklisted: "You have been blacklisted", managerOnly: "This operation is limited to administrators only", cooldown: "Group chat cooling down... Please try again in {0} seconds", caveTitle: "Echo Cave —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0} file size exceeds limit" } } } };
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
|
|
@@ -53,11 +53,11 @@ __export(src_exports, {
|
|
|
53
53
|
name: () => name
|
|
54
54
|
});
|
|
55
55
|
module.exports = __toCommonJS(src_exports);
|
|
56
|
-
var
|
|
57
|
-
var
|
|
58
|
-
var
|
|
56
|
+
var import_koishi5 = require("koishi");
|
|
57
|
+
var fs5 = __toESM(require("fs"));
|
|
58
|
+
var path5 = __toESM(require("path"));
|
|
59
59
|
|
|
60
|
-
// src/utils/
|
|
60
|
+
// src/utils/FileHandle.ts
|
|
61
61
|
var fs = __toESM(require("fs"));
|
|
62
62
|
var path = __toESM(require("path"));
|
|
63
63
|
var import_koishi = require("koishi");
|
|
@@ -216,11 +216,11 @@ var FileHandler = class {
|
|
|
216
216
|
}
|
|
217
217
|
};
|
|
218
218
|
|
|
219
|
-
// src/utils/
|
|
219
|
+
// src/utils/IdManage.ts
|
|
220
220
|
var fs2 = __toESM(require("fs"));
|
|
221
221
|
var path2 = __toESM(require("path"));
|
|
222
222
|
var import_koishi2 = require("koishi");
|
|
223
|
-
var logger2 = new import_koishi2.Logger("
|
|
223
|
+
var logger2 = new import_koishi2.Logger("IdManager");
|
|
224
224
|
var IdManager = class {
|
|
225
225
|
static {
|
|
226
226
|
__name(this, "IdManager");
|
|
@@ -288,11 +288,15 @@ var IdManager = class {
|
|
|
288
288
|
...status.deletedIds || [],
|
|
289
289
|
0
|
|
290
290
|
);
|
|
291
|
-
this.deletedIds = new Set(
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
this.deletedIds = new Set(status.deletedIds || []);
|
|
292
|
+
for (let i = 1; i <= this.maxId; i++) {
|
|
293
|
+
if (!this.usedIds.has(i)) {
|
|
294
|
+
this.deletedIds.add(i);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
294
297
|
await this.saveStatus();
|
|
295
298
|
this.initialized = true;
|
|
299
|
+
logger2.success(`Cave ID Manager initialized with ${this.maxId}(-${this.deletedIds.size}) IDs`);
|
|
296
300
|
} catch (error) {
|
|
297
301
|
this.initialized = false;
|
|
298
302
|
logger2.error(`ID Manager initialization failed: ${error.message}`);
|
|
@@ -430,16 +434,16 @@ var IdManager = class {
|
|
|
430
434
|
}
|
|
431
435
|
};
|
|
432
436
|
|
|
433
|
-
// src/utils/
|
|
437
|
+
// src/utils/HashManage.ts
|
|
434
438
|
var import_koishi3 = require("koishi");
|
|
435
439
|
var fs3 = __toESM(require("fs"));
|
|
436
440
|
var path3 = __toESM(require("path"));
|
|
437
441
|
|
|
438
|
-
// src/utils/
|
|
442
|
+
// src/utils/ContentHash.ts
|
|
439
443
|
var import_sharp = __toESM(require("sharp"));
|
|
440
|
-
var
|
|
444
|
+
var ContentHasher = class {
|
|
441
445
|
static {
|
|
442
|
-
__name(this, "
|
|
446
|
+
__name(this, "ContentHasher");
|
|
443
447
|
}
|
|
444
448
|
/**
|
|
445
449
|
* 计算图片的感知哈希值
|
|
@@ -580,6 +584,21 @@ var ImageHasher = class {
|
|
|
580
584
|
const distance = this.calculateDistance(hash1, hash2);
|
|
581
585
|
return (64 - distance) / 64;
|
|
582
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* 计算文本的哈希值
|
|
589
|
+
* @param text - 输入文本
|
|
590
|
+
* @returns 文本的哈希值(36进制字符串)
|
|
591
|
+
*/
|
|
592
|
+
static calculateTextHash(text) {
|
|
593
|
+
const normalizedText = text.toLowerCase().trim().replace(/\s+/g, " ");
|
|
594
|
+
let hash = 0;
|
|
595
|
+
for (let i = 0; i < normalizedText.length; i++) {
|
|
596
|
+
const char = normalizedText.charCodeAt(i);
|
|
597
|
+
hash = (hash << 5) - hash + char;
|
|
598
|
+
hash = hash & hash;
|
|
599
|
+
}
|
|
600
|
+
return hash.toString(36);
|
|
601
|
+
}
|
|
583
602
|
/**
|
|
584
603
|
* 批量比较一个新哈希值与多个已存在哈希值的相似度
|
|
585
604
|
* @param newHash - 新的哈希值
|
|
@@ -591,93 +610,199 @@ var ImageHasher = class {
|
|
|
591
610
|
}
|
|
592
611
|
};
|
|
593
612
|
|
|
594
|
-
// src/utils/
|
|
595
|
-
var
|
|
596
|
-
var logger3 = new import_koishi3.Logger("
|
|
597
|
-
var
|
|
613
|
+
// src/utils/HashManage.ts
|
|
614
|
+
var import_util = require("util");
|
|
615
|
+
var logger3 = new import_koishi3.Logger("HashManager");
|
|
616
|
+
var readFileAsync = (0, import_util.promisify)(fs3.readFile);
|
|
617
|
+
var ContentHashManager = class _ContentHashManager {
|
|
598
618
|
/**
|
|
599
|
-
*
|
|
600
|
-
* @param caveDir
|
|
619
|
+
* 初始化HashManager实例
|
|
620
|
+
* @param caveDir 回声洞数据目录路径
|
|
601
621
|
*/
|
|
602
622
|
constructor(caveDir) {
|
|
603
623
|
this.caveDir = caveDir;
|
|
604
|
-
this.filePath = path3.join(caveDir, "hash.json");
|
|
605
|
-
this.resourceDir = path3.join(caveDir, "resources");
|
|
606
|
-
this.caveFilePath = path3.join(caveDir, "cave.json");
|
|
607
624
|
}
|
|
608
625
|
static {
|
|
609
|
-
__name(this, "
|
|
626
|
+
__name(this, "ContentHashManager");
|
|
610
627
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
628
|
+
// 哈希数据文件名
|
|
629
|
+
static HASH_FILE = "hash.json";
|
|
630
|
+
// 回声洞数据文件名
|
|
631
|
+
static CAVE_FILE = "cave.json";
|
|
632
|
+
// 批处理大小
|
|
633
|
+
static BATCH_SIZE = 50;
|
|
634
|
+
// 存储回声洞ID到图片哈希值的映射
|
|
614
635
|
imageHashes = /* @__PURE__ */ new Map();
|
|
615
636
|
textHashes = /* @__PURE__ */ new Map();
|
|
637
|
+
// 初始化状态标志
|
|
616
638
|
initialized = false;
|
|
639
|
+
get filePath() {
|
|
640
|
+
return path3.join(this.caveDir, _ContentHashManager.HASH_FILE);
|
|
641
|
+
}
|
|
642
|
+
get resourceDir() {
|
|
643
|
+
return path3.join(this.caveDir, "resources");
|
|
644
|
+
}
|
|
645
|
+
get caveFilePath() {
|
|
646
|
+
return path3.join(this.caveDir, _ContentHashManager.CAVE_FILE);
|
|
647
|
+
}
|
|
617
648
|
/**
|
|
618
649
|
* 初始化哈希存储
|
|
650
|
+
* 读取现有哈希数据或重新构建哈希值
|
|
619
651
|
* @throws 初始化失败时抛出错误
|
|
620
652
|
*/
|
|
621
653
|
async initialize() {
|
|
622
654
|
if (this.initialized) return;
|
|
623
655
|
try {
|
|
624
656
|
const hashData = await FileHandler.readJsonData(this.filePath).then((data) => data[0]).catch(() => null);
|
|
625
|
-
if (!hashData?.imageHashes) {
|
|
657
|
+
if (!hashData?.imageHashes || !hashData?.textHashes || Object.keys(hashData.imageHashes).length === 0) {
|
|
658
|
+
this.imageHashes.clear();
|
|
659
|
+
this.textHashes.clear();
|
|
626
660
|
await this.buildInitialHashes();
|
|
627
661
|
} else {
|
|
628
|
-
this.
|
|
662
|
+
this.imageHashes = new Map(
|
|
663
|
+
Object.entries(hashData.imageHashes).map(([k, v]) => [Number(k), v])
|
|
664
|
+
);
|
|
665
|
+
this.textHashes = new Map(
|
|
666
|
+
Object.entries(hashData.textHashes).map(([k, v]) => [Number(k), v])
|
|
667
|
+
);
|
|
629
668
|
await this.updateMissingHashes();
|
|
630
669
|
}
|
|
670
|
+
const totalCaves = (/* @__PURE__ */ new Set([...this.imageHashes.keys(), ...this.textHashes.keys()])).size;
|
|
631
671
|
this.initialized = true;
|
|
672
|
+
logger3.success(`Cave Hash Manager initialized with ${totalCaves} hashes`);
|
|
632
673
|
} catch (error) {
|
|
633
|
-
logger3.error(`
|
|
674
|
+
logger3.error(`Initialization failed: ${error.message}`);
|
|
675
|
+
this.initialized = false;
|
|
634
676
|
throw error;
|
|
635
677
|
}
|
|
636
678
|
}
|
|
637
679
|
/**
|
|
638
|
-
*
|
|
639
|
-
* @
|
|
640
|
-
* @private
|
|
680
|
+
* 获取当前哈希存储状态
|
|
681
|
+
* @returns 包含最后更新时间和所有条目的状态对象
|
|
641
682
|
*/
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
683
|
+
async getStatus() {
|
|
684
|
+
if (!this.initialized) await this.initialize();
|
|
685
|
+
return {
|
|
686
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
687
|
+
entries: Array.from(this.imageHashes.entries()).map(([caveId, imgHashes]) => ({
|
|
688
|
+
caveId,
|
|
689
|
+
imageHashes: imgHashes,
|
|
690
|
+
textHashes: this.textHashes.get(caveId) || []
|
|
691
|
+
}))
|
|
692
|
+
};
|
|
645
693
|
}
|
|
646
694
|
/**
|
|
647
|
-
*
|
|
648
|
-
* @param caveId
|
|
649
|
-
* @param
|
|
650
|
-
* @param content - 要计算哈希的内容
|
|
695
|
+
* 更新指定回声洞的图片哈希值
|
|
696
|
+
* @param caveId 回声洞ID
|
|
697
|
+
* @param content 图片buffer数组
|
|
651
698
|
*/
|
|
652
|
-
async
|
|
699
|
+
async updateCaveContent(caveId, content) {
|
|
653
700
|
if (!this.initialized) await this.initialize();
|
|
654
701
|
try {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
702
|
+
if (content.images?.length) {
|
|
703
|
+
const imageHashes = await Promise.all(
|
|
704
|
+
content.images.map((buffer) => ContentHasher.calculateHash(buffer))
|
|
705
|
+
);
|
|
706
|
+
this.imageHashes.set(caveId, imageHashes);
|
|
707
|
+
}
|
|
708
|
+
if (content.texts?.length) {
|
|
709
|
+
const textHashes = content.texts.map((text) => ContentHasher.calculateTextHash(text));
|
|
710
|
+
this.textHashes.set(caveId, textHashes);
|
|
711
|
+
}
|
|
712
|
+
if (!content.images && !content.texts) {
|
|
713
|
+
this.imageHashes.delete(caveId);
|
|
714
|
+
this.textHashes.delete(caveId);
|
|
715
|
+
}
|
|
716
|
+
await this.saveContentHashes();
|
|
660
717
|
} catch (error) {
|
|
661
|
-
logger3.error(`Failed to update
|
|
718
|
+
logger3.error(`Failed to update content hashes (cave ${caveId}): ${error.message}`);
|
|
662
719
|
}
|
|
663
720
|
}
|
|
664
721
|
/**
|
|
665
|
-
*
|
|
666
|
-
* @param
|
|
667
|
-
* @param hashes - 要查找的哈希值数组
|
|
668
|
-
* @param threshold - 相似度阈值,默认为1
|
|
669
|
-
* @returns 匹配结果数组
|
|
722
|
+
* 更新所有回声洞的哈希值
|
|
723
|
+
* @param isInitialBuild 是否为初始构建
|
|
670
724
|
*/
|
|
671
|
-
async
|
|
725
|
+
async updateAllCaves(isInitialBuild = false) {
|
|
726
|
+
if (!this.initialized && !isInitialBuild) {
|
|
727
|
+
await this.initialize();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
logger3.info("Starting full hash update...");
|
|
732
|
+
const caveData = await this.loadCaveData();
|
|
733
|
+
const cavesWithImages = caveData.filter(
|
|
734
|
+
(cave) => cave.elements?.some((el) => el.type === "img" && el.file)
|
|
735
|
+
);
|
|
736
|
+
this.imageHashes.clear();
|
|
737
|
+
let processedCount = 0;
|
|
738
|
+
const totalImages = cavesWithImages.length;
|
|
739
|
+
const processCave = /* @__PURE__ */ __name(async (cave) => {
|
|
740
|
+
const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
|
|
741
|
+
if (imgElements.length === 0) return;
|
|
742
|
+
try {
|
|
743
|
+
const hashes = await Promise.all(
|
|
744
|
+
imgElements.map(async (imgElement) => {
|
|
745
|
+
const filePath = path3.join(this.resourceDir, imgElement.file);
|
|
746
|
+
if (!fs3.existsSync(filePath)) {
|
|
747
|
+
logger3.warn(`Image file not found: ${filePath}`);
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
const imgBuffer = await readFileAsync(filePath);
|
|
751
|
+
return await ContentHasher.calculateHash(imgBuffer);
|
|
752
|
+
})
|
|
753
|
+
);
|
|
754
|
+
const validHashes = hashes.filter((hash) => hash !== null);
|
|
755
|
+
if (validHashes.length > 0) {
|
|
756
|
+
this.imageHashes.set(cave.cave_id, validHashes);
|
|
757
|
+
processedCount++;
|
|
758
|
+
if (processedCount % 100 === 0) {
|
|
759
|
+
logger3.info(`Progress: ${processedCount}/${totalImages}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
} catch (error) {
|
|
763
|
+
logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
|
|
764
|
+
}
|
|
765
|
+
}, "processCave");
|
|
766
|
+
await this.processBatch(cavesWithImages, processCave);
|
|
767
|
+
await this.saveContentHashes();
|
|
768
|
+
logger3.success(`Update completed. Processed ${processedCount}/${totalImages} images`);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
logger3.error(`Full update failed: ${error.message}`);
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* 查找重复的图片
|
|
776
|
+
* @param content 待查找的图片buffer数组
|
|
777
|
+
* @param thresholds 相似度阈值
|
|
778
|
+
* @returns 匹配结果数组,包含索引、回声洞ID和相似度
|
|
779
|
+
*/
|
|
780
|
+
async findDuplicates(content, thresholds) {
|
|
672
781
|
if (!this.initialized) await this.initialize();
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
782
|
+
const results = [];
|
|
783
|
+
if (content.images?.length) {
|
|
784
|
+
const imageResults = await this.findImageDuplicates(content.images, thresholds.image);
|
|
785
|
+
results.push(...imageResults.map(
|
|
786
|
+
(result) => result ? { ...result, type: "image" } : null
|
|
787
|
+
));
|
|
788
|
+
}
|
|
789
|
+
if (content.texts?.length) {
|
|
790
|
+
const textResults = await this.findTextDuplicates(content.texts, thresholds.text);
|
|
791
|
+
results.push(...textResults.map(
|
|
792
|
+
(result) => result ? { ...result, type: "text" } : null
|
|
793
|
+
));
|
|
794
|
+
}
|
|
795
|
+
return results;
|
|
796
|
+
}
|
|
797
|
+
async findTextDuplicates(texts, threshold) {
|
|
798
|
+
const inputHashes = texts.map((text) => ContentHasher.calculateTextHash(text));
|
|
799
|
+
const existingHashes = Array.from(this.textHashes.entries());
|
|
800
|
+
return inputHashes.map((hash, index) => {
|
|
676
801
|
let maxSimilarity = 0;
|
|
677
802
|
let matchedCaveId = null;
|
|
678
|
-
for (const [caveId,
|
|
679
|
-
for (const existingHash of
|
|
680
|
-
const similarity =
|
|
803
|
+
for (const [caveId, hashes] of existingHashes) {
|
|
804
|
+
for (const existingHash of hashes) {
|
|
805
|
+
const similarity = this.calculateTextSimilarity(hash, existingHash);
|
|
681
806
|
if (similarity >= threshold && similarity > maxSimilarity) {
|
|
682
807
|
maxSimilarity = similarity;
|
|
683
808
|
matchedCaveId = caveId;
|
|
@@ -686,37 +811,122 @@ var HashStorage = class _HashStorage {
|
|
|
686
811
|
}
|
|
687
812
|
if (maxSimilarity === 1) break;
|
|
688
813
|
}
|
|
689
|
-
return matchedCaveId ? {
|
|
814
|
+
return matchedCaveId ? {
|
|
815
|
+
index,
|
|
816
|
+
caveId: matchedCaveId,
|
|
817
|
+
similarity: maxSimilarity
|
|
818
|
+
} : null;
|
|
690
819
|
});
|
|
691
820
|
}
|
|
821
|
+
calculateTextSimilarity(hash1, hash2) {
|
|
822
|
+
if (hash1 === hash2) return 1;
|
|
823
|
+
const length = Math.max(hash1.length, hash2.length);
|
|
824
|
+
let matches = 0;
|
|
825
|
+
for (let i = 0; i < length; i++) {
|
|
826
|
+
if (hash1[i] === hash2[i]) matches++;
|
|
827
|
+
}
|
|
828
|
+
return matches / length;
|
|
829
|
+
}
|
|
830
|
+
// 重命名原有的图片哈希相关方法
|
|
831
|
+
async findImageDuplicates(images, threshold) {
|
|
832
|
+
if (!this.initialized) await this.initialize();
|
|
833
|
+
const inputHashes = await Promise.all(
|
|
834
|
+
images.map((buffer) => ContentHasher.calculateHash(buffer))
|
|
835
|
+
);
|
|
836
|
+
const existingHashes = Array.from(this.imageHashes.entries());
|
|
837
|
+
return Promise.all(
|
|
838
|
+
inputHashes.map(async (hash, index) => {
|
|
839
|
+
try {
|
|
840
|
+
let maxSimilarity = 0;
|
|
841
|
+
let matchedCaveId = null;
|
|
842
|
+
for (const [caveId, hashes] of existingHashes) {
|
|
843
|
+
for (const existingHash of hashes) {
|
|
844
|
+
const similarity = ContentHasher.calculateSimilarity(hash, existingHash);
|
|
845
|
+
if (similarity >= threshold && similarity > maxSimilarity) {
|
|
846
|
+
maxSimilarity = similarity;
|
|
847
|
+
matchedCaveId = caveId;
|
|
848
|
+
if (Math.abs(similarity - 1) < Number.EPSILON) break;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (Math.abs(maxSimilarity - 1) < Number.EPSILON) break;
|
|
852
|
+
}
|
|
853
|
+
return matchedCaveId ? {
|
|
854
|
+
index,
|
|
855
|
+
caveId: matchedCaveId,
|
|
856
|
+
similarity: maxSimilarity
|
|
857
|
+
} : null;
|
|
858
|
+
} catch (error) {
|
|
859
|
+
logger3.warn(`处理图片 ${index} 失败: ${error.message}`);
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
})
|
|
863
|
+
);
|
|
864
|
+
}
|
|
692
865
|
/**
|
|
693
|
-
*
|
|
694
|
-
* @
|
|
866
|
+
* 加载回声洞数据
|
|
867
|
+
* @returns 回声洞数据数组
|
|
868
|
+
* @private
|
|
695
869
|
*/
|
|
696
|
-
async
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
870
|
+
async loadCaveData() {
|
|
871
|
+
const data = await FileHandler.readJsonData(this.caveFilePath);
|
|
872
|
+
return Array.isArray(data) ? data.flat() : [];
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* 保存哈希数据到文件
|
|
876
|
+
* @private
|
|
877
|
+
*/
|
|
878
|
+
async saveContentHashes() {
|
|
879
|
+
const data = {
|
|
880
|
+
imageHashes: Object.fromEntries(this.imageHashes),
|
|
881
|
+
textHashes: Object.fromEntries(this.textHashes),
|
|
882
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
883
|
+
};
|
|
884
|
+
await FileHandler.writeJsonData(this.filePath, [data]);
|
|
701
885
|
}
|
|
702
886
|
/**
|
|
703
|
-
*
|
|
887
|
+
* 构建初始哈希数据
|
|
704
888
|
* @private
|
|
705
889
|
*/
|
|
706
890
|
async buildInitialHashes() {
|
|
707
891
|
const caveData = await this.loadCaveData();
|
|
708
892
|
let processedCount = 0;
|
|
709
|
-
const
|
|
893
|
+
const totalCaves = caveData.length;
|
|
894
|
+
logger3.info(`Building hash data for ${totalCaves} caves...`);
|
|
710
895
|
for (const cave of caveData) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
896
|
+
try {
|
|
897
|
+
const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
|
|
898
|
+
if (imgElements.length > 0) {
|
|
899
|
+
const hashes = await Promise.all(
|
|
900
|
+
imgElements.map(async (imgElement) => {
|
|
901
|
+
const filePath = path3.join(this.resourceDir, imgElement.file);
|
|
902
|
+
if (!fs3.existsSync(filePath)) {
|
|
903
|
+
logger3.warn(`Image not found: ${filePath}`);
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
const imgBuffer = await fs3.promises.readFile(filePath);
|
|
907
|
+
return await ContentHasher.calculateHash(imgBuffer);
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
const validHashes = hashes.filter((hash) => hash !== null);
|
|
911
|
+
if (validHashes.length > 0) {
|
|
912
|
+
this.imageHashes.set(cave.cave_id, validHashes);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const textElements = cave.elements?.filter((el) => el.type === "text" && el.content) || [];
|
|
916
|
+
if (textElements.length > 0) {
|
|
917
|
+
const textHashes = textElements.map((el) => ContentHasher.calculateTextHash(el.content));
|
|
918
|
+
this.textHashes.set(cave.cave_id, textHashes);
|
|
919
|
+
}
|
|
920
|
+
processedCount++;
|
|
921
|
+
if (processedCount % 100 === 0) {
|
|
922
|
+
logger3.info(`Progress: ${processedCount}/${totalCaves} caves`);
|
|
923
|
+
}
|
|
924
|
+
} catch (error) {
|
|
925
|
+
logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
|
|
716
926
|
}
|
|
717
927
|
}
|
|
718
|
-
await this.
|
|
719
|
-
logger3.
|
|
928
|
+
await this.saveContentHashes();
|
|
929
|
+
logger3.success(`Build completed. Processed ${processedCount}/${totalCaves} caves`);
|
|
720
930
|
}
|
|
721
931
|
/**
|
|
722
932
|
* 更新缺失的哈希值
|
|
@@ -724,128 +934,285 @@ var HashStorage = class _HashStorage {
|
|
|
724
934
|
*/
|
|
725
935
|
async updateMissingHashes() {
|
|
726
936
|
const caveData = await this.loadCaveData();
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
await
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
937
|
+
let updatedCount = 0;
|
|
938
|
+
for (const cave of caveData) {
|
|
939
|
+
if (this.imageHashes.has(cave.cave_id)) continue;
|
|
940
|
+
const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
|
|
941
|
+
if (imgElements.length === 0) continue;
|
|
942
|
+
try {
|
|
943
|
+
const hashes = await Promise.all(
|
|
944
|
+
imgElements.map(async (imgElement) => {
|
|
945
|
+
const filePath = path3.join(this.resourceDir, imgElement.file);
|
|
946
|
+
if (!fs3.existsSync(filePath)) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
const imgBuffer = await fs3.promises.readFile(filePath);
|
|
950
|
+
return ContentHasher.calculateHash(imgBuffer);
|
|
951
|
+
})
|
|
952
|
+
);
|
|
953
|
+
const validHashes = hashes.filter((hash) => hash !== null);
|
|
954
|
+
if (validHashes.length > 0) {
|
|
955
|
+
this.imageHashes.set(cave.cave_id, validHashes);
|
|
956
|
+
updatedCount++;
|
|
744
957
|
}
|
|
958
|
+
} catch (error) {
|
|
959
|
+
logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
|
|
745
960
|
}
|
|
746
|
-
await this.saveHashes();
|
|
747
|
-
logger3.info(`Updated ${missingImageCaves.length} missing images and ${missingTextCaves.length} missing texts`);
|
|
748
961
|
}
|
|
749
962
|
}
|
|
750
963
|
/**
|
|
751
|
-
*
|
|
752
|
-
* @param
|
|
964
|
+
* 批量处理数组项
|
|
965
|
+
* @param items 待处理项数组
|
|
966
|
+
* @param processor 处理函数
|
|
967
|
+
* @param batchSize 批处理大小
|
|
753
968
|
* @private
|
|
754
969
|
*/
|
|
755
|
-
async
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
970
|
+
async processBatch(items, processor, batchSize = _ContentHashManager.BATCH_SIZE) {
|
|
971
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
972
|
+
const batch = items.slice(i, i + batchSize);
|
|
973
|
+
await Promise.all(
|
|
974
|
+
batch.map(async (item) => {
|
|
975
|
+
try {
|
|
976
|
+
await processor(item);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
logger3.error(`Batch processing error: ${error.message}`);
|
|
979
|
+
}
|
|
980
|
+
})
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
// src/utils/AuditManage.ts
|
|
987
|
+
var import_koishi4 = require("koishi");
|
|
988
|
+
var fs4 = __toESM(require("fs"));
|
|
989
|
+
var path4 = __toESM(require("path"));
|
|
990
|
+
var AuditManager = class {
|
|
991
|
+
constructor(ctx, config, caveDir, idManager) {
|
|
992
|
+
this.ctx = ctx;
|
|
993
|
+
this.config = config;
|
|
994
|
+
this.caveDir = caveDir;
|
|
995
|
+
this.idManager = idManager;
|
|
996
|
+
}
|
|
997
|
+
static {
|
|
998
|
+
__name(this, "AuditManager");
|
|
999
|
+
}
|
|
1000
|
+
logger = new import_koishi4.Logger("AuditManager");
|
|
1001
|
+
async processAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, targetId) {
|
|
1002
|
+
if (pendingData.length === 0) {
|
|
1003
|
+
return this.sendMessage(session, "commands.cave.audit.noPending", [], true);
|
|
1004
|
+
}
|
|
1005
|
+
if (typeof targetId === "number") {
|
|
1006
|
+
return await this.handleSingleAudit(
|
|
1007
|
+
pendingData,
|
|
1008
|
+
isApprove,
|
|
1009
|
+
caveFilePath,
|
|
1010
|
+
resourceDir,
|
|
1011
|
+
pendingFilePath,
|
|
1012
|
+
targetId,
|
|
1013
|
+
session
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return await this.handleBatchAudit(
|
|
1017
|
+
pendingData,
|
|
1018
|
+
isApprove,
|
|
1019
|
+
caveFilePath,
|
|
1020
|
+
resourceDir,
|
|
1021
|
+
pendingFilePath,
|
|
1022
|
+
session
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
async handleSingleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, targetId, session) {
|
|
1026
|
+
const targetCave = pendingData.find((item) => item.cave_id === targetId);
|
|
1027
|
+
if (!targetCave) {
|
|
1028
|
+
return this.sendMessage(session, "commands.cave.audit.pendingNotFound", [], true);
|
|
1029
|
+
}
|
|
1030
|
+
const newPendingData = pendingData.filter((item) => item.cave_id !== targetId);
|
|
1031
|
+
if (isApprove) {
|
|
1032
|
+
const oldCaveData = await FileHandler.readJsonData(caveFilePath);
|
|
1033
|
+
const newCaveData = [...oldCaveData, {
|
|
1034
|
+
...targetCave,
|
|
1035
|
+
cave_id: targetId,
|
|
1036
|
+
elements: this.cleanElementsForSave(targetCave.elements, false)
|
|
1037
|
+
}];
|
|
1038
|
+
await FileHandler.withTransaction([
|
|
1039
|
+
{
|
|
1040
|
+
filePath: caveFilePath,
|
|
1041
|
+
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, newCaveData), "operation"),
|
|
1042
|
+
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldCaveData), "rollback")
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
filePath: pendingFilePath,
|
|
1046
|
+
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, newPendingData), "operation"),
|
|
1047
|
+
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
|
|
1048
|
+
}
|
|
1049
|
+
]);
|
|
1050
|
+
await this.idManager.addStat(targetCave.contributor_number, targetId);
|
|
1051
|
+
} else {
|
|
1052
|
+
await FileHandler.writeJsonData(pendingFilePath, newPendingData);
|
|
1053
|
+
await this.idManager.markDeleted(targetId);
|
|
1054
|
+
await this.deleteMediaFiles(targetCave, resourceDir);
|
|
1055
|
+
}
|
|
1056
|
+
const remainingCount = newPendingData.length;
|
|
1057
|
+
if (remainingCount > 0) {
|
|
1058
|
+
const remainingIds = newPendingData.map((c) => c.cave_id).join(", ");
|
|
1059
|
+
const action = isApprove ? "auditPassed" : "auditRejected";
|
|
1060
|
+
return this.sendMessage(session, "commands.cave.audit.pendingResult", [
|
|
1061
|
+
session.text(`commands.cave.audit.${action}`),
|
|
1062
|
+
remainingCount,
|
|
1063
|
+
remainingIds
|
|
1064
|
+
], false);
|
|
1065
|
+
}
|
|
1066
|
+
return this.sendMessage(
|
|
1067
|
+
session,
|
|
1068
|
+
isApprove ? "commands.cave.audit.auditPassed" : "commands.cave.audit.auditRejected",
|
|
1069
|
+
[],
|
|
1070
|
+
false
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
async handleBatchAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session) {
|
|
1074
|
+
const data = isApprove ? await FileHandler.readJsonData(caveFilePath) : null;
|
|
1075
|
+
let processedCount = 0;
|
|
1076
|
+
if (isApprove && data) {
|
|
1077
|
+
const oldData = [...data];
|
|
1078
|
+
const newData = [...data];
|
|
1079
|
+
await FileHandler.withTransaction([
|
|
1080
|
+
{
|
|
1081
|
+
filePath: caveFilePath,
|
|
1082
|
+
operation: /* @__PURE__ */ __name(async () => {
|
|
1083
|
+
for (const cave of pendingData) {
|
|
1084
|
+
newData.push({
|
|
1085
|
+
...cave,
|
|
1086
|
+
cave_id: cave.cave_id,
|
|
1087
|
+
elements: this.cleanElementsForSave(cave.elements, false)
|
|
1088
|
+
});
|
|
1089
|
+
processedCount++;
|
|
1090
|
+
await this.idManager.addStat(cave.contributor_number, cave.cave_id);
|
|
1091
|
+
}
|
|
1092
|
+
return FileHandler.writeJsonData(caveFilePath, newData);
|
|
1093
|
+
}, "operation"),
|
|
1094
|
+
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldData), "rollback")
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
filePath: pendingFilePath,
|
|
1098
|
+
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, []), "operation"),
|
|
1099
|
+
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
|
|
1100
|
+
}
|
|
1101
|
+
]);
|
|
1102
|
+
} else {
|
|
1103
|
+
for (const cave of pendingData) {
|
|
1104
|
+
await this.idManager.markDeleted(cave.cave_id);
|
|
1105
|
+
await this.deleteMediaFiles(cave, resourceDir);
|
|
1106
|
+
processedCount++;
|
|
767
1107
|
}
|
|
768
|
-
|
|
769
|
-
logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
|
|
1108
|
+
await FileHandler.writeJsonData(pendingFilePath, []);
|
|
770
1109
|
}
|
|
1110
|
+
return this.sendMessage(session, "commands.cave.audit.batchAuditResult", [
|
|
1111
|
+
isApprove ? "通过" : "拒绝",
|
|
1112
|
+
processedCount,
|
|
1113
|
+
pendingData.length
|
|
1114
|
+
], false);
|
|
771
1115
|
}
|
|
772
|
-
async
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1116
|
+
async sendAuditMessage(cave, content, session) {
|
|
1117
|
+
const auditMessage = `${session.text("commands.cave.audit.title")}
|
|
1118
|
+
${content}
|
|
1119
|
+
${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
|
|
1120
|
+
for (const managerId of this.config.manager) {
|
|
1121
|
+
const bot = this.ctx.bots[0];
|
|
1122
|
+
if (bot) {
|
|
1123
|
+
try {
|
|
1124
|
+
await bot.sendPrivateMessage(managerId, auditMessage);
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
this.logger.error(session.text("commands.cave.audit.sendFailed", [managerId]));
|
|
1127
|
+
}
|
|
779
1128
|
}
|
|
780
|
-
} catch (error) {
|
|
781
|
-
logger3.error(`Failed to process text hashes for cave ${cave.cave_id}: ${error.message}`);
|
|
782
1129
|
}
|
|
783
1130
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
};
|
|
795
|
-
await FileHandler.writeJsonData(this.filePath, [data]);
|
|
796
|
-
} catch (error) {
|
|
797
|
-
logger3.error(`Failed to save hash data: ${error.message}`);
|
|
798
|
-
throw error;
|
|
1131
|
+
async deleteMediaFiles(cave, resourceDir) {
|
|
1132
|
+
if (cave.elements) {
|
|
1133
|
+
for (const element of cave.elements) {
|
|
1134
|
+
if ((element.type === "img" || element.type === "video") && element.file) {
|
|
1135
|
+
const fullPath = path4.join(resourceDir, element.file);
|
|
1136
|
+
if (fs4.existsSync(fullPath)) {
|
|
1137
|
+
await fs4.promises.unlink(fullPath);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
799
1141
|
}
|
|
800
1142
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1143
|
+
cleanElementsForSave(elements, keepIndex = false) {
|
|
1144
|
+
if (!elements?.length) return [];
|
|
1145
|
+
const cleanedElements = elements.map((element) => {
|
|
1146
|
+
if (element.type === "text") {
|
|
1147
|
+
const cleanedElement = {
|
|
1148
|
+
type: "text",
|
|
1149
|
+
content: element.content
|
|
1150
|
+
};
|
|
1151
|
+
if (keepIndex) cleanedElement.index = element.index;
|
|
1152
|
+
return cleanedElement;
|
|
1153
|
+
} else if (element.type === "img" || element.type === "video") {
|
|
1154
|
+
const mediaElement = element;
|
|
1155
|
+
const cleanedElement = {
|
|
1156
|
+
type: mediaElement.type
|
|
1157
|
+
};
|
|
1158
|
+
if (mediaElement.file) cleanedElement.file = mediaElement.file;
|
|
1159
|
+
if (keepIndex) cleanedElement.index = element.index;
|
|
1160
|
+
return cleanedElement;
|
|
1161
|
+
}
|
|
1162
|
+
return element;
|
|
1163
|
+
});
|
|
1164
|
+
return keepIndex ? cleanedElements.sort((a, b) => (a.index || 0) - (b.index || 0)) : cleanedElements;
|
|
809
1165
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1166
|
+
async sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
|
|
1167
|
+
try {
|
|
1168
|
+
const msg = await session.send(session.text(key, params));
|
|
1169
|
+
if (isTemp && msg) {
|
|
1170
|
+
setTimeout(async () => {
|
|
1171
|
+
try {
|
|
1172
|
+
await session.bot.deleteMessage(session.channelId, msg);
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
this.logger.debug(`Failed to delete temporary message: ${error.message}`);
|
|
1175
|
+
}
|
|
1176
|
+
}, timeout);
|
|
1177
|
+
}
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
this.logger.error(`Failed to send message: ${error.message}`);
|
|
1180
|
+
}
|
|
1181
|
+
return "";
|
|
817
1182
|
}
|
|
818
1183
|
};
|
|
819
1184
|
|
|
820
1185
|
// src/index.ts
|
|
821
1186
|
var name = "best-cave";
|
|
822
1187
|
var inject = ["database"];
|
|
823
|
-
var Config =
|
|
824
|
-
manager:
|
|
1188
|
+
var Config = import_koishi5.Schema.object({
|
|
1189
|
+
manager: import_koishi5.Schema.array(import_koishi5.Schema.string()).required(),
|
|
825
1190
|
// 管理员用户ID
|
|
826
|
-
number:
|
|
1191
|
+
number: import_koishi5.Schema.number().default(60),
|
|
827
1192
|
// 冷却时间(秒)
|
|
828
|
-
enableAudit:
|
|
1193
|
+
enableAudit: import_koishi5.Schema.boolean().default(false),
|
|
829
1194
|
// 启用审核
|
|
830
|
-
|
|
1195
|
+
enableTextDuplicate: import_koishi5.Schema.boolean().default(true),
|
|
1196
|
+
// 启用文本查重
|
|
1197
|
+
textDuplicateThreshold: import_koishi5.Schema.number().default(0.9),
|
|
1198
|
+
// 文本查重阈值
|
|
1199
|
+
enableImageDuplicate: import_koishi5.Schema.boolean().default(true),
|
|
1200
|
+
// 开启图片查重
|
|
1201
|
+
imageDuplicateThreshold: import_koishi5.Schema.number().default(0.8),
|
|
1202
|
+
// 图片查重阈值
|
|
1203
|
+
imageMaxSize: import_koishi5.Schema.number().default(4),
|
|
831
1204
|
// 图片大小限制(MB)
|
|
832
|
-
|
|
833
|
-
// 启用MD5查重
|
|
834
|
-
enableDuplicate: import_koishi4.Schema.boolean().default(true),
|
|
835
|
-
// 启用相似度查重
|
|
836
|
-
duplicateThreshold: import_koishi4.Schema.number().default(0.8),
|
|
837
|
-
// 相似度查重阈值(0-1)
|
|
838
|
-
allowVideo: import_koishi4.Schema.boolean().default(true),
|
|
1205
|
+
allowVideo: import_koishi5.Schema.boolean().default(true),
|
|
839
1206
|
// 允许视频
|
|
840
|
-
videoMaxSize:
|
|
1207
|
+
videoMaxSize: import_koishi5.Schema.number().default(16),
|
|
841
1208
|
// 视频大小限制(MB)
|
|
842
|
-
enablePagination:
|
|
1209
|
+
enablePagination: import_koishi5.Schema.boolean().default(false),
|
|
843
1210
|
// 启用分页
|
|
844
|
-
itemsPerPage:
|
|
1211
|
+
itemsPerPage: import_koishi5.Schema.number().default(10),
|
|
845
1212
|
// 每页条数
|
|
846
|
-
blacklist:
|
|
1213
|
+
blacklist: import_koishi5.Schema.array(import_koishi5.Schema.string()).default([]),
|
|
847
1214
|
// 黑名单
|
|
848
|
-
whitelist:
|
|
1215
|
+
whitelist: import_koishi5.Schema.array(import_koishi5.Schema.string()).default([])
|
|
849
1216
|
// 白名单
|
|
850
1217
|
}).i18n({
|
|
851
1218
|
"zh-CN": require_zh_CN()._config,
|
|
@@ -854,19 +1221,20 @@ var Config = import_koishi4.Schema.object({
|
|
|
854
1221
|
async function apply(ctx, config) {
|
|
855
1222
|
ctx.i18n.define("zh-CN", require_zh_CN());
|
|
856
1223
|
ctx.i18n.define("en-US", require_en_US());
|
|
857
|
-
const dataDir =
|
|
858
|
-
const caveDir =
|
|
1224
|
+
const dataDir = path5.join(ctx.baseDir, "data");
|
|
1225
|
+
const caveDir = path5.join(dataDir, "cave");
|
|
859
1226
|
await FileHandler.ensureDirectory(dataDir);
|
|
860
1227
|
await FileHandler.ensureDirectory(caveDir);
|
|
861
|
-
await FileHandler.ensureDirectory(
|
|
862
|
-
await FileHandler.ensureJsonFile(
|
|
863
|
-
await FileHandler.ensureJsonFile(
|
|
864
|
-
await FileHandler.ensureJsonFile(
|
|
1228
|
+
await FileHandler.ensureDirectory(path5.join(caveDir, "resources"));
|
|
1229
|
+
await FileHandler.ensureJsonFile(path5.join(caveDir, "cave.json"));
|
|
1230
|
+
await FileHandler.ensureJsonFile(path5.join(caveDir, "pending.json"));
|
|
1231
|
+
await FileHandler.ensureJsonFile(path5.join(caveDir, "hash.json"));
|
|
865
1232
|
const idManager = new IdManager(ctx.baseDir);
|
|
866
|
-
const
|
|
1233
|
+
const contentHashManager = new ContentHashManager(caveDir);
|
|
1234
|
+
const auditManager = new AuditManager(ctx, config, caveDir, idManager);
|
|
867
1235
|
await Promise.all([
|
|
868
|
-
idManager.initialize(
|
|
869
|
-
|
|
1236
|
+
idManager.initialize(path5.join(caveDir, "cave.json"), path5.join(caveDir, "pending.json")),
|
|
1237
|
+
contentHashManager.initialize()
|
|
870
1238
|
]);
|
|
871
1239
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
872
1240
|
async function processList(session, config2, userId, pageNum = 1) {
|
|
@@ -895,13 +1263,13 @@ async function apply(ctx, config) {
|
|
|
895
1263
|
const pendingData = await FileHandler.readJsonData(pendingFilePath);
|
|
896
1264
|
const isApprove = Boolean(options.p);
|
|
897
1265
|
if (options.p === true && content[0] === "all" || options.d === true && content[0] === "all") {
|
|
898
|
-
return await
|
|
1266
|
+
return await auditManager.processAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session);
|
|
899
1267
|
}
|
|
900
1268
|
const id = parseInt(content[0] || (typeof options.p === "string" ? options.p : "") || (typeof options.d === "string" ? options.d : ""));
|
|
901
1269
|
if (isNaN(id)) {
|
|
902
1270
|
return sendMessage(session, "commands.cave.error.invalidId", [], true);
|
|
903
1271
|
}
|
|
904
|
-
return
|
|
1272
|
+
return await auditManager.processAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, id);
|
|
905
1273
|
}
|
|
906
1274
|
__name(processAudit, "processAudit");
|
|
907
1275
|
async function processView(caveFilePath, resourceDir, session, options, content) {
|
|
@@ -944,14 +1312,15 @@ async function apply(ctx, config) {
|
|
|
944
1312
|
}
|
|
945
1313
|
const caveContent = await buildMessage(targetCave, resourceDir, session);
|
|
946
1314
|
if (targetCave.elements) {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1315
|
+
await contentHashManager.updateCaveContent(caveId, {
|
|
1316
|
+
images: void 0,
|
|
1317
|
+
texts: void 0
|
|
1318
|
+
});
|
|
950
1319
|
for (const element of targetCave.elements) {
|
|
951
1320
|
if ((element.type === "img" || element.type === "video") && element.file) {
|
|
952
|
-
const fullPath =
|
|
953
|
-
if (
|
|
954
|
-
await
|
|
1321
|
+
const fullPath = path5.join(resourceDir, element.file);
|
|
1322
|
+
if (fs5.existsSync(fullPath)) {
|
|
1323
|
+
await fs5.promises.unlink(fullPath);
|
|
955
1324
|
}
|
|
956
1325
|
}
|
|
957
1326
|
}
|
|
@@ -971,36 +1340,24 @@ async function apply(ctx, config) {
|
|
|
971
1340
|
}
|
|
972
1341
|
__name(processDelete, "processDelete");
|
|
973
1342
|
async function processAdd(ctx2, config2, caveFilePath, resourceDir, pendingFilePath, session, content) {
|
|
1343
|
+
let caveId;
|
|
974
1344
|
try {
|
|
975
1345
|
const inputContent = content.length > 0 ? content.join("\n") : await (async () => {
|
|
976
1346
|
await sendMessage(session, "commands.cave.add.noContent", [], true);
|
|
977
1347
|
const reply = await session.prompt({ timeout: 6e4 });
|
|
978
|
-
if (!reply) session.text("commands.cave.add.operationTimeout");
|
|
1348
|
+
if (!reply) throw new Error(session.text("commands.cave.add.operationTimeout"));
|
|
979
1349
|
return reply;
|
|
980
1350
|
})();
|
|
981
|
-
|
|
1351
|
+
caveId = await idManager.getNextId();
|
|
982
1352
|
if (inputContent.includes("/app/.config/QQ/")) {
|
|
983
1353
|
return sendMessage(session, "commands.cave.add.localFileNotAllowed", [], true);
|
|
984
1354
|
}
|
|
985
1355
|
const bypassAudit = config2.whitelist.includes(session.userId) || config2.whitelist.includes(session.guildId) || config2.whitelist.includes(session.channelId);
|
|
986
1356
|
const { imageUrls, imageElements, videoUrls, videoElements, textParts } = await extractMediaContent(inputContent, config2, session);
|
|
987
|
-
const pureText = textParts.filter((tp) => tp.type === "text").map((tp) => tp.content.trim()).join("\n").trim();
|
|
988
|
-
if (config2.enableMD5 && pureText) {
|
|
989
|
-
const textHash = HashStorage.hashText(pureText);
|
|
990
|
-
const textDuplicates = await hashStorage.findDuplicates("text", [textHash]);
|
|
991
|
-
if (textDuplicates[0]) {
|
|
992
|
-
const data2 = await FileHandler.readJsonData(caveFilePath);
|
|
993
|
-
const duplicateCave = data2.find((item) => item.cave_id === textDuplicates[0].caveId);
|
|
994
|
-
if (duplicateCave) {
|
|
995
|
-
const message = session.text("commands.cave.error.exactDuplicateFound");
|
|
996
|
-
await session.send(message + await buildMessage(duplicateCave, resourceDir, session));
|
|
997
|
-
throw new Error("duplicate_found");
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
1357
|
if (videoUrls.length > 0 && !config2.allowVideo) {
|
|
1002
1358
|
return sendMessage(session, "commands.cave.add.videoDisabled", [], true);
|
|
1003
1359
|
}
|
|
1360
|
+
const imageBuffers = [];
|
|
1004
1361
|
const [savedImages, savedVideos] = await Promise.all([
|
|
1005
1362
|
imageUrls.length > 0 ? saveMedia(
|
|
1006
1363
|
imageUrls,
|
|
@@ -1010,7 +1367,9 @@ async function apply(ctx, config) {
|
|
|
1010
1367
|
"img",
|
|
1011
1368
|
config2,
|
|
1012
1369
|
ctx2,
|
|
1013
|
-
session
|
|
1370
|
+
session,
|
|
1371
|
+
imageBuffers
|
|
1372
|
+
// 添加参数用于收集buffer
|
|
1014
1373
|
) : [],
|
|
1015
1374
|
videoUrls.length > 0 ? saveMedia(
|
|
1016
1375
|
videoUrls,
|
|
@@ -1033,7 +1392,7 @@ async function apply(ctx, config) {
|
|
|
1033
1392
|
// 保持原始文本和图片的相对位置
|
|
1034
1393
|
index: el.index
|
|
1035
1394
|
}))
|
|
1036
|
-
].sort((a) => a.index - a.index),
|
|
1395
|
+
].sort((a, b) => a.index - a.index),
|
|
1037
1396
|
contributor_number: session.userId,
|
|
1038
1397
|
contributor_name: session.username
|
|
1039
1398
|
};
|
|
@@ -1044,19 +1403,24 @@ async function apply(ctx, config) {
|
|
|
1044
1403
|
index: Number.MAX_SAFE_INTEGER
|
|
1045
1404
|
});
|
|
1046
1405
|
}
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
)
|
|
1051
|
-
|
|
1052
|
-
|
|
1406
|
+
const hashStorage = new ContentHashManager(path5.join(ctx2.baseDir, "data", "cave"));
|
|
1407
|
+
await hashStorage.initialize();
|
|
1408
|
+
const hashStatus = await hashStorage.getStatus();
|
|
1409
|
+
if (!hashStatus.lastUpdated || hashStatus.entries.length === 0) {
|
|
1410
|
+
const existingData = await FileHandler.readJsonData(caveFilePath);
|
|
1411
|
+
const hasImages = existingData.some(
|
|
1412
|
+
(cave) => cave.elements?.some((element) => element.type === "img" && element.file)
|
|
1413
|
+
);
|
|
1414
|
+
if (hasImages) {
|
|
1415
|
+
await hashStorage.updateAllCaves(true);
|
|
1416
|
+
}
|
|
1053
1417
|
}
|
|
1054
1418
|
if (config2.enableAudit && !bypassAudit) {
|
|
1055
1419
|
const pendingData = await FileHandler.readJsonData(pendingFilePath);
|
|
1056
1420
|
pendingData.push(newCave);
|
|
1057
1421
|
await Promise.all([
|
|
1058
1422
|
FileHandler.writeJsonData(pendingFilePath, pendingData),
|
|
1059
|
-
sendAuditMessage(
|
|
1423
|
+
auditManager.sendAuditMessage(newCave, await buildMessage(newCave, resourceDir, session), session)
|
|
1060
1424
|
]);
|
|
1061
1425
|
return sendMessage(session, "commands.cave.add.submitPending", [caveId], false);
|
|
1062
1426
|
}
|
|
@@ -1065,137 +1429,58 @@ async function apply(ctx, config) {
|
|
|
1065
1429
|
...newCave,
|
|
1066
1430
|
elements: cleanElementsForSave(newCave.elements, false)
|
|
1067
1431
|
});
|
|
1432
|
+
if (config2.enableImageDuplicate || config2.enableTextDuplicate) {
|
|
1433
|
+
try {
|
|
1434
|
+
const duplicateResults = await contentHashManager.findDuplicates({
|
|
1435
|
+
images: config2.enableImageDuplicate ? imageBuffers : void 0,
|
|
1436
|
+
texts: config2.enableTextDuplicate ? textParts.filter((p) => p.type === "text").map((p) => p.content) : void 0
|
|
1437
|
+
}, {
|
|
1438
|
+
image: config2.imageDuplicateThreshold,
|
|
1439
|
+
text: config2.textDuplicateThreshold
|
|
1440
|
+
});
|
|
1441
|
+
for (const result of duplicateResults) {
|
|
1442
|
+
if (!result) continue;
|
|
1443
|
+
const originalCave = data.find((item) => item.cave_id === result.caveId);
|
|
1444
|
+
if (!originalCave) continue;
|
|
1445
|
+
await idManager.markDeleted(caveId);
|
|
1446
|
+
const duplicateMessage = session.text(
|
|
1447
|
+
"commands.cave.error.similarDuplicateFound",
|
|
1448
|
+
[(result.similarity * 100).toFixed(1)]
|
|
1449
|
+
);
|
|
1450
|
+
await session.send(duplicateMessage + await buildMessage(originalCave, resourceDir, session));
|
|
1451
|
+
throw new Error("duplicate_found");
|
|
1452
|
+
}
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
if (error.message !== "duplicate_found") {
|
|
1455
|
+
await idManager.markDeleted(caveId);
|
|
1456
|
+
}
|
|
1457
|
+
if (error.message === "duplicate_found") {
|
|
1458
|
+
return "";
|
|
1459
|
+
}
|
|
1460
|
+
return sendMessage(session, "commands.cave.error.addFailed", [], true);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1068
1463
|
await Promise.all([
|
|
1069
1464
|
FileHandler.writeJsonData(caveFilePath, data),
|
|
1070
|
-
|
|
1071
|
-
|
|
1465
|
+
contentHashManager.updateCaveContent(caveId, {
|
|
1466
|
+
images: savedImages.length > 0 ? await Promise.all(savedImages.map((file) => fs5.promises.readFile(path5.join(resourceDir, file)))) : void 0,
|
|
1467
|
+
texts: textParts.filter((p) => p.type === "text").map((p) => p.content)
|
|
1468
|
+
})
|
|
1072
1469
|
]);
|
|
1073
1470
|
await idManager.addStat(session.userId, caveId);
|
|
1074
|
-
if (config2.enableMD5 && pureText) {
|
|
1075
|
-
const textHash = HashStorage.hashText(pureText);
|
|
1076
|
-
await hashStorage.updateHash(caveId, "text", textHash);
|
|
1077
|
-
}
|
|
1078
1471
|
return sendMessage(session, "commands.cave.add.addSuccess", [caveId], false);
|
|
1079
1472
|
} catch (error) {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
}
|
|
1083
|
-
__name(processAdd, "processAdd");
|
|
1084
|
-
async function handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, targetId) {
|
|
1085
|
-
if (pendingData.length === 0) {
|
|
1086
|
-
return sendMessage(session, "commands.cave.audit.noPending", [], true);
|
|
1087
|
-
}
|
|
1088
|
-
if (typeof targetId === "number") {
|
|
1089
|
-
const targetCave = pendingData.find((item) => item.cave_id === targetId);
|
|
1090
|
-
if (!targetCave) {
|
|
1091
|
-
return sendMessage(session, "commands.cave.audit.pendingNotFound", [], true);
|
|
1473
|
+
if (error.message !== "duplicate_found") {
|
|
1474
|
+
await idManager.markDeleted(caveId);
|
|
1092
1475
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
const oldCaveData = await FileHandler.readJsonData(caveFilePath);
|
|
1096
|
-
const newCaveData = [...oldCaveData, {
|
|
1097
|
-
...targetCave,
|
|
1098
|
-
cave_id: targetId,
|
|
1099
|
-
elements: cleanElementsForSave(targetCave.elements, false)
|
|
1100
|
-
}];
|
|
1101
|
-
await FileHandler.withTransaction([
|
|
1102
|
-
{
|
|
1103
|
-
filePath: caveFilePath,
|
|
1104
|
-
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, newCaveData), "operation"),
|
|
1105
|
-
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldCaveData), "rollback")
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
filePath: pendingFilePath,
|
|
1109
|
-
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, newPendingData), "operation"),
|
|
1110
|
-
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
|
|
1111
|
-
}
|
|
1112
|
-
]);
|
|
1113
|
-
await idManager.addStat(targetCave.contributor_number, targetId);
|
|
1114
|
-
} else {
|
|
1115
|
-
await FileHandler.writeJsonData(pendingFilePath, newPendingData);
|
|
1116
|
-
await idManager.markDeleted(targetId);
|
|
1117
|
-
if (targetCave.elements) {
|
|
1118
|
-
for (const element of targetCave.elements) {
|
|
1119
|
-
if ((element.type === "img" || element.type === "video") && element.file) {
|
|
1120
|
-
const fullPath = path4.join(resourceDir, element.file);
|
|
1121
|
-
if (fs4.existsSync(fullPath)) {
|
|
1122
|
-
await fs4.promises.unlink(fullPath);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
const remainingCount = newPendingData.length;
|
|
1129
|
-
if (remainingCount > 0) {
|
|
1130
|
-
const remainingIds = newPendingData.map((c) => c.cave_id).join(", ");
|
|
1131
|
-
const action = isApprove ? "auditPassed" : "auditRejected";
|
|
1132
|
-
return sendMessage(session, "commands.cave.audit.pendingResult", [
|
|
1133
|
-
session.text(`commands.cave.audit.${action}`),
|
|
1134
|
-
remainingCount,
|
|
1135
|
-
remainingIds
|
|
1136
|
-
], false);
|
|
1137
|
-
}
|
|
1138
|
-
return sendMessage(
|
|
1139
|
-
session,
|
|
1140
|
-
isApprove ? "commands.cave.audit.auditPassed" : "commands.cave.audit.auditRejected",
|
|
1141
|
-
[],
|
|
1142
|
-
false
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
const data = isApprove ? await FileHandler.readJsonData(caveFilePath) : null;
|
|
1146
|
-
let processedCount = 0;
|
|
1147
|
-
if (isApprove && data) {
|
|
1148
|
-
const oldData = [...data];
|
|
1149
|
-
const newData = [...data];
|
|
1150
|
-
await FileHandler.withTransaction([
|
|
1151
|
-
{
|
|
1152
|
-
filePath: caveFilePath,
|
|
1153
|
-
operation: /* @__PURE__ */ __name(async () => {
|
|
1154
|
-
for (const cave of pendingData) {
|
|
1155
|
-
newData.push({
|
|
1156
|
-
...cave,
|
|
1157
|
-
cave_id: cave.cave_id,
|
|
1158
|
-
elements: cleanElementsForSave(cave.elements, false)
|
|
1159
|
-
});
|
|
1160
|
-
processedCount++;
|
|
1161
|
-
await idManager.addStat(cave.contributor_number, cave.cave_id);
|
|
1162
|
-
}
|
|
1163
|
-
return FileHandler.writeJsonData(caveFilePath, newData);
|
|
1164
|
-
}, "operation"),
|
|
1165
|
-
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldData), "rollback")
|
|
1166
|
-
},
|
|
1167
|
-
{
|
|
1168
|
-
filePath: pendingFilePath,
|
|
1169
|
-
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, []), "operation"),
|
|
1170
|
-
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
|
|
1171
|
-
}
|
|
1172
|
-
]);
|
|
1173
|
-
} else {
|
|
1174
|
-
for (const cave of pendingData) {
|
|
1175
|
-
await idManager.markDeleted(cave.cave_id);
|
|
1176
|
-
}
|
|
1177
|
-
await FileHandler.writeJsonData(pendingFilePath, []);
|
|
1178
|
-
for (const cave of pendingData) {
|
|
1179
|
-
if (cave.elements) {
|
|
1180
|
-
for (const element of cave.elements) {
|
|
1181
|
-
if ((element.type === "img" || element.type === "video") && element.file) {
|
|
1182
|
-
const fullPath = path4.join(resourceDir, element.file);
|
|
1183
|
-
if (fs4.existsSync(fullPath)) {
|
|
1184
|
-
await fs4.promises.unlink(fullPath);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
processedCount++;
|
|
1476
|
+
if (error.message === "duplicate_found") {
|
|
1477
|
+
return "";
|
|
1190
1478
|
}
|
|
1479
|
+
logger4.error(`Failed to process add command: ${error.message}`);
|
|
1480
|
+
return sendMessage(session, "commands.cave.error.addFailed", [], true);
|
|
1191
1481
|
}
|
|
1192
|
-
return sendMessage(session, "commands.cave.audit.batchAuditResult", [
|
|
1193
|
-
isApprove ? "通过" : "拒绝",
|
|
1194
|
-
processedCount,
|
|
1195
|
-
pendingData.length
|
|
1196
|
-
], false);
|
|
1197
1482
|
}
|
|
1198
|
-
__name(
|
|
1483
|
+
__name(processAdd, "processAdd");
|
|
1199
1484
|
ctx.command("cave [message]").option("a", "添加回声洞").option("g", "查看回声洞", { type: "string" }).option("r", "删除回声洞", { type: "string" }).option("p", "通过审核", { type: "string" }).option("d", "拒绝审核", { type: "string" }).option("l", "查询投稿统计", { type: "string" }).before(async ({ session, options }) => {
|
|
1200
1485
|
if (config.blacklist.includes(session.userId)) {
|
|
1201
1486
|
return sendMessage(session, "commands.cave.message.blacklisted", [], true);
|
|
@@ -1204,11 +1489,11 @@ async function apply(ctx, config) {
|
|
|
1204
1489
|
return sendMessage(session, "commands.cave.message.managerOnly", [], true);
|
|
1205
1490
|
}
|
|
1206
1491
|
}).action(async ({ session, options }, ...content) => {
|
|
1207
|
-
const dataDir2 =
|
|
1208
|
-
const caveDir2 =
|
|
1209
|
-
const caveFilePath =
|
|
1210
|
-
const resourceDir =
|
|
1211
|
-
const pendingFilePath =
|
|
1492
|
+
const dataDir2 = path5.join(ctx.baseDir, "data");
|
|
1493
|
+
const caveDir2 = path5.join(dataDir2, "cave");
|
|
1494
|
+
const caveFilePath = path5.join(caveDir2, "cave.json");
|
|
1495
|
+
const resourceDir = path5.join(caveDir2, "resources");
|
|
1496
|
+
const pendingFilePath = path5.join(caveDir2, "pending.json");
|
|
1212
1497
|
const needsCooldown = !options.l && !options.a && !options.p && !options.d;
|
|
1213
1498
|
if (needsCooldown) {
|
|
1214
1499
|
const guildId = session.guildId;
|
|
@@ -1255,7 +1540,7 @@ async function apply(ctx, config) {
|
|
|
1255
1540
|
});
|
|
1256
1541
|
}
|
|
1257
1542
|
__name(apply, "apply");
|
|
1258
|
-
var logger4 = new
|
|
1543
|
+
var logger4 = new import_koishi5.Logger("cave");
|
|
1259
1544
|
async function sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
|
|
1260
1545
|
try {
|
|
1261
1546
|
const msg = await session.send(session.text(key, params));
|
|
@@ -1274,22 +1559,6 @@ async function sendMessage(session, key, params = [], isTemp = true, timeout = 1
|
|
|
1274
1559
|
return "";
|
|
1275
1560
|
}
|
|
1276
1561
|
__name(sendMessage, "sendMessage");
|
|
1277
|
-
async function sendAuditMessage(ctx, config, cave, content, session) {
|
|
1278
|
-
const auditMessage = `${session.text("commands.cave.audit.title")}
|
|
1279
|
-
${content}
|
|
1280
|
-
${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
|
|
1281
|
-
for (const managerId of config.manager) {
|
|
1282
|
-
const bot = ctx.bots[0];
|
|
1283
|
-
if (bot) {
|
|
1284
|
-
try {
|
|
1285
|
-
await bot.sendPrivateMessage(managerId, auditMessage);
|
|
1286
|
-
} catch (error) {
|
|
1287
|
-
logger4.error(session.text("commands.cave.audit.sendFailed", [managerId]));
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
__name(sendAuditMessage, "sendAuditMessage");
|
|
1293
1562
|
function cleanElementsForSave(elements, keepIndex = false) {
|
|
1294
1563
|
if (!elements?.length) return [];
|
|
1295
1564
|
const cleanedElements = elements.map((element) => {
|
|
@@ -1315,7 +1584,7 @@ function cleanElementsForSave(elements, keepIndex = false) {
|
|
|
1315
1584
|
}
|
|
1316
1585
|
__name(cleanElementsForSave, "cleanElementsForSave");
|
|
1317
1586
|
async function processMediaFile(filePath, type) {
|
|
1318
|
-
const data = await
|
|
1587
|
+
const data = await fs5.promises.readFile(filePath).catch(() => null);
|
|
1319
1588
|
if (!data) return null;
|
|
1320
1589
|
return `data:${type}/${type === "image" ? "png" : "mp4"};base64,${data.toString("base64")}`;
|
|
1321
1590
|
}
|
|
@@ -1332,10 +1601,10 @@ async function buildMessage(cave, resourceDir, session) {
|
|
|
1332
1601
|
session.text("commands.cave.message.contributorSuffix", [cave.contributor_name])
|
|
1333
1602
|
].join("\n");
|
|
1334
1603
|
await session?.send(basicInfo);
|
|
1335
|
-
const filePath =
|
|
1604
|
+
const filePath = path5.join(resourceDir, videoElement.file);
|
|
1336
1605
|
const base64Data = await processMediaFile(filePath, "video");
|
|
1337
1606
|
if (base64Data && session) {
|
|
1338
|
-
await session.send((0,
|
|
1607
|
+
await session.send((0, import_koishi5.h)("video", { src: base64Data }));
|
|
1339
1608
|
}
|
|
1340
1609
|
return "";
|
|
1341
1610
|
}
|
|
@@ -1344,10 +1613,10 @@ async function buildMessage(cave, resourceDir, session) {
|
|
|
1344
1613
|
if (element.type === "text") {
|
|
1345
1614
|
lines.push(element.content);
|
|
1346
1615
|
} else if (element.type === "img" && element.file) {
|
|
1347
|
-
const filePath =
|
|
1616
|
+
const filePath = path5.join(resourceDir, element.file);
|
|
1348
1617
|
const base64Data = await processMediaFile(filePath, "image");
|
|
1349
1618
|
if (base64Data) {
|
|
1350
|
-
lines.push((0,
|
|
1619
|
+
lines.push((0, import_koishi5.h)("image", { src: base64Data }));
|
|
1351
1620
|
}
|
|
1352
1621
|
}
|
|
1353
1622
|
}
|
|
@@ -1396,13 +1665,13 @@ async function extractMediaContent(originalContent, config, session) {
|
|
|
1396
1665
|
return { imageUrls, imageElements, videoUrls, videoElements, textParts };
|
|
1397
1666
|
}
|
|
1398
1667
|
__name(extractMediaContent, "extractMediaContent");
|
|
1399
|
-
async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config, ctx, session) {
|
|
1668
|
+
async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config, ctx, session, buffers) {
|
|
1400
1669
|
const accept = mediaType === "img" ? "image/*" : "video/*";
|
|
1401
|
-
const hashStorage = new
|
|
1670
|
+
const hashStorage = new ContentHashManager(path5.join(ctx.baseDir, "data", "cave"));
|
|
1402
1671
|
await hashStorage.initialize();
|
|
1403
1672
|
const downloadTasks = urls.map(async (url, i) => {
|
|
1404
1673
|
const fileName = fileNames[i];
|
|
1405
|
-
const ext =
|
|
1674
|
+
const ext = path5.extname(fileName || url) || (mediaType === "img" ? ".png" : ".mp4");
|
|
1406
1675
|
try {
|
|
1407
1676
|
const response = await ctx.http(decodeURIComponent(url).replace(/&/g, "&"), {
|
|
1408
1677
|
method: "GET",
|
|
@@ -1416,75 +1685,58 @@ async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config
|
|
|
1416
1685
|
});
|
|
1417
1686
|
if (!response.data) throw new Error("empty_response");
|
|
1418
1687
|
const buffer = Buffer.from(response.data);
|
|
1419
|
-
if (mediaType === "img") {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1688
|
+
if (buffers && mediaType === "img") {
|
|
1689
|
+
buffers.push(buffer);
|
|
1690
|
+
}
|
|
1691
|
+
const md5 = path5.basename(fileName || `${mediaType}`, ext).replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
|
|
1692
|
+
const files = await fs5.promises.readdir(resourceDir);
|
|
1693
|
+
const duplicateFile = files.find((file) => {
|
|
1694
|
+
const match = file.match(/^\d+_([^.]+)/);
|
|
1695
|
+
return match && match[1] === md5;
|
|
1696
|
+
});
|
|
1697
|
+
if (duplicateFile) {
|
|
1698
|
+
const duplicateCaveId = parseInt(duplicateFile.split("_")[0]);
|
|
1699
|
+
if (!isNaN(duplicateCaveId)) {
|
|
1700
|
+
const caveFilePath = path5.join(ctx.baseDir, "data", "cave", "cave.json");
|
|
1701
|
+
const data = await FileHandler.readJsonData(caveFilePath);
|
|
1702
|
+
const originalCave = data.find((item) => item.cave_id === duplicateCaveId);
|
|
1703
|
+
if (originalCave) {
|
|
1704
|
+
const message = session.text("commands.cave.error.exactDuplicateFound");
|
|
1705
|
+
await session.send(message + await buildMessage(originalCave, resourceDir, session));
|
|
1706
|
+
throw new Error("duplicate_found");
|
|
1436
1707
|
}
|
|
1437
1708
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
if (similarity >= config.duplicateThreshold) {
|
|
1446
|
-
const caveFilePath = path4.join(ctx.baseDir, "data", "cave", "cave.json");
|
|
1447
|
-
const data = await FileHandler.readJsonData(caveFilePath);
|
|
1448
|
-
const originalCave = data.find((item) => item.cave_id === duplicate.caveId);
|
|
1449
|
-
if (originalCave) {
|
|
1450
|
-
const message = session.text(
|
|
1451
|
-
"commands.cave.error.similarDuplicateFound",
|
|
1452
|
-
[(similarity * 100).toFixed(1)]
|
|
1453
|
-
);
|
|
1454
|
-
await session.send(message + await buildMessage(originalCave, resourceDir, session));
|
|
1455
|
-
throw new Error("duplicate_found");
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1709
|
+
}
|
|
1710
|
+
if (mediaType === "img" && config.enableImageDuplicate) {
|
|
1711
|
+
const result = await hashStorage.findDuplicates(
|
|
1712
|
+
{ images: [buffer] },
|
|
1713
|
+
{
|
|
1714
|
+
image: config.imageDuplicateThreshold,
|
|
1715
|
+
text: config.textDuplicateThreshold
|
|
1458
1716
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
const originalCave = data.find((item) => item.cave_id === duplicateCaveId);
|
|
1475
|
-
if (originalCave) {
|
|
1476
|
-
const message = session.text("commands.cave.error.exactDuplicateFound");
|
|
1477
|
-
await session.send(message + await buildMessage(originalCave, resourceDir, session));
|
|
1478
|
-
throw new Error("duplicate_found");
|
|
1479
|
-
}
|
|
1717
|
+
);
|
|
1718
|
+
if (result.length > 0 && result[0] !== null) {
|
|
1719
|
+
const duplicate = result[0];
|
|
1720
|
+
const similarity = duplicate.similarity;
|
|
1721
|
+
if (similarity >= config.imageDuplicateThreshold) {
|
|
1722
|
+
const caveFilePath = path5.join(ctx.baseDir, "data", "cave", "cave.json");
|
|
1723
|
+
const data = await FileHandler.readJsonData(caveFilePath);
|
|
1724
|
+
const originalCave = data.find((item) => item.cave_id === duplicate.caveId);
|
|
1725
|
+
if (originalCave) {
|
|
1726
|
+
const message = session.text(
|
|
1727
|
+
"commands.cave.error.similarDuplicateFound",
|
|
1728
|
+
[(similarity * 100).toFixed(1)]
|
|
1729
|
+
);
|
|
1730
|
+
await session.send(message + await buildMessage(originalCave, resourceDir, session));
|
|
1731
|
+
throw new Error("duplicate_found");
|
|
1480
1732
|
}
|
|
1481
1733
|
}
|
|
1482
1734
|
}
|
|
1483
|
-
const finalFileName = `${caveId}_${baseName}${ext}`;
|
|
1484
|
-
const filePath = path4.join(resourceDir, finalFileName);
|
|
1485
|
-
await FileHandler.saveMediaFile(filePath, buffer);
|
|
1486
|
-
return finalFileName;
|
|
1487
1735
|
}
|
|
1736
|
+
const finalFileName = `${caveId}_${md5}${ext}`;
|
|
1737
|
+
const filePath = path5.join(resourceDir, finalFileName);
|
|
1738
|
+
await FileHandler.saveMediaFile(filePath, buffer);
|
|
1739
|
+
return finalFileName;
|
|
1488
1740
|
} catch (error) {
|
|
1489
1741
|
if (error.message === "duplicate_found") {
|
|
1490
1742
|
throw error;
|