koishi-plugin-chat-patch 5.3.0 → 5.3.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-Bo5JdcNg.js +28 -0
- package/client/web/dist/assets/{Chat-CTdyeaX2.css → Chat-C-rmzMIw.css} +1 -1
- package/client/web/dist/assets/{MsgBody-DDacjWJ9.js → MsgBody-h4C1nnBQ.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-a902ps2r.js} +2 -2
- package/client/web/dist/assets/{index-BlLut5xn.js → index-CdTmWcO4.js} +46 -46
- package/client/web/dist/assets/{index-fW_zcub3.css → index-wjp_A4n1.css} +1 -1
- package/client/web/dist/index.html +2 -2
- package/client/web/src/components/AboutPan.vue +2 -1
- package/client/web/src/components/BulletinBody.vue +1 -1
- package/client/web/src/components/FriendBody.vue +1 -0
- package/client/web/src/components/MsgBody.vue +1 -0
- package/client/web/src/components/TinySessionBody.vue +1 -0
- package/client/web/src/components/UpdatePan.vue +2 -2
- package/client/web/src/components/tooltip/UserInfoTooltip.vue +1 -1
- package/client/web/src/function/connect.ts +47 -14
- package/client/web/src/function/satori-model.ts +8 -0
- package/client/web/src/function/satori.ts +31 -1
- package/client/web/src/function/utils/avatarUtil.ts +18 -3
- package/client/web/src/main.ts +5 -0
- package/client/web/src/pages/Chat.vue +3 -4
- package/client/web/src/pages/Friends.vue +23 -17
- package/client/web/src/pages/Info.vue +3 -3
- package/client/web/src/pages/Messages.vue +20 -14
- package/client/web/src/pages/Qzone.vue +2 -2
- package/client/web/src/pages/chat-view/SystemNotice.vue +3 -3
- package/client/web/src/pages/options/OptAccount.vue +6 -4
- package/lib/index.js +14 -14
- package/lib/types.d.ts +115 -0
- package/package.json +1 -1
- package/src/bootstrap.ts +1 -0
- package/src/config.ts +1 -1
- package/src/recorder.ts +7 -7
- package/src/types.ts +4 -0
- package/src/web.ts +8 -9
- package/client/web/dist/assets/Chat-B448YOeN.js +0 -28
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<link rel="stylesheet" href="./bcui/css/style.css">
|
|
12
12
|
<link rel="stylesheet" href="./bcui/css/color-light.css">
|
|
13
13
|
<link rel="stylesheet" href="./css/append-light.css">
|
|
14
|
-
<script type="module" crossorigin src="./assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
14
|
+
<script type="module" crossorigin src="./assets/index-CdTmWcO4.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="./assets/index-wjp_A4n1.css">
|
|
16
16
|
</head>
|
|
17
17
|
|
|
18
18
|
<body>
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
<span> {{ $t('赞助者') }} </span>
|
|
23
23
|
<div class="contributors">
|
|
24
24
|
<div v-for="info in sponsorList.slice(0, 3)" :key="info.user.name">
|
|
25
|
-
<img lazy :src="info.user.avatar">
|
|
25
|
+
<img lazy data-avatar :src="info.user.avatar">
|
|
26
26
|
<div>
|
|
27
27
|
<span>{{ info.user.name }}</span>
|
|
28
28
|
<span>{{ Intl.DateTimeFormat(trueLang, {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
<img v-for="info in sponsorList.slice(3)"
|
|
38
38
|
:key="info.user.name"
|
|
39
39
|
lazy
|
|
40
|
+
data-avatar
|
|
40
41
|
:src="info.user.avatar">
|
|
41
42
|
</div>
|
|
42
43
|
</div>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
<span v-show="needShow && !showAll">{{ $t('点击展开') }}</span>
|
|
37
37
|
<div class="info">
|
|
38
|
-
<img :src="data.avatar || '/img/icons/icon.svg'">
|
|
38
|
+
<img data-avatar :src="data.avatar || '/img/icons/icon.svg'">
|
|
39
39
|
<a>{{
|
|
40
40
|
chatStore.chatInfo.info.group_members.filter((item) => {
|
|
41
41
|
return Number(item.user_id) === Number(data.sender)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<font-awesome-icon v-if="data.user_id == -10000" :icon="['fas', 'bell']" />
|
|
16
16
|
<font-awesome-icon v-else-if="data.user_id == -10001" :icon="['fas', 'user-group']" />
|
|
17
17
|
<img v-else loading="lazy" :title="getShowName(data.group_name || data.nickname, data.remark)"
|
|
18
|
+
data-avatar
|
|
18
19
|
:src="data.avatar || '/img/icons/icon.svg'" @error="avatarError">
|
|
19
20
|
<div>
|
|
20
21
|
<div>
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
<template v-if="type != 'body'">
|
|
32
32
|
<img v-menu.prevent="event => $emit('showMenu', event, data)"
|
|
33
33
|
v-user-tooltip="() => getUserById(data.sender.user_id)"
|
|
34
|
+
data-avatar
|
|
34
35
|
name="avatar"
|
|
35
36
|
:src="data.sender.avatar || '/img/icons/icon.svg'"
|
|
36
37
|
:alt="data.sender.card ? data.sender.card : data.sender.nickname"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<span>{{ updated ? $t('更新记录') : $t('新版本') }}</span>
|
|
5
5
|
<a>{{ version }}</a>
|
|
6
6
|
<div class="title">
|
|
7
|
-
<img :src="user?.avatar || ''">
|
|
7
|
+
<img data-avatar :src="user?.avatar || ''">
|
|
8
8
|
<a :href="user?.url || ''">{{ user?.name || '' }}</a>
|
|
9
9
|
<span>
|
|
10
10
|
{{
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<div class="release-header">
|
|
38
38
|
<a class="release-version">{{ release.version }} - {{ release.info.title.replace('Release', '') }}</a>
|
|
39
39
|
<div class="release-meta">
|
|
40
|
-
<img :src="release.user.avatar">
|
|
40
|
+
<img data-avatar :src="release.user.avatar">
|
|
41
41
|
<a :href="release.user.url" target="_blank">{{ release.user.name }}</a>
|
|
42
42
|
<span>
|
|
43
43
|
{{
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import { reactive } from 'vue'
|
|
4
4
|
import { dispatch, msgPreprocess } from './msg'
|
|
5
5
|
import {
|
|
6
|
+
botKey,
|
|
6
7
|
connect as connectSatori,
|
|
7
8
|
getActiveBot,
|
|
9
|
+
getBlockedPlatforms,
|
|
8
10
|
getBootstrap,
|
|
9
11
|
getLogins,
|
|
10
12
|
request,
|
|
@@ -42,6 +44,14 @@ function encodeKeyPart(value: string): string {
|
|
|
42
44
|
return encodeURIComponent(value)
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
function isBlockedPlatform(platform: string): boolean {
|
|
48
|
+
return getBlockedPlatforms().some((item) => {
|
|
49
|
+
return item.exactMatch
|
|
50
|
+
? platform === item.platformName
|
|
51
|
+
: platform.includes(item.platformName)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
function getChannelKind(value: unknown): 'group' | 'direct' | 'unknown' {
|
|
46
56
|
if (typeof value === 'number') {
|
|
47
57
|
if (value === 0) return 'group'
|
|
@@ -350,7 +360,7 @@ function updateBotStateGroup(
|
|
|
350
360
|
apply: (target: Record<string, unknown>) => void,
|
|
351
361
|
) {
|
|
352
362
|
const contactStore = useContactStore()
|
|
353
|
-
const state = contactStore.botStates.get(selfId)
|
|
363
|
+
const state = contactStore.botStates.get(botKey(platform, selfId))
|
|
354
364
|
const session = state?.onMsgList.find((item) => String(item.group_id) === sessionId)
|
|
355
365
|
if (!state || !session) return
|
|
356
366
|
apply(session as unknown as Record<string, unknown>)
|
|
@@ -363,7 +373,7 @@ function updateBotStateGroup(
|
|
|
363
373
|
} else {
|
|
364
374
|
baseList.push([key, typedSession])
|
|
365
375
|
}
|
|
366
|
-
contactStore.botStates.set(selfId, {
|
|
376
|
+
contactStore.botStates.set(botKey(platform, selfId), {
|
|
367
377
|
userList: [...state.userList],
|
|
368
378
|
baseList,
|
|
369
379
|
onMsgList: [...state.onMsgList],
|
|
@@ -578,7 +588,7 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
|
|
|
578
588
|
? (eventGroupAvatar || cachedGroupInfo?.avatar || buildQqGroupAvatar(platform, sessionId) || undefined)
|
|
579
589
|
: undefined
|
|
580
590
|
const contactStore = useContactStore()
|
|
581
|
-
const state = contactStore.botStates.get(selfId) ?? {
|
|
591
|
+
const state = contactStore.botStates.get(botKey(platform, selfId)) ?? {
|
|
582
592
|
userList: [],
|
|
583
593
|
baseList: [],
|
|
584
594
|
onMsgList: [],
|
|
@@ -650,7 +660,7 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
|
|
|
650
660
|
} else {
|
|
651
661
|
baseList.push([baseKey, typedSession])
|
|
652
662
|
}
|
|
653
|
-
contactStore.botStates.set(selfId, {
|
|
663
|
+
contactStore.botStates.set(botKey(platform, selfId), {
|
|
654
664
|
userList: [...state.userList],
|
|
655
665
|
baseList,
|
|
656
666
|
onMsgList: [...state.onMsgList],
|
|
@@ -726,6 +736,26 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
|
|
|
726
736
|
)
|
|
727
737
|
if (senderId && senderId !== '0') {
|
|
728
738
|
void fetchUserIdentity(platform, selfId, senderId, null, msg)
|
|
739
|
+
if (isCurrentChat) {
|
|
740
|
+
const members = chatStore.chatInfo.info.group_members
|
|
741
|
+
const member = {
|
|
742
|
+
user_id: senderId,
|
|
743
|
+
nickname: String(sender.nickname || sender.card || senderName || senderId),
|
|
744
|
+
card: String(sender.card || ''),
|
|
745
|
+
role: String(sender.role || ''),
|
|
746
|
+
avatar: hasUsableAvatar(sender.avatar) ? getString(sender.avatar) : undefined,
|
|
747
|
+
title: '',
|
|
748
|
+
join_time: 0,
|
|
749
|
+
last_sent_time: Math.floor(sessionTime),
|
|
750
|
+
level: 0,
|
|
751
|
+
rank: '',
|
|
752
|
+
sex: '',
|
|
753
|
+
shutup_time: 0,
|
|
754
|
+
}
|
|
755
|
+
const index = members.findIndex((item) => String(item.user_id) === senderId)
|
|
756
|
+
if (index >= 0) Object.assign(members[index], member)
|
|
757
|
+
else members.push(member)
|
|
758
|
+
}
|
|
729
759
|
}
|
|
730
760
|
} else if (senderId && senderId !== '0') {
|
|
731
761
|
identityDebug('recordBotMessage: private identity branch', {
|
|
@@ -780,7 +810,7 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
|
|
|
780
810
|
}
|
|
781
811
|
if (!sessions.length) return
|
|
782
812
|
|
|
783
|
-
const state = contactStore.botStates.get(selfId) ?? {
|
|
813
|
+
const state = contactStore.botStates.get(botKey(platform, selfId)) ?? {
|
|
784
814
|
userList: [],
|
|
785
815
|
baseList: [],
|
|
786
816
|
onMsgList: [],
|
|
@@ -809,7 +839,7 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
|
|
|
809
839
|
baseKeys.add(id)
|
|
810
840
|
}
|
|
811
841
|
}
|
|
812
|
-
contactStore.botStates.set(selfId, {
|
|
842
|
+
contactStore.botStates.set(botKey(platform, selfId), {
|
|
813
843
|
userList: [...state.userList],
|
|
814
844
|
baseList,
|
|
815
845
|
onMsgList: [...state.onMsgList],
|
|
@@ -817,6 +847,7 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
|
|
|
817
847
|
}
|
|
818
848
|
|
|
819
849
|
function onSatoriEvent(event: SatoriEvent) {
|
|
850
|
+
if (isBlockedPlatform(event.platform)) return
|
|
820
851
|
const oneBot = satoriEventToOneBot(event.body, String(event.platform || ''))
|
|
821
852
|
if (oneBot) {
|
|
822
853
|
oneBot._rawSatori = {
|
|
@@ -865,6 +896,7 @@ export function flushPendingBotEvents(platform: string, selfId: string) {
|
|
|
865
896
|
if (!events?.length) return
|
|
866
897
|
pendingBotEvents.delete(key)
|
|
867
898
|
for (const event of events) {
|
|
899
|
+
if (isBlockedPlatform(event.platform)) continue
|
|
868
900
|
const oneBot = satoriEventToOneBot(event.body, String(event.platform || ''))
|
|
869
901
|
if (oneBot) {
|
|
870
902
|
oneBot._rawSatori = {
|
|
@@ -893,7 +925,8 @@ function onSatoriReady(logins: Array<{ platform: string; selfId: string; name: s
|
|
|
893
925
|
login.creating = false
|
|
894
926
|
login.status = true
|
|
895
927
|
const authStore = useAuthStore()
|
|
896
|
-
|
|
928
|
+
const visibleLogins = logins.filter((item) => !isBlockedPlatform(item.platform))
|
|
929
|
+
for (const item of visibleLogins) {
|
|
897
930
|
if (!item.platform || !item.selfId) continue
|
|
898
931
|
identityInfoCache.set(identityCacheKey('user', item.platform, item.selfId, item.selfId), {
|
|
899
932
|
name: item.name || item.selfId,
|
|
@@ -907,7 +940,7 @@ function onSatoriReady(logins: Array<{ platform: string; selfId: string; name: s
|
|
|
907
940
|
},
|
|
908
941
|
})
|
|
909
942
|
}
|
|
910
|
-
const first =
|
|
943
|
+
const first = visibleLogins[0]
|
|
911
944
|
if (!first) return
|
|
912
945
|
|
|
913
946
|
setActiveBot(first.platform, first.selfId)
|
|
@@ -915,16 +948,16 @@ function onSatoriReady(logins: Array<{ platform: string; selfId: string; name: s
|
|
|
915
948
|
login.uin = first.selfId
|
|
916
949
|
login.nickname = first.name
|
|
917
950
|
login.platform = first.platform
|
|
918
|
-
login.satoriLogins =
|
|
919
|
-
login.selectedSatoriBot = first.selfId
|
|
951
|
+
login.satoriLogins = visibleLogins
|
|
952
|
+
login.selectedSatoriBot = botKey(first.platform, first.selfId)
|
|
920
953
|
authStore.loginInfo = {
|
|
921
954
|
uin: first.selfId,
|
|
922
955
|
user_id: first.selfId,
|
|
923
956
|
nickname: first.name,
|
|
924
957
|
platform: first.platform,
|
|
925
958
|
avatar: first.avatar,
|
|
926
|
-
satoriLogins:
|
|
927
|
-
selectedSatoriBot: first.selfId,
|
|
959
|
+
satoriLogins: visibleLogins,
|
|
960
|
+
selectedSatoriBot: botKey(first.platform, first.selfId),
|
|
928
961
|
}
|
|
929
962
|
authStore.botInfo = {
|
|
930
963
|
app_name: 'Satori',
|
|
@@ -1406,9 +1439,9 @@ async function loadAllBotsCache() {
|
|
|
1406
1439
|
}
|
|
1407
1440
|
const userList = [...groupRaws, ...friendRaws] as unknown as (UserFriendElem & UserGroupElem)[]
|
|
1408
1441
|
|
|
1409
|
-
const state = contactStore.botStates.get(selfId)
|
|
1442
|
+
const state = contactStore.botStates.get(botKey(platform, selfId))
|
|
1410
1443
|
if (!state || state.userList.length === 0) {
|
|
1411
|
-
contactStore.botStates.set(selfId, {
|
|
1444
|
+
contactStore.botStates.set(botKey(platform, selfId), {
|
|
1412
1445
|
userList,
|
|
1413
1446
|
baseList: state?.baseList ?? [],
|
|
1414
1447
|
onMsgList: state?.onMsgList ?? [],
|
|
@@ -234,6 +234,13 @@ function containsElementMarkup(source: string): boolean {
|
|
|
234
234
|
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
235
|
}
|
|
236
236
|
|
|
237
|
+
// QQ 官方机器人会把部分大表情/图片消息的私有标记塞进 content,渲染前需要去掉
|
|
238
|
+
function cleanQqFaceMarkup(source: string): string {
|
|
239
|
+
return source
|
|
240
|
+
.replace(/<faceType\b[^>]*>/gi, '')
|
|
241
|
+
.replace(/<faceType\b[^>]*>/gi, '')
|
|
242
|
+
}
|
|
243
|
+
|
|
237
244
|
function splitMarkupTextSegments(
|
|
238
245
|
segments: Array<Record<string, unknown>>,
|
|
239
246
|
): Array<Record<string, unknown>> {
|
|
@@ -250,6 +257,7 @@ function splitMarkupTextSegments(
|
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
export function parseSatoriMarkup(source: string): Array<Record<string, unknown>> {
|
|
260
|
+
source = cleanQqFaceMarkup(source)
|
|
253
261
|
const result: Array<Record<string, unknown>> = []
|
|
254
262
|
let index = 0
|
|
255
263
|
while (index < source.length) {
|
|
@@ -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) {
|
|
@@ -94,6 +114,9 @@ function receiveBootstrap(data: Record<string, unknown>) {
|
|
|
94
114
|
endpoint: getString(payload.endpoint) || '/satori',
|
|
95
115
|
token: getString(payload.token),
|
|
96
116
|
basePath: getString(payload.basePath) || '/chat-patch',
|
|
117
|
+
blockedPlatforms: Array.isArray(payload.blockedPlatforms)
|
|
118
|
+
? payload.blockedPlatforms as SatoriBootstrap['blockedPlatforms']
|
|
119
|
+
: [],
|
|
97
120
|
}
|
|
98
121
|
window.clearTimeout(bootstrapTimeout)
|
|
99
122
|
resolveBootstrapCallback?.(bootstrap)
|
|
@@ -118,6 +141,10 @@ export function getBootstrap(): Promise<SatoriBootstrap> {
|
|
|
118
141
|
return resolveBootstrap()
|
|
119
142
|
}
|
|
120
143
|
|
|
144
|
+
export function getBlockedPlatforms(): SatoriBootstrap['blockedPlatforms'] {
|
|
145
|
+
return bootstrap?.blockedPlatforms ?? []
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
export function getLogins(): SatoriLogin[] {
|
|
122
149
|
return logins
|
|
123
150
|
}
|
|
@@ -219,6 +246,9 @@ export function connect(
|
|
|
219
246
|
endpoint: getString(payload.endpoint) || '/satori',
|
|
220
247
|
token: getString(payload.token),
|
|
221
248
|
basePath: getString(payload.basePath) || '/chat-patch',
|
|
249
|
+
blockedPlatforms: Array.isArray(payload.blockedPlatforms)
|
|
250
|
+
? payload.blockedPlatforms as SatoriBootstrap['blockedPlatforms']
|
|
251
|
+
: [],
|
|
222
252
|
}
|
|
223
253
|
}
|
|
224
254
|
}
|
|
@@ -258,7 +288,7 @@ export function connect(
|
|
|
258
288
|
const platform = getString(login.platform) || getString(user.platform)
|
|
259
289
|
const selfId = getString(user.id) || getString(login.selfId)
|
|
260
290
|
const key = [platform, selfId].map((value) => encodeURIComponent(value)).join(':')
|
|
261
|
-
if (!platform || !selfId || seen.has(key)) continue
|
|
291
|
+
if (!platform || !selfId || seen.has(key) || isBlockedPlatform(platform, bootstrap?.blockedPlatforms ?? [])) continue
|
|
262
292
|
seen.add(key)
|
|
263
293
|
nextLogins.push({
|
|
264
294
|
platform: getString(login.platform) || getString(user.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
|
}
|
package/client/web/src/main.ts
CHANGED
|
@@ -21,10 +21,15 @@ import './assets/css/sys_notice.css'
|
|
|
21
21
|
|
|
22
22
|
import { getPortableFileLang } from './function/utils/systemUtil'
|
|
23
23
|
import { preloadPinyin } from './function/utils/pinyin'
|
|
24
|
+
import { avatarError, isAvatarImage } from './function/utils/avatarUtil'
|
|
24
25
|
|
|
25
26
|
// 媒体资源由 <img>/<audio> 等元素加载时,失败事件只在前端控制台输出
|
|
26
27
|
window.addEventListener('error', (event) => {
|
|
27
28
|
const target = event.target
|
|
29
|
+
if (target instanceof HTMLImageElement && isAvatarImage(target)) {
|
|
30
|
+
avatarError(event)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
28
33
|
if (!(target instanceof HTMLImageElement) && !(target instanceof HTMLMediaElement)) return
|
|
29
34
|
const source = target.currentSrc || target.src
|
|
30
35
|
if (source.includes('/chat-patch/web/media') || source.includes('/chat-patch/api/media')) {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<!-- 聊天基本信息 -->
|
|
26
26
|
<div class="info">
|
|
27
27
|
<font-awesome-icon class="back" :icon="['fas', 'angle-left']" @click="exitWin" />
|
|
28
|
-
<img :src="chat.show.avatar || '/img/icons/icon.svg'" @error="avatarError">
|
|
28
|
+
<img data-avatar :src="chat.show.avatar || '/img/icons/icon.svg'" @error="avatarError">
|
|
29
29
|
<div class="info">
|
|
30
30
|
<p>
|
|
31
31
|
{{ chat.show.name }}
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
<div v-for="(item, index) in chat.info.jin_info.list"
|
|
182
182
|
:key="'jin-' + index">
|
|
183
183
|
<div>
|
|
184
|
-
<img :src="item.sender_avatar || '/img/icons/icon.svg'">
|
|
184
|
+
<img data-avatar :src="item.sender_avatar || '/img/icons/icon.svg'">
|
|
185
185
|
<div>
|
|
186
186
|
<a>{{ item.sender_nick }}</a>
|
|
187
187
|
<span>{{ item.sender_time ? Intl.DateTimeFormat(
|
|
@@ -517,7 +517,7 @@
|
|
|
517
517
|
<div><font-awesome-icon :icon="['fas', 'arrow-up-right-from-square']" /></div>
|
|
518
518
|
<a>{{ $t('跳转到消息') }}</a>
|
|
519
519
|
</div>
|
|
520
|
-
<div
|
|
520
|
+
<div @click="consoleLogMsg">
|
|
521
521
|
<div><font-awesome-icon :icon="['fas', 'screwdriver-wrench']" /></div>
|
|
522
522
|
<a>{{ $t('调试信息') }}</a>
|
|
523
523
|
</div>
|
|
@@ -738,7 +738,6 @@ const searchRequestId = ref(0)
|
|
|
738
738
|
const forwardList = ref(contactStore.userList)
|
|
739
739
|
const chatImg = ref<any>(undefined)
|
|
740
740
|
const trueLang = getTrueLang()
|
|
741
|
-
const isDev = import.meta.env.DEV
|
|
742
741
|
let selfRefreshTimer: number | null = null
|
|
743
742
|
|
|
744
743
|
function ensureChannelPrefix(channelId: string, prefix: string): string {
|
|
@@ -56,17 +56,17 @@
|
|
|
56
56
|
@click="userClick(item, $event)" />
|
|
57
57
|
</div>
|
|
58
58
|
<template v-else>
|
|
59
|
-
<div v-for="bot in satoriLogins" :key="'bot-' + bot.selfId"
|
|
60
|
-
:class="['robot-accordion', { expanded: bot.selfId === activeBotId }]">
|
|
59
|
+
<div v-for="bot in satoriLogins" :key="'bot-' + botKey(bot.platform, bot.selfId)"
|
|
60
|
+
:class="['robot-accordion', { expanded: botKey(bot.platform, bot.selfId) === activeBotId }]">
|
|
61
61
|
<div class="robot-accordion-header" @click="toggleRobot(bot)">
|
|
62
|
-
<img :src="bot.avatar || '/img/icons/icon.svg'" :alt="bot.name" @error="avatarError">
|
|
62
|
+
<img data-avatar :src="bot.avatar || '/img/icons/icon.svg'" :alt="bot.name" @error="avatarError">
|
|
63
63
|
<div>
|
|
64
64
|
<span :title="bot.name">{{ bot.name }}</span>
|
|
65
65
|
<small>{{ bot.platform }}</small>
|
|
66
66
|
</div>
|
|
67
|
-
<font-awesome-icon :icon="['fas', bot.selfId === activeBotId ? 'angle-up' : 'angle-down']" />
|
|
67
|
+
<font-awesome-icon :icon="['fas', botKey(bot.platform, bot.selfId) === activeBotId ? 'angle-up' : 'angle-down']" />
|
|
68
68
|
</div>
|
|
69
|
-
<div v-if="bot.selfId === activeBotId" class="robot-accordion-content">
|
|
69
|
+
<div v-if="botKey(bot.platform, bot.selfId) === activeBotId" class="robot-accordion-content">
|
|
70
70
|
<div v-if="!contactStore.friendLoading && contactStore.userList.length === 0"
|
|
71
71
|
class="empty-state">
|
|
72
72
|
{{ $t('空') }}
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
|
|
184
184
|
import { reloadUsers } from '../function/utils/appUtil'
|
|
185
185
|
import { Connector, flushPendingBotEvents, loadContactsFromCache, login as loginInfo } from '../function/connect'
|
|
186
|
-
import { getActiveBot, getLogins, setActiveBot } from '../function/satori'
|
|
186
|
+
import { botKey, getActiveBot, getLogins, setActiveBot } from '../function/satori'
|
|
187
187
|
import { normalizeGroupId, normalizeSessionId } from '../function/utils/sessionUtil'
|
|
188
188
|
import { avatarError } from '../function/utils/avatarUtil'
|
|
189
189
|
import { backend } from '../runtime/backend'
|
|
@@ -220,7 +220,12 @@
|
|
|
220
220
|
const searchInfo = ref('')
|
|
221
221
|
const classStatus = ref<{ [key: string]: boolean }>({})
|
|
222
222
|
const satoriLogins = ref<Array<{ platform: string; selfId: string; name: string; avatar?: string; status?: number; features?: string[] }>>(getLogins())
|
|
223
|
-
const
|
|
223
|
+
const active = getActiveBot()
|
|
224
|
+
const activeBotId = ref(
|
|
225
|
+
active?.platform && active.selfId
|
|
226
|
+
? botKey(active.platform, active.selfId)
|
|
227
|
+
: loginInfo.selectedSatoriBot ?? '',
|
|
228
|
+
)
|
|
224
229
|
|
|
225
230
|
watch(() => loginInfo.satoriLogins, (list) => {
|
|
226
231
|
satoriLogins.value = list ?? []
|
|
@@ -230,7 +235,7 @@
|
|
|
230
235
|
}, { immediate: true })
|
|
231
236
|
|
|
232
237
|
const currentBotName = computed(() => {
|
|
233
|
-
return satoriLogins.value.find((item) => item.selfId === activeBotId.value)?.name ?? ''
|
|
238
|
+
return satoriLogins.value.find((item) => botKey(item.platform, item.selfId) === activeBotId.value)?.name ?? ''
|
|
234
239
|
})
|
|
235
240
|
|
|
236
241
|
const allContacts = computed<ContactWithBot[]>(() => {
|
|
@@ -257,10 +262,10 @@
|
|
|
257
262
|
map.set(key, { ...item, _bot: bot })
|
|
258
263
|
}
|
|
259
264
|
}
|
|
260
|
-
const currentBot = satoriLogins.value.find((item) => item.selfId === activeBotId.value)
|
|
265
|
+
const currentBot = satoriLogins.value.find((item) => botKey(item.platform, item.selfId) === activeBotId.value)
|
|
261
266
|
addContacts(contactStore.userList, currentBot)
|
|
262
|
-
for (const [
|
|
263
|
-
const bot = satoriLogins.value.find((item) => item.selfId ===
|
|
267
|
+
for (const [stateKey, state] of contactStore.botStates) {
|
|
268
|
+
const bot = satoriLogins.value.find((item) => botKey(item.platform, item.selfId) === stateKey)
|
|
264
269
|
addContacts(state.userList, bot)
|
|
265
270
|
}
|
|
266
271
|
return [...map.values()]
|
|
@@ -284,9 +289,10 @@
|
|
|
284
289
|
}
|
|
285
290
|
|
|
286
291
|
function selectRobot(bot: { platform: string; selfId: string; name: string; avatar?: string; features?: string[] }) {
|
|
287
|
-
|
|
292
|
+
const key = botKey(bot.platform, bot.selfId)
|
|
293
|
+
if (key === activeBotId.value) return
|
|
288
294
|
snapshotCurrentBot()
|
|
289
|
-
activeBotId.value =
|
|
295
|
+
activeBotId.value = key
|
|
290
296
|
setActiveBot(bot.platform, bot.selfId)
|
|
291
297
|
loginInfo.uin = bot.selfId
|
|
292
298
|
loginInfo.nickname = bot.name
|
|
@@ -299,9 +305,9 @@
|
|
|
299
305
|
platform: bot.platform,
|
|
300
306
|
avatar: bot.avatar,
|
|
301
307
|
satoriLogins: satoriLogins.value,
|
|
302
|
-
selectedSatoriBot:
|
|
308
|
+
selectedSatoriBot: key,
|
|
303
309
|
}
|
|
304
|
-
const saved = contactStore.botStates.get(
|
|
310
|
+
const saved = contactStore.botStates.get(key)
|
|
305
311
|
if (saved) {
|
|
306
312
|
contactStore.userList = saved.userList
|
|
307
313
|
contactStore.baseOnMsgList.clear()
|
|
@@ -339,7 +345,7 @@
|
|
|
339
345
|
}
|
|
340
346
|
|
|
341
347
|
function toggleRobot(bot: { platform: string; selfId: string; name: string; avatar?: string; features?: string[] }) {
|
|
342
|
-
if (activeBotId.value === bot.selfId) {
|
|
348
|
+
if (activeBotId.value === botKey(bot.platform, bot.selfId)) {
|
|
343
349
|
backToRobots()
|
|
344
350
|
} else {
|
|
345
351
|
selectRobot(bot)
|
|
@@ -395,7 +401,7 @@
|
|
|
395
401
|
}
|
|
396
402
|
|
|
397
403
|
function userClick(data: ContactWithBot, event: Event) {
|
|
398
|
-
if (data._bot && data._bot.selfId !== activeBotId.value) {
|
|
404
|
+
if (data._bot && botKey(data._bot.platform, data._bot.selfId) !== activeBotId.value) {
|
|
399
405
|
selectRobot(data._bot)
|
|
400
406
|
}
|
|
401
407
|
const sender = event.currentTarget as HTMLDivElement
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
</header>
|
|
17
17
|
<div class="chat-info-base user">
|
|
18
18
|
<div>
|
|
19
|
-
<img :src="chat.show.avatar || '/img/icons/icon.svg'" @error="avatarError">
|
|
19
|
+
<img data-avatar :src="chat.show.avatar || '/img/icons/icon.svg'" @error="avatarError">
|
|
20
20
|
<div>
|
|
21
21
|
<a>{{ chat.show.name }}</a>
|
|
22
22
|
<span>{{ chat.show.id }}</span>
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
:item-size="60"
|
|
110
110
|
key-field="user_id">
|
|
111
111
|
<div class="member-item edit">
|
|
112
|
-
<img alt="nk" loading="lazy"
|
|
112
|
+
<img alt="nk" loading="lazy" data-avatar
|
|
113
113
|
:src="memberAvatar(item) || '/img/icons/icon.svg'">
|
|
114
114
|
<div>
|
|
115
115
|
<a @click="startChat(item)">{{
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
</BcTab>
|
|
129
129
|
<div :class="'ss-card user-config' + (Object.keys(showUserConfig).length > 0 ? ' show' : '')">
|
|
130
130
|
<div>
|
|
131
|
-
<img alt="nk" :src="showUserConfig.avatar || '/img/icons/icon.svg'">
|
|
131
|
+
<img alt="nk" data-avatar :src="showUserConfig.avatar || '/img/icons/icon.svg'">
|
|
132
132
|
<div>
|
|
133
133
|
<a>{{ showUserConfig.card != '' ? showUserConfig.card : showUserConfig.nickname }}</a>
|
|
134
134
|
<span>{{ showUserConfig.user_id }}</span>
|