@forgetmelody/koishi-plugin-best-cave 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +94 -21
- package/package.json +1 -1
- package/readme.md +12 -0
package/lib/index.js
CHANGED
|
@@ -304,7 +304,6 @@ var import_koishi2 = require("koishi");
|
|
|
304
304
|
var import_koishi = require("koishi");
|
|
305
305
|
var path2 = __toESM(require("path"));
|
|
306
306
|
var crypto = __toESM(require("crypto"));
|
|
307
|
-
var import_jimp = __toESM(require("jimp"));
|
|
308
307
|
var mimeTypeMap = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".webp": "image/webp" };
|
|
309
308
|
function resolveCaveHeaderFooter(cave, config, prefix) {
|
|
310
309
|
const data = {
|
|
@@ -424,10 +423,87 @@ async function buildCaveMessage(cave, config, fileManager, logger2, platform, pr
|
|
|
424
423
|
__name(buildCaveMessage, "buildCaveMessage");
|
|
425
424
|
var qqImageCache = /* @__PURE__ */ new Map();
|
|
426
425
|
var qqImageTypes = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
426
|
+
function detectImageType(buffer) {
|
|
427
|
+
if (buffer.length >= 24 && buffer[0] === 137 && buffer.toString("ascii", 1, 4) === "PNG") {
|
|
428
|
+
return { ext: ".png", mime: "image/png" };
|
|
429
|
+
}
|
|
430
|
+
if (buffer.length >= 10 && buffer.toString("ascii", 0, 3) === "GIF") {
|
|
431
|
+
return { ext: ".gif", mime: "image/gif" };
|
|
432
|
+
}
|
|
433
|
+
if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
|
|
434
|
+
return { ext: ".jpg", mime: "image/jpeg" };
|
|
435
|
+
}
|
|
436
|
+
if (buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") {
|
|
437
|
+
return { ext: ".webp", mime: "image/webp" };
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
__name(detectImageType, "detectImageType");
|
|
442
|
+
function readUInt24LE(buffer, offset) {
|
|
443
|
+
return buffer[offset] | buffer[offset + 1] << 8 | buffer[offset + 2] << 16;
|
|
444
|
+
}
|
|
445
|
+
__name(readUInt24LE, "readUInt24LE");
|
|
446
|
+
function getJpegSize(buffer) {
|
|
447
|
+
let offset = 2;
|
|
448
|
+
while (offset + 9 < buffer.length) {
|
|
449
|
+
if (buffer[offset] !== 255) {
|
|
450
|
+
offset++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const marker = buffer[offset + 1];
|
|
454
|
+
if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
|
|
455
|
+
return { height: buffer.readUInt16BE(offset + 5), width: buffer.readUInt16BE(offset + 7) };
|
|
456
|
+
}
|
|
457
|
+
const length = buffer.readUInt16BE(offset + 2);
|
|
458
|
+
if (length < 2) break;
|
|
459
|
+
offset += 2 + length;
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
__name(getJpegSize, "getJpegSize");
|
|
464
|
+
function getWebpSize(buffer) {
|
|
465
|
+
const chunk = buffer.toString("ascii", 12, 16);
|
|
466
|
+
if (chunk === "VP8X") {
|
|
467
|
+
return { width: 1 + readUInt24LE(buffer, 24), height: 1 + readUInt24LE(buffer, 27) };
|
|
468
|
+
}
|
|
469
|
+
if (chunk === "VP8 ") {
|
|
470
|
+
return { width: buffer.readUInt16LE(26) & 16383, height: buffer.readUInt16LE(28) & 16383 };
|
|
471
|
+
}
|
|
472
|
+
if (chunk === "VP8L") {
|
|
473
|
+
const b0 = buffer[21], b1 = buffer[22], b2 = buffer[23], b3 = buffer[24];
|
|
474
|
+
return { width: 1 + (b0 | (b1 & 63) << 8), height: 1 + (b1 >> 6 | b2 << 2 | (b3 & 15) << 10) };
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
__name(getWebpSize, "getWebpSize");
|
|
479
|
+
function getImageSize(buffer) {
|
|
480
|
+
const type = detectImageType(buffer);
|
|
481
|
+
if (!type) return null;
|
|
482
|
+
if (type.ext === ".png") return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
483
|
+
if (type.ext === ".gif") return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) };
|
|
484
|
+
if (type.ext === ".jpg") return getJpegSize(buffer);
|
|
485
|
+
if (type.ext === ".webp") return getWebpSize(buffer);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
__name(getImageSize, "getImageSize");
|
|
489
|
+
async function getImageSizeFromUrl(session, url) {
|
|
490
|
+
if (!url.startsWith("http")) return null;
|
|
491
|
+
try {
|
|
492
|
+
const data = await session.bot.ctx.http.get(url, { responseType: "arraybuffer", timeout: 15e3 });
|
|
493
|
+
return getImageSize(Buffer.from(data));
|
|
494
|
+
} catch {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
__name(getImageSizeFromUrl, "getImageSizeFromUrl");
|
|
427
499
|
async function uploadQQImage(session, buffer, fileName) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
500
|
+
if (buffer.length > 10 * 1024 * 1024) return null;
|
|
501
|
+
const detected = detectImageType(buffer);
|
|
502
|
+
const contentType = detected?.mime ?? qqImageTypes[path2.extname(fileName).toLowerCase()];
|
|
503
|
+
if (!contentType) return null;
|
|
504
|
+
const uploadName = detected ? `${path2.basename(fileName, path2.extname(fileName))}${detected.ext}` : fileName;
|
|
505
|
+
const cacheKey = `${session.bot.selfId}:${fileName}`;
|
|
506
|
+
const cached = qqImageCache.get(cacheKey);
|
|
431
507
|
if (cached) return cached;
|
|
432
508
|
const bot = session.bot;
|
|
433
509
|
const request = bot.internal.http();
|
|
@@ -435,7 +511,7 @@ async function uploadQQImage(session, buffer, fileName) {
|
|
|
435
511
|
const sha1 = crypto.createHash("sha1").update(buffer).digest("hex");
|
|
436
512
|
const prepared = await request("/v2/media-images/prepare", {
|
|
437
513
|
method: "POST",
|
|
438
|
-
data: { file_name:
|
|
514
|
+
data: { file_name: uploadName, file_size: buffer.length, content_type: contentType, md5, sha1 }
|
|
439
515
|
});
|
|
440
516
|
await bot.ctx.http.put(prepared.presigned_url, buffer, { headers: { "Content-Type": contentType } });
|
|
441
517
|
const completed = await request("/v2/media-images/complete", {
|
|
@@ -443,19 +519,10 @@ async function uploadQQImage(session, buffer, fileName) {
|
|
|
443
519
|
data: { upload_id: prepared.upload_id, md5 }
|
|
444
520
|
});
|
|
445
521
|
if (!completed?.url) return null;
|
|
446
|
-
qqImageCache.set(
|
|
522
|
+
qqImageCache.set(cacheKey, completed.url);
|
|
447
523
|
return completed.url;
|
|
448
524
|
}
|
|
449
525
|
__name(uploadQQImage, "uploadQQImage");
|
|
450
|
-
async function getImageSize(buffer) {
|
|
451
|
-
try {
|
|
452
|
-
const image = await import_jimp.default.read(buffer);
|
|
453
|
-
return { width: image.bitmap.width, height: image.bitmap.height };
|
|
454
|
-
} catch {
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
__name(getImageSize, "getImageSize");
|
|
459
526
|
async function buildQQCaveMarkdown(cave, config, fileManager, logger2, session, prefix) {
|
|
460
527
|
const { header, footer } = resolveCaveHeaderFooter(cave, config, prefix);
|
|
461
528
|
const lines = [];
|
|
@@ -479,7 +546,8 @@ async function buildQQCaveMarkdown(cave, config, fileManager, logger2, session,
|
|
|
479
546
|
}
|
|
480
547
|
let url = null;
|
|
481
548
|
if (config.enableS3 && config.publicUrl) {
|
|
482
|
-
|
|
549
|
+
const base = config.publicUrl.endsWith("/") ? config.publicUrl : `${config.publicUrl}/`;
|
|
550
|
+
url = new URL(fileName, base).href;
|
|
483
551
|
} else if (buffer) {
|
|
484
552
|
try {
|
|
485
553
|
url = await uploadQQImage(session, buffer, fileName);
|
|
@@ -491,8 +559,13 @@ async function buildQQCaveMarkdown(cave, config, fileManager, logger2, session,
|
|
|
491
559
|
lines.push("[图片]");
|
|
492
560
|
continue;
|
|
493
561
|
}
|
|
494
|
-
const size = buffer ?
|
|
495
|
-
|
|
562
|
+
const size = buffer ? getImageSize(buffer) : await getImageSizeFromUrl(session, url);
|
|
563
|
+
if (!size) {
|
|
564
|
+
logger2.warn(`无法获取图片 ${fileName} 的尺寸,QQ 端将显示为 [图片]`);
|
|
565
|
+
lines.push("[图片]");
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
lines.push(``);
|
|
496
569
|
} else if (el.type === "video") {
|
|
497
570
|
lines.push("[视频]");
|
|
498
571
|
} else if (el.type === "audio") {
|
|
@@ -988,7 +1061,7 @@ ${pendingCaves.map((c) => c.id).join("|")}`;
|
|
|
988
1061
|
};
|
|
989
1062
|
|
|
990
1063
|
// src/HashManager.ts
|
|
991
|
-
var
|
|
1064
|
+
var import_jimp = __toESM(require("jimp"));
|
|
992
1065
|
var crypto2 = __toESM(require("crypto"));
|
|
993
1066
|
var HashManager = class {
|
|
994
1067
|
/**
|
|
@@ -1278,8 +1351,8 @@ ${imageResult.reportLines.join("\n")}`;
|
|
|
1278
1351
|
* @returns 64位十六进制 pHash 字符串。
|
|
1279
1352
|
*/
|
|
1280
1353
|
async generatePHash(imageBuffer) {
|
|
1281
|
-
const image = await
|
|
1282
|
-
image.resize(32, 32,
|
|
1354
|
+
const image = await import_jimp.default.read(imageBuffer);
|
|
1355
|
+
image.resize(32, 32, import_jimp.default.RESIZE_BILINEAR).greyscale();
|
|
1283
1356
|
const matrix = Array.from({ length: 32 }, () => new Array(32).fill(0));
|
|
1284
1357
|
image.scan(0, 0, 32, 32, (x, y, idx) => {
|
|
1285
1358
|
matrix[y][x] = image.bitmap.data[idx];
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -113,6 +113,18 @@
|
|
|
113
113
|
| `accessKeyId` | `string` | | **(S3 必填)** S3 访问密钥 ID。 |
|
|
114
114
|
| `secretAccessKey` | `string` | | **(S3 必填)** S3 秘密访问密钥。 |
|
|
115
115
|
|
|
116
|
+
## 🤖 QQ 官方机器人
|
|
117
|
+
|
|
118
|
+
插件会自动识别 `qq` 平台的群聊消息,将回声洞改为 **Markdown 消息**(`msg_type: 2`)发送:
|
|
119
|
+
|
|
120
|
+
- **所有内容同一条消息**:页眉页脚、文字、@ 用户以及该回声洞的全部图片都内联在同一段 Markdown 中(`msg_type: 2` 请求的 `markdown` 与 `keyboard` 字段并列),不会再拆成多条。
|
|
121
|
+
- **图片带真实宽高**:图片按 `` 输出,并直接从文件头解析 PNG / JPEG / GIF / WebP 尺寸,移动端不会显示为 `[图片]` 占位。
|
|
122
|
+
- **S3 图片**:配置了 `enableS3` 和 `publicUrl` 时直接使用公网 URL;未配置 `publicUrl` 时自动上传到 QQ 素材库(单张 ≤10MB,支持 png/jpeg/gif/webp)。若 S3 读取失败但公网可访问,会改从 `publicUrl` 读取尺寸。
|
|
123
|
+
- **按钮**:消息底部附带「再来一个cave」按钮,点击后随机再抽一条,无需申请按钮模板 ID。
|
|
124
|
+
- **不支持内联的元素**:视频、音频、文件、合并转发会显示为 `[视频]`、`[音频]`、`[文件]`、`[合并转发]` 占位文本;多条删除在 QQ 下会逐条发送。
|
|
125
|
+
|
|
126
|
+
非 QQ 平台仍使用原有消息格式。
|
|
127
|
+
|
|
116
128
|
## ⚠️ 注意事项
|
|
117
129
|
|
|
118
130
|
1. **媒体发送方式**:插件会按以下优先级决定如何发送图片、视频等媒体文件:
|