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
@@ -3,30 +3,7 @@ export default class Emoji {
3
3
  static readonly descMap = new Map<string, string>()
4
4
  static readonly allList = new Set<number>()
5
5
  static readonly allSuperList = new Set<number>()
6
-
7
- static readonly superList: readonly number[] = [
8
- 5, 311, 312, 314, 317, 318, 319, 320, 324, 325, 337,
9
- 338, 339, 341, 342, 343, 344, 345, 346, 181, 74, 75,
10
- 351, 349, 350, 395, 114, 326, 53, 137, 333, 424, 415,
11
- 392, 425, 427, 426, 419, 429, 472, 474, 475, 476, 477,
12
- 478, 479,
13
- ]
14
-
15
- static readonly normalList: readonly number[] = [
16
- 14, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 0, 15, 16,
17
- 96, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
18
- 31, 32, 33, 34, 35, 36, 37, 38, 39, 97, 98, 99, 100,
19
- 101, 102, 103, 104, 105, 106, 107, 108, 305, 109, 110,
20
- 111, 172, 182, 179, 173, 174, 212, 175, 178, 177, 176,
21
- 183, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271,
22
- 272, 277, 307, 306, 281, 282, 283, 284, 285, 293, 286,
23
- 287, 289, 294, 297, 298, 299, 300, 323, 332, 336, 353,
24
- 355, 356, 354, 352, 357, 428, 334, 347, 303, 302, 295,
25
- 49, 66, 63, 64, 187, 146, 116, 67, 60, 185, 76, 124,
26
- 118, 78, 119, 79, 120, 121, 77, 123, 201, 273, 46, 112,
27
- 56, 169, 171, 59, 144, 147, 148, 89, 41, 125, 42, 43, 86, 129,
28
- 85,
29
- ]
6
+ static readonly normalList: readonly number[] = []
30
7
 
31
8
  static readonly emojiList: readonly number[] = [
32
9
  128522, 128524, 128538, 128531, 128560, 128541, 128513, 128540,
@@ -40,19 +17,6 @@ export default class Emoji {
40
17
  10068, 128299, 128147, 127978,
41
18
  ]
42
19
 
43
- static readonly responseApngId: readonly number[] = [
44
- 5, 314, 318, 319, 320, 324, 337, 338, 339, 341, 342, 343, 344,
45
- 345, 346, 181, 74, 75, 351, 349, 350, 395, 326, 53, 333, 424,
46
- 425, 427, 426, 14, 4, 8, 9, 10, 12, 16, 96, 21, 23, 24, 25, 26,
47
- 27, 28, 29, 30, 32, 33, 34, 38, 39, 97, 98, 99, 100, 101, 102,
48
- 103, 104, 106, 305, 109, 111, 182, 179, 173, 174, 212, 175, 176,
49
- 183, 262, 264, 265, 266, 267, 268, 269, 270, 271, 272, 277, 307,
50
- 306, 281, 282, 284, 285, 293, 287, 289, 294, 297, 298, 299, 332,
51
- 336, 353, 355, 356, 354, 352, 357, 428, 334, 347, 303, 302, 295,
52
- 49, 66, 63, 116, 60, 76, 124, 118, 78, 79, 120, 123, 201, 273,
53
- 171, 144, 147, 89, 41, 125, 42, 43, 129, 85,
54
- ]
55
-
56
20
  static readonly responseEmojiId: readonly number[] = [
57
21
  128522, 128524, 128538, 128531, 128560, 128541, 128513, 128540,
58
22
  9786, 128532, 128516, 128527, 128530, 128563, 128536, 128557,
@@ -67,7 +31,7 @@ export default class Emoji {
67
31
 
68
32
  private constructor(
69
33
  public id: number,
70
- public type: 'apng' | 'emoji',
34
+ public type: 'emoji',
71
35
  public hasSuper: boolean,
72
36
  public superSuffix: number[],
73
37
  ) {
@@ -77,28 +41,16 @@ export default class Emoji {
77
41
  }
78
42
 
79
43
  static get(id: number): Emoji | undefined {
80
- if (id < 5000) {
81
- const value = this.apngMap.get(id)
82
- return value ? new Emoji(id, 'apng', value.super, value.suffix) : undefined
83
- }
44
+ if (id < 5000) return undefined
84
45
  return new Emoji(id, 'emoji', false, [])
85
46
  }
86
47
 
87
48
  static has(id: number): boolean {
88
- return id >= 5000 || this.apngMap.has(id)
89
- }
90
-
91
- static init() {
92
- for (const id of [...this.superList, ...this.normalList, ...this.responseApngId]) {
93
- const isSuper = this.superList.includes(id)
94
- this.apngMap.set(id, { super: isSuper, suffix: [] })
95
- if (isSuper) this.allSuperList.add(id)
96
- this.allList.add(id)
97
- }
49
+ return id >= 5000
98
50
  }
99
51
 
100
52
  static get responseId(): readonly number[] {
101
- return [...this.responseApngId, ...this.responseEmojiId]
53
+ return this.responseEmojiId
102
54
  }
103
55
 
104
56
  get description(): string {
@@ -106,22 +58,10 @@ export default class Emoji {
106
58
  }
107
59
 
108
60
  get value(): string {
109
- return this.type === 'apng' ? this.getNormalUrl(this.id) : String.fromCodePoint(this.id)
61
+ return String.fromCodePoint(this.id)
110
62
  }
111
63
 
112
64
  get superValue(): string | undefined {
113
- if (!this.hasSuper) return undefined
114
- return this.getSuperUrl(this.id, this.suffixId)
115
- }
116
-
117
- private getNormalUrl(id: number): string {
118
- return `https://lib.stapxs.cn/download/stapxs-qq-lite/qq_emoji/${id}/apng/${id}.png`
119
- }
120
-
121
- private getSuperUrl(id: number, suffix?: number): string {
122
- const name = suffix ? `${id}_${suffix}` : `${id}`
123
- return `https://lib.stapxs.cn/download/stapxs-qq-lite/qq_emoji/${id}/lottie/${name}.json`
65
+ return undefined
124
66
  }
125
67
  }
126
-
127
- setTimeout(Emoji.init.bind(Emoji), 0)
@@ -87,6 +87,7 @@ import { useUIStore } from '../state/ui'
87
87
  import { useSettingsStore } from '../state/settings'
88
88
  import { useQzoneStore } from '../state/qzone'
89
89
  import {
90
+ findSessionContact,
90
91
  getSessionId,
91
92
  getMissingGroupPreviewSessions,
92
93
  mergeEarlySessionContacts,
@@ -119,6 +120,11 @@ function getString(value: unknown): string {
119
120
  return typeof value === 'string' ? value : String(value)
120
121
  }
121
122
 
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
+
122
128
  // 其他 tag
123
129
  let listLoadTimes = 0
124
130
  const logger = new Logger()
@@ -963,9 +969,11 @@ const msgFunctions = {
963
969
  )
964
970
  // 更新消息列表
965
971
  const onmsg = contactStore.baseOnMsgList.get(normalizeSessionId(id))
972
+ ?? findSessionContact(Array.from(contactStore.baseOnMsgList.values()), id)
966
973
  if (onmsg && list[0]) {
974
+ const canonicalId = normalizeSessionId(getSessionId(onmsg))
967
975
  Object.assign(onmsg, formatMessageData(list[0], Boolean(onmsg.group_id)))
968
- contactStore.baseOnMsgList.set(normalizeSessionId(id), onmsg)
976
+ contactStore.baseOnMsgList.set(canonicalId, onmsg)
969
977
  updateBaseOnMsgList()
970
978
  }
971
979
  }
@@ -1606,12 +1614,18 @@ function saveUser(msg: { [key: string]: any }, type: string) {
1606
1614
  list = list.filter((item, index, arr) => {
1607
1615
  return (
1608
1616
  arr.findIndex((item2) => {
1609
- const idA = String(item2.channel_id ?? item2.channelId ?? item2.group_id)
1610
- const idB = String(item.channel_id ?? item.channelId ?? item.group_id)
1611
- return idA === idB || (
1612
- !item2.channel_id && !item.channel_id &&
1613
- normalizeGroupId(idA) === normalizeGroupId(idB)
1614
- )
1617
+ const channelA = String(item2.channel_id ?? item2.channelId ?? '')
1618
+ const channelB = String(item.channel_id ?? item.channelId ?? '')
1619
+ const groupA = String(item2.group_id ?? item2.id ?? item2.guild_id ?? '')
1620
+ const groupB = String(item.group_id ?? item.id ?? item.guild_id ?? '')
1621
+ if (channelA && channelB) {
1622
+ return normalizeGroupId(channelA) === normalizeGroupId(channelB)
1623
+ }
1624
+ if (channelA || channelB) {
1625
+ return normalizeGroupId(channelA || channelB)
1626
+ === normalizeGroupId(groupA || groupB)
1627
+ }
1628
+ return normalizeGroupId(groupA) === normalizeGroupId(groupB)
1615
1629
  }) == index
1616
1630
  )
1617
1631
  })
@@ -1681,15 +1695,34 @@ function saveUser(msg: { [key: string]: any }, type: string) {
1681
1695
  list,
1682
1696
  contactStore.baseOnMsgList,
1683
1697
  )
1684
- const existingIds = new Set(
1685
- contactStore.userList.map((item) => {
1686
- return normalizeSessionId(String(item.channel_id ?? item.channelId ?? item.user_id ?? item.group_id ?? ''))
1687
- }),
1688
- )
1698
+ const sessionDedupKey = (item: any): string => {
1699
+ const channel = String(item.channel_id ?? item.channelId ?? '')
1700
+ const group = String(item.group_id ?? '')
1701
+ const user = String(item.user_id ?? '')
1702
+ if (group) return `group:${normalizeGroupId(channel || group)}`
1703
+ return `user:${String(user).replace(/^(?:private|direct):/i, '')}`
1704
+ }
1705
+ const existingIds = new Set(contactStore.userList.map(sessionDedupKey))
1689
1706
  const freshList = list.filter((item) => {
1690
- return !existingIds.has(normalizeSessionId(String(item.channel_id ?? item.channelId ?? item.user_id ?? item.group_id ?? '')))
1707
+ return !existingIds.has(sessionDedupKey(item))
1691
1708
  })
1692
- contactStore.userList = contactStore.userList.concat(freshList)
1709
+ const mergedContacts = [...contactStore.userList, ...freshList] as (UserFriendElem & UserGroupElem)[]
1710
+ const userMap = new Map<string, UserFriendElem & UserGroupElem>()
1711
+ for (const item of mergedContacts) {
1712
+ const key = sessionDedupKey(item)
1713
+ const existing = userMap.get(key)
1714
+ if (existing) {
1715
+ const pyName = existing.py_name
1716
+ const pyStart = existing.py_start
1717
+ Object.assign(existing, item)
1718
+ if (pyName) existing.py_name = pyName
1719
+ if (pyStart) existing.py_start = pyStart
1720
+ userMap.set(key, existing)
1721
+ } else {
1722
+ userMap.set(key, item)
1723
+ }
1724
+ }
1725
+ contactStore.userList = [...userMap.values()]
1693
1726
  if (
1694
1727
  settingsStore.sysConfig.session_display_mode === 'all' ||
1695
1728
  didMergeEarlySessions
@@ -1858,13 +1891,11 @@ async function saveMsg(msg: any, append = undefined as undefined | string) {
1858
1891
  const lastMsg =
1859
1892
  chatStore.messageList[chatStore.messageList.length - 1]
1860
1893
  if (lastMsg) {
1861
- const user = contactStore.userList.find((item) => {
1862
- return (
1863
- item.group_id == chatStore.chatInfo.show.id ||
1864
- item.user_id == chatStore.chatInfo.show.id
1865
- )
1866
- })
1867
- const sessionId = normalizeSessionId(chatStore.chatInfo.show.id)
1894
+ const user = findSessionContact(contactStore.userList, chatStore.chatInfo.show.id)
1895
+ const sessionId = normalizeSessionId(
1896
+ chatStore.chatInfo.show.channel_id
1897
+ ?? chatStore.chatInfo.show.id,
1898
+ )
1868
1899
  const session = contactStore.baseOnMsgList.get(sessionId) ?? user
1869
1900
  if (session) {
1870
1901
  const preview = formatMessageData(
@@ -2302,6 +2333,7 @@ function newMsg(_: string, data: any) {
2302
2333
  getString(getDataObject(data.channel).id)
2303
2334
  const loginId = String(authStore.loginInfo.uin ?? '')
2304
2335
  const showId = String(chatStore.chatInfo.show.id ?? '')
2336
+ const showChannelId = getString(chatStore.chatInfo.show.channel_id)
2305
2337
  const sender = getString(rawInfo.sender) ||
2306
2338
  getString(getDataObject(data.sender).user_id) ||
2307
2339
  getString(data.user_id)
@@ -2323,6 +2355,16 @@ function newMsg(_: string, data: any) {
2323
2355
  const sessionChannelId = getString(rawInfo.channel_id) || getString(data.channel_id)
2324
2356
  const sessionGuildId = getString(rawInfo.guild_id) || getString(data.guild_id)
2325
2357
  if (!id || id === '0') return
2358
+ const isCurrentSession = Boolean(
2359
+ (
2360
+ sessionChannelId &&
2361
+ showChannelId &&
2362
+ normalizeChannelForMatch(sessionChannelId) === normalizeChannelForMatch(showChannelId)
2363
+ ) || (
2364
+ (!sessionChannelId || !showChannelId) &&
2365
+ (id === showId || targetId === showId || String(sessionChannelId || sessionGuildId || '') === showId)
2366
+ ),
2367
+ )
2326
2368
  // 在好友列表里找一下他
2327
2369
  const senderInfo = contactStore.userList.find((item) => {
2328
2370
  return String(item.user_id ?? '') === sender
@@ -2377,7 +2419,7 @@ function newMsg(_: string, data: any) {
2377
2419
  }
2378
2420
 
2379
2421
  // 显示消息 ============================================
2380
- if (id && (id === showId || targetId === showId)) {
2422
+ if (id && isCurrentSession) {
2381
2423
  // 如果有正在输入的提示,清除它
2382
2424
  chatStore.chatInfo.show.appendInfo = undefined
2383
2425
  const cachedAvatar = getCachedUserAvatar(
@@ -2517,7 +2559,7 @@ function newMsg(_: string, data: any) {
2517
2559
  sender != loginId &&
2518
2560
  sender !== '' &&
2519
2561
  sender !== '0' &&
2520
- sessionId !== showId &&
2562
+ !isCurrentSession &&
2521
2563
  allowGroupInnerNotice
2522
2564
  ) {
2523
2565
  if (!session.new_msg) {
@@ -2525,7 +2567,7 @@ function newMsg(_: string, data: any) {
2525
2567
  contactStore.newMsgCount++
2526
2568
  }
2527
2569
  }
2528
- if (sessionId !== showId && allowGroupInnerNotice) {
2570
+ if (!isCurrentSession && allowGroupInnerNotice) {
2529
2571
  if (data.atme) { session.highlight = $t('[有人@你]') }
2530
2572
  if (data.atall) { session.highlight = $t('[@全体]') }
2531
2573
  if (isImportant) { session.highlight = $t('[特別关心]') }
@@ -2542,7 +2584,7 @@ function newMsg(_: string, data: any) {
2542
2584
  allowGroupSystemNotice
2543
2585
  ) {
2544
2586
  logger.add(LogType.DEBUG, '通知判定:', {
2545
- notShow: sessionId !== showId,
2587
+ notShow: !isCurrentSession,
2546
2588
  notFocus: !document.hasFocus(),
2547
2589
  hidden: document.hidden,
2548
2590
  isImportant: isImportant
@@ -2554,7 +2596,7 @@ function newMsg(_: string, data: any) {
2554
2596
  if (
2555
2597
  forceGroupSystemNotice ||
2556
2598
  forceImportantNotice ||
2557
- sessionId !== showId ||
2599
+ !isCurrentSession ||
2558
2600
  !document.hasFocus() ||
2559
2601
  document.hidden
2560
2602
  ) {
@@ -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),
@@ -1026,10 +1026,7 @@ export function checkOpenTimes() {
1026
1026
  */
1027
1027
  export function checkNotice() {
1028
1028
  const uiStore = useUIStore()
1029
- let url = 'https://lib.stapxs.cn/download/stapxs-qq-lite/notice-config.json'
1030
- if (import.meta.env.DEV) {
1031
- url = 'notice_local.json'
1032
- }
1029
+ const url = 'notice_local.json'
1033
1030
  const version = 3
1034
1031
  const fetchData = {
1035
1032
  time: new Date().getTime().toString(),
@@ -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
  }
@@ -20,11 +20,14 @@ import { useContactStore } from '../../state/contact'
20
20
  import { useUIStore } from '../../state/ui'
21
21
  import { useAuthStore } from '../../state/auth'
22
22
  import { useChatStore } from '../../state/chat'
23
- import {
24
- findSessionContact,
25
- getSessionId,
26
- normalizeSessionId,
27
- } from './sessionUtil'
23
+ import {
24
+ findSessionContact,
25
+ getSessionAliases,
26
+ getSessionDedupKey,
27
+ getSessionId,
28
+ mergeSessionState,
29
+ normalizeSessionId,
30
+ } from './sessionUtil'
28
31
 
29
32
  const logger = new Logger()
30
33
 
@@ -787,8 +790,9 @@ export function updateLastestHistory(item: UserFriendElem & UserGroupElem) {
787
790
  const authStore = useAuthStore()
788
791
  // 发起获取历史消息请求
789
792
  const type = item.user_id ? 'user' : 'group'
790
- const id = item.user_id ? item.user_id : item.group_id
791
- let name
793
+ const id = item.user_id ? item.user_id : item.group_id
794
+ const sessionId = normalizeSessionId(getSessionId(item))
795
+ let name
792
796
  if (authStore.jsonMap.message_list && type != 'group') {
793
797
  name = authStore.jsonMap.message_list.private_name
794
798
  } else {
@@ -804,7 +808,7 @@ export function updateLastestHistory(item: UserFriendElem & UserGroupElem) {
804
808
  message_id: 0,
805
809
  count: 1,
806
810
  },
807
- 'getChatHistoryOnMsg_' + id,
811
+ 'getChatHistoryOnMsg_' + sessionId,
808
812
  )
809
813
  }
810
814
 
@@ -817,47 +821,75 @@ function getSessionSortName(item: UserFriendElem & UserGroupElem) {
817
821
  return item.py_start ?? getShowName(item.group_name ?? item.nickname ?? '', item.remark ?? '')
818
822
  }
819
823
 
820
- function normalizeBaseSessionMap() {
821
- const contactStore = useContactStore()
822
- let needNormalize = false
823
- for (const id of contactStore.baseOnMsgList.keys()) {
824
- if (typeof id !== 'string') {
825
- needNormalize = true
826
- break
827
- }
828
- }
829
- if (!needNormalize) return
830
-
831
- const normalized = new Map<string, UserFriendElem & UserGroupElem>()
832
- contactStore.baseOnMsgList.forEach((item, id) => {
833
- normalized.set(normalizeSessionId(id), item)
834
- })
835
- contactStore.baseOnMsgList.clear()
836
- for (const [id, item] of normalized) {
837
- contactStore.baseOnMsgList.set(id, item)
838
- }
839
- }
840
-
841
- function getSessionList() {
842
- const contactStore = useContactStore()
843
- const settingsStore = useSettingsStore()
844
- const sessionMap = new Map<string, UserFriendElem & UserGroupElem>()
845
-
846
- if (settingsStore.sysConfig.session_display_mode === 'all') {
847
- contactStore.userList.forEach((item) => {
848
- const id = normalizeSessionId(getSessionId(item))
849
- if (id !== '0' && id !== '') {
850
- sessionMap.set(id, item)
851
- }
852
- })
853
- }
854
-
855
- contactStore.baseOnMsgList.forEach((item, id) => {
856
- sessionMap.set(normalizeSessionId(id), item)
857
- })
858
-
859
- return [...sessionMap.values()]
860
- }
824
+ function normalizeBaseSessionMap() {
825
+ const contactStore = useContactStore()
826
+ let needNormalize = false
827
+ const seen = new Map<string, UserFriendElem & UserGroupElem>()
828
+ for (const [rawKey, item] of contactStore.baseOnMsgList) {
829
+ const id = getSessionDedupKey(item)
830
+ if (id.endsWith(':0') || id.endsWith(':')) continue
831
+ const existing = seen.get(id)
832
+ if (existing && existing !== item) {
833
+ needNormalize = true
834
+ continue
835
+ }
836
+ seen.set(id, item)
837
+ if (
838
+ typeof rawKey !== 'string' ||
839
+ !getSessionAliases(item).includes(normalizeSessionId(rawKey))
840
+ ) {
841
+ needNormalize = true
842
+ }
843
+ }
844
+ if (!needNormalize) return
845
+
846
+ const canonical = new Map<string, UserFriendElem & UserGroupElem>()
847
+ contactStore.baseOnMsgList.forEach((item) => {
848
+ const id = getSessionDedupKey(item)
849
+ if (id === 'group:0' || id === 'group:' || id === 'user:0' || id === 'user:') return
850
+ const existing = canonical.get(id)
851
+ if (existing && existing !== item) {
852
+ canonical.set(id, mergeSessionState(existing, item))
853
+ } else if (!existing) {
854
+ canonical.set(id, item)
855
+ }
856
+ })
857
+ contactStore.baseOnMsgList.clear()
858
+ for (const [id, item] of canonical) {
859
+ for (const alias of getSessionAliases(item)) {
860
+ contactStore.baseOnMsgList.set(alias, item)
861
+ }
862
+ }
863
+ }
864
+
865
+ function getSessionList() {
866
+ const contactStore = useContactStore()
867
+ const settingsStore = useSettingsStore()
868
+ const sessionMap = new Map<string, UserFriendElem & UserGroupElem>()
869
+
870
+ const addSession = (item: UserFriendElem & UserGroupElem) => {
871
+ const id = getSessionDedupKey(item)
872
+ if (id === 'group:0' || id === 'group:' || id === 'user:0' || id === 'user:') return
873
+ const existing = sessionMap.get(id)
874
+ if (existing && existing !== item) {
875
+ sessionMap.set(id, mergeSessionState(existing, item))
876
+ } else if (!existing) {
877
+ sessionMap.set(id, item)
878
+ }
879
+ }
880
+
881
+ if (settingsStore.sysConfig.session_display_mode === 'all') {
882
+ contactStore.userList.forEach((item) => {
883
+ addSession(item)
884
+ })
885
+ }
886
+
887
+ contactStore.baseOnMsgList.forEach((item) => {
888
+ addSession(item)
889
+ })
890
+
891
+ return [...sessionMap.values()]
892
+ }
861
893
 
862
894
  /**
863
895
  * 刷新消息列表排序
@@ -890,9 +922,11 @@ export function updateBaseOnMsgList() {
890
922
  let groupAssistList = [] as any[]
891
923
  if (settingsStore.sysConfig.bubble_sort_user) {
892
924
  // 将 normalList 进行拆分
893
- const shouldShowInMainList = (item: UserFriendElem & UserGroupElem) => {
894
- return item.user_id || item.always_top
895
- }
925
+ const shouldShowInMainList = (item: UserFriendElem & UserGroupElem) => {
926
+ return item.group_id
927
+ ? item.always_top
928
+ : Boolean(item.user_id) || item.always_top
929
+ }
896
930
  onMsgList = topList.concat(normalList.filter((item) => {
897
931
  return shouldShowInMainList(item)
898
932
  }))