koishi-plugin-best-cave 1.1.5 → 1.3.0

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.js CHANGED
@@ -33,14 +33,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
33
33
  // src/locales/zh-CN.yml
34
34
  var require_zh_CN = __commonJS({
35
35
  "src/locales/zh-CN.yml"(exports2, module2) {
36
- module2.exports = { _config: { manager: "管理员", blacklist: "黑名单(用户)", whitelist: "审核白名单(用户/群组/频道)", number: "冷却时间(秒)", enableAudit: "启用审核", allowVideo: "允许视频上传", videoMaxSize: "视频最大大小(MB)", imageMaxSize: "图片最大大小(MB)", enablePagination: "启用统计分页", itemsPerPage: "每页显示数目" }, commands: { cave: { description: "回声洞", usage: "支持添加、抽取、查看、管理回声洞", examples: "使用 cave 随机抽取回声洞\n使用 -a 直接添加或引用添加\n使用 -g 查看指定回声洞\n使用 -r 删除指定回声洞", options: { a: "添加回声洞", g: "查看回声洞", r: "删除回声洞", p: "通过审核(批量)", d: "拒绝审核(批量)", l: "查询投稿统计" }, add: { noContent: "请在一分钟内发送内容", operationTimeout: "操作超时,添加取消", videoDisabled: "不允许上传视频", submitPending: "提交成功,序号为({0})", addSuccess: "添加成功,序号为({0})", mediaSizeExceeded: "{0}文件大小超过限制", mediaFormatUnsupported: "{0}格式不支持", videoSendFailed: "视频发送失败", localFileNotAllowed: "检测到本地文件路径,无法保存" }, remove: { noPermission: "你无权删除他人添加的回声洞", deletePending: "删除(待审核)", deleted: "已删除" }, list: { pageInfo: "第 {0} / {1} 页", header: "当前共有 {0} 项回声洞:", totalItems: "用户 {0} 共计投稿 {1} 项:", idsLine: "{0}" }, audit: { noPending: "暂无待审核回声洞", pendingNotFound: "未找到待审核回声洞", pendingResult: "{0},剩余 {1} 个待审核回声洞:[{2}]", auditPassed: "已通过", auditRejected: "已拒绝", batchAuditResult: "已{0} {1}/{2} 项回声洞", title: "待审核回声洞:", from: "投稿人:", sendFailed: "发送审核消息失败,无法联系管理员 {0}" }, error: { commandProcess: "命令处理失败:{0}", getCave: "获取回声洞失败", noCave: "暂无回声洞", saveFailed: "保存数据失败", mediaLoadFailed: "[加载{0}失败]", videoSendFailed: "视频发送失败", uploadImageFailed: "图片上传失败", uploadVideoFailed: "视频上传失败", fileRead: "读取 JSON 数据失败:{0}", fileWrite: "写入 JSON 数据失败:{0}", notFound: "未找到", userInfo: "获取用户信息失败:{0}", auditProcess: "审核处理失败:{0}", invalidId: "请输入有效的回声洞序号" }, message: { blacklisted: "你已被列入黑名单", managerOnly: "此操作仅限管理员可用", cooldown: "群聊冷却中...请在 {0} 秒后重试", caveTitle: "回声洞 —— ({0})", videoSending: "[视频发送中]", mediaInvalid: "[无效的{0}]", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0}文件大小超过限制" } } } };
36
+ module2.exports = { _config: { manager: "管理员", number: "冷却时间(秒)", enableAudit: "启用审核", imageMaxSize: "图片最大大小(MB)", duplicateThreshold: "图片相似度阈值(0-1)", allowVideo: "允许视频上传", videoMaxSize: "视频最大大小(MB)", enablePagination: "启用统计分页", itemsPerPage: "每页显示数目", blacklist: "黑名单(用户)", whitelist: "审核白名单(用户/群组/频道)" }, commands: { cave: { description: "回声洞", usage: "支持添加、抽取、查看、管理回声洞", examples: "使用 cave 随机抽取回声洞\n使用 -a 直接添加或引用添加\n使用 -g 查看指定回声洞\n使用 -r 删除指定回声洞", options: { a: "添加回声洞", g: "查看回声洞", r: "删除回声洞", p: "通过审核(批量)", d: "拒绝审核(批量)", l: "查询投稿统计" }, add: { noContent: "请在一分钟内发送内容", operationTimeout: "操作超时,添加取消", videoDisabled: "不允许上传视频", submitPending: "提交成功,序号为({0})", addSuccess: "添加成功,序号为({0})", mediaSizeExceeded: "{0}文件大小超过限制", localFileNotAllowed: "检测到本地文件路径,无法保存" }, remove: { noPermission: "你无权删除他人添加的回声洞", deletePending: "删除(待审核)", deleted: "已删除" }, list: { pageInfo: "第 {0} / {1} 页", header: "当前共有 {0} 项回声洞:", totalItems: "用户 {0} 共计投稿 {1} 项:", idsLine: "{0}" }, audit: { noPending: "暂无待审核回声洞", pendingNotFound: "未找到待审核回声洞", pendingResult: "{0},剩余 {1} 个待审核回声洞:[{2}]", auditPassed: "已通过", auditRejected: "已拒绝", batchAuditResult: "已{0} {1}/{2} 项回声洞", title: "待审核回声洞:", from: "投稿人:", sendFailed: "发送审核消息失败,无法联系管理员 {0}" }, error: { noContent: "回声洞内容为空", getCave: "获取回声洞失败", noCave: "没有回声洞", invalidId: "请输入有效的回声洞ID", notFound: "未找到该回声洞", duplicateFound: "发现相似度为 {0}% " }, message: { blacklisted: "你已被列入黑名单", managerOnly: "此操作仅限管理员可用", cooldown: "群聊冷却中...请在 {0} 秒后重试", caveTitle: "回声洞 —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0}文件大小超过限制" } } } };
37
37
  }
38
38
  });
39
39
 
40
40
  // src/locales/en-US.yml
41
41
  var require_en_US = __commonJS({
42
42
  "src/locales/en-US.yml"(exports2, module2) {
43
- module2.exports = { _config: { manager: "Manager", blacklist: "Blacklist (User)", whitelist: "Whitelist (User/Group/Channel)", number: "Cooldown Time (Seconds)", enableAudit: "Enable Audit", allowVideo: "Allow Video Upload", videoMaxSize: "Max Video Size (MB)", imageMaxSize: "Max Image Size (MB)", enablePagination: "Enable Pagination", itemsPerPage: "Items Per Page" }, commands: { cave: { description: "Echo Cave", usage: "Support adding, drawing, viewing, and managing Echo Cave", examples: "Use cave to randomly draw Echo Cave\nUse -a to add or reference directly\nUse -g to view specified Echo Cave\nUse -r to delete specified Echo Cave", options: { a: "Add Echo Cave", g: "View Echo Cave", r: "Delete Echo Cave", p: "Approve Audit (Batch)", d: "Reject Audit (Batch)", l: "Query Submission Statistics" }, add: { noContent: "Please send content within one minute", operationTimeout: "Operation timed out, addition canceled", videoDisabled: "Video upload not allowed", submitPending: "Submission successful, ID is ({0})", addSuccess: "Addition successful, ID is ({0})", mediaSizeExceeded: "{0} file size exceeds limit", mediaFormatUnsupported: "{0} format not supported", videoSendFailed: "Video send failed", localFileNotAllowed: "Local file path detected, cannot save" }, remove: { noPermission: "You do not have permission to delete others' Echo Cave", deletePending: "Delete (Pending Audit)", deleted: "Deleted" }, list: { pageInfo: "Page {0} / {1}", header: "There are currently {0} Echo Caves:", totalItems: "User {0} has submitted a total of {1} items:", idsLine: "{0}" }, audit: { noPending: "No pending Echo Caves", pendingNotFound: "Pending Echo Cave not found", pendingResult: "{0}, {1} pending Echo Caves remaining: [{2}]", auditPassed: "Approved", auditRejected: "Rejected", batchAuditResult: "{0} {1}/{2} Echo Caves", title: "Pending Echo Caves:", from: "Contributor:", sendFailed: "Failed to send audit message, unable to contact manager {0}" }, error: { commandProcess: "Command processing failed: {0}", getCave: "Failed to get Echo Cave", noCave: "No Echo Caves", saveFailed: "Failed to save data", mediaLoadFailed: "[Failed to load {0}]", videoSendFailed: "Video send failed", uploadImageFailed: "Image upload failed", uploadVideoFailed: "Video upload failed", fileRead: "Failed to read JSON data: {0}", fileWrite: "Failed to write JSON data: {0}", notFound: "Not found", userInfo: "Failed to get user information: {0}", auditProcess: "Audit processing failed: {0}", invalidId: "Please enter a valid Echo Cave ID" }, message: { blacklisted: "You have been blacklisted", managerOnly: "This operation is for managers only", cooldown: "Group chat cooldown... Please try again in {0} seconds", caveTitle: "Echo Cave —— ({0})", videoSending: "[Video sending]", mediaInvalid: "[Invalid {0}]", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0} file size exceeds limit" } } } };
43
+ module2.exports = { _config: { manager: "Administrator", number: "Cooldown time (seconds)", enableAudit: "Enable moderation", imageMaxSize: "Maximum image size (MB)", duplicateThreshold: "Image similarity threshold (0-1)", allowVideo: "Allow video uploads", videoMaxSize: "Maximum video size (MB)", enablePagination: "Enable statistics pagination", itemsPerPage: "Items per page", blacklist: "Blacklist (users)", whitelist: "Moderation whitelist (users/groups/channels)" }, commands: { cave: { description: "Echo Cave", usage: "Support adding, drawing, viewing, and managing echo cave entries", examples: "Use cave to randomly draw an entry\nUse -a to add directly or by reference\nUse -g to view specific entry\nUse -r to delete specific entry", options: { a: "Add entry", g: "View entry", r: "Delete entry", p: "Approve submission (batch)", d: "Reject submission (batch)", l: "Check submission statistics" }, add: { noContent: "Please send content within one minute", operationTimeout: "Operation timeout, addition cancelled", videoDisabled: "Video uploads are not allowed", submitPending: "Submission successful, ID is ({0})", addSuccess: "Added successfully, ID is ({0})", mediaSizeExceeded: "{0} file size exceeds limit", localFileNotAllowed: "Local file path detected, cannot save" }, remove: { noPermission: "You don't have permission to delete others' entries", deletePending: "Delete (pending)", deleted: "Deleted" }, list: { pageInfo: "Page {0} / {1}", header: "Currently there are {0} entries:", totalItems: "User {0} has submitted {1} entries:", idsLine: "{0}" }, audit: { noPending: "No pending entries", pendingNotFound: "Pending entry not found", pendingResult: "{0}, {1} pending entries remaining: [{2}]", auditPassed: "Approved", auditRejected: "Rejected", batchAuditResult: "{0} {1}/{2} entries", title: "Pending entries:", from: "Submitted by:", sendFailed: "Failed to send moderation message, cannot contact administrator {0}" }, error: { noContent: "Entry content is empty", getCave: "Failed to get entry", noCave: "No entries available", invalidId: "Please enter a valid entry ID", notFound: "Entry not found", duplicateFound: "Found duplicate image with similarity of {0}%" }, message: { blacklisted: "You have been blacklisted", managerOnly: "This operation is for administrators only", cooldown: "Group chat cooldown... Please try again in {0} seconds", caveTitle: "Echo Cave —— ({0})", contributorSuffix: "—— {0}", mediaSizeExceeded: "{0} file size exceeds limit" } } } };
44
44
  }
45
45
  });
46
46
 
@@ -53,22 +53,690 @@ __export(src_exports, {
53
53
  name: () => name
54
54
  });
55
55
  module.exports = __toCommonJS(src_exports);
56
- var import_koishi = require("koishi");
56
+ var import_koishi4 = require("koishi");
57
+ var fs4 = __toESM(require("fs"));
58
+ var path4 = __toESM(require("path"));
59
+
60
+ // src/utils/fileHandler.ts
57
61
  var fs = __toESM(require("fs"));
58
62
  var path = __toESM(require("path"));
63
+ var import_koishi = require("koishi");
64
+ var logger = new import_koishi.Logger("fileHandler");
65
+ var FileHandler = class {
66
+ static {
67
+ __name(this, "FileHandler");
68
+ }
69
+ static locks = /* @__PURE__ */ new Map();
70
+ static RETRY_COUNT = 3;
71
+ static RETRY_DELAY = 1e3;
72
+ static CONCURRENCY_LIMIT = 5;
73
+ // 并发控制
74
+ static async withConcurrencyLimit(operation, limit = this.CONCURRENCY_LIMIT) {
75
+ while (this.locks.size >= limit) {
76
+ await Promise.race(this.locks.values());
77
+ }
78
+ return operation();
79
+ }
80
+ // 文件操作包装器
81
+ static async withFileOp(filePath, operation) {
82
+ const key = filePath;
83
+ while (this.locks.has(key)) {
84
+ await this.locks.get(key);
85
+ }
86
+ const operationPromise = (async () => {
87
+ for (let i = 0; i < this.RETRY_COUNT; i++) {
88
+ try {
89
+ return await operation();
90
+ } catch (error) {
91
+ if (i === this.RETRY_COUNT - 1) throw error;
92
+ await new Promise((resolve) => setTimeout(resolve, this.RETRY_DELAY));
93
+ }
94
+ }
95
+ throw new Error("Operation failed after retries");
96
+ })();
97
+ this.locks.set(key, operationPromise);
98
+ try {
99
+ return await operationPromise;
100
+ } finally {
101
+ this.locks.delete(key);
102
+ }
103
+ }
104
+ // 事务处理
105
+ static async withTransaction(operations) {
106
+ const results = [];
107
+ const completed = /* @__PURE__ */ new Set();
108
+ try {
109
+ for (const { filePath, operation } of operations) {
110
+ const result = await this.withFileOp(filePath, operation);
111
+ results.push(result);
112
+ completed.add(filePath);
113
+ }
114
+ return results;
115
+ } catch (error) {
116
+ await Promise.all(
117
+ operations.filter(({ filePath }) => completed.has(filePath)).map(async ({ filePath, rollback }) => {
118
+ if (rollback) {
119
+ await this.withFileOp(filePath, rollback).catch(
120
+ (e) => logger.error(`Rollback failed for ${filePath}: ${e.message}`)
121
+ );
122
+ }
123
+ })
124
+ );
125
+ throw error;
126
+ }
127
+ }
128
+ // JSON读写
129
+ static async readJsonData(filePath) {
130
+ return this.withFileOp(filePath, async () => {
131
+ try {
132
+ const data = await fs.promises.readFile(filePath, "utf8");
133
+ return JSON.parse(data || "[]");
134
+ } catch (error) {
135
+ return [];
136
+ }
137
+ });
138
+ }
139
+ static async writeJsonData(filePath, data) {
140
+ const tmpPath = `${filePath}.tmp`;
141
+ await this.withFileOp(filePath, async () => {
142
+ await fs.promises.writeFile(tmpPath, JSON.stringify(data, null, 2));
143
+ await fs.promises.rename(tmpPath, filePath);
144
+ });
145
+ }
146
+ // 目录和文件操作
147
+ static async ensureDirectory(dir) {
148
+ await this.withConcurrencyLimit(async () => {
149
+ if (!fs.existsSync(dir)) {
150
+ await fs.promises.mkdir(dir, { recursive: true });
151
+ }
152
+ });
153
+ }
154
+ static async ensureJsonFile(filePath) {
155
+ await this.withFileOp(filePath, async () => {
156
+ if (!fs.existsSync(filePath)) {
157
+ await fs.promises.writeFile(filePath, "[]", "utf8");
158
+ }
159
+ });
160
+ }
161
+ // 媒体文件操作
162
+ static async saveMediaFile(filePath, data) {
163
+ await this.withConcurrencyLimit(async () => {
164
+ const dir = path.dirname(filePath);
165
+ await this.ensureDirectory(dir);
166
+ await this.withFileOp(
167
+ filePath,
168
+ () => fs.promises.writeFile(filePath, data)
169
+ );
170
+ });
171
+ }
172
+ static async deleteMediaFile(filePath) {
173
+ await this.withFileOp(filePath, async () => {
174
+ if (fs.existsSync(filePath)) {
175
+ await fs.promises.unlink(filePath);
176
+ }
177
+ });
178
+ }
179
+ };
180
+
181
+ // src/utils/idManager.ts
182
+ var fs2 = __toESM(require("fs"));
183
+ var path2 = __toESM(require("path"));
184
+ var import_koishi2 = require("koishi");
185
+ var logger2 = new import_koishi2.Logger("idManager");
186
+ var IdManager = class {
187
+ static {
188
+ __name(this, "IdManager");
189
+ }
190
+ deletedIds = /* @__PURE__ */ new Set();
191
+ maxId = 0;
192
+ initialized = false;
193
+ statusFilePath;
194
+ stats = {};
195
+ usedIds = /* @__PURE__ */ new Set();
196
+ constructor(baseDir) {
197
+ const caveDir = path2.join(baseDir, "data", "cave");
198
+ this.statusFilePath = path2.join(caveDir, "status.json");
199
+ }
200
+ async initialize(caveFilePath, pendingFilePath) {
201
+ if (this.initialized) return;
202
+ try {
203
+ const status = fs2.existsSync(this.statusFilePath) ? JSON.parse(await fs2.promises.readFile(this.statusFilePath, "utf8")) : {
204
+ deletedIds: [],
205
+ maxId: 0,
206
+ stats: {},
207
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
208
+ };
209
+ const [caveData, pendingData] = await Promise.all([
210
+ FileHandler.readJsonData(caveFilePath),
211
+ FileHandler.readJsonData(pendingFilePath)
212
+ ]);
213
+ this.usedIds.clear();
214
+ this.stats = {};
215
+ const conflicts = /* @__PURE__ */ new Map();
216
+ for (const data of [caveData, pendingData]) {
217
+ for (const item of data) {
218
+ if (this.usedIds.has(item.cave_id)) {
219
+ if (!conflicts.has(item.cave_id)) {
220
+ conflicts.set(item.cave_id, []);
221
+ }
222
+ conflicts.get(item.cave_id)?.push(item);
223
+ } else {
224
+ this.usedIds.add(item.cave_id);
225
+ if (data === caveData && item.contributor_number !== "10000") {
226
+ if (!this.stats[item.contributor_number]) {
227
+ this.stats[item.contributor_number] = [];
228
+ }
229
+ this.stats[item.contributor_number].push(item.cave_id);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ if (conflicts.size > 0) {
235
+ await this.handleConflicts(conflicts, caveFilePath, pendingFilePath, caveData, pendingData);
236
+ }
237
+ this.maxId = Math.max(
238
+ status.maxId || 0,
239
+ ...[...this.usedIds],
240
+ ...status.deletedIds || [],
241
+ 0
242
+ );
243
+ this.deletedIds = new Set(
244
+ status.deletedIds?.filter((id) => !this.usedIds.has(id)) || []
245
+ );
246
+ await this.saveStatus();
247
+ this.initialized = true;
248
+ logger2.success("ID Manager initialized");
249
+ } catch (error) {
250
+ this.initialized = false;
251
+ logger2.error(`ID Manager initialization failed: ${error.message}`);
252
+ throw error;
253
+ }
254
+ }
255
+ async handleConflicts(conflicts, caveFilePath, pendingFilePath, caveData, pendingData) {
256
+ logger2.warn(`Found ${conflicts.size} ID conflicts`);
257
+ let modified = false;
258
+ for (const items of conflicts.values()) {
259
+ items.slice(1).forEach((item) => {
260
+ let newId = this.maxId + 1;
261
+ while (this.usedIds.has(newId)) {
262
+ newId++;
263
+ }
264
+ logger2.info(`Reassigning ID: ${item.cave_id} -> ${newId}`);
265
+ item.cave_id = newId;
266
+ this.usedIds.add(newId);
267
+ this.maxId = Math.max(this.maxId, newId);
268
+ modified = true;
269
+ });
270
+ }
271
+ if (modified) {
272
+ await Promise.all([
273
+ FileHandler.writeJsonData(caveFilePath, caveData),
274
+ FileHandler.writeJsonData(pendingFilePath, pendingData)
275
+ ]);
276
+ logger2.success("ID conflicts resolved");
277
+ }
278
+ }
279
+ getNextId() {
280
+ if (!this.initialized) {
281
+ throw new Error("IdManager not initialized");
282
+ }
283
+ let nextId;
284
+ if (this.deletedIds.size === 0) {
285
+ nextId = ++this.maxId;
286
+ } else {
287
+ nextId = Math.min(...Array.from(this.deletedIds));
288
+ this.deletedIds.delete(nextId);
289
+ }
290
+ while (this.usedIds.has(nextId)) {
291
+ nextId = ++this.maxId;
292
+ }
293
+ this.usedIds.add(nextId);
294
+ this.saveStatus().catch(
295
+ (err) => logger2.error(`Failed to save status after getNextId: ${err.message}`)
296
+ );
297
+ return nextId;
298
+ }
299
+ async markDeleted(id) {
300
+ if (!this.initialized) {
301
+ throw new Error("IdManager not initialized");
302
+ }
303
+ this.deletedIds.add(id);
304
+ this.usedIds.delete(id);
305
+ const maxUsedId = Math.max(...Array.from(this.usedIds), 0);
306
+ const maxDeletedId = Math.max(...Array.from(this.deletedIds), 0);
307
+ this.maxId = Math.max(maxUsedId, maxDeletedId);
308
+ await this.saveStatus();
309
+ }
310
+ async addStat(contributorNumber, caveId) {
311
+ if (contributorNumber === "10000") return;
312
+ if (!this.stats[contributorNumber]) {
313
+ this.stats[contributorNumber] = [];
314
+ }
315
+ this.stats[contributorNumber].push(caveId);
316
+ await this.saveStatus();
317
+ }
318
+ async removeStat(contributorNumber, caveId) {
319
+ if (this.stats[contributorNumber]) {
320
+ this.stats[contributorNumber] = this.stats[contributorNumber].filter((id) => id !== caveId);
321
+ if (this.stats[contributorNumber].length === 0) {
322
+ delete this.stats[contributorNumber];
323
+ }
324
+ await this.saveStatus();
325
+ }
326
+ }
327
+ getStats() {
328
+ return this.stats;
329
+ }
330
+ async saveStatus() {
331
+ try {
332
+ const status = {
333
+ deletedIds: Array.from(this.deletedIds).sort((a, b) => a - b),
334
+ maxId: this.maxId,
335
+ stats: this.stats,
336
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
337
+ };
338
+ const tmpPath = `${this.statusFilePath}.tmp`;
339
+ await fs2.promises.writeFile(tmpPath, JSON.stringify(status, null, 2), "utf8");
340
+ await fs2.promises.rename(tmpPath, this.statusFilePath);
341
+ } catch (error) {
342
+ logger2.error(`Status save failed: ${error.message}`);
343
+ throw error;
344
+ }
345
+ }
346
+ };
347
+
348
+ // src/utils/HashStorage.ts
349
+ var import_koishi3 = require("koishi");
350
+ var fs3 = __toESM(require("fs"));
351
+ var path3 = __toESM(require("path"));
352
+
353
+ // src/utils/ImageHasher.ts
354
+ var import_sharp = __toESM(require("sharp"));
355
+ var ImageHasher = class {
356
+ static {
357
+ __name(this, "ImageHasher");
358
+ }
359
+ /**
360
+ * 计算图片哈希值
361
+ */
362
+ static async calculateHash(imageBuffer) {
363
+ const { data } = await (0, import_sharp.default)(imageBuffer).grayscale().resize(32, 32, { fit: "fill" }).raw().toBuffer({ resolveWithObject: true });
364
+ const waveletMatrix = this.haarWaveletTransform(data, 32);
365
+ const features = this.extractFeatures(waveletMatrix, 32);
366
+ const mean = features.reduce((sum, val) => sum + val, 0) / features.length;
367
+ const binaryHash = features.map((val) => val > mean ? "1" : "0").join("");
368
+ return this.binaryToHex(binaryHash);
369
+ }
370
+ static binaryToHex(binary) {
371
+ const hex = [];
372
+ for (let i = 0; i < binary.length; i += 4) {
373
+ const chunk = binary.slice(i, i + 4);
374
+ hex.push(parseInt(chunk, 2).toString(16));
375
+ }
376
+ return hex.join("");
377
+ }
378
+ static hexToBinary(hex) {
379
+ let binary = "";
380
+ for (const char of hex) {
381
+ const bin = parseInt(char, 16).toString(2).padStart(4, "0");
382
+ binary += bin;
383
+ }
384
+ return binary;
385
+ }
386
+ static haarWaveletTransform(data, size) {
387
+ const matrix = Array(size).fill(0).map(() => Array(size).fill(0));
388
+ for (let i = 0; i < size; i++) {
389
+ for (let j = 0; j < size; j++) {
390
+ matrix[i][j] = data[i * size + j];
391
+ }
392
+ }
393
+ for (let i = 0; i < size; i++) {
394
+ this.haarTransform1D(matrix[i]);
395
+ }
396
+ for (let j = 0; j < size; j++) {
397
+ const col = matrix.map((row) => row[j]);
398
+ this.haarTransform1D(col);
399
+ for (let i = 0; i < size; i++) {
400
+ matrix[i][j] = col[i];
401
+ }
402
+ }
403
+ return matrix;
404
+ }
405
+ static haarTransform1D(arr) {
406
+ const len = arr.length;
407
+ const temp = new Array(len).fill(0);
408
+ for (let i = 0; i < len; i += 2) {
409
+ if (i + 1 < len) {
410
+ temp[i / 2] = (arr[i] + arr[i + 1]) / 2;
411
+ temp[len / 2 + i / 2] = (arr[i] - arr[i + 1]) / 2;
412
+ } else {
413
+ temp[i / 2] = arr[i];
414
+ }
415
+ }
416
+ for (let i = 0; i < len; i++) {
417
+ arr[i] = temp[i];
418
+ }
419
+ }
420
+ static extractFeatures(matrix, size) {
421
+ const features = [];
422
+ const featureSize = 8;
423
+ for (let i = 0; i < featureSize; i++) {
424
+ for (let j = 0; j < featureSize; j++) {
425
+ features.push(matrix[i][j]);
426
+ }
427
+ }
428
+ return features;
429
+ }
430
+ /**
431
+ * 计算两个hash值的汉明距离
432
+ */
433
+ static calculateDistance(hash1, hash2) {
434
+ if (hash1.length !== hash2.length) {
435
+ throw new Error("Hash lengths must be equal");
436
+ }
437
+ const bin1 = this.hexToBinary(hash1);
438
+ const bin2 = this.hexToBinary(hash2);
439
+ let distance = 0;
440
+ for (let i = 0; i < bin1.length; i++) {
441
+ if (bin1[i] !== bin2[i]) distance++;
442
+ }
443
+ return distance;
444
+ }
445
+ /**
446
+ * 计算图片相似度(0-1)
447
+ */
448
+ static calculateSimilarity(hash1, hash2) {
449
+ const distance = this.calculateDistance(hash1, hash2);
450
+ return (64 - distance) / 64;
451
+ }
452
+ /**
453
+ * 批量比较图片相似度
454
+ */
455
+ static batchCompareSimilarity(newHash, existingHashes) {
456
+ return existingHashes.map((hash) => this.calculateSimilarity(newHash, hash));
457
+ }
458
+ };
459
+
460
+ // src/utils/HashStorage.ts
461
+ var import_util = require("util");
462
+ var logger3 = new import_koishi3.Logger("HashStorage");
463
+ var readFileAsync = (0, import_util.promisify)(fs3.readFile);
464
+ var HashStorage = class _HashStorage {
465
+ constructor(caveDir) {
466
+ this.caveDir = caveDir;
467
+ }
468
+ static {
469
+ __name(this, "HashStorage");
470
+ }
471
+ // 哈希数据文件名
472
+ static HASH_FILE = "hash.json";
473
+ // 洞穴数据文件名
474
+ static CAVE_FILE = "cave.json";
475
+ // 批处理大小
476
+ static BATCH_SIZE = 50;
477
+ // 存储洞穴ID到图片哈希值的映射
478
+ hashes = /* @__PURE__ */ new Map();
479
+ // 初始化状态标志
480
+ initialized = false;
481
+ get filePath() {
482
+ return path3.join(this.caveDir, _HashStorage.HASH_FILE);
483
+ }
484
+ get resourceDir() {
485
+ return path3.join(this.caveDir, "resources");
486
+ }
487
+ get caveFilePath() {
488
+ return path3.join(this.caveDir, _HashStorage.CAVE_FILE);
489
+ }
490
+ async initialize() {
491
+ if (this.initialized) return;
492
+ try {
493
+ const hashData = await FileHandler.readJsonData(this.filePath).then((data) => data[0]).catch(() => null);
494
+ if (!hashData?.hashes || Object.keys(hashData.hashes).length === 0) {
495
+ this.hashes.clear();
496
+ await this.buildInitialHashes();
497
+ } else {
498
+ this.hashes = new Map(
499
+ Object.entries(hashData.hashes).map(([k, v]) => [Number(k), v])
500
+ );
501
+ await this.updateMissingHashes();
502
+ }
503
+ this.initialized = true;
504
+ } catch (error) {
505
+ logger3.error(`Initialization failed: ${error.message}`);
506
+ this.initialized = false;
507
+ throw error;
508
+ }
509
+ }
510
+ async getStatus() {
511
+ if (!this.initialized) await this.initialize();
512
+ return {
513
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
514
+ entries: Array.from(this.hashes.entries()).map(([caveId, hashes]) => ({
515
+ caveId,
516
+ hashes
517
+ }))
518
+ };
519
+ }
520
+ async updateCaveHash(caveId, imgBuffers) {
521
+ if (!this.initialized) await this.initialize();
522
+ try {
523
+ if (imgBuffers?.length) {
524
+ const hashes = await Promise.all(
525
+ imgBuffers.map((buffer) => ImageHasher.calculateHash(buffer))
526
+ );
527
+ this.hashes.set(caveId, hashes);
528
+ } else {
529
+ this.hashes.delete(caveId);
530
+ }
531
+ await this.saveHashes();
532
+ } catch (error) {
533
+ logger3.error(`Failed to update hash (cave ${caveId}): ${error.message}`);
534
+ }
535
+ }
536
+ async updateAllCaves(isInitialBuild = false) {
537
+ if (!this.initialized && !isInitialBuild) {
538
+ await this.initialize();
539
+ return;
540
+ }
541
+ try {
542
+ logger3.info("Starting full hash update...");
543
+ const caveData = await this.loadCaveData();
544
+ const cavesWithImages = caveData.filter(
545
+ (cave) => cave.elements?.some((el) => el.type === "img" && el.file)
546
+ );
547
+ this.hashes.clear();
548
+ let processedCount = 0;
549
+ const totalImages = cavesWithImages.length;
550
+ const processCave = /* @__PURE__ */ __name(async (cave) => {
551
+ const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
552
+ if (imgElements.length === 0) return;
553
+ try {
554
+ const hashes = await Promise.all(
555
+ imgElements.map(async (imgElement) => {
556
+ const filePath = path3.join(this.resourceDir, imgElement.file);
557
+ if (!fs3.existsSync(filePath)) {
558
+ logger3.warn(`Image file not found: ${filePath}`);
559
+ return null;
560
+ }
561
+ const imgBuffer = await readFileAsync(filePath);
562
+ return await ImageHasher.calculateHash(imgBuffer);
563
+ })
564
+ );
565
+ const validHashes = hashes.filter((hash) => hash !== null);
566
+ if (validHashes.length > 0) {
567
+ this.hashes.set(cave.cave_id, validHashes);
568
+ processedCount++;
569
+ if (processedCount % 100 === 0) {
570
+ logger3.info(`Progress: ${processedCount}/${totalImages}`);
571
+ }
572
+ }
573
+ } catch (error) {
574
+ logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
575
+ }
576
+ }, "processCave");
577
+ await this.processBatch(cavesWithImages, processCave);
578
+ await this.saveHashes();
579
+ logger3.success(`Update completed. Processed ${processedCount}/${totalImages} images`);
580
+ } catch (error) {
581
+ logger3.error(`Full update failed: ${error.message}`);
582
+ throw error;
583
+ }
584
+ }
585
+ async findDuplicates(imgBuffers, threshold) {
586
+ if (!this.initialized) await this.initialize();
587
+ const inputHashes = await Promise.all(
588
+ imgBuffers.map((buffer) => ImageHasher.calculateHash(buffer))
589
+ );
590
+ const existingHashes = Array.from(this.hashes.entries());
591
+ return Promise.all(
592
+ inputHashes.map(async (hash, index) => {
593
+ try {
594
+ let maxSimilarity = 0;
595
+ let matchedCaveId = null;
596
+ for (const [caveId, hashes] of existingHashes) {
597
+ for (const existingHash of hashes) {
598
+ const similarity = ImageHasher.calculateSimilarity(hash, existingHash);
599
+ if (similarity >= threshold && similarity > maxSimilarity) {
600
+ maxSimilarity = similarity;
601
+ matchedCaveId = caveId;
602
+ if (Math.abs(similarity - 1) < Number.EPSILON) break;
603
+ }
604
+ }
605
+ if (Math.abs(maxSimilarity - 1) < Number.EPSILON) break;
606
+ }
607
+ return matchedCaveId ? {
608
+ index,
609
+ caveId: matchedCaveId,
610
+ similarity: maxSimilarity
611
+ } : null;
612
+ } catch (error) {
613
+ logger3.warn(`处理图片 ${index} 失败: ${error.message}`);
614
+ return null;
615
+ }
616
+ })
617
+ );
618
+ }
619
+ async loadCaveData() {
620
+ const data = await FileHandler.readJsonData(this.caveFilePath);
621
+ return Array.isArray(data) ? data.flat() : [];
622
+ }
623
+ async saveHashes() {
624
+ const data = {
625
+ hashes: Object.fromEntries(this.hashes),
626
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
627
+ };
628
+ await FileHandler.writeJsonData(this.filePath, [data]);
629
+ }
630
+ async buildInitialHashes() {
631
+ const caveData = await this.loadCaveData();
632
+ let processedImageCount = 0;
633
+ const totalImages = caveData.reduce((sum, cave) => sum + (cave.elements?.filter((el) => el.type === "img" && el.file).length || 0), 0);
634
+ logger3.info(`Building hash data for ${totalImages} images...`);
635
+ for (const cave of caveData) {
636
+ const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
637
+ if (imgElements.length === 0) continue;
638
+ try {
639
+ const hashes = await Promise.all(
640
+ imgElements.map(async (imgElement) => {
641
+ const filePath = path3.join(this.resourceDir, imgElement.file);
642
+ if (!fs3.existsSync(filePath)) {
643
+ logger3.warn(`Image not found: ${filePath}`);
644
+ return null;
645
+ }
646
+ const imgBuffer = await fs3.promises.readFile(filePath);
647
+ const hash = await ImageHasher.calculateHash(imgBuffer);
648
+ processedImageCount++;
649
+ if (processedImageCount % 100 === 0) {
650
+ logger3.info(`Progress: ${processedImageCount}/${totalImages} images`);
651
+ }
652
+ return hash;
653
+ })
654
+ );
655
+ const validHashes = hashes.filter((hash) => hash !== null);
656
+ if (validHashes.length > 0) {
657
+ this.hashes.set(cave.cave_id, validHashes);
658
+ }
659
+ } catch (error) {
660
+ logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
661
+ }
662
+ }
663
+ await this.saveHashes();
664
+ logger3.success(`Build completed. Processed ${processedImageCount}/${totalImages} images`);
665
+ }
666
+ async updateMissingHashes() {
667
+ const caveData = await this.loadCaveData();
668
+ let updatedCount = 0;
669
+ for (const cave of caveData) {
670
+ if (this.hashes.has(cave.cave_id)) continue;
671
+ const imgElements = cave.elements?.filter((el) => el.type === "img" && el.file) || [];
672
+ if (imgElements.length === 0) continue;
673
+ try {
674
+ const hashes = await Promise.all(
675
+ imgElements.map(async (imgElement) => {
676
+ const filePath = path3.join(this.resourceDir, imgElement.file);
677
+ if (!fs3.existsSync(filePath)) {
678
+ return null;
679
+ }
680
+ const imgBuffer = await fs3.promises.readFile(filePath);
681
+ return ImageHasher.calculateHash(imgBuffer);
682
+ })
683
+ );
684
+ const validHashes = hashes.filter((hash) => hash !== null);
685
+ if (validHashes.length > 0) {
686
+ this.hashes.set(cave.cave_id, validHashes);
687
+ updatedCount++;
688
+ }
689
+ } catch (error) {
690
+ logger3.error(`Failed to process cave ${cave.cave_id}: ${error.message}`);
691
+ }
692
+ }
693
+ if (updatedCount > 0) {
694
+ await this.saveHashes();
695
+ logger3.info(`Updated ${updatedCount} new hashes`);
696
+ }
697
+ }
698
+ async processBatch(items, processor, batchSize = _HashStorage.BATCH_SIZE) {
699
+ for (let i = 0; i < items.length; i += batchSize) {
700
+ const batch = items.slice(i, i + batchSize);
701
+ await Promise.all(
702
+ batch.map(async (item) => {
703
+ try {
704
+ await processor(item);
705
+ } catch (error) {
706
+ logger3.error(`Batch processing error: ${error.message}`);
707
+ }
708
+ })
709
+ );
710
+ }
711
+ }
712
+ };
713
+
714
+ // src/index.ts
59
715
  var name = "best-cave";
60
716
  var inject = ["database"];
61
- var Config = import_koishi.Schema.object({
62
- manager: import_koishi.Schema.array(import_koishi.Schema.string()).required(),
63
- blacklist: import_koishi.Schema.array(import_koishi.Schema.string()).default([]),
64
- whitelist: import_koishi.Schema.array(import_koishi.Schema.string()).default([]),
65
- number: import_koishi.Schema.number().default(60),
66
- enableAudit: import_koishi.Schema.boolean().default(false),
67
- allowVideo: import_koishi.Schema.boolean().default(true),
68
- videoMaxSize: import_koishi.Schema.number().default(16),
69
- imageMaxSize: import_koishi.Schema.number().default(4),
70
- enablePagination: import_koishi.Schema.boolean().default(false),
71
- itemsPerPage: import_koishi.Schema.number().default(10)
717
+ var Config = import_koishi4.Schema.object({
718
+ manager: import_koishi4.Schema.array(import_koishi4.Schema.string()).required(),
719
+ // 管理员用户ID
720
+ number: import_koishi4.Schema.number().default(60),
721
+ // 冷却时间()
722
+ enableAudit: import_koishi4.Schema.boolean().default(false),
723
+ // 启用审核
724
+ imageMaxSize: import_koishi4.Schema.number().default(4),
725
+ // 图片大小限制(MB)
726
+ duplicateThreshold: import_koishi4.Schema.number().default(0.8),
727
+ // 查重阈值(0-1)
728
+ allowVideo: import_koishi4.Schema.boolean().default(true),
729
+ // 允许视频
730
+ videoMaxSize: import_koishi4.Schema.number().default(16),
731
+ // 视频大小限制(MB)
732
+ enablePagination: import_koishi4.Schema.boolean().default(false),
733
+ // 启用分页
734
+ itemsPerPage: import_koishi4.Schema.number().default(10),
735
+ // 每页条数
736
+ blacklist: import_koishi4.Schema.array(import_koishi4.Schema.string()).default([]),
737
+ // 黑名单
738
+ whitelist: import_koishi4.Schema.array(import_koishi4.Schema.string()).default([])
739
+ // 白名单
72
740
  }).i18n({
73
741
  "zh-CN": require_zh_CN()._config,
74
742
  "en-US": require_en_US()._config
@@ -76,28 +744,27 @@ var Config = import_koishi.Schema.object({
76
744
  async function apply(ctx, config) {
77
745
  ctx.i18n.define("zh-CN", require_zh_CN());
78
746
  ctx.i18n.define("en-US", require_en_US());
79
- const dataDir = path.join(ctx.baseDir, "data");
80
- const caveDir = path.join(dataDir, "cave");
81
- const caveFilePath = path.join(caveDir, "cave.json");
82
- const resourceDir = path.join(caveDir, "resources");
83
- const pendingFilePath = path.join(caveDir, "pending.json");
747
+ const dataDir = path4.join(ctx.baseDir, "data");
748
+ const caveDir = path4.join(dataDir, "cave");
84
749
  await FileHandler.ensureDirectory(dataDir);
85
750
  await FileHandler.ensureDirectory(caveDir);
86
- await FileHandler.ensureDirectory(resourceDir);
87
- await FileHandler.ensureJsonFile(caveFilePath);
88
- await FileHandler.ensureJsonFile(pendingFilePath);
751
+ await FileHandler.ensureDirectory(path4.join(caveDir, "resources"));
752
+ await FileHandler.ensureJsonFile(path4.join(caveDir, "cave.json"));
753
+ await FileHandler.ensureJsonFile(path4.join(caveDir, "pending.json"));
754
+ await FileHandler.ensureJsonFile(path4.join(caveDir, "hash.json"));
755
+ const idManager = new IdManager(ctx.baseDir);
756
+ const hashStorage = new HashStorage(caveDir);
757
+ await Promise.all([
758
+ idManager.initialize(path4.join(caveDir, "cave.json"), path4.join(caveDir, "pending.json")),
759
+ hashStorage.initialize()
760
+ ]);
89
761
  const lastUsed = /* @__PURE__ */ new Map();
90
- async function processList(caveFilePath2, session, content, options, config2) {
91
- const caveData = await CacheManager.getCaveData(caveFilePath2, session);
92
- const caveDir2 = path.dirname(caveFilePath2);
93
- const stats = {};
94
- for (const cave of caveData) {
95
- if (cave.contributor_number === "10000") continue;
96
- if (!stats[cave.contributor_number]) stats[cave.contributor_number] = [];
97
- stats[cave.contributor_number].push(cave.cave_id);
762
+ async function processList(session, config2, userId, pageNum = 1) {
763
+ const stats = idManager.getStats();
764
+ if (userId && userId in stats) {
765
+ const ids = stats[userId];
766
+ return session.text("commands.cave.list.totalItems", [userId, ids.length]) + "\n" + session.text("commands.cave.list.idsLine", [ids.join(",")]);
98
767
  }
99
- const statFilePath = path.join(caveDir2, "stat.json");
100
- fs.writeFileSync(statFilePath, JSON.stringify(stats, null, 2), "utf8");
101
768
  const lines = Object.entries(stats).map(([cid, ids]) => {
102
769
  return session.text("commands.cave.list.totalItems", [cid, ids.length]) + "\n" + session.text("commands.cave.list.idsLine", [ids.join(",")]);
103
770
  });
@@ -105,10 +772,7 @@ async function apply(ctx, config) {
105
772
  if (config2.enablePagination) {
106
773
  const itemsPerPage = config2.itemsPerPage;
107
774
  const totalPages = Math.max(1, Math.ceil(lines.length / itemsPerPage));
108
- let query = (content[0] || String(options.l) || "").trim();
109
- let pageNum = parseInt(query, 10);
110
- if (isNaN(pageNum) || pageNum < 1) pageNum = 1;
111
- if (pageNum > totalPages) pageNum = totalPages;
775
+ pageNum = Math.min(Math.max(1, pageNum), totalPages);
112
776
  const start = (pageNum - 1) * itemsPerPage;
113
777
  const paginatedLines = lines.slice(start, start + itemsPerPage);
114
778
  return session.text("commands.cave.list.header", [totalSubmissions]) + "\n" + paginatedLines.join("\n") + "\n" + session.text("commands.cave.list.pageInfo", [pageNum, totalPages]);
@@ -117,595 +781,556 @@ async function apply(ctx, config) {
117
781
  }
118
782
  }
119
783
  __name(processList, "processList");
120
- async function processAudit(ctx2, pendingFilePath2, caveFilePath2, resourceDir2, session, options, content) {
121
- const pendingData = await CacheManager.getPendingData(pendingFilePath2, session);
784
+ async function processAudit(pendingFilePath, caveFilePath, resourceDir, session, options, content) {
785
+ const pendingData = await FileHandler.readJsonData(pendingFilePath);
122
786
  const isApprove = Boolean(options.p);
123
787
  if (options.p === true && content[0] === "all" || options.d === true && content[0] === "all") {
124
- return await handleAudit(ctx2, pendingData, isApprove, caveFilePath2, resourceDir2, pendingFilePath2, session);
788
+ return await handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session);
125
789
  }
126
790
  const id = parseInt(content[0] || (typeof options.p === "string" ? options.p : "") || (typeof options.d === "string" ? options.d : ""));
127
791
  if (isNaN(id)) {
128
792
  return sendMessage(session, "commands.cave.error.invalidId", [], true);
129
793
  }
130
- return sendMessage(session, await handleAudit(ctx2, pendingData, isApprove, caveFilePath2, resourceDir2, pendingFilePath2, session, id), [], true);
794
+ return sendMessage(session, await handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, id), [], true);
131
795
  }
132
796
  __name(processAudit, "processAudit");
133
- async function processView(caveFilePath2, resourceDir2, session, options, content) {
797
+ async function processView(caveFilePath, resourceDir, session, options, content) {
134
798
  const caveId = parseInt(content[0] || (typeof options.g === "string" ? options.g : ""));
135
799
  if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
136
- const data = await CacheManager.getCaveData(caveFilePath2, session);
800
+ const data = await FileHandler.readJsonData(caveFilePath);
137
801
  const cave = data.find((item) => item.cave_id === caveId);
138
802
  if (!cave) return sendMessage(session, "commands.cave.error.notFound", [], true);
139
- const caveContent = await buildMessage(cave, resourceDir2, session);
140
- return caveContent;
803
+ return buildMessage(cave, resourceDir, session);
141
804
  }
142
805
  __name(processView, "processView");
143
- async function processRandom(caveFilePath2, resourceDir2, session, config2, lastUsed2) {
144
- try {
145
- const data = await CacheManager.getCaveData(caveFilePath2, session);
146
- if (data.length === 0) {
147
- return sendMessage(session, "commands.cave.error.noCave", [], true);
148
- }
149
- const guildId = session.guildId;
150
- const now = Date.now();
151
- const lastCall = lastUsed2.get(guildId) || 0;
152
- const isManager = config2.manager.includes(session.userId);
153
- if (!isManager && now - lastCall < config2.number * 1e3) {
154
- const waitTime = Math.ceil((config2.number * 1e3 - (now - lastCall)) / 1e3);
155
- return sendMessage(session, "commands.cave.message.cooldown", [waitTime], true);
156
- }
157
- if (!isManager) lastUsed2.set(guildId, now);
158
- const cave = (() => {
159
- const validCaves = data.filter((cave2) => cave2.elements && cave2.elements.length > 0);
160
- if (!validCaves.length) return void 0;
161
- const randomIndex = Math.floor(Math.random() * validCaves.length);
162
- return validCaves[randomIndex];
163
- })();
164
- return cave ? buildMessage(cave, resourceDir2, session) : sendMessage(session, "commands.cave.error.getCave", [], true);
165
- } catch (error) {
166
- return sendMessage(session, "commands.cave.error.commandProcess", [error.message], true);
806
+ async function processRandom(caveFilePath, resourceDir, session) {
807
+ const data = await FileHandler.readJsonData(caveFilePath);
808
+ if (data.length === 0) {
809
+ return sendMessage(session, "commands.cave.error.noCave", [], true);
167
810
  }
811
+ const cave = (() => {
812
+ const validCaves = data.filter((cave2) => cave2.elements && cave2.elements.length > 0);
813
+ if (!validCaves.length) return void 0;
814
+ const randomIndex = Math.floor(Math.random() * validCaves.length);
815
+ return validCaves[randomIndex];
816
+ })();
817
+ return cave ? buildMessage(cave, resourceDir, session) : sendMessage(session, "commands.cave.error.getCave", [], true);
168
818
  }
169
819
  __name(processRandom, "processRandom");
170
- async function processDelete(caveFilePath2, resourceDir2, pendingFilePath2, session, config2, options, content) {
820
+ async function processDelete(caveFilePath, resourceDir, pendingFilePath, session, config2, options, content) {
171
821
  const caveId = parseInt(content[0] || (typeof options.r === "string" ? options.r : ""));
172
822
  if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
173
- const data = await CacheManager.getCaveData(caveFilePath2, session);
174
- const pendingData = await CacheManager.getPendingData(pendingFilePath2, session);
175
- const index = data.findIndex((item) => item.cave_id === caveId);
176
- const pendingIndex = pendingData.findIndex((item) => item.cave_id === caveId);
177
- if (index === -1 && pendingIndex === -1) return sendMessage(session, "commands.cave.error.notFound", [], true);
178
- let targetCave;
179
- let isPending = false;
180
- if (index !== -1) {
181
- targetCave = data[index];
182
- } else {
183
- targetCave = pendingData[pendingIndex];
184
- isPending = true;
823
+ const data = await FileHandler.readJsonData(caveFilePath);
824
+ const pendingData = await FileHandler.readJsonData(pendingFilePath);
825
+ const targetInData = data.find((item) => item.cave_id === caveId);
826
+ const targetInPending = pendingData.find((item) => item.cave_id === caveId);
827
+ if (!targetInData && !targetInPending) {
828
+ return sendMessage(session, "commands.cave.error.notFound", [], true);
185
829
  }
830
+ const targetCave = targetInData || targetInPending;
831
+ const isPending = !targetInData;
186
832
  if (targetCave.contributor_number !== session.userId && !config2.manager.includes(session.userId)) {
187
833
  return sendMessage(session, "commands.cave.remove.noPermission", [], true);
188
834
  }
189
- const caveContent = await buildMessage(targetCave, resourceDir2, session);
835
+ const caveContent = await buildMessage(targetCave, resourceDir, session);
190
836
  if (targetCave.elements) {
837
+ const hashStorage2 = new HashStorage(caveDir);
838
+ await hashStorage2.initialize();
839
+ await hashStorage2.updateCaveHash(caveId);
191
840
  for (const element of targetCave.elements) {
192
841
  if ((element.type === "img" || element.type === "video") && element.file) {
193
- const fullPath = path.join(resourceDir2, element.file);
194
- if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
842
+ const fullPath = path4.join(resourceDir, element.file);
843
+ if (fs4.existsSync(fullPath)) {
844
+ await fs4.promises.unlink(fullPath);
845
+ }
195
846
  }
196
847
  }
197
848
  }
198
849
  if (isPending) {
199
- pendingData.splice(pendingIndex, 1);
200
- await FileHandler.writeJsonData(pendingFilePath2, pendingData, session);
201
- const deleteStatus = isPending ? session.text("commands.cave.remove.deletePending") : "";
202
- const deleteMessage = session.text("commands.cave.remove.deleted");
203
- return `${deleteMessage}${deleteStatus}
204
- ${caveContent}`;
850
+ const newPendingData = pendingData.filter((item) => item.cave_id !== caveId);
851
+ await FileHandler.writeJsonData(pendingFilePath, newPendingData);
205
852
  } else {
206
- data.splice(index, 1);
207
- await FileHandler.writeJsonData(caveFilePath2, data, session);
208
- const deleteStatus = isPending ? session.text("commands.cave.remove.deletePending") : "";
209
- const deleteMessage = session.text("commands.cave.remove.deleted");
210
- return `${deleteMessage}${deleteStatus}${caveContent}`;
853
+ const newData = data.filter((item) => item.cave_id !== caveId);
854
+ await FileHandler.writeJsonData(caveFilePath, newData);
855
+ await idManager.removeStat(targetCave.contributor_number, caveId);
211
856
  }
857
+ await idManager.markDeleted(caveId);
858
+ const deleteStatus = isPending ? session.text("commands.cave.remove.deletePending") : "";
859
+ const deleteMessage = session.text("commands.cave.remove.deleted");
860
+ return `${deleteMessage}${deleteStatus}${caveContent}`;
212
861
  }
213
862
  __name(processDelete, "processDelete");
214
- async function processAdd(ctx2, config2, caveFilePath2, resourceDir2, pendingFilePath2, session, content) {
863
+ async function processAdd(ctx2, config2, caveFilePath, resourceDir, pendingFilePath, session, content) {
215
864
  try {
216
- let inputParts = [];
217
- if (content.length > 0) {
218
- inputParts = content;
219
- }
220
- if (!inputParts.length) {
865
+ const inputContent = content.length > 0 ? content.join("\n") : await (async () => {
221
866
  await sendMessage(session, "commands.cave.add.noContent", [], true);
222
867
  const reply = await session.prompt({ timeout: 6e4 });
223
- if (!reply || reply.trim() === "") {
224
- return sendMessage(session, "commands.cave.add.operationTimeout", [], true);
225
- }
226
- inputParts = [reply];
227
- }
228
- const combinedInput = inputParts.join("\n");
229
- if (combinedInput.includes("/app/.config/QQ/")) {
868
+ if (!reply) throw new Error(session.text("commands.cave.add.operationTimeout"));
869
+ return reply;
870
+ })();
871
+ const caveId = await idManager.getNextId();
872
+ if (inputContent.includes("/app/.config/QQ/")) {
230
873
  return sendMessage(session, "commands.cave.add.localFileNotAllowed", [], true);
231
874
  }
232
- let { imageUrls, imageElements, videoUrls, videoElements, textParts } = await extractMediaContent(combinedInput);
875
+ const bypassAudit = config2.whitelist.includes(session.userId) || config2.whitelist.includes(session.guildId) || config2.whitelist.includes(session.channelId);
876
+ const { imageUrls, imageElements, videoUrls, videoElements, textParts } = await extractMediaContent(inputContent, config2, session);
233
877
  if (videoUrls.length > 0 && !config2.allowVideo) {
234
878
  return sendMessage(session, "commands.cave.add.videoDisabled", [], true);
235
879
  }
236
- const pendingData = await CacheManager.getPendingData(pendingFilePath2, session);
237
- const data = await CacheManager.getCaveData(caveFilePath2, session);
238
- const usedIds = /* @__PURE__ */ new Set([...data.map((item) => item.cave_id), ...pendingData.map((item) => item.cave_id)]);
239
- let caveId = 1;
240
- while (usedIds.has(caveId)) {
241
- caveId++;
242
- }
243
- let savedImages = [];
244
- if (imageUrls.length > 0) {
245
- try {
246
- const imageFileNames = imageElements.map((el) => el.fileName);
247
- const imageFileSizes = imageElements.map((el) => el.fileSize);
248
- savedImages = await saveMedia(
249
- imageUrls,
250
- imageFileNames,
251
- imageFileSizes,
252
- resourceDir2,
253
- caveId,
254
- config2,
255
- ctx2,
256
- "img",
257
- session
258
- );
259
- } catch (error) {
260
- return sendMessage(session, "commands.cave.error.uploadImageFailed", [], true);
261
- }
262
- }
263
- let savedVideos = [];
264
- if (videoUrls.length > 0) {
265
- try {
266
- const videoFileNames = videoElements.map((el) => el.fileName);
267
- const videoFileSizes = videoElements.map((el) => el.fileSize);
268
- savedVideos = await saveMedia(
269
- videoUrls,
270
- videoFileNames,
271
- videoFileSizes,
272
- resourceDir2,
273
- caveId,
274
- config2,
275
- ctx2,
276
- "video",
277
- session
278
- );
279
- } catch (error) {
280
- return sendMessage(session, "commands.cave.error.uploadVideoFailed", [], true);
281
- }
282
- }
283
- const elements = [];
284
- elements.push(...textParts);
285
- savedImages.forEach((file, idx) => {
286
- if (imageElements[idx]) {
287
- elements.push({ ...imageElements[idx], type: "img", file });
288
- }
289
- });
290
- savedVideos.forEach((file, idx) => {
291
- if (videoElements[idx]) {
292
- elements.push({ ...videoElements[idx], type: "video", file });
293
- }
294
- });
295
- elements.sort((a, b) => a.index - b.index);
296
- let contributorName = session.username;
297
- if (ctx2.database) {
298
- try {
299
- const userInfo = await ctx2.database.getUser(session.platform, session.userId);
300
- contributorName = userInfo?.nickname || session.username;
301
- } catch (error) {
302
- return sendMessage(
303
- session,
304
- "commands.cave.error.userInfo",
305
- [error.message],
306
- true
307
- );
308
- }
309
- }
880
+ const [savedImages, savedVideos] = await Promise.all([
881
+ imageUrls.length > 0 ? saveMedia(
882
+ imageUrls,
883
+ imageElements.map((el) => el.fileName),
884
+ resourceDir,
885
+ caveId,
886
+ "img",
887
+ config2,
888
+ ctx2,
889
+ session
890
+ ) : [],
891
+ videoUrls.length > 0 ? saveMedia(
892
+ videoUrls,
893
+ videoElements.map((el) => el.fileName),
894
+ resourceDir,
895
+ caveId,
896
+ "video",
897
+ config2,
898
+ ctx2,
899
+ session
900
+ ) : []
901
+ ]);
310
902
  const newCave = {
311
903
  cave_id: caveId,
312
- elements: cleanElementsForSave(elements, true),
904
+ elements: [
905
+ ...textParts,
906
+ ...imageElements.map((el, idx) => ({
907
+ ...el,
908
+ file: savedImages[idx],
909
+ // 保持原始文本和图片的相对位置
910
+ index: el.index
911
+ }))
912
+ ].sort((a, b) => a.index - a.index),
313
913
  contributor_number: session.userId,
314
- contributor_name: contributorName
914
+ contributor_name: session.username
315
915
  };
316
- const bypassAudit = config2.whitelist.includes(session.userId) || session.guildId && config2.whitelist.includes(session.guildId) || session.channelId && config2.whitelist.includes(session.channelId);
916
+ if (videoUrls.length > 0 && savedVideos.length > 0) {
917
+ newCave.elements.push({
918
+ type: "video",
919
+ file: savedVideos[0],
920
+ index: Number.MAX_SAFE_INTEGER
921
+ // 确保视频总是在最后
922
+ });
923
+ }
924
+ const hashStorage2 = new HashStorage(path4.join(ctx2.baseDir, "data", "cave"));
925
+ await hashStorage2.initialize();
926
+ const hashStatus = await hashStorage2.getStatus();
927
+ if (!hashStatus.lastUpdated || hashStatus.entries.length === 0) {
928
+ const existingData = await FileHandler.readJsonData(caveFilePath);
929
+ const hasImages = existingData.some(
930
+ (cave) => cave.elements?.some((element) => element.type === "img" && element.file)
931
+ );
932
+ if (hasImages) {
933
+ await hashStorage2.updateAllCaves(true);
934
+ }
935
+ }
317
936
  if (config2.enableAudit && !bypassAudit) {
318
- pendingData.push({ ...newCave, elements: cleanElementsForSave(elements, true) });
319
- await FileHandler.writeJsonData(pendingFilePath2, pendingData, session);
320
- await sendAuditMessage(ctx2, config2, newCave, await buildMessage(newCave, resourceDir2, session), session);
937
+ const pendingData = await FileHandler.readJsonData(pendingFilePath);
938
+ pendingData.push(newCave);
939
+ await Promise.all([
940
+ FileHandler.writeJsonData(pendingFilePath, pendingData),
941
+ sendAuditMessage(ctx2, config2, newCave, await buildMessage(newCave, resourceDir, session), session)
942
+ ]);
321
943
  return sendMessage(session, "commands.cave.add.submitPending", [caveId], false);
322
- } else {
323
- const caveWithoutIndex = { ...newCave, elements: cleanElementsForSave(elements, false) };
324
- data.push(caveWithoutIndex);
325
- await FileHandler.writeJsonData(caveFilePath2, data, session);
326
- return sendMessage(session, "commands.cave.add.addSuccess", [caveId], false);
327
944
  }
945
+ const data = await FileHandler.readJsonData(caveFilePath);
946
+ data.push({
947
+ ...newCave,
948
+ elements: cleanElementsForSave(newCave.elements, false)
949
+ });
950
+ await Promise.all([
951
+ FileHandler.writeJsonData(caveFilePath, data),
952
+ hashStorage2.updateCaveHash(caveId)
953
+ ]);
954
+ await idManager.addStat(session.userId, caveId);
955
+ return sendMessage(session, "commands.cave.add.addSuccess", [caveId], false);
328
956
  } catch (error) {
329
- return sendMessage(session, "commands.cave.error.commandProcess", [error.message], true);
957
+ logger4.error(`Failed to process add command: ${error.message}`);
330
958
  }
331
959
  }
332
960
  __name(processAdd, "processAdd");
333
- ctx.command("cave [message]").option("a", "添加回声洞").option("g", "查看回声洞", { type: "string" }).option("r", "删除回声洞", { type: "string" }).option("p", "通过审核", { type: "string" }).option("d", "拒绝审核", { type: "string" }).option("l", "查询投稿统计", { type: "string" }).before(async ({ session, options }) => {
334
- if (config.blacklist.includes(session.userId)) {
335
- return sendTempMessage(session, "commands.cave.message.blacklisted");
336
- }
337
- if (session.content && session.content.includes("-help")) return;
338
- if ((options.l || options.p || options.d) && !config.manager.includes(session.userId)) {
339
- return sendTempMessage(session, "commands.cave.message.managerOnly");
961
+ async function handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, targetId) {
962
+ if (pendingData.length === 0) {
963
+ return sendMessage(session, "commands.cave.audit.noPending", [], true);
340
964
  }
341
- }).action(async ({ session, options }, ...content) => {
342
- try {
343
- const dataDir2 = path.join(ctx.baseDir, "data");
344
- const caveDir2 = path.join(dataDir2, "cave");
345
- const caveFilePath2 = path.join(caveDir2, "cave.json");
346
- const resourceDir2 = path.join(caveDir2, "resources");
347
- const pendingFilePath2 = path.join(caveDir2, "pending.json");
348
- if (options.l !== void 0) {
349
- return await processList(caveFilePath2, session, content, options, config);
965
+ if (typeof targetId === "number") {
966
+ const targetCave = pendingData.find((item) => item.cave_id === targetId);
967
+ if (!targetCave) {
968
+ return sendMessage(session, "commands.cave.audit.pendingNotFound", [], true);
350
969
  }
351
- if (options.p || options.d) {
352
- return await processAudit(ctx, pendingFilePath2, caveFilePath2, resourceDir2, session, options, content);
970
+ const newPendingData = pendingData.filter((item) => item.cave_id !== targetId);
971
+ if (isApprove) {
972
+ const oldCaveData = await FileHandler.readJsonData(caveFilePath);
973
+ const newCaveData = [...oldCaveData, {
974
+ ...targetCave,
975
+ cave_id: targetId,
976
+ elements: cleanElementsForSave(targetCave.elements, false)
977
+ }];
978
+ await FileHandler.withTransaction([
979
+ {
980
+ filePath: caveFilePath,
981
+ operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, newCaveData), "operation"),
982
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldCaveData), "rollback")
983
+ },
984
+ {
985
+ filePath: pendingFilePath,
986
+ operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, newPendingData), "operation"),
987
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
988
+ }
989
+ ]);
990
+ await idManager.addStat(targetCave.contributor_number, targetId);
991
+ } else {
992
+ await FileHandler.writeJsonData(pendingFilePath, newPendingData);
993
+ await idManager.markDeleted(targetId);
994
+ if (targetCave.elements) {
995
+ for (const element of targetCave.elements) {
996
+ if ((element.type === "img" || element.type === "video") && element.file) {
997
+ const fullPath = path4.join(resourceDir, element.file);
998
+ if (fs4.existsSync(fullPath)) {
999
+ await fs4.promises.unlink(fullPath);
1000
+ }
1001
+ }
1002
+ }
1003
+ }
353
1004
  }
354
- if (options.g) {
355
- return await processView(caveFilePath2, resourceDir2, session, options, content);
1005
+ const remainingCount = newPendingData.length;
1006
+ if (remainingCount > 0) {
1007
+ const remainingIds = newPendingData.map((c) => c.cave_id).join(", ");
1008
+ const action = isApprove ? "auditPassed" : "auditRejected";
1009
+ return sendMessage(session, "commands.cave.audit.pendingResult", [
1010
+ session.text(`commands.cave.audit.${action}`),
1011
+ remainingCount,
1012
+ remainingIds
1013
+ ], false);
356
1014
  }
357
- if (options.r) {
358
- return await processDelete(caveFilePath2, resourceDir2, pendingFilePath2, session, config, options, content);
1015
+ return sendMessage(
1016
+ session,
1017
+ isApprove ? "commands.cave.audit.auditPassed" : "commands.cave.audit.auditRejected",
1018
+ [],
1019
+ false
1020
+ );
1021
+ }
1022
+ const data = isApprove ? await FileHandler.readJsonData(caveFilePath) : null;
1023
+ let processedCount = 0;
1024
+ if (isApprove && data) {
1025
+ const oldData = [...data];
1026
+ const newData = [...data];
1027
+ await FileHandler.withTransaction([
1028
+ {
1029
+ filePath: caveFilePath,
1030
+ operation: /* @__PURE__ */ __name(async () => {
1031
+ for (const cave of pendingData) {
1032
+ newData.push({
1033
+ ...cave,
1034
+ cave_id: cave.cave_id,
1035
+ elements: cleanElementsForSave(cave.elements, false)
1036
+ });
1037
+ processedCount++;
1038
+ await idManager.addStat(cave.contributor_number, cave.cave_id);
1039
+ }
1040
+ return FileHandler.writeJsonData(caveFilePath, newData);
1041
+ }, "operation"),
1042
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldData), "rollback")
1043
+ },
1044
+ {
1045
+ filePath: pendingFilePath,
1046
+ operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, []), "operation"),
1047
+ rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
1048
+ }
1049
+ ]);
1050
+ } else {
1051
+ for (const cave of pendingData) {
1052
+ await idManager.markDeleted(cave.cave_id);
359
1053
  }
360
- if (options.a) {
361
- return await processAdd(ctx, config, caveFilePath2, resourceDir2, pendingFilePath2, session, content);
1054
+ await FileHandler.writeJsonData(pendingFilePath, []);
1055
+ for (const cave of pendingData) {
1056
+ if (cave.elements) {
1057
+ for (const element of cave.elements) {
1058
+ if ((element.type === "img" || element.type === "video") && element.file) {
1059
+ const fullPath = path4.join(resourceDir, element.file);
1060
+ if (fs4.existsSync(fullPath)) {
1061
+ await fs4.promises.unlink(fullPath);
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ processedCount++;
362
1067
  }
363
- return await processRandom(caveFilePath2, resourceDir2, session, config, lastUsed);
364
- } catch (error) {
365
- logger.error(error);
366
- return sendMessage(session, "commands.cave.error.commandProcess", [error.message], true);
367
1068
  }
368
- });
369
- }
370
- __name(apply, "apply");
371
- var logger = new import_koishi.Logger("cave");
372
- var FileHandler = class {
373
- static {
374
- __name(this, "FileHandler");
1069
+ return sendMessage(session, "commands.cave.audit.batchAuditResult", [
1070
+ isApprove ? "通过" : "拒绝",
1071
+ processedCount,
1072
+ pendingData.length
1073
+ ], false);
375
1074
  }
376
- static writeQueue = /* @__PURE__ */ new Map();
377
- static async readJsonData(filePath, session, validator) {
378
- try {
379
- const data = await fs.promises.readFile(filePath, "utf8");
380
- const parsed = JSON.parse(data || "[]");
381
- return Array.isArray(parsed) ? validator ? parsed.filter(validator) : parsed : [];
382
- } catch (error) {
383
- logger.error(session.text("commands.cave.error.fileRead", [error.message]));
384
- return [];
1075
+ __name(handleAudit, "handleAudit");
1076
+ ctx.command("cave [message]").option("a", "添加回声洞").option("g", "查看回声洞", { type: "string" }).option("r", "删除回声洞", { type: "string" }).option("p", "通过审核", { type: "string" }).option("d", "拒绝审核", { type: "string" }).option("l", "查询投稿统计", { type: "string" }).before(async ({ session, options }) => {
1077
+ if (config.blacklist.includes(session.userId)) {
1078
+ return sendMessage(session, "commands.cave.message.blacklisted", [], true);
385
1079
  }
386
- }
387
- static async writeJsonData(filePath, data, session) {
388
- const queueKey = filePath;
389
- const writeOperation = /* @__PURE__ */ __name(async () => {
390
- try {
391
- const jsonString = JSON.stringify(data, null, 2);
392
- await fs.promises.writeFile(filePath, jsonString, "utf8");
393
- } catch (error) {
394
- logger.error(session.text("commands.cave.error.fileWrite", [error.message]));
395
- throw new Error(session.text("commands.cave.error.saveFailed"));
396
- }
397
- }, "writeOperation");
398
- if (!this.writeQueue.has(queueKey)) {
399
- this.writeQueue.set(queueKey, Promise.resolve());
1080
+ if ((options.p || options.d) && !config.manager.includes(session.userId)) {
1081
+ return sendMessage(session, "commands.cave.message.managerOnly", [], true);
400
1082
  }
401
- const currentPromise = this.writeQueue.get(queueKey).then(writeOperation).finally(() => {
402
- if (this.writeQueue.get(queueKey) === currentPromise) {
403
- this.writeQueue.delete(queueKey);
1083
+ }).action(async ({ session, options }, ...content) => {
1084
+ const dataDir2 = path4.join(ctx.baseDir, "data");
1085
+ const caveDir2 = path4.join(dataDir2, "cave");
1086
+ const caveFilePath = path4.join(caveDir2, "cave.json");
1087
+ const resourceDir = path4.join(caveDir2, "resources");
1088
+ const pendingFilePath = path4.join(caveDir2, "pending.json");
1089
+ const needsCooldown = !options.l && !options.a && !options.p && !options.d;
1090
+ if (needsCooldown) {
1091
+ const guildId = session.guildId;
1092
+ const now = Date.now();
1093
+ const lastTime = lastUsed.get(guildId) || 0;
1094
+ const isManager = config.manager.includes(session.userId);
1095
+ if (!isManager && now - lastTime < config.number * 1e3) {
1096
+ const waitTime = Math.ceil((config.number * 1e3 - (now - lastTime)) / 1e3);
1097
+ return sendMessage(session, "commands.cave.message.cooldown", [waitTime], true);
404
1098
  }
405
- });
406
- this.writeQueue.set(queueKey, currentPromise);
407
- return currentPromise;
408
- }
409
- static async ensureDirectory(dir) {
410
- !fs.existsSync(dir) && await fs.promises.mkdir(dir, { recursive: true });
411
- }
412
- static async ensureJsonFile(filePath, defaultContent = "[]") {
413
- !fs.existsSync(filePath) && await fs.promises.writeFile(filePath, defaultContent, "utf8");
414
- }
415
- };
416
- async function saveMedia(urls, fileNames, fileSizes, resourceDir, caveId, config, ctx, mediaType, session) {
417
- const defaults = mediaType === "img" ? { ext: "png", accept: "image/*", maxSize: config.imageMaxSize } : { ext: "mp4", accept: "video/*", maxSize: config.videoMaxSize };
418
- const extPattern = /\.[a-zA-Z0-9]+$/;
419
- const downloadPromises = urls.map(async (url, i) => {
420
- try {
421
- const processedUrl = (() => {
422
- try {
423
- const decodedUrl = decodeURIComponent(url);
424
- return decodedUrl.includes("multimedia.nt.qq.com.cn") ? decodedUrl.replace(/&amp;/g, "&") : url;
425
- } catch {
426
- return url;
427
- }
428
- })();
429
- let ext = defaults.ext;
430
- const fileName = fileNames[i];
431
- const fileSize = fileSizes[i];
432
- if (fileSize) {
433
- const sizeInBytes = parseInt(fileSize);
434
- if (sizeInBytes > defaults.maxSize * 1024 * 1024) {
435
- logger.warn(session.text("commands.cave.message.mediaSizeExceeded", [mediaType]));
436
- return null;
1099
+ lastUsed.set(guildId, now);
1100
+ }
1101
+ if (options.l !== void 0) {
1102
+ const input = typeof options.l === "string" ? options.l : content[0];
1103
+ const num = parseInt(input);
1104
+ if (config.manager.includes(session.userId)) {
1105
+ if (!isNaN(num)) {
1106
+ if (num < 1e4) {
1107
+ return await processList(session, config, void 0, num);
1108
+ } else {
1109
+ return await processList(session, config, num.toString());
1110
+ }
1111
+ } else if (input) {
1112
+ return await processList(session, config, input);
437
1113
  }
1114
+ return await processList(session, config);
1115
+ } else {
1116
+ return await processList(session, config, session.userId);
438
1117
  }
439
- if (fileName && extPattern.test(fileName)) {
440
- ext = fileName.match(extPattern)[0].slice(1);
441
- }
442
- const finalFileName = fileName ? `${caveId}_${path.basename(fileName)}` : `${caveId}_${i + 1}.${ext}`;
443
- const targetPath = path.join(resourceDir, finalFileName);
444
- const response = await ctx.http(processedUrl, {
445
- method: "GET",
446
- responseType: "arraybuffer",
447
- timeout: 3e4,
448
- headers: {
449
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
450
- "Accept": defaults.accept,
451
- "Referer": "https://qq.com"
452
- }
453
- });
454
- const fileBuffer = Buffer.from(response.data);
455
- await fs.promises.writeFile(targetPath, fileBuffer);
456
- return finalFileName;
457
- } catch (error) {
458
- const errorKey = mediaType === "img" ? "commands.cave.error.uploadImageFailed" : "commands.cave.error.uploadVideoFailed";
459
- await sendTempMessage(session, errorKey);
460
- return null;
461
1118
  }
462
- });
463
- const results = await Promise.all(downloadPromises);
464
- return results.filter((name2) => name2 !== null);
465
- }
466
- __name(saveMedia, "saveMedia");
467
- async function sendAuditMessage(ctx, config, cave, content, session) {
468
- const auditMessage = `${session.text("commands.cave.audit.title")}
469
- ${content}
470
- ${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
471
- for (const managerId of config.manager) {
472
- try {
473
- await ctx.bots[0]?.sendPrivateMessage(managerId, auditMessage);
474
- } catch (error) {
475
- logger.error(session.text("commands.cave.audit.sendFailed", [managerId]));
1119
+ if (options.p || options.d) {
1120
+ return await processAudit(pendingFilePath, caveFilePath, resourceDir, session, options, content);
476
1121
  }
477
- }
478
- }
479
- __name(sendAuditMessage, "sendAuditMessage");
480
- async function handleSingleCaveAudit(ctx, cave, isApprove, resourceDir, data, session) {
481
- try {
482
- if (isApprove && data) {
483
- const caveWithoutIndex = {
484
- ...cave,
485
- elements: cleanElementsForSave(cave.elements, false)
486
- };
487
- data.push(caveWithoutIndex);
488
- } else if (!isApprove && cave.elements) {
489
- for (const element of cave.elements) {
490
- if (element.type === "img" && element.file) {
491
- const fullPath = path.join(resourceDir, element.file);
492
- if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
493
- }
494
- }
1122
+ if (options.g) {
1123
+ return await processView(caveFilePath, resourceDir, session, options, content);
495
1124
  }
496
- return true;
497
- } catch (error) {
498
- return sendTempMessage(session, "commands.cave.error.auditProcess", [error.message]);
499
- }
500
- }
501
- __name(handleSingleCaveAudit, "handleSingleCaveAudit");
502
- async function handleAudit(ctx, pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, targetId) {
503
- if (pendingData.length === 0) return sendMessage(session, "commands.cave.audit.noPending", [], true);
504
- if (typeof targetId === "number") {
505
- const pendingIndex = pendingData.findIndex((item) => item.cave_id === targetId);
506
- if (pendingIndex === -1) return sendMessage(session, "commands.cave.audit.pendingNotFound", [], true);
507
- const cave = pendingData[pendingIndex];
508
- const data2 = isApprove ? await FileHandler.readJsonData(caveFilePath, session) : null;
509
- const auditResult = await handleSingleCaveAudit(ctx, cave, isApprove, resourceDir, data2, session);
510
- if (typeof auditResult === "string") return auditResult;
511
- if (isApprove && data2) await FileHandler.writeJsonData(caveFilePath, data2, session);
512
- pendingData.splice(pendingIndex, 1);
513
- await FileHandler.writeJsonData(pendingFilePath, pendingData, session);
514
- const remainingCount = pendingData.length;
515
- if (remainingCount > 0) {
516
- const remainingIds = pendingData.map((c) => c.cave_id).join(", ");
517
- const action = isApprove ? "auditPassed" : "auditRejected";
518
- return sendMessage(session, "commands.cave.audit.pendingResult", [
519
- session.text(`commands.cave.audit.${action}`),
520
- remainingCount,
521
- remainingIds
522
- ], false);
523
- }
524
- return sendMessage(
525
- session,
526
- isApprove ? "commands.cave.audit.auditPassed" : "commands.cave.audit.auditRejected",
527
- [],
528
- false
529
- // 审核结果改为永久消息
530
- );
531
- }
532
- const data = isApprove ? await FileHandler.readJsonData(caveFilePath, session) : null;
533
- let processedCount = 0;
534
- for (const cave of pendingData) {
535
- await handleSingleCaveAudit(ctx, cave, isApprove, resourceDir, data, session) && processedCount++;
536
- }
537
- if (isApprove && data) await FileHandler.writeJsonData(caveFilePath, data, session);
538
- await FileHandler.writeJsonData(pendingFilePath, [], session);
539
- return sendMessage(session, "commands.cave.audit.batchAuditResult", [
540
- isApprove ? "通过" : "拒绝",
541
- processedCount,
542
- pendingData.length
543
- ], false);
544
- }
545
- __name(handleAudit, "handleAudit");
546
- function cleanElementsForSave(elements, keepIndex = false) {
547
- const sorted = elements.sort((a, b) => a.index - b.index);
548
- return sorted.map(({ type, content, file, index }) => ({
549
- type,
550
- ...keepIndex && { index },
551
- ...content && { content },
552
- ...file && { file }
553
- }));
554
- }
555
- __name(cleanElementsForSave, "cleanElementsForSave");
556
- async function sendTempMessage(session, key, params = [], timeout = 1e4) {
557
- const msg = await session.send(session.text(key, params));
558
- setTimeout(async () => {
559
- try {
560
- await session.bot.deleteMessage(session.channelId, msg);
561
- } catch (error) {
562
- logger.error("Failed to delete message:", error);
1125
+ if (options.r) {
1126
+ return await processDelete(caveFilePath, resourceDir, pendingFilePath, session, config, options, content);
563
1127
  }
564
- }, timeout);
565
- return "";
1128
+ if (options.a) {
1129
+ return await processAdd(ctx, config, caveFilePath, resourceDir, pendingFilePath, session, content);
1130
+ }
1131
+ return await processRandom(caveFilePath, resourceDir, session);
1132
+ });
566
1133
  }
567
- __name(sendTempMessage, "sendTempMessage");
568
- var messageQueue = /* @__PURE__ */ new Map();
1134
+ __name(apply, "apply");
1135
+ var logger4 = new import_koishi4.Logger("cave");
569
1136
  async function sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
570
- const channelId = session.channelId;
571
- const sendOperation = /* @__PURE__ */ __name(async () => {
1137
+ try {
572
1138
  const msg = await session.send(session.text(key, params));
573
- if (isTemp) {
1139
+ if (isTemp && msg) {
574
1140
  setTimeout(async () => {
575
1141
  try {
576
- await session.bot.deleteMessage(channelId, msg);
1142
+ await session.bot.deleteMessage(session.channelId, msg);
577
1143
  } catch (error) {
578
- logger.error("Failed to delete message:", error);
1144
+ logger4.debug(`Failed to delete temporary message: ${error.message}`);
579
1145
  }
580
1146
  }, timeout);
581
1147
  }
582
- }, "sendOperation");
583
- if (!messageQueue.has(channelId)) {
584
- messageQueue.set(channelId, Promise.resolve());
1148
+ } catch (error) {
1149
+ logger4.error(`Failed to send message: ${error.message}`);
585
1150
  }
586
- const currentPromise = messageQueue.get(channelId).then(sendOperation).finally(() => {
587
- if (messageQueue.get(channelId) === currentPromise) {
588
- messageQueue.delete(channelId);
589
- }
590
- });
591
- messageQueue.set(channelId, currentPromise);
592
1151
  return "";
593
1152
  }
594
1153
  __name(sendMessage, "sendMessage");
595
- async function extractMediaContent(originalContent) {
596
- const parsedTexts = originalContent.split(/<img[^>]+>|<video[^>]+>/g).map((t) => t.trim()).filter((t) => t);
597
- const textParts = [];
598
- parsedTexts.forEach((text, idx) => {
599
- textParts.push({ type: "text", content: text, index: idx * 3 });
600
- });
601
- const imageUrls = [];
602
- const imageElements = [];
603
- const videoUrls = [];
604
- const videoElements = [];
605
- const imgMatches = originalContent.match(/<img[^>]+src="([^"]+)"[^>]*>/g) || [];
606
- imgMatches.forEach((img, idx) => {
607
- const srcMatch = img.match(/src="([^"]+)"/);
608
- const fileName = img.match(/file="([^"]+)"/)?.[1];
609
- const fileSize = img.match(/fileSize="([^"]+)"/)?.[1];
610
- if (srcMatch?.[1]) {
611
- imageUrls.push(srcMatch[1]);
612
- imageElements.push({ type: "img", index: idx * 3 + 1, fileName, fileSize });
1154
+ async function sendAuditMessage(ctx, config, cave, content, session) {
1155
+ const auditMessage = `${session.text("commands.cave.audit.title")}
1156
+ ${content}
1157
+ ${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
1158
+ for (const managerId of config.manager) {
1159
+ const bot = ctx.bots[0];
1160
+ if (bot) {
1161
+ try {
1162
+ await bot.sendPrivateMessage(managerId, auditMessage);
1163
+ } catch (error) {
1164
+ logger4.error(session.text("commands.cave.audit.sendFailed", [managerId]));
1165
+ }
613
1166
  }
614
- });
615
- const videoMatches = originalContent.match(/<video[^>]+src="([^"]+)"[^>]*>/g) || [];
616
- videoMatches.forEach((video, idx) => {
617
- const srcMatch = video.match(/src="([^"]+)"/);
618
- const fileName = video.match(/file="([^"]+)"/)?.[1];
619
- const fileSize = video.match(/fileSize="([^"]+)"/)?.[1];
620
- if (srcMatch?.[1]) {
621
- videoUrls.push(srcMatch[1]);
622
- videoElements.push({ type: "video", index: idx * 3 + 2, fileName, fileSize });
1167
+ }
1168
+ }
1169
+ __name(sendAuditMessage, "sendAuditMessage");
1170
+ function cleanElementsForSave(elements, keepIndex = false) {
1171
+ if (!elements?.length) return [];
1172
+ const cleanedElements = elements.map((element) => {
1173
+ if (element.type === "text") {
1174
+ const cleanedElement = {
1175
+ type: "text",
1176
+ content: element.content
1177
+ };
1178
+ if (keepIndex) cleanedElement.index = element.index;
1179
+ return cleanedElement;
1180
+ } else if (element.type === "img" || element.type === "video") {
1181
+ const mediaElement = element;
1182
+ const cleanedElement = {
1183
+ type: mediaElement.type
1184
+ };
1185
+ if (mediaElement.file) cleanedElement.file = mediaElement.file;
1186
+ if (keepIndex) cleanedElement.index = element.index;
1187
+ return cleanedElement;
623
1188
  }
1189
+ return element;
624
1190
  });
625
- return { imageUrls, imageElements, videoUrls, videoElements, textParts };
1191
+ return keepIndex ? cleanedElements.sort((a, b) => (a.index || 0) - (b.index || 0)) : cleanedElements;
626
1192
  }
627
- __name(extractMediaContent, "extractMediaContent");
1193
+ __name(cleanElementsForSave, "cleanElementsForSave");
1194
+ async function processMediaFile(filePath, type) {
1195
+ const data = await fs4.promises.readFile(filePath).catch(() => null);
1196
+ if (!data) return null;
1197
+ return `data:${type}/${type === "image" ? "png" : "mp4"};base64,${data.toString("base64")}`;
1198
+ }
1199
+ __name(processMediaFile, "processMediaFile");
628
1200
  async function buildMessage(cave, resourceDir, session) {
629
- let content = session.text("commands.cave.message.caveTitle", [cave.cave_id]) + "\n";
630
- const videoElements = [];
631
- for (const element of cave.elements) {
1201
+ if (!cave?.elements?.length) {
1202
+ return session.text("commands.cave.error.noContent");
1203
+ }
1204
+ const videoElement = cave.elements.find((el) => el.type === "video");
1205
+ const nonVideoElements = cave.elements.filter((el) => el.type !== "video").sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
1206
+ if (videoElement?.file) {
1207
+ const basicInfo = [
1208
+ session.text("commands.cave.message.caveTitle", [cave.cave_id]),
1209
+ session.text("commands.cave.message.contributorSuffix", [cave.contributor_name])
1210
+ ].join("\n");
1211
+ await session?.send(basicInfo);
1212
+ const filePath = path4.join(resourceDir, videoElement.file);
1213
+ const base64Data = await processMediaFile(filePath, "video");
1214
+ if (base64Data && session) {
1215
+ await session.send((0, import_koishi4.h)("video", { src: base64Data }));
1216
+ }
1217
+ return "";
1218
+ }
1219
+ const lines = [session.text("commands.cave.message.caveTitle", [cave.cave_id])];
1220
+ for (const element of nonVideoElements) {
632
1221
  if (element.type === "text") {
633
- content += element.content + "\n";
1222
+ lines.push(element.content);
634
1223
  } else if (element.type === "img" && element.file) {
635
- const fullImagePath = path.join(resourceDir, element.file);
636
- if (fs.existsSync(fullImagePath)) {
637
- try {
638
- const imageBuffer = fs.readFileSync(fullImagePath);
639
- content += (0, import_koishi.h)("image", { src: `data:image/png;base64,${imageBuffer.toString("base64")}` }) + "\n";
640
- } catch (error) {
641
- content += session.text("commands.cave.error.mediaLoadFailed", ["图片"]) + "\n";
642
- }
643
- } else {
644
- content += session.text("commands.cave.message.mediaInvalid", ["图片"]) + "\n";
1224
+ const filePath = path4.join(resourceDir, element.file);
1225
+ const base64Data = await processMediaFile(filePath, "image");
1226
+ if (base64Data) {
1227
+ lines.push((0, import_koishi4.h)("image", { src: base64Data }));
645
1228
  }
646
- } else if (element.type === "video" && element.file) {
647
- videoElements.push({ file: element.file });
648
1229
  }
649
1230
  }
650
- if (videoElements.length > 0 && session) {
651
- content += session.text("commands.cave.message.videoSending") + "\n";
652
- for (const video of videoElements) {
653
- const fullVideoPath = path.join(resourceDir, video.file);
654
- if (fs.existsSync(fullVideoPath)) {
655
- try {
656
- const videoBuffer = fs.readFileSync(fullVideoPath);
657
- session.send((0, import_koishi.h)("video", { src: `data:video/mp4;base64,${videoBuffer.toString("base64")}` })).catch((error) => logger.warn(session.text("commands.cave.error.videoSendFailed"), error.message));
658
- } catch (error) {
659
- content += session.text("commands.cave.error.mediaLoadFailed", ["视频"]) + "\n";
1231
+ lines.push(session.text("commands.cave.message.contributorSuffix", [cave.contributor_name]));
1232
+ return lines.join("\n");
1233
+ }
1234
+ __name(buildMessage, "buildMessage");
1235
+ async function extractMediaContent(originalContent, config, session) {
1236
+ const textParts = originalContent.split(/<(img|video)[^>]+>/).map((text, idx) => text.trim() && {
1237
+ type: "text",
1238
+ content: text.replace(/^(img|video)$/, "").trim(),
1239
+ index: idx * 3
1240
+ }).filter((text) => text && text.content);
1241
+ const getMediaElements = /* @__PURE__ */ __name((type, maxSize) => {
1242
+ const regex = new RegExp(`<${type}[^>]+src="([^"]+)"[^>]*>`, "g");
1243
+ const elements = [];
1244
+ const urls = [];
1245
+ let match;
1246
+ let idx = 0;
1247
+ while ((match = regex.exec(originalContent)) !== null) {
1248
+ const element = match[0];
1249
+ const url = match[1];
1250
+ const fileName = element.match(/file="([^"]+)"/)?.[1];
1251
+ const fileSize = element.match(/fileSize="([^"]+)"/)?.[1];
1252
+ if (fileSize) {
1253
+ const sizeInBytes = parseInt(fileSize);
1254
+ if (sizeInBytes > maxSize * 1024 * 1024) {
1255
+ throw new Error(session.text("commands.cave.message.mediaSizeExceeded", [type]));
1256
+ }
1257
+ }
1258
+ urls.push(url);
1259
+ elements.push({
1260
+ type,
1261
+ index: type === "video" ? Number.MAX_SAFE_INTEGER : idx * 3 + 1,
1262
+ fileName,
1263
+ fileSize
1264
+ });
1265
+ idx++;
1266
+ }
1267
+ return { urls, elements };
1268
+ }, "getMediaElements");
1269
+ const { urls: imageUrls, elements: imageElementsRaw } = getMediaElements("img", config.imageMaxSize);
1270
+ const imageElements = imageElementsRaw;
1271
+ const { urls: videoUrls, elements: videoElementsRaw } = getMediaElements("video", config.videoMaxSize);
1272
+ const videoElements = videoElementsRaw;
1273
+ return { imageUrls, imageElements, videoUrls, videoElements, textParts };
1274
+ }
1275
+ __name(extractMediaContent, "extractMediaContent");
1276
+ async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, config, ctx, session) {
1277
+ const accept = mediaType === "img" ? "image/*" : "video/*";
1278
+ const hashStorage = new HashStorage(path4.join(ctx.baseDir, "data", "cave"));
1279
+ await hashStorage.initialize();
1280
+ const downloadTasks = urls.map(async (url, i) => {
1281
+ const fileName = fileNames[i];
1282
+ const ext = path4.extname(fileName || url) || (mediaType === "img" ? ".png" : ".mp4");
1283
+ const baseName = path4.basename(fileName || "media", ext).replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, "");
1284
+ const finalFileName = `${caveId}_${baseName}${ext}`;
1285
+ const filePath = path4.join(resourceDir, finalFileName);
1286
+ try {
1287
+ const response = await ctx.http(decodeURIComponent(url).replace(/&amp;/g, "&"), {
1288
+ method: "GET",
1289
+ responseType: "arraybuffer",
1290
+ timeout: 3e4,
1291
+ headers: {
1292
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
1293
+ "Accept": accept,
1294
+ "Referer": "https://qq.com"
1295
+ }
1296
+ });
1297
+ if (!response.data) throw new Error("empty_response");
1298
+ if (mediaType === "img") {
1299
+ const buffer = Buffer.from(response.data);
1300
+ const result = await hashStorage.findDuplicates([buffer], config.duplicateThreshold);
1301
+ if (result.length > 0 && result[0] !== null) {
1302
+ const duplicate = result[0];
1303
+ const similarity = duplicate.similarity;
1304
+ if (similarity >= config.duplicateThreshold) {
1305
+ const caveFilePath = path4.join(ctx.baseDir, "data", "cave", "cave.json");
1306
+ const data = await FileHandler.readJsonData(caveFilePath);
1307
+ const originalCave = data.find((item) => item.cave_id === duplicate.caveId);
1308
+ if (originalCave) {
1309
+ const message = session.text(
1310
+ "commands.cave.error.duplicateFound",
1311
+ [(similarity * 100).toFixed(1)]
1312
+ );
1313
+ await session.send(message + await buildMessage(originalCave, resourceDir, session));
1314
+ throw new Error("duplicate_found");
1315
+ }
1316
+ }
660
1317
  }
1318
+ await FileHandler.saveMediaFile(filePath, buffer);
661
1319
  } else {
662
- content += session.text("commands.cave.message.mediaInvalid", ["视频"]) + "\n";
1320
+ await FileHandler.saveMediaFile(filePath, Buffer.from(response.data));
1321
+ }
1322
+ return finalFileName;
1323
+ } catch (error) {
1324
+ if (error.message === "duplicate_found") {
1325
+ throw error;
663
1326
  }
1327
+ logger4.error(`Failed to download media: ${error.message}`);
1328
+ throw new Error(session.text(`commands.cave.error.upload${mediaType === "img" ? "Image" : "Video"}Failed`));
664
1329
  }
665
- }
666
- content += session.text("commands.cave.message.contributorSuffix", [cave.contributor_name]);
667
- return content;
1330
+ });
1331
+ return Promise.all(downloadTasks);
668
1332
  }
669
- __name(buildMessage, "buildMessage");
670
- var CacheManager = class {
671
- static {
672
- __name(this, "CacheManager");
673
- }
674
- static caveDataCache = null;
675
- static pendingDataCache = null;
676
- static cacheTimeout = 5 * 60 * 1e3;
677
- // 5分钟缓存
678
- static lastCaveUpdate = 0;
679
- static lastPendingUpdate = 0;
680
- static clearCache() {
681
- this.caveDataCache = null;
682
- this.pendingDataCache = null;
683
- }
684
- static async getCaveData(filePath, session) {
685
- const now = Date.now();
686
- if (!this.caveDataCache || now - this.lastCaveUpdate > this.cacheTimeout) {
687
- this.caveDataCache = await FileHandler.readJsonData(filePath, session);
688
- this.lastCaveUpdate = now;
689
- }
690
- return this.caveDataCache;
691
- }
692
- static async getPendingData(filePath, session) {
693
- const now = Date.now();
694
- if (!this.pendingDataCache || now - this.lastPendingUpdate > this.cacheTimeout) {
695
- this.pendingDataCache = await FileHandler.readJsonData(filePath, session);
696
- this.lastPendingUpdate = now;
697
- }
698
- return this.pendingDataCache;
699
- }
700
- static updateCaveData(data) {
701
- this.caveDataCache = data;
702
- this.lastCaveUpdate = Date.now();
703
- }
704
- static updatePendingData(data) {
705
- this.pendingDataCache = data;
706
- this.lastPendingUpdate = Date.now();
707
- }
708
- };
1333
+ __name(saveMedia, "saveMedia");
709
1334
  // Annotate the CommonJS export names for ESM import in node:
710
1335
  0 && (module.exports = {
711
1336
  Config,