@napuketto/kernel 0.1.1 → 0.2.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/dist/index.mjs CHANGED
@@ -708,17 +708,17 @@ function videoToCanonical(video) {
708
708
  type: "video",
709
709
  path: ""
710
710
  };
711
- const path = video.filePath ?? video.videoUrl ?? "";
712
- const url = video.videoUrl;
713
- if (url === void 0) return {
711
+ const base = {
714
712
  type: "video",
715
- path
716
- };
717
- return {
718
- type: "video",
719
- path,
720
- url
713
+ path: video.filePath ?? video.videoUrl ?? ""
721
714
  };
715
+ if (video.videoUrl !== void 0) base.url = video.videoUrl;
716
+ const thumb = video.thumbPath?.get(0);
717
+ if (thumb !== void 0 && thumb !== "") base.thumbPath = thumb;
718
+ if (video.thumbWidth !== void 0 && video.thumbWidth > 0) base.width = video.thumbWidth;
719
+ if (video.thumbHeight !== void 0 && video.thumbHeight > 0) base.height = video.thumbHeight;
720
+ if (video.fileTime !== void 0 && video.fileTime > 0) base.durationSec = video.fileTime;
721
+ return base;
722
722
  }
723
723
  /** FILE 元素 → canonical file。 */
724
724
  function fileToCanonical(file) {
@@ -824,7 +824,7 @@ function toSendElements(elements) {
824
824
  }
825
825
  });
826
826
  break;
827
- case "video":
827
+ case "video": throw kernelError("视频发送需经 MsgApi 预处理(prepareVideoElement)", "INVALID_PARAM");
828
828
  case "file":
829
829
  case "forward":
830
830
  case "json":
@@ -1231,6 +1231,27 @@ const PTT_WAVE_AMPLITUDES = [
1231
1231
  15,
1232
1232
  17
1233
1233
  ];
1234
+ /**
1235
+ * VIDEO 元素发送契约值(2026-10-17 收向实测字段 + 发送实测调通)。
1236
+ *
1237
+ * 依据:历史视频消息 `getLatestDbMsgs` 的 videoElement 实测快照(fileFormat /
1238
+ * busiType / subBusiType / storeID / original / videoFrom / sourceVideoCodecFormat
1239
+ * 均为实测出现的取值)。2026-10-17 18:12 自建宿主实测调通:`sendMsg` 成功
1240
+ * (sendStatus 2/3,群内可播放);关键坑是**缩略图目标路径必须用视频 md5**
1241
+ * (见 prepareVideoElement 注释),与字段取值无关。
1242
+ */
1243
+ const VIDEO_FILE_FORMAT = 0;
1244
+ const VIDEO_BUSI_TYPE = 0;
1245
+ const VIDEO_SUB_BUSI_TYPE = 0;
1246
+ const VIDEO_STORE_ID = 1;
1247
+ const VIDEO_ORIGINAL = true;
1248
+ const VIDEO_FROM = 0;
1249
+ const VIDEO_SOURCE_CODEC_FORMAT = 0;
1250
+ /** 视频缩略图在 thumbPath Map 中的 key(收向实测 Map<0, path>)。 */
1251
+ const VIDEO_THUMB_KEY = 0;
1252
+ /** getRichMediaFilePathForGuild 的 downloadType:1 = 原文件,2 = 缩略图。 */
1253
+ const RICH_MEDIA_DOWNLOAD_TYPE_ORIGIN = 1;
1254
+ const RICH_MEDIA_DOWNLOAD_TYPE_THUMB = 2;
1234
1255
  /** downloadRichMedia 契约值(2026-09-08 B1 实测:downloadType=2 可用 / thumbSize=0)。 */
1235
1256
  const DOWNLOAD_TYPE_COMMON = 2;
1236
1257
  const THUMB_SIZE_NONE = 0;
@@ -1244,20 +1265,20 @@ function sleep$1(ms) {
1244
1265
  });
1245
1266
  }
1246
1267
  /** 读文件 stat(富媒体发送预处理;文件不存在抛 INVALID_PARAM)。 */
