koishi-plugin-chat-patch 5.1.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 (53) 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-DLeOklIb.js → MsgBody-h4C1nnBQ.js} +1 -1
  4. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-a902ps2r.js +67 -0
  5. package/client/web/dist/assets/{index-Cz89G4we.js → index-CdTmWcO4.js} +121 -129
  6. package/client/web/dist/assets/index-wjp_A4n1.css +1 -0
  7. package/client/web/dist/assets/pinyin-Dsq98249.js +157657 -0
  8. package/client/web/dist/index.html +2 -2
  9. package/client/web/dist/notice_local.json +1 -0
  10. package/client/web/package-lock.json +49 -0
  11. package/client/web/package.json +1 -0
  12. package/client/web/public/notice_local.json +1 -0
  13. package/client/web/src/components/AboutPan.vue +2 -1
  14. package/client/web/src/components/BulletinBody.vue +1 -1
  15. package/client/web/src/components/EmojiFace.vue +0 -6
  16. package/client/web/src/components/FacePan.vue +0 -12
  17. package/client/web/src/components/FriendBody.vue +1 -0
  18. package/client/web/src/components/MsgBody.vue +4 -4
  19. package/client/web/src/components/SettingsTab.vue +133 -0
  20. package/client/web/src/components/TinySessionBody.vue +1 -0
  21. package/client/web/src/components/UpdatePan.vue +2 -2
  22. package/client/web/src/components/tooltip/UserInfoTooltip.vue +1 -1
  23. package/client/web/src/function/connect.ts +81 -28
  24. package/client/web/src/function/model/emoji.ts +7 -67
  25. package/client/web/src/function/msg.ts +68 -26
  26. package/client/web/src/function/satori-model.ts +8 -0
  27. package/client/web/src/function/satori.ts +31 -1
  28. package/client/web/src/function/utils/appUtil.ts +1 -4
  29. package/client/web/src/function/utils/avatarUtil.ts +18 -3
  30. package/client/web/src/function/utils/msgUtil.ts +86 -52
  31. package/client/web/src/function/utils/pinyin.ts +25 -65
  32. package/client/web/src/function/utils/sessionUtil.ts +90 -14
  33. package/client/web/src/main.ts +5 -0
  34. package/client/web/src/pages/Chat.vue +3 -4
  35. package/client/web/src/pages/Friends.vue +72 -30
  36. package/client/web/src/pages/Info.vue +3 -3
  37. package/client/web/src/pages/Messages.vue +59 -26
  38. package/client/web/src/pages/Options.vue +4 -4
  39. package/client/web/src/pages/Qzone.vue +2 -2
  40. package/client/web/src/pages/chat-view/SystemNotice.vue +3 -3
  41. package/client/web/src/pages/options/OptAccount.vue +6 -4
  42. package/client/web/src/pages/options/OptDev.vue +1 -14
  43. package/lib/index.js +14 -14
  44. package/lib/types.d.ts +115 -0
  45. package/package.json +1 -1
  46. package/src/bootstrap.ts +1 -0
  47. package/src/config.ts +1 -1
  48. package/src/recorder.ts +7 -7
  49. package/src/types.ts +4 -0
  50. package/src/web.ts +8 -9
  51. package/client/web/dist/assets/Chat-BoNiWGxb.js +0 -28
  52. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CGjy4Jzz.js +0 -67
  53. package/client/web/dist/assets/index-BgOX_tNa.css +0 -1
@@ -5,30 +5,25 @@ export type PinYinData = {
5
5
  short: string[]
6
6
  }
7
7
 
8
- /* eslint-disable no-console */
9
-
10
- const PINYIN_SCRIPT_SRC = 'https://lib.stapxs.cn/modules/pinyin.min.js'
8
+ type PinyinModule = typeof import('pinyin')
11
9
 
10
+ let pinyinModule: PinyinModule | null = null
12
11
  let pinyinLoadPromise: Promise<boolean> | null = null
13
12
 
