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.
Files changed (35) hide show
  1. package/client/web/dist/assets/Chat-Bo5JdcNg.js +28 -0
  2. package/client/web/dist/assets/{Chat-CTdyeaX2.css → Chat-C-rmzMIw.css} +1 -1
  3. package/client/web/dist/assets/{MsgBody-DDacjWJ9.js → MsgBody-h4C1nnBQ.js} +1 -1
  4. package/client/web/dist/assets/{MsgBody.vue_vue_type_script_setup_true_lang-B1dWyCB0.js → MsgBody.vue_vue_type_script_setup_true_lang-a902ps2r.js} +2 -2
  5. package/client/web/dist/assets/{index-BlLut5xn.js → index-CdTmWcO4.js} +46 -46
  6. package/client/web/dist/assets/{index-fW_zcub3.css → index-wjp_A4n1.css} +1 -1
  7. package/client/web/dist/index.html +2 -2
  8. package/client/web/src/components/AboutPan.vue +2 -1
  9. package/client/web/src/components/BulletinBody.vue +1 -1
  10. package/client/web/src/components/FriendBody.vue +1 -0
  11. package/client/web/src/components/MsgBody.vue +1 -0
  12. package/client/web/src/components/TinySessionBody.vue +1 -0
  13. package/client/web/src/components/UpdatePan.vue +2 -2
  14. package/client/web/src/components/tooltip/UserInfoTooltip.vue +1 -1
  15. package/client/web/src/function/connect.ts +47 -14
  16. package/client/web/src/function/satori-model.ts +8 -0
  17. package/client/web/src/function/satori.ts +31 -1
  18. package/client/web/src/function/utils/avatarUtil.ts +18 -3
  19. package/client/web/src/main.ts +5 -0
  20. package/client/web/src/pages/Chat.vue +3 -4
  21. package/client/web/src/pages/Friends.vue +23 -17
  22. package/client/web/src/pages/Info.vue +3 -3
  23. package/client/web/src/pages/Messages.vue +20 -14
  24. package/client/web/src/pages/Qzone.vue +2 -2
  25. package/client/web/src/pages/chat-view/SystemNotice.vue +3 -3
  26. package/client/web/src/pages/options/OptAccount.vue +6 -4
  27. package/lib/index.js +14 -14
  28. package/lib/types.d.ts +115 -0
  29. package/package.json +1 -1
  30. package/src/bootstrap.ts +1 -0
  31. package/src/config.ts +1 -1
  32. package/src/recorder.ts +7 -7
  33. package/src/types.ts +4 -0
  34. package/src/web.ts +8 -9
  35. 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-BlLut5xn.js"></script>
15
- <link rel="stylesheet" crossorigin href="./assets/index-fW_zcub3.css">
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"
@@ -17,6 +17,7 @@
17
17
  <div />
18
18
  <img loading="lazy"
19
19
  :title="getShowName(session.group_name || session.nickname, session.remark)"
20
+ data-avatar
20
21
  :src="session.avatar || '/img/icons/icon.svg'">
21
22
  <div>
22
23
  <p>
@@ -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
  {{
@@ -26,7 +26,7 @@
26
26
  <!-- 群成员 -->
27
27
  <template v-else>
28
28
  <div>
29
- <img :src="userInfo.avatar || '/img/icons/icon.svg'">
29
+ <img data-avatar :src="userInfo.avatar || '/img/icons/icon.svg'">
30
30
  <div>
31
31
  <span name="id">{{ userInfo.user_id }}</span>
32
32
  <div>
@@ -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
- for (const item of logins) {
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 = logins[0]
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 = logins
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: logins,
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(/&lt;faceType\b[^>]*&gt;/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 as HTMLImageElement
29
- const fallback = new URL('/img/icons/icon.svg', location.origin).href
30
- if (img.src !== fallback) img.src = fallback
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
  }
@@ -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 v-show="isDev" @click="consoleLogMsg">
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 activeBotId = ref(getActiveBot()?.selfId ?? loginInfo.selectedSatoriBot ?? '')
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 [selfId, state] of contactStore.botStates) {
263
- const bot = satoriLogins.value.find((item) => item.selfId === 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
- if (bot.selfId === activeBotId.value) return
292
+ const key = botKey(bot.platform, bot.selfId)
293
+ if (key === activeBotId.value) return
288
294
  snapshotCurrentBot()
289
- activeBotId.value = bot.selfId
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: bot.selfId,
308
+ selectedSatoriBot: key,
303
309
  }
304
- const saved = contactStore.botStates.get(bot.selfId)
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>