1247
- async function statFile(path) {
1268
+ async function statFile(path, label = "图片") {
1248
1269
  try {
1249
1270
  return await stat(path);
1250
1271
  } catch {
1251
- throw kernelError(`图片文件不存在: ${path}`, "INVALID_PARAM");
1272
+ throw kernelError(`${label}文件不存在: ${path}`, "INVALID_PARAM");
1252
1273
  }
1253
1274
  }
1254
1275
  /** 计算文件 md5(十六进制小写)。 */
1255
- async function hashFile(path) {
1276
+ async function hashFile(path, label = "图片") {
1256
1277
  try {
1257
1278
  const buf = await readFile(path);
1258
1279
  return createHash("md5").update(buf).digest("hex");
1259
1280
  } catch {
1260
- throw kernelError(`图片文件读取失败: ${path}`, "INVALID_PARAM");
1281
+ throw kernelError(`${label}文件读取失败: ${path}`, "INVALID_PARAM");
1261
1282
  }
1262
1283
  }
1263
1284
  /**
@@ -1349,6 +1370,9 @@ var MsgApi = class {
1349
1370
  case "voice":
1350
1371
  out.push(await this.preparePttElement(el.path));
1351
1372
  break;
1373
+ case "video":
1374
+ out.push(await this.prepareVideoElement(el));
1375
+ break;
1352
1376
  default: out.push(...toSendElements([el]));
1353
1377
  }
1354
1378
  return out;
@@ -1420,10 +1444,61 @@ var MsgApi = class {
1420
1444
  };
1421
1445
  }
1422
1446
  /**
1423
- * 富媒体文件放置(PIC/PTT 共用):md5 + 文件名 → QQ 内部目标路径
1424
- * (getRichMediaFilePathForGuild)→ util.copyFile 放置 → 返回相对数据根路径。
1447
+ * VIDEO 元素预处理:md5 → 目标路径 → copyFile → 完整 videoElement。
1448
+ *
1449
+ * 字段依据:2026-10-17 收向实测(历史视频消息 videoElement 全字段快照)+
1450
+ * 发送实测(2026-10-17 18:12 自建宿主发送成功,群内可播放):
1451
+ * 1. `thumbPath` 必须是真实 `Map<number, string>`(收向实测 `Map<0, path>`)——
1452
+ * 经 JSON/IPC 传参会退化成 `{}`,wrapper 转换失败;
1453
+ * 2. **缩略图目标路径用视频 md5**(`Video/<YYYY-MM>/Thumb/<videoMd5>_0.png`):
1454
+ * 真实消息实测 videoMd5=`3d27…`、thumbMd5=`46da…`,落盘文件名却是
1455
+ * `3d27…_0.png`;此前按 thumbMd5 放置 → native 传输按视频 md5 找不到缩略图
1456
+ * → `rich media transfer failed`(20+ 组字段变体都无效的真正原因);
1457
+ * 3. `thumbMd5` 是缩略图**内容**的 md5,与文件名无关;`fileTime` 为秒。
1458
+ */
1459
+ async prepareVideoElement(el) {
1460
+ const file = await statFile(el.path, "视频");
1461
+ const md5 = await hashFile(el.path, "视频");
1462
+ const fileName = basename(el.path);
1463
+ const videoElement = {
1464
+ fileName,
1465
+ filePath: await this.placeMediaFile(el.path, ElementType.VIDEO, md5, fileName, RICH_MEDIA_DOWNLOAD_TYPE_ORIGIN),
1466
+ fileSize: String(file.size),
1467
+ videoMd5: md5,
1468
+ fileTime: el.durationSec ?? 0,
1469
+ fileFormat: VIDEO_FILE_FORMAT,
1470
+ busiType: VIDEO_BUSI_TYPE,
1471
+ subBusiType: VIDEO_SUB_BUSI_TYPE,
1472
+ fileUuid: "",
1473
+ fileSubId: "",
1474
+ storeID: VIDEO_STORE_ID,
1475
+ original: VIDEO_ORIGINAL,
1476
+ videoFrom: VIDEO_FROM,
1477
+ sourceVideoCodecFormat: VIDEO_SOURCE_CODEC_FORMAT
1478
+ };
1479
+ if (el.thumbPath !== void 0 && el.thumbPath !== "") {
1480
+ const thumbSize = await statFile(el.thumbPath, "视频缩略图");
1481
+ const thumbMd5 = await hashFile(el.thumbPath, "视频缩略图");
1482
+ const thumbRelPath = await this.placeMediaFile(el.thumbPath, ElementType.VIDEO, md5, `${md5}${extname(el.thumbPath)}`, RICH_MEDIA_DOWNLOAD_TYPE_THUMB);
1483
+ videoElement.thumbMd5 = thumbMd5;
1484
+ videoElement.thumbSize = thumbSize.size;
1485
+ videoElement.thumbWidth = el.width ?? 0;
1486
+ videoElement.thumbHeight = el.height ?? 0;
1487
+ videoElement.thumbPath = /* @__PURE__ */ new Map([[VIDEO_THUMB_KEY, thumbRelPath]]);
1488
+ }
1489
+ return {
1490
+ elementType: ElementType.VIDEO,
1491
+ elementId: "",
1492
+ videoElement
1493
+ };
1494
+ }
1495
+ /**
1496
+ * 富媒体文件放置(PIC/PTT/VIDEO 共用):md5 + 文件名 → QQ 内部目标路径
1497
+ * (getRichMediaFilePathForGuild)→ util.copyFile 放置 → 返回目标路径。
1498
+ *
1499
+ * `downloadType`:1 = 原文件(Ori),2 = 缩略图(Thumb,视频缩略图必须)。
1425
1500
  */
1426
- async placeMediaFile(path, elementType, md5, fileName) {
1501
+ async placeMediaFile(path, elementType, md5, fileName, downloadType = RICH_MEDIA_DOWNLOAD_TYPE_ORIGIN) {
1427
1502
  const service = this.service;
1428
1503
  const util = this.util;
1429
1504
  const relPath = service.getRichMediaFilePathForGuild({
@@ -1433,7 +1508,7 @@ var MsgApi = class {
1433
1508
  elementSubType: 0,
1434
1509
  thumbSize: 0,
1435
1510
  needCreate: true,
1436
- downloadType: 1,
1511
+ downloadType,
1437
1512
  file_uuid: ""
1438
1513
  });
1439
1514
  if (util !== null) {