koishi-plugin-best-cave 1.5.4 → 1.5.6
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 +24 -0
- package/lib/index.js +198 -219
- package/lib/types.d.ts +24 -0
- package/lib/utils/AuditHandler.d.ts +17 -0
- package/lib/utils/ContentHasher.d.ts +80 -0
- package/lib/utils/FileHandler.d.ts +63 -0
- package/lib/utils/HashManager.d.ts +108 -0
- package/lib/utils/IdManager.d.ts +69 -0
- package/lib/utils/MediaHandler.d.ts +30 -0
- package/lib/utils/ProcessHandle.d.ts +7 -0
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { Context, Schema } from 'koishi';
|
|
2
2
|
export declare const name = "best-cave";
|
|
3
3
|
export declare const inject: string[];
|
|
4
|
+
export interface BaseElement {
|
|
5
|
+
type: 'text' | 'img' | 'video';
|
|
6
|
+
index: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TextElement extends BaseElement {
|
|
9
|
+
type: 'text';
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MediaElement extends BaseElement {
|
|
13
|
+
type: 'img' | 'video';
|
|
14
|
+
file?: string;
|
|
15
|
+
fileName?: string;
|
|
16
|
+
fileSize?: string;
|
|
17
|
+
filePath?: string;
|
|
18
|
+
}
|
|
19
|
+
export type Element = TextElement | MediaElement;
|
|
20
|
+
export interface CaveObject {
|
|
21
|
+
cave_id: number;
|
|
22
|
+
elements: Element[];
|
|
23
|
+
contributor_number: string;
|
|
24
|
+
contributor_name: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PendingCave extends CaveObject {
|
|
27
|
+
}
|
|
4
28
|
/**
|
|
5
29
|
* 插件配置项
|
|
6
30
|
* @type {Schema}
|
package/lib/index.js
CHANGED
|
@@ -54,10 +54,10 @@ __export(src_exports, {
|
|
|
54
54
|
});
|
|
55
55
|
module.exports = __toCommonJS(src_exports);
|
|
56
56
|
var import_koishi6 = require("koishi");
|
|
57
|
-
var
|
|
58
|
-
var
|
|
57
|
+
var fs7 = __toESM(require("fs"));
|
|
58
|
+
var path7 = __toESM(require("path"));
|
|
59
59
|
|
|
60
|
-
// src/utils/
|
|
60
|
+
// src/utils/FileHandler.ts
|
|
61
61
|
var fs = __toESM(require("fs"));
|
|
62
62
|
var path = __toESM(require("path"));
|
|
63
63
|
var import_koishi = require("koishi");
|
|
@@ -216,7 +216,7 @@ var FileHandler = class {
|
|
|
216
216
|
}
|
|
217
217
|
};
|
|
218
218
|
|
|
219
|
-
// src/utils/
|
|
219
|
+
// src/utils/IdManager.ts
|
|
220
220
|
var fs2 = __toESM(require("fs"));
|
|
221
221
|
var path2 = __toESM(require("path"));
|
|
222
222
|
var import_koishi2 = require("koishi");
|
|
@@ -440,12 +440,12 @@ var IdManager = class {
|
|
|
440
440
|
}
|
|
441
441
|
};
|
|
442
442
|
|
|
443
|
-
// src/utils/
|
|
443
|
+
// src/utils/HashManager.ts
|
|
444
444
|
var import_koishi3 = require("koishi");
|
|
445
445
|
var fs3 = __toESM(require("fs"));
|
|
446
446
|
var path3 = __toESM(require("path"));
|
|
447
447
|
|
|
448
|
-
// src/utils/
|
|
448
|
+
// src/utils/ContentHasher.ts
|
|
449
449
|
var import_sharp = __toESM(require("sharp"));
|
|
450
450
|
var ContentHasher = class {
|
|
451
451
|
static {
|
|
@@ -605,22 +605,13 @@ var ContentHasher = class {
|
|
|
605
605
|
}
|
|
606
606
|
return hash.toString(36);
|
|
607
607
|
}
|
|
608
|
-
/**
|
|
609
|
-
* 批量比较一个新哈希值与多个已存在哈希值的相似度
|
|
610
|
-
* @param newHash - 新的哈希值
|
|
611
|
-
* @param existingHashes - 已存在的哈希值数组
|
|
612
|
-
* @returns 相似度数组,每个元素对应一个已存在哈希值的相似度
|
|
613
|
-
*/
|
|
614
|
-
static batchCompareSimilarity(newHash, existingHashes) {
|
|
615
|
-
return existingHashes.map((hash) => this.calculateSimilarity(newHash, hash));
|
|
616
|
-
}
|
|
617
608
|
};
|
|
618
609
|
|
|
619
|
-
// src/utils/
|
|
610
|
+
// src/utils/HashManager.ts
|
|
620
611
|
var import_util = require("util");
|
|
621
612
|
var logger3 = new import_koishi3.Logger("HashManager");
|
|
622
613
|
var readFileAsync = (0, import_util.promisify)(fs3.readFile);
|
|
623
|
-
var
|
|
614
|
+
var HashManager = class _HashManager {
|
|
624
615
|
/**
|
|
625
616
|
* 初始化HashManager实例
|
|
626
617
|
* @param caveDir 回声洞数据目录路径
|
|
@@ -629,7 +620,7 @@ var ContentHashManager = class _ContentHashManager {
|
|
|
629
620
|
this.caveDir = caveDir;
|
|
630
621
|
}
|
|
631
622
|
static {
|
|
632
|
-
__name(this, "
|
|
623
|
+
__name(this, "HashManager");
|
|
633
624
|
}
|
|
634
625
|
// 哈希数据文件名
|
|
635
626
|
static HASH_FILE = "hash.json";
|
|
@@ -643,13 +634,13 @@ var ContentHashManager = class _ContentHashManager {
|
|
|
643
634
|
// 初始化状态标志
|
|
644
635
|
initialized = false;
|
|
645
636
|
get filePath() {
|
|
646
|
-
return path3.join(this.caveDir,
|
|
637
|
+
return path3.join(this.caveDir, _HashManager.HASH_FILE);
|
|
647
638
|
}
|
|
648
639
|
get resourceDir() {
|
|
649
640
|
return path3.join(this.caveDir, "resources");
|
|
650
641
|
}
|
|
651
642
|
get caveFilePath() {
|
|
652
|
-
return path3.join(this.caveDir,
|
|
643
|
+
return path3.join(this.caveDir, _HashManager.CAVE_FILE);
|
|
653
644
|
}
|
|
654
645
|
/**
|
|
655
646
|
* 初始化哈希存储
|
|
@@ -973,7 +964,7 @@ var ContentHashManager = class _ContentHashManager {
|
|
|
973
964
|
* @param batchSize 批处理大小
|
|
974
965
|
* @private
|
|
975
966
|
*/
|
|
976
|
-
async processBatch(items, processor, batchSize =
|
|
967
|
+
async processBatch(items, processor, batchSize = _HashManager.BATCH_SIZE) {
|
|
977
968
|
for (let i = 0; i < items.length; i += batchSize) {
|
|
978
969
|
const batch = items.slice(i, i + batchSize);
|
|
979
970
|
await Promise.all(
|
|
@@ -989,15 +980,14 @@ var ContentHashManager = class _ContentHashManager {
|
|
|
989
980
|
}
|
|
990
981
|
};
|
|
991
982
|
|
|
992
|
-
// src/utils/
|
|
983
|
+
// src/utils/AuditHandler.ts
|
|
993
984
|
var import_koishi4 = require("koishi");
|
|
994
985
|
var fs4 = __toESM(require("fs"));
|
|
995
986
|
var path4 = __toESM(require("path"));
|
|
996
987
|
var AuditManager = class {
|
|
997
|
-
constructor(ctx, config,
|
|
988
|
+
constructor(ctx, config, idManager) {
|
|
998
989
|
this.ctx = ctx;
|
|
999
990
|
this.config = config;
|
|
1000
|
-
this.caveDir = caveDir;
|
|
1001
991
|
this.idManager = idManager;
|
|
1002
992
|
}
|
|
1003
993
|
static {
|
|
@@ -1188,11 +1178,70 @@ ${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
|
|
|
1188
1178
|
}
|
|
1189
1179
|
};
|
|
1190
1180
|
|
|
1191
|
-
// src/utils/
|
|
1181
|
+
// src/utils/MediaHandler.ts
|
|
1192
1182
|
var import_koishi5 = require("koishi");
|
|
1193
1183
|
var fs5 = __toESM(require("fs"));
|
|
1194
1184
|
var path5 = __toESM(require("path"));
|
|
1195
1185
|
var logger4 = new import_koishi5.Logger("MediaHandle");
|
|
1186
|
+
async function buildMessage(cave, resourceDir, session) {
|
|
1187
|
+
if (!cave?.elements?.length) {
|
|
1188
|
+
return session.text("commands.cave.error.noContent");
|
|
1189
|
+
}
|
|
1190
|
+
const videoElement = cave.elements.find((el) => el.type === "video");
|
|
1191
|
+
const nonVideoElements = cave.elements.filter((el) => el.type !== "video").sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1192
|
+
if (videoElement?.file) {
|
|
1193
|
+
const basicInfo = [
|
|
1194
|
+
session.text("commands.cave.message.caveTitle", [cave.cave_id]),
|
|
1195
|
+
session.text("commands.cave.message.contributorSuffix", [cave.contributor_name])
|
|
1196
|
+
].join("\n");
|
|
1197
|
+
await session?.send(basicInfo);
|
|
1198
|
+
const filePath = path5.join(resourceDir, videoElement.file);
|
|
1199
|
+
const base64Data = await processMediaFile(filePath, "video");
|
|
1200
|
+
if (base64Data && session) {
|
|
1201
|
+
await session.send((0, import_koishi5.h)("video", { src: base64Data }));
|
|
1202
|
+
}
|
|
1203
|
+
return "";
|
|
1204
|
+
}
|
|
1205
|
+
const lines = [session.text("commands.cave.message.caveTitle", [cave.cave_id])];
|
|
1206
|
+
for (const element of nonVideoElements) {
|
|
1207
|
+
if (element.type === "text") {
|
|
1208
|
+
lines.push(element.content);
|
|
1209
|
+
} else if (element.type === "img" && element.file) {
|
|
1210
|
+
const filePath = path5.join(resourceDir, element.file);
|
|
1211
|
+
const base64Data = await processMediaFile(filePath, "image");
|
|
1212
|
+
if (base64Data) {
|
|
1213
|
+
lines.push((0, import_koishi5.h)("image", { src: base64Data }));
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
lines.push(session.text("commands.cave.message.contributorSuffix", [cave.contributor_name]));
|
|
1218
|
+
return lines.join("\n");
|
|
1219
|
+
}
|
|
1220
|
+
__name(buildMessage, "buildMessage");
|
|
1221
|
+
async function sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
|
|
1222
|
+
try {
|
|
1223
|
+
const msg = await session.send(session.text(key, params));
|
|
1224
|
+
if (isTemp && msg) {
|
|
1225
|
+
setTimeout(async () => {
|
|
1226
|
+
try {
|
|
1227
|
+
await session.bot.deleteMessage(session.channelId, msg);
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
logger4.debug(`Failed to delete temporary message: ${error.message}`);
|
|
1230
|
+
}
|
|
1231
|
+
}, timeout);
|
|
1232
|
+
}
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
logger4.error(`Failed to send message: ${error.message}`);
|
|
1235
|
+
}
|
|
1236
|
+
return "";
|
|
1237
|
+
}
|
|
1238
|
+
__name(sendMessage, "sendMessage");
|
|
1239
|
+
async function processMediaFile(filePath, type) {
|
|
1240
|
+
const data = await fs5.promises.readFile(filePath).catch(() => null);
|
|
1241
|
+
if (!data) return null;
|
|
1242
|
+
return `data:${type}/${type === "image" ? "png" : "mp4"};base64,${data.toString("base64")}`;
|
|
1243
|
+
}
|
|
1244
|
+
__name(processMediaFile, "processMediaFile");
|
|
1196
1245
|
async function extractMediaContent(originalContent, config, session) {
|
|
1197
1246
|
const textParts = originalContent.split(/<(img|video)[^>]+>/).map((text, idx) => text.trim() && {
|
|
1198
1247
|
type: "text",
|
|
@@ -1236,7 +1285,7 @@ async function extractMediaContent(originalContent, config, session) {
|
|
|
1236
1285
|
__name(extractMediaContent, "extractMediaContent");
|
|
1237
1286
|
async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config, ctx, session, buffers) {
|
|
1238
1287
|
const accept = mediaType === "img" ? "image/*" : "video/*";
|
|
1239
|
-
const hashStorage = new
|
|
1288
|
+
const hashStorage = new HashManager(path5.join(ctx.baseDir, "data", "cave"));
|
|
1240
1289
|
await hashStorage.initialize();
|
|
1241
1290
|
const downloadTasks = urls.map(async (url, i) => {
|
|
1242
1291
|
const fileName = fileNames[i];
|
|
@@ -1317,20 +1366,99 @@ async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config
|
|
|
1317
1366
|
return Promise.all(downloadTasks);
|
|
1318
1367
|
}
|
|
1319
1368
|
__name(saveMedia, "saveMedia");
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1369
|
+
|
|
1370
|
+
// src/utils/ProcessHandle.ts
|
|
1371
|
+
var fs6 = __toESM(require("fs"));
|
|
1372
|
+
var path6 = __toESM(require("path"));
|
|
1373
|
+
async function processList(session, config, idManager, userId, pageNum = 1) {
|
|
1374
|
+
const stats = idManager.getStats();
|
|
1375
|
+
if (userId && userId in stats) {
|
|
1376
|
+
const ids = stats[userId];
|
|
1377
|
+
return session.text("commands.cave.list.totalItems", [userId, ids.length]) + "\n" + session.text("commands.cave.list.idsLine", [ids.join(",")]);
|
|
1378
|
+
}
|
|
1379
|
+
const lines = Object.entries(stats).map(([cid, ids]) => {
|
|
1380
|
+
return session.text("commands.cave.list.totalItems", [cid, ids.length]) + "\n" + session.text("commands.cave.list.idsLine", [ids.join(",")]);
|
|
1381
|
+
});
|
|
1382
|
+
const totalSubmissions = Object.values(stats).reduce((sum, arr) => sum + arr.length, 0);
|
|
1383
|
+
if (config.enablePagination) {
|
|
1384
|
+
const itemsPerPage = config.itemsPerPage;
|
|
1385
|
+
const totalPages = Math.max(1, Math.ceil(lines.length / itemsPerPage));
|
|
1386
|
+
pageNum = Math.min(Math.max(1, pageNum), totalPages);
|
|
1387
|
+
const start = (pageNum - 1) * itemsPerPage;
|
|
1388
|
+
const paginatedLines = lines.slice(start, start + itemsPerPage);
|
|
1389
|
+
return session.text("commands.cave.list.header", [totalSubmissions]) + "\n" + paginatedLines.join("\n") + "\n" + session.text("commands.cave.list.pageInfo", [pageNum, totalPages]);
|
|
1390
|
+
} else {
|
|
1391
|
+
return session.text("commands.cave.list.header", [totalSubmissions]) + "\n" + lines.join("\n");
|
|
1323
1392
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1393
|
+
}
|
|
1394
|
+
__name(processList, "processList");
|
|
1395
|
+
async function processView(caveFilePath, resourceDir, session, options, content) {
|
|
1396
|
+
const caveId = parseInt(content[0] || (typeof options.g === "string" ? options.g : ""));
|
|
1397
|
+
if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
|
|
1398
|
+
const data = await FileHandler.readJsonData(caveFilePath);
|
|
1399
|
+
const cave = data.find((item) => item.cave_id === caveId);
|
|
1400
|
+
if (!cave) return sendMessage(session, "commands.cave.error.notFound", [], true);
|
|
1401
|
+
return buildMessage(cave, resourceDir, session);
|
|
1402
|
+
}
|
|
1403
|
+
__name(processView, "processView");
|
|
1404
|
+
async function processRandom(caveFilePath, resourceDir, session) {
|
|
1405
|
+
const data = await FileHandler.readJsonData(caveFilePath);
|
|
1406
|
+
if (data.length === 0) {
|
|
1407
|
+
return sendMessage(session, "commands.cave.error.noCave", [], true);
|
|
1408
|
+
}
|
|
1409
|
+
const cave = (() => {
|
|
1410
|
+
const validCaves = data.filter((cave2) => cave2.elements && cave2.elements.length > 0);
|
|
1411
|
+
if (!validCaves.length) return void 0;
|
|
1412
|
+
const randomIndex = Math.floor(Math.random() * validCaves.length);
|
|
1413
|
+
return validCaves[randomIndex];
|
|
1414
|
+
})();
|
|
1415
|
+
return cave ? buildMessage(cave, resourceDir, session) : sendMessage(session, "commands.cave.error.getCave", [], true);
|
|
1416
|
+
}
|
|
1417
|
+
__name(processRandom, "processRandom");
|
|
1418
|
+
async function processDelete(caveFilePath, resourceDir, pendingFilePath, session, config, options, content, idManager, HashManager2) {
|
|
1419
|
+
const caveId = parseInt(content[0] || (typeof options.r === "string" ? options.r : ""));
|
|
1420
|
+
if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
|
|
1421
|
+
const data = await FileHandler.readJsonData(caveFilePath);
|
|
1422
|
+
const pendingData = await FileHandler.readJsonData(pendingFilePath);
|
|
1423
|
+
const targetInData = data.find((item) => item.cave_id === caveId);
|
|
1424
|
+
const targetInPending = pendingData.find((item) => item.cave_id === caveId);
|
|
1425
|
+
if (!targetInData && !targetInPending) {
|
|
1426
|
+
return sendMessage(session, "commands.cave.error.notFound", [], true);
|
|
1427
|
+
}
|
|
1428
|
+
const targetCave = targetInData || targetInPending;
|
|
1429
|
+
const isPending = !targetInData;
|
|
1430
|
+
if (targetCave.contributor_number !== session.userId && !config.manager.includes(session.userId)) {
|
|
1431
|
+
return sendMessage(session, "commands.cave.remove.noPermission", [], true);
|
|
1432
|
+
}
|
|
1433
|
+
const caveContent = await buildMessage(targetCave, resourceDir, session);
|
|
1434
|
+
if (targetCave.elements) {
|
|
1435
|
+
await HashManager2.updateCaveContent(caveId, {
|
|
1436
|
+
images: void 0,
|
|
1437
|
+
texts: void 0
|
|
1438
|
+
});
|
|
1439
|
+
for (const element of targetCave.elements) {
|
|
1440
|
+
if ((element.type === "img" || element.type === "video") && element.file) {
|
|
1441
|
+
const fullPath = path6.join(resourceDir, element.file);
|
|
1442
|
+
if (fs6.existsSync(fullPath)) {
|
|
1443
|
+
await fs6.promises.unlink(fullPath);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1328
1446
|
}
|
|
1329
1447
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1448
|
+
if (isPending) {
|
|
1449
|
+
const newPendingData = pendingData.filter((item) => item.cave_id !== caveId);
|
|
1450
|
+
await FileHandler.writeJsonData(pendingFilePath, newPendingData);
|
|
1451
|
+
} else {
|
|
1452
|
+
const newData = data.filter((item) => item.cave_id !== caveId);
|
|
1453
|
+
await FileHandler.writeJsonData(caveFilePath, newData);
|
|
1454
|
+
await idManager.removeStat(targetCave.contributor_number, caveId);
|
|
1455
|
+
}
|
|
1456
|
+
await idManager.markDeleted(caveId);
|
|
1457
|
+
const deleteStatus = isPending ? session.text("commands.cave.remove.deletePending") : "";
|
|
1458
|
+
const deleteMessage = session.text("commands.cave.remove.deleted");
|
|
1459
|
+
return `${deleteMessage}${deleteStatus}${caveContent}`;
|
|
1332
1460
|
}
|
|
1333
|
-
__name(
|
|
1461
|
+
__name(processDelete, "processDelete");
|
|
1334
1462
|
|
|
1335
1463
|
// src/index.ts
|
|
1336
1464
|
var name = "best-cave";
|
|
@@ -1371,111 +1499,21 @@ var Config = import_koishi6.Schema.object({
|
|
|
1371
1499
|
async function apply(ctx, config) {
|
|
1372
1500
|
ctx.i18n.define("zh-CN", require_zh_CN());
|
|
1373
1501
|
ctx.i18n.define("en-US", require_en_US());
|
|
1374
|
-
const dataDir =
|
|
1375
|
-
const caveDir =
|
|
1376
|
-
await FileHandler.ensureDirectory(dataDir);
|
|
1502
|
+
const dataDir = path7.join(ctx.baseDir, "data");
|
|
1503
|
+
const caveDir = path7.join(dataDir, "cave");
|
|
1377
1504
|
await FileHandler.ensureDirectory(caveDir);
|
|
1378
|
-
await FileHandler.ensureDirectory(
|
|
1379
|
-
await FileHandler.ensureJsonFile(
|
|
1380
|
-
await FileHandler.ensureJsonFile(
|
|
1381
|
-
await FileHandler.ensureJsonFile(
|
|
1505
|
+
await FileHandler.ensureDirectory(path7.join(caveDir, "resources"));
|
|
1506
|
+
await FileHandler.ensureJsonFile(path7.join(caveDir, "cave.json"));
|
|
1507
|
+
await FileHandler.ensureJsonFile(path7.join(caveDir, "pending.json"));
|
|
1508
|
+
await FileHandler.ensureJsonFile(path7.join(caveDir, "hash.json"));
|
|
1382
1509
|
const idManager = new IdManager(ctx.baseDir);
|
|
1383
|
-
const contentHashManager = new
|
|
1384
|
-
const auditManager = new AuditManager(ctx, config,
|
|
1510
|
+
const contentHashManager = new HashManager(caveDir);
|
|
1511
|
+
const auditManager = new AuditManager(ctx, config, idManager);
|
|
1385
1512
|
await Promise.all([
|
|
1386
|
-
idManager.initialize(
|
|
1513
|
+
idManager.initialize(path7.join(caveDir, "cave.json"), path7.join(caveDir, "pending.json")),
|
|
1387
1514
|
contentHashManager.initialize()
|
|
1388
1515
|
]);
|
|
1389
1516
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
1390
|
-
async function processList(session, config2, userId, pageNum = 1) {
|
|
1391
|
-
const stats = idManager.getStats();
|
|
1392
|
-
if (userId && userId in stats) {
|
|
1393
|
-
const ids = stats[userId];
|
|
1394
|
-
return session.text("commands.cave.list.totalItems", [userId, ids.length]) + "\n" + session.text("commands.cave.list.idsLine", [ids.join(",")]);
|
|
1395
|
-
}
|
|
1396
|
-
const lines = Object.entries(stats).map(([cid, ids]) => {
|
|
1397
|
-
return session.text("commands.cave.list.totalItems", [cid, ids.length]) + "\n" + session.text("commands.cave.list.idsLine", [ids.join(",")]);
|
|
1398
|
-
});
|
|
1399
|
-
const totalSubmissions = Object.values(stats).reduce((sum, arr) => sum + arr.length, 0);
|
|
1400
|
-
if (config2.enablePagination) {
|
|
1401
|
-
const itemsPerPage = config2.itemsPerPage;
|
|
1402
|
-
const totalPages = Math.max(1, Math.ceil(lines.length / itemsPerPage));
|
|
1403
|
-
pageNum = Math.min(Math.max(1, pageNum), totalPages);
|
|
1404
|
-
const start = (pageNum - 1) * itemsPerPage;
|
|
1405
|
-
const paginatedLines = lines.slice(start, start + itemsPerPage);
|
|
1406
|
-
return session.text("commands.cave.list.header", [totalSubmissions]) + "\n" + paginatedLines.join("\n") + "\n" + session.text("commands.cave.list.pageInfo", [pageNum, totalPages]);
|
|
1407
|
-
} else {
|
|
1408
|
-
return session.text("commands.cave.list.header", [totalSubmissions]) + "\n" + lines.join("\n");
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
__name(processList, "processList");
|
|
1412
|
-
async function processView(caveFilePath, resourceDir, session, options, content) {
|
|
1413
|
-
const caveId = parseInt(content[0] || (typeof options.g === "string" ? options.g : ""));
|
|
1414
|
-
if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
|
|
1415
|
-
const data = await FileHandler.readJsonData(caveFilePath);
|
|
1416
|
-
const cave = data.find((item) => item.cave_id === caveId);
|
|
1417
|
-
if (!cave) return sendMessage(session, "commands.cave.error.notFound", [], true);
|
|
1418
|
-
return buildMessage2(cave, resourceDir, session);
|
|
1419
|
-
}
|
|
1420
|
-
__name(processView, "processView");
|
|
1421
|
-
async function processRandom(caveFilePath, resourceDir, session) {
|
|
1422
|
-
const data = await FileHandler.readJsonData(caveFilePath);
|
|
1423
|
-
if (data.length === 0) {
|
|
1424
|
-
return sendMessage(session, "commands.cave.error.noCave", [], true);
|
|
1425
|
-
}
|
|
1426
|
-
const cave = (() => {
|
|
1427
|
-
const validCaves = data.filter((cave2) => cave2.elements && cave2.elements.length > 0);
|
|
1428
|
-
if (!validCaves.length) return void 0;
|
|
1429
|
-
const randomIndex = Math.floor(Math.random() * validCaves.length);
|
|
1430
|
-
return validCaves[randomIndex];
|
|
1431
|
-
})();
|
|
1432
|
-
return cave ? buildMessage2(cave, resourceDir, session) : sendMessage(session, "commands.cave.error.getCave", [], true);
|
|
1433
|
-
}
|
|
1434
|
-
__name(processRandom, "processRandom");
|
|
1435
|
-
async function processDelete(caveFilePath, resourceDir, pendingFilePath, session, config2, options, content) {
|
|
1436
|
-
const caveId = parseInt(content[0] || (typeof options.r === "string" ? options.r : ""));
|
|
1437
|
-
if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
|
|
1438
|
-
const data = await FileHandler.readJsonData(caveFilePath);
|
|
1439
|
-
const pendingData = await FileHandler.readJsonData(pendingFilePath);
|
|
1440
|
-
const targetInData = data.find((item) => item.cave_id === caveId);
|
|
1441
|
-
const targetInPending = pendingData.find((item) => item.cave_id === caveId);
|
|
1442
|
-
if (!targetInData && !targetInPending) {
|
|
1443
|
-
return sendMessage(session, "commands.cave.error.notFound", [], true);
|
|
1444
|
-
}
|
|
1445
|
-
const targetCave = targetInData || targetInPending;
|
|
1446
|
-
const isPending = !targetInData;
|
|
1447
|
-
if (targetCave.contributor_number !== session.userId && !config2.manager.includes(session.userId)) {
|
|
1448
|
-
return sendMessage(session, "commands.cave.remove.noPermission", [], true);
|
|
1449
|
-
}
|
|
1450
|
-
const caveContent = await buildMessage2(targetCave, resourceDir, session);
|
|
1451
|
-
if (targetCave.elements) {
|
|
1452
|
-
await contentHashManager.updateCaveContent(caveId, {
|
|
1453
|
-
images: void 0,
|
|
1454
|
-
texts: void 0
|
|
1455
|
-
});
|
|
1456
|
-
for (const element of targetCave.elements) {
|
|
1457
|
-
if ((element.type === "img" || element.type === "video") && element.file) {
|
|
1458
|
-
const fullPath = path6.join(resourceDir, element.file);
|
|
1459
|
-
if (fs6.existsSync(fullPath)) {
|
|
1460
|
-
await fs6.promises.unlink(fullPath);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
if (isPending) {
|
|
1466
|
-
const newPendingData = pendingData.filter((item) => item.cave_id !== caveId);
|
|
1467
|
-
await FileHandler.writeJsonData(pendingFilePath, newPendingData);
|
|
1468
|
-
} else {
|
|
1469
|
-
const newData = data.filter((item) => item.cave_id !== caveId);
|
|
1470
|
-
await FileHandler.writeJsonData(caveFilePath, newData);
|
|
1471
|
-
await idManager.removeStat(targetCave.contributor_number, caveId);
|
|
1472
|
-
}
|
|
1473
|
-
await idManager.markDeleted(caveId);
|
|
1474
|
-
const deleteStatus = isPending ? session.text("commands.cave.remove.deletePending") : "";
|
|
1475
|
-
const deleteMessage = session.text("commands.cave.remove.deleted");
|
|
1476
|
-
return `${deleteMessage}${deleteStatus}${caveContent}`;
|
|
1477
|
-
}
|
|
1478
|
-
__name(processDelete, "processDelete");
|
|
1479
1517
|
async function processAdd(ctx2, config2, caveFilePath, resourceDir, pendingFilePath, session, content) {
|
|
1480
1518
|
let caveId;
|
|
1481
1519
|
try {
|
|
@@ -1552,7 +1590,7 @@ async function apply(ctx, config) {
|
|
|
1552
1590
|
index: Number.MAX_SAFE_INTEGER
|
|
1553
1591
|
});
|
|
1554
1592
|
}
|
|
1555
|
-
const hashStorage = new
|
|
1593
|
+
const hashStorage = new HashManager(path7.join(ctx2.baseDir, "data", "cave"));
|
|
1556
1594
|
await hashStorage.initialize();
|
|
1557
1595
|
const hashStatus = await hashStorage.getStatus();
|
|
1558
1596
|
if (!hashStatus.lastUpdated || hashStatus.entries.length === 0) {
|
|
@@ -1569,7 +1607,7 @@ async function apply(ctx, config) {
|
|
|
1569
1607
|
pendingData.push(newCave);
|
|
1570
1608
|
await Promise.all([
|
|
1571
1609
|
FileHandler.writeJsonData(pendingFilePath, pendingData),
|
|
1572
|
-
auditManager.sendAuditMessage(newCave, await
|
|
1610
|
+
auditManager.sendAuditMessage(newCave, await buildMessage(newCave, resourceDir, session), session)
|
|
1573
1611
|
]);
|
|
1574
1612
|
return sendMessage(session, "commands.cave.add.submitPending", [caveId], false);
|
|
1575
1613
|
}
|
|
@@ -1595,14 +1633,14 @@ async function apply(ctx, config) {
|
|
|
1595
1633
|
"commands.cave.error.similarDuplicateFound",
|
|
1596
1634
|
[(result.similarity * 100).toFixed(1)]
|
|
1597
1635
|
);
|
|
1598
|
-
await session.send(duplicateMessage + await
|
|
1636
|
+
await session.send(duplicateMessage + await buildMessage(originalCave, resourceDir, session));
|
|
1599
1637
|
throw new Error("duplicate_found");
|
|
1600
1638
|
}
|
|
1601
1639
|
}
|
|
1602
1640
|
await Promise.all([
|
|
1603
1641
|
FileHandler.writeJsonData(caveFilePath, data),
|
|
1604
1642
|
contentHashManager.updateCaveContent(caveId, {
|
|
1605
|
-
images: savedImages.length > 0 ? await Promise.all(savedImages.map((file) =>
|
|
1643
|
+
images: savedImages.length > 0 ? await Promise.all(savedImages.map((file) => fs7.promises.readFile(path7.join(resourceDir, file)))) : void 0,
|
|
1606
1644
|
texts: textParts.filter((p) => p.type === "text").map((p) => p.content)
|
|
1607
1645
|
})
|
|
1608
1646
|
]);
|
|
@@ -1625,11 +1663,11 @@ async function apply(ctx, config) {
|
|
|
1625
1663
|
return sendMessage(session, "commands.cave.message.blacklisted", [], true);
|
|
1626
1664
|
}
|
|
1627
1665
|
}).action(async ({ session, options }, ...content) => {
|
|
1628
|
-
const dataDir2 =
|
|
1629
|
-
const caveDir2 =
|
|
1630
|
-
const caveFilePath =
|
|
1631
|
-
const resourceDir =
|
|
1632
|
-
const pendingFilePath =
|
|
1666
|
+
const dataDir2 = path7.join(ctx.baseDir, "data");
|
|
1667
|
+
const caveDir2 = path7.join(dataDir2, "cave");
|
|
1668
|
+
const caveFilePath = path7.join(caveDir2, "cave.json");
|
|
1669
|
+
const resourceDir = path7.join(caveDir2, "resources");
|
|
1670
|
+
const pendingFilePath = path7.join(caveDir2, "pending.json");
|
|
1633
1671
|
const needsCooldown = !options.l && !options.a;
|
|
1634
1672
|
if (needsCooldown) {
|
|
1635
1673
|
const guildId = session.guildId;
|
|
@@ -1648,23 +1686,23 @@ async function apply(ctx, config) {
|
|
|
1648
1686
|
if (config.manager.includes(session.userId)) {
|
|
1649
1687
|
if (!isNaN(num)) {
|
|
1650
1688
|
if (num < 1e4) {
|
|
1651
|
-
return await processList(session, config, void 0, num);
|
|
1689
|
+
return await processList(session, config, idManager, void 0, num);
|
|
1652
1690
|
} else {
|
|
1653
|
-
return await processList(session, config, num.toString());
|
|
1691
|
+
return await processList(session, config, idManager, num.toString());
|
|
1654
1692
|
}
|
|
1655
1693
|
} else if (input) {
|
|
1656
|
-
return await processList(session, config, input);
|
|
1694
|
+
return await processList(session, config, idManager, input);
|
|
1657
1695
|
}
|
|
1658
|
-
return await processList(session, config);
|
|
1696
|
+
return await processList(session, config, idManager);
|
|
1659
1697
|
} else {
|
|
1660
|
-
return await processList(session, config, session.userId);
|
|
1698
|
+
return await processList(session, config, idManager, session.userId);
|
|
1661
1699
|
}
|
|
1662
1700
|
}
|
|
1663
1701
|
if (options.g) {
|
|
1664
1702
|
return await processView(caveFilePath, resourceDir, session, options, content);
|
|
1665
1703
|
}
|
|
1666
1704
|
if (options.r) {
|
|
1667
|
-
return await processDelete(caveFilePath, resourceDir, pendingFilePath, session, config, options, content);
|
|
1705
|
+
return await processDelete(caveFilePath, resourceDir, pendingFilePath, session, config, options, content, idManager, contentHashManager);
|
|
1668
1706
|
}
|
|
1669
1707
|
if (options.a) {
|
|
1670
1708
|
return await processAdd(ctx, config, caveFilePath, resourceDir, pendingFilePath, session, content);
|
|
@@ -1676,11 +1714,11 @@ async function apply(ctx, config) {
|
|
|
1676
1714
|
return sendMessage(session, "commands.cave.message.managerOnly", [], true);
|
|
1677
1715
|
}
|
|
1678
1716
|
}).action(async ({ session }, id) => {
|
|
1679
|
-
const dataDir2 =
|
|
1680
|
-
const caveDir2 =
|
|
1681
|
-
const caveFilePath =
|
|
1682
|
-
const resourceDir =
|
|
1683
|
-
const pendingFilePath =
|
|
1717
|
+
const dataDir2 = path7.join(ctx.baseDir, "data");
|
|
1718
|
+
const caveDir2 = path7.join(dataDir2, "cave");
|
|
1719
|
+
const caveFilePath = path7.join(caveDir2, "cave.json");
|
|
1720
|
+
const resourceDir = path7.join(caveDir2, "resources");
|
|
1721
|
+
const pendingFilePath = path7.join(caveDir2, "pending.json");
|
|
1684
1722
|
const pendingData = await FileHandler.readJsonData(pendingFilePath);
|
|
1685
1723
|
return await auditManager.processAudit(pendingData, true, caveFilePath, resourceDir, pendingFilePath, session, id === "all" ? void 0 : parseInt(id));
|
|
1686
1724
|
});
|
|
@@ -1689,35 +1727,17 @@ async function apply(ctx, config) {
|
|
|
1689
1727
|
return sendMessage(session, "commands.cave.message.managerOnly", [], true);
|
|
1690
1728
|
}
|
|
1691
1729
|
}).action(async ({ session }, id) => {
|
|
1692
|
-
const dataDir2 =
|
|
1693
|
-
const caveDir2 =
|
|
1694
|
-
const caveFilePath =
|
|
1695
|
-
const resourceDir =
|
|
1696
|
-
const pendingFilePath =
|
|
1730
|
+
const dataDir2 = path7.join(ctx.baseDir, "data");
|
|
1731
|
+
const caveDir2 = path7.join(dataDir2, "cave");
|
|
1732
|
+
const caveFilePath = path7.join(caveDir2, "cave.json");
|
|
1733
|
+
const resourceDir = path7.join(caveDir2, "resources");
|
|
1734
|
+
const pendingFilePath = path7.join(caveDir2, "pending.json");
|
|
1697
1735
|
const pendingData = await FileHandler.readJsonData(pendingFilePath);
|
|
1698
1736
|
return await auditManager.processAudit(pendingData, false, caveFilePath, resourceDir, pendingFilePath, session, id === "all" ? void 0 : parseInt(id));
|
|
1699
1737
|
});
|
|
1700
1738
|
}
|
|
1701
1739
|
__name(apply, "apply");
|
|
1702
1740
|
var logger5 = new import_koishi6.Logger("cave");
|
|
1703
|
-
async function sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
|
|
1704
|
-
try {
|
|
1705
|
-
const msg = await session.send(session.text(key, params));
|
|
1706
|
-
if (isTemp && msg) {
|
|
1707
|
-
setTimeout(async () => {
|
|
1708
|
-
try {
|
|
1709
|
-
await session.bot.deleteMessage(session.channelId, msg);
|
|
1710
|
-
} catch (error) {
|
|
1711
|
-
logger5.debug(`Failed to delete temporary message: ${error.message}`);
|
|
1712
|
-
}
|
|
1713
|
-
}, timeout);
|
|
1714
|
-
}
|
|
1715
|
-
} catch (error) {
|
|
1716
|
-
logger5.error(`Failed to send message: ${error.message}`);
|
|
1717
|
-
}
|
|
1718
|
-
return "";
|
|
1719
|
-
}
|
|
1720
|
-
__name(sendMessage, "sendMessage");
|
|
1721
1741
|
function cleanElementsForSave(elements, keepIndex = false) {
|
|
1722
1742
|
if (!elements?.length) return [];
|
|
1723
1743
|
const cleanedElements = elements.map((element) => {
|
|
@@ -1742,47 +1762,6 @@ function cleanElementsForSave(elements, keepIndex = false) {
|
|
|
1742
1762
|
return keepIndex ? cleanedElements.sort((a, b) => (a.index || 0) - (b.index || 0)) : cleanedElements;
|
|
1743
1763
|
}
|
|
1744
1764
|
__name(cleanElementsForSave, "cleanElementsForSave");
|
|
1745
|
-
async function processMediaFile(filePath, type) {
|
|
1746
|
-
const data = await fs6.promises.readFile(filePath).catch(() => null);
|
|
1747
|
-
if (!data) return null;
|
|
1748
|
-
return `data:${type}/${type === "image" ? "png" : "mp4"};base64,${data.toString("base64")}`;
|
|
1749
|
-
}
|
|
1750
|
-
__name(processMediaFile, "processMediaFile");
|
|
1751
|
-
async function buildMessage2(cave, resourceDir, session) {
|
|
1752
|
-
if (!cave?.elements?.length) {
|
|
1753
|
-
return session.text("commands.cave.error.noContent");
|
|
1754
|
-
}
|
|
1755
|
-
const videoElement = cave.elements.find((el) => el.type === "video");
|
|
1756
|
-
const nonVideoElements = cave.elements.filter((el) => el.type !== "video").sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1757
|
-
if (videoElement?.file) {
|
|
1758
|
-
const basicInfo = [
|
|
1759
|
-
session.text("commands.cave.message.caveTitle", [cave.cave_id]),
|
|
1760
|
-
session.text("commands.cave.message.contributorSuffix", [cave.contributor_name])
|
|
1761
|
-
].join("\n");
|
|
1762
|
-
await session?.send(basicInfo);
|
|
1763
|
-
const filePath = path6.join(resourceDir, videoElement.file);
|
|
1764
|
-
const base64Data = await processMediaFile(filePath, "video");
|
|
1765
|
-
if (base64Data && session) {
|
|
1766
|
-
await session.send((0, import_koishi6.h)("video", { src: base64Data }));
|
|
1767
|
-
}
|
|
1768
|
-
return "";
|
|
1769
|
-
}
|
|
1770
|
-
const lines = [session.text("commands.cave.message.caveTitle", [cave.cave_id])];
|
|
1771
|
-
for (const element of nonVideoElements) {
|
|
1772
|
-
if (element.type === "text") {
|
|
1773
|
-
lines.push(element.content);
|
|
1774
|
-
} else if (element.type === "img" && element.file) {
|
|
1775
|
-
const filePath = path6.join(resourceDir, element.file);
|
|
1776
|
-
const base64Data = await processMediaFile(filePath, "image");
|
|
1777
|
-
if (base64Data) {
|
|
1778
|
-
lines.push((0, import_koishi6.h)("image", { src: base64Data }));
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
lines.push(session.text("commands.cave.message.contributorSuffix", [cave.contributor_name]));
|
|
1783
|
-
return lines.join("\n");
|
|
1784
|
-
}
|
|
1785
|
-
__name(buildMessage2, "buildMessage");
|
|
1786
1765
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1787
1766
|
0 && (module.exports = {
|
|
1788
1767
|
Config,
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface BaseElement {
|
|
2
|
+
type: 'text' | 'img' | 'video';
|
|
3
|
+
index: number;
|
|
4
|
+
}
|
|
5
|
+
export interface TextElement extends BaseElement {
|
|
6
|
+
type: 'text';
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MediaElement extends BaseElement {
|
|
10
|
+
type: 'img' | 'video';
|
|
11
|
+
file?: string;
|
|
12
|
+
fileName?: string;
|
|
13
|
+
fileSize?: string;
|
|
14
|
+
filePath?: string;
|
|
15
|
+
}
|
|
16
|
+
export type Element = TextElement | MediaElement;
|
|
17
|
+
export interface CaveObject {
|
|
18
|
+
cave_id: number;
|
|
19
|
+
elements: Element[];
|
|
20
|
+
contributor_number: string;
|
|
21
|
+
contributor_name: string;
|
|
22
|
+
}
|
|
23
|
+
export interface PendingCave extends CaveObject {
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config, PendingCave } from '..';
|
|
3
|
+
import { IdManager } from './IdManager';
|
|
4
|
+
export declare class AuditManager {
|
|
5
|
+
private ctx;
|
|
6
|
+
private config;
|
|
7
|
+
private idManager;
|
|
8
|
+
private logger;
|
|
9
|
+
constructor(ctx: Context, config: Config, idManager: IdManager);
|
|
10
|
+
processAudit(pendingData: PendingCave[], isApprove: boolean, caveFilePath: string, resourceDir: string, pendingFilePath: string, session: any, targetId?: number): Promise<string>;
|
|
11
|
+
private handleSingleAudit;
|
|
12
|
+
private handleBatchAudit;
|
|
13
|
+
sendAuditMessage(cave: PendingCave, content: string, session: any): Promise<void>;
|
|
14
|
+
private deleteMediaFiles;
|
|
15
|
+
private cleanElementsForSave;
|
|
16
|
+
private sendMessage;
|
|
17
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
/**
|
|
3
|
+
* 图片哈希计算工具类
|
|
4
|
+
* 使用 DCT(离散余弦变换)方法计算图片的感知哈希值,可用于图片相似度比较
|
|
5
|
+
*/
|
|
6
|
+
export declare class ContentHasher {
|
|
7
|
+
/**
|
|
8
|
+
* 计算图片的感知哈希值
|
|
9
|
+
* @param imageBuffer - 图片的二进制数据
|
|
10
|
+
* @returns 返回64位的十六进制哈希字符串
|
|
11
|
+
* @throws 当图片处理失败时可能抛出错误
|
|
12
|
+
*/
|
|
13
|
+
static calculateHash(imageBuffer: Buffer): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* 将二进制字符串转换为十六进制
|
|
16
|
+
* @param binary - 二进制字符串
|
|
17
|
+
* @returns 十六进制字符串
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
private static binaryToHex;
|
|
21
|
+
/**
|
|
22
|
+
* 将十六进制字符串转换为二进制
|
|
23
|
+
* @param hex - 十六进制字符串
|
|
24
|
+
* @returns 二进制字符串
|
|
25
|
+
* @private
|
|
26
|
+
*/
|
|
27
|
+
private static hexToBinary;
|
|
28
|
+
/**
|
|
29
|
+
* 计算图像的DCT(离散余弦变换)
|
|
30
|
+
* @param data - 图像数据
|
|
31
|
+
* @param size - 图像尺寸
|
|
32
|
+
* @returns DCT变换后的矩阵
|
|
33
|
+
* @private
|
|
34
|
+
*/
|
|
35
|
+
private static computeDCT;
|
|
36
|
+
/**
|
|
37
|
+
* 获取DCT系数
|
|
38
|
+
* @param index - 索引值
|
|
39
|
+
* @param size - 矩阵大小
|
|
40
|
+
* @returns DCT系数
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
private static getDCTCoefficient;
|
|
44
|
+
/**
|
|
45
|
+
* 计算数组的中位数
|
|
46
|
+
* @param arr - 输入数组
|
|
47
|
+
* @returns 中位数
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
private static calculateMedian;
|
|
51
|
+
/**
|
|
52
|
+
* 从DCT矩阵中提取特征值
|
|
53
|
+
* @param matrix - DCT矩阵
|
|
54
|
+
* @param size - 矩阵大小
|
|
55
|
+
* @returns 特征值数组
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
private static extractFeatures;
|
|
59
|
+
/**
|
|
60
|
+
* 计算两个哈希值之间的汉明距离
|
|
61
|
+
* @param hash1 - 第一个哈希值
|
|
62
|
+
* @param hash2 - 第二个哈希值
|
|
63
|
+
* @returns 汉明距离
|
|
64
|
+
* @throws 当两个哈希值长度不等时抛出错误
|
|
65
|
+
*/
|
|
66
|
+
static calculateDistance(hash1: string, hash2: string): number;
|
|
67
|
+
/**
|
|
68
|
+
* 计算两个图片哈希值的相似度
|
|
69
|
+
* @param hash1 - 第一个哈希值
|
|
70
|
+
* @param hash2 - 第二个哈希值
|
|
71
|
+
* @returns 返回0-1之间的相似度值,1表示完全相同,0表示完全不同
|
|
72
|
+
*/
|
|
73
|
+
static calculateSimilarity(hash1: string, hash2: string): number;
|
|
74
|
+
/**
|
|
75
|
+
* 计算文本的哈希值
|
|
76
|
+
* @param text - 输入文本
|
|
77
|
+
* @returns 文本的哈希值(36进制字符串)
|
|
78
|
+
*/
|
|
79
|
+
static calculateTextHash(text: string): string;
|
|
80
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export declare class FileHandler {
|
|
2
|
+
private static locks;
|
|
3
|
+
private static readonly RETRY_COUNT;
|
|
4
|
+
private static readonly RETRY_DELAY;
|
|
5
|
+
private static readonly CONCURRENCY_LIMIT;
|
|
6
|
+
/**
|
|
7
|
+
* 并发控制
|
|
8
|
+
* @param operation 要执行的操作
|
|
9
|
+
* @param limit 并发限制
|
|
10
|
+
* @returns 操作结果
|
|
11
|
+
*/
|
|
12
|
+
private static withConcurrencyLimit;
|
|
13
|
+
/**
|
|
14
|
+
* 文件操作包装器
|
|
15
|
+
* @param filePath 文件路径
|
|
16
|
+
* @param operation 要执行的操作
|
|
17
|
+
* @returns 操作结果
|
|
18
|
+
*/
|
|
19
|
+
private static withFileOp;
|
|
20
|
+
/**
|
|
21
|
+
* 事务处理
|
|
22
|
+
* @param operations 要执行的操作数组
|
|
23
|
+
* @returns 操作结果数组
|
|
24
|
+
*/
|
|
25
|
+
static withTransaction<T>(operations: Array<{
|
|
26
|
+
filePath: string;
|
|
27
|
+
operation: () => Promise<T>;
|
|
28
|
+
rollback?: () => Promise<void>;
|
|
29
|
+
}>): Promise<T[]>;
|
|
30
|
+
/**
|
|
31
|
+
* 读取 JSON 数据
|
|
32
|
+
* @param filePath 文件路径
|
|
33
|
+
* @returns JSON 数据
|
|
34
|
+
*/
|
|
35
|
+
static readJsonData<T>(filePath: string): Promise<T[]>;
|
|
36
|
+
/**
|
|
37
|
+
* 写入 JSON 数据
|
|
38
|
+
* @param filePath 文件路径
|
|
39
|
+
* @param data 要写入的数据
|
|
40
|
+
*/
|
|
41
|
+
static writeJsonData<T>(filePath: string, data: T[]): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* 确保目录存在
|
|
44
|
+
* @param dir 目录路径
|
|
45
|
+
*/
|
|
46
|
+
static ensureDirectory(dir: string): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* 确保 JSON 文件存在
|
|
49
|
+
* @param filePath 文件路径
|
|
50
|
+
*/
|
|
51
|
+
static ensureJsonFile(filePath: string): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* 保存媒体文件
|
|
54
|
+
* @param filePath 文件路径
|
|
55
|
+
* @param data 文件数据
|
|
56
|
+
*/
|
|
57
|
+
static saveMediaFile(filePath: string, data: Buffer | string): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* 删除媒体文件
|
|
60
|
+
* @param filePath 文件路径
|
|
61
|
+
*/
|
|
62
|
+
static deleteMediaFile(filePath: string): Promise<void>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 哈希存储状态类型
|
|
3
|
+
*/
|
|
4
|
+
interface HashStorageStatus {
|
|
5
|
+
lastUpdated: string;
|
|
6
|
+
entries: Array<{
|
|
7
|
+
caveId: number;
|
|
8
|
+
imageHashes: string[];
|
|
9
|
+
textHashes: string[];
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 图片哈希值存储管理类
|
|
14
|
+
* 负责管理和维护回声洞图片的哈希值
|
|
15
|
+
*/
|
|
16
|
+
export declare class HashManager {
|
|
17
|
+
private readonly caveDir;
|
|
18
|
+
private static readonly HASH_FILE;
|
|
19
|
+
private static readonly CAVE_FILE;
|
|
20
|
+
private static readonly BATCH_SIZE;
|
|
21
|
+
private imageHashes;
|
|
22
|
+
private textHashes;
|
|
23
|
+
private initialized;
|
|
24
|
+
/**
|
|
25
|
+
* 初始化HashManager实例
|
|
26
|
+
* @param caveDir 回声洞数据目录路径
|
|
27
|
+
*/
|
|
28
|
+
constructor(caveDir: string);
|
|
29
|
+
private get filePath();
|
|
30
|
+
private get resourceDir();
|
|
31
|
+
private get caveFilePath();
|
|
32
|
+
/**
|
|
33
|
+
* 初始化哈希存储
|
|
34
|
+
* 读取现有哈希数据或重新构建哈希值
|
|
35
|
+
* @throws 初始化失败时抛出错误
|
|
36
|
+
*/
|
|
37
|
+
initialize(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* 获取当前哈希存储状态
|
|
40
|
+
* @returns 包含最后更新时间和所有条目的状态对象
|
|
41
|
+
*/
|
|
42
|
+
getStatus(): Promise<HashStorageStatus>;
|
|
43
|
+
/**
|
|
44
|
+
* 更新指定回声洞的图片哈希值
|
|
45
|
+
* @param caveId 回声洞ID
|
|
46
|
+
* @param content 图片buffer数组
|
|
47
|
+
*/
|
|
48
|
+
updateCaveContent(caveId: number, content: {
|
|
49
|
+
images?: Buffer[];
|
|
50
|
+
texts?: string[];
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* 更新所有回声洞的哈希值
|
|
54
|
+
* @param isInitialBuild 是否为初始构建
|
|
55
|
+
*/
|
|
56
|
+
updateAllCaves(isInitialBuild?: boolean): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* 查找重复的图片
|
|
59
|
+
* @param content 待查找的图片buffer数组
|
|
60
|
+
* @param thresholds 相似度阈值
|
|
61
|
+
* @returns 匹配结果数组,包含索引、回声洞ID和相似度
|
|
62
|
+
*/
|
|
63
|
+
findDuplicates(content: {
|
|
64
|
+
images?: Buffer[];
|
|
65
|
+
texts?: string[];
|
|
66
|
+
}, thresholds: {
|
|
67
|
+
image: number;
|
|
68
|
+
text: number;
|
|
69
|
+
}): Promise<Array<{
|
|
70
|
+
type: 'image' | 'text';
|
|
71
|
+
index: number;
|
|
72
|
+
caveId: number;
|
|
73
|
+
similarity: number;
|
|
74
|
+
} | null>>;
|
|
75
|
+
private findTextDuplicates;
|
|
76
|
+
private calculateTextSimilarity;
|
|
77
|
+
private findImageDuplicates;
|
|
78
|
+
/**
|
|
79
|
+
* 加载回声洞数据
|
|
80
|
+
* @returns 回声洞数据数组
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
private loadCaveData;
|
|
84
|
+
/**
|
|
85
|
+
* 保存哈希数据到文件
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
private saveContentHashes;
|
|
89
|
+
/**
|
|
90
|
+
* 构建初始哈希数据
|
|
91
|
+
* @private
|
|
92
|
+
*/
|
|
93
|
+
private buildInitialHashes;
|
|
94
|
+
/**
|
|
95
|
+
* 更新缺失的哈希值
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
private updateMissingHashes;
|
|
99
|
+
/**
|
|
100
|
+
* 批量处理数组项
|
|
101
|
+
* @param items 待处理项数组
|
|
102
|
+
* @param processor 处理函数
|
|
103
|
+
* @param batchSize 批处理大小
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
private processBatch;
|
|
107
|
+
}
|
|
108
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ID管理器类
|
|
3
|
+
* 负责管理回声洞ID的分配、删除和统计信息
|
|
4
|
+
*/
|
|
5
|
+
export declare class IdManager {
|
|
6
|
+
private deletedIds;
|
|
7
|
+
private maxId;
|
|
8
|
+
private initialized;
|
|
9
|
+
private readonly statusFilePath;
|
|
10
|
+
private stats;
|
|
11
|
+
private usedIds;
|
|
12
|
+
/**
|
|
13
|
+
* 初始化ID管理器
|
|
14
|
+
* @param baseDir - 基础目录路径
|
|
15
|
+
*/
|
|
16
|
+
constructor(baseDir: string);
|
|
17
|
+
/**
|
|
18
|
+
* 初始化ID管理系统
|
|
19
|
+
* @param caveFilePath - 正式回声洞数据文件路径
|
|
20
|
+
* @param pendingFilePath - 待处理回声洞数据文件路径
|
|
21
|
+
* @throws 当初始化失败时抛出错误
|
|
22
|
+
*/
|
|
23
|
+
initialize(caveFilePath: string, pendingFilePath: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* 处理ID冲突
|
|
26
|
+
* @param conflicts - ID冲突映射表
|
|
27
|
+
* @param caveFilePath - 正式回声洞数据文件路径
|
|
28
|
+
* @param pendingFilePath - 待处理回声洞数据文件路径
|
|
29
|
+
* @param caveData - 正式回声洞数据
|
|
30
|
+
* @param pendingData - 待处理回声洞数据
|
|
31
|
+
* @private
|
|
32
|
+
*/
|
|
33
|
+
private handleConflicts;
|
|
34
|
+
/**
|
|
35
|
+
* 获取下一个可用的ID
|
|
36
|
+
* @returns 下一个可用的ID
|
|
37
|
+
* @throws 当ID管理器未初始化时抛出错误
|
|
38
|
+
*/
|
|
39
|
+
getNextId(): number;
|
|
40
|
+
/**
|
|
41
|
+
* 标记ID为已删除状态
|
|
42
|
+
* @param id - 要标记为删除的ID
|
|
43
|
+
* @throws 当ID管理器未初始化时抛出错误
|
|
44
|
+
*/
|
|
45
|
+
markDeleted(id: number): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* 添加贡献统计
|
|
48
|
+
* @param contributorNumber - 贡献者编号
|
|
49
|
+
* @param caveId - 回声洞ID
|
|
50
|
+
*/
|
|
51
|
+
addStat(contributorNumber: string, caveId: number): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* 移除贡献统计
|
|
54
|
+
* @param contributorNumber - 贡献者编号
|
|
55
|
+
* @param caveId - 回声洞ID
|
|
56
|
+
*/
|
|
57
|
+
removeStat(contributorNumber: string, caveId: number): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* 获取所有贡献统计信息
|
|
60
|
+
* @returns 贡献者编号到回声洞ID列表的映射
|
|
61
|
+
*/
|
|
62
|
+
getStats(): Record<string, number[]>;
|
|
63
|
+
/**
|
|
64
|
+
* 保存当前状态到文件
|
|
65
|
+
* @private
|
|
66
|
+
* @throws 当保存失败时抛出错误
|
|
67
|
+
*/
|
|
68
|
+
private saveStatus;
|
|
69
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Element, CaveObject } from '..';
|
|
3
|
+
export declare function buildMessage(cave: CaveObject, resourceDir: string, session?: any): Promise<string>;
|
|
4
|
+
export declare function sendMessage(session: any, key: string, params?: any[], isTemp?: boolean, timeout?: number): Promise<string>;
|
|
5
|
+
export declare function processMediaFile(filePath: string, type: 'image' | 'video'): Promise<string | null>;
|
|
6
|
+
export declare function extractMediaContent(originalContent: string, config: {
|
|
7
|
+
imageMaxSize: number;
|
|
8
|
+
videoMaxSize: number;
|
|
9
|
+
}, session: any): Promise<{
|
|
10
|
+
imageUrls: string[];
|
|
11
|
+
imageElements: Array<{
|
|
12
|
+
type: 'img';
|
|
13
|
+
index: number;
|
|
14
|
+
fileName?: string;
|
|
15
|
+
fileSize?: string;
|
|
16
|
+
}>;
|
|
17
|
+
videoUrls: string[];
|
|
18
|
+
videoElements: Array<{
|
|
19
|
+
type: 'video';
|
|
20
|
+
index: number;
|
|
21
|
+
fileName?: string;
|
|
22
|
+
fileSize?: string;
|
|
23
|
+
}>;
|
|
24
|
+
textParts: Element[];
|
|
25
|
+
}>;
|
|
26
|
+
export declare function saveMedia(urls: string[], fileNames: (string | undefined)[], resourceDir: string, caveId: number, mediaType: 'img' | 'video', config: {
|
|
27
|
+
enableImageDuplicate: boolean;
|
|
28
|
+
imageDuplicateThreshold: number;
|
|
29
|
+
textDuplicateThreshold: number;
|
|
30
|
+
}, ctx: Context, session: any, buffers?: Buffer[]): Promise<string[]>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Config } from '..';
|
|
2
|
+
import { IdManager } from './IdManager';
|
|
3
|
+
import { HashManager } from './HashManager';
|
|
4
|
+
export declare function processList(session: any, config: Config, idManager: IdManager, userId?: string, pageNum?: number): Promise<string>;
|
|
5
|
+
export declare function processView(caveFilePath: string, resourceDir: string, session: any, options: any, content: string[]): Promise<string>;
|
|
6
|
+
export declare function processRandom(caveFilePath: string, resourceDir: string, session: any): Promise<string | void>;
|
|
7
|
+
export declare function processDelete(caveFilePath: string, resourceDir: string, pendingFilePath: string, session: any, config: Config, options: any, content: string[], idManager: IdManager, HashManager: HashManager): Promise<string>;
|