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.
- package/client/web/dist/assets/Chat-BduEIlJM.js +34 -0
- package/client/web/dist/assets/{Chat-D4tCqRrO.css → Chat-DgkvjRIh.css} +1 -1
- package/client/web/dist/assets/{MsgBody-BaoDyXUz.js → MsgBody-CVtXorvl.js} +1 -1
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-D_70Txdf.js +67 -0
- package/client/web/dist/assets/{index-X9hFOAza.css → index-BW47RWcA.css} +1 -1
- package/client/web/dist/assets/{index--7-eCx0r.js → index-N2_GDY3v.js} +5 -8
- package/client/web/dist/index.html +2 -2
- package/client/web/src/assets/css/chat.css +24 -2
- package/client/web/src/assets/l10n/zh-CN.po +0 -3
- package/client/web/src/components/MsgBody.vue +29 -20
- package/client/web/src/function/connect.ts +12 -6
- package/client/web/src/function/custom-element.ts +31 -0
- package/client/web/src/function/msg.ts +2 -0
- package/client/web/src/function/satori-model.ts +2 -0
- package/client/web/src/function/utils/msgUtil.ts +66 -44
- package/client/web/src/pages/Chat.vue +139 -15
- package/lib/index.js +91 -31
- package/lib/types.d.ts +142 -0
- package/package.json +1 -1
- package/src/database.ts +27 -19
- package/src/recorder.ts +2 -1
- package/src/self-message.ts +4 -1
- package/src/types.ts +4 -0
- package/src/web-error-filter.ts +30 -0
- package/src/web.ts +107 -77
- package/client/web/dist/assets/Chat-BkRnAj_r.js +0 -28
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CgpEB8mj.js +0 -67
|
@@ -376,15 +376,29 @@
|
|
|
376
376
|
</div>
|
|
377
377
|
<!-- 消息发送框 -->
|
|
378
378
|
<div>
|
|
379
|
-
<div
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
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.
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
8219
|
-
const timeB =
|
|
8273
|
+
const timeA = messageSortTime(a);
|
|
8274
|
+
const timeB = messageSortTime(b);
|
|
8220
8275
|
return timeA - timeB;
|
|
8221
8276
|
});
|
|
8222
|
-
selfMessages.sort((a, b) => a
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|