koishi-plugin-best-cave 2.7.6 → 2.7.8
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/HashManager.d.ts +13 -21
- package/lib/Utils.d.ts +2 -2
- package/lib/index.d.ts +1 -1
- package/lib/index.js +272 -236
- package/package.json +2 -2
package/lib/HashManager.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { FileManager } from './FileManager';
|
|
|
7
7
|
export interface CaveHashObject {
|
|
8
8
|
cave: number;
|
|
9
9
|
hash: string;
|
|
10
|
-
type: '
|
|
10
|
+
type: 'text' | 'image';
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* @class HashManager
|
|
@@ -32,11 +32,6 @@ export declare class HashManager {
|
|
|
32
32
|
* @param cave - 主 `cave` 命令实例。
|
|
33
33
|
*/
|
|
34
34
|
registerCommands(cave: any): void;
|
|
35
|
-
/**
|
|
36
|
-
* @description 检查数据库中所有回声洞,为没有哈希记录的历史数据生成哈希。
|
|
37
|
-
* @returns 一个包含操作结果的报告字符串。
|
|
38
|
-
*/
|
|
39
|
-
generateHashesForHistoricalCaves(): Promise<string>;
|
|
40
35
|
/**
|
|
41
36
|
* @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
|
|
42
37
|
* @param cave - 回声洞对象。
|
|
@@ -44,27 +39,24 @@ export declare class HashManager {
|
|
|
44
39
|
*/
|
|
45
40
|
generateAllHashesForCave(cave: Pick<CaveObject, 'id' | 'elements'>): Promise<CaveHashObject[]>;
|
|
46
41
|
/**
|
|
47
|
-
* @description
|
|
48
|
-
* @param
|
|
49
|
-
* @returns
|
|
42
|
+
* @description 执行一维离散余弦变换 (DCT-II) 的方法。
|
|
43
|
+
* @param input - 输入的数字数组。
|
|
44
|
+
* @returns DCT 变换后的数组。
|
|
50
45
|
*/
|
|
51
|
-
|
|
52
|
-
textThreshold?: number;
|
|
53
|
-
imageThreshold?: number;
|
|
54
|
-
}): Promise<string>;
|
|
46
|
+
private dct1D;
|
|
55
47
|
/**
|
|
56
|
-
* @description 执行二维离散余弦变换 (DCT-II)
|
|
48
|
+
* @description 执行二维离散余弦变换 (DCT-II) 的方法。
|
|
49
|
+
* 通过对行和列分别应用一维 DCT 来实现。
|
|
57
50
|
* @param matrix - 输入的 N x N 像素亮度矩阵。
|
|
58
|
-
* @returns DCT变换后的 N x N 系数矩阵。
|
|
51
|
+
* @returns DCT 变换后的 N x N 系数矩阵。
|
|
59
52
|
*/
|
|
60
|
-
private
|
|
53
|
+
private dct2D;
|
|
61
54
|
/**
|
|
62
|
-
* @description pHash
|
|
63
|
-
* @param imageBuffer - 图片的Buffer。
|
|
64
|
-
* @
|
|
65
|
-
* @returns 十六进制pHash字符串。
|
|
55
|
+
* @description pHash 算法核心实现,使用 Jimp 和自定义 DCT。
|
|
56
|
+
* @param imageBuffer - 图片的 Buffer。
|
|
57
|
+
* @returns 64位十六进制 pHash 字符串。
|
|
66
58
|
*/
|
|
67
|
-
generatePHash(imageBuffer: Buffer
|
|
59
|
+
generatePHash(imageBuffer: Buffer): Promise<string>;
|
|
68
60
|
/**
|
|
69
61
|
* @description 计算两个十六进制哈希字符串之间的汉明距离 (不同位的数量)。
|
|
70
62
|
* @param hex1 - 第一个哈希。
|
package/lib/Utils.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare function buildCaveMessage(cave: CaveObject, config: Config, fileM
|
|
|
20
20
|
* @param logger 日志记录器实例。
|
|
21
21
|
* @param reusableIds 可复用 ID 的内存缓存。
|
|
22
22
|
*/
|
|
23
|
-
export declare function cleanupPendingDeletions(ctx: Context, fileManager: FileManager, logger: Logger, reusableIds: Set<number>): Promise<void>;
|
|
23
|
+
export declare function cleanupPendingDeletions(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, reusableIds: Set<number>): Promise<void>;
|
|
24
24
|
/**
|
|
25
25
|
* @description 根据配置和会话,生成数据库查询的范围条件。
|
|
26
26
|
* @param session 当前会话。
|
|
@@ -81,7 +81,7 @@ export declare function performSimilarityChecks(ctx: Context, config: Config, ha
|
|
|
81
81
|
export declare function handleFileUploads(ctx: Context, config: Config, fileManager: FileManager, logger: Logger, cave: CaveObject, downloadedMedia: {
|
|
82
82
|
fileName: string;
|
|
83
83
|
buffer: Buffer;
|
|
84
|
-
}[], reusableIds: Set<number>, session: Session): Promise<'pending' | 'active'
|
|
84
|
+
}[], reusableIds: Set<number>, session: Session): Promise<'pending' | 'active'>;
|
|
85
85
|
/**
|
|
86
86
|
* @description 校验会话是否来自指定的管理群组。
|
|
87
87
|
* @param session 当前会话。
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -37,7 +37,7 @@ __export(src_exports, {
|
|
|
37
37
|
usage: () => usage
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(src_exports);
|
|
40
|
-
var
|
|
40
|
+
var import_koishi3 = require("koishi");
|
|
41
41
|
|
|
42
42
|
// src/FileManager.ts
|
|
43
43
|
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
@@ -321,7 +321,7 @@ async function buildCaveMessage(cave, config, fileManager, logger2, platform, pr
|
|
|
321
321
|
return [finalInitialMessage, ...followUpMessages].filter((msg) => msg.length > 0);
|
|
322
322
|
}
|
|
323
323
|
__name(buildCaveMessage, "buildCaveMessage");
|
|
324
|
-
async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
|
|
324
|
+
async function cleanupPendingDeletions(ctx, config, fileManager, logger2, reusableIds) {
|
|
325
325
|
try {
|
|
326
326
|
const cavesToDelete = await ctx.database.get("cave", { status: "delete" });
|
|
327
327
|
if (!cavesToDelete.length) return;
|
|
@@ -331,7 +331,7 @@ async function cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds) {
|
|
|
331
331
|
idsToDelete.forEach((id) => reusableIds.add(id));
|
|
332
332
|
await ctx.database.remove("cave", { id: { $in: idsToDelete } });
|
|
333
333
|
await ctx.database.remove("cave_hash", { cave: { $in: idsToDelete } });
|
|
334
|
-
await ctx.database.remove("cave_meta", { cave: { $in: idsToDelete } });
|
|
334
|
+
if (config.enableAI) await ctx.database.remove("cave_meta", { cave: { $in: idsToDelete } });
|
|
335
335
|
} catch (error) {
|
|
336
336
|
logger2.error("清理回声洞时发生错误:", error);
|
|
337
337
|
}
|
|
@@ -457,25 +457,25 @@ async function performSimilarityChecks(ctx, config, hashManager, finalElementsFo
|
|
|
457
457
|
if (combinedText) {
|
|
458
458
|
const newSimhash = hashManager.generateTextSimhash(combinedText);
|
|
459
459
|
if (newSimhash) {
|
|
460
|
-
const existingTextHashes = await ctx.database.get("cave_hash", { type: "
|
|
460
|
+
const existingTextHashes = await ctx.database.get("cave_hash", { type: "text" });
|
|
461
461
|
for (const existing of existingTextHashes) {
|
|
462
462
|
const similarity = hashManager.calculateSimilarity(newSimhash, existing.hash);
|
|
463
463
|
if (similarity >= config.textThreshold) return { duplicate: true, message: `文本与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值` };
|
|
464
464
|
}
|
|
465
|
-
textHashesToStore.push({ hash: newSimhash, type: "
|
|
465
|
+
textHashesToStore.push({ hash: newSimhash, type: "text" });
|
|
466
466
|
}
|
|
467
467
|
}
|
|
468
468
|
if (downloadedMedia.length > 0) {
|
|
469
|
-
const allExistingImageHashes = await ctx.database.get("cave_hash", { type: "
|
|
469
|
+
const allExistingImageHashes = await ctx.database.get("cave_hash", { type: "image" });
|
|
470
470
|
for (const media of downloadedMedia) {
|
|
471
471
|
if ([".png", ".jpg", ".jpeg", ".webp"].includes(path2.extname(media.fileName).toLowerCase())) {
|
|
472
|
-
const imageHash = await hashManager.generatePHash(media.buffer
|
|
472
|
+
const imageHash = await hashManager.generatePHash(media.buffer);
|
|
473
473
|
for (const existing of allExistingImageHashes) {
|
|
474
474
|
const similarity = hashManager.calculateSimilarity(imageHash, existing.hash);
|
|
475
475
|
if (similarity >= config.imageThreshold) return { duplicate: true, message: `图片与回声洞(${existing.cave})的相似度(${similarity.toFixed(2)}%)超过阈值` };
|
|
476
476
|
}
|
|
477
|
-
imageHashesToStore.push({ hash: imageHash, type: "
|
|
478
|
-
allExistingImageHashes.push({ cave: 0, hash: imageHash, type: "
|
|
477
|
+
imageHashesToStore.push({ hash: imageHash, type: "image" });
|
|
478
|
+
allExistingImageHashes.push({ cave: 0, hash: imageHash, type: "image" });
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
481
|
}
|
|
@@ -492,8 +492,8 @@ async function handleFileUploads(ctx, config, fileManager, logger2, cave, downlo
|
|
|
492
492
|
} catch (fileProcessingError) {
|
|
493
493
|
logger2.error(`回声洞(${cave.id})文件处理失败:`, fileProcessingError);
|
|
494
494
|
await ctx.database.upsert("cave", [{ id: cave.id, status: "delete" }]);
|
|
495
|
-
cleanupPendingDeletions(ctx, fileManager, logger2, reusableIds);
|
|
496
|
-
|
|
495
|
+
cleanupPendingDeletions(ctx, config, fileManager, logger2, reusableIds);
|
|
496
|
+
throw fileProcessingError;
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
499
|
__name(handleFileUploads, "handleFileUploads");
|
|
@@ -655,7 +655,7 @@ ${pendingCaves.map((c) => c.id).join("|")}`;
|
|
|
655
655
|
if (cavesToProcess.length === 0) return `回声洞(${idsToProcess.join("|")})无需审核或不存在`;
|
|
656
656
|
const processedIds = cavesToProcess.map((cave2) => cave2.id);
|
|
657
657
|
await this.ctx.database.upsert("cave", processedIds.map((id) => ({ id, status: targetStatus })));
|
|
658
|
-
if (targetStatus === "delete") cleanupPendingDeletions(this.ctx, this.fileManager, this.logger, this.reusableIds);
|
|
658
|
+
if (targetStatus === "delete") cleanupPendingDeletions(this.ctx, this.config, this.fileManager, this.logger, this.reusableIds);
|
|
659
659
|
return `已${actionText}回声洞(${processedIds.join("|")})`;
|
|
660
660
|
} catch (error) {
|
|
661
661
|
this.logger.error(`审核操作失败:`, error);
|
|
@@ -686,7 +686,7 @@ ${pendingCaves.map((c) => c.id).join("|")}`;
|
|
|
686
686
|
};
|
|
687
687
|
|
|
688
688
|
// src/HashManager.ts
|
|
689
|
-
var
|
|
689
|
+
var import_jimp = __toESM(require("jimp"));
|
|
690
690
|
var crypto = __toESM(require("crypto"));
|
|
691
691
|
var HashManager = class {
|
|
692
692
|
/**
|
|
@@ -718,73 +718,167 @@ var HashManager = class {
|
|
|
718
718
|
* @param cave - 主 `cave` 命令实例。
|
|
719
719
|
*/
|
|
720
720
|
registerCommands(cave) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
724
|
-
}, "adminCheck");
|
|
725
|
-
cave.subcommand(".hash", "校验回声洞", { hidden: true, authority: 3 }).usage("校验缺失哈希的回声洞,补全哈希记录。").action(async (argv) => {
|
|
726
|
-
const checkResult = adminCheck(argv);
|
|
727
|
-
if (checkResult) return checkResult;
|
|
728
|
-
await argv.session.send("正在处理,请稍候...");
|
|
721
|
+
cave.subcommand(".hash", "校验回声洞", { hidden: true, authority: 3 }).usage("校验缺失哈希的回声洞,补全哈希记录。").action(async ({ session }) => {
|
|
722
|
+
if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
|
|
729
723
|
try {
|
|
730
|
-
|
|
724
|
+
const allCaves = await this.ctx.database.get("cave", { status: "active" });
|
|
725
|
+
if (allCaves.length === 0) return "无需补全回声洞哈希";
|
|
726
|
+
await session.send(`开始补全 ${allCaves.length} 个回声洞的哈希...`);
|
|
727
|
+
const existingHashes = await this.ctx.database.get("cave_hash", {});
|
|
728
|
+
const existingHashSet = new Set(existingHashes.map((h4) => `${h4.cave}-${h4.hash}-${h4.type}`));
|
|
729
|
+
let hashesToInsert = [];
|
|
730
|
+
let processedCaveCount = 0;
|
|
731
|
+
let totalHashesGenerated = 0;
|
|
732
|
+
let errorCount = 0;
|
|
733
|
+
const flushBatch = /* @__PURE__ */ __name(async () => {
|
|
734
|
+
if (hashesToInsert.length === 0) return;
|
|
735
|
+
await this.ctx.database.upsert("cave_hash", hashesToInsert);
|
|
736
|
+
totalHashesGenerated += hashesToInsert.length;
|
|
737
|
+
this.logger.info(`[${processedCaveCount}/${allCaves.length}] 正在导入 ${hashesToInsert.length} 条回声洞哈希...`);
|
|
738
|
+
hashesToInsert = [];
|
|
739
|
+
}, "flushBatch");
|
|
740
|
+
for (const cave2 of allCaves) {
|
|
741
|
+
processedCaveCount++;
|
|
742
|
+
try {
|
|
743
|
+
const newHashesForCave = await this.generateAllHashesForCave(cave2);
|
|
744
|
+
for (const hashObj of newHashesForCave) {
|
|
745
|
+
const uniqueKey = `${hashObj.cave}-${hashObj.hash}-${hashObj.type}`;
|
|
746
|
+
if (!existingHashSet.has(uniqueKey)) {
|
|
747
|
+
hashesToInsert.push(hashObj);
|
|
748
|
+
existingHashSet.add(uniqueKey);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (hashesToInsert.length >= 100) await flushBatch();
|
|
752
|
+
} catch (error) {
|
|
753
|
+
errorCount++;
|
|
754
|
+
this.logger.warn(`补全回声洞(${cave2.id})哈希时出错: ${error.message}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
await flushBatch();
|
|
758
|
+
return `已补全 ${allCaves.length} 个回声洞的 ${totalHashesGenerated} 条哈希(失败 ${errorCount} 条)`;
|
|
731
759
|
} catch (error) {
|
|
732
|
-
this.logger.error("
|
|
760
|
+
this.logger.error("生成哈希失败:", error);
|
|
733
761
|
return `操作失败: ${error.message}`;
|
|
734
762
|
}
|
|
735
763
|
});
|
|
736
|
-
cave.subcommand(".check", "检查相似度", { hidden: true }).usage("检查所有回声洞,找出相似度过高的内容。").option("textThreshold", "-t <threshold:number> 文本相似度阈值 (%)").option("imageThreshold", "-i <threshold:number> 图片相似度阈值 (%)").action(async (
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
await argv.session.send("正在检查,请稍候...");
|
|
764
|
+
cave.subcommand(".check", "检查相似度", { hidden: true }).usage("检查所有回声洞,找出相似度过高的内容。").option("textThreshold", "-t <threshold:number> 文本相似度阈值 (%)").option("imageThreshold", "-i <threshold:number> 图片相似度阈值 (%)").action(async ({ session, options }) => {
|
|
765
|
+
if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
|
|
766
|
+
await session.send("正在检查,请稍候...");
|
|
740
767
|
try {
|
|
741
|
-
|
|
768
|
+
const textThreshold = options.textThreshold ?? this.config.textThreshold;
|
|
769
|
+
const imageThreshold = options.imageThreshold ?? this.config.imageThreshold;
|
|
770
|
+
const allHashes = await this.ctx.database.get("cave_hash", {});
|
|
771
|
+
if (allHashes.length < 2) return "无可比较哈希";
|
|
772
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
773
|
+
const hashLookup = /* @__PURE__ */ new Map();
|
|
774
|
+
for (const hashObj of allHashes) {
|
|
775
|
+
if (!hashLookup.has(hashObj.cave)) hashLookup.set(hashObj.cave, {});
|
|
776
|
+
hashLookup.get(hashObj.cave)[hashObj.type] = hashObj.hash;
|
|
777
|
+
const binHash = BigInt("0x" + hashObj.hash).toString(2).padStart(64, "0");
|
|
778
|
+
for (let i = 0; i < 4; i++) {
|
|
779
|
+
const band = binHash.substring(i * 16, (i + 1) * 16);
|
|
780
|
+
const bucketKey = `${hashObj.type}:${i}:${band}`;
|
|
781
|
+
if (!buckets.has(bucketKey)) buckets.set(bucketKey, []);
|
|
782
|
+
buckets.get(bucketKey).push(hashObj.cave);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const candidatePairs = /* @__PURE__ */ new Set();
|
|
786
|
+
const similarPairs = { text: /* @__PURE__ */ new Set(), image: /* @__PURE__ */ new Set() };
|
|
787
|
+
for (const ids of buckets.values()) {
|
|
788
|
+
if (ids.length < 2) continue;
|
|
789
|
+
for (let i = 0; i < ids.length; i++) {
|
|
790
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
791
|
+
const id1 = ids[i];
|
|
792
|
+
const id2 = ids[j];
|
|
793
|
+
const pairKey = [id1, id2].sort((a, b) => a - b).join("-");
|
|
794
|
+
if (candidatePairs.has(pairKey)) continue;
|
|
795
|
+
candidatePairs.add(pairKey);
|
|
796
|
+
const cave1Hashes = hashLookup.get(id1);
|
|
797
|
+
const cave2Hashes = hashLookup.get(id2);
|
|
798
|
+
if (cave1Hashes?.text && cave2Hashes?.text) {
|
|
799
|
+
const similarity = this.calculateSimilarity(cave1Hashes.text, cave2Hashes.text);
|
|
800
|
+
if (similarity >= textThreshold) similarPairs.text.add(`${id1} & ${id2} = ${similarity.toFixed(2)}%`);
|
|
801
|
+
}
|
|
802
|
+
if (cave1Hashes?.image && cave2Hashes?.image) {
|
|
803
|
+
const similarity = this.calculateSimilarity(cave1Hashes.image, cave2Hashes.image);
|
|
804
|
+
if (similarity >= imageThreshold) similarPairs.image.add(`${id1} & ${id2} = ${similarity.toFixed(2)}%`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const totalFindings = similarPairs.text.size + similarPairs.image.size;
|
|
810
|
+
if (totalFindings === 0) return "未发现高相似度的内容";
|
|
811
|
+
let report = `已发现 ${totalFindings} 组高相似度的内容:`;
|
|
812
|
+
if (similarPairs.text.size > 0) report += "\n文本内容相似:\n" + [...similarPairs.text].join("\n");
|
|
813
|
+
if (similarPairs.image.size > 0) report += "\n图片内容相似:\n" + [...similarPairs.image].join("\n");
|
|
814
|
+
return report.trim();
|
|
742
815
|
} catch (error) {
|
|
743
816
|
this.logger.error("检查相似度失败:", error);
|
|
744
817
|
return `检查失败: ${error.message}`;
|
|
745
818
|
}
|
|
746
819
|
});
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
* @returns 一个包含操作结果的报告字符串。
|
|
751
|
-
*/
|
|
752
|
-
async generateHashesForHistoricalCaves() {
|
|
753
|
-
const allCaves = await this.ctx.database.get("cave", { status: "active" });
|
|
754
|
-
const existingHashes = await this.ctx.database.get("cave_hash", {});
|
|
755
|
-
const existingHashSet = new Set(existingHashes.map((h5) => `${h5.cave}-${h5.hash}-${h5.type}`));
|
|
756
|
-
if (allCaves.length === 0) return "无需补全回声洞哈希";
|
|
757
|
-
this.logger.info(`开始补全 ${allCaves.length} 个回声洞的哈希...`);
|
|
758
|
-
let hashesToInsert = [];
|
|
759
|
-
let processedCaveCount = 0;
|
|
760
|
-
let totalHashesGenerated = 0;
|
|
761
|
-
let errorCount = 0;
|
|
762
|
-
const flushBatch = /* @__PURE__ */ __name(async () => {
|
|
763
|
-
if (hashesToInsert.length === 0) return;
|
|
764
|
-
await this.ctx.database.upsert("cave_hash", hashesToInsert);
|
|
765
|
-
totalHashesGenerated += hashesToInsert.length;
|
|
766
|
-
this.logger.info(`[${processedCaveCount}/${allCaves.length}] 正在导入 ${hashesToInsert.length} 条回声洞哈希...`);
|
|
767
|
-
hashesToInsert = [];
|
|
768
|
-
}, "flushBatch");
|
|
769
|
-
for (const cave of allCaves) {
|
|
770
|
-
processedCaveCount++;
|
|
820
|
+
cave.subcommand(".fix [...ids:posint]", "修复回声洞", { hidden: true, authority: 3 }).usage("扫描并修复回声洞中的图片,可指定一个或多个 ID。").action(async ({ session }, ...ids) => {
|
|
821
|
+
if (requireAdmin(session, this.config)) return requireAdmin(session, this.config);
|
|
822
|
+
let cavesToProcess;
|
|
771
823
|
try {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
824
|
+
if (ids.length === 0) {
|
|
825
|
+
await session.send("正在修复,请稍候...");
|
|
826
|
+
cavesToProcess = await this.ctx.database.get("cave", { status: "active" });
|
|
827
|
+
} else {
|
|
828
|
+
cavesToProcess = await this.ctx.database.get("cave", { id: { $in: ids }, status: "active" });
|
|
829
|
+
}
|
|
830
|
+
if (!cavesToProcess.length) return "无可修复回声洞";
|
|
831
|
+
let fixedFiles = 0;
|
|
832
|
+
let errorCount = 0;
|
|
833
|
+
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
834
|
+
const JPEG_SIGNATURE = Buffer.from([255, 216]);
|
|
835
|
+
const GIF_SIGNATURE = Buffer.from("GIF");
|
|
836
|
+
for (const cave2 of cavesToProcess) {
|
|
837
|
+
const imageElements = cave2.elements.filter((el) => el.type === "image" && el.file);
|
|
838
|
+
for (const element of imageElements) {
|
|
839
|
+
try {
|
|
840
|
+
const originalBuffer = await this.fileManager.readFile(element.file);
|
|
841
|
+
let sanitizedBuffer = originalBuffer;
|
|
842
|
+
if (originalBuffer.slice(0, 8).equals(PNG_SIGNATURE)) {
|
|
843
|
+
const IEND_CHUNK = Buffer.from("IEND");
|
|
844
|
+
const iendIndex = originalBuffer.lastIndexOf(IEND_CHUNK);
|
|
845
|
+
if (iendIndex !== -1) {
|
|
846
|
+
const endOfPngData = iendIndex + 8;
|
|
847
|
+
if (originalBuffer.length > endOfPngData) sanitizedBuffer = originalBuffer.slice(0, endOfPngData);
|
|
848
|
+
}
|
|
849
|
+
} else if (originalBuffer.slice(0, 2).equals(JPEG_SIGNATURE)) {
|
|
850
|
+
const EOI_MARKER = Buffer.from([255, 217]);
|
|
851
|
+
const eoiIndex = originalBuffer.lastIndexOf(EOI_MARKER);
|
|
852
|
+
if (eoiIndex !== -1) {
|
|
853
|
+
const endOfJpegData = eoiIndex + 2;
|
|
854
|
+
if (originalBuffer.length > endOfJpegData) sanitizedBuffer = originalBuffer.slice(0, endOfJpegData);
|
|
855
|
+
}
|
|
856
|
+
} else if (originalBuffer.slice(0, 3).equals(GIF_SIGNATURE)) {
|
|
857
|
+
const GIF_TERMINATOR = Buffer.from([59]);
|
|
858
|
+
const terminatorIndex = originalBuffer.lastIndexOf(GIF_TERMINATOR);
|
|
859
|
+
if (terminatorIndex !== -1) {
|
|
860
|
+
const endOfGifData = terminatorIndex + 1;
|
|
861
|
+
if (originalBuffer.length > endOfGifData) sanitizedBuffer = originalBuffer.slice(0, endOfGifData);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (!originalBuffer.equals(sanitizedBuffer)) {
|
|
865
|
+
await this.fileManager.saveFile(element.file, sanitizedBuffer);
|
|
866
|
+
fixedFiles++;
|
|
867
|
+
}
|
|
868
|
+
} catch (error) {
|
|
869
|
+
if (error.code !== "ENOENT" && error.name !== "NoSuchKey") {
|
|
870
|
+
this.logger.warn(`无法修复回声洞(${cave2.id})的图片(${element.file}):`, error);
|
|
871
|
+
errorCount++;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
778
874
|
}
|
|
779
875
|
}
|
|
780
|
-
|
|
876
|
+
return `已修复 ${cavesToProcess.length} 个回声洞的 ${fixedFiles} 张图片(失败 ${errorCount} 条)`;
|
|
781
877
|
} catch (error) {
|
|
782
|
-
|
|
783
|
-
|
|
878
|
+
this.logger.error("修复图像文件时发生严重错误:", error);
|
|
879
|
+
return `操作失败: ${error.message}`;
|
|
784
880
|
}
|
|
785
|
-
}
|
|
786
|
-
await flushBatch();
|
|
787
|
-
return `已补全 ${allCaves.length} 个回声洞的 ${totalHashesGenerated} 条哈希(失败 ${errorCount} 条)`;
|
|
881
|
+
});
|
|
788
882
|
}
|
|
789
883
|
/**
|
|
790
884
|
* @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
|
|
@@ -804,115 +898,72 @@ var HashManager = class {
|
|
|
804
898
|
const combinedText = cave.elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join(" ");
|
|
805
899
|
if (combinedText) {
|
|
806
900
|
const textHash = this.generateTextSimhash(combinedText);
|
|
807
|
-
if (textHash) addUniqueHash({ cave: cave.id, hash: textHash, type: "
|
|
901
|
+
if (textHash) addUniqueHash({ cave: cave.id, hash: textHash, type: "text" });
|
|
808
902
|
}
|
|
809
903
|
for (const el of cave.elements.filter((el2) => el2.type === "image" && el2.file)) {
|
|
810
904
|
try {
|
|
811
905
|
const imageBuffer = await this.fileManager.readFile(el.file);
|
|
812
|
-
const imageHash = await this.generatePHash(imageBuffer
|
|
813
|
-
addUniqueHash({ cave: cave.id, hash: imageHash, type: "
|
|
814
|
-
} catch (
|
|
815
|
-
this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`,
|
|
906
|
+
const imageHash = await this.generatePHash(imageBuffer);
|
|
907
|
+
addUniqueHash({ cave: cave.id, hash: imageHash, type: "image" });
|
|
908
|
+
} catch (error) {
|
|
909
|
+
this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`, error);
|
|
910
|
+
throw error;
|
|
816
911
|
}
|
|
817
912
|
}
|
|
818
913
|
return tempHashes;
|
|
819
914
|
}
|
|
820
915
|
/**
|
|
821
|
-
* @description
|
|
822
|
-
* @param
|
|
823
|
-
* @returns
|
|
916
|
+
* @description 执行一维离散余弦变换 (DCT-II) 的方法。
|
|
917
|
+
* @param input - 输入的数字数组。
|
|
918
|
+
* @returns DCT 变换后的数组。
|
|
824
919
|
*/
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
textHashes.set(hash.cave, hash.hash);
|
|
835
|
-
} else if (hash.type === "phash") {
|
|
836
|
-
imageHashes.set(hash.cave, hash.hash);
|
|
837
|
-
}
|
|
920
|
+
dct1D(input) {
|
|
921
|
+
const N = input.length;
|
|
922
|
+
const output = new Array(N).fill(0);
|
|
923
|
+
const c0 = 1 / Math.sqrt(2);
|
|
924
|
+
for (let k = 0; k < N; k++) {
|
|
925
|
+
let sum = 0;
|
|
926
|
+
for (let n = 0; n < N; n++) sum += input[n] * Math.cos(Math.PI * (2 * n + 1) * k / (2 * N));
|
|
927
|
+
const ck = k === 0 ? c0 : 1;
|
|
928
|
+
output[k] = Math.sqrt(2 / N) * ck * sum;
|
|
838
929
|
}
|
|
839
|
-
|
|
840
|
-
text: /* @__PURE__ */ new Set(),
|
|
841
|
-
image: /* @__PURE__ */ new Set()
|
|
842
|
-
};
|
|
843
|
-
for (let i = 0; i < allCaveIds.length; i++) {
|
|
844
|
-
for (let j = i + 1; j < allCaveIds.length; j++) {
|
|
845
|
-
const id1 = allCaveIds[i];
|
|
846
|
-
const id2 = allCaveIds[j];
|
|
847
|
-
const pair = [id1, id2].sort((a, b) => a - b).join(" & ");
|
|
848
|
-
const text1 = textHashes.get(id1);
|
|
849
|
-
const text2 = textHashes.get(id2);
|
|
850
|
-
if (text1 && text2) {
|
|
851
|
-
const similarity = this.calculateSimilarity(text1, text2);
|
|
852
|
-
if (similarity >= textThreshold) similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
853
|
-
}
|
|
854
|
-
const image1 = imageHashes.get(id1);
|
|
855
|
-
const image2 = imageHashes.get(id2);
|
|
856
|
-
if (image1 && image2) {
|
|
857
|
-
const similarity = this.calculateSimilarity(image1, image2);
|
|
858
|
-
if (similarity >= imageThreshold) similarPairs.image.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
const totalFindings = similarPairs.text.size + similarPairs.image.size;
|
|
863
|
-
if (totalFindings === 0) return "未发现高相似度的内容";
|
|
864
|
-
let report = `已发现 ${totalFindings} 组高相似度的内容:`;
|
|
865
|
-
if (similarPairs.text.size > 0) report += "\n文本内容相似:\n" + [...similarPairs.text].join("\n");
|
|
866
|
-
if (similarPairs.image.size > 0) report += "\n图片内容相似:\n" + [...similarPairs.image].join("\n");
|
|
867
|
-
return report.trim();
|
|
930
|
+
return output;
|
|
868
931
|
}
|
|
869
932
|
/**
|
|
870
|
-
* @description 执行二维离散余弦变换 (DCT-II)
|
|
933
|
+
* @description 执行二维离散余弦变换 (DCT-II) 的方法。
|
|
934
|
+
* 通过对行和列分别应用一维 DCT 来实现。
|
|
871
935
|
* @param matrix - 输入的 N x N 像素亮度矩阵。
|
|
872
|
-
* @returns DCT变换后的 N x N 系数矩阵。
|
|
936
|
+
* @returns DCT 变换后的 N x N 系数矩阵。
|
|
873
937
|
*/
|
|
874
|
-
|
|
938
|
+
dct2D(matrix) {
|
|
875
939
|
const N = matrix.length;
|
|
876
940
|
if (N === 0) return [];
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
);
|
|
881
|
-
|
|
882
|
-
const output = new Array(N).fill(0);
|
|
883
|
-
const scale = Math.sqrt(2 / N);
|
|
884
|
-
for (let k = 0; k < N; k++) {
|
|
885
|
-
let sum = 0;
|
|
886
|
-
for (let n = 0; n < N; n++) sum += input[n] * cosines[n][k];
|
|
887
|
-
output[k] = scale * sum;
|
|
888
|
-
}
|
|
889
|
-
output[0] /= Math.sqrt(2);
|
|
890
|
-
return output;
|
|
891
|
-
}, "applyDct1D");
|
|
892
|
-
const tempMatrix = matrix.map((row) => applyDct1D(row));
|
|
893
|
-
const transposed = tempMatrix[0].map((_, col) => tempMatrix.map((row) => row[col]));
|
|
894
|
-
const dctResult = transposed.map((row) => applyDct1D(row));
|
|
895
|
-
return dctResult[0].map((_, col) => dctResult.map((row) => row[col]));
|
|
941
|
+
const tempMatrix = matrix.map((row) => this.dct1D(row));
|
|
942
|
+
const transposed = tempMatrix.map((_, colIndex) => tempMatrix.map((row) => row[colIndex]));
|
|
943
|
+
const dctResultTransposed = transposed.map((row) => this.dct1D(row));
|
|
944
|
+
const dctResult = dctResultTransposed.map((_, colIndex) => dctResultTransposed.map((row) => row[colIndex]));
|
|
945
|
+
return dctResult;
|
|
896
946
|
}
|
|
897
947
|
/**
|
|
898
|
-
* @description pHash
|
|
899
|
-
* @param imageBuffer - 图片的Buffer。
|
|
900
|
-
* @
|
|
901
|
-
* @returns 十六进制pHash字符串。
|
|
948
|
+
* @description pHash 算法核心实现,使用 Jimp 和自定义 DCT。
|
|
949
|
+
* @param imageBuffer - 图片的 Buffer。
|
|
950
|
+
* @returns 64位十六进制 pHash 字符串。
|
|
902
951
|
*/
|
|
903
|
-
async generatePHash(imageBuffer
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const dctMatrix = this.
|
|
952
|
+
async generatePHash(imageBuffer) {
|
|
953
|
+
const image = await import_jimp.default.read(imageBuffer);
|
|
954
|
+
image.resize(32, 32, import_jimp.default.RESIZE_BILINEAR).greyscale();
|
|
955
|
+
const matrix = Array.from({ length: 32 }, () => new Array(32).fill(0));
|
|
956
|
+
image.scan(0, 0, 32, 32, (x, y, idx) => {
|
|
957
|
+
matrix[y][x] = image.bitmap.data[idx];
|
|
958
|
+
});
|
|
959
|
+
const dctMatrix = this.dct2D(matrix);
|
|
911
960
|
const coefficients = [];
|
|
912
|
-
for (let y = 0; y <
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
961
|
+
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) coefficients.push(dctMatrix[y][x]);
|
|
962
|
+
const acCoefficients = coefficients.slice(1);
|
|
963
|
+
const average = acCoefficients.reduce((sum, val) => sum + val, 0) / acCoefficients.length;
|
|
964
|
+
let binaryHash = "";
|
|
965
|
+
for (const val of coefficients) binaryHash += val > average ? "1" : "0";
|
|
966
|
+
return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
|
|
916
967
|
}
|
|
917
968
|
/**
|
|
918
969
|
* @description 计算两个十六进制哈希字符串之间的汉明距离 (不同位的数量)。
|
|
@@ -922,8 +973,10 @@ var HashManager = class {
|
|
|
922
973
|
*/
|
|
923
974
|
calculateHammingDistance(hex1, hex2) {
|
|
924
975
|
let distance = 0;
|
|
925
|
-
|
|
926
|
-
const
|
|
976
|
+
let bin1 = "";
|
|
977
|
+
for (const char of hex1) bin1 += parseInt(char, 16).toString(2).padStart(4, "0");
|
|
978
|
+
let bin2 = "";
|
|
979
|
+
for (const char of hex2) bin2 += parseInt(char, 16).toString(2).padStart(4, "0");
|
|
927
980
|
const len = Math.min(bin1.length, bin2.length);
|
|
928
981
|
for (let i = 0; i < len; i++) if (bin1[i] !== bin2[i]) distance++;
|
|
929
982
|
return distance;
|
|
@@ -947,14 +1000,8 @@ var HashManager = class {
|
|
|
947
1000
|
generateTextSimhash(text) {
|
|
948
1001
|
const cleanText = (text || "").toLowerCase().replace(/\s+/g, "");
|
|
949
1002
|
if (!cleanText) return "";
|
|
950
|
-
const
|
|
951
|
-
const
|
|
952
|
-
if (cleanText.length < n) {
|
|
953
|
-
tokens.add(cleanText);
|
|
954
|
-
} else {
|
|
955
|
-
for (let i = 0; i <= cleanText.length - n; i++) tokens.add(cleanText.substring(i, i + n));
|
|
956
|
-
}
|
|
957
|
-
const tokenArray = Array.from(tokens);
|
|
1003
|
+
const tokens = Array.from(cleanText);
|
|
1004
|
+
const tokenArray = Array.from(new Set(tokens));
|
|
958
1005
|
if (tokenArray.length === 0) return "";
|
|
959
1006
|
const vector = new Array(64).fill(0);
|
|
960
1007
|
tokenArray.forEach((token) => {
|
|
@@ -965,15 +1012,8 @@ var HashManager = class {
|
|
|
965
1012
|
return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
|
|
966
1013
|
}
|
|
967
1014
|
};
|
|
968
|
-
function hexToBinary(hex) {
|
|
969
|
-
let bin = "";
|
|
970
|
-
for (const char of hex) bin += parseInt(char, 16).toString(2).padStart(4, "0");
|
|
971
|
-
return bin;
|
|
972
|
-
}
|
|
973
|
-
__name(hexToBinary, "hexToBinary");
|
|
974
1015
|
|
|
975
1016
|
// src/AIManager.ts
|
|
976
|
-
var import_koishi3 = require("koishi");
|
|
977
1017
|
var path3 = __toESM(require("path"));
|
|
978
1018
|
var AIManager = class {
|
|
979
1019
|
/**
|
|
@@ -1015,40 +1055,21 @@ var AIManager = class {
|
|
|
1015
1055
|
if (cavesToAnalyze.length === 0) return "无需分析回声洞";
|
|
1016
1056
|
await session.send(`开始分析 ${cavesToAnalyze.length} 个回声洞...`);
|
|
1017
1057
|
let successCount = 0;
|
|
1018
|
-
let failedCount = 0;
|
|
1019
1058
|
for (const [index, cave2] of cavesToAnalyze.entries()) {
|
|
1020
|
-
this.logger.info(`[${index + 1}/${cavesToAnalyze.length}] 正在分析回声洞 (${cave2.id})...`);
|
|
1021
1059
|
try {
|
|
1060
|
+
this.logger.info(`[${index + 1}/${cavesToAnalyze.length}] 正在分析回声洞 (${cave2.id})...`);
|
|
1022
1061
|
await this.analyzeAndStore(cave2);
|
|
1023
1062
|
successCount++;
|
|
1024
1063
|
} catch (error) {
|
|
1025
|
-
|
|
1026
|
-
this.logger.error(`分析回声洞(${cave2.id})时出错:`, error);
|
|
1064
|
+
return `分析回声洞(${cave2.id})时出错: ${error.message}`;
|
|
1027
1065
|
}
|
|
1028
1066
|
}
|
|
1029
|
-
return `已分析 ${successCount}
|
|
1067
|
+
return `已分析 ${successCount} 个回声洞`;
|
|
1030
1068
|
} catch (error) {
|
|
1031
1069
|
this.logger.error("分析回声洞失败:", error);
|
|
1032
1070
|
return `操作失败: ${error.message}`;
|
|
1033
1071
|
}
|
|
1034
1072
|
});
|
|
1035
|
-
cave.subcommand(".desc <id:posint>", "查询回声洞").action(async ({}, id) => {
|
|
1036
|
-
if (!id) return "请输入要查看的回声洞序号";
|
|
1037
|
-
try {
|
|
1038
|
-
const [meta] = await this.ctx.database.get("cave_meta", { cave: id });
|
|
1039
|
-
if (!meta) return `回声洞(${id})尚未分析`;
|
|
1040
|
-
const report = [
|
|
1041
|
-
`回声洞(${id})分析结果:`,
|
|
1042
|
-
`描述:${meta.description}`,
|
|
1043
|
-
`关键词:${meta.keywords.join(", ")}`,
|
|
1044
|
-
`综合评分:${meta.rating}/100`
|
|
1045
|
-
];
|
|
1046
|
-
return import_koishi3.h.text(report.join("\n"));
|
|
1047
|
-
} catch (error) {
|
|
1048
|
-
this.logger.error(`查询回声洞(${id})失败:`, error);
|
|
1049
|
-
return "查询失败,请稍后再试";
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
1052
1073
|
}
|
|
1053
1074
|
/**
|
|
1054
1075
|
* @description 对新提交的内容执行 AI 驱动的查重检查。
|
|
@@ -1170,17 +1191,26 @@ var AIManager = class {
|
|
|
1170
1191
|
this.rateLimitResetTime = now + 6e4;
|
|
1171
1192
|
this.requestCount = 0;
|
|
1172
1193
|
}
|
|
1173
|
-
if (this.requestCount >= this.config.
|
|
1194
|
+
if (this.requestCount >= this.config.aiRPM) {
|
|
1174
1195
|
const delay = this.rateLimitResetTime - now;
|
|
1175
1196
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1176
1197
|
this.rateLimitResetTime = Date.now() + 6e4;
|
|
1177
1198
|
this.requestCount = 0;
|
|
1178
1199
|
}
|
|
1179
1200
|
let schema = JSON.parse(schemaString);
|
|
1201
|
+
const toolName = "extract_data";
|
|
1180
1202
|
const payload = {
|
|
1181
1203
|
model: this.config.aiModel,
|
|
1182
1204
|
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
|
1183
|
-
|
|
1205
|
+
tools: [{
|
|
1206
|
+
type: "function",
|
|
1207
|
+
function: {
|
|
1208
|
+
name: toolName,
|
|
1209
|
+
description: "根据提供的内容提取或分析信息。",
|
|
1210
|
+
parameters: schema
|
|
1211
|
+
}
|
|
1212
|
+
}],
|
|
1213
|
+
tool_choice: { type: "function", function: { name: toolName } }
|
|
1184
1214
|
};
|
|
1185
1215
|
const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/chat/completions`;
|
|
1186
1216
|
const headers = {
|
|
@@ -1190,7 +1220,13 @@ var AIManager = class {
|
|
|
1190
1220
|
try {
|
|
1191
1221
|
this.requestCount++;
|
|
1192
1222
|
const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
|
|
1193
|
-
|
|
1223
|
+
const toolCall = response.choices?.[0]?.message?.tool_calls?.[0];
|
|
1224
|
+
if (toolCall?.function?.arguments) {
|
|
1225
|
+
return JSON.parse(toolCall.function.arguments);
|
|
1226
|
+
} else {
|
|
1227
|
+
this.logger.error("AI 响应格式不正确:", JSON.stringify(response));
|
|
1228
|
+
throw new Error("AI 响应格式不正确");
|
|
1229
|
+
}
|
|
1194
1230
|
} catch (error) {
|
|
1195
1231
|
const errorMessage = error.response ? JSON.stringify(error.response.data) : error.message;
|
|
1196
1232
|
this.logger.error(`请求 API 失败: ${errorMessage}`);
|
|
@@ -1214,29 +1250,29 @@ var usage = `
|
|
|
1214
1250
|
<p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p>
|
|
1215
1251
|
</div>
|
|
1216
1252
|
`;
|
|
1217
|
-
var logger = new
|
|
1218
|
-
var Config =
|
|
1219
|
-
|
|
1220
|
-
perChannel:
|
|
1221
|
-
enableName:
|
|
1222
|
-
enableIO:
|
|
1223
|
-
adminChannel:
|
|
1224
|
-
caveFormat:
|
|
1253
|
+
var logger = new import_koishi3.Logger("best-cave");
|
|
1254
|
+
var Config = import_koishi3.Schema.intersect([
|
|
1255
|
+
import_koishi3.Schema.object({
|
|
1256
|
+
perChannel: import_koishi3.Schema.boolean().default(false).description("启用分群模式"),
|
|
1257
|
+
enableName: import_koishi3.Schema.boolean().default(false).description("启用自定义昵称"),
|
|
1258
|
+
enableIO: import_koishi3.Schema.boolean().default(false).description("启用导入导出"),
|
|
1259
|
+
adminChannel: import_koishi3.Schema.string().default("onebot:").description("管理群组 ID"),
|
|
1260
|
+
caveFormat: import_koishi3.Schema.string().default("回声洞 ——({id})|—— {name}").description("自定义文本(参见 README)")
|
|
1225
1261
|
}).description("基础配置"),
|
|
1226
|
-
|
|
1227
|
-
enablePend:
|
|
1228
|
-
enableSimilarity:
|
|
1229
|
-
textThreshold:
|
|
1230
|
-
imageThreshold:
|
|
1262
|
+
import_koishi3.Schema.object({
|
|
1263
|
+
enablePend: import_koishi3.Schema.boolean().default(false).description("启用审核"),
|
|
1264
|
+
enableSimilarity: import_koishi3.Schema.boolean().default(false).description("启用查重"),
|
|
1265
|
+
textThreshold: import_koishi3.Schema.number().min(0).max(100).step(0.01).default(90).description("文本相似度阈值 (%)"),
|
|
1266
|
+
imageThreshold: import_koishi3.Schema.number().min(0).max(100).step(0.01).default(90).description("图片相似度阈值 (%)")
|
|
1231
1267
|
}).description("复核配置"),
|
|
1232
|
-
|
|
1233
|
-
enableAI:
|
|
1234
|
-
aiEndpoint:
|
|
1235
|
-
aiApiKey:
|
|
1236
|
-
aiModel:
|
|
1237
|
-
|
|
1238
|
-
AnalysePrompt:
|
|
1239
|
-
aiAnalyseSchema:
|
|
1268
|
+
import_koishi3.Schema.object({
|
|
1269
|
+
enableAI: import_koishi3.Schema.boolean().default(false).description("启用 AI"),
|
|
1270
|
+
aiEndpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
|
|
1271
|
+
aiApiKey: import_koishi3.Schema.string().description("密钥 (Key)").role("secret"),
|
|
1272
|
+
aiModel: import_koishi3.Schema.string().description("模型 (Model)").default("gemini-2.5-flash"),
|
|
1273
|
+
aiRPM: import_koishi3.Schema.number().description("每分钟请求数 (RPM)").default(60),
|
|
1274
|
+
AnalysePrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析 Prompt"),
|
|
1275
|
+
aiAnalyseSchema: import_koishi3.Schema.string().role("textarea").default(
|
|
1240
1276
|
`{
|
|
1241
1277
|
"type": "object",
|
|
1242
1278
|
"properties": {
|
|
@@ -1259,8 +1295,8 @@ var Config = import_koishi4.Schema.intersect([
|
|
|
1259
1295
|
"required": ["keywords", "description", "rating"]
|
|
1260
1296
|
}`
|
|
1261
1297
|
).description("分析 JSON Schema"),
|
|
1262
|
-
aiCheckPrompt:
|
|
1263
|
-
aiCheckSchema:
|
|
1298
|
+
aiCheckPrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重 Prompt"),
|
|
1299
|
+
aiCheckSchema: import_koishi3.Schema.string().role("textarea").default(
|
|
1264
1300
|
`{
|
|
1265
1301
|
"type": "object",
|
|
1266
1302
|
"properties": {
|
|
@@ -1277,15 +1313,15 @@ var Config = import_koishi4.Schema.intersect([
|
|
|
1277
1313
|
}`
|
|
1278
1314
|
).description("查重 JSON Schema")
|
|
1279
1315
|
}).description("模型配置"),
|
|
1280
|
-
|
|
1281
|
-
localPath:
|
|
1282
|
-
enableS3:
|
|
1283
|
-
publicUrl:
|
|
1284
|
-
endpoint:
|
|
1285
|
-
bucket:
|
|
1286
|
-
region:
|
|
1287
|
-
accessKeyId:
|
|
1288
|
-
secretAccessKey:
|
|
1316
|
+
import_koishi3.Schema.object({
|
|
1317
|
+
localPath: import_koishi3.Schema.string().description("文件映射路径"),
|
|
1318
|
+
enableS3: import_koishi3.Schema.boolean().default(false).description("启用 S3 存储"),
|
|
1319
|
+
publicUrl: import_koishi3.Schema.string().description("公共访问 URL").role("link"),
|
|
1320
|
+
endpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link"),
|
|
1321
|
+
bucket: import_koishi3.Schema.string().description("存储桶 (Bucket)"),
|
|
1322
|
+
region: import_koishi3.Schema.string().default("auto").description("区域 (Region)"),
|
|
1323
|
+
accessKeyId: import_koishi3.Schema.string().description("Access Key ID").role("secret"),
|
|
1324
|
+
secretAccessKey: import_koishi3.Schema.string().description("Secret Access Key").role("secret")
|
|
1289
1325
|
}).description("存储配置")
|
|
1290
1326
|
]);
|
|
1291
1327
|
function apply(ctx, config) {
|
|
@@ -1314,7 +1350,7 @@ function apply(ctx, config) {
|
|
|
1314
1350
|
if (staleCaves.length > 0) {
|
|
1315
1351
|
const idsToMark = staleCaves.map((c) => ({ id: c.id, status: "delete" }));
|
|
1316
1352
|
await ctx.database.upsert("cave", idsToMark);
|
|
1317
|
-
await cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
|
|
1353
|
+
await cleanupPendingDeletions(ctx, config, fileManager, logger, reusableIds);
|
|
1318
1354
|
}
|
|
1319
1355
|
} catch (error) {
|
|
1320
1356
|
logger.error("清理残留回声洞时发生错误:", error);
|
|
@@ -1332,7 +1368,7 @@ function apply(ctx, config) {
|
|
|
1332
1368
|
const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
|
|
1333
1369
|
const [randomCave] = await ctx.database.get("cave", { ...query, id: randomId });
|
|
1334
1370
|
const messages = await buildCaveMessage(randomCave, config, fileManager, logger, session.platform);
|
|
1335
|
-
for (const message of messages) if (message.length > 0) await session.send(
|
|
1371
|
+
for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1336
1372
|
} catch (error) {
|
|
1337
1373
|
logger.error("随机获取回声洞失败:", error);
|
|
1338
1374
|
return "随机获取回声洞失败";
|
|
@@ -1344,12 +1380,12 @@ function apply(ctx, config) {
|
|
|
1344
1380
|
if (session.quote?.elements) {
|
|
1345
1381
|
sourceElements = session.quote.elements;
|
|
1346
1382
|
} else if (content?.trim()) {
|
|
1347
|
-
sourceElements =
|
|
1383
|
+
sourceElements = import_koishi3.h.parse(content);
|
|
1348
1384
|
} else {
|
|
1349
1385
|
await session.send("请在一分钟内发送你要添加的内容");
|
|
1350
1386
|
const reply = await session.prompt(6e4);
|
|
1351
1387
|
if (!reply) return "等待操作超时";
|
|
1352
|
-
sourceElements =
|
|
1388
|
+
sourceElements = import_koishi3.h.parse(reply);
|
|
1353
1389
|
}
|
|
1354
1390
|
const newId = await getNextCaveId(ctx, reusableIds);
|
|
1355
1391
|
const creationTime = /* @__PURE__ */ new Date();
|
|
@@ -1388,11 +1424,11 @@ function apply(ctx, config) {
|
|
|
1388
1424
|
time: creationTime
|
|
1389
1425
|
});
|
|
1390
1426
|
if (hasMedia) finalStatus = await handleFileUploads(ctx, config, fileManager, logger, newCave, downloadedMedia, reusableIds, session);
|
|
1391
|
-
if (finalStatus !== "preload"
|
|
1427
|
+
if (finalStatus !== "preload") {
|
|
1392
1428
|
newCave.status = finalStatus;
|
|
1393
1429
|
if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
|
|
1394
1430
|
if (hashManager) {
|
|
1395
|
-
const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((
|
|
1431
|
+
const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h4) => ({ ...h4, cave: newCave.id }));
|
|
1396
1432
|
if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
|
|
1397
1433
|
}
|
|
1398
1434
|
if (finalStatus === "pending" && reviewManager) reviewManager.sendForPend(newCave);
|
|
@@ -1409,7 +1445,7 @@ function apply(ctx, config) {
|
|
|
1409
1445
|
const [targetCave] = await ctx.database.get("cave", { ...getScopeQuery(session, config), id });
|
|
1410
1446
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
1411
1447
|
const messages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform);
|
|
1412
|
-
for (const message of messages) if (message.length > 0) await session.send(
|
|
1448
|
+
for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1413
1449
|
} catch (error) {
|
|
1414
1450
|
logger.error(`查看回声洞(${id})失败:`, error);
|
|
1415
1451
|
return "查看失败,请稍后再试";
|
|
@@ -1425,8 +1461,8 @@ function apply(ctx, config) {
|
|
|
1425
1461
|
if (!isAuthor && !isAdmin) return "你没有权限删除这条回声洞";
|
|
1426
1462
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
1427
1463
|
const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
|
|
1428
|
-
|
|
1429
|
-
|
|
1464
|
+
for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1465
|
+
cleanupPendingDeletions(ctx, config, fileManager, logger, reusableIds);
|
|
1430
1466
|
} catch (error) {
|
|
1431
1467
|
logger.error(`标记回声洞(${id})失败:`, error);
|
|
1432
1468
|
return "删除失败,请稍后再试";
|
|
@@ -1437,7 +1473,7 @@ function apply(ctx, config) {
|
|
|
1437
1473
|
const adminChannelId = config.adminChannel?.split(":")[1];
|
|
1438
1474
|
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
1439
1475
|
try {
|
|
1440
|
-
const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) =>
|
|
1476
|
+
const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) => import_koishi3.$.count(row.id), "count") }).execute();
|
|
1441
1477
|
if (!aggregatedStats.length) return "目前没有回声洞投稿";
|
|
1442
1478
|
const userStats = /* @__PURE__ */ new Map();
|
|
1443
1479
|
for (const stat of aggregatedStats) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-best-cave",
|
|
3
3
|
"description": "功能强大、高度可定制的回声洞。支持丰富的媒体类型、内容查重、人工审核、用户昵称、数据迁移以及本地/S3 双重文件存储后端。",
|
|
4
|
-
"version": "2.7.
|
|
4
|
+
"version": "2.7.8",
|
|
5
5
|
"contributors": [
|
|
6
6
|
"Yis_Rime <yis_rime@outlook.com>"
|
|
7
7
|
],
|
|
@@ -29,6 +29,6 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@aws-sdk/client-s3": "^3.800.0",
|
|
32
|
-
"
|
|
32
|
+
"jimp": "^0.22.9"
|
|
33
33
|
}
|
|
34
34
|
}
|