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.
- package/client/web/dist/assets/Chat-Clqxn7CB.js +28 -0
- package/client/web/dist/assets/{Chat-CTdyeaX2.css → Chat-DTEsebnY.css} +1 -1
- package/client/web/dist/assets/{MsgBody-DDacjWJ9.js → MsgBody-C0TKsXhC.js} +1 -1
- 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
- package/client/web/dist/assets/{append_darwin-CqBNU62J.css → append_darwin-Cql-dV6d.css} +1 -1
- package/client/web/dist/assets/{append_full_vibrancy-j0D2LM9m.css → append_full_vibrancy-ih3loy1O.css} +1 -1
- package/client/web/dist/assets/{append_linux-uhxeBqeC.css → append_linux-B3LvR8KB.css} +1 -1
- package/client/web/dist/assets/{append_new-407Lo5_V.css → append_new-DfhN_NX9.css} +1 -1
- package/client/web/dist/assets/{append_vibrancy-6FHctkOY.css → append_vibrancy-D3pVsmAa.css} +1 -1
- package/client/web/dist/assets/{append_win32-srmD-78y.css → append_win32-Dh9JlmlE.css} +1 -1
- package/client/web/dist/assets/index-B71pPwae.css +1 -0
- package/client/web/dist/assets/{index-BlLut5xn.js → index-BA37EQGa.js} +97 -76
- package/client/web/dist/index.html +2 -2
- package/client/web/src/App.vue +2 -2
- package/client/web/src/assets/css/append/append_darwin.css +2 -2
- package/client/web/src/assets/css/append/append_full_vibrancy.css +2 -2
- package/client/web/src/assets/css/append/append_linux.css +2 -2
- package/client/web/src/assets/css/append/append_new.css +2 -2
- package/client/web/src/assets/css/append/append_vibrancy.css +1 -1
- package/client/web/src/assets/css/append/append_win32.css +2 -2
- package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +1 -1
- package/client/web/src/assets/css/chat.css +4 -3
- package/client/web/src/assets/css/msg.css +7 -3
- package/client/web/src/assets/css/options.css +2 -2
- package/client/web/src/assets/css/view.css +14 -3
- package/client/web/src/assets/l10n/zh-CN.po +21 -0
- package/client/web/src/components/AboutPan.vue +7 -4
- package/client/web/src/components/BulletinBody.vue +1 -1
- package/client/web/src/components/DepPan.vue +1 -1
- package/client/web/src/components/FriendBody.vue +1 -0
- package/client/web/src/components/MsgBody.vue +1 -3
- package/client/web/src/components/ThemeColorPickerPan.vue +1 -1
- package/client/web/src/components/TinySessionBody.vue +1 -0
- package/client/web/src/components/UmamiInfoPan.vue +1 -1
- package/client/web/src/components/UpdatePan.vue +2 -2
- package/client/web/src/components/WelPan.vue +1 -1
- package/client/web/src/components/tooltip/UserInfoTooltip.vue +1 -1
- package/client/web/src/function/connect.ts +279 -86
- package/client/web/src/function/elements/information.ts +1 -0
- package/client/web/src/function/msg.ts +42 -23
- package/client/web/src/function/satori-model.ts +35 -13
- package/client/web/src/function/satori.ts +33 -2
- package/client/web/src/function/utils/appUtil.ts +3 -11
- package/client/web/src/function/utils/avatarUtil.ts +18 -3
- package/client/web/src/function/utils/localHistoryUtil.ts +36 -12
- package/client/web/src/function/utils/msgUtil.ts +32 -21
- package/client/web/src/function/utils/sessionUtil.ts +20 -18
- package/client/web/src/main.ts +5 -0
- package/client/web/src/pages/Chat.vue +50 -47
- package/client/web/src/pages/Friends.vue +28 -24
- package/client/web/src/pages/Info.vue +5 -5
- package/client/web/src/pages/Messages.vue +57 -36
- package/client/web/src/pages/Qzone.vue +2 -2
- package/client/web/src/pages/chat-view/ChatTerminal.vue +4 -1
- package/client/web/src/pages/chat-view/SystemNotice.vue +3 -3
- package/client/web/src/pages/options/OptAccount.vue +6 -4
- package/client/web/src/pages/options/OptDev.vue +45 -2
- package/lib/database.d.ts +58 -0
- package/lib/index.js +234 -82
- package/lib/media.d.ts +24 -0
- package/lib/recorder.d.ts +21 -0
- package/lib/self-message.d.ts +27 -0
- package/lib/types.d.ts +121 -0
- package/lib/web.d.ts +7 -0
- package/package.json +1 -1
- package/src/bootstrap.ts +1 -0
- package/src/cache.ts +1 -5
- package/src/config.ts +1 -1
- package/src/database.ts +52 -8
- package/src/index.ts +4 -4
- package/src/media.ts +97 -0
- package/src/recorder.ts +35 -26
- package/src/self-message.ts +68 -19
- package/src/types.ts +10 -0
- package/src/web.ts +57 -48
- package/client/web/dist/assets/Chat-B448YOeN.js +0 -28
- package/client/web/dist/assets/index-fW_zcub3.css +0 -1
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
normalizeSessionId,
|
|
96
96
|
resolveIncomingSession,
|
|
97
97
|
setSessionContact,
|
|
98
|
+
upsertSessionContact,
|
|
98
99
|
} from './utils/sessionUtil'
|
|
99
100
|
|
|
100
101
|
const popInfo = new PopInfo()
|
|
@@ -120,11 +121,6 @@ function getString(value: unknown): string {
|
|
|
120
121
|
return typeof value === 'string' ? value : String(value)
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
function normalizeChannelForMatch(value: string): string {
|
|
124
|
-
if (/^(?:private|direct):/i.test(value)) return value.replace(/^(?:private|direct):/i, '')
|
|
125
|
-
return normalizeGroupId(value)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
124
|
// 其他 tag
|
|
129
125
|
let listLoadTimes = 0
|
|
130
126
|
const logger = new Logger()
|
|
@@ -995,10 +991,13 @@ const msgFunctions = {
|
|
|
995
991
|
const chatStore = useChatStore()
|
|
996
992
|
const rawData = msg.data
|
|
997
993
|
const firstData = Array.isArray(rawData) ? rawData[0] : rawData
|
|
994
|
+
const firstRawId = getDataObject(firstData).id
|
|
995
|
+
?? getDataObject(rawData).id
|
|
998
996
|
const realMessageId = getString(msg.message_id)
|
|
999
997
|
|| getString(getDataObject(rawData).message_id)
|
|
1000
998
|
|| getString(getDataObject(firstData).id)
|
|
1001
999
|
|| getString(getDataObject(firstData).message_id)
|
|
1000
|
+
|| (firstRawId == null ? '' : String(firstRawId))
|
|
1002
1001
|
if (realMessageId) msg.message_id = realMessageId
|
|
1003
1002
|
if (echoList[1] == 'forward') {
|
|
1004
1003
|
// PS:这儿写是写了转发成功,事实上不确定消息有没有真的发送出去(x
|
|
@@ -1341,8 +1340,16 @@ const msgFunctions = {
|
|
|
1341
1340
|
)
|
|
1342
1341
|
getMessageList(trueMsg).then((trueMsg) => {
|
|
1343
1342
|
if (trueMsg?.length == 1) {
|
|
1343
|
+
const nextMessage = Array.isArray(trueMsg[0].message)
|
|
1344
|
+
&& trueMsg[0].message.some((item: any) => {
|
|
1345
|
+
if (item?.type !== 'text') return true
|
|
1346
|
+
const text = String(item?.text ?? item?.content ?? '')
|
|
1347
|
+
return text.trim() !== ''
|
|
1348
|
+
})
|
|
1349
|
+
? trueMsg[0].message
|
|
1350
|
+
: fakeMsg.message
|
|
1344
1351
|
// 使用消息对象引用直接更新,避免索引问题
|
|
1345
|
-
fakeMsg.message =
|
|
1352
|
+
if (Array.isArray(nextMessage)) fakeMsg.message = nextMessage
|
|
1346
1353
|
fakeMsg.raw_message = trueMsg[0].raw_message
|
|
1347
1354
|
fakeMsg.time = trueMsg[0].time
|
|
1348
1355
|
fakeMsg.fake_msg = undefined
|
|
@@ -1699,8 +1706,9 @@ function saveUser(msg: { [key: string]: any }, type: string) {
|
|
|
1699
1706
|
const channel = String(item.channel_id ?? item.channelId ?? '')
|
|
1700
1707
|
const group = String(item.group_id ?? '')
|
|
1701
1708
|
const user = String(item.user_id ?? '')
|
|
1702
|
-
if (
|
|
1703
|
-
|
|
1709
|
+
if (channel) return normalizeSessionId(channel)
|
|
1710
|
+
if (group) return normalizeSessionId(group)
|
|
1711
|
+
return normalizeSessionId(user)
|
|
1704
1712
|
}
|
|
1705
1713
|
const existingIds = new Set(contactStore.userList.map(sessionDedupKey))
|
|
1706
1714
|
const freshList = list.filter((item) => {
|
|
@@ -1891,19 +1899,28 @@ async function saveMsg(msg: any, append = undefined as undefined | string) {
|
|
|
1891
1899
|
const lastMsg =
|
|
1892
1900
|
chatStore.messageList[chatStore.messageList.length - 1]
|
|
1893
1901
|
if (lastMsg) {
|
|
1902
|
+
const isGroup = chatStore.chatInfo.show.type === 'group'
|
|
1894
1903
|
const user = findSessionContact(contactStore.userList, chatStore.chatInfo.show.id)
|
|
1895
1904
|
const sessionId = normalizeSessionId(
|
|
1896
1905
|
chatStore.chatInfo.show.channel_id
|
|
1897
1906
|
?? chatStore.chatInfo.show.id,
|
|
1898
1907
|
)
|
|
1899
|
-
const session =
|
|
1908
|
+
const session = isGroup
|
|
1909
|
+
? user
|
|
1910
|
+
: contactStore.baseOnMsgList.get(sessionId) ?? user
|
|
1900
1911
|
if (session) {
|
|
1901
1912
|
const preview = formatMessageData(
|
|
1902
1913
|
lastMsg,
|
|
1903
|
-
|
|
1914
|
+
isGroup,
|
|
1904
1915
|
)
|
|
1905
1916
|
if (user) Object.assign(user, preview)
|
|
1906
1917
|
Object.assign(session, preview)
|
|
1918
|
+
if (isGroup) {
|
|
1919
|
+
// 群历史同步到联系人列表,同时进入群会话列表
|
|
1920
|
+
contactStore.userList = upsertSessionContact(contactStore.userList, session)
|
|
1921
|
+
} else if (session._groupMember) {
|
|
1922
|
+
session._groupMember = false
|
|
1923
|
+
}
|
|
1907
1924
|
contactStore.baseOnMsgList.set(sessionId, session)
|
|
1908
1925
|
updateBaseOnMsgList()
|
|
1909
1926
|
}
|
|
@@ -2326,7 +2343,7 @@ function newMsg(_: string, data: any) {
|
|
|
2326
2343
|
}
|
|
2327
2344
|
// 消息基础信息:优先使用 Satori 转换层生成的 infoList,避免 OneBot pathMap 误解析。
|
|
2328
2345
|
const rawInfo = getDataObject(data.infoList ?? data)
|
|
2329
|
-
const
|
|
2346
|
+
const rawId = getString(rawInfo.group_id) ||
|
|
2330
2347
|
getString(rawInfo.private_id) ||
|
|
2331
2348
|
getString(data.group_id) ||
|
|
2332
2349
|
getString(data.user_id) ||
|
|
@@ -2354,12 +2371,16 @@ function newMsg(_: string, data: any) {
|
|
|
2354
2371
|
: data.message_type === 'group' || Boolean(data.group_id) || Boolean(rawInfo.group_id)
|
|
2355
2372
|
const sessionChannelId = getString(rawInfo.channel_id) || getString(data.channel_id)
|
|
2356
2373
|
const sessionGuildId = getString(rawInfo.guild_id) || getString(data.guild_id)
|
|
2374
|
+
// 群消息必须用群/频道 ID 作为会话,不能退化成发送者用户 ID
|
|
2375
|
+
const id = isGroupMessage
|
|
2376
|
+
? getString(rawInfo.group_id) || getString(data.group_id) || sessionChannelId || sessionGuildId || ''
|
|
2377
|
+
: rawId
|
|
2357
2378
|
if (!id || id === '0') return
|
|
2358
2379
|
const isCurrentSession = Boolean(
|
|
2359
2380
|
(
|
|
2360
2381
|
sessionChannelId &&
|
|
2361
2382
|
showChannelId &&
|
|
2362
|
-
|
|
2383
|
+
sessionChannelId === showChannelId
|
|
2363
2384
|
) || (
|
|
2364
2385
|
(!sessionChannelId || !showChannelId) &&
|
|
2365
2386
|
(id === showId || targetId === showId || String(sessionChannelId || sessionGuildId || '') === showId)
|
|
@@ -2572,6 +2593,13 @@ function newMsg(_: string, data: any) {
|
|
|
2572
2593
|
if (data.atall) { session.highlight = $t('[@全体]') }
|
|
2573
2594
|
if (isImportant) { session.highlight = $t('[特別关心]') }
|
|
2574
2595
|
}
|
|
2596
|
+
if (!isGroupMessage && session._groupMember) {
|
|
2597
|
+
session._groupMember = false
|
|
2598
|
+
}
|
|
2599
|
+
if (isGroupMessage) {
|
|
2600
|
+
// 群消息同步到联系人列表,同时进入群会话列表
|
|
2601
|
+
contactStore.userList = upsertSessionContact(contactStore.userList, session)
|
|
2602
|
+
}
|
|
2575
2603
|
setSessionContact(contactStore.baseOnMsgList, session)
|
|
2576
2604
|
updateBaseOnMsgList()
|
|
2577
2605
|
}
|
|
@@ -2589,17 +2617,8 @@ function newMsg(_: string, data: any) {
|
|
|
2589
2617
|
hidden: document.hidden,
|
|
2590
2618
|
isImportant: isImportant
|
|
2591
2619
|
})
|
|
2592
|
-
//
|
|
2593
|
-
|
|
2594
|
-
const forceImportantNotice = isImportant ||
|
|
2595
|
-
(isGroupMessage && (data.atme || data.atall || isGroupNotice))
|
|
2596
|
-
if (
|
|
2597
|
-
forceGroupSystemNotice ||
|
|
2598
|
-
forceImportantNotice ||
|
|
2599
|
-
!isCurrentSession ||
|
|
2600
|
-
!document.hasFocus() ||
|
|
2601
|
-
document.hidden
|
|
2602
|
-
) {
|
|
2620
|
+
// 当前会话已打开时不弹系统通知,新消息交给右下角置底箭头提示
|
|
2621
|
+
if (!isCurrentSession) {
|
|
2603
2622
|
// 准备消息内容
|
|
2604
2623
|
let raw = getMsgRawTxt(data)
|
|
2605
2624
|
raw = raw === '' ? data.raw_message : raw
|
|
@@ -21,9 +21,7 @@ function getString(value: unknown): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function normalizeGroupId(value: string): string {
|
|
24
|
-
|
|
25
|
-
const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/)
|
|
26
|
-
return wrapped ? wrapped[1] : raw || value
|
|
24
|
+
return String(value)
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
function isDirectChannelType(channel: SatoriObject): boolean {
|
|
@@ -234,6 +232,13 @@ function containsElementMarkup(source: string): boolean {
|
|
|
234
232
|
return /<(img|image|audio|video|file|mface|face|quote|at|forward|json|xml|markdown|keyboard|p|br|i18n|a|text|sharp)(?:\s|\/|>)/i.test(source)
|
|
235
233
|
}
|
|
236
234
|
|
|
235
|
+
// QQ 官方机器人会把部分大表情/图片消息的私有标记塞进 content,渲染前需要去掉
|
|
236
|
+
function cleanQqFaceMarkup(source: string): string {
|
|
237
|
+
return source
|
|
238
|
+
.replace(/<faceType\b[^>]*>/gi, '')
|
|
239
|
+
.replace(/<faceType\b[^>]*>/gi, '')
|
|
240
|
+
}
|
|
241
|
+
|
|
237
242
|
function splitMarkupTextSegments(
|
|
238
243
|
segments: Array<Record<string, unknown>>,
|
|
239
244
|
): Array<Record<string, unknown>> {
|
|
@@ -250,6 +255,7 @@ function splitMarkupTextSegments(
|
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
export function parseSatoriMarkup(source: string): Array<Record<string, unknown>> {
|
|
258
|
+
source = cleanQqFaceMarkup(source)
|
|
253
259
|
const result: Array<Record<string, unknown>> = []
|
|
254
260
|
let index = 0
|
|
255
261
|
while (index < source.length) {
|
|
@@ -441,11 +447,11 @@ export function satoriEventToOneBot(
|
|
|
441
447
|
if (type === 'message' || type === 'message-created' || type === 'send') {
|
|
442
448
|
const isGroup = isGroupChannel(channel, guild)
|
|
443
449
|
const userId = getString(user.id)
|
|
444
|
-
|| (isGroup ? '' : getString(channel.id)
|
|
450
|
+
|| (isGroup ? '' : getString(channel.id))
|
|
445
451
|
const groupId = isGroup
|
|
446
452
|
? normalizeGroupId(getString(guild.id)) || normalizeGroupId(getString(channel.id))
|
|
447
453
|
: ''
|
|
448
|
-
const directChannelId = isGroup ? groupId : userId
|
|
454
|
+
const directChannelId = isGroup ? groupId : getString(channel.id) || userId
|
|
449
455
|
const nickname = getString(user.name) || getString(user.nick) || userId
|
|
450
456
|
const card = getString(member.nick) || getString(member.name) || ''
|
|
451
457
|
return {
|
|
@@ -462,7 +468,7 @@ export function satoriEventToOneBot(
|
|
|
462
468
|
group_avatar: isGroup
|
|
463
469
|
? (usableAvatar(guild.avatar) || usableAvatar(channel.avatar) || buildQqGroupAvatar(platform, groupId) || undefined)
|
|
464
470
|
: undefined,
|
|
465
|
-
channel_id: getString(channel.id) || (isGroup ? groupId :
|
|
471
|
+
channel_id: getString(channel.id) || (isGroup ? groupId : userId),
|
|
466
472
|
guild_id: getString(guild.id),
|
|
467
473
|
target_id: selfId,
|
|
468
474
|
message: messageSegments(message),
|
|
@@ -478,7 +484,7 @@ export function satoriEventToOneBot(
|
|
|
478
484
|
message_id: getString(message.id),
|
|
479
485
|
private_id: isGroup ? '' : directChannelId,
|
|
480
486
|
group_id: groupId,
|
|
481
|
-
channel_id: getString(channel.id) || (isGroup ? groupId :
|
|
487
|
+
channel_id: getString(channel.id) || (isGroup ? groupId : userId),
|
|
482
488
|
guild_id: getString(guild.id),
|
|
483
489
|
target_id: selfId,
|
|
484
490
|
sender: userId,
|
|
@@ -525,6 +531,24 @@ export function satoriEventToOneBot(
|
|
|
525
531
|
}
|
|
526
532
|
}
|
|
527
533
|
|
|
534
|
+
if (type === 'message-deleted') {
|
|
535
|
+
const isGroup = isGroupChannel(channel, guild)
|
|
536
|
+
const userId = getString(user.id) || (isGroup ? '' : getString(channel.id))
|
|
537
|
+
const groupId = isGroup
|
|
538
|
+
? normalizeGroupId(getString(guild.id)) || normalizeGroupId(getString(channel.id))
|
|
539
|
+
: ''
|
|
540
|
+
return {
|
|
541
|
+
...base,
|
|
542
|
+
post_type: 'notice',
|
|
543
|
+
notice_type: isGroup ? 'group_recall' : 'friend_recall',
|
|
544
|
+
group_id: groupId,
|
|
545
|
+
user_id: userId,
|
|
546
|
+
message_id: getString(message.id),
|
|
547
|
+
channel_id: getString(channel.id) || (isGroup ? groupId : userId),
|
|
548
|
+
guild_id: getString(guild.id),
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
528
552
|
return null
|
|
529
553
|
}
|
|
530
554
|
|
|
@@ -629,9 +653,7 @@ export function mapAction(action: string, params: Record<string, unknown>): {
|
|
|
629
653
|
const guildId = getString(params.group_id) || getString(params.guild_id)
|
|
630
654
|
const userId = getString(params.user_id)
|
|
631
655
|
const messageId = getString(params.message_id)
|
|
632
|
-
const directChannel = userId
|
|
633
|
-
? `private:${userId}`
|
|
634
|
-
: userId
|
|
656
|
+
const directChannel = userId
|
|
635
657
|
const channelId = getString(params.channel_id) || guildId || directChannel || userId
|
|
636
658
|
const content = Array.isArray(params.message)
|
|
637
659
|
? serializeSatoriContent(params.message)
|
|
@@ -688,8 +710,8 @@ function messageListFromResponse(data: unknown): unknown[] {
|
|
|
688
710
|
? normalizeGroupId(getString(guild.id)) || normalizeGroupId(getString(channel.id))
|
|
689
711
|
: ''
|
|
690
712
|
const userId = getString(user.id)
|
|
691
|
-
|| (isGroup ? '' : getString(channel.id)
|
|
692
|
-
const directChannelId = isGroup ? groupId : userId
|
|
713
|
+
|| (isGroup ? '' : getString(channel.id))
|
|
714
|
+
const directChannelId = isGroup ? groupId : getString(channel.id) || userId
|
|
693
715
|
return {
|
|
694
716
|
message_id: getString(message.id),
|
|
695
717
|
sn: getNumber(message.sn),
|
|
@@ -701,7 +723,7 @@ function messageListFromResponse(data: unknown): unknown[] {
|
|
|
701
723
|
channel_type: channel.type,
|
|
702
724
|
group_id: groupId,
|
|
703
725
|
user_id: groupId ? userId : directChannelId,
|
|
704
|
-
channel_id: getString(channel.id) || (isGroup ? groupId :
|
|
726
|
+
channel_id: getString(channel.id) || (isGroup ? groupId : userId),
|
|
705
727
|
guild_id: getString(guild.id),
|
|
706
728
|
sender: {
|
|
707
729
|
user_id: userId,
|
|
@@ -4,6 +4,10 @@ export interface SatoriBootstrap {
|
|
|
4
4
|
endpoint: string
|
|
5
5
|
token: string
|
|
6
6
|
basePath: string
|
|
7
|
+
blockedPlatforms: Array<{
|
|
8
|
+
platformName: string
|
|
9
|
+
exactMatch: boolean
|
|
10
|
+
}>
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export interface SatoriLogin {
|
|
@@ -24,6 +28,11 @@ export interface SatoriEvent {
|
|
|
24
28
|
body: Record<string, unknown>
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
// 同一机器人 ID 可能同时存在于不同平台(例如 qq / qqguild),用平台 + ID 做唯一键
|
|
32
|
+
export function botKey(platform: string, selfId: string): string {
|
|
33
|
+
return [platform, selfId].map((value) => encodeURIComponent(value)).join(':')
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
interface PendingConsoleRequest {
|
|
28
37
|
resolve: (value: unknown) => void
|
|
29
38
|
reject: (reason: unknown) => void
|
|
@@ -47,6 +56,17 @@ function getObject(value: unknown): Record<string, unknown> {
|
|
|
47
56
|
return typeof value === 'object' && value !== null ? value as Record<string, unknown> : {}
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
function isBlockedPlatform(
|
|
60
|
+
platform: string,
|
|
61
|
+
blockedPlatforms: SatoriBootstrap['blockedPlatforms'],
|
|
62
|
+
): boolean {
|
|
63
|
+
return (blockedPlatforms ?? []).some((item) => {
|
|
64
|
+
return item.exactMatch
|
|
65
|
+
? platform === item.platformName
|
|
66
|
+
: platform.includes(item.platformName)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
async function parseJsonResponse(response: Response): Promise<unknown> {
|
|
51
71
|
const text = await response.text()
|
|
52
72
|
if (!response.ok) {
|
|
@@ -55,7 +75,8 @@ async function parseJsonResponse(response: Response): Promise<unknown> {
|
|
|
55
75
|
try {
|
|
56
76
|
return JSON.parse(text) as unknown
|
|
57
77
|
} catch {
|
|
58
|
-
|
|
78
|
+
// 部分平台对 delete 等操作只回显纯文本(例如 OK),HTTP 200 即视为成功
|
|
79
|
+
return text
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
|
|
@@ -94,6 +115,9 @@ function receiveBootstrap(data: Record<string, unknown>) {
|
|
|
94
115
|
endpoint: getString(payload.endpoint) || '/satori',
|
|
95
116
|
token: getString(payload.token),
|
|
96
117
|
basePath: getString(payload.basePath) || '/chat-patch',
|
|
118
|
+
blockedPlatforms: Array.isArray(payload.blockedPlatforms)
|
|
119
|
+
? payload.blockedPlatforms as SatoriBootstrap['blockedPlatforms']
|
|
120
|
+
: [],
|
|
97
121
|
}
|
|
98
122
|
window.clearTimeout(bootstrapTimeout)
|
|
99
123
|
resolveBootstrapCallback?.(bootstrap)
|
|
@@ -118,6 +142,10 @@ export function getBootstrap(): Promise<SatoriBootstrap> {
|
|
|
118
142
|
return resolveBootstrap()
|
|
119
143
|
}
|
|
120
144
|
|
|
145
|
+
export function getBlockedPlatforms(): SatoriBootstrap['blockedPlatforms'] {
|
|
146
|
+
return bootstrap?.blockedPlatforms ?? []
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
export function getLogins(): SatoriLogin[] {
|
|
122
150
|
return logins
|
|
123
151
|
}
|
|
@@ -219,6 +247,9 @@ export function connect(
|
|
|
219
247
|
endpoint: getString(payload.endpoint) || '/satori',
|
|
220
248
|
token: getString(payload.token),
|
|
221
249
|
basePath: getString(payload.basePath) || '/chat-patch',
|
|
250
|
+
blockedPlatforms: Array.isArray(payload.blockedPlatforms)
|
|
251
|
+
? payload.blockedPlatforms as SatoriBootstrap['blockedPlatforms']
|
|
252
|
+
: [],
|
|
222
253
|
}
|
|
223
254
|
}
|
|
224
255
|
}
|
|
@@ -258,7 +289,7 @@ export function connect(
|
|
|
258
289
|
const platform = getString(login.platform) || getString(user.platform)
|
|
259
290
|
const selfId = getString(user.id) || getString(login.selfId)
|
|
260
291
|
const key = [platform, selfId].map((value) => encodeURIComponent(value)).join(':')
|
|
261
|
-
if (!platform || !selfId || seen.has(key)) continue
|
|
292
|
+
if (!platform || !selfId || seen.has(key) || isBlockedPlatform(platform, bootstrap?.blockedPlatforms ?? [])) continue
|
|
262
293
|
seen.add(key)
|
|
263
294
|
nextLogins.push({
|
|
264
295
|
platform: getString(login.platform) || getString(user.platform),
|
|
@@ -53,11 +53,6 @@ function encodeKeyPart(value: unknown): string {
|
|
|
53
53
|
return encodeURIComponent(String(value ?? ''))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function ensureChannelPrefix(channelId: string, prefix: string): string {
|
|
57
|
-
if (/^(?:group|room|chat|channel|guild|private):/i.test(channelId)) return channelId
|
|
58
|
-
return `${prefix}:${channelId}`
|
|
59
|
-
}
|
|
60
|
-
|
|
61
56
|
/**
|
|
62
57
|
* 滚动到目标消息(不自动加载)
|
|
63
58
|
* @param seqName DOM 名(chat-xx)
|
|
@@ -121,7 +116,8 @@ export async function loadHistory(info: BaseChatInfoElem) {
|
|
|
121
116
|
uiStore.nowGetHistory = false
|
|
122
117
|
uiStore.historyBeforeTime = undefined
|
|
123
118
|
chatStore.messageList = []
|
|
124
|
-
const
|
|
119
|
+
const channelId = String(info.channel_id ?? info.id ?? '')
|
|
120
|
+
const cacheKey = [authStore.loginInfo.platform, authStore.loginInfo.uin, channelId].map(encodeKeyPart).join(':')
|
|
125
121
|
const cachedMessages = chatStore.sessionMessageCache.get(cacheKey)
|
|
126
122
|
if (cachedMessages?.length) {
|
|
127
123
|
chatStore.messageList = [...cachedMessages]
|
|
@@ -133,17 +129,13 @@ export async function loadHistory(info: BaseChatInfoElem) {
|
|
|
133
129
|
) {
|
|
134
130
|
const localMsgs = await dbGetLatest(
|
|
135
131
|
authStore.loginInfo.uin,
|
|
136
|
-
|
|
132
|
+
channelId,
|
|
137
133
|
20,
|
|
138
134
|
)
|
|
139
135
|
if (localMsgs.length > 0) {
|
|
140
136
|
chatStore.messageList = localMsgs
|
|
141
137
|
}
|
|
142
138
|
}
|
|
143
|
-
const rawChannelId = String(info.channel_id ?? '')
|
|
144
|
-
const channelId = info.type === 'group'
|
|
145
|
-
? ensureChannelPrefix(rawChannelId || String(info.id), 'group')
|
|
146
|
-
: ensureChannelPrefix(rawChannelId || String(info.id), 'private')
|
|
147
139
|
try {
|
|
148
140
|
const cachedMessages = await loadChatHistoryFromCache({
|
|
149
141
|
platform: String(authStore.loginInfo.platform ?? ''),
|
|
@@ -7,6 +7,8 @@ const QQ_GROUP_AVATAR_PLATFORMS = new Set([
|
|
|
7
7
|
'llob',
|
|
8
8
|
])
|
|
9
9
|
|
|
10
|
+
export const DEFAULT_AVATAR = '/img/icons/icon.svg'
|
|
11
|
+
|
|
10
12
|
// 仅用于白名单平台的 QQ 群头像,不能用于用户头像
|
|
11
13
|
export function buildQqGroupAvatar(
|
|
12
14
|
platform: string,
|
|
@@ -24,8 +26,21 @@ export function buildQqGroupAvatar(
|
|
|
24
26
|
return `https://p.qlogo.cn/gh/${rawId}/${rawId}/${safeSize}`
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
// 命中头像上下文后,任何加载失败都统一换默认 SVG
|
|
30
|
+
export function isAvatarImage(img: HTMLImageElement): boolean {
|
|
31
|
+
if (img.hasAttribute('data-avatar') || img.getAttribute('name') === 'avatar') return true
|
|
32
|
+
if (img.dataset.avatarFallback === 'true') return true
|
|
33
|
+
if (img.closest('[data-avatar], .avatar, [class*="avatar"], [name="avatar"]')) return true
|
|
34
|
+
const source = img.currentSrc || img.src || img.getAttribute('src') || ''
|
|
35
|
+
return !source ? false : /(?:q\.qlogo\.cn|p\.qlogo\.cn|\/qqapp\/)/i.test(source)
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export function avatarError(event: Event) {
|
|
28
|
-
const img = event.target
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
const img = event.target
|
|
40
|
+
if (!(img instanceof HTMLImageElement)) return
|
|
41
|
+
const fallback = new URL(DEFAULT_AVATAR, location.origin).href
|
|
42
|
+
const source = img.currentSrc || img.src
|
|
43
|
+
if (!source || source === fallback || img.dataset.avatarFallback === 'true') return
|
|
44
|
+
img.dataset.avatarFallback = 'true'
|
|
45
|
+
img.src = fallback
|
|
31
46
|
}
|
|
@@ -18,7 +18,7 @@ const logger = new Logger()
|
|
|
18
18
|
export interface LocalMsgRecord {
|
|
19
19
|
/** Bot 侧的消息 ID */
|
|
20
20
|
message_id: string
|
|
21
|
-
/** 会话 ID
|
|
21
|
+
/** 会话 ID:保存完整 channel_id,不剥离前缀 */
|
|
22
22
|
chat_id: number | string
|
|
23
23
|
/** 会话类型:"group" | "private" */
|
|
24
24
|
chat_type: string
|
|
@@ -115,18 +115,28 @@ function computeRawMessage(msg: any): string | null {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function firstDefinedId(value: any, keys: string[]): number | string | undefined {
|
|
119
|
+
for (const key of keys) {
|
|
120
|
+
const current = value?.[key]
|
|
121
|
+
if (current != null && String(current) !== '') return current
|
|
122
|
+
}
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
118
126
|
function deriveChatId(selfId: string | number, msgs: any[]): number | string | undefined {
|
|
119
127
|
const firstMsg = msgs[0]
|
|
120
|
-
|
|
128
|
+
const firstInfo = firstMsg?.infoList ?? firstMsg
|
|
129
|
+
const chatId = firstDefinedId(firstInfo, ['channel_id', 'channelId', 'private_id', 'group_id', 'user_id'])
|
|
130
|
+
?? firstDefinedId(firstMsg, ['channel_id', 'channelId', 'private_id', 'group_id', 'user_id'])
|
|
121
131
|
if (chatId != null) return chatId
|
|
122
132
|
|
|
123
133
|
for (const item of msgs) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
const info = item?.infoList ?? item
|
|
135
|
+
if (info?.sender != null && String(info.sender) !== String(selfId)) {
|
|
136
|
+
return info.sender
|
|
127
137
|
}
|
|
128
138
|
}
|
|
129
|
-
return
|
|
139
|
+
return undefined
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
export function ensureChatIdOnMsgs(selfId: string | number, msgs: any[]): any[] {
|
|
@@ -135,14 +145,21 @@ export function ensureChatIdOnMsgs(selfId: string | number, msgs: any[]): any[]
|
|
|
135
145
|
|
|
136
146
|
return msgs.map((item: any) => {
|
|
137
147
|
if (!item?.infoList) return item
|
|
138
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
item.infoList.channel_id != null
|
|
150
|
+
|| item.infoList.channelId != null
|
|
151
|
+
|| item.infoList.private_id != null
|
|
152
|
+
|| item.infoList.group_id != null
|
|
153
|
+
|| item.infoList.user_id != null
|
|
154
|
+
) {
|
|
139
155
|
return item
|
|
140
156
|
}
|
|
141
157
|
return {
|
|
142
158
|
...item,
|
|
143
159
|
infoList: {
|
|
144
160
|
...item.infoList,
|
|
145
|
-
|
|
161
|
+
channel_id: chatId,
|
|
162
|
+
channelId: chatId,
|
|
146
163
|
},
|
|
147
164
|
}
|
|
148
165
|
})
|
|
@@ -158,12 +175,14 @@ export function msgToRecord(msg: any): LocalMsgRecord | null {
|
|
|
158
175
|
const messageId = msg.message_id
|
|
159
176
|
if (!messageId) return null
|
|
160
177
|
|
|
161
|
-
const
|
|
178
|
+
const info = msg.infoList ?? msg
|
|
179
|
+
const chatId = firstDefinedId(info, ['channel_id', 'channelId', 'private_id', 'group_id', 'user_id'])
|
|
180
|
+
?? firstDefinedId(msg, ['channel_id', 'channelId', 'private_id', 'group_id', 'user_id'])
|
|
162
181
|
if (chatId == null) return null
|
|
163
182
|
|
|
164
183
|
const chatType: string =
|
|
165
184
|
msg.message_type ?? (msg.group_id != null ? 'group' : 'private')
|
|
166
|
-
const senderId: number | string = msg.
|
|
185
|
+
const senderId: number | string = info.sender ?? msg.sender?.user_id
|
|
167
186
|
if (senderId == null) return null
|
|
168
187
|
|
|
169
188
|
const senderName: string | null =
|
|
@@ -490,7 +509,8 @@ function deserializeRecord(record: LocalMsgRecord): any {
|
|
|
490
509
|
message_id: record.message_id,
|
|
491
510
|
private_id: isGroup ? undefined : record.chat_id,
|
|
492
511
|
group_id: isGroup ? record.chat_id : undefined,
|
|
493
|
-
|
|
512
|
+
channel_id: record.chat_id,
|
|
513
|
+
channelId: record.chat_id,
|
|
494
514
|
sender: record.sender_id,
|
|
495
515
|
}
|
|
496
516
|
|
|
@@ -499,7 +519,9 @@ function deserializeRecord(record: LocalMsgRecord): any {
|
|
|
499
519
|
message_id: record.message_id,
|
|
500
520
|
message_type: record.chat_type,
|
|
501
521
|
// 根据 chat_type 恢复对应的 id 字段
|
|
502
|
-
...(isGroup ? { group_id: record.chat_id } : { user_id: record.
|
|
522
|
+
...(isGroup ? { group_id: record.chat_id } : { user_id: record.sender_id }),
|
|
523
|
+
channel_id: record.chat_id,
|
|
524
|
+
channelId: record.chat_id,
|
|
503
525
|
sender,
|
|
504
526
|
time: record.time,
|
|
505
527
|
timestamp_ms: timeMs || undefined,
|
|
@@ -509,6 +531,8 @@ function deserializeRecord(record: LocalMsgRecord): any {
|
|
|
509
531
|
infoList,
|
|
510
532
|
raw_message: record.raw_message ?? '',
|
|
511
533
|
revoked: record.revoked,
|
|
534
|
+
revoke: record.revoked,
|
|
535
|
+
revokeTime: record.revoked ? Date.now() / 1000 : undefined,
|
|
512
536
|
// 消息序列号(并非所有 Bot 都提供,可为 null)
|
|
513
537
|
...(record.seq != null ? { message_seq: record.seq, seq_id: record.seq } : {}),
|
|
514
538
|
// 标记来源为本地缓存,业务层可按需用此字段区分
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getSessionId,
|
|
28
28
|
mergeSessionState,
|
|
29
29
|
normalizeSessionId,
|
|
30
|
+
upsertSessionContact,
|
|
30
31
|
} from './sessionUtil'
|
|
31
32
|
|
|
32
33
|
const logger = new Logger()
|
|
@@ -681,21 +682,31 @@ export async function sendMsgRaw(
|
|
|
681
682
|
chatStore.messageList = chatStore.messageList.concat([showMsg])
|
|
682
683
|
|
|
683
684
|
// 发送方不一定会上报自身消息事件,先用预发送消息同步会话预览。
|
|
684
|
-
const sessionId = normalizeSessionId(
|
|
685
|
+
const sessionId = normalizeSessionId(
|
|
686
|
+
targetChannelId
|
|
687
|
+
|| chatStore.chatInfo.show.channel_id
|
|
688
|
+
|| String(id).split('/')[0],
|
|
689
|
+
)
|
|
685
690
|
const session = contactStore.baseOnMsgList.get(sessionId) ??
|
|
686
691
|
findSessionContact(contactStore.userList, sessionId)
|
|
687
692
|
if (session) {
|
|
688
693
|
const raw = getMsgRawTxt(showMsg)
|
|
689
694
|
const senderName = authStore.loginInfo.nickname
|
|
690
|
-
Object.assign(session, {
|
|
691
|
-
message_id: showMsg.message_id,
|
|
692
|
-
raw_msg: type === 'group' && senderName ? `${senderName}: ${raw}` : raw,
|
|
693
|
-
raw_msg_base: raw,
|
|
694
|
-
time: showMsg.time * 1000,
|
|
695
|
-
})
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
695
|
+
Object.assign(session, {
|
|
696
|
+
message_id: showMsg.message_id,
|
|
697
|
+
raw_msg: type === 'group' && senderName ? `${senderName}: ${raw}` : raw,
|
|
698
|
+
raw_msg_base: raw,
|
|
699
|
+
time: showMsg.time * 1000,
|
|
700
|
+
})
|
|
701
|
+
if (type === 'group') {
|
|
702
|
+
// 群发送预览同步到联系人列表,同时进入群会话列表
|
|
703
|
+
contactStore.userList = upsertSessionContact(contactStore.userList, session)
|
|
704
|
+
} else if (session._groupMember) {
|
|
705
|
+
session._groupMember = false
|
|
706
|
+
}
|
|
707
|
+
contactStore.baseOnMsgList.set(normalizeSessionId(sessionId), session)
|
|
708
|
+
updateBaseOnMsgList()
|
|
709
|
+
}
|
|
699
710
|
}
|
|
700
711
|
// 预发送保留 Base64 用于本地预览;真正发送前先写入后端临时目录
|
|
701
712
|
try {
|
|
@@ -762,7 +773,7 @@ export async function sendMsgRaw(
|
|
|
762
773
|
{
|
|
763
774
|
user_id: id.split('/')[0],
|
|
764
775
|
group_id: id.split('/')[1],
|
|
765
|
-
channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ??
|
|
776
|
+
channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ?? String(id).split('/')[0]),
|
|
766
777
|
message: msg,
|
|
767
778
|
},
|
|
768
779
|
echo + '_uuid_' + msgUUID,
|
|
@@ -773,7 +784,7 @@ export async function sendMsgRaw(
|
|
|
773
784
|
'send_msg',
|
|
774
785
|
{
|
|
775
786
|
user_id: id,
|
|
776
|
-
channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ??
|
|
787
|
+
channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ?? String(id)),
|
|
777
788
|
message: msg,
|
|
778
789
|
},
|
|
779
790
|
echo + '_uuid_' + msgUUID,
|
|
@@ -827,7 +838,7 @@ function normalizeBaseSessionMap() {
|
|
|
827
838
|
const seen = new Map<string, UserFriendElem & UserGroupElem>()
|
|
828
839
|
for (const [rawKey, item] of contactStore.baseOnMsgList) {
|
|
829
840
|
const id = getSessionDedupKey(item)
|
|
830
|
-
if (id
|
|
841
|
+
if (id === '0' || id === '') continue
|
|
831
842
|
const existing = seen.get(id)
|
|
832
843
|
if (existing && existing !== item) {
|
|
833
844
|
needNormalize = true
|
|
@@ -846,7 +857,7 @@ function normalizeBaseSessionMap() {
|
|
|
846
857
|
const canonical = new Map<string, UserFriendElem & UserGroupElem>()
|
|
847
858
|
contactStore.baseOnMsgList.forEach((item) => {
|
|
848
859
|
const id = getSessionDedupKey(item)
|
|
849
|
-
if (id === '
|
|
860
|
+
if (id === '0' || id === '') return
|
|
850
861
|
const existing = canonical.get(id)
|
|
851
862
|
if (existing && existing !== item) {
|
|
852
863
|
canonical.set(id, mergeSessionState(existing, item))
|
|
@@ -869,7 +880,7 @@ function getSessionList() {
|
|
|
869
880
|
|
|
870
881
|
const addSession = (item: UserFriendElem & UserGroupElem) => {
|
|
871
882
|
const id = getSessionDedupKey(item)
|
|
872
|
-
if (id === '
|
|
883
|
+
if (id === '0' || id === '') return
|
|
873
884
|
const existing = sessionMap.get(id)
|
|
874
885
|
if (existing && existing !== item) {
|
|
875
886
|
sessionMap.set(id, mergeSessionState(existing, item))
|
|
@@ -880,7 +891,7 @@ function getSessionList() {
|
|
|
880
891
|
|
|
881
892
|
if (settingsStore.sysConfig.session_display_mode === 'all') {
|
|
882
893
|
contactStore.userList.forEach((item) => {
|
|
883
|
-
addSession(item)
|
|
894
|
+
if (!item._groupMember) addSession(item)
|
|
884
895
|
})
|
|
885
896
|
}
|
|
886
897
|
|
|
@@ -894,11 +905,11 @@ function getSessionList() {
|
|
|
894
905
|
/**
|
|
895
906
|
* 刷新消息列表排序
|
|
896
907
|
*/
|
|
897
|
-
export function updateBaseOnMsgList() {
|
|
898
|
-
const contactStore = useContactStore()
|
|
899
|
-
const settingsStore = useSettingsStore()
|
|
900
|
-
normalizeBaseSessionMap()
|
|
901
|
-
const allList = getSessionList()
|
|
908
|
+
export function updateBaseOnMsgList() {
|
|
909
|
+
const contactStore = useContactStore()
|
|
910
|
+
const settingsStore = useSettingsStore()
|
|
911
|
+
normalizeBaseSessionMap()
|
|
912
|
+
const allList = getSessionList()
|
|
902
913
|
// 先更具 item.always_top 是不是 true 拆为两个数组
|
|
903
914
|
const topList = allList.filter((item) => item.always_top)
|
|
904
915
|
const normalList = allList.filter((item) => !item.always_top)
|