koishi-plugin-best-cave 2.3.15 → 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 +18 -2
- package/lib/index.js +74 -112
- package/package.json +1 -1
package/lib/Utils.d.ts
CHANGED
|
@@ -9,9 +9,11 @@ import { PendManager } from './PendManager';
|
|
|
9
9
|
* @param config 插件配置。
|
|
10
10
|
* @param fileManager 文件管理器实例。
|
|
11
11
|
* @param logger 日志记录器实例。
|
|
12
|
-
* @
|
|
12
|
+
* @param platform 目标平台名称 (e.g., 'onebot')。
|
|
13
|
+
* @param prefix 可选的消息前缀 (e.g., '已删除', '待审核')。
|
|
14
|
+
* @returns 包含多条消息的数组,每条消息是一个 (string | h)[] 数组。
|
|
13
15
|
*/
|
|
14
|
-
export declare function buildCaveMessage(cave: CaveObject, config: Config, fileManager: FileManager, logger: Logger): Promise<(string | h)[]>;
|
|
16
|
+
export declare function buildCaveMessage(cave: CaveObject, config: Config, fileManager: FileManager, logger: Logger, platform?: string, prefix?: string): Promise<(string | h)[][]>;
|
|
15
17
|
/**
|
|
16
18
|
* @description 清理数据库中标记为 'delete' 状态的回声洞及其关联文件和哈希。
|
|
17
19
|
* @param ctx Koishi 上下文。
|
|
@@ -61,6 +63,20 @@ export declare function processMessageElements(sourceElements: h[], newId: numbe
|
|
|
61
63
|
fileName: string;
|
|
62
64
|
}[];
|
|
63
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
|
+
*/
|
|
64
80
|
export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reviewManager: PendManager, cave: CaveObject, mediaToToSave: {
|
|
65
81
|
sourceUrl: string;
|
|
66
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) {
|
|
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) {
|
|
|
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,12 +326,35 @@ async function buildCaveMessage(cave, config, fileManager, logger2) {
|
|
|
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 [
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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() : "";
|
|
333
|
+
const problematicTypes = ["video", "audio", "file", "forward"];
|
|
334
|
+
const placeholderMap = { video: "[视频]", audio: "[音频]", file: "[文件]", forward: "[合并转发]" };
|
|
335
|
+
const containsProblematic = platform === "onebot" && caveHElements.some((el) => problematicTypes.includes(el.type));
|
|
336
|
+
if (!containsProblematic) {
|
|
337
|
+
const finalMessage = [];
|
|
338
|
+
if (header) finalMessage.push(header + "\n");
|
|
339
|
+
finalMessage.push(...caveHElements);
|
|
340
|
+
if (footer) finalMessage.push("\n" + footer);
|
|
341
|
+
return [finalMessage.length > 0 ? finalMessage : []];
|
|
342
|
+
}
|
|
343
|
+
const initialMessageContent = [];
|
|
344
|
+
const followUpMessages = [];
|
|
345
|
+
for (const el of caveHElements) {
|
|
346
|
+
if (problematicTypes.includes(el.type)) {
|
|
347
|
+
initialMessageContent.push(import_koishi.h.text(placeholderMap[el.type]));
|
|
348
|
+
followUpMessages.push([el]);
|
|
349
|
+
} else {
|
|
350
|
+
initialMessageContent.push(el);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const finalInitialMessage = [];
|
|
354
|
+
if (header) finalInitialMessage.push(header + "\n");
|
|
355
|
+
finalInitialMessage.push(...initialMessageContent);
|
|
356
|
+
if (footer) finalInitialMessage.push("\n" + footer);
|
|
357
|
+
return [finalInitialMessage, ...followUpMessages].filter((msg) => msg.length > 0);
|
|
345
358
|
}
|
|
346
359
|
__name(buildCaveMessage, "buildCaveMessage");
|
|
347
360
|
async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
|
|
@@ -349,9 +362,7 @@ async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
|
|
|
349
362
|
const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
|
|
350
363
|
if (!cavesToDelete.length) return;
|
|
351
364
|
const idsToDelete = cavesToDelete.map((c) => c.id);
|
|
352
|
-
for (const cave of cavesToDelete)
|
|
353
|
-
await Promise.all(cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file)));
|
|
354
|
-
}
|
|
365
|
+
for (const cave of cavesToDelete) await Promise.all(cave.elements.filter((el) => el.file).map((el) => fileManager.deleteFile(el.file)));
|
|
355
366
|
reusableIds.delete(0);
|
|
356
367
|
idsToDelete.forEach((id) => reusableIds.add(id));
|
|
357
368
|
await ctx.database.remove("cave", { id: { $in: idsToDelete } });
|
|
@@ -384,30 +395,22 @@ async function getNextCaveId(ctx, query = {}, reusableIds) {
|
|
|
384
395
|
const existingIds = new Set(allCaveIds);
|
|
385
396
|
let newId = 1;
|
|
386
397
|
while (existingIds.has(newId)) newId++;
|
|
387
|
-
if (existingIds.size === (allCaveIds.length > 0 ? Math.max(...allCaveIds) : 0))
|
|
388
|
-
reusableIds.add(0);
|
|
389
|
-
}
|
|
398
|
+
if (existingIds.size === (allCaveIds.length > 0 ? Math.max(...allCaveIds) : 0)) reusableIds.add(0);
|
|
390
399
|
return newId;
|
|
391
400
|
}
|
|
392
401
|
__name(getNextCaveId, "getNextCaveId");
|
|
393
402
|
function checkCooldown(session, config, lastUsed) {
|
|
394
403
|
const adminChannelId = config.adminChannel?.split(":")[1];
|
|
395
|
-
if (adminChannelId && session.channelId === adminChannelId)
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
404
|
+
if (adminChannelId && session.channelId === adminChannelId) return null;
|
|
398
405
|
if (config.coolDown <= 0 || !session.channelId) return null;
|
|
399
406
|
const lastTime = lastUsed.get(session.channelId) || 0;
|
|
400
407
|
const remainingTime = lastTime + config.coolDown * 1e3 - Date.now();
|
|
401
|
-
if (remainingTime > 0) {
|
|
402
|
-
return `指令冷却中,请在 ${Math.ceil(remainingTime / 1e3)} 秒后重试`;
|
|
403
|
-
}
|
|
408
|
+
if (remainingTime > 0) return `指令冷却中,请在 ${Math.ceil(remainingTime / 1e3)} 秒后重试`;
|
|
404
409
|
return null;
|
|
405
410
|
}
|
|
406
411
|
__name(checkCooldown, "checkCooldown");
|
|
407
412
|
function updateCooldownTimestamp(session, config, lastUsed) {
|
|
408
|
-
if (config.coolDown > 0 && session.channelId)
|
|
409
|
-
lastUsed.set(session.channelId, Date.now());
|
|
410
|
-
}
|
|
413
|
+
if (config.coolDown > 0 && session.channelId) lastUsed.set(session.channelId, Date.now());
|
|
411
414
|
}
|
|
412
415
|
__name(updateCooldownTimestamp, "updateCooldownTimestamp");
|
|
413
416
|
async function processMessageElements(sourceElements, newId, session, config, logger2) {
|
|
@@ -453,13 +456,9 @@ async function processMessageElements(sourceElements, newId, session, config, lo
|
|
|
453
456
|
return (0, import_koishi.h)(type2, attrs);
|
|
454
457
|
});
|
|
455
458
|
const contentElements = await transform(elementsToProcess);
|
|
456
|
-
if (contentElements.length > 0) {
|
|
457
|
-
forwardNodes.push({ userId, userName, elements: contentElements });
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
if (forwardNodes.length > 0) {
|
|
461
|
-
result.push({ type: "forward", content: forwardNodes });
|
|
459
|
+
if (contentElements.length > 0) forwardNodes.push({ userId, userName, elements: contentElements });
|
|
462
460
|
}
|
|
461
|
+
if (forwardNodes.length > 0) result.push({ type: "forward", content: forwardNodes });
|
|
463
462
|
} else if (["image", "video", "audio", "file"].includes(type) && el.attrs.src) {
|
|
464
463
|
let fileIdentifier = el.attrs.src;
|
|
465
464
|
if (fileIdentifier.startsWith("http")) {
|
|
@@ -511,9 +510,7 @@ async function handleFileUploads(ctx, config, fileManager, logger2, reviewManage
|
|
|
511
510
|
await ctx.database.upsert("cave", [{ id: cave.id, status: finalStatus }]);
|
|
512
511
|
if (hashManager) {
|
|
513
512
|
const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h4) => ({ ...h4, cave: cave.id }));
|
|
514
|
-
if (allHashesToInsert.length > 0)
|
|
515
|
-
await ctx.database.upsert("cave_hash", allHashesToInsert);
|
|
516
|
-
}
|
|
513
|
+
if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
|
|
517
514
|
}
|
|
518
515
|
if (finalStatus === "pending" && reviewManager) {
|
|
519
516
|
const [finalCave] = await ctx.database.get("cave", { id: cave.id });
|
|
@@ -552,9 +549,7 @@ var PendManager = class {
|
|
|
552
549
|
*/
|
|
553
550
|
registerCommands(cave) {
|
|
554
551
|
const requireAdmin = /* @__PURE__ */ __name((session) => {
|
|
555
|
-
if (session.channelId !== this.config.adminChannel?.split(":")[1])
|
|
556
|
-
return "此指令仅限在管理群组中使用";
|
|
557
|
-
}
|
|
552
|
+
if (session.channelId !== this.config.adminChannel?.split(":")[1]) return "此指令仅限在管理群组中使用";
|
|
558
553
|
return null;
|
|
559
554
|
}, "requireAdmin");
|
|
560
555
|
const pend = cave.subcommand(".pend [id:posint]", "审核回声洞").action(async ({ session }, id) => {
|
|
@@ -564,7 +559,9 @@ var PendManager = class {
|
|
|
564
559
|
const [targetCave] = await this.ctx.database.get("cave", { id });
|
|
565
560
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
566
561
|
if (targetCave.status !== "pending") return `回声洞(${id})无需审核`;
|
|
567
|
-
|
|
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));
|
|
564
|
+
return;
|
|
568
565
|
}
|
|
569
566
|
const pendingCaves = await this.ctx.database.get("cave", { status: "pending" }, { fields: ["id"] });
|
|
570
567
|
if (!pendingCaves.length) return "当前没有需要审核的回声洞";
|
|
@@ -608,8 +605,9 @@ ${pendingCaves.map((c) => c.id).join("|")}`;
|
|
|
608
605
|
return;
|
|
609
606
|
}
|
|
610
607
|
try {
|
|
611
|
-
const
|
|
612
|
-
await
|
|
608
|
+
const [platform] = this.config.adminChannel.split(":", 1);
|
|
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));
|
|
613
611
|
} catch (error) {
|
|
614
612
|
this.logger.error(`发送回声洞(${cave.id})审核消息失败:`, error);
|
|
615
613
|
}
|
|
@@ -650,9 +648,7 @@ var HashManager = class {
|
|
|
650
648
|
registerCommands(cave) {
|
|
651
649
|
const adminCheck = /* @__PURE__ */ __name(({ session }) => {
|
|
652
650
|
const adminChannelId = this.config.adminChannel?.split(":")[1];
|
|
653
|
-
if (session.channelId !== adminChannelId)
|
|
654
|
-
return "此指令仅限在管理群组中使用";
|
|
655
|
-
}
|
|
651
|
+
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
656
652
|
}, "adminCheck");
|
|
657
653
|
cave.subcommand(".hash", "校验回声洞").usage("校验缺失哈希的回声洞,补全哈希记录。").action(async (argv) => {
|
|
658
654
|
const checkResult = adminCheck(argv);
|
|
@@ -709,9 +705,7 @@ var HashManager = class {
|
|
|
709
705
|
existingHashSet.add(uniqueKey);
|
|
710
706
|
}
|
|
711
707
|
}
|
|
712
|
-
if (hashesToInsert.length >= 100)
|
|
713
|
-
await flushBatch();
|
|
714
|
-
}
|
|
708
|
+
if (hashesToInsert.length >= 100) await flushBatch();
|
|
715
709
|
} catch (error) {
|
|
716
710
|
errorCount++;
|
|
717
711
|
this.logger.warn(`补全回声洞(${cave.id})哈希时发生错误: ${error.message}`);
|
|
@@ -794,17 +788,13 @@ var HashManager = class {
|
|
|
794
788
|
const text2 = textHashes.get(id2);
|
|
795
789
|
if (text1 && text2) {
|
|
796
790
|
const similarity = this.calculateSimilarity(text1, text2);
|
|
797
|
-
if (similarity >= textThreshold) {
|
|
798
|
-
similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
799
|
-
}
|
|
791
|
+
if (similarity >= textThreshold) similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
800
792
|
}
|
|
801
793
|
const global1 = globalHashes.get(id1);
|
|
802
794
|
const global2 = globalHashes.get(id2);
|
|
803
795
|
if (global1 && global2) {
|
|
804
796
|
const similarity = this.calculateSimilarity(global1, global2);
|
|
805
|
-
if (similarity >= imageThreshold) {
|
|
806
|
-
similarPairs.global.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
807
|
-
}
|
|
797
|
+
if (similarity >= imageThreshold) similarPairs.global.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
808
798
|
}
|
|
809
799
|
}
|
|
810
800
|
}
|
|
@@ -833,9 +823,7 @@ var HashManager = class {
|
|
|
833
823
|
components.get(root).add(id);
|
|
834
824
|
}
|
|
835
825
|
const partialGroups = [];
|
|
836
|
-
for (const component of components.values())
|
|
837
|
-
if (component.size > 1) partialGroups.push(Array.from(component).sort((a, b) => a - b).join(" & "));
|
|
838
|
-
}
|
|
826
|
+
for (const component of components.values()) if (component.size > 1) partialGroups.push(Array.from(component).sort((a, b) => a - b).join(" & "));
|
|
839
827
|
const totalFindings = similarPairs.text.size + similarPairs.global.size + partialGroups.length;
|
|
840
828
|
if (totalFindings === 0) return "未发现高相似度的内容";
|
|
841
829
|
let report = `已发现 ${totalFindings} 组高相似度的内容:`;
|
|
@@ -884,9 +872,7 @@ var HashManager = class {
|
|
|
884
872
|
const scale = Math.sqrt(2 / N);
|
|
885
873
|
for (let k = 0; k < N; k++) {
|
|
886
874
|
let sum = 0;
|
|
887
|
-
for (let n = 0; n < N; n++)
|
|
888
|
-
sum += input[n] * cosines[n][k];
|
|
889
|
-
}
|
|
875
|
+
for (let n = 0; n < N; n++) sum += input[n] * cosines[n][k];
|
|
890
876
|
output[k] = scale * sum;
|
|
891
877
|
}
|
|
892
878
|
output[0] /= Math.sqrt(2);
|
|
@@ -909,16 +895,10 @@ var HashManager = class {
|
|
|
909
895
|
if (!Number.isInteger(hashGridSize)) throw new Error("哈希位数必须是完全平方数");
|
|
910
896
|
const pixels = await (0, import_sharp.default)(imageBuffer).grayscale().resize(dctSize, dctSize, { fit: "fill" }).raw().toBuffer();
|
|
911
897
|
const matrix = [];
|
|
912
|
-
for (let y = 0; y < dctSize; y++)
|
|
913
|
-
matrix.push(Array.from(pixels.slice(y * dctSize, (y + 1) * dctSize)));
|
|
914
|
-
}
|
|
898
|
+
for (let y = 0; y < dctSize; y++) matrix.push(Array.from(pixels.slice(y * dctSize, (y + 1) * dctSize)));
|
|
915
899
|
const dctMatrix = this._dct2D(matrix);
|
|
916
900
|
const coefficients = [];
|
|
917
|
-
for (let y = 0; y < hashGridSize; y++)
|
|
918
|
-
for (let x = 0; x < hashGridSize; x++) {
|
|
919
|
-
coefficients.push(dctMatrix[y][x]);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
901
|
+
for (let y = 0; y < hashGridSize; y++) for (let x = 0; x < hashGridSize; x++) coefficients.push(dctMatrix[y][x]);
|
|
922
902
|
const median = [...coefficients.slice(1)].sort((a, b) => a - b)[Math.floor((coefficients.length - 1) / 2)];
|
|
923
903
|
const binaryHash = coefficients.map((val) => val > median ? "1" : "0").join("");
|
|
924
904
|
return BigInt("0b" + binaryHash).toString(16).padStart(size / 4, "0");
|
|
@@ -934,9 +914,7 @@ var HashManager = class {
|
|
|
934
914
|
const bin1 = hexToBinary(hex1);
|
|
935
915
|
const bin2 = hexToBinary(hex2);
|
|
936
916
|
const len = Math.min(bin1.length, bin2.length);
|
|
937
|
-
for (let i = 0; i < len; i++)
|
|
938
|
-
if (bin1[i] !== bin2[i]) distance++;
|
|
939
|
-
}
|
|
917
|
+
for (let i = 0; i < len; i++) if (bin1[i] !== bin2[i]) distance++;
|
|
940
918
|
return distance;
|
|
941
919
|
}
|
|
942
920
|
/**
|
|
@@ -963,18 +941,14 @@ var HashManager = class {
|
|
|
963
941
|
if (cleanText.length < n) {
|
|
964
942
|
tokens.add(cleanText);
|
|
965
943
|
} else {
|
|
966
|
-
for (let i = 0; i <= cleanText.length - n; i++)
|
|
967
|
-
tokens.add(cleanText.substring(i, i + n));
|
|
968
|
-
}
|
|
944
|
+
for (let i = 0; i <= cleanText.length - n; i++) tokens.add(cleanText.substring(i, i + n));
|
|
969
945
|
}
|
|
970
946
|
const tokenArray = Array.from(tokens);
|
|
971
947
|
if (tokenArray.length === 0) return "";
|
|
972
948
|
const vector = new Array(64).fill(0);
|
|
973
949
|
tokenArray.forEach((token) => {
|
|
974
950
|
const hash = crypto.createHash("md5").update(token).digest();
|
|
975
|
-
for (let i = 0; i < 64; i++)
|
|
976
|
-
vector[i] += hash[Math.floor(i / 8)] >> i % 8 & 1 ? 1 : -1;
|
|
977
|
-
}
|
|
951
|
+
for (let i = 0; i < 64; i++) vector[i] += hash[Math.floor(i / 8)] >> i % 8 & 1 ? 1 : -1;
|
|
978
952
|
});
|
|
979
953
|
const binaryHash = vector.map((v) => v > 0 ? "1" : "0").join("");
|
|
980
954
|
return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
|
|
@@ -982,9 +956,7 @@ var HashManager = class {
|
|
|
982
956
|
};
|
|
983
957
|
function hexToBinary(hex) {
|
|
984
958
|
let bin = "";
|
|
985
|
-
for (const char of hex)
|
|
986
|
-
bin += parseInt(char, 16).toString(2).padStart(4, "0");
|
|
987
|
-
}
|
|
959
|
+
for (const char of hex) bin += parseInt(char, 16).toString(2).padStart(4, "0");
|
|
988
960
|
return bin;
|
|
989
961
|
}
|
|
990
962
|
__name(hexToBinary, "hexToBinary");
|
|
@@ -1061,13 +1033,12 @@ function apply(ctx, config) {
|
|
|
1061
1033
|
try {
|
|
1062
1034
|
const query = getScopeQuery(session, config);
|
|
1063
1035
|
const candidates = await ctx.database.get("cave", query, { fields: ["id"] });
|
|
1064
|
-
if (!candidates.length) {
|
|
1065
|
-
return `当前${config.perChannel && session.channelId ? "本群" : ""}还没有任何回声洞`;
|
|
1066
|
-
}
|
|
1036
|
+
if (!candidates.length) return `当前${config.perChannel && session.channelId ? "本群" : ""}还没有任何回声洞`;
|
|
1067
1037
|
const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
|
|
1068
1038
|
const [randomCave] = await ctx.database.get("cave", { ...query, id: randomId });
|
|
1069
1039
|
updateCooldownTimestamp(session, config, lastUsed);
|
|
1070
|
-
|
|
1040
|
+
const messages = await buildCaveMessage(randomCave, config, fileManager, logger, session.platform);
|
|
1041
|
+
for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1071
1042
|
} catch (error) {
|
|
1072
1043
|
logger.error("随机获取回声洞失败:", error);
|
|
1073
1044
|
return "随机获取回声洞失败";
|
|
@@ -1094,10 +1065,8 @@ ${JSON.stringify(session, null, 2)}`);
|
|
|
1094
1065
|
}
|
|
1095
1066
|
const newId = await getNextCaveId(ctx, getScopeQuery(session, config, false), reusableIds);
|
|
1096
1067
|
const { finalElementsForDb, mediaToSave } = await processMessageElements(sourceElements, newId, session, config, logger);
|
|
1097
|
-
if (config.debug)
|
|
1098
|
-
logger.info(`数据库元素:
|
|
1068
|
+
if (config.debug) logger.info(`数据库元素:
|
|
1099
1069
|
${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
1100
|
-
}
|
|
1101
1070
|
if (finalElementsForDb.length === 0) return "无可添加内容";
|
|
1102
1071
|
const textHashesToStore = [];
|
|
1103
1072
|
if (hashManager) {
|
|
@@ -1108,9 +1077,7 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1108
1077
|
const existingTextHashes = await ctx.database.get("cave_hash", { type: "simhash" });
|
|
1109
1078
|
for (const existing of existingTextHashes) {
|
|
1110
1079
|
const similarity = hashManager.calculateSimilarity(newSimhash, existing.hash);
|
|
1111
|
-
if (similarity >= config.textThreshold) {
|
|
1112
|
-
return `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值`;
|
|
1113
|
-
}
|
|
1080
|
+
if (similarity >= config.textThreshold) return `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值`;
|
|
1114
1081
|
}
|
|
1115
1082
|
textHashesToStore.push({ hash: newSimhash, type: "simhash" });
|
|
1116
1083
|
}
|
|
@@ -1131,12 +1098,8 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1131
1098
|
if (hasMedia) {
|
|
1132
1099
|
handleFileUploads(ctx, config, fileManager, logger, reviewManager, newCave, mediaToSave, reusableIds, session, hashManager, textHashesToStore);
|
|
1133
1100
|
} else {
|
|
1134
|
-
if (hashManager && textHashesToStore.length > 0) {
|
|
1135
|
-
|
|
1136
|
-
}
|
|
1137
|
-
if (initialStatus === "pending") {
|
|
1138
|
-
reviewManager.sendForPend(newCave);
|
|
1139
|
-
}
|
|
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);
|
|
1140
1103
|
}
|
|
1141
1104
|
return initialStatus === "pending" || initialStatus === "preload" && config.enablePend ? `提交成功,序号为(${newCave.id})` : `添加成功,序号为(${newCave.id})`;
|
|
1142
1105
|
} catch (error) {
|
|
@@ -1152,7 +1115,8 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1152
1115
|
const [targetCave] = await ctx.database.get("cave", { ...getScopeQuery(session, config), id });
|
|
1153
1116
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
1154
1117
|
updateCooldownTimestamp(session, config, lastUsed);
|
|
1155
|
-
|
|
1118
|
+
const messages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform);
|
|
1119
|
+
for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1156
1120
|
} catch (error) {
|
|
1157
1121
|
logger.error(`查看回声洞(${id})失败:`, error);
|
|
1158
1122
|
return "查看失败,请稍后再试";
|
|
@@ -1167,9 +1131,9 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1167
1131
|
const isAdmin = session.channelId === config.adminChannel?.split(":")[1];
|
|
1168
1132
|
if (!isAuthor && !isAdmin) return "你没有权限删除这条回声洞";
|
|
1169
1133
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
1170
|
-
const
|
|
1134
|
+
const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
|
|
1171
1135
|
cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
|
|
1172
|
-
|
|
1136
|
+
for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1173
1137
|
} catch (error) {
|
|
1174
1138
|
logger.error(`标记回声洞(${id})失败:`, error);
|
|
1175
1139
|
return "删除失败,请稍后再试";
|
|
@@ -1178,9 +1142,7 @@ ${JSON.stringify(finalElementsForDb, null, 2)}`);
|
|
|
1178
1142
|
cave.subcommand(".list", "查询投稿统计").option("user", "-u <user:user> 指定用户").option("all", "-a 查看排行").action(async ({ session, options }) => {
|
|
1179
1143
|
if (options.all) {
|
|
1180
1144
|
const adminChannelId = config.adminChannel?.split(":")[1];
|
|
1181
|
-
if (session.channelId !== adminChannelId)
|
|
1182
|
-
return "此指令仅限在管理群组中使用";
|
|
1183
|
-
}
|
|
1145
|
+
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
1184
1146
|
try {
|
|
1185
1147
|
const allCaves = await ctx.database.get("cave", { status: "active" });
|
|
1186
1148
|
if (!allCaves.length) return "目前没有任何回声洞投稿。";
|