koishi-plugin-chat-patch 5.3.0 → 5.5.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.
Files changed (77) hide show
  1. package/client/web/dist/assets/Chat-Clqxn7CB.js +28 -0
  2. package/client/web/dist/assets/{Chat-CTdyeaX2.css → Chat-DTEsebnY.css} +1 -1
  3. package/client/web/dist/assets/{MsgBody-DDacjWJ9.js → MsgBody-C0TKsXhC.js} +1 -1
  4. package/client/web/dist/assets/{MsgBody.vue_vue_type_script_setup_true_lang-B1dWyCB0.js → MsgBody.vue_vue_type_script_setup_true_lang-CCdBwlN_.js} +2 -2
  5. package/client/web/dist/assets/{append_darwin-CqBNU62J.css → append_darwin-Cql-dV6d.css} +1 -1
  6. package/client/web/dist/assets/{append_full_vibrancy-j0D2LM9m.css → append_full_vibrancy-ih3loy1O.css} +1 -1
  7. package/client/web/dist/assets/{append_linux-uhxeBqeC.css → append_linux-B3LvR8KB.css} +1 -1
  8. package/client/web/dist/assets/{append_new-407Lo5_V.css → append_new-DfhN_NX9.css} +1 -1
  9. package/client/web/dist/assets/{append_vibrancy-6FHctkOY.css → append_vibrancy-D3pVsmAa.css} +1 -1
  10. package/client/web/dist/assets/{append_win32-srmD-78y.css → append_win32-Dh9JlmlE.css} +1 -1
  11. package/client/web/dist/assets/index-B71pPwae.css +1 -0
  12. package/client/web/dist/assets/{index-BlLut5xn.js → index-BA37EQGa.js} +97 -76
  13. package/client/web/dist/index.html +2 -2
  14. package/client/web/src/App.vue +2 -2
  15. package/client/web/src/assets/css/append/append_darwin.css +2 -2
  16. package/client/web/src/assets/css/append/append_full_vibrancy.css +2 -2
  17. package/client/web/src/assets/css/append/append_linux.css +2 -2
  18. package/client/web/src/assets/css/append/append_new.css +2 -2
  19. package/client/web/src/assets/css/append/append_vibrancy.css +1 -1
  20. package/client/web/src/assets/css/append/append_win32.css +2 -2
  21. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +1 -1
  22. package/client/web/src/assets/css/chat.css +4 -3
  23. package/client/web/src/assets/css/msg.css +7 -3
  24. package/client/web/src/assets/css/options.css +2 -2
  25. package/client/web/src/assets/css/view.css +14 -3
  26. package/client/web/src/assets/l10n/zh-CN.po +21 -0
  27. package/client/web/src/components/AboutPan.vue +7 -4
  28. package/client/web/src/components/BulletinBody.vue +1 -1
  29. package/client/web/src/components/DepPan.vue +1 -1
  30. package/client/web/src/components/FriendBody.vue +1 -0
  31. package/client/web/src/components/MsgBody.vue +1 -3
  32. package/client/web/src/components/ThemeColorPickerPan.vue +1 -1
  33. package/client/web/src/components/TinySessionBody.vue +1 -0
  34. package/client/web/src/components/UmamiInfoPan.vue +1 -1
  35. package/client/web/src/components/UpdatePan.vue +2 -2
  36. package/client/web/src/components/WelPan.vue +1 -1
  37. package/client/web/src/components/tooltip/UserInfoTooltip.vue +1 -1
  38. package/client/web/src/function/connect.ts +279 -86
  39. package/client/web/src/function/elements/information.ts +1 -0
  40. package/client/web/src/function/msg.ts +42 -23
  41. package/client/web/src/function/satori-model.ts +35 -13
  42. package/client/web/src/function/satori.ts +33 -2
  43. package/client/web/src/function/utils/appUtil.ts +3 -11
  44. package/client/web/src/function/utils/avatarUtil.ts +18 -3
  45. package/client/web/src/function/utils/localHistoryUtil.ts +36 -12
  46. package/client/web/src/function/utils/msgUtil.ts +32 -21
  47. package/client/web/src/function/utils/sessionUtil.ts +20 -18
  48. package/client/web/src/main.ts +5 -0
  49. package/client/web/src/pages/Chat.vue +50 -47
  50. package/client/web/src/pages/Friends.vue +28 -24
  51. package/client/web/src/pages/Info.vue +5 -5
  52. package/client/web/src/pages/Messages.vue +57 -36
  53. package/client/web/src/pages/Qzone.vue +2 -2
  54. package/client/web/src/pages/chat-view/ChatTerminal.vue +4 -1
  55. package/client/web/src/pages/chat-view/SystemNotice.vue +3 -3
  56. package/client/web/src/pages/options/OptAccount.vue +6 -4
  57. package/client/web/src/pages/options/OptDev.vue +45 -2
  58. package/lib/database.d.ts +58 -0
  59. package/lib/index.js +234 -82
  60. package/lib/media.d.ts +24 -0
  61. package/lib/recorder.d.ts +21 -0
  62. package/lib/self-message.d.ts +27 -0
  63. package/lib/types.d.ts +121 -0
  64. package/lib/web.d.ts +7 -0
  65. package/package.json +1 -1
  66. package/src/bootstrap.ts +1 -0
  67. package/src/cache.ts +1 -5
  68. package/src/config.ts +1 -1
  69. package/src/database.ts +52 -8
  70. package/src/index.ts +4 -4
  71. package/src/media.ts +97 -0
  72. package/src/recorder.ts +35 -26
  73. package/src/self-message.ts +68 -19
  74. package/src/types.ts +10 -0
  75. package/src/web.ts +57 -48
  76. package/client/web/dist/assets/Chat-B448YOeN.js +0 -28
  77. package/client/web/dist/assets/index-fW_zcub3.css +0 -1
