koishi-plugin-chat-patch 6.0.0 → 6.1.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.
@@ -376,15 +376,29 @@
376
376
  </div>
377
377
  <!-- 消息发送框 -->
378
378
  <div>
379
- <div v-menu.prevent="_=>moreFunClick()"
380
- @click="moreFunClick(settingsStore.sysConfig.quick_send)">
381
- <font-awesome-icon v-if="tags.showMoreDetail || details.find(item => item.open)" :icon="['fas', 'minus']" />
382
- <font-awesome-icon v-else-if="settingsStore.sysConfig.quick_send == 'default'" :icon="['fas', 'plus']" />
383
- <font-awesome-icon v-else-if="settingsStore.sysConfig.quick_send == 'img'" :icon="['fas', 'image']" />
384
- <font-awesome-icon v-else-if="settingsStore.sysConfig.quick_send == 'file'" :icon="['fas', 'folder']" />
379
+ <div class="send-tool-buttons">
380
+ <div class="send-tool-button"
381
+ v-menu.prevent="_=>moreFunClick()"
382
+ @click="moreFunClick(settingsStore.sysConfig.quick_send)">
383
+ <font-awesome-icon v-if="tags.showMoreDetail || details.find(item => item.open)" :icon="['fas', 'minus']" />
384
+ <font-awesome-icon v-else-if="settingsStore.sysConfig.quick_send == 'default'" :icon="['fas', 'plus']" />
385
+ <font-awesome-icon v-else-if="settingsStore.sysConfig.quick_send == 'img'" :icon="['fas', 'image']" />
386
+ <font-awesome-icon v-else-if="settingsStore.sysConfig.quick_send == 'file'" :icon="['fas', 'folder']" />
387
+ </div>
388
+ <div
389
+ class="send-tool-button custom-element-toggle"
390
+ :class="{ active: customElementMode }"
391
+ :title="$t('自定义元素')"
392
+ :aria-pressed="customElementMode"
393
+ role="button"
394
+ @click="toggleCustomElementMode">
395
+ <font-awesome-icon :icon="['fas', 'code']" />
396
+ </div>
385
397
  </div>
386
398
  <div>
387
- <form @submit="mainSubmit">
399
+ <form
400
+ :class="{ 'custom-element-form': customElementMode }"
401
+ @submit="mainSubmit">
388
402
  <template v-if="pendingVoice">
389
403
  <div id="voice-pending-input" class="voice-pending" role="button"
390
404
  @click="cancelPendingVoice">
@@ -392,6 +406,51 @@
392
406
  <font-awesome-icon :icon="['fas', 'xmark']" />
393
407
  </div>
394
408
  </template>
409
+ <template v-else-if="customElementMode">
410
+ <label for="custom-element-type" class="sr-only">{{ $t('元素类型') }}</label>
411
+ <input
412
+ id="custom-element-type"
413
+ ref="customElementTypeInput"
414
+ v-model="customElementType"
415
+ class="custom-element-type-input"
416
+ type="text"
417
+ autocomplete="off"
418
+ placeholder="text / img / file"
419
+ :disabled="uiStore.openSideBar || chat.info.me_info.shut_up_timestamp > 0">
420
+ <template v-if="settingsStore.sysConfig.send_key === 'none'">
421
+ <label for="main-input" class="sr-only">{{ $t('消息输入框') }}</label>
422
+ <input
423
+ id="main-input"
424
+ ref="mainInput"
425
+ v-model="msg"
426
+ class="custom-element-content-input"
427
+ type="text"
428
+ autocomplete="off"
429
+ placeholder="URL / content"
430
+ :disabled="uiStore.openSideBar || chat.info.me_info.shut_up_timestamp > 0"
431
+ @keydown="mainAtKey"
432
+ @keyup="mainKeyUp"
433
+ @click="selectSQIn"
434
+ @input="handleInput">
435
+ </template>
436
+ <template v-else>
437
+ <label for="main-input-ex" class="sr-only">{{ $t('消息输入框') }}</label>
438
+ <textarea
439
+ id="main-input-ex"
440
+ ref="mainInput"
441
+ v-model="msg"
442
+ class="custom-element-content-input"
443
+ type="text"
444
+ placeholder="URL / content"
445
+ :disabled="uiStore.openSideBar"
446
+ @keydown="mainKey"
447
+ @keyup="mainKeyUp"
448
+ @click="selectSQIn"
449
+ @input="handleInput"
450
+ @compositionstart="handleCompositionStart"
451
+ @compositionend="handleCompositionEnd" />
452
+ </template>
453
+ </template>
395
454
  <template v-else>
