koishi-plugin-best-cave 2.7.5 → 2.7.7
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/AIManager.d.ts +2 -0
- package/lib/HashManager.d.ts +13 -21
- package/lib/index.d.ts +1 -0
- package/lib/index.js +215 -238
- package/package.json +2 -2
package/lib/AIManager.d.ts
CHANGED
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/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");
|
|
@@ -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
|
}
|
|
@@ -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,74 +718,98 @@ 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
|
+
const allCaveIds = [...new Set(allHashes.map((h4) => h4.cave))];
|
|
772
|
+
const textHashes = /* @__PURE__ */ new Map();
|
|
773
|
+
const imageHashes = /* @__PURE__ */ new Map();
|
|
774
|
+
for (const hash of allHashes) {
|
|
775
|
+
if (hash.type === "text") {
|
|
776
|
+
textHashes.set(hash.cave, hash.hash);
|
|
777
|
+
} else if (hash.type === "image") {
|
|
778
|
+
imageHashes.set(hash.cave, hash.hash);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const similarPairs = { text: /* @__PURE__ */ new Set(), image: /* @__PURE__ */ new Set() };
|
|
782
|
+
for (let i = 0; i < allCaveIds.length; i++) {
|
|
783
|
+
for (let j = i + 1; j < allCaveIds.length; j++) {
|
|
784
|
+
const id1 = allCaveIds[i];
|
|
785
|
+
const id2 = allCaveIds[j];
|
|
786
|
+
const pair = [id1, id2].sort((a, b) => a - b).join(" & ");
|
|
787
|
+
const text1 = textHashes.get(id1);
|
|
788
|
+
const text2 = textHashes.get(id2);
|
|
789
|
+
if (text1 && text2) {
|
|
790
|
+
const similarity = this.calculateSimilarity(text1, text2);
|
|
791
|
+
if (similarity >= textThreshold) similarPairs.text.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
792
|
+
}
|
|
793
|
+
const image1 = imageHashes.get(id1);
|
|
794
|
+
const image2 = imageHashes.get(id2);
|
|
795
|
+
if (image1 && image2) {
|
|
796
|
+
const similarity = this.calculateSimilarity(image1, image2);
|
|
797
|
+
if (similarity >= imageThreshold) similarPairs.image.add(`${pair} = ${similarity.toFixed(2)}%`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const totalFindings = similarPairs.text.size + similarPairs.image.size;
|
|
802
|
+
if (totalFindings === 0) return "未发现高相似度的内容";
|
|
803
|
+
let report = `已发现 ${totalFindings} 组高相似度的内容:`;
|
|
804
|
+
if (similarPairs.text.size > 0) report += "\n文本内容相似:\n" + [...similarPairs.text].join("\n");
|
|
805
|
+
if (similarPairs.image.size > 0) report += "\n图片内容相似:\n" + [...similarPairs.image].join("\n");
|
|
806
|
+
return report.trim();
|
|
742
807
|
} catch (error) {
|
|
743
808
|
this.logger.error("检查相似度失败:", error);
|
|
744
809
|
return `检查失败: ${error.message}`;
|
|
745
810
|
}
|
|
746
811
|
});
|
|
747
812
|
}
|
|
748
|
-
/**
|
|
749
|
-
* @description 检查数据库中所有回声洞,为没有哈希记录的历史数据生成哈希。
|
|
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++;
|
|
771
|
-
try {
|
|
772
|
-
const newHashesForCave = await this.generateAllHashesForCave(cave);
|
|
773
|
-
for (const hashObj of newHashesForCave) {
|
|
774
|
-
const uniqueKey = `${hashObj.cave}-${hashObj.hash}-${hashObj.type}`;
|
|
775
|
-
if (!existingHashSet.has(uniqueKey)) {
|
|
776
|
-
hashesToInsert.push(hashObj);
|
|
777
|
-
existingHashSet.add(uniqueKey);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
if (hashesToInsert.length >= 100) await flushBatch();
|
|
781
|
-
} catch (error) {
|
|
782
|
-
errorCount++;
|
|
783
|
-
this.logger.warn(`补全回声洞(${cave.id})哈希时发生错误: ${error.message}`);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
await flushBatch();
|
|
787
|
-
return `已补全 ${allCaves.length} 个回声洞的 ${totalHashesGenerated} 条哈希(失败 ${errorCount} 条)`;
|
|
788
|
-
}
|
|
789
813
|
/**
|
|
790
814
|
* @description 为单个回声洞对象生成所有类型的哈希(文本+图片)。
|
|
791
815
|
* @param cave - 回声洞对象。
|
|
@@ -804,13 +828,13 @@ var HashManager = class {
|
|
|
804
828
|
const combinedText = cave.elements.filter((el) => el.type === "text" && el.content).map((el) => el.content).join(" ");
|
|
805
829
|
if (combinedText) {
|
|
806
830
|
const textHash = this.generateTextSimhash(combinedText);
|
|
807
|
-
if (textHash) addUniqueHash({ cave: cave.id, hash: textHash, type: "
|
|
831
|
+
if (textHash) addUniqueHash({ cave: cave.id, hash: textHash, type: "text" });
|
|
808
832
|
}
|
|
809
833
|
for (const el of cave.elements.filter((el2) => el2.type === "image" && el2.file)) {
|
|
810
834
|
try {
|
|
811
835
|
const imageBuffer = await this.fileManager.readFile(el.file);
|
|
812
|
-
const imageHash = await this.generatePHash(imageBuffer
|
|
813
|
-
addUniqueHash({ cave: cave.id, hash: imageHash, type: "
|
|
836
|
+
const imageHash = await this.generatePHash(imageBuffer);
|
|
837
|
+
addUniqueHash({ cave: cave.id, hash: imageHash, type: "image" });
|
|
814
838
|
} catch (e) {
|
|
815
839
|
this.logger.warn(`无法为回声洞(${cave.id})的图片(${el.file})生成哈希:`, e);
|
|
816
840
|
}
|
|
@@ -818,101 +842,57 @@ var HashManager = class {
|
|
|
818
842
|
return tempHashes;
|
|
819
843
|
}
|
|
820
844
|
/**
|
|
821
|
-
* @description
|
|
822
|
-
* @param
|
|
823
|
-
* @returns
|
|
845
|
+
* @description 执行一维离散余弦变换 (DCT-II) 的方法。
|
|
846
|
+
* @param input - 输入的数字数组。
|
|
847
|
+
* @returns DCT 变换后的数组。
|
|
824
848
|
*/
|
|
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
|
-
}
|
|
838
|
-
}
|
|
839
|
-
const similarPairs = {
|
|
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
|
-
}
|
|
849
|
+
dct1D(input) {
|
|
850
|
+
const N = input.length;
|
|
851
|
+
const output = new Array(N).fill(0);
|
|
852
|
+
const c0 = 1 / Math.sqrt(2);
|
|
853
|
+
for (let k = 0; k < N; k++) {
|
|
854
|
+
let sum = 0;
|
|
855
|
+
for (let n = 0; n < N; n++) sum += input[n] * Math.cos(Math.PI * (2 * n + 1) * k / (2 * N));
|
|
856
|
+
const ck = k === 0 ? c0 : 1;
|
|
857
|
+
output[k] = Math.sqrt(2 / N) * ck * sum;
|
|
861
858
|
}
|
|
862
|
-
|
|
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();
|
|
859
|
+
return output;
|
|
868
860
|
}
|
|
869
861
|
/**
|
|
870
|
-
* @description 执行二维离散余弦变换 (DCT-II)
|
|
862
|
+
* @description 执行二维离散余弦变换 (DCT-II) 的方法。
|
|
863
|
+
* 通过对行和列分别应用一维 DCT 来实现。
|
|
871
864
|
* @param matrix - 输入的 N x N 像素亮度矩阵。
|
|
872
|
-
* @returns DCT变换后的 N x N 系数矩阵。
|
|
865
|
+
* @returns DCT 变换后的 N x N 系数矩阵。
|
|
873
866
|
*/
|
|
874
|
-
|
|
867
|
+
dct2D(matrix) {
|
|
875
868
|
const N = matrix.length;
|
|
876
869
|
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]));
|
|
870
|
+
const tempMatrix = matrix.map((row) => this.dct1D(row));
|
|
871
|
+
const transposed = tempMatrix.map((_, colIndex) => tempMatrix.map((row) => row[colIndex]));
|
|
872
|
+
const dctResultTransposed = transposed.map((row) => this.dct1D(row));
|
|
873
|
+
const dctResult = dctResultTransposed.map((_, colIndex) => dctResultTransposed.map((row) => row[colIndex]));
|
|
874
|
+
return dctResult;
|
|
896
875
|
}
|
|
897
876
|
/**
|
|
898
|
-
* @description pHash
|
|
899
|
-
* @param imageBuffer - 图片的Buffer。
|
|
900
|
-
* @
|
|
901
|
-
* @returns 十六进制pHash字符串。
|
|
877
|
+
* @description pHash 算法核心实现,使用 Jimp 和自定义 DCT。
|
|
878
|
+
* @param imageBuffer - 图片的 Buffer。
|
|
879
|
+
* @returns 64位十六进制 pHash 字符串。
|
|
902
880
|
*/
|
|
903
|
-
async generatePHash(imageBuffer
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const dctMatrix = this.
|
|
881
|
+
async generatePHash(imageBuffer) {
|
|
882
|
+
const image = await import_jimp.default.read(imageBuffer);
|
|
883
|
+
image.resize(32, 32, import_jimp.default.RESIZE_BILINEAR).greyscale();
|
|
884
|
+
const matrix = Array.from({ length: 32 }, () => new Array(32).fill(0));
|
|
885
|
+
image.scan(0, 0, 32, 32, (x, y, idx) => {
|
|
886
|
+
matrix[y][x] = image.bitmap.data[idx];
|
|
887
|
+
});
|
|
888
|
+
const dctMatrix = this.dct2D(matrix);
|
|
911
889
|
const coefficients = [];
|
|
912
|
-
for (let y = 0; y <
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
890
|
+
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) coefficients.push(dctMatrix[y][x]);
|
|
891
|
+
const acCoefficients = coefficients.slice(1);
|
|
892
|
+
const average = acCoefficients.reduce((sum, val) => sum + val, 0) / acCoefficients.length;
|
|
893
|
+
let binaryHash = "";
|
|
894
|
+
for (const val of coefficients) binaryHash += val > average ? "1" : "0";
|
|
895
|
+
return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
|
|
916
896
|
}
|
|
917
897
|
/**
|
|
918
898
|
* @description 计算两个十六进制哈希字符串之间的汉明距离 (不同位的数量)。
|
|
@@ -922,8 +902,10 @@ var HashManager = class {
|
|
|
922
902
|
*/
|
|
923
903
|
calculateHammingDistance(hex1, hex2) {
|
|
924
904
|
let distance = 0;
|
|
925
|
-
|
|
926
|
-
const
|
|
905
|
+
let bin1 = "";
|
|
906
|
+
for (const char of hex1) bin1 += parseInt(char, 16).toString(2).padStart(4, "0");
|
|
907
|
+
let bin2 = "";
|
|
908
|
+
for (const char of hex2) bin2 += parseInt(char, 16).toString(2).padStart(4, "0");
|
|
927
909
|
const len = Math.min(bin1.length, bin2.length);
|
|
928
910
|
for (let i = 0; i < len; i++) if (bin1[i] !== bin2[i]) distance++;
|
|
929
911
|
return distance;
|
|
@@ -947,14 +929,8 @@ var HashManager = class {
|
|
|
947
929
|
generateTextSimhash(text) {
|
|
948
930
|
const cleanText = (text || "").toLowerCase().replace(/\s+/g, "");
|
|
949
931
|
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);
|
|
932
|
+
const tokens = Array.from(cleanText);
|
|
933
|
+
const tokenArray = Array.from(new Set(tokens));
|
|
958
934
|
if (tokenArray.length === 0) return "";
|
|
959
935
|
const vector = new Array(64).fill(0);
|
|
960
936
|
tokenArray.forEach((token) => {
|
|
@@ -965,15 +941,8 @@ var HashManager = class {
|
|
|
965
941
|
return BigInt("0b" + binaryHash).toString(16).padStart(16, "0");
|
|
966
942
|
}
|
|
967
943
|
};
|
|
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
944
|
|
|
975
945
|
// src/AIManager.ts
|
|
976
|
-
var import_koishi3 = require("koishi");
|
|
977
946
|
var path3 = __toESM(require("path"));
|
|
978
947
|
var AIManager = class {
|
|
979
948
|
/**
|
|
@@ -999,6 +968,8 @@ var AIManager = class {
|
|
|
999
968
|
__name(this, "AIManager");
|
|
1000
969
|
}
|
|
1001
970
|
http;
|
|
971
|
+
requestCount = 0;
|
|
972
|
+
rateLimitResetTime = 0;
|
|
1002
973
|
/**
|
|
1003
974
|
* @description 注册所有与 AIManager 功能相关的 Koishi 命令。
|
|
1004
975
|
* @param {any} cave - 主 `cave` 命令的实例,用于在其下注册子命令。
|
|
@@ -1013,18 +984,13 @@ var AIManager = class {
|
|
|
1013
984
|
if (cavesToAnalyze.length === 0) return "无需分析回声洞";
|
|
1014
985
|
await session.send(`开始分析 ${cavesToAnalyze.length} 个回声洞...`);
|
|
1015
986
|
let successCount = 0;
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
successCount++;
|
|
1024
|
-
} catch (error) {
|
|
1025
|
-
this.logger.error(`分析回声洞(${cave2.id})时出错:`, error);
|
|
1026
|
-
return `分析回声洞(${cave2.id})时出错: ${error.message}`;
|
|
1027
|
-
}
|
|
987
|
+
for (const [index, cave2] of cavesToAnalyze.entries()) {
|
|
988
|
+
try {
|
|
989
|
+
this.logger.info(`[${index + 1}/${cavesToAnalyze.length}] 正在分析回声洞 (${cave2.id})...`);
|
|
990
|
+
await this.analyzeAndStore(cave2);
|
|
991
|
+
successCount++;
|
|
992
|
+
} catch (error) {
|
|
993
|
+
return `分析回声洞(${cave2.id})时出错: ${error.message}`;
|
|
1028
994
|
}
|
|
1029
995
|
}
|
|
1030
996
|
return `已分析 ${successCount} 个回声洞`;
|
|
@@ -1033,23 +999,6 @@ var AIManager = class {
|
|
|
1033
999
|
return `操作失败: ${error.message}`;
|
|
1034
1000
|
}
|
|
1035
1001
|
});
|
|
1036
|
-
cave.subcommand(".desc <id:posint>", "查询回声洞").action(async ({}, id) => {
|
|
1037
|
-
if (!id) return "请输入要查看的回声洞序号";
|
|
1038
|
-
try {
|
|
1039
|
-
const [meta] = await this.ctx.database.get("cave_meta", { cave: id });
|
|
1040
|
-
if (!meta) return `回声洞(${id})尚未分析`;
|
|
1041
|
-
const report = [
|
|
1042
|
-
`回声洞(${id})分析结果:`,
|
|
1043
|
-
`描述:${meta.description}`,
|
|
1044
|
-
`关键词:${meta.keywords.join(", ")}`,
|
|
1045
|
-
`综合评分:${meta.rating}/100`
|
|
1046
|
-
];
|
|
1047
|
-
return import_koishi3.h.text(report.join("\n"));
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
this.logger.error(`查询回声洞(${id})失败:`, error);
|
|
1050
|
-
return "查询失败,请稍后再试";
|
|
1051
|
-
}
|
|
1052
|
-
});
|
|
1053
1002
|
}
|
|
1054
1003
|
/**
|
|
1055
1004
|
* @description 对新提交的内容执行 AI 驱动的查重检查。
|
|
@@ -1166,11 +1115,31 @@ var AIManager = class {
|
|
|
1166
1115
|
* @throws {Error} 当 JSON Schema 解析失败、网络请求失败或 AI 返回错误时,抛出异常。
|
|
1167
1116
|
*/
|
|
1168
1117
|
async requestAI(messages, systemPrompt, schemaString) {
|
|
1118
|
+
const now = Date.now();
|
|
1119
|
+
if (now > this.rateLimitResetTime) {
|
|
1120
|
+
this.rateLimitResetTime = now + 6e4;
|
|
1121
|
+
this.requestCount = 0;
|
|
1122
|
+
}
|
|
1123
|
+
if (this.requestCount >= this.config.aiRPM) {
|
|
1124
|
+
const delay = this.rateLimitResetTime - now;
|
|
1125
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1126
|
+
this.rateLimitResetTime = Date.now() + 6e4;
|
|
1127
|
+
this.requestCount = 0;
|
|
1128
|
+
}
|
|
1169
1129
|
let schema = JSON.parse(schemaString);
|
|
1130
|
+
const toolName = "extract_data";
|
|
1170
1131
|
const payload = {
|
|
1171
1132
|
model: this.config.aiModel,
|
|
1172
1133
|
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
|
1173
|
-
|
|
1134
|
+
tools: [{
|
|
1135
|
+
type: "function",
|
|
1136
|
+
function: {
|
|
1137
|
+
name: toolName,
|
|
1138
|
+
description: "根据提供的内容提取或分析信息。",
|
|
1139
|
+
parameters: schema
|
|
1140
|
+
}
|
|
1141
|
+
}],
|
|
1142
|
+
tool_choice: { type: "function", function: { name: toolName } }
|
|
1174
1143
|
};
|
|
1175
1144
|
const fullUrl = `${this.config.aiEndpoint.replace(/\/$/, "")}/chat/completions`;
|
|
1176
1145
|
const headers = {
|
|
@@ -1178,8 +1147,15 @@ var AIManager = class {
|
|
|
1178
1147
|
"Authorization": `Bearer ${this.config.aiApiKey}`
|
|
1179
1148
|
};
|
|
1180
1149
|
try {
|
|
1150
|
+
this.requestCount++;
|
|
1181
1151
|
const response = await this.http.post(fullUrl, payload, { headers, timeout: 9e4 });
|
|
1182
|
-
|
|
1152
|
+
const toolCall = response.choices?.[0]?.message?.tool_calls?.[0];
|
|
1153
|
+
if (toolCall?.function?.arguments) {
|
|
1154
|
+
return JSON.parse(toolCall.function.arguments);
|
|
1155
|
+
} else {
|
|
1156
|
+
this.logger.error("AI 响应格式不正确:", JSON.stringify(response));
|
|
1157
|
+
throw new Error("AI 响应格式不正确");
|
|
1158
|
+
}
|
|
1183
1159
|
} catch (error) {
|
|
1184
1160
|
const errorMessage = error.response ? JSON.stringify(error.response.data) : error.message;
|
|
1185
1161
|
this.logger.error(`请求 API 失败: ${errorMessage}`);
|
|
@@ -1203,28 +1179,29 @@ var usage = `
|
|
|
1203
1179
|
<p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p>
|
|
1204
1180
|
</div>
|
|
1205
1181
|
`;
|
|
1206
|
-
var logger = new
|
|
1207
|
-
var Config =
|
|
1208
|
-
|
|
1209
|
-
perChannel:
|
|
1210
|
-
enableName:
|
|
1211
|
-
enableIO:
|
|
1212
|
-
adminChannel:
|
|
1213
|
-
caveFormat:
|
|
1182
|
+
var logger = new import_koishi3.Logger("best-cave");
|
|
1183
|
+
var Config = import_koishi3.Schema.intersect([
|
|
1184
|
+
import_koishi3.Schema.object({
|
|
1185
|
+
perChannel: import_koishi3.Schema.boolean().default(false).description("启用分群模式"),
|
|
1186
|
+
enableName: import_koishi3.Schema.boolean().default(false).description("启用自定义昵称"),
|
|
1187
|
+
enableIO: import_koishi3.Schema.boolean().default(false).description("启用导入导出"),
|
|
1188
|
+
adminChannel: import_koishi3.Schema.string().default("onebot:").description("管理群组 ID"),
|
|
1189
|
+
caveFormat: import_koishi3.Schema.string().default("回声洞 ——({id})|—— {name}").description("自定义文本(参见 README)")
|
|
1214
1190
|
}).description("基础配置"),
|
|
1215
|
-
|
|
1216
|
-
enablePend:
|
|
1217
|
-
enableSimilarity:
|
|
1218
|
-
textThreshold:
|
|
1219
|
-
imageThreshold:
|
|
1191
|
+
import_koishi3.Schema.object({
|
|
1192
|
+
enablePend: import_koishi3.Schema.boolean().default(false).description("启用审核"),
|
|
1193
|
+
enableSimilarity: import_koishi3.Schema.boolean().default(false).description("启用查重"),
|
|
1194
|
+
textThreshold: import_koishi3.Schema.number().min(0).max(100).step(0.01).default(90).description("文本相似度阈值 (%)"),
|
|
1195
|
+
imageThreshold: import_koishi3.Schema.number().min(0).max(100).step(0.01).default(90).description("图片相似度阈值 (%)")
|
|
1220
1196
|
}).description("复核配置"),
|
|
1221
|
-
|
|
1222
|
-
enableAI:
|
|
1223
|
-
aiEndpoint:
|
|
1224
|
-
aiApiKey:
|
|
1225
|
-
aiModel:
|
|
1226
|
-
|
|
1227
|
-
|
|
1197
|
+
import_koishi3.Schema.object({
|
|
1198
|
+
enableAI: import_koishi3.Schema.boolean().default(false).description("启用 AI"),
|
|
1199
|
+
aiEndpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link").default("https://generativelanguage.googleapis.com/v1beta/openai"),
|
|
1200
|
+
aiApiKey: import_koishi3.Schema.string().description("密钥 (Key)").role("secret"),
|
|
1201
|
+
aiModel: import_koishi3.Schema.string().description("模型 (Model)").default("gemini-2.5-flash"),
|
|
1202
|
+
aiRPM: import_koishi3.Schema.number().description("每分钟请求数 (RPM)").default(60),
|
|
1203
|
+
AnalysePrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容分析专家。请分析我提供的内容,总结关键词,概括内容并进行评分。`).description("分析 Prompt"),
|
|
1204
|
+
aiAnalyseSchema: import_koishi3.Schema.string().role("textarea").default(
|
|
1228
1205
|
`{
|
|
1229
1206
|
"type": "object",
|
|
1230
1207
|
"properties": {
|
|
@@ -1246,9 +1223,9 @@ var Config = import_koishi4.Schema.intersect([
|
|
|
1246
1223
|
},
|
|
1247
1224
|
"required": ["keywords", "description", "rating"]
|
|
1248
1225
|
}`
|
|
1249
|
-
).description("
|
|
1250
|
-
aiCheckPrompt:
|
|
1251
|
-
aiCheckSchema:
|
|
1226
|
+
).description("分析 JSON Schema"),
|
|
1227
|
+
aiCheckPrompt: import_koishi3.Schema.string().role("textarea").default(`你是一位内容查重专家。请判断我提供的"新内容"是否与"已有内容"重复或高度相似。`).description("查重 Prompt"),
|
|
1228
|
+
aiCheckSchema: import_koishi3.Schema.string().role("textarea").default(
|
|
1252
1229
|
`{
|
|
1253
1230
|
"type": "object",
|
|
1254
1231
|
"properties": {
|
|
@@ -1263,17 +1240,17 @@ var Config = import_koishi4.Schema.intersect([
|
|
|
1263
1240
|
},
|
|
1264
1241
|
"required": ["duplicate"]
|
|
1265
1242
|
}`
|
|
1266
|
-
).description("
|
|
1243
|
+
).description("查重 JSON Schema")
|
|
1267
1244
|
}).description("模型配置"),
|
|
1268
|
-
|
|
1269
|
-
localPath:
|
|
1270
|
-
enableS3:
|
|
1271
|
-
publicUrl:
|
|
1272
|
-
endpoint:
|
|
1273
|
-
bucket:
|
|
1274
|
-
region:
|
|
1275
|
-
accessKeyId:
|
|
1276
|
-
secretAccessKey:
|
|
1245
|
+
import_koishi3.Schema.object({
|
|
1246
|
+
localPath: import_koishi3.Schema.string().description("文件映射路径"),
|
|
1247
|
+
enableS3: import_koishi3.Schema.boolean().default(false).description("启用 S3 存储"),
|
|
1248
|
+
publicUrl: import_koishi3.Schema.string().description("公共访问 URL").role("link"),
|
|
1249
|
+
endpoint: import_koishi3.Schema.string().description("端点 (Endpoint)").role("link"),
|
|
1250
|
+
bucket: import_koishi3.Schema.string().description("存储桶 (Bucket)"),
|
|
1251
|
+
region: import_koishi3.Schema.string().default("auto").description("区域 (Region)"),
|
|
1252
|
+
accessKeyId: import_koishi3.Schema.string().description("Access Key ID").role("secret"),
|
|
1253
|
+
secretAccessKey: import_koishi3.Schema.string().description("Secret Access Key").role("secret")
|
|
1277
1254
|
}).description("存储配置")
|
|
1278
1255
|
]);
|
|
1279
1256
|
function apply(ctx, config) {
|
|
@@ -1320,7 +1297,7 @@ function apply(ctx, config) {
|
|
|
1320
1297
|
const randomId = candidates[Math.floor(Math.random() * candidates.length)].id;
|
|
1321
1298
|
const [randomCave] = await ctx.database.get("cave", { ...query, id: randomId });
|
|
1322
1299
|
const messages = await buildCaveMessage(randomCave, config, fileManager, logger, session.platform);
|
|
1323
|
-
for (const message of messages) if (message.length > 0) await session.send(
|
|
1300
|
+
for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1324
1301
|
} catch (error) {
|
|
1325
1302
|
logger.error("随机获取回声洞失败:", error);
|
|
1326
1303
|
return "随机获取回声洞失败";
|
|
@@ -1332,12 +1309,12 @@ function apply(ctx, config) {
|
|
|
1332
1309
|
if (session.quote?.elements) {
|
|
1333
1310
|
sourceElements = session.quote.elements;
|
|
1334
1311
|
} else if (content?.trim()) {
|
|
1335
|
-
sourceElements =
|
|
1312
|
+
sourceElements = import_koishi3.h.parse(content);
|
|
1336
1313
|
} else {
|
|
1337
1314
|
await session.send("请在一分钟内发送你要添加的内容");
|
|
1338
1315
|
const reply = await session.prompt(6e4);
|
|
1339
1316
|
if (!reply) return "等待操作超时";
|
|
1340
|
-
sourceElements =
|
|
1317
|
+
sourceElements = import_koishi3.h.parse(reply);
|
|
1341
1318
|
}
|
|
1342
1319
|
const newId = await getNextCaveId(ctx, reusableIds);
|
|
1343
1320
|
const creationTime = /* @__PURE__ */ new Date();
|
|
@@ -1380,7 +1357,7 @@ function apply(ctx, config) {
|
|
|
1380
1357
|
newCave.status = finalStatus;
|
|
1381
1358
|
if (aiManager) await aiManager.analyzeAndStore(newCave, downloadedMedia);
|
|
1382
1359
|
if (hashManager) {
|
|
1383
|
-
const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((
|
|
1360
|
+
const allHashesToInsert = [...textHashesToStore, ...imageHashesToStore].map((h4) => ({ ...h4, cave: newCave.id }));
|
|
1384
1361
|
if (allHashesToInsert.length > 0) await ctx.database.upsert("cave_hash", allHashesToInsert);
|
|
1385
1362
|
}
|
|
1386
1363
|
if (finalStatus === "pending" && reviewManager) reviewManager.sendForPend(newCave);
|
|
@@ -1397,7 +1374,7 @@ function apply(ctx, config) {
|
|
|
1397
1374
|
const [targetCave] = await ctx.database.get("cave", { ...getScopeQuery(session, config), id });
|
|
1398
1375
|
if (!targetCave) return `回声洞(${id})不存在`;
|
|
1399
1376
|
const messages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform);
|
|
1400
|
-
for (const message of messages) if (message.length > 0) await session.send(
|
|
1377
|
+
for (const message of messages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1401
1378
|
} catch (error) {
|
|
1402
1379
|
logger.error(`查看回声洞(${id})失败:`, error);
|
|
1403
1380
|
return "查看失败,请稍后再试";
|
|
@@ -1414,7 +1391,7 @@ function apply(ctx, config) {
|
|
|
1414
1391
|
await ctx.database.upsert("cave", [{ id, status: "delete" }]);
|
|
1415
1392
|
const caveMessages = await buildCaveMessage(targetCave, config, fileManager, logger, session.platform, "已删除");
|
|
1416
1393
|
cleanupPendingDeletions(ctx, fileManager, logger, reusableIds);
|
|
1417
|
-
for (const message of caveMessages) if (message.length > 0) await session.send(
|
|
1394
|
+
for (const message of caveMessages) if (message.length > 0) await session.send(import_koishi3.h.normalize(message));
|
|
1418
1395
|
} catch (error) {
|
|
1419
1396
|
logger.error(`标记回声洞(${id})失败:`, error);
|
|
1420
1397
|
return "删除失败,请稍后再试";
|
|
@@ -1425,7 +1402,7 @@ function apply(ctx, config) {
|
|
|
1425
1402
|
const adminChannelId = config.adminChannel?.split(":")[1];
|
|
1426
1403
|
if (session.channelId !== adminChannelId) return "此指令仅限在管理群组中使用";
|
|
1427
1404
|
try {
|
|
1428
|
-
const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) =>
|
|
1405
|
+
const aggregatedStats = await ctx.database.select("cave", { status: "active" }).groupBy(["userId", "userName"], { count: /* @__PURE__ */ __name((row) => import_koishi3.$.count(row.id), "count") }).execute();
|
|
1429
1406
|
if (!aggregatedStats.length) return "目前没有回声洞投稿";
|
|
1430
1407
|
const userStats = /* @__PURE__ */ new Map();
|
|
1431
1408
|
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.7",
|
|
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
|
}
|