14
13
  function createEmptyPinyinData(): PinYinData {
15
14
  return {
16
15
  main: [],
17
- short: []
16
+ short: [],
18
17
  }
19
18
  }
20
19
 
21
- function hasPinyinLib() {
22
- return typeof window !== 'undefined' && typeof window.pinyin !== 'undefined'
23
- }
24
-
25
20
  function scheduleIdleTask(task: () => void) {
26
21
  if (typeof window === 'undefined') return
27
22
 
28
23
  const idleWindow = window as Window & {
29
24
  requestIdleCallback?: (
30
25
  callback: IdleRequestCallback,
31
- options?: IdleRequestOptions
26
+ options?: IdleRequestOptions,
32
27
  ) => number
33
28
  }
34
29
 
@@ -41,57 +36,23 @@ function scheduleIdleTask(task: () => void) {
41
36
  }
42
37
 
43
38
  export function isPinyinReady() {
44
- return hasPinyinLib()
39
+ return pinyinModule !== null
45
40
  }
46
41
 
47
42
  export function ensurePinyinLoaded(): Promise<boolean> {
48
- if (hasPinyinLib()) return Promise.resolve(true)
49
- if (typeof window === 'undefined' || typeof document === 'undefined') {
50
- return Promise.resolve(false)
51
- }
52
- if (pinyinLoadPromise !== null) return pinyinLoadPromise
53
-
54
- pinyinLoadPromise = new Promise((resolve) => {
55
- let script = document.querySelector(
56
- 'script[data-ssqq-pinyin-lib="true"]',
57
- ) as HTMLScriptElement | null
58
-
59
- const finish = (success: boolean) => {
60
- if (!success) {
61
- pinyinLoadPromise = null
62
- }
63
- resolve(success)
64
- }
65
-
66
- const handleLoad = () => {
67
- if (script) {
68
- script.dataset.loaded = 'true'
69
- }
70
- finish(hasPinyinLib())
71
- }
72
-
73
- const handleError = () => {
74
- if (isDebugMode()) console.warn('拼音库加载失败')
75
- script?.remove()
76
- finish(false)
77
- }
78
-
79
- if (script?.dataset.loaded === 'true') {
80
- finish(hasPinyinLib())
81
- return
82
- }
83
-
84
- if (!script) {
85
- script = document.createElement('script')
86
- script.src = PINYIN_SCRIPT_SRC
87
- script.async = true
88
- script.dataset.ssqqPinyinLib = 'true'
89
- document.body.appendChild(script)
90
- }
91
-
92
- script.addEventListener('load', handleLoad, { once: true })
93
- script.addEventListener('error', handleError, { once: true })
94
- })
43
+ if (pinyinModule) return Promise.resolve(true)
44
+ if (pinyinLoadPromise) return pinyinLoadPromise
45
+
46
+ pinyinLoadPromise = import('pinyin')
47
+ .then((mod) => {
48
+ pinyinModule = mod
49
+ return true
50
+ })
51
+ .catch((error: unknown) => {
52
+ if (isDebugMode()) console.warn('本地拼音库加载失败:', error)
53
+ pinyinLoadPromise = null
54
+ return false
55
+ })
95
56
 
96
57
  return pinyinLoadPromise
97
58
  }
@@ -103,22 +64,21 @@ export function preloadPinyin() {
103
64
  }
104
65
 
105
66
  export function getPinyin(name: string): PinYinData {
106
- if (!hasPinyinLib()) return createEmptyPinyinData()
67
+ if (!pinyinModule) return createEmptyPinyinData()
107
68
 
108
69
  try {
109
- const pinyinLib = window.pinyin
110
- if (!pinyinLib) return createEmptyPinyinData()
70
+ const pinyinLib = pinyinModule.pinyin
111
71
  return {
112
- main: pinyinLib.pinyin(name, {
72
+ main: pinyinLib(name, {
113
73
  heteronym: true,
114
74
  compact: true,
115
75
  style: 'normal',
116
- }).map((item: string[]) => item.join('').toLowerCase()),
117
- short: pinyinLib.pinyin(name, {
76
+ }).map((item) => item.join('').toLowerCase()),
77
+ short: pinyinLib(name, {
118
78
  heteronym: true,
119
79
  compact: true,
120
80
  style: 'first_letter',
121
- }).map((item: string[]) => item.join('').toLowerCase()),
81
+ }).map((item) => item.join('').toLowerCase()),
122
82
  }
123
83
  } catch (error) {
124
84
  if (isDebugMode()) console.warn('拼音转换失败:', error)
@@ -128,7 +88,7 @@ export function getPinyin(name: string): PinYinData {
128
88
 
129
89
  export function matchPinyin(
130
90
  pinyinData: PinYinData,
131
- matchStr: string
91
+ matchStr: string,
132
92
  ): boolean {
133
93
  const str = matchStr.toLowerCase()
134
94
  for (const py of pinyinData.main) {
@@ -18,15 +18,45 @@ export function getSessionId(item: Session): number | string {
18
18
  return item.user_id ?? item.group_id ?? 0
19
19
  }
20
20
 
21
+ function getLegacySessionId(item: Session): number | string | undefined {
22
+ return item.user_id ?? item.group_id
23
+ }
24
+
25
+ export function getSessionAliases(item: Session): string[] {
26
+ const rawAliases: Array<number | string | undefined> = [
27
+ getSessionId(item),
28
+ getLegacySessionId(item),
29
+ ]
30
+ if (item.group_id !== undefined && item.group_id !== null) {
31
+ rawAliases.push(normalizeGroupId(String(item.group_id)))
32
+ }
33
+ if (item.user_id !== undefined && item.user_id !== null) {
34
+ rawAliases.push(String(item.user_id).replace(/^(?:private|direct):/i, ''))
35
+ }
36
+ const aliases = rawAliases.filter((value): value is number | string => {
37
+ const text = String(value ?? '')
38
+ return text !== '' && text !== '0'
39
+ })
40
+ return [...new Set(aliases.map((value) => normalizeSessionId(value)))]
41
+ }
42
+
43
+ export function getSessionDedupKey(item: Session): string {
44
+ const channelId = String(item.channel_id ?? item.channelId ?? '')
45
+ const groupId = String(item.group_id ?? '')
46
+ const userId = String(item.user_id ?? '')
47
+ if (groupId) return `group:${normalizeGroupId(channelId || groupId)}`
48
+ if (userId) return `user:${userId.replace(/^(?:private|direct):/i, '')}`
49
+ return /^(?:private|direct):/i.test(channelId)
50
+ ? `user:${channelId.replace(/^(?:private|direct):/i, '')}`
51
+ : `group:${normalizeGroupId(channelId)}`
52
+ }
53
+
21
54
  export function setSessionContact(
22
55
  map: Map<number | string, Session>,
23
56
  item: Session,
24
57
  ) {
25
- const key = normalizeSessionId(getSessionId(item))
26
- map.set(key, item)
27
- const legacyId = item.user_id ?? item.group_id
28
- if (legacyId !== undefined && legacyId !== null && String(legacyId) !== key) {
29
- map.set(normalizeSessionId(String(legacyId)), item)
58
+ for (const alias of getSessionAliases(item)) {
59
+ map.set(alias, item)
30
60
  }
31
61
  }
32
62
 
@@ -34,8 +64,13 @@ export function findSessionContact(
34
64
  contacts: Session[],
35
65
  sessionId: number | string,
36
66
  ) {
67
+ const target = normalizeSessionId(sessionId)
37
68
  return contacts.find((item) => {
38
- return normalizeSessionId(getSessionId(item)) === normalizeSessionId(sessionId)
69
+ return getSessionAliases(item).some((alias) => alias === target)
70
+ || (
71
+ Boolean(item.group_id) &&
72
+ normalizeGroupId(String(item.group_id)) === normalizeGroupId(target)
73
+ )
39
74
  })
40
75
  }
41
76
 
@@ -98,6 +133,28 @@ function copyDefinedSessionState<K extends SessionStateKey>(
98
133
  ) {
99
134
  const value = currentSession[key]
100
135
  if (value !== undefined) {
136
+ const current = contact[key]
137
+ if (
138
+ key === 'time' &&
139
+ typeof current === 'number' &&
140
+ typeof value === 'number' &&
141
+ current > value
142
+ ) {
143
+ return
144
+ }
145
+ contact[key] = value
146
+ }
147
+ }
148
+
149
+ function copyMissingIdentity<K extends keyof Session>(
150
+ contact: Session,
151
+ currentSession: Session,
152
+ key: K,
153
+ ) {
154
+ const value = currentSession[key]
155
+ if (value === undefined || value === null || String(value) === '') return
156
+ const current = contact[key]
157
+ if (current === undefined || current === null || String(current) === '') {
101
158
  contact[key] = value
102
159
  }
103
160
  }
@@ -106,6 +163,21 @@ export function mergeSessionState(
106
163
  contact: Session,
107
164
  currentSession: Session,
108
165
  ) {
166
+ const identityKeys = [
167
+ 'channel_id',
168
+ 'channelId',
169
+ 'guild_id',
170
+ 'guildId',
171
+ 'group_id',
172
+ 'user_id',
173
+ 'group_name',
174
+ 'nickname',
175
+ 'remark',
176
+ 'avatar',
177
+ ] as const
178
+ identityKeys.forEach((key) =>
179
+ copyMissingIdentity(contact, currentSession, key),
180
+ )
109
181
  SESSION_STATE_KEYS.forEach((key) =>
110
182
  copyDefinedSessionState(contact, currentSession, key),
111
183
  )
@@ -122,14 +194,18 @@ export function mergeEarlySessionContacts(
122
194
  ) {
123
195
  let didMerge = false
124
196
  contacts.forEach((contact) => {
125
- const sessionId = normalizeSessionId(getSessionId(contact))
126
- const currentSession = sessions.get(sessionId)
127
- if (currentSession && currentSession !== contact) {
128
- sessions.set(
129
- sessionId,
130
- mergeSessionState(contact, currentSession),
131
- )
132
- didMerge = true
197
+ for (const [key, currentSession] of sessions.entries()) {
198
+ if (currentSession === contact) continue
199
+ if (
200
+ findSessionContact([currentSession], getSessionId(contact))
201
+ || getSessionDedupKey(contact) === getSessionDedupKey(currentSession)
202
+ ) {
203
+ sessions.set(
204
+ key,
205
+ mergeSessionState(contact, currentSession),
206
+ )
207
+ didMerge = true
208
+ }
133
209
  }
134
210
  })
135
211
  return didMerge
@@ -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 {
@@ -50,23 +50,23 @@
50
50
  {{ $t('空') }}
51
51
  </div>
52
52
  <FriendBody v-for="item in contactStore.showList"
53
- :key="'search-' + item.user_id + '-' + item.group_id"
53
+ :key="'search-' + (item.channel_id || item.channelId || item.user_id || item.group_id)"
54
54
  :data="item"
55
55
  from="friend"
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('空') }}
@@ -100,7 +100,7 @@
100
100
  return ( get.class_id == info.class_id )
101
101
  },
102
102
  )"
103
- :key=" 'fb-' + (item.user_id ? item.user_id : item.group_id) "
103
+ :key=" 'fb-' + (item.channel_id || item.channelId || item.user_id || item.group_id) "
104
104
  :data="item" from="friend"
105
105
  @click="userClick(item, $event)" />
106
106
  </div>
@@ -125,7 +125,7 @@
125
125
  return get.class_id == undefined
126
126
  },
127
127
  )"
128
- :key="'fb-' + (item.user_id ? item.user_id : item.group_id)"
128
+ :key="'fb-' + (item.channel_id || item.channelId || item.user_id || item.group_id)"
129
129
  :data="item"
130
130
  from="friend"
131
131
  @click="userClick(item, $event)" />
@@ -134,7 +134,7 @@
134
134
  </template>
135
135
  <template v-else>
136
136
  <FriendBody v-for="item in contactStore.userList"
137
- :key="'fb-' + (item.user_id ? item.user_id : item.group_id)"
137
+ :key="'fb-' + (item.channel_id || item.channelId || item.user_id || item.group_id)"
138
138
  :data="item"
139
139
  from="friend"
140
140
  @click="userClick(item, $event)" />
@@ -144,7 +144,7 @@
144
144
  <div v-else class="list">
145
145
  <div>
146
146
  <FriendBody v-for="item in contactStore.showList"
147
- :key="'fb-' + (item.user_id ? item.user_id : item.group_id)"
147
+ :key="'fb-' + (item.channel_id || item.channelId || item.user_id || item.group_id)"
148
148
  :data="item" from="friend"
149
149
  @click="userClick(item, $event)" />
150
150
  </div>
@@ -183,8 +183,8 @@
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'
187
- import { normalizeSessionId } from '../function/utils/sessionUtil'
186
+ import { botKey, getActiveBot, getLogins, setActiveBot } from '../function/satori'
187
+ import { normalizeGroupId, normalizeSessionId } from '../function/utils/sessionUtil'
188
188
  import { avatarError } from '../function/utils/avatarUtil'
189
189
  import { backend } from '../runtime/backend'
190
190
  import { matchPinyin } from '../function/utils/pinyin'
@@ -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[]>(() => {
@@ -240,17 +245,27 @@
240
245
  bot?: ContactWithBot['_bot'],
241
246
  ) => {
242
247
  for (const item of items) {
243
- const id = String(item.user_id ?? item.group_id ?? '')
248
+ const rawId = String(
249
+ item.channel_id
250
+ || item.channelId
251
+ || item.user_id
252
+ || item.group_id
253
+ || '',
254
+ )
255
+ const groupId = String(item.group_id || '')
256
+ const id = groupId
257
+ ? `group:${normalizeGroupId(rawId || groupId)}`
258
+ : `user:${rawId.replace(/^(?:private|direct):/i, '')}`
244
259
  const key = [bot?.platform ?? '', bot?.selfId ?? '', id]
245
260
  .map((value) => encodeURIComponent(String(value)))
246
261
  .join(':')
247
262
  map.set(key, { ...item, _bot: bot })
248
263
  }
249
264
  }
250
- const currentBot = satoriLogins.value.find((item) => item.selfId === activeBotId.value)
265
+ const currentBot = satoriLogins.value.find((item) => botKey(item.platform, item.selfId) === activeBotId.value)
251
266
  addContacts(contactStore.userList, currentBot)
252
- for (const [selfId, state] of contactStore.botStates) {
253
- 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)
254
269
  addContacts(state.userList, bot)
255
270
  }
256
271
  return [...map.values()]
@@ -274,9 +289,10 @@
274
289
  }
275
290
 
276
291
  function selectRobot(bot: { platform: string; selfId: string; name: string; avatar?: string; features?: string[] }) {
277
- if (bot.selfId === activeBotId.value) return
292
+ const key = botKey(bot.platform, bot.selfId)
293
+ if (key === activeBotId.value) return
278
294
  snapshotCurrentBot()
279
- activeBotId.value = bot.selfId
295
+ activeBotId.value = key
280
296
  setActiveBot(bot.platform, bot.selfId)
281
297
  loginInfo.uin = bot.selfId
282
298
  loginInfo.nickname = bot.name
@@ -289,14 +305,31 @@
289
305
  platform: bot.platform,
290
306
  avatar: bot.avatar,
291
307
  satoriLogins: satoriLogins.value,
292
- selectedSatoriBot: bot.selfId,
308
+ selectedSatoriBot: key,
293
309
  }
294
- const saved = contactStore.botStates.get(bot.selfId)
310
+ const saved = contactStore.botStates.get(key)
295
311
  if (saved) {
296
312
  contactStore.userList = saved.userList
297
313
  contactStore.baseOnMsgList.clear()
298
- for (const [key, value] of saved.baseList) {
299
- contactStore.baseOnMsgList.set(normalizeSessionId(key), value)
314
+ for (const [, value] of saved.baseList) {
315
+ const id = String(value.channel_id || value.channelId || value.user_id || value.group_id || '')
316
+ if (id && id !== '0') {
317
+ contactStore.baseOnMsgList.set(normalizeSessionId(id), value)
318
+ const legacyId = String(value.user_id || value.group_id || '')
319
+ if (legacyId && legacyId !== id) {
320
+ contactStore.baseOnMsgList.set(normalizeSessionId(legacyId), value)
321
+ }
322
+ }
323
+ }
324
+ for (const item of saved.onMsgList) {
325
+ const id = String(item.channel_id || item.channelId || item.user_id || item.group_id || '')
326
+ if (id && id !== '0') {
327
+ contactStore.baseOnMsgList.set(normalizeSessionId(id), item)
328
+ const legacyId = String(item.user_id || item.group_id || '')
329
+ if (legacyId && legacyId !== id) {
330
+ contactStore.baseOnMsgList.set(normalizeSessionId(legacyId), item)
331
+ }
332
+ }
300
333
  }
301
334
  contactStore.onMsgList = saved.onMsgList
302
335
  flushPendingBotEvents(bot.platform, bot.selfId)
@@ -312,7 +345,7 @@
312
345
  }
313
346
 
314
347
  function toggleRobot(bot: { platform: string; selfId: string; name: string; avatar?: string; features?: string[] }) {
315
- if (activeBotId.value === bot.selfId) {
348
+ if (activeBotId.value === botKey(bot.platform, bot.selfId)) {
316
349
  backToRobots()
317
350
  } else {
318
351
  selectRobot(bot)
@@ -368,7 +401,7 @@
368
401
  }
369
402
 
370
403
  function userClick(data: ContactWithBot, event: Event) {
371
- if (data._bot && data._bot.selfId !== activeBotId.value) {
404
+ if (data._bot && botKey(data._bot.platform, data._bot.selfId) !== activeBotId.value) {
372
405
  selectRobot(data._bot)
373
406
  }
374
407
  const sender = event.currentTarget as HTMLDivElement
@@ -380,18 +413,27 @@
380
413
  contactStore.showList = [] as any[]
381
414
 
382
415
  const back = {
383
- type: data.user_id ? 'user' : 'group',
384
- id: data.user_id ? data.user_id : data.group_id,
416
+ type: data.group_id ? 'group' : 'user',
417
+ id: data.group_id || data.user_id,
385
418
  name: getShowName(data),
386
419
  avatar: data.avatar || '/img/icons/icon.svg',
387
420
  jump: sender.dataset.jump,
388
- channel_id: data.channel_id,
421
+ channel_id: data.channel_id || data.channelId,
389
422
  guild_id: data.guild_id,
390
423
  } as BaseChatInfoElem
391
424
  if (back.id === undefined || back.id === null || String(back.id) === '' || String(back.id) === '0') return
392
425
  // 更新聊天框
393
426
  emit('userClick', back)
394
- contactStore.baseOnMsgList.set(normalizeSessionId(back.id), data)
427
+ const sessionId = String(
428
+ data.channel_id
429
+ || data.channelId
430
+ || data.user_id
431
+ || data.group_id
432
+ || '',
433
+ )
434
+ if (sessionId && sessionId !== '0') {
435
+ contactStore.baseOnMsgList.set(normalizeSessionId(sessionId), data)
436
+ }
395
437
  // 获取历史消息
396
438
  if(!uiStore.nowGetHistory) {
397
439
  emit('loadHistory', back)
@@ -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>