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.d.ts +10 -9
- package/lib/index.js +1151 -526
- 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,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 =
|
|
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"));
|
|
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(
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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(
|
|
121
|
-
const pendingData = await
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
140
|
-
return caveContent;
|
|
803
|
+
return buildMessage(cave, resourceDir, session);
|
|
141
804
|
}
|
|
142
805
|
__name(processView, "processView");
|
|
143
|
-
async function processRandom(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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(
|
|
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
|
|
174
|
-
const pendingData = await
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
178
|
-
|
|
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,
|
|
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 =
|
|
194
|
-
if (
|
|
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.
|
|
200
|
-
await FileHandler.writeJsonData(
|
|
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.
|
|
207
|
-
await FileHandler.writeJsonData(
|
|
208
|
-
|
|
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,
|
|
863
|
+
async function processAdd(ctx2, config2, caveFilePath, resourceDir, pendingFilePath, session, content) {
|
|
215
864
|
try {
|
|
216
|
-
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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:
|
|
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:
|
|
914
|
+
contributor_name: session.username
|
|
315
915
|
};
|
|
316
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
await
|
|
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
|
-
|
|
957
|
+
logger4.error(`Failed to process add command: ${error.message}`);
|
|
330
958
|
}
|
|
331
959
|
}
|
|
332
960
|
__name(processAdd, "processAdd");
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
return
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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(/&/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
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
565
|
-
|
|
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(
|
|
568
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
1144
|
+
logger4.debug(`Failed to delete temporary message: ${error.message}`);
|
|
579
1145
|
}
|
|
580
1146
|
}, timeout);
|
|
581
1147
|
}
|
|
582
|
-
}
|
|
583
|
-
|
|
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
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
|
|
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
|
|
1191
|
+
return keepIndex ? cleanedElements.sort((a, b) => (a.index || 0) - (b.index || 0)) : cleanedElements;
|
|
626
1192
|
}
|
|
627
|
-
__name(
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
1222
|
+
lines.push(element.content);
|
|
634
1223
|
} else if (element.type === "img" && element.file) {
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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(/&/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
|
-
|
|
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
|
-
|
|
667
|
-
return content;
|
|
1330
|
+
});
|
|
1331
|
+
return Promise.all(downloadTasks);
|
|
668
1332
|
}
|
|
669
|
-
__name(
|
|
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,
|