396
455
  <template v-if="settingsStore.sysConfig.send_key === 'none'">
397
456
  <label for="main-input" class="sr-only">{{ $t('消息输入框') }}</label>
@@ -502,13 +561,6 @@
502
561
  <div><font-awesome-icon :icon="['fas', 'trash-can']" /></div>
503
562
  <a>{{ $t('移出群聊') }}</a>
504
563
  </div>
505
- <div v-show="tags.menuDisplay.config"
506
- @click="openChatInfoPan();
507
- ($refs.infoRef as any).openMoreConfig(selectedMsg?.sender.user_id);
508
- closeMsgMenu();">
509
- <div><font-awesome-icon :icon="['fas', 'cog']" /></div>
510
- <a>{{ $t('成员设置') }}</a>
511
- </div>
512
564
  <div v-show="tags.menuDisplay.jumpToMsg" @click="jumpSearchMsg">
513
565
  <div><font-awesome-icon :icon="['fas', 'arrow-up-right-from-square']" /></div>
514
566
  <a>{{ $t('跳转到消息') }}</a>
@@ -564,6 +616,7 @@
564
616
  import app from '../main'
565
617
  import { i18n } from '../main'
566
618
  import SendUtil from '../function/sender'
619
+ import { buildCustomElement } from '../function/custom-element'
567
620
  import Option, { get } from '../function/option'
568
621
  import Info from '../pages/Info.vue'
569
622
  import MsgBody from '../components/MsgBody.vue'
@@ -657,6 +710,7 @@ const msgPan = useTemplateRef<HTMLDivElement>('msgPan')
657
710
  const chatPadding = useTemplateRef<HTMLSpanElement>('chatPadding')
658
711
  const sendMore = useTemplateRef<HTMLDivElement>('sendMore')
659
712
  const mainInput = useTemplateRef<HTMLInputElement | HTMLTextAreaElement>('mainInput')
713
+ const customElementTypeInput = useTemplateRef<HTMLInputElement>('customElementTypeInput')
660
714
 
661
715
  type ForwardAction = 'single-message' | 'individual-messages' | 'merged-messages'
662
716
 
