koishi-plugin-best-cave 2.3.16 → 2.3.17
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 +16 -1
- package/lib/index.js +45 -121
- package/package.json +1 -1
package/lib/Utils.d.ts
CHANGED
|
@@ -10,9 +10,10 @@ import { PendManager } from './PendManager';
|
|
|
10
10
|
* @param fileManager 文件管理器实例。
|
|
11
11
|
* @param logger 日志记录器实例。
|
|
12
12
|
* @param platform 目标平台名称 (e.g., 'onebot')。
|
|
13
|
+
* @param prefix 可选的消息前缀 (e.g., '已删除', '待审核')。
|
|
13
14
|
* @returns 包含多条消息的数组,每条消息是一个 (string | h)[] 数组。
|
|
14
15
|
*/
|
|
15
|
-
export declare function buildCaveMessage(cave: CaveObject, config: Config, fileManager: FileManager, logger: Logger, platform?: string): Promise<(string | h)[][]>;
|
|
16
|
+
export declare function buildCaveMessage(cave: CaveObject, config: Config, fileManager: FileManager, logger: Logger, platform?: string, prefix?: string): Promise<(string | h)[][]>;
|
|
16
17
|
/**
|
|
17
18
|
* @description 清理数据库中标记为 'delete' 状态的回声洞及其关联文件和哈希。
|
|
18
19
|
* @param ctx Koishi 上下文。
|
|
@@ -62,6 +63,20 @@ export declare function processMessageElements(sourceElements: h[], newId: numbe
|
|
|
62
63
|
fileName: string;
|
|
63
64
|
}[];
|
|
64
65
|
}>;
|
|
66
|
+
/**
|
|
67
|
+
* @description 异步处理文件上传、查重和状态更新的后台任务。
|
|
68
|
+
* @param ctx - Koishi 上下文。
|
|
69
|
+
* @param config - 插件配置。
|
|
70
|
+
* @param fileManager - FileManager 实例,用于保存文件。
|
|
71
|
+
* @param logger - 日志记录器实例。
|
|
72
|
+
* @param reviewManager - ReviewManager 实例,用于提交审核。
|
|
73
|
+
* @param cave - 刚刚在数据库中创建的 `preload` 状态的回声洞对象。
|
|
74
|
+
* @param mediaToSave - 需要下载和处理的媒体文件列表。
|
|
75
|
+
* @param reusableIds - 可复用 ID 的内存缓存。
|
|
76
|
+
* @param session - 触发此操作的用户会话,用于发送反馈。
|
|
77
|
+
* @param hashManager - HashManager 实例,如果启用则用于哈希计算和比较。
|
|
78
|
+
* @param textHashesToStore - 已预先计算好的、待存入数据库的文本哈希对象数组。
|
|
79
|
+
*/
|
|
65
80
|
export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reviewManager: PendManager, cave: CaveObject, mediaToToSave: {
|
|
66
81
|
sourceUrl: string;
|
|
67
82
|
fileName: string;
|
package/lib/index.js
CHANGED
|
@@ -80,9 +80,7 @@ var FileManager = class {
|
|
|
80
80
|
* @returns 异步操作的结果。
|
|
81
81
|
*/
|
|
82
82
|
async withLock(fullPath, operation) {
|
|
83
|
-
while (this.locks.has(fullPath))
|
|
84
|
-
await this.locks.get(fullPath);
|
|
85
|
-
}
|
|
83
|
+
while (this.locks.has(fullPath)) await this.locks.get(fullPath);
|
|
86
84
|
const promise = operation().finally(() => {
|
|
87
85
|
this.locks.delete(fullPath);
|
|
88
86
|
});
|
|
@@ -232,9 +230,7 @@ var DataManager = class {
|
|
|
232
230
|
*/
|
|
233
231
|
registerCommands(cave) {
|
|
234
232
|
const requireAdmin = /* @__PURE__ */ __name((action) => async ({ session }) => {
|
|
235
|
-
if (session.channelId !== this.config.adminChannel?.split(":")[1])
|
|
236
|
-
return "此指令仅限在管理群组中使用";
|
|
237
|
-
}
|
|
233
|
+
if (session.channelId !== this.config.adminChannel?.split(":")[1]) return "此指令仅限在管理群组中使用";
|
|
238
234
|
try {
|
|
239
235
|
await session.send("正在处理,请稍候...");
|
|
240
236
|
return await action();
|
|
@@ -267,9 +263,7 @@ var DataManager = class {
|
|
|
267
263
|
try {
|
|
268
264
|
const fileContent = await this.fileManager.readFile(fileName);
|
|
269
265
|
importedCaves = JSON.parse(fileContent.toString("utf-8"));
|
|
270
|
-
if (!Array.isArray(importedCaves) || !importedCaves.length)
|
|
271
|
-
throw new Error("导入文件格式无效或为空");
|
|
272
|
-
}
|
|
266
|
+
if (!Array.isArray(importedCaves) || !importedCaves.length) throw new Error("导入文件格式无效或为空");
|
|
273
267
|
} catch (error) {
|
|
274
268
|
throw new Error(`读取导入文件失败: ${error.message}`);
|
|
275
269
|
}
|
|
@@ -292,7 +286,7 @@ var import_koishi2 = require("koishi");
|
|
|
292
286
|
var import_koishi = require("koishi");
|
|
293
287
|
var path2 = __toESM(require("path"));
|
|
294
288
|
var mimeTypeMap = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".webp": "image/webp" };
|
|
295
|
-
async function buildCaveMessage(cave, config, fileManager, logger2, platform) {
|
|
289
|
+
async function buildCaveMessage(cave, config, fileManager, logger2, platform, prefix) {
|
|
296
290
|
async function transformToH(elements) {
|
|
297
291
|
return Promise.all(elements.map(async (el) => {
|
|
298
292
|
if (el.type === "text") return import_koishi.h.text(el.content);
|
|
@@ -315,12 +309,8 @@ async function buildCaveMessage(cave, config, fileManager, logger2, platform) {
|
|
|
315
309
|
if (["image", "video", "audio", "file"].includes(el.type)) {
|
|
316
310
|
const fileName = el.file;
|
|
317
311
|
if (!fileName) return (0, import_koishi.h)("p", {}, `[${el.type}]`);
|
|
318
|
-
if (config.enableS3 && config.publicUrl) {
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
if (config.localPath) {
|
|
322
|
-
return (0, import_koishi.h)(el.type, { ...el, src: `file://${path2.join(config.localPath, fileName)}` });
|
|
323
|
-
}
|
|
312
|
+
if (config.enableS3 && config.publicUrl) return (0, import_koishi.h)(el.type, { ...el, src: new URL(fileName, config.publicUrl).href });
|
|
313
|
+
if (config.localPath) return (0, import_koishi.h)(el.type, { ...el, src: `file://${path2.join(config.localPath, fileName)}` });
|
|
324
314
|
try {
|
|
325
315
|
const data = await fileManager.readFile(fileName);
|
|
326
316
|
const mimeType = mimeTypeMap[path2.extname(fileName).toLowerCase()] || "application/octet-stream";
|
|
@@ -336,7 +326,10 @@ async function buildCaveMessage(cave, config, fileManager, logger2, platform) {
|
|
|
336
326
|
__name(transformToH, "transformToH");
|
|
337
327
|
const caveHElements = await transformToH(cave.elements);
|
|
338
328
|
const replacements = { id: cave.id.toString(), name: cave.userName };
|
|
339
|
-
const [
|
|
329
|
+
const [rawHeader, rawFooter] = config.caveFormat.split("|", 2);
|
|
330
|
+
let header = rawHeader ? rawHeader.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]).trim() : "";
|
|
331
|
+
if (prefix) header = `${prefix}${header}`;
|
|
332
|
+
const footer = rawFooter ? rawFooter.replace(/\{id\}|\{name\}/g, (match) => replacements[match.slice(1, -1)]).trim() : "";
|
|
340
333
|
const problematicTypes = ["video", "audio", "file", "forward"];
|
|
341
334
|
const placeholderMap = { video: "[视频]", audio: "[音频]", file: "[文件]", forward: "[合并转发]" };
|
|
342
335
|
const containsProblematic = platform === "onebot" && caveHElements.some((el) => problematicTypes.includes(el.type));
|
|
@@ -369,9 +362,7 @@ async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
|
|
|
369
362
|
const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
|
|
370
363
|
if (!cavesToDelete.length) return;
|
|
371
364
|
const idsToDelete = cavesToDelete.map((c) => c.id);
|
|
372
|
-
for (const cave of cavesToDelete)
|
|
373
|
-
await Promise.all(cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file)));
|
|
374
|
-
}
|
|
365
|
+
for (const cave of cavesToDelete) await Promise.all(cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file)));
|
|
375
366
|
reusableIds.delete(0);
|
|
376
367
|
idsToDelete.forEach((id) => reusableIds.add(id));
|
|
377
368
|
await ctx.database.remove("cave", { id: { $in: idsToDelete } });
|
|
@@ -404,30 +395,22 @@ async function getNextCaveId(ctx, query = {}, reusableIds) {
|
|
|
404
395
|
const existingIds = new Set(allCaveIds);
|
|
405
396
|
let newId = 1;
|
|
406
397
|
while (existingIds.has(newId)) newId++;
|
|
407
|
-
if (existingIds.size === (allCaveIds.length > 0 ? Math.max(...allCaveIds) : 0))
|
|
408
|
-
reusableIds.add(0);
|
|
409
|
-
}
|
|
398
|
+
if (existingIds.size === (allCaveIds.length > 0 ? Math.max(...allCaveIds) : 0)) reusableIds.add(0);
|
|
410
399
|
return newId;
|
|
411
400
|
}
|
|
412
401
|
__name(getNextCaveId, "getNextCaveId");
|
|
413
402
|
function checkCooldown(session, config, lastUsed) {
|
|
414
403
|
const adminChannelId = config.adminChannel?.split(":")[1];
|
|
415
|
-
if (adminChannelId && session.channelId === adminChannelId)
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
404
|
+
if (adminChannelId && session.channelId === adminChannelId) return null;
|
|
418
405
|
if (config.coolDown <= 0 || !session.channelId) return null;
|
|
419
406
|
const lastTime = lastUsed.get(session.channelId) || 0;
|
|
420
407
|
const remainingTime = lastTime + config.coolDown * 1e3 - Date.now();
|
|
421
|
-
if (remainingTime > 0) {
|
|
422
|
-
return `指令冷却中,请在 ${Math.ceil(remainingTime / 1e3)} 秒后重试`;
|
|
423
|
-
}
|
|
408
|
+
if (remainingTime > 0) return `指令冷却中,请在 ${Math.ceil(remainingTime / 1e3)} 秒后重试`;
|
|
424
409
|
return null;
|
|
425
410
|
}
|
|
426
411
|
__name(checkCooldown, "checkCooldown");
|
|
427
412
|
function updateCooldownTimestamp(session, config, lastUsed) {
|
|
428
|
-
if (config.coolDown > 0 && session.channelId)
|
|
429
|
-
lastUsed.set(session.channelId, Date.now());
|
|
430
|
-
}
|
|
413
|
+
if (config.coolDown > 0 && session.channelId) lastUsed.set(session.channelId, Date.now());
|
|
431
414
|
}
|
|
432
415
|
__name(updateCooldownTimestamp, "updateCooldownTimestamp");
|
|
433
416
|
async function processMessageElements(sourceElements, newId, session, config, logger2) {
|
|
@@ -473,13 +456,9 @@ async function processMessageElements(sourceElements, newId, session, config, lo
|
|
|
473
456
|
return (0, import_koishi.h)(type2, attrs);
|
|
474
457
|
});
|
|
475
458
|
const contentElements = await transform(elementsToProcess);
|
|
476
|
-
if (contentElements.length > 0) {
|
|
477
|
-
forwardNodes.push({ userId, userName, elements: contentElements });
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (forwardNodes.length > 0) {
|
|
481
|
-
result.push({ type: "forward", content: forwardNodes });
|
|
459
|
+
if (contentElements.length > 0) forwardNodes.push({ userId, userName, elements: contentElements });
|
|
482
460
|
}
|
|
461
|
+
if (forwardNodes.length > 0) result.push({ type: "forward", content: forwardNodes });
|
|
483
462
|
} else if (["image", "video", "audio", "file"].includes(type) && el.attrs.src) {
|
|
484
463
|
let fileIdentifier = el.attrs.src;
|
|
485
464
|
if (fileIdentifier.startsWith("http")) {
|
|
@@ -531,9 +510,7 @@ async function handleFileUploads(ctx, config, fileManager, logger2, reviewManage
|
|
|
531
510
|
await ctx.database.upsert("cave", [{ id: cave.id, status: finalStatus }]);
|
|
532
511
|
if (hashManager) {
|
|
533
512
|
const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h4) => ({ ...h4, cave: cave.id }));
|
|
534
|
-
if (allHashesToInsert.length > 0)
|
|
535
|
-
await ctx.database.upsert("cave_hash", allHashesToInsert);
|
|
536
|
-
}
|
|
513
|
+
if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
|
|
537
514
|
}
|
|
538
515
|
if (finalStatus === "pending" && reviewManager) {
|
|
539
516
|
const [finalCave] = await ctx.database.get("cave", { id: cave.id });
|
|
@@ -572,9 +549,7 @@ var PendManager = class {
|
|
|
572
549
|
*/
|
|
573
550
|
registerCommands(cave) {
|
|
574
551
|
const requireAdmin = /* @__PURE__ */ __name((session) => {
|
|
575
|
-
if (session.channelId !== this.config.adminChannel?.split(":")[1])
|
|
576
|
-
return "此指令仅限在管理群组中使用";
|
|
577
|
-
}
|
|
552
|
+
if (session.channelId !== this.config.adminChannel?.split(":")[1]) return "此指令仅限在管理群组中使用";
|
|
578
553
|
return null;
|
|
579
554
|
}, "requireAdmin");
|
|
580
555
|
const pend = cave.subcommand(".pend [id:posint]", "审核回声洞").action(async ({ session }, id) => {
|
|
@@ -584,11 +559,8 @@ var PendManager = class {
|
|
|
584
559
|
const [targetCave] = await this.ctx.database.get("cave", { id });
|
|
585
560
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
586
561
|
if (targetCave.status !== "pending") return `回声洞(${id})无需审核`;
|
|
587
|
-
await session.
|
|
588
|
-
const caveMessages
|
|
589
|
-
for (const message of caveMessages) {
|
|
590
|
-
if (message.length > 0) await session.send(import_koishi2.h.normalize(message));
|
|
591
|
-
}
|
|
562
|
+
const caveMessages = await buildCaveMessage(targetCave, this.config, this.fileManager, this.logger, session.platform, "待审核");
|
|
563
|
+
for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi2.h.normalize(message));
|
|
592
564
|
return;
|
|
593
565
|
}
|
|
594
566
|
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" }, { fields: ["id"] });
|
|
@@ -634,11 +606,8 @@ ${pendingCaves.map((c) => c.id).join("|")}`;
|
|
|
634
606
|
}
|
|
635
607
|
try {
|
|
636
608
|
const [platform] = this.config.adminChannel.split(":", 1);
|
|
637
|
-
const caveMessages = await buildCaveMessage(cave, this.config, this.fileManager, this.logger, platform);
|
|
638
|
-
await this.ctx.broadcast([this.config.adminChannel],
|
|
639
|
-
for (const message of caveMessages) {
|
|
640
|
-
if (message.length > 0) await this.ctx.broadcast([this.config.adminChannel], import_koishi2.h.normalize(message));
|
|
641
|
-
}
|
|
609
|
+
const caveMessages = await buildCaveMessage(cave, this.config, this.fileManager, this.logger, platform, "待审核");
|
|
610
|
+
for (const message of caveMessages) if (message.length > 0) await this.ctx.broadcast([this.config.adminChannel], import_koishi2.h.normalize(message));
|
|
642
611
|
} catch (error) {
|
|
643
612
|
this.logger.error(`发送回声洞(${cave.id})审核消息失败:`, error);
|
|
644
613
|
}
|
|
@@ -679,9 +648,7 @@ var HashManager = class {
|
|
|
679
648
|
registerCommands(cave) {
|
|
680
649
|
const adminCheck = /* @__PURE__ */ __name(({ session }) => {
|
|
681
650
|
const adminChannelId = this.config.adminChannel?.split(":")[1];
|
|
682
|
-
if (session.channelId !== adminChannelId)
|
|
683
|
-
return "此指令仅限在管理群组中使用";
|
|
684
|
-
}
|
|
651
|
+
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
685
652
|
}, "adminCheck");
|
|
686
653
|
cave.subcommand(".hash", "校验回声洞").usage("校验缺失哈希的回声洞,补全哈希记录。").action(async (argv) => {
|
|
687
654
|
const checkResult = adminCheck(argv);
|
|
@@ -738,9 +705,7 @@ var HashManager = class {
|
|
|
738
705
|
existingHashSet.add(uniqueKey);
|
|
739
706
|
}
|
|
740
707
|
}
|
|
741
|
-
if (hashesToInsert.length >= 100)
|
|
742
|
-
await flushBatch();
|
|
743
|
-
}
|
|
708
|
+
if (hashesToInsert.length >= 100) await flushBatch();
|
|
744
709
|
} catch (error) {
|
|
745
710
|
errorCount++;
|
|
746
711
|
this.logger.warn(`补全回声洞(${cave.id})哈希时发生错误: ${error.message}`);
|
|
@@ -823,17 +788,13 @@ var HashManager = class {
|
|
|
823
788
|
const text2 = textHashes.get(id2);
|
|
824
789
|
if (text1 && text2) {
|
|
825
790
|
const similarity = this.calculateSimilarity(text1, text2);
|
|
826
|
-
if (similarity >= textThreshold) {
|
|
827
|
-
similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
828
|
-
}
|
|
791
|
+
if (similarity >= textThreshold) similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
829
792
|
}
|
|
830
793
|
const global1 = globalHashes.get(id1);
|
|
831
794
|
const global2 = globalHashes.get(id2);
|
|
832
795
|
if (global1 && global2) {
|
|
833
796
|
const similarity = this.calculateSimilarity(global1, global2);
|
|
834
|
-
if (similarity >= imageThreshold) {
|
|
835
|
-
similarPairs.global.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
836
|
-
}
|
|
797
|
+
if (similarity >= imageThreshold) similarPairs.global.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
837
798
|
}
|
|
838
799
|
}
|
|
839
800
|
}
|
|
@@ -862,9 +823,7 @@ var HashManager = class {
|
|
|
862
823
|
components.get(root).add(id);
|
|
863
824
|
}
|
|
864
825
|
const partialGroups = [];
|
|
865
|
-
for (const component of components.values())
|
|
866
|
-
if (component.size > 1) partialGroups.push(Array.from(component).sort((a, b) => a - b).join(" & "));
|
|
867
|
-
}
|
|
826
|
+
for (const component of components.values()) if (component.size > 1) partialGroups.push(Array.from(component).sort((a, b) => a - b).join(" & "));
|
|
868
827
|
const totalFindings = similarPairs.text.size + similarPairs.global.size + partialGroups.length;
|
|
869
828
|
if (totalFindings === 0) return "未发现高相似度的内容";
|
|
870
829
|
let report = `已发现 ${totalFindings} 组高相似度的内容:`;
|
|
@@ -913,9 +872,7 @@ var HashManager = class {
|
|
|
913
872
|
const scale = Math.sqrt(2 / N);
|
|
914
873
|
for (let k = 0; k < N; k++) {
|
|
915
874
|
let sum = 0;
|
|
916
|
-
for (let n = 0; n < N; n++)
|
|
917
|
-
sum += input[n] * cosines[n][k];
|
|
918
|
-
}
|
|
875
|
+
for (let n = 0; n < N; n++) sum += input[n] * cosines[n][k];
|
|
919
876
|
output[k] = scale * sum;
|
|
920
877
|
}
|
|
921
878
|
output[0] /= Math.sqrt(2);
|
|
@@ -938,16 +895,10 @@ var HashManager = class {
|
|
|
938
895
|
if (!Number.isInteger(hashGridSize)) throw new Error("哈希位数必须是完全平方数");
|
|
939
896
|
const pixels = await (0, import_sharp.default)(imageBuffer).grayscale().resize(dctSize, dctSize, { fit: "fill" }).raw().toBuffer();
|
|
940
897
|
const matrix = [];
|
|
941
|
-
for (let y = 0; y < dctSize; y++)
|
|
942
|
-
matrix.push(Array.from(pixels.slice(y * dctSize, (y + 1) * dctSize)));
|
|
943
|
-
}
|
|
898
|
+
for (let y = 0; y < dctSize; y++) matrix.push(Array.from(pixels.slice(y * dctSize, (y + 1) * dctSize)));
|
|
944
899
|
const dctMatrix = this._dct2D(matrix);
|
|
945
900
|
const coefficients = [];
|
|
946
|
-
for (let y = 0; y < hashGridSize; y++)
|
|
947
|
-
for (let x = 0; x < hashGridSize; x++) {
|
|
948
|
-
coefficients.push(dctMatrix[y][x]);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
901
|
+
for (let y = 0; y < hashGridSize; y++) for (let x = 0; x < hashGridSize; x++) coefficients.push(dctMatrix[y][x]);
|
|
951
902
|
const median = [...coefficients.slice(1)].sort((a, b) => a - b)[Math.floor((coefficients.length - 1) / 2)];
|
|
952
903
|
const binaryHash = coefficients.map((val) => val > median ? "1" : "0").join("");
|
|
953
904
|
return BigInt("0b" + binaryHash).toString(16).padStart(size / 4, "0");
|
|
@@ -963,9 +914,7 @@ var HashManager = class {
|
|
|
963
914
|
const bin1 = hexToBinary(hex1);
|
|
964
915
|
const bin2 = hexToBinary(hex2);
|
|
965
916
|
const len = Math.min(bin1.length, bin2.length);
|
|
966
|
-
for (let i = 0; i < len; i++)
|
|
967
|
-
if (bin1[i] !== bin2[i]) distance++;
|
|
968
|
-
}
|
|
917
|
+
for (let i = 0; i < len; i++) if (bin1[i] !== bin2[i]) distance++;
|
|
969
918
|
return distance;
|
|
970
919
|
}
|
|
971
920
|
/**
|
|
@@ -992,18 +941,14 @@ var HashManager = class {
|
|
|
992
941
|
if (cleanText.length < n) {
|
|
993
942
|
tokens.add(cleanText);
|
|
994
943
|
} else {
|
|
995
|
-
for (let i = 0; i <= cleanText.length - n; i++)
|
|
996
|
-
tokens.add(cleanText.substring(i, i + n));
|
|
997
|
-
}
|
|
944
|
+
for (let i = 0; i <= cleanText.length - n; i++) tokens.add(cleanText.substring(i, i + n));
|
|
998
945
|
}
|
|
999
946
|
const tokenArray = Array.from(tokens);
|
|
1000
947
|
if (tokenArray.length === 0) return "";
|
|
1001
948
|
const vector = new Array(64).fill(0);
|
|
1002
949
|
tokenArray.forEach((token) => {
|
|
1003
950
|
const hash = crypto.createHash("md5").update(token).digest();
|
|
1004
|
-
for (let i = 0; i < 64; i++)
|
|
1005
|
-
vector[i] += hash[Math.floor(i / 8)] >> i % 8 & 1 ? 1 : -1;
|
|
1006
|
-
}
|
|
951
|
+
for (let i = 0; i < 64; i++) vector[i] += hash[Math.floor(i / 8)] >> i % 8 & 1 ? 1 : -1;
|
|
1007
952
|
});
|
|
1008
953
|
const binaryHash = vector.map((v) => v > 0 ? "1" : "0").join("");
|
|
1009
954
|
return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
|
|
@@ -1011,9 +956,7 @@ var HashManager = class {
|
|
|
1011
956
|
};
|
|
1012
957
|
function hexToBinary(hex) {
|
|
1013
958
|
let bin = "";
|
|
1014
|
-
for (const char of hex)
|
|
1015
|
-
bin += parseInt(char, 16).toString(2).padStart(4, "0");
|
|
1016
|
-
}
|
|
959
|
+
for (const char of hex) bin += parseInt(char, 16).toString(2).padStart(4, "0");
|
|
1017
960
|
return bin;
|
|
1018
961
|
}
|
|
1019
962
|
__name(hexToBinary, "hexToBinary");
|
|
@@ -1090,16 +1033,12 @@ function apply(ctx, config) {
|
|
|
1090
1033
|
try {
|
|
1091
1034
|
const query = getScopeQuery(session, config);
|
|
1092
1035
|
const candidates = await ctx.database.get("cave", query, { fields: ["id"] });
|
|
1093
|
-
if (!candidates.length) {
|
|
1094
|
-
return `当前${config.perChannel && session.channelId ? "本群" : ""}还没有任何回声洞`;
|
|
1095
|
-
}
|
|
1036
|
+
if (!candidates.length) return `当前${config.perChannel && session.channelId ? "本群" : ""}还没有任何回声洞`;
|
|
1096
1037
|
const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
|
|
1097
1038
|
const [randomCave] = await ctx.database.get("cave", { ...query, id: randomId });
|
|
1098
1039
|
updateCooldownTimestamp(session, config, lastUsed);
|
|
1099
1040
|
const messages = await buildCaveMessage(randomCave, config, fileManager, logger, session.platform);
|
|
1100
|
-
for (const message of messages)
|
|
1101
|
-
if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1102
|
-
}
|
|
1041
|
+
for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1103
1042
|
} catch (error) {
|
|
1104
1043
|
logger.error("随机获取回声洞失败:", error);
|
|
1105
1044
|
return "随机获取回声洞失败";
|
|
@@ -1126,10 +1065,8 @@ ${JSON.stringify(session, null, 2)}`);
|
|
|
1126
1065
|
}
|
|
1127
1066
|
const newId = await getNextCaveId(ctx, getScopeQuery(session, config, false), reusableIds);
|
|
1128
1067
|
const { finalElementsForDb, mediaToSave } = await processMessageElements(sourceElements, newId, session, config, logger);
|
|
1129
|
-
if (config.debug)
|
|
1130
|
-
logger.info(`数据库元素:
|
|
1068
|
+
if (config.debug) logger.info(`数据库元素:
|
|
1131
1069
|
${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
1132
|
-
}
|
|
1133
1070
|
if (finalElementsForDb.length === 0) return "无可添加内容";
|
|
1134
1071
|
const textHashesToStore = [];
|
|
1135
1072
|
if (hashManager) {
|
|
@@ -1140,9 +1077,7 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1140
1077
|
const existingTextHashes = await ctx.database.get("cave_hash", { type: "simhash" });
|
|
1141
1078
|
for (const existing of existingTextHashes) {
|
|
1142
1079
|
const similarity = hashManager.calculateSimilarity(newSimhash, existing.hash);
|
|
1143
|
-
if (similarity >= config.textThreshold) {
|
|
1144
|
-
return `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值`;
|
|
1145
|
-
}
|
|
1080
|
+
if (similarity >= config.textThreshold) return `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值`;
|
|
1146
1081
|
}
|
|
1147
1082
|
textHashesToStore.push({ hash: newSimhash, type: "simhash" });
|
|
1148
1083
|
}
|
|
@@ -1163,12 +1098,8 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1163
1098
|
if (hasMedia) {
|
|
1164
1099
|
handleFileUploads(ctx, config, fileManager, logger, reviewManager, newCave, mediaToSave, reusableIds, session, hashManager, textHashesToStore);
|
|
1165
1100
|
} else {
|
|
1166
|
-
if (hashManager && textHashesToStore.length > 0) {
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1169
|
-
if (initialStatus === "pending") {
|
|
1170
|
-
reviewManager.sendForPend(newCave);
|
|
1171
|
-
}
|
|
1101
|
+
if (hashManager && textHashesToStore.length > 0) await ctx.database.upsert("cave_hash", textHashesToStore.map((h4) => ({ ...h4, cave: newCave.id })));
|
|
1102
|
+
if (initialStatus === "pending") reviewManager.sendForPend(newCave);
|
|
1172
1103
|
}
|
|
1173
1104
|
return initialStatus === "pending" || initialStatus === "preload" && config.enablePend ? `提交成功,序号为(${newCave.id})` : `添加成功,序号为(${newCave.id})`;
|
|
1174
1105
|
} catch (error) {
|
|
@@ -1185,9 +1116,7 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1185
1116
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
1186
1117
|
updateCooldownTimestamp(session, config, lastUsed);
|
|
1187
1118
|
const messages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform);
|
|
1188
|
-
for (const message of messages)
|
|
1189
|
-
if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1190
|
-
}
|
|
1119
|
+
for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1191
1120
|
} catch (error) {
|
|
1192
1121
|
logger.error(`查看回声洞(${id})失败:`, error);
|
|
1193
1122
|
return "查看失败,请稍后再试";
|
|
@@ -1202,12 +1131,9 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1202
1131
|
const isAdmin = session.channelId === config.adminChannel?.split(":")[1];
|
|
1203
1132
|
if (!isAuthor && !isAdmin) return "你没有权限删除这条回声洞";
|
|
1204
1133
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
1205
|
-
const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform);
|
|
1134
|
+
const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
|
|
1206
1135
|
cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
|
|
1207
|
-
await session.send(
|
|
1208
|
-
for (const message of caveMessages) {
|
|
1209
|
-
if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1210
|
-
}
|
|
1136
|
+
for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1211
1137
|
} catch (error) {
|
|
1212
1138
|
logger.error(`标记回声洞(${id})失败:`, error);
|
|
1213
1139
|
return "删除失败,请稍后再试";
|
|
@@ -1216,9 +1142,7 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1216
1142
|
cave.subcommand(".list", "查询投稿统计").option("user", "-u <user:user> 指定用户").option("all", "-a 查看排行").action(async ({ session, options }) => {
|
|
1217
1143
|
if (options.all) {
|
|
1218
1144
|
const adminChannelId = config.adminChannel?.split(":")[1];
|
|
1219
|
-
if (session.channelId !== adminChannelId)
|
|
1220
|
-
return "此指令仅限在管理群组中使用";
|
|
1221
|
-
}
|
|
1145
|
+
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
1222
1146
|
try {
|
|
1223
1147
|
const allCaves = await ctx.database.get("cave", { status: "active" });
|
|
1224
1148
|
if (!allCaves.length) return "目前没有任何回声洞投稿。";
|