package/lib/index.js CHANGED
@@ -72,9 +72,7 @@ function isUsableName(value, id) {
72
72
  }
73
73
  __name(isUsableName, "isUsableName");
74
74
  function normalizeGroupId(value) {
75
- const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, "").trim();
76
- const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/);
77
- return wrapped ? wrapped[1] : raw || value;
75
+ return String(value);
78
76
  }
79
77
  __name(normalizeGroupId, "normalizeGroupId");
80
78
  function isPrivateChannelType(value) {
@@ -170,7 +168,6 @@ var _ContactCacheService = class _ContactCacheService {
170
168
  const guild = guildId && guildId !== "0" ? guildId : void 0;
171
169
  const channel = channelId && channelId !== "0" ? channelId : void 0;
172
170
  if (isPrivateChannelType(channelType)) return null;
173
- if (channelType === void 0 && (/^(?:private|direct):/i.test(id) || /^(?:private|direct):/i.test(channel || ""))) return null;
174
171
  const rawId = id && id !== "0" ? normalizeGroupId(id) : "";
175
172
  const cacheId = rawId || guild || normalizeGroupId(channel || "") || "";
176
173
  if (!cacheId) return null;
@@ -353,10 +350,6 @@ function legacyGroupMemberKey(platform, selfId, groupId) {
353
350
  return `gm:${platform}:${selfId}:${groupId}`;
354
351
  }
355
352
  __name(legacyGroupMemberKey, "legacyGroupMemberKey");
356
- function isPrivateChannelId(value) {
357
- return typeof value === "string" && /^(?:private|direct):/i.test(value);
358
- }
359
- __name(isPrivateChannelId, "isPrivateChannelId");
360
353
  function isPrivateChannelType2(value) {
361
354
  const num = Number(value);
362
355
  if (Number.isFinite(num)) return num === 1;
@@ -365,10 +358,9 @@ function isPrivateChannelType2(value) {
365
358
  }
366
359
  __name(isPrivateChannelType2, "isPrivateChannelType");
367
360
  function isUsableGroupContact(item) {
368
- if (isPrivateChannelId(item.channelId)) return false;
369
361
  if (typeof item.raw === "object" && item.raw !== null) {
370
362
  const raw = item.raw;
371
- if (isPrivateChannelType2(raw.channel_type) || isPrivateChannelId(raw.channel_id) || isPrivateChannelId(raw.channelId)) return false;
363
+ if (isPrivateChannelType2(raw.channel_type)) return false;
372
364
  }
373
365
  return true;
374
366
  }
@@ -393,6 +385,12 @@ var _ChatDatabase = class _ChatDatabase {
393
385
  this.opened = false;
394
386
  this.logger.logInfo("LevelDB closed");
395
387
  }
388
+ async clearAll() {
389
+ await this.db.clear();
390
+ const db = this.db;
391
+ await db.compactRange("", "￿");
392
+ this.logger.logInfo("数据库缓存已全部清空并完成压缩");
393
+ }
396
394
  async appendMessage(record) {
397
395
  await this.db.put(messageKey(record), JSON.stringify(record));
398
396
  await this.trimMessages(record.platform, record.selfId, record.channelId || "");
@@ -458,6 +456,33 @@ var _ChatDatabase = class _ChatDatabase {
458
456
  }
459
457
  return false;
460
458
  }
459
+ async updateMessageRevoked(platform, selfId, channelId, messageId, patch) {
460
+ const operations = [];
461
+ for (const prefix of [messagePrefix(platform, selfId, channelId), legacyMessagePrefix(platform, selfId, channelId)]) {
462
+ for await (const [key, value] of this.db.iterator({
463
+ gte: prefix,
464
+ lte: `${prefix}￿`
465
+ })) {
466
+ try {
467
+ const parsed = JSON.parse(value);
468
+ if (typeof parsed !== "object" || parsed === null) continue;
469
+ const record = parsed;
470
+ const raw = typeof record.raw === "object" && record.raw !== null ? record.raw : {};
471
+ const rawMessage = typeof raw.message === "object" && raw.message !== null ? raw.message : {};
472
+ const id = String(record.id ?? rawMessage.id ?? "");
473
+ if (!id || id !== messageId) continue;
474
+ operations.push({
475
+ type: "put",
476
+ key,
477
+ value: JSON.stringify({ ...parsed, ...patch })
478
+ });
479
+ } catch {
480
+ }
481
+ }
482
+ }
483
+ if (operations.length) await this.db.batch(operations);
484
+ return operations.length > 0;
485
+ }
461
486
  async findSelfForwardContent(platform, selfId, channelId, id) {
462
487
  const prefixes = channelId ? [selfMessagePrefix(platform, selfId, channelId)] : [`sm:${encodeKeyPart2(platform)}:${encodeKeyPart2(selfId)}:`];
463
488
  for (const prefix of prefixes) {
@@ -818,9 +843,7 @@ var ChatDatabase = _ChatDatabase;
818
843
  // src/recorder.ts
819
844
  var MESSAGE_TYPES = /* @__PURE__ */ new Set(["message", "message-created"]);
820
845
  function normalizeGroupId2(value) {
821
- const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, "").trim();
822
- const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/);
823
- return wrapped ? wrapped[1] : raw || value;
846
+ return String(value);
824
847
  }
825
848
  __name(normalizeGroupId2, "normalizeGroupId");
826
849
  var _Recorder = class _Recorder {
@@ -834,6 +857,12 @@ var _Recorder = class _Recorder {
834
857
  }
835
858
  start() {
836
859
  this.ctx.on("internal/session", (session) => {
860
+ if (session.type === "message-deleted") {
861
+ void this.handleMessageDeleted(session).catch((error) => {
862
+ this.logger.warn("处理消息撤回失败:", error);
863
+ });
864
+ return;
865
+ }
837
866
  void this.handleSession(session);
838
867
  });
839
868
  }
@@ -844,8 +873,8 @@ var _Recorder = class _Recorder {
844
873
  }
845
874
  async handleSession(session) {
846
875
  const platform = session.platform || "unknown";
847
- if (this.isBlocked(platform)) return;
848
876
  if (!MESSAGE_TYPES.has(session.type)) return;
877
+ if (this.isBlocked(platform)) return;
849
878
  const event = session.toJSON();
850
879
  const message = event.message;
851
880
  const record = {
@@ -866,15 +895,29 @@ var _Recorder = class _Recorder {
866
895
  };
867
896
  try {
868
897
  await this.database.appendMessage(record);
869
- void this.cacheMessageContacts(event).catch((error) => {
870
- this.logger.warn("缓存消息联系人失败:", error);
871
- });
872
- void this.cacheMessageMedia(event).catch((error) => {
873
- this.logger.warn("异步缓存消息媒体失败:", error);
874
- });
875
898
  } catch (error) {
876
899
  this.logger.warn("写入历史消息失败:", error);
877
900
  }
901
+ void this.cacheMessageContacts(event).catch((error) => {
902
+ this.logger.warn("缓存消息联系人失败:", error);
903
+ });
904
+ void this.cacheMessageMedia(event).catch((error) => {
905
+ this.logger.warn("异步缓存消息媒体失败:", error);
906
+ });
907
+ }
908
+ async handleMessageDeleted(session) {
909
+ const platform = session.platform || "unknown";
910
+ if (this.isBlocked(platform)) return;
911
+ const event = session.toJSON();
912
+ const rawMessage = typeof event.message === "object" && event.message !== null ? event.message : {};
913
+ const messageId = session.messageId || String(rawMessage.id ?? "");
914
+ const channelId = String(
915
+ event.channel?.id || event.guild?.id || session.channelId || ""
916
+ );
917
+ if (!messageId || !channelId) return;
918
+ const patch = { revoked: true, revokedAt: Date.now() };
919
+ await this.database.updateMessageRevoked(platform, session.selfId, channelId, messageId, patch);
920
+ await this.database.updateSelfMessageByMessageId(platform, session.selfId, channelId, messageId, patch);
878
921
  }
879
922
  async cacheMessageContacts(event) {
880
923
  const platform = event.platform || "";
@@ -887,8 +930,8 @@ var _Recorder = class _Recorder {
887
930
  const guildId = guild?.id || "";
888
931
  const channelId = channel?.id || "";
889
932
  const channelType = String(channel?.type ?? "");
890
- const isPrivateChannel2 = channelType === "1" || ["direct", "private"].includes(channelType.toLowerCase()) || !channelType && /^private:/i.test(channelId);
891
- const groupId = guildId || (isPrivateChannel2 ? "" : normalizeGroupId2(channelId)) || "";
933
+ const isPrivateChannel = channelType === "1" || ["direct", "private"].includes(channelType.toLowerCase());
934
+ const groupId = guildId || (isPrivateChannel ? "" : normalizeGroupId2(channelId)) || "";
892
935
  const userName = user?.name || user?.nick || member?.nick || member?.name || "";
893
936
  const userAvatar = user?.avatar || member?.avatar || "";
894
937
  const groupName = guild?.name || channel?.name || "";
@@ -947,24 +990,10 @@ var _Recorder = class _Recorder {
947
990
  }
948
991
  }
949
992
  async cacheMessageMedia(event) {
950
- const elements = Array.isArray(event.message?.elements) ? event.message.elements : [];
951
993
  const channelId = String(
952
994
  event.channel?.id || event.guild?.id || ""
953
995
  );
954
- const urls = [];
955
- const collect = /* @__PURE__ */ __name((list) => {
956
- for (const raw of list) {
957
- const element = typeof raw === "object" && raw !== null ? raw : {};
958
- const attrs = typeof element.attrs === "object" && element.attrs !== null ? element.attrs : {};
959
- if (["img", "image", "mface", "audio", "video", "file"].includes(String(element.type))) {
960
- const url = String(attrs.src ?? attrs.url ?? attrs.file ?? "");
961
- if (url) urls.push(url);
962
- }
963
- if (Array.isArray(element.children)) collect(element.children);
964
- }
965
- }, "collect");
966
- collect(elements);
967
- await Promise.allSettled(urls.map((url) => this.media.cacheUrl(url, channelId)));
996
+ await this.media.cacheMessageMedia(event.message, channelId);
968
997
  }
969
998
  };
970
999
  __name(_Recorder, "Recorder");
@@ -993,6 +1022,68 @@ function toLocalFilePath(value) {
993
1022
  return null;
994
1023
  }
995
1024
  __name(toLocalFilePath, "toLocalFilePath");
1025
+ var MEDIA_TYPES = /* @__PURE__ */ new Set([
1026
+ "img",
1027
+ "image",
1028
+ "face",
1029
+ "mface",
1030
+ "audio",
1031
+ "record",
1032
+ "video",
1033
+ "file"
1034
+ ]);
1035
+ function isCacheableMediaUrl(value) {
1036
+ const url = String(value ?? "").trim();
1037
+ if (!url || url.startsWith("data:") || url.startsWith("blob:") || url.startsWith("base64://")) return "";
1038
+ return url;
1039
+ }
1040
+ __name(isCacheableMediaUrl, "isCacheableMediaUrl");
1041
+ function isUrlLike(value) {
1042
+ return /^(?:https?:\/\/|file:\/\/|[a-z]:[\\/]|\\\\)/i.test(value);
1043
+ }
1044
+ __name(isUrlLike, "isUrlLike");
1045
+ function decodeMarkupValue(value) {
1046
+ return value.replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
1047
+ }
1048
+ __name(decodeMarkupValue, "decodeMarkupValue");
1049
+ function collectMarkupMediaUrls(markup, result) {
1050
+ const pattern = /<(?:img|image|face|mface|audio|record|video|file)\b[^>]*?\b(?:src|url|file|href)\s*=\s*(["'])([\s\S]*?)\1/gi;
1051
+ let match;
1052
+ while ((match = pattern.exec(markup)) !== null) {
1053
+ const url = isCacheableMediaUrl(decodeMarkupValue(match[2]));
1054
+ if (url) result.add(url);
1055
+ }
1056
+ }
1057
+ __name(collectMarkupMediaUrls, "collectMarkupMediaUrls");
1058
+ function collectMediaUrls(source, result) {
1059
+ if (typeof source === "string") {
1060
+ const direct = isCacheableMediaUrl(source);
1061
+ if (direct && isUrlLike(direct)) result.add(direct);
1062
+ collectMarkupMediaUrls(source, result);
1063
+ try {
1064
+ collectMediaUrls(JSON.parse(source), result);
1065
+ } catch {
1066
+ }
1067
+ return;
1068
+ }
1069
+ if (Array.isArray(source)) {
1070
+ for (const item of source) collectMediaUrls(item, result);
1071
+ return;
1072
+ }
1073
+ if (typeof source !== "object" || source === null) return;
1074
+ const record = source;
1075
+ const attrs = typeof record.attrs === "object" && record.attrs !== null ? record.attrs : {};
1076
+ if (MEDIA_TYPES.has(String(record.type ?? ""))) {
1077
+ for (const key of ["src", "url", "file", "href", "path"]) {
1078
+ const url = isCacheableMediaUrl(attrs[key]);
1079
+ if (url) result.add(url);
1080
+ }
1081
+ }
1082
+ for (const key of ["elements", "children", "content", "data", "html", "message"]) {
1083
+ collectMediaUrls(attrs[key] ?? record[key], result);
1084
+ }
1085
+ }
1086
+ __name(collectMediaUrls, "collectMediaUrls");
996
1087
  var _MediaManager = class _MediaManager {
997
1088
  constructor(ctx, config, database, logger) {
998
1089
  this.ctx = ctx;
@@ -1062,6 +1153,15 @@ var _MediaManager = class _MediaManager {
1062
1153
  dispose() {
1063
1154
  this.cleanupTimer?.();
1064
1155
  }
1156
+ async clearAll() {
1157
+ const root = import_node_path2.default.resolve(this.ctx.baseDir, "data", "chat-patch");
1158
+ for (const dir of [this.mediaDir, this.uploadDir]) {
1159
+ const resolved = import_node_path2.default.resolve(dir);
1160
+ if (resolved !== root && !resolved.startsWith(`${root}${import_node_path2.default.sep}`)) continue;
1161
+ await import_node_fs.promises.rm(resolved, { recursive: true, force: true });
1162
+ }
1163
+ this.logger.logInfo("媒体缓存目录已全部清空");
1164
+ }
1065
1165
  async cacheUrl(url, channelId = "") {
1066
1166
  try {
1067
1167
  const filePath = await this.resolveAndDownload(url, channelId);
@@ -1074,6 +1174,16 @@ var _MediaManager = class _MediaManager {
1074
1174
  return null;
1075
1175
  }
1076
1176
  }
1177
+ // 收到或发送消息时立即预缓存富媒体,避免等前端渲染时原 URL 已过期
1178
+ async cacheMessageMedia(source, channelId = "") {
1179
+ const urls = /* @__PURE__ */ new Set();
1180
+ collectMediaUrls(source, urls);
1181
+ await Promise.allSettled([...urls].map((url) => this.cacheUrl(url, channelId)));
1182
+ if (urls.size) {
1183
+ this.logger.logInfo("消息媒体已预缓存:", urls.size, "个 URL");
1184
+ }
1185
+ return [...urls];
1186
+ }
1077
1187
  async resolveAndDownload(url, channelId) {
1078
1188
  try {
1079
1189
  await import_node_fs.promises.mkdir(this.mediaDir, { recursive: true });
@@ -1251,6 +1361,11 @@ function getString2(value) {
1251
1361
  return typeof value === "string" ? value : "";
1252
1362
  }
1253
1363
  __name(getString2, "getString");
1364
+ function getMessageId(value) {
1365
+ const raw = getObject(value).id ?? getObject(value).message_id;
1366
+ return raw == null ? "" : String(raw);
1367
+ }
1368
+ __name(getMessageId, "getMessageId");
1254
1369
  function getNumber(value) {
1255
1370
  const num = Number(value);
1256
1371
  return Number.isFinite(num) ? num : 0;
@@ -1262,7 +1377,8 @@ function getObject(value) {
1262
1377
  __name(getObject, "getObject");
1263
1378
  function toSatoriElement(value) {
1264
1379
  const raw = getObject(value);
1265
- const attrs = typeof raw.attrs === "object" && raw.attrs !== null ? raw.attrs : {};
1380
+ const rawAttrs = raw.attrs ?? raw.data;
1381
+ const attrs = typeof rawAttrs === "object" && rawAttrs !== null ? rawAttrs : {};
1266
1382
  const children = Array.isArray(raw.children) ? raw.children.map(toSatoriElement) : [];
1267
1383
  return {
1268
1384
  type: raw.type,
@@ -1275,10 +1391,6 @@ function isForwardContainer(element) {
1275
1391
  return element.type === "figure" || element.type === "forward" || element.type === "message" && ("forward" in (element.attrs ?? {}) || element.attrs?.forward === true);
1276
1392
  }
1277
1393
  __name(isForwardContainer, "isForwardContainer");
1278
- function isPrivateChannel(value) {
1279
- return /^(?:private|direct):/i.test(value);
1280
- }
1281
- __name(isPrivateChannel, "isPrivateChannel");
1282
1394
  function toSegments(elements, resolveI18n) {
1283
1395
  const result = [];
1284
1396
  for (const raw of elements) {
@@ -1286,9 +1398,9 @@ function toSegments(elements, resolveI18n) {
1286
1398
  const attrs = element.attrs ?? {};
1287
1399
  const type = getString2(element.type);
1288
1400
  if (type === "text") {
1289
- result.push({ type: "text", text: getString2(attrs.content) });
1401
+ result.push({ type: "text", text: getString2(attrs.content) || getString2(attrs.text) });
1290
1402
  } else if (type === "at") {
1291
- const id = getString2(attrs.id);
1403
+ const id = getString2(attrs.id) || getString2(attrs.qq);
1292
1404
  const name2 = getString2(attrs.name) || (attrs.type === "all" ? "所有人" : id);
1293
1405
  result.push({ type: "at", qq: id, text: name2.startsWith("@") ? name2 : `@${name2}` });
1294
1406
  } else if (type === "img" || type === "image") {
@@ -1400,10 +1512,11 @@ function forwardId(elements) {
1400
1512
  }
1401
1513
  __name(forwardId, "forwardId");
1402
1514
  var _SelfMessageRecorder = class _SelfMessageRecorder {
1403
- constructor(ctx, config, database, logger) {
1515
+ constructor(ctx, config, database, media, logger) {
1404
1516
  this.ctx = ctx;
1405
1517
  this.config = config;
1406
1518
  this.database = database;
1519
+ this.media = media;
1407
1520
  this.logger = logger;
1408
1521
  this.wrapped = /* @__PURE__ */ new Map();
1409
1522
  this.sequence = 0;
@@ -1434,7 +1547,8 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1434
1547
  const methods = {
1435
1548
  createMessage: bot.createMessage,
1436
1549
  sendMessage: bot.sendMessage,
1437
- sendPrivateMessage: bot.sendPrivateMessage
1550
+ sendPrivateMessage: bot.sendPrivateMessage,
1551
+ deleteMessage: bot.deleteMessage
1438
1552
  };
1439
1553
  this.wrapped.set(bot, methods);
1440
1554
  bot.createMessage = async (channelId, content, referrer, options) => {
@@ -1446,13 +1560,17 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1446
1560
  sendOptions.chatPatchId = localId;
1447
1561
  try {
1448
1562
  const messages = await methods.createMessage.call(bot, channelId, content, referrer, sendOptions);
1449
- const ids = Array.isArray(messages) ? messages.map((item) => getString2(getObject(item).id)).filter(Boolean) : [];
1563
+ const ids = Array.isArray(messages) ? messages.map((item) => getMessageId(item)).filter((id) => id !== "") : [];
1450
1564
  await this.recordFromWrapper(bot, channelId, content, ids, options, "create", localId);
1451
1565
  return messages;
1452
1566
  } finally {
1453
1567
  delete sendOptions.chatPatchId;
1454
1568
  }
1455
1569
  };
1570
+ bot.deleteMessage = async (channelId, messageId) => {
1571
+ await methods.deleteMessage.call(bot, channelId, messageId);
1572
+ await this.markSelfMessageRevoked(bot, channelId, messageId);
1573
+ };
1456
1574
  bot.sendMessage = async (channelId, content, referrer, options) => {
1457
1575
  const sendOptions = this.prepareOptions(options);
1458
1576
  if (sendOptions.chatPatchId) {
@@ -1461,7 +1579,7 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1461
1579
  const localId = (0, import_node_crypto3.randomUUID)();
1462
1580
  sendOptions.chatPatchId = localId;
1463
1581
  try {
1464
- const ids = await methods.sendMessage.call(bot, channelId, content, referrer, sendOptions);
1582
+ const ids = (await methods.sendMessage.call(bot, channelId, content, referrer, sendOptions)).filter((id) => id !== "");
1465
1583
  await this.recordFromWrapper(bot, channelId, content, ids, options, "send", localId);
1466
1584
  return ids;
1467
1585
  } finally {
@@ -1476,7 +1594,7 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1476
1594
  const localId = (0, import_node_crypto3.randomUUID)();
1477
1595
  sendOptions.chatPatchId = localId;
1478
1596
  try {
1479
- const ids = await methods.sendPrivateMessage.call(bot, userId, content, guildId, sendOptions);
1597
+ const ids = (await methods.sendPrivateMessage.call(bot, userId, content, guildId, sendOptions)).filter((id) => id !== "");
1480
1598
  await this.recordFromWrapper(bot, userId, content, ids, options, "private", localId);
1481
1599
  return ids;
1482
1600
  } finally {
@@ -1490,6 +1608,7 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1490
1608
  bot.createMessage = methods.createMessage;
1491
1609
  bot.sendMessage = methods.sendMessage;
1492
1610
  bot.sendPrivateMessage = methods.sendPrivateMessage;
1611
+ bot.deleteMessage = methods.deleteMessage;
1493
1612
  this.wrapped.delete(bot);
1494
1613
  }
1495
1614
  prepareOptions(options) {
@@ -1497,6 +1616,10 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1497
1616
  }
1498
1617
  async recordFromWrapper(bot, channelId, content, ids, options, mode, localId) {
1499
1618
  if (!channelId || this.isBlocked(bot.platform ?? "")) return;
1619
+ if (!ids.length) {
1620
+ this.logger.warn("发送未返回消息 ID,判定为发送失败:", bot.platform, channelId);
1621
+ return;
1622
+ }
1500
1623
  const source = options?.session ? "plugin" : "bot";
1501
1624
  const record = this.buildRecord(
1502
1625
  bot.platform ?? "",
@@ -1510,8 +1633,24 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1510
1633
  localId
1511
1634
  );
1512
1635
  await this.database.upsertSelfMessage(record);
1636
+ void this.media.cacheMessageMedia(content, channelId).catch((error) => {
1637
+ this.logger.warn("缓存机器人消息媒体失败:", error);
1638
+ });
1513
1639
  this.logger.logInfo("机器人消息已记录:", record.kind, record.channelId, record.messageId || record.id);
1514
1640
  }
1641
+ async markSelfMessageRevoked(bot, channelId, messageId) {
1642
+ if (!messageId || !channelId || this.isBlocked(bot.platform ?? "")) return;
1643
+ const updated = await this.database.updateSelfMessageByMessageId(
1644
+ bot.platform ?? "",
1645
+ bot.selfId,
1646
+ channelId,
1647
+ messageId,
1648
+ { revoked: true, revokedAt: Date.now() }
1649
+ );
1650
+ if (!updated) {
1651
+ this.logger.warn("撤回消息未找到本地自消息记录:", bot.platform, channelId, messageId);
1652
+ }
1653
+ }
1515
1654
  // 收到 send 事件时只补消息 id,避免重复记录
1516
1655
  async recordSendEvent(session) {
1517
1656
  if (session.type !== "send") return;
@@ -1522,7 +1661,7 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1522
1661
  const eventMessage = getObject(event.message);
1523
1662
  const channelId = session.channelId || getString2(eventChannel.id);
1524
1663
  if (this.isBlocked(platform) || !platform || !selfId || !channelId) return;
1525
- const messageId = session.messageId || getString2(eventMessage.id);
1664
+ const messageId = session.messageId || getMessageId(eventMessage);
1526
1665
  if (messageId) {
1527
1666
  const updated = await this.database.updateSelfMessageByMessageId(
1528
1667
  platform,
@@ -1547,6 +1686,9 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1547
1686
  (0, import_node_crypto3.randomUUID)()
1548
1687
  );
1549
1688
  await this.database.upsertSelfMessage(record);
1689
+ void this.media.cacheMessageMedia(session.content ?? "", channelId).catch((error) => {
1690
+ this.logger.warn("缓存 send 事件媒体失败:", error);
1691
+ });
1550
1692
  }
1551
1693
  resolveI18nElement(attrs) {
1552
1694
  const path5 = getString2(attrs.path);
@@ -1562,8 +1704,8 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1562
1704
  return `[${path5}]`;
1563
1705
  }
1564
1706
  buildRecord(platform, selfId, channelId, guildId, content, messageId, source, mode, localId) {
1565
- const normalizedChannelId = mode === "private" && !isPrivateChannel(channelId) ? `private:${channelId}` : channelId;
1566
- const channelType = mode === "private" || isPrivateChannel(normalizedChannelId) ? "user" : "group";
1707
+ const normalizedChannelId = channelId;
1708
+ const channelType = mode === "private" ? "user" : "group";
1567
1709
  const elements = this.normalizeElements(content);
1568
1710
  const contentText = typeof content === "string" ? content : import_koishi.h.toElementArray(content).join("");
1569
1711
  const fingerprint = (0, import_node_crypto3.createHash)("sha256").update([
@@ -1594,7 +1736,14 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
1594
1736
  }
1595
1737
  normalizeElements(content) {
1596
1738
  try {
1597
- const elements = typeof content === "string" ? import_koishi.h.parse(content) : import_koishi.h.toElementArray(content);
1739
+ const normalized = typeof content === "string" ? content : Array.isArray(content) ? content.map((raw) => {
1740
+ const item = getObject(raw);
1741
+ if (typeof item.data === "object" && item.data !== null && item.attrs === void 0) {
1742
+ return (0, import_koishi.h)(getString2(item.type), item.data);
1743
+ }
1744
+ return raw;
1745
+ }) : content;
1746
+ const elements = typeof normalized === "string" ? import_koishi.h.parse(normalized) : import_koishi.h.toElementArray(normalized);
1598
1747
  return JSON.parse(JSON.stringify(elements));
1599
1748
  } catch {
1600
1749
  return [];
@@ -1622,7 +1771,8 @@ function registerBootstrap(ctx, config, database, logger) {
1622
1771
  return {
1623
1772
  endpoint: resolveEndpoint(ctx, config),
1624
1773
  token: ctx.satori?.server?.config?.token ?? "",
1625
- basePath: config.basePath
1774
+ basePath: config.basePath,
1775
+ blockedPlatforms: config.blockedPlatforms ?? []
1626
1776
  };
1627
1777
  }, { authority: 4 });
1628
1778
  ctx.console.addListener("chat-patch/history", async (query) => {
@@ -1667,7 +1817,7 @@ var import_node_path3 = __toESM(require("node:path"));
1667
1817
  var import_node_url2 = require("node:url");
1668
1818
  var import_koa_send = __toESM(require("koa-send"));
1669
1819
  var import_file_type2 = __toESM(require("file-type"));
1670
- function registerWeb(ctx, config, database, contactCache, logger) {
1820
+ function registerWeb(ctx, config, database, contactCache, media, logger) {
1671
1821
  const webRoot = import_node_path3.default.resolve(__dirname, "..", "client", "web", "dist");
1672
1822
  const webSource = import_node_path3.default.resolve(__dirname, "..", "client", "web", "src");
1673
1823
  const webPublic = import_node_path3.default.resolve(__dirname, "..", "client", "web", "public");
@@ -1706,34 +1856,21 @@ function registerWeb(ctx, config, database, contactCache, logger) {
1706
1856
  koa.status = 404;
1707
1857
  koa.body = "";
1708
1858
  }, "servePublicFile");
1709
- const registerDevPublic = /* @__PURE__ */ __name((pattern, prefix) => {
1710
- ctx.server.get(pattern, async (koa, next) => {
1711
- if (!getVite()) return next();
1859
+ const registerPublic = /* @__PURE__ */ __name((pattern, prefix) => {
1860
+ ctx.server.get(pattern, async (koa) => {
1712
1861
  await servePublicFile(koa, prefix);
1713
1862
  });
1714
- }, "registerDevPublic");
1715
- registerDevPublic("/img(.*)", "img");
1863
+ }, "registerPublic");
1864
+ registerPublic("/img(.*)", "img");
1716
1865
  const toExtension2 = /* @__PURE__ */ __name((value) => {
1717
1866
  const ext = value.startsWith(".") ? value : `.${value}`;
1718
1867
  return ext.toLowerCase();
1719
1868
  }, "toExtension");
1720
1869
  const normalizeGroupId3 = /* @__PURE__ */ __name((value) => {
1721
- const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, "").trim();
1722
- const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/);
1723
- return wrapped ? wrapped[1] : raw || value;
1870
+ return String(value);
1724
1871
  }, "normalizeGroupId");
1725
1872
  const historyChannelCandidates = /* @__PURE__ */ __name((channelId) => {
1726
- const raw = normalizeGroupId3(channelId);
1727
- const candidates = [channelId];
1728
- if (raw && raw !== channelId) candidates.push(raw);
1729
- const id = raw || channelId;
1730
- if (!/^(?:group|room|chat|channel|guild|private):/i.test(channelId)) {
1731
- candidates.push(`group:${id}`, `private:${id}`);
1732
- }
1733
- if (id) {
1734
- candidates.push(` [_${id}_] `, `_${id}_`, `[${id}]`, `group:${id}`);
1735
- }
1736
- return [...new Set(candidates)];
1873
+ return channelId ? [channelId] : [];
1737
1874
  }, "historyChannelCandidates");
1738
1875
  const mimeToExt = {
1739
1876
  "audio/mpeg": ".mp3",
@@ -1973,7 +2110,7 @@ function registerWeb(ctx, config, database, contactCache, logger) {
1973
2110
  channelId: String(body.channelId ?? ""),
1974
2111
  guildId: typeof body.guildId === "string" ? body.guildId : void 0,
1975
2112
  channelType: body.channelType === "user" ? "user" : "group",
1976
- messageId: typeof body.messageId === "string" ? body.messageId : void 0,
2113
+ messageId: typeof body.messageId === "string" || typeof body.messageId === "number" ? String(body.messageId) : void 0,
1977
2114
  content: typeof body.content === "string" ? body.content : void 0,
1978
2115
  elements: Array.isArray(body.elements) ? body.elements : void 0,
1979
2116
  message: Array.isArray(body.message) ? body.message : void 0,
@@ -1982,7 +2119,9 @@ function registerWeb(ctx, config, database, contactCache, logger) {
1982
2119
  sentAt: typeof body.sentAt === "number" && Number.isFinite(body.sentAt) ? body.sentAt : Date.now(),
1983
2120
  sequence: typeof body.sequence === "number" && Number.isFinite(body.sequence) ? body.sequence : 0,
1984
2121
  source: body.source === "plugin" ? "plugin" : "webui",
1985
- kind: typeof body.kind === "string" ? body.kind : "text"
2122
+ kind: typeof body.kind === "string" ? body.kind : "text",
2123
+ revoked: body.revoked === true,
2124
+ revokedAt: typeof body.revokedAt === "number" && Number.isFinite(body.revokedAt) ? body.revokedAt : void 0
1986
2125
  };
1987
2126
  if (!payload.platform || !payload.selfId || !payload.channelId) {
1988
2127
  koa.status = 400;
@@ -2005,7 +2144,9 @@ function registerWeb(ctx, config, database, contactCache, logger) {
2005
2144
  sentAt: payload.sentAt ?? Date.now(),
2006
2145
  sequence: payload.sequence ?? 0,
2007
2146
  source: payload.source ?? "webui",
2008
- kind: payload.kind ?? "text"
2147
+ kind: payload.kind ?? "text",
2148
+ revoked: payload.revoked,
2149
+ revokedAt: payload.revokedAt
2009
2150
  };
2010
2151
  await database.upsertSelfMessage(record);
2011
2152
  koa.body = { ok: true, message: record };
@@ -2236,6 +2377,17 @@ function registerWeb(ctx, config, database, contactCache, logger) {
2236
2377
  contacts: await database.getContacts(platform, selfId, normalizedType)
2237
2378
  };
2238
2379
  });
2380
+ ctx.server.post(`${config.basePath}/api/clear-all`, async (koa) => {
2381
+ try {
2382
+ await database.clearAll();
2383
+ await media.clearAll();
2384
+ koa.body = { ok: true };
2385
+ } catch (error) {
2386
+ logger.warn("清空全部缓存失败:", error);
2387
+ koa.status = 500;
2388
+ koa.body = { ok: false, error: String(error) };
2389
+ }
2390
+ });
2239
2391
  const serveFile = /* @__PURE__ */ __name(async (koa, fileName) => {
2240
2392
  const fullPath = import_node_path3.default.join(webRoot, fileName);
2241
2393
  if ((0, import_node_fs2.existsSync)(fullPath) && (0, import_node_fs2.statSync)(fullPath).isFile()) {
@@ -2283,7 +2435,7 @@ var Config = import_koishi2.Schema.intersect([
2283
2435
  blockedPlatforms: import_koishi2.Schema.array(import_koishi2.Schema.object({
2284
2436
  platformName: import_koishi2.Schema.string(),
2285
2437
  exactMatch: import_koishi2.Schema.boolean().default(true)
2286
- })).role("table").description("不记录历史的平台").default([
2438
+ })).role("table").description("屏蔽消息接收的平台").default([
2287
2439
  { platformName: "qq", exactMatch: true },
2288
2440
  { platformName: "qqguild", exactMatch: true },
2289
2441
  { platformName: "sandbox", exactMatch: false }
@@ -2316,18 +2468,18 @@ async function apply(ctx, config) {
2316
2468
  media.start();
2317
2469
  const recorder = new Recorder(ctx, config, database, media, contactCache, pluginLogger);
2318
2470
  recorder.start();
2319
- const selfMessages = new SelfMessageRecorder(ctx, config, database, pluginLogger);
2471
+ const selfMessages = new SelfMessageRecorder(ctx, config, database, media, pluginLogger);
2320
2472
  selfMessages.start();
2321
2473
  registerBootstrap(ctx, config, database, pluginLogger);
2322
- registerWeb(ctx, config, database, contactCache, pluginLogger);
2474
+ registerWeb(ctx, config, database, contactCache, media, pluginLogger);
2323
2475
  ctx.console.addEntry({
2324
2476
  dev: import_node_path4.default.resolve(__dirname, "../client/index.ts"),
2325
2477
  prod: import_node_path4.default.resolve(__dirname, "../dist")
2326
2478
  });
2327
- ctx.on("dispose", () => {
2479
+ ctx.on("dispose", async () => {
2328
2480
  selfMessages.dispose();
2329
2481
  media.dispose();
2330
- void database.dispose();
2482
+ await database.dispose();
2331
2483
  pluginLogger.logInfo("chat-patch 已卸载");
2332
2484
  });
2333
2485
  }
package/lib/media.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { Context } from 'koishi';
2
+ import { Config } from './config';
3
+ import { ChatDatabase } from './database';
4
+ import { PluginLogger } from './logger';
5
+ export declare class MediaManager {
6
+ private ctx;
7
+ private config;
8
+ private database;
9
+ private logger;
10
+ private mediaDir;
11
+ private uploadDir;
12
+ private cleanupTimer?;
13
+ constructor(ctx: Context, config: Config, database: ChatDatabase, logger: PluginLogger);
14
+ start(): void;
15
+ dispose(): void;
16
+ clearAll(): Promise<void>;
17
+ cacheUrl(url: string, channelId?: string): Promise<string | null>;
18
+ cacheMessageMedia(source: unknown, channelId?: string): Promise<string[]>;
19
+ private resolveAndDownload;
20
+ private detectExtension;
21
+ private migrateCachedMedia;
22
+ private cleanup;
23
+ private exists;
24
+ }
@@ -0,0 +1,21 @@
1
+ import { Context } from 'koishi';
2
+ import { Config } from './config';
3
+ import { ContactCacheService } from './cache';
4
+ import { ChatDatabase } from './database';
5
+ import { MediaManager } from './media';
6
+ import { PluginLogger } from './logger';
7
+ export declare class Recorder {
8
+ private ctx;
9
+ private config;
10
+ private database;
11
+ private media;
12
+ private contactCache;
13
+ private logger;
14
+ constructor(ctx: Context, config: Config, database: ChatDatabase, media: MediaManager, contactCache: ContactCacheService, logger: PluginLogger);
15
+ start(): void;
16
+ private isBlocked;
17
+ private handleSession;
18
+ private handleMessageDeleted;
19
+ private cacheMessageContacts;
20
+ private cacheMessageMedia;
21
+ }