@@ -713,6 +767,8 @@ const details = ref([
713
767
  const msgMenus = ref<any[]>([])
714
768
  const NewMsgNum = ref(0)
715
769
  const msg = ref('')
770
+ const customElementMode = ref(false)
771
+ const customElementType = ref('')
716
772
  const oldMsg = ref('')
717
773
  const imgCache = ref(new Map<number, string>())
718
774
  const voiceRecording = ref(false)
@@ -849,6 +905,8 @@ const chatMoveOptions: VMoveOptions<HTMLDivElement> = {
849
905
 
850
906
  function resetState() {
851
907
  NewMsgNum.value = 0
908
+ customElementMode.value = false
909
+ customElementType.value = ''
852
910
  tags.value = {
853
911
  sendTag: 'REFUSE',
854
912
  showBottomButton: false,
@@ -2089,6 +2147,8 @@ function forwardMsg(data: UserFriendElem & UserGroupElem) {
2089
2147
  forwardContent: msgBody,
2090
2148
  source: 'webui',
2091
2149
  sentAt: sentTime,
2150
+ timestamp: Math.floor(sentTime / 1000),
2151
+ timestampMs: sentTime,
2092
2152
  kind: 'forward',
2093
2153
  })
2094
2154
  }
@@ -2876,6 +2936,44 @@ function sendMsg(echo = 'sendMsgBack') {
2876
2936
  return
2877
2937
  }
2878
2938
 
2939
+ if (customElementMode.value) {
2940
+ const customElement = buildCustomElement(
2941
+ customElementType.value,
2942
+ msg.value,
2943
+ )
2944
+ if (!customElement) {
2945
+ new PopInfo().add(PopType.ERR, $t('请输入元素类型和内容'))
2946
+ return
2947
+ }
2948
+ const parsedMsg = customElement.toString()
2949
+ if (chat.show.temp) {
2950
+ sendMsgRaw(
2951
+ chat.show.id + '/' + chat.show.temp,
2952
+ chat.show.type,
2953
+ parsedMsg,
2954
+ true,
2955
+ echo,
2956
+ )
2957
+ } else {
2958
+ sendMsgRaw(
2959
+ chat.show.id,
2960
+ chat.show.type,
2961
+ parsedMsg,
2962
+ true,
2963
+ echo,
2964
+ )
2965
+ }
2966
+ customElementType.value = ''
2967
+ msg.value = ''
2968
+ sendCache.value = []
2969
+ imgCache.value.clear()
2970
+ scrollBottom()
2971
+ cancelReply()
2972
+ scheduleResizeMainInput(undefined, true)
2973
+ nextTick(() => customElementTypeInput.value?.focus())
2974
+ return
2975
+ }
2976
+
2879
2977
  for (const [key, base64data] of imgCache.value) {
2880
2978
  sendCache.value[key] = {
2881
2979
  type: 'image',
@@ -2996,7 +3094,7 @@ function updateList(newLength: number, oldLength: number) {
2996
3094
  msgItem.type === 'image' &&
2997
3095
  msgItem.file != 'marketface'
2998
3096
  ) {
2999
- getImgList.push(msgItem.url)
3097
+ getImgList.push(msgItem.url || msgItem.file)
3000
3098
  }
3001
3099
  }
3002
3100
  }
@@ -3294,6 +3392,24 @@ function viewerEssImg(url: string) {
3294
3392
  viewerRef.value.open(new Img(url))
3295
3393
  }
3296
3394
 
3395
+ function toggleCustomElementMode() {
3396
+ if (pendingVoice.value) return
3397
+ customElementMode.value = !customElementMode.value
3398
+ details.value.forEach((item) => {
3399
+ item.open = false
3400
+ })
3401
+ tags.value.showMoreDetail = false
3402
+ voiceMenuShow.value = false
3403
+ tags.value.onAtFind = false
3404
+ nextTick(() => {
3405
+ const input = customElementMode.value
3406
+ ? customElementTypeInput.value
3407
+ : mainInput.value
3408
+ input?.focus()
3409
+ scheduleResizeMainInput()
3410
+ })
3411
+ }
3412
+
3297
3413
  function moreFunClick(type = 'default') {
3298
3414
  let hasOpen = false
3299
3415
  details.value.forEach((item) => {
@@ -3391,6 +3507,14 @@ function exitWin() {
3391
3507
  overflow: visible;
3392
3508
  }
3393
3509
 
3510
+ .send-tool-button.custom-element-toggle.active {
3511
+ background: var(--color-main);
3512
+ }
3513
+
3514
+ .send-tool-button.custom-element-toggle.active > svg {
3515
+ color: #fff;
3516
+ }
3517
+
3394
3518
  .voice-menu-wrap {
3395
3519
  position: relative;
3396
3520
  width: 50px;
package/lib/index.js CHANGED
@@ -5142,6 +5142,12 @@ function encodeKeyPart2(value) {
5142
5142
  return encodeURIComponent(value);
5143
5143
  }
5144
5144
  __name(encodeKeyPart2, "encodeKeyPart");
5145
+ function normalizeTimestampMs(value) {
5146
+ const num = Number(value ?? 0);
5147
+ if (!Number.isFinite(num) || num <= 0) return 0;
5148
+ return num > 1e12 ? num : num * 1e3;
5149
+ }
5150
+ __name(normalizeTimestampMs, "normalizeTimestampMs");
5145
5151
  function decodeKeyPart(value) {
5146
5152
  try {
5147
5153
  return decodeURIComponent(value);
@@ -5160,7 +5166,7 @@ function legacyMessagePrefix(platform, selfId, channelId) {
5160
5166
  __name(legacyMessagePrefix, "legacyMessagePrefix");
5161
5167
  function messageKey(record) {
5162
5168
  const time = String(
5163
- record.receivedAt ?? record.timestampMs ?? Number(record.timestamp) * 1e3
5169
+ normalizeTimestampMs(record.timestampMs ?? record.timestamp ?? record.receivedAt)
5164
5170
  ).padStart(16, "0");
5165
5171
  return `${messagePrefix(record.platform, record.selfId, record.channelId || "")}${time}:${encodeKeyPart2(record.id || "unknown")}`;
5166
5172
  }
@@ -5170,7 +5176,9 @@ function selfMessagePrefix(platform, selfId, channelId) {
5170
5176
  }
5171
5177
  __name(selfMessagePrefix, "selfMessagePrefix");
5172
5178
  function selfMessageKey(record) {
5173
- const time = String(record.sentAt).padStart(16, "0");
5179
+ const time = String(
5180
+ normalizeTimestampMs(record.timestampMs ?? record.timestamp ?? record.sentAt)
5181
+ ).padStart(16, "0");
5174
5182
  return `${selfMessagePrefix(record.platform, record.selfId, record.channelId)}${time}:${encodeKeyPart2(record.id || "unknown")}`;
5175
5183
  }
5176
5184
  __name(selfMessageKey, "selfMessageKey");
@@ -5824,7 +5832,7 @@ var _ChatDatabase = class _ChatDatabase {
5824
5832
  extractRecordTime(value) {
5825
5833
  try {
5826
5834
  const parsed = JSON.parse(value);
5827
- return Number(parsed.receivedAt ?? parsed.timestampMs ?? parsed.timestamp ?? parsed.sentAt ?? 0) || 0;
5835
+ return normalizeTimestampMs(parsed.timestampMs ?? parsed.timestamp ?? parsed.sentAt ?? parsed.receivedAt);
5828
5836
  } catch {
5829
5837
  return 0;
5830
5838
  }
@@ -5890,6 +5898,7 @@ var _Recorder = class _Recorder {
5890
5898
  const user = getObject(body.user);
5891
5899
  const sn = getNumber(body.sn);
5892
5900
  const timestamp = getNumber(body.timestamp) || Date.now();
5901
+ const timestampMs = timestamp > 1e12 ? timestamp : timestamp * 1e3;
5893
5902
  const record = {
5894
5903
  id: getString2(message2.id) || `satori-${sn}`,
5895
5904
  sequence: sn,
@@ -5900,7 +5909,7 @@ var _Recorder = class _Recorder {
5900
5909
  guildId: getString2(guild.id) || void 0,
5901
5910
  userId: getString2(user.id) || void 0,
5902
5911
  timestamp,
5903
- timestampMs: timestamp,
5912
+ timestampMs,
5904
5913
  receivedAt: Date.now(),
5905
5914
  content: getString2(message2.content) || getString2(message2.raw_message) || void 0,
5906
5915
  elements: Array.isArray(message2.elements) ? message2.elements : void 0,
@@ -7823,6 +7832,7 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
7823
7832
  const channelType = mode === "private" ? "user" : "group";
7824
7833
  const elements = this.normalizeElements(content);
7825
7834
  const contentText = typeof content === "string" ? content : import_koishi.h.toElementArray(content).join("");
7835
+ const sentAt = Date.now();
7826
7836
  const fingerprint = (0, import_node_crypto3.createHash)("sha256").update([
7827
7837
  platform,
7828
7838
  selfId,
@@ -7842,7 +7852,9 @@ var _SelfMessageRecorder = class _SelfMessageRecorder {
7842
7852
  message: toSegments(elements, (attrs) => this.resolveI18nElement(attrs)),
7843
7853
  forwardId: forwardId(elements),
7844
7854
  forwardContent: toForwardNodes(elements, (attrs) => this.resolveI18nElement(attrs)),
7845
- sentAt: Date.now(),
7855
+ sentAt,
7856
+ timestamp: Math.floor(sentAt / 1e3),
7857
+ timestampMs: sentAt,
7846
7858
  sequence: ++this.sequence,
7847
7859
  source,
7848
7860
  kind: detectKind(elements),
@@ -7926,12 +7938,41 @@ var import_node_path3 = __toESM(require("node:path"));
7926
7938
  var import_node_url2 = require("node:url");
7927
7939
  var import_koa_send = __toESM(require_koa_send());
7928
7940
  var import_file_type2 = __toESM(require_file_type());
7941
+
7942
+ // src/web-error-filter.ts
7943
+ function isIgnoredWebError(error) {
7944
+ if (error.name === "RangeError" && error.message === "Invalid string length") return true;
7945
+ return error.code === "ECONNRESET";
7946
+ }
7947
+ __name(isIgnoredWebError, "isIgnoredWebError");
7948
+ function registerWebErrorFilter(ctx) {
7949
+ const app = ctx.server._koa;
7950
+ const defaultHandler = app.onerror;
7951
+ const hasDefaultHandler = app.listeners("error").includes(defaultHandler);
7952
+ if (hasDefaultHandler) app.off("error", defaultHandler);
7953
+ const errorHandler = /* @__PURE__ */ __name((error) => {
7954
+ if (isIgnoredWebError(error)) return;
7955
+ if (hasDefaultHandler) defaultHandler.call(app, error);
7956
+ }, "errorHandler");
7957
+ app.on("error", errorHandler);
7958
+ ctx.on("dispose", () => {
7959
+ app.off("error", errorHandler);
7960
+ if (hasDefaultHandler && !app.listeners("error").includes(defaultHandler)) {
7961
+ app.on("error", defaultHandler);
7962
+ }
7963
+ });
7964
+ }
7965
+ __name(registerWebErrorFilter, "registerWebErrorFilter");
7966
+
7967
+ // src/web.ts
7929
7968
  function registerWeb(ctx, config, database, contactCache, media, logger) {
7969
+ registerWebErrorFilter(ctx);
7930
7970
  const webRoot = import_node_path3.default.resolve(__dirname, "..", "client", "web", "dist");
7931
7971
  const webSource = import_node_path3.default.resolve(__dirname, "..", "client", "web", "src");
7932
7972
  const webPublic = import_node_path3.default.resolve(__dirname, "..", "client", "web", "public");
7933
7973
  const webIndex = import_node_path3.default.resolve(__dirname, "..", "client", "web", "index.html");
7934
7974
  const uploadDir = import_node_path3.default.resolve(ctx.baseDir, "data", "chat-patch", "upload-media");
7975
+ const mediaDir = import_node_path3.default.resolve(ctx.baseDir, "data", "chat-patch", "media");
7935
7976
  const cacheType = /* @__PURE__ */ __name((type) => type === "user" ? "friend" : type, "cacheType");
7936
7977
  const getVite = /* @__PURE__ */ __name(() => {
7937
7978
  return ctx.console.vite;
@@ -7981,6 +8022,20 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
7981
8022
  const historyChannelCandidates = /* @__PURE__ */ __name((channelId) => {
7982
8023
  return channelId ? [channelId] : [];
7983
8024
  }, "historyChannelCandidates");
8025
+ const messageSortTime = /* @__PURE__ */ __name((item) => {
8026
+ const timestampMs = Number(item.timestampMs ?? 0);
8027
+ if (timestampMs > 0) return timestampMs > 1e12 ? timestampMs : timestampMs * 1e3;
8028
+ const timestamp = Number(item.timestamp ?? 0);
8029
+ if (timestamp > 0) return timestamp * 1e3;
8030
+ return Number(item.receivedAt ?? 0);
8031
+ }, "messageSortTime");
8032
+ const selfMessageSortTime = /* @__PURE__ */ __name((item) => {
8033
+ const timestampMs = Number(item.timestampMs ?? item.sentAt ?? 0);
8034
+ if (timestampMs > 0) return timestampMs > 1e12 ? timestampMs : timestampMs * 1e3;
8035
+ const timestamp = Number(item.timestamp ?? 0);
8036
+ if (timestamp > 0) return timestamp * 1e3;
8037
+ return item.sentAt;
8038
+ }, "selfMessageSortTime");
7984
8039
  const mimeToExt = {
7985
8040
  "audio/mpeg": ".mp3",
7986
8041
  "audio/mp3": ".mp3",
@@ -8105,26 +8160,9 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8105
8160
  localPath: filePath
8106
8161
  };
8107
8162
  });
8108
- ctx.server.get(`${config.basePath}/api/media`, async (koa) => {
8109
- const fileName = import_node_path3.default.basename(String(koa.query.file ?? koa.query.name ?? ""));
8110
- if (!fileName || fileName === "." || fileName === "..") {
8111
- koa.status = 400;
8112
- koa.body = { error: "missing media file" };
8113
- return;
8114
- }
8115
- const filePath = import_node_path3.default.resolve(uploadDir, fileName);
8116
- const root = import_node_path3.default.resolve(uploadDir);
8117
- if (filePath !== root && !filePath.startsWith(`${root}${import_node_path3.default.sep}`)) {
8118
- koa.status = 400;
8119
- koa.body = { error: "invalid media file" };
8120
- return;
8121
- }
8122
- if (!(0, import_node_fs3.existsSync)(filePath) || !(0, import_node_fs3.statSync)(filePath).isFile()) {
8123
- koa.status = 404;
8124
- koa.body = { error: "media file not found" };
8125
- return;
8126
- }
8163
+ const sendLocalFile = /* @__PURE__ */ __name(async (koa, filePath) => {
8127
8164
  const size = (0, import_node_fs3.statSync)(filePath).size;
8165
+ const fileName = import_node_path3.default.basename(filePath);
8128
8166
  koa.set("Accept-Ranges", "bytes");
8129
8167
  if (fileName.toLowerCase().endsWith(".webm")) {
8130
8168
  koa.type = "audio/webm";
@@ -8164,6 +8202,23 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8164
8202
  }
8165
8203
  koa.set("Content-Length", String(size));
8166
8204
  koa.body = (0, import_node_fs3.createReadStream)(filePath);
8205
+ }, "sendLocalFile");
8206
+ ctx.server.get(`${config.basePath}/api/media`, async (koa) => {
8207
+ const fileName = import_node_path3.default.basename(String(koa.query.file ?? koa.query.name ?? ""));
8208
+ if (!fileName || fileName === "." || fileName === "..") {
8209
+ koa.status = 400;
8210
+ koa.body = { error: "missing media file" };
8211
+ return;
8212
+ }
8213
+ for (const root of [uploadDir, mediaDir]) {
8214
+ const filePath = import_node_path3.default.resolve(root, fileName);
8215
+ if (filePath !== root && filePath.startsWith(`${root}${import_node_path3.default.sep}`) && (0, import_node_fs3.existsSync)(filePath) && (0, import_node_fs3.statSync)(filePath).isFile()) {
8216
+ await sendLocalFile(koa, filePath);
8217
+ return;
8218
+ }
8219
+ }
8220
+ koa.status = 404;
8221
+ koa.body = { error: "media file not found" };
8167
8222
  });
8168
8223
  ctx.server.get(`${config.basePath}/api/cache/all`, async (koa) => {
8169
8224
  const entries = await database.getAllContacts();
@@ -8215,21 +8270,21 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8215
8270
  if (selfMessages.length) break;
8216
8271
  }
8217
8272
  messages.sort((a, b) => {
8218
- const timeA = Number(a?.receivedAt ?? a?.timestampMs ?? a?.timestamp ?? 0);
8219
- const timeB = Number(b?.receivedAt ?? b?.timestampMs ?? b?.timestamp ?? 0);
8273
+ const timeA = messageSortTime(a);
8274
+ const timeB = messageSortTime(b);
8220
8275
  return timeA - timeB;
8221
8276
  });
8222
- selfMessages.sort((a, b) => a.sentAt - b.sentAt);
8277
+ selfMessages.sort((a, b) => selfMessageSortTime(a) - selfMessageSortTime(b));
8223
8278
  const combined = [];
8224
8279
  for (const item of messages) {
8225
8280
  combined.push({
8226
8281
  kind: "message",
8227
- time: Number(item?.receivedAt ?? item?.timestampMs ?? item?.timestamp ?? 0),
8282
+ time: messageSortTime(item),
8228
8283
  record: item
8229
8284
  });
8230
8285
  }
8231
8286
  for (const item of selfMessages) {
8232
- combined.push({ kind: "self", time: item.sentAt, record: item });
8287
+ combined.push({ kind: "self", time: selfMessageSortTime(item), record: item });
8233
8288
  }
8234
8289
  combined.sort((a, b) => a.time - b.time);
8235
8290
  const capped = combined.slice(-limit);
@@ -8255,7 +8310,7 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8255
8310
  messages = Number.isFinite(beforeTime) && beforeTime > 0 ? await database.listSelfMessagesBefore(platform, selfId, candidate, beforeTime, limit) : await database.listSelfMessages(platform, selfId, candidate, limit);
8256
8311
  if (messages.length) break;
8257
8312
  }
8258
- messages.sort((a, b) => a.sentAt - b.sentAt);
8313
+ messages.sort((a, b) => selfMessageSortTime(a) - selfMessageSortTime(b));
8259
8314
  koa.body = { messages };
8260
8315
  });
8261
8316
  ctx.server.post(`${config.basePath}/api/self-messages`, async (koa) => {
@@ -8275,6 +8330,8 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8275
8330
  forwardId: typeof body.forwardId === "string" ? body.forwardId : void 0,
8276
8331
  forwardContent: Array.isArray(body.forwardContent) ? body.forwardContent : void 0,
8277
8332
  sentAt: typeof body.sentAt === "number" && Number.isFinite(body.sentAt) ? body.sentAt : Date.now(),
8333
+ timestamp: typeof body.timestamp === "number" && Number.isFinite(body.timestamp) ? body.timestamp : void 0,
8334
+ timestampMs: typeof body.timestampMs === "number" && Number.isFinite(body.timestampMs) ? body.timestampMs : void 0,
8278
8335
  sequence: typeof body.sequence === "number" && Number.isFinite(body.sequence) ? body.sequence : 0,
8279
8336
  source: body.source === "plugin" ? "plugin" : "webui",
8280
8337
  kind: typeof body.kind === "string" ? body.kind : "text",
@@ -8286,6 +8343,7 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8286
8343
  koa.body = { error: "missing self-message params" };
8287
8344
  return;
8288
8345
  }
8346
+ const sentAt = payload.sentAt ?? Date.now();
8289
8347
  const record = {
8290
8348
  id: payload.id || `web-${Date.now()}-${Math.random().toString(36).slice(2)}`,
8291
8349
  platform: payload.platform,
@@ -8299,7 +8357,9 @@ function registerWeb(ctx, config, database, contactCache, media, logger) {
8299
8357
  message: payload.message,
8300
8358
  forwardId: payload.forwardId,
8301
8359
  forwardContent: payload.forwardContent,
8302
- sentAt: payload.sentAt ?? Date.now(),
8360
+ sentAt,
8361
+ timestamp: payload.timestamp ?? Math.floor(sentAt / 1e3),
8362
+ timestampMs: payload.timestampMs ?? sentAt,
8303
8363
  sequence: payload.sequence ?? 0,
8304
8364
  source: payload.source ?? "webui",
8305
8365
  kind: payload.kind ?? "text",
package/lib/types.d.ts ADDED
@@ -0,0 +1,142 @@
1
+ import type { Awaitable } from 'koishi';
2
+ export interface SatoriBootstrap {
3
+ endpoint: string;
4
+ token: string;
5
+ basePath: string;
6
+ logins: SatoriLoginInfo[];
7
+ blockedPlatforms: Array<{
8
+ platformName: string;
9
+ exactMatch: boolean;
10
+ }>;
11
+ }
12
+ export interface SatoriLoginInfo {
13
+ platform: string;
14
+ selfId: string;
15
+ name: string;
16
+ avatar?: string;
17
+ status: number;
18
+ features?: string[];
19
+ }
20
+ export interface SatoriEventPayload {
21
+ type: string;
22
+ platform: string;
23
+ selfId: string;
24
+ timestamp: number;
25
+ sn: number;
26
+ body: Record<string, unknown>;
27
+ }
28
+ export interface MessageRecord {
29
+ id?: string;
30
+ sequence?: number;
31
+ type: string;
32
+ platform: string;
33
+ selfId: string;
34
+ channelId?: string;
35
+ guildId?: string;
36
+ userId?: string;
37
+ timestamp: number;
38
+ timestampMs?: number;
39
+ receivedAt?: number;
40
+ content?: string;
41
+ elements?: unknown[];
42
+ raw?: unknown;
43
+ revoked?: boolean;
44
+ revokedAt?: number;
45
+ }
46
+ export type SelfMessageChannelType = 'group' | 'user';
47
+ export type SelfMessageSource = 'webui' | 'bot' | 'plugin';
48
+ export interface SelfMessageRecord {
49
+ id: string;
50
+ platform: string;
51
+ selfId: string;
52
+ channelId: string;
53
+ guildId?: string;
54
+ channelType?: SelfMessageChannelType;
55
+ messageId?: string;
56
+ content?: string;
57
+ elements?: unknown[];
58
+ message?: unknown[];
59
+ forwardId?: string;
60
+ forwardContent?: unknown[];
61
+ sentAt: number;
62
+ timestamp?: number;
63
+ timestampMs?: number;
64
+ sequence: number;
65
+ source: SelfMessageSource;
66
+ kind: string;
67
+ fingerprint?: string;
68
+ revoked?: boolean;
69
+ revokedAt?: number;
70
+ }
71
+ export interface SelfMessagePayload {
72
+ id?: string;
73
+ platform: string;
74
+ selfId: string;
75
+ channelId: string;
76
+ guildId?: string;
77
+ channelType?: SelfMessageChannelType;
78
+ messageId?: string;
79
+ content?: string;
80
+ elements?: unknown[];
81
+ message?: unknown[];
82
+ forwardId?: string;
83
+ forwardContent?: unknown[];
84
+ sentAt?: number;
85
+ timestamp?: number;
86
+ timestampMs?: number;
87
+ sequence?: number;
88
+ source?: SelfMessageSource;
89
+ kind?: string;
90
+ revoked?: boolean;
91
+ revokedAt?: number;
92
+ }
93
+ export interface HistoryQuery {
94
+ platform: string;
95
+ selfId: string;
96
+ channelId: string;
97
+ limit?: number;
98
+ }
99
+ export interface HistoryResult {
100
+ messages: MessageRecord[];
101
+ }
102
+ export interface PinnedState {
103
+ bots: string[];
104
+ channels: string[];
105
+ }
106
+ export interface PluginConfigPayload {
107
+ basePath: string;
108
+ maxMessagesPerChannel: number;
109
+ historyPageSize: number;
110
+ maxMediaFiles: number;
111
+ blockedPlatforms: Array<{
112
+ platformName: string;
113
+ exactMatch: boolean;
114
+ }>;
115
+ loggerinfo: boolean;
116
+ }
117
+ export interface ContactCacheItem {
118
+ id: string;
119
+ name: string;
120
+ avatar?: string;
121
+ channelId?: string;
122
+ guildId?: string;
123
+ raw?: unknown;
124
+ }
125
+ export interface ContactCacheQuery {
126
+ platform: string;
127
+ selfId: string;
128
+ type: string;
129
+ contacts?: ContactCacheItem[];
130
+ append?: boolean;
131
+ }
132
+ export interface ContactCacheResult {
133
+ contacts?: ContactCacheItem[];
134
+ }
135
+ declare module '@koishijs/console' {
136
+ interface Events {
137
+ 'chat-patch/bootstrap'(): SatoriBootstrap;
138
+ 'chat-patch/history'(query: HistoryQuery): Awaitable<HistoryResult>;
139
+ 'chat-patch/config'(): PluginConfigPayload;
140
+ 'chat-patch/contact-cache'(query: ContactCacheQuery): Awaitable<ContactCacheResult>;
141
+ }
142
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-patch",
3
3
  "description": "[<ruby>chat-patch<rp>(</rp><rt>点我预览效果</rt><rp>)</rp></ruby>](https://i0.hdslb.com/bfs/openplatform/71074dfc9e5256fc3333d8bd8478bec1af874046.png) 视奸小插件((bushi( (低性能警告)。手机端适配。灵感来自 chat 插件。",
4
- "version": "6.0.0",
4
+ "version": "6.1.1",
5
5
  "scripts": {
6
6
  "build:web": "npm --prefix client/web run build",
7
7
  "prepack": "npm run build:web",