koishi-plugin-best-cave 1.2.0 → 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.d.ts +6 -30
- package/lib/index.js +834 -425
- package/lib/utils/HashStorage.d.ts +34 -0
- package/lib/utils/ImageHasher.d.ts +27 -0
- package/lib/utils/fileHandler.d.ts +19 -0
- package/lib/utils/idManager.d.ts +17 -0
- package/package.json +8 -2
- package/readme.md +29 -8
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: "管理员",
|
|
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: "
|
|
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
|
|
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 =
|
|
62
|
-
manager:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
enableAudit:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,18 +744,20 @@ 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 =
|
|
80
|
-
const caveDir =
|
|
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(
|
|
87
|
-
await FileHandler.ensureJsonFile(
|
|
88
|
-
await FileHandler.ensureJsonFile(
|
|
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"));
|
|
89
755
|
const idManager = new IdManager(ctx.baseDir);
|
|
90
|
-
|
|
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
|
+
]);
|
|
91
761
|
const lastUsed = /* @__PURE__ */ new Map();
|
|
92
762
|
async function processList(session, config2, userId, pageNum = 1) {
|
|
93
763
|
const stats = idManager.getStats();
|
|
@@ -111,53 +781,47 @@ async function apply(ctx, config) {
|
|
|
111
781
|
}
|
|
112
782
|
}
|
|
113
783
|
__name(processList, "processList");
|
|
114
|
-
async function processAudit(
|
|
115
|
-
const pendingData = await FileHandler.readJsonData(
|
|
784
|
+
async function processAudit(pendingFilePath, caveFilePath, resourceDir, session, options, content) {
|
|
785
|
+
const pendingData = await FileHandler.readJsonData(pendingFilePath);
|
|
116
786
|
const isApprove = Boolean(options.p);
|
|
117
787
|
if (options.p === true && content[0] === "all" || options.d === true && content[0] === "all") {
|
|
118
|
-
return await handleAudit(
|
|
788
|
+
return await handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session);
|
|
119
789
|
}
|
|
120
790
|
const id = parseInt(content[0] || (typeof options.p === "string" ? options.p : "") || (typeof options.d === "string" ? options.d : ""));
|
|
121
791
|
if (isNaN(id)) {
|
|
122
792
|
return sendMessage(session, "commands.cave.error.invalidId", [], true);
|
|
123
793
|
}
|
|
124
|
-
return sendMessage(session, await handleAudit(
|
|
794
|
+
return sendMessage(session, await handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, id), [], true);
|
|
125
795
|
}
|
|
126
796
|
__name(processAudit, "processAudit");
|
|
127
|
-
async function processView(
|
|
128
|
-
if (!await checkCooldown(session, config2)) {
|
|
129
|
-
return "";
|
|
130
|
-
}
|
|
797
|
+
async function processView(caveFilePath, resourceDir, session, options, content) {
|
|
131
798
|
const caveId = parseInt(content[0] || (typeof options.g === "string" ? options.g : ""));
|
|
132
799
|
if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
|
|
133
|
-
const data = await FileHandler.readJsonData(
|
|
800
|
+
const data = await FileHandler.readJsonData(caveFilePath);
|
|
134
801
|
const cave = data.find((item) => item.cave_id === caveId);
|
|
135
802
|
if (!cave) return sendMessage(session, "commands.cave.error.notFound", [], true);
|
|
136
|
-
return buildMessage(cave,
|
|
803
|
+
return buildMessage(cave, resourceDir, session);
|
|
137
804
|
}
|
|
138
805
|
__name(processView, "processView");
|
|
139
|
-
async function processRandom(
|
|
140
|
-
const data = await FileHandler.readJsonData(
|
|
806
|
+
async function processRandom(caveFilePath, resourceDir, session) {
|
|
807
|
+
const data = await FileHandler.readJsonData(caveFilePath);
|
|
141
808
|
if (data.length === 0) {
|
|
142
809
|
return sendMessage(session, "commands.cave.error.noCave", [], true);
|
|
143
810
|
}
|
|
144
|
-
if (!await checkCooldown(session, config2)) {
|
|
145
|
-
return "";
|
|
146
|
-
}
|
|
147
811
|
const cave = (() => {
|
|
148
812
|
const validCaves = data.filter((cave2) => cave2.elements && cave2.elements.length > 0);
|
|
149
813
|
if (!validCaves.length) return void 0;
|
|
150
814
|
const randomIndex = Math.floor(Math.random() * validCaves.length);
|
|
151
815
|
return validCaves[randomIndex];
|
|
152
816
|
})();
|
|
153
|
-
return cave ? buildMessage(cave,
|
|
817
|
+
return cave ? buildMessage(cave, resourceDir, session) : sendMessage(session, "commands.cave.error.getCave", [], true);
|
|
154
818
|
}
|
|
155
819
|
__name(processRandom, "processRandom");
|
|
156
|
-
async function processDelete(
|
|
820
|
+
async function processDelete(caveFilePath, resourceDir, pendingFilePath, session, config2, options, content) {
|
|
157
821
|
const caveId = parseInt(content[0] || (typeof options.r === "string" ? options.r : ""));
|
|
158
822
|
if (isNaN(caveId)) return sendMessage(session, "commands.cave.error.invalidId", [], true);
|
|
159
|
-
const data = await FileHandler.readJsonData(
|
|
160
|
-
const pendingData = await FileHandler.readJsonData(
|
|
823
|
+
const data = await FileHandler.readJsonData(caveFilePath);
|
|
824
|
+
const pendingData = await FileHandler.readJsonData(pendingFilePath);
|
|
161
825
|
const targetInData = data.find((item) => item.cave_id === caveId);
|
|
162
826
|
const targetInPending = pendingData.find((item) => item.cave_id === caveId);
|
|
163
827
|
if (!targetInData && !targetInPending) {
|
|
@@ -168,23 +832,26 @@ async function apply(ctx, config) {
|
|
|
168
832
|
if (targetCave.contributor_number !== session.userId && !config2.manager.includes(session.userId)) {
|
|
169
833
|
return sendMessage(session, "commands.cave.remove.noPermission", [], true);
|
|
170
834
|
}
|
|
171
|
-
const caveContent = await buildMessage(targetCave,
|
|
835
|
+
const caveContent = await buildMessage(targetCave, resourceDir, session);
|
|
172
836
|
if (targetCave.elements) {
|
|
837
|
+
const hashStorage2 = new HashStorage(caveDir);
|
|
838
|
+
await hashStorage2.initialize();
|
|
839
|
+
await hashStorage2.updateCaveHash(caveId);
|
|
173
840
|
for (const element of targetCave.elements) {
|
|
174
841
|
if ((element.type === "img" || element.type === "video") && element.file) {
|
|
175
|
-
const fullPath =
|
|
176
|
-
if (
|
|
177
|
-
await
|
|
842
|
+
const fullPath = path4.join(resourceDir, element.file);
|
|
843
|
+
if (fs4.existsSync(fullPath)) {
|
|
844
|
+
await fs4.promises.unlink(fullPath);
|
|
178
845
|
}
|
|
179
846
|
}
|
|
180
847
|
}
|
|
181
848
|
}
|
|
182
849
|
if (isPending) {
|
|
183
850
|
const newPendingData = pendingData.filter((item) => item.cave_id !== caveId);
|
|
184
|
-
await FileHandler.writeJsonData(
|
|
851
|
+
await FileHandler.writeJsonData(pendingFilePath, newPendingData);
|
|
185
852
|
} else {
|
|
186
853
|
const newData = data.filter((item) => item.cave_id !== caveId);
|
|
187
|
-
await FileHandler.writeJsonData(
|
|
854
|
+
await FileHandler.writeJsonData(caveFilePath, newData);
|
|
188
855
|
await idManager.removeStat(targetCave.contributor_number, caveId);
|
|
189
856
|
}
|
|
190
857
|
await idManager.markDeleted(caveId);
|
|
@@ -193,7 +860,7 @@ async function apply(ctx, config) {
|
|
|
193
860
|
return `${deleteMessage}${deleteStatus}${caveContent}`;
|
|
194
861
|
}
|
|
195
862
|
__name(processDelete, "processDelete");
|
|
196
|
-
async function processAdd(ctx2, config2,
|
|
863
|
+
async function processAdd(ctx2, config2, caveFilePath, resourceDir, pendingFilePath, session, content) {
|
|
197
864
|
try {
|
|
198
865
|
const inputContent = content.length > 0 ? content.join("\n") : await (async () => {
|
|
199
866
|
await sendMessage(session, "commands.cave.add.noContent", [], true);
|
|
@@ -214,18 +881,20 @@ async function apply(ctx, config) {
|
|
|
214
881
|
imageUrls.length > 0 ? saveMedia(
|
|
215
882
|
imageUrls,
|
|
216
883
|
imageElements.map((el) => el.fileName),
|
|
217
|
-
|
|
884
|
+
resourceDir,
|
|
218
885
|
caveId,
|
|
219
886
|
"img",
|
|
887
|
+
config2,
|
|
220
888
|
ctx2,
|
|
221
889
|
session
|
|
222
890
|
) : [],
|
|
223
891
|
videoUrls.length > 0 ? saveMedia(
|
|
224
892
|
videoUrls,
|
|
225
893
|
videoElements.map((el) => el.fileName),
|
|
226
|
-
|
|
894
|
+
resourceDir,
|
|
227
895
|
caveId,
|
|
228
896
|
"video",
|
|
897
|
+
config2,
|
|
229
898
|
ctx2,
|
|
230
899
|
session
|
|
231
900
|
) : []
|
|
@@ -240,7 +909,7 @@ async function apply(ctx, config) {
|
|
|
240
909
|
// 保持原始文本和图片的相对位置
|
|
241
910
|
index: el.index
|
|
242
911
|
}))
|
|
243
|
-
].sort((a, b) => a.index -
|
|
912
|
+
].sort((a, b) => a.index - a.index),
|
|
244
913
|
contributor_number: session.userId,
|
|
245
914
|
contributor_name: session.username
|
|
246
915
|
};
|
|
@@ -252,30 +921,44 @@ async function apply(ctx, config) {
|
|
|
252
921
|
// 确保视频总是在最后
|
|
253
922
|
});
|
|
254
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
|
+
}
|
|
255
936
|
if (config2.enableAudit && !bypassAudit) {
|
|
256
|
-
const pendingData = await FileHandler.readJsonData(
|
|
937
|
+
const pendingData = await FileHandler.readJsonData(pendingFilePath);
|
|
257
938
|
pendingData.push(newCave);
|
|
258
939
|
await Promise.all([
|
|
259
|
-
FileHandler.writeJsonData(
|
|
260
|
-
sendAuditMessage(ctx2, config2, newCave, await buildMessage(newCave,
|
|
940
|
+
FileHandler.writeJsonData(pendingFilePath, pendingData),
|
|
941
|
+
sendAuditMessage(ctx2, config2, newCave, await buildMessage(newCave, resourceDir, session), session)
|
|
261
942
|
]);
|
|
262
943
|
return sendMessage(session, "commands.cave.add.submitPending", [caveId], false);
|
|
263
944
|
}
|
|
264
|
-
const data = await FileHandler.readJsonData(
|
|
945
|
+
const data = await FileHandler.readJsonData(caveFilePath);
|
|
265
946
|
data.push({
|
|
266
947
|
...newCave,
|
|
267
948
|
elements: cleanElementsForSave(newCave.elements, false)
|
|
268
949
|
});
|
|
269
|
-
await
|
|
950
|
+
await Promise.all([
|
|
951
|
+
FileHandler.writeJsonData(caveFilePath, data),
|
|
952
|
+
hashStorage2.updateCaveHash(caveId)
|
|
953
|
+
]);
|
|
270
954
|
await idManager.addStat(session.userId, caveId);
|
|
271
955
|
return sendMessage(session, "commands.cave.add.addSuccess", [caveId], false);
|
|
272
956
|
} catch (error) {
|
|
273
|
-
|
|
274
|
-
return sendMessage(session, `commands.cave.error.${error.code || "unknown"}`, [], true);
|
|
957
|
+
logger4.error(`Failed to process add command: ${error.message}`);
|
|
275
958
|
}
|
|
276
959
|
}
|
|
277
960
|
__name(processAdd, "processAdd");
|
|
278
|
-
async function handleAudit(
|
|
961
|
+
async function handleAudit(pendingData, isApprove, caveFilePath, resourceDir, pendingFilePath, session, targetId) {
|
|
279
962
|
if (pendingData.length === 0) {
|
|
280
963
|
return sendMessage(session, "commands.cave.audit.noPending", [], true);
|
|
281
964
|
}
|
|
@@ -286,36 +969,34 @@ async function apply(ctx, config) {
|
|
|
286
969
|
}
|
|
287
970
|
const newPendingData = pendingData.filter((item) => item.cave_id !== targetId);
|
|
288
971
|
if (isApprove) {
|
|
289
|
-
const oldCaveData = await FileHandler.readJsonData(
|
|
972
|
+
const oldCaveData = await FileHandler.readJsonData(caveFilePath);
|
|
290
973
|
const newCaveData = [...oldCaveData, {
|
|
291
974
|
...targetCave,
|
|
292
975
|
cave_id: targetId,
|
|
293
|
-
// 明确指定ID
|
|
294
|
-
// 保存到 cave.json 时移除 index
|
|
295
976
|
elements: cleanElementsForSave(targetCave.elements, false)
|
|
296
977
|
}];
|
|
297
978
|
await FileHandler.withTransaction([
|
|
298
979
|
{
|
|
299
|
-
filePath:
|
|
300
|
-
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(
|
|
301
|
-
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(
|
|
980
|
+
filePath: caveFilePath,
|
|
981
|
+
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, newCaveData), "operation"),
|
|
982
|
+
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldCaveData), "rollback")
|
|
302
983
|
},
|
|
303
984
|
{
|
|
304
|
-
filePath:
|
|
305
|
-
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(
|
|
306
|
-
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(
|
|
985
|
+
filePath: pendingFilePath,
|
|
986
|
+
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, newPendingData), "operation"),
|
|
987
|
+
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
|
|
307
988
|
}
|
|
308
989
|
]);
|
|
309
990
|
await idManager.addStat(targetCave.contributor_number, targetId);
|
|
310
991
|
} else {
|
|
311
|
-
await FileHandler.writeJsonData(
|
|
992
|
+
await FileHandler.writeJsonData(pendingFilePath, newPendingData);
|
|
312
993
|
await idManager.markDeleted(targetId);
|
|
313
994
|
if (targetCave.elements) {
|
|
314
995
|
for (const element of targetCave.elements) {
|
|
315
996
|
if ((element.type === "img" || element.type === "video") && element.file) {
|
|
316
|
-
const fullPath =
|
|
317
|
-
if (
|
|
318
|
-
await
|
|
997
|
+
const fullPath = path4.join(resourceDir, element.file);
|
|
998
|
+
if (fs4.existsSync(fullPath)) {
|
|
999
|
+
await fs4.promises.unlink(fullPath);
|
|
319
1000
|
}
|
|
320
1001
|
}
|
|
321
1002
|
}
|
|
@@ -338,48 +1019,46 @@ async function apply(ctx, config) {
|
|
|
338
1019
|
false
|
|
339
1020
|
);
|
|
340
1021
|
}
|
|
341
|
-
const data = isApprove ? await FileHandler.readJsonData(
|
|
1022
|
+
const data = isApprove ? await FileHandler.readJsonData(caveFilePath) : null;
|
|
342
1023
|
let processedCount = 0;
|
|
343
1024
|
if (isApprove && data) {
|
|
344
1025
|
const oldData = [...data];
|
|
345
1026
|
const newData = [...data];
|
|
346
1027
|
await FileHandler.withTransaction([
|
|
347
1028
|
{
|
|
348
|
-
filePath:
|
|
1029
|
+
filePath: caveFilePath,
|
|
349
1030
|
operation: /* @__PURE__ */ __name(async () => {
|
|
350
1031
|
for (const cave of pendingData) {
|
|
351
1032
|
newData.push({
|
|
352
1033
|
...cave,
|
|
353
1034
|
cave_id: cave.cave_id,
|
|
354
|
-
// 确保ID保持不变
|
|
355
|
-
// 保存到 cave.json 时移除 index
|
|
356
1035
|
elements: cleanElementsForSave(cave.elements, false)
|
|
357
1036
|
});
|
|
358
1037
|
processedCount++;
|
|
359
1038
|
await idManager.addStat(cave.contributor_number, cave.cave_id);
|
|
360
1039
|
}
|
|
361
|
-
return FileHandler.writeJsonData(
|
|
1040
|
+
return FileHandler.writeJsonData(caveFilePath, newData);
|
|
362
1041
|
}, "operation"),
|
|
363
|
-
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(
|
|
1042
|
+
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(caveFilePath, oldData), "rollback")
|
|
364
1043
|
},
|
|
365
1044
|
{
|
|
366
|
-
filePath:
|
|
367
|
-
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(
|
|
368
|
-
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(
|
|
1045
|
+
filePath: pendingFilePath,
|
|
1046
|
+
operation: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, []), "operation"),
|
|
1047
|
+
rollback: /* @__PURE__ */ __name(async () => FileHandler.writeJsonData(pendingFilePath, pendingData), "rollback")
|
|
369
1048
|
}
|
|
370
1049
|
]);
|
|
371
1050
|
} else {
|
|
372
1051
|
for (const cave of pendingData) {
|
|
373
1052
|
await idManager.markDeleted(cave.cave_id);
|
|
374
1053
|
}
|
|
375
|
-
await FileHandler.writeJsonData(
|
|
1054
|
+
await FileHandler.writeJsonData(pendingFilePath, []);
|
|
376
1055
|
for (const cave of pendingData) {
|
|
377
1056
|
if (cave.elements) {
|
|
378
1057
|
for (const element of cave.elements) {
|
|
379
1058
|
if ((element.type === "img" || element.type === "video") && element.file) {
|
|
380
|
-
const fullPath =
|
|
381
|
-
if (
|
|
382
|
-
await
|
|
1059
|
+
const fullPath = path4.join(resourceDir, element.file);
|
|
1060
|
+
if (fs4.existsSync(fullPath)) {
|
|
1061
|
+
await fs4.promises.unlink(fullPath);
|
|
383
1062
|
}
|
|
384
1063
|
}
|
|
385
1064
|
}
|
|
@@ -394,20 +1073,6 @@ async function apply(ctx, config) {
|
|
|
394
1073
|
], false);
|
|
395
1074
|
}
|
|
396
1075
|
__name(handleAudit, "handleAudit");
|
|
397
|
-
async function checkCooldown(session, config2) {
|
|
398
|
-
const guildId = session.guildId;
|
|
399
|
-
const now = Date.now();
|
|
400
|
-
const lastTime = lastUsed.get(guildId) || 0;
|
|
401
|
-
const isManager = config2.manager.includes(session.userId);
|
|
402
|
-
if (!isManager && now - lastTime < config2.number * 1e3) {
|
|
403
|
-
const waitTime = Math.ceil((config2.number * 1e3 - (now - lastTime)) / 1e3);
|
|
404
|
-
await sendMessage(session, "commands.cave.message.cooldown", [waitTime], true);
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
407
|
-
lastUsed.set(guildId, now);
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
__name(checkCooldown, "checkCooldown");
|
|
411
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 }) => {
|
|
412
1077
|
if (config.blacklist.includes(session.userId)) {
|
|
413
1078
|
return sendMessage(session, "commands.cave.message.blacklisted", [], true);
|
|
@@ -416,11 +1081,23 @@ async function apply(ctx, config) {
|
|
|
416
1081
|
return sendMessage(session, "commands.cave.message.managerOnly", [], true);
|
|
417
1082
|
}
|
|
418
1083
|
}).action(async ({ session, options }, ...content) => {
|
|
419
|
-
const dataDir2 =
|
|
420
|
-
const caveDir2 =
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
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);
|
|
1098
|
+
}
|
|
1099
|
+
lastUsed.set(guildId, now);
|
|
1100
|
+
}
|
|
424
1101
|
if (options.l !== void 0) {
|
|
425
1102
|
const input = typeof options.l === "string" ? options.l : content[0];
|
|
426
1103
|
const num = parseInt(input);
|
|
@@ -440,311 +1117,22 @@ async function apply(ctx, config) {
|
|
|
440
1117
|
}
|
|
441
1118
|
}
|
|
442
1119
|
if (options.p || options.d) {
|
|
443
|
-
return await processAudit(
|
|
1120
|
+
return await processAudit(pendingFilePath, caveFilePath, resourceDir, session, options, content);
|
|
444
1121
|
}
|
|
445
1122
|
if (options.g) {
|
|
446
|
-
return await processView(
|
|
1123
|
+
return await processView(caveFilePath, resourceDir, session, options, content);
|
|
447
1124
|
}
|
|
448
1125
|
if (options.r) {
|
|
449
|
-
return await processDelete(
|
|
1126
|
+
return await processDelete(caveFilePath, resourceDir, pendingFilePath, session, config, options, content);
|
|
450
1127
|
}
|
|
451
1128
|
if (options.a) {
|
|
452
|
-
return await processAdd(ctx, config,
|
|
1129
|
+
return await processAdd(ctx, config, caveFilePath, resourceDir, pendingFilePath, session, content);
|
|
453
1130
|
}
|
|
454
|
-
return await processRandom(
|
|
1131
|
+
return await processRandom(caveFilePath, resourceDir, session);
|
|
455
1132
|
});
|
|
456
1133
|
}
|
|
457
1134
|
__name(apply, "apply");
|
|
458
|
-
var
|
|
459
|
-
var FileHandler = class {
|
|
460
|
-
static {
|
|
461
|
-
__name(this, "FileHandler");
|
|
462
|
-
}
|
|
463
|
-
static locks = /* @__PURE__ */ new Map();
|
|
464
|
-
static RETRY_COUNT = 3;
|
|
465
|
-
static RETRY_DELAY = 1e3;
|
|
466
|
-
static CONCURRENCY_LIMIT = 5;
|
|
467
|
-
/**
|
|
468
|
-
* 并发控制
|
|
469
|
-
*/
|
|
470
|
-
static async withConcurrencyLimit(operation, limit = this.CONCURRENCY_LIMIT) {
|
|
471
|
-
while (this.locks.size >= limit) {
|
|
472
|
-
await Promise.race(this.locks.values());
|
|
473
|
-
}
|
|
474
|
-
return operation();
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* 统一的文件操作包装器
|
|
478
|
-
*/
|
|
479
|
-
static async withFileOp(filePath, operation) {
|
|
480
|
-
const key = filePath;
|
|
481
|
-
while (this.locks.has(key)) {
|
|
482
|
-
await this.locks.get(key);
|
|
483
|
-
}
|
|
484
|
-
const operationPromise = (async () => {
|
|
485
|
-
for (let i = 0; i < this.RETRY_COUNT; i++) {
|
|
486
|
-
try {
|
|
487
|
-
return await operation();
|
|
488
|
-
} catch (error) {
|
|
489
|
-
if (i === this.RETRY_COUNT - 1) throw error;
|
|
490
|
-
await new Promise((resolve) => setTimeout(resolve, this.RETRY_DELAY));
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
throw new Error("Operation failed after retries");
|
|
494
|
-
})();
|
|
495
|
-
this.locks.set(key, operationPromise);
|
|
496
|
-
try {
|
|
497
|
-
return await operationPromise;
|
|
498
|
-
} finally {
|
|
499
|
-
this.locks.delete(key);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* 事务处理
|
|
504
|
-
*/
|
|
505
|
-
static async withTransaction(operations) {
|
|
506
|
-
const results = [];
|
|
507
|
-
const completed = /* @__PURE__ */ new Set();
|
|
508
|
-
try {
|
|
509
|
-
for (const { filePath, operation } of operations) {
|
|
510
|
-
const result = await this.withFileOp(filePath, operation);
|
|
511
|
-
results.push(result);
|
|
512
|
-
completed.add(filePath);
|
|
513
|
-
}
|
|
514
|
-
return results;
|
|
515
|
-
} catch (error) {
|
|
516
|
-
await Promise.all(
|
|
517
|
-
operations.filter(({ filePath }) => completed.has(filePath)).map(async ({ filePath, rollback }) => {
|
|
518
|
-
if (rollback) {
|
|
519
|
-
await this.withFileOp(filePath, rollback).catch(
|
|
520
|
-
(e) => logger.error(`Rollback failed for ${filePath}: ${e.message}`)
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
})
|
|
524
|
-
);
|
|
525
|
-
throw error;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* JSON文件读写
|
|
530
|
-
*/
|
|
531
|
-
static async readJsonData(filePath) {
|
|
532
|
-
return this.withFileOp(filePath, async () => {
|
|
533
|
-
try {
|
|
534
|
-
const data = await fs.promises.readFile(filePath, "utf8");
|
|
535
|
-
return JSON.parse(data || "[]");
|
|
536
|
-
} catch (error) {
|
|
537
|
-
return [];
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
static async writeJsonData(filePath, data) {
|
|
542
|
-
const tmpPath = `${filePath}.tmp`;
|
|
543
|
-
await this.withFileOp(filePath, async () => {
|
|
544
|
-
await fs.promises.writeFile(tmpPath, JSON.stringify(data, null, 2));
|
|
545
|
-
await fs.promises.rename(tmpPath, filePath);
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* 文件系统操作
|
|
550
|
-
*/
|
|
551
|
-
static async ensureDirectory(dir) {
|
|
552
|
-
await this.withConcurrencyLimit(async () => {
|
|
553
|
-
if (!fs.existsSync(dir)) {
|
|
554
|
-
await fs.promises.mkdir(dir, { recursive: true });
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
static async ensureJsonFile(filePath) {
|
|
559
|
-
await this.withFileOp(filePath, async () => {
|
|
560
|
-
if (!fs.existsSync(filePath)) {
|
|
561
|
-
await fs.promises.writeFile(filePath, "[]", "utf8");
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* 媒体文件操作
|
|
567
|
-
*/
|
|
568
|
-
static async saveMediaFile(filePath, data) {
|
|
569
|
-
await this.withConcurrencyLimit(async () => {
|
|
570
|
-
const dir = path.dirname(filePath);
|
|
571
|
-
await this.ensureDirectory(dir);
|
|
572
|
-
await this.withFileOp(
|
|
573
|
-
filePath,
|
|
574
|
-
() => fs.promises.writeFile(filePath, data)
|
|
575
|
-
);
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
static async deleteMediaFile(filePath) {
|
|
579
|
-
await this.withFileOp(filePath, async () => {
|
|
580
|
-
if (fs.existsSync(filePath)) {
|
|
581
|
-
await fs.promises.unlink(filePath);
|
|
582
|
-
}
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
var IdManager = class {
|
|
587
|
-
static {
|
|
588
|
-
__name(this, "IdManager");
|
|
589
|
-
}
|
|
590
|
-
deletedIds = /* @__PURE__ */ new Set();
|
|
591
|
-
maxId = 0;
|
|
592
|
-
initialized = false;
|
|
593
|
-
statusFilePath;
|
|
594
|
-
stats = {};
|
|
595
|
-
usedIds = /* @__PURE__ */ new Set();
|
|
596
|
-
// 新增:跟踪所有使用中的ID
|
|
597
|
-
constructor(baseDir) {
|
|
598
|
-
const caveDir = path.join(baseDir, "data", "cave");
|
|
599
|
-
this.statusFilePath = path.join(caveDir, "status.json");
|
|
600
|
-
}
|
|
601
|
-
async initialize(caveFilePath, pendingFilePath) {
|
|
602
|
-
if (this.initialized) return;
|
|
603
|
-
try {
|
|
604
|
-
this.initialized = true;
|
|
605
|
-
const status = fs.existsSync(this.statusFilePath) ? JSON.parse(await fs.promises.readFile(this.statusFilePath, "utf8")) : {
|
|
606
|
-
deletedIds: [],
|
|
607
|
-
maxId: 0,
|
|
608
|
-
stats: {},
|
|
609
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
610
|
-
};
|
|
611
|
-
const [caveData, pendingData] = await Promise.all([
|
|
612
|
-
FileHandler.readJsonData(caveFilePath),
|
|
613
|
-
FileHandler.readJsonData(pendingFilePath)
|
|
614
|
-
]);
|
|
615
|
-
this.usedIds.clear();
|
|
616
|
-
const conflicts = /* @__PURE__ */ new Map();
|
|
617
|
-
const collectIds = /* @__PURE__ */ __name((items) => {
|
|
618
|
-
items.forEach((item) => {
|
|
619
|
-
if (this.usedIds.has(item.cave_id)) {
|
|
620
|
-
if (!conflicts.has(item.cave_id)) {
|
|
621
|
-
conflicts.set(item.cave_id, []);
|
|
622
|
-
}
|
|
623
|
-
conflicts.get(item.cave_id)?.push(item);
|
|
624
|
-
} else {
|
|
625
|
-
this.usedIds.add(item.cave_id);
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
}, "collectIds");
|
|
629
|
-
collectIds(caveData);
|
|
630
|
-
collectIds(pendingData);
|
|
631
|
-
if (conflicts.size > 0) {
|
|
632
|
-
logger.warn(`Found ${conflicts.size} ID conflicts, auto-fixing...`);
|
|
633
|
-
for (const [conflictId, items] of conflicts) {
|
|
634
|
-
items.forEach((item, index) => {
|
|
635
|
-
if (index > 0) {
|
|
636
|
-
let newId = this.maxId + 1;
|
|
637
|
-
while (this.usedIds.has(newId)) {
|
|
638
|
-
newId++;
|
|
639
|
-
}
|
|
640
|
-
logger.info(`Reassigning ID ${item.cave_id} -> ${newId} for item`);
|
|
641
|
-
item.cave_id = newId;
|
|
642
|
-
this.usedIds.add(newId);
|
|
643
|
-
this.maxId = Math.max(this.maxId, newId);
|
|
644
|
-
}
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
await Promise.all([
|
|
648
|
-
FileHandler.writeJsonData(caveFilePath, caveData),
|
|
649
|
-
FileHandler.writeJsonData(pendingFilePath, pendingData)
|
|
650
|
-
]);
|
|
651
|
-
}
|
|
652
|
-
this.maxId = Math.max(
|
|
653
|
-
this.maxId,
|
|
654
|
-
status.maxId || 0,
|
|
655
|
-
...[...this.usedIds]
|
|
656
|
-
);
|
|
657
|
-
this.deletedIds = new Set(
|
|
658
|
-
status.deletedIds?.filter((id) => !this.usedIds.has(id)) || []
|
|
659
|
-
);
|
|
660
|
-
this.stats = {};
|
|
661
|
-
for (const cave of caveData) {
|
|
662
|
-
if (cave.contributor_number === "10000") continue;
|
|
663
|
-
if (!this.stats[cave.contributor_number]) {
|
|
664
|
-
this.stats[cave.contributor_number] = [];
|
|
665
|
-
}
|
|
666
|
-
this.stats[cave.contributor_number].push(cave.cave_id);
|
|
667
|
-
}
|
|
668
|
-
await this.saveStatus();
|
|
669
|
-
this.initialized = true;
|
|
670
|
-
} catch (error) {
|
|
671
|
-
this.initialized = false;
|
|
672
|
-
logger.error(`IdManager initialization failed: ${error.message}`);
|
|
673
|
-
throw error;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
getNextId() {
|
|
677
|
-
if (!this.initialized) {
|
|
678
|
-
throw new Error("IdManager not initialized");
|
|
679
|
-
}
|
|
680
|
-
let nextId;
|
|
681
|
-
if (this.deletedIds.size === 0) {
|
|
682
|
-
nextId = ++this.maxId;
|
|
683
|
-
} else {
|
|
684
|
-
nextId = Math.min(...Array.from(this.deletedIds));
|
|
685
|
-
this.deletedIds.delete(nextId);
|
|
686
|
-
}
|
|
687
|
-
while (this.usedIds.has(nextId)) {
|
|
688
|
-
nextId = ++this.maxId;
|
|
689
|
-
}
|
|
690
|
-
this.usedIds.add(nextId);
|
|
691
|
-
this.saveStatus().catch(
|
|
692
|
-
(err) => logger.error(`Failed to save status after getNextId: ${err.message}`)
|
|
693
|
-
);
|
|
694
|
-
return nextId;
|
|
695
|
-
}
|
|
696
|
-
async markDeleted(id) {
|
|
697
|
-
if (!this.initialized) {
|
|
698
|
-
throw new Error("IdManager not initialized");
|
|
699
|
-
}
|
|
700
|
-
this.deletedIds.add(id);
|
|
701
|
-
this.usedIds.delete(id);
|
|
702
|
-
if (id === this.maxId) {
|
|
703
|
-
const maxUsedId = Math.max(...Array.from(this.usedIds));
|
|
704
|
-
this.maxId = maxUsedId;
|
|
705
|
-
}
|
|
706
|
-
await this.saveStatus();
|
|
707
|
-
}
|
|
708
|
-
// 添加新的统计记录
|
|
709
|
-
async addStat(contributorNumber, caveId) {
|
|
710
|
-
if (contributorNumber === "10000") return;
|
|
711
|
-
if (!this.stats[contributorNumber]) {
|
|
712
|
-
this.stats[contributorNumber] = [];
|
|
713
|
-
}
|
|
714
|
-
this.stats[contributorNumber].push(caveId);
|
|
715
|
-
await this.saveStatus();
|
|
716
|
-
}
|
|
717
|
-
// 删除统计记录
|
|
718
|
-
async removeStat(contributorNumber, caveId) {
|
|
719
|
-
if (this.stats[contributorNumber]) {
|
|
720
|
-
this.stats[contributorNumber] = this.stats[contributorNumber].filter((id) => id !== caveId);
|
|
721
|
-
if (this.stats[contributorNumber].length === 0) {
|
|
722
|
-
delete this.stats[contributorNumber];
|
|
723
|
-
}
|
|
724
|
-
await this.saveStatus();
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
// 获取统计信息
|
|
728
|
-
getStats() {
|
|
729
|
-
return this.stats;
|
|
730
|
-
}
|
|
731
|
-
async saveStatus() {
|
|
732
|
-
try {
|
|
733
|
-
const status = {
|
|
734
|
-
deletedIds: Array.from(this.deletedIds).sort((a, b) => a - b),
|
|
735
|
-
maxId: this.maxId,
|
|
736
|
-
stats: this.stats,
|
|
737
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
738
|
-
};
|
|
739
|
-
const tmpPath = `${this.statusFilePath}.tmp`;
|
|
740
|
-
await fs.promises.writeFile(tmpPath, JSON.stringify(status, null, 2), "utf8");
|
|
741
|
-
await fs.promises.rename(tmpPath, this.statusFilePath);
|
|
742
|
-
} catch (error) {
|
|
743
|
-
logger.error(`Failed to save status: ${error.message}`);
|
|
744
|
-
throw error;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
};
|
|
1135
|
+
var logger4 = new import_koishi4.Logger("cave");
|
|
748
1136
|
async function sendMessage(session, key, params = [], isTemp = true, timeout = 1e4) {
|
|
749
1137
|
try {
|
|
750
1138
|
const msg = await session.send(session.text(key, params));
|
|
@@ -753,12 +1141,12 @@ async function sendMessage(session, key, params = [], isTemp = true, timeout = 1
|
|
|
753
1141
|
try {
|
|
754
1142
|
await session.bot.deleteMessage(session.channelId, msg);
|
|
755
1143
|
} catch (error) {
|
|
756
|
-
|
|
1144
|
+
logger4.debug(`Failed to delete temporary message: ${error.message}`);
|
|
757
1145
|
}
|
|
758
1146
|
}, timeout);
|
|
759
1147
|
}
|
|
760
1148
|
} catch (error) {
|
|
761
|
-
|
|
1149
|
+
logger4.error(`Failed to send message: ${error.message}`);
|
|
762
1150
|
}
|
|
763
1151
|
return "";
|
|
764
1152
|
}
|
|
@@ -773,7 +1161,7 @@ ${session.text("commands.cave.audit.from")}${cave.contributor_number}`;
|
|
|
773
1161
|
try {
|
|
774
1162
|
await bot.sendPrivateMessage(managerId, auditMessage);
|
|
775
1163
|
} catch (error) {
|
|
776
|
-
|
|
1164
|
+
logger4.error(session.text("commands.cave.audit.sendFailed", [managerId]));
|
|
777
1165
|
}
|
|
778
1166
|
}
|
|
779
1167
|
}
|
|
@@ -804,7 +1192,7 @@ function cleanElementsForSave(elements, keepIndex = false) {
|
|
|
804
1192
|
}
|
|
805
1193
|
__name(cleanElementsForSave, "cleanElementsForSave");
|
|
806
1194
|
async function processMediaFile(filePath, type) {
|
|
807
|
-
const data = await
|
|
1195
|
+
const data = await fs4.promises.readFile(filePath).catch(() => null);
|
|
808
1196
|
if (!data) return null;
|
|
809
1197
|
return `data:${type}/${type === "image" ? "png" : "mp4"};base64,${data.toString("base64")}`;
|
|
810
1198
|
}
|
|
@@ -821,10 +1209,10 @@ async function buildMessage(cave, resourceDir, session) {
|
|
|
821
1209
|
session.text("commands.cave.message.contributorSuffix", [cave.contributor_name])
|
|
822
1210
|
].join("\n");
|
|
823
1211
|
await session?.send(basicInfo);
|
|
824
|
-
const filePath =
|
|
1212
|
+
const filePath = path4.join(resourceDir, videoElement.file);
|
|
825
1213
|
const base64Data = await processMediaFile(filePath, "video");
|
|
826
1214
|
if (base64Data && session) {
|
|
827
|
-
await session.send((0,
|
|
1215
|
+
await session.send((0, import_koishi4.h)("video", { src: base64Data }));
|
|
828
1216
|
}
|
|
829
1217
|
return "";
|
|
830
1218
|
}
|
|
@@ -833,10 +1221,10 @@ async function buildMessage(cave, resourceDir, session) {
|
|
|
833
1221
|
if (element.type === "text") {
|
|
834
1222
|
lines.push(element.content);
|
|
835
1223
|
} else if (element.type === "img" && element.file) {
|
|
836
|
-
const filePath =
|
|
1224
|
+
const filePath = path4.join(resourceDir, element.file);
|
|
837
1225
|
const base64Data = await processMediaFile(filePath, "image");
|
|
838
1226
|
if (base64Data) {
|
|
839
|
-
lines.push((0,
|
|
1227
|
+
lines.push((0, import_koishi4.h)("image", { src: base64Data }));
|
|
840
1228
|
}
|
|
841
1229
|
}
|
|
842
1230
|
}
|
|
@@ -871,7 +1259,6 @@ async function extractMediaContent(originalContent, config, session) {
|
|
|
871
1259
|
elements.push({
|
|
872
1260
|
type,
|
|
873
1261
|
index: type === "video" ? Number.MAX_SAFE_INTEGER : idx * 3 + 1,
|
|
874
|
-
// 视频始终在最后
|
|
875
1262
|
fileName,
|
|
876
1263
|
fileSize
|
|
877
1264
|
});
|
|
@@ -886,15 +1273,16 @@ async function extractMediaContent(originalContent, config, session) {
|
|
|
886
1273
|
return { imageUrls, imageElements, videoUrls, videoElements, textParts };
|
|
887
1274
|
}
|
|
888
1275
|
__name(extractMediaContent, "extractMediaContent");
|
|
889
|
-
async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, ctx, session) {
|
|
890
|
-
const
|
|
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();
|
|
891
1280
|
const downloadTasks = urls.map(async (url, i) => {
|
|
892
1281
|
const fileName = fileNames[i];
|
|
893
|
-
const
|
|
894
|
-
const baseName = fileName;
|
|
895
|
-
|
|
896
|
-
const
|
|
897
|
-
const filePath = path.join(resourceDir, finalFileName);
|
|
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);
|
|
898
1286
|
try {
|
|
899
1287
|
const response = await ctx.http(decodeURIComponent(url).replace(/&/g, "&"), {
|
|
900
1288
|
method: "GET",
|
|
@@ -907,19 +1295,40 @@ async function saveMedia(urls, fileNames, resourceDir, caveId, mediaType, ctx, s
|
|
|
907
1295
|
}
|
|
908
1296
|
});
|
|
909
1297
|
if (!response.data) throw new Error("empty_response");
|
|
910
|
-
|
|
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
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
await FileHandler.saveMediaFile(filePath, buffer);
|
|
1319
|
+
} else {
|
|
1320
|
+
await FileHandler.saveMediaFile(filePath, Buffer.from(response.data));
|
|
1321
|
+
}
|
|
911
1322
|
return finalFileName;
|
|
912
1323
|
} catch (error) {
|
|
913
|
-
|
|
914
|
-
|
|
1324
|
+
if (error.message === "duplicate_found") {
|
|
1325
|
+
throw error;
|
|
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`));
|
|
915
1329
|
}
|
|
916
1330
|
});
|
|
917
|
-
|
|
918
|
-
const successfulResults = results.filter((result) => result.status === "fulfilled").map((result) => result.value);
|
|
919
|
-
if (!successfulResults.length) {
|
|
920
|
-
throw new Error(session.text(`commands.cave.error.upload${mediaType === "img" ? "Image" : "Video"}Failed`));
|
|
921
|
-
}
|
|
922
|
-
return successfulResults;
|
|
1331
|
+
return Promise.all(downloadTasks);
|
|
923
1332
|
}
|
|
924
1333
|
__name(saveMedia, "saveMedia");
|
|
925
1334
|
// Annotate the CommonJS export names for ESM import in node:
|