koishi-plugin-chat-patch 5.1.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/client/web/dist/assets/Chat-B448YOeN.js +28 -0
  2. package/client/web/dist/assets/{MsgBody-DLeOklIb.js → MsgBody-DDacjWJ9.js} +1 -1
  3. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-B1dWyCB0.js +67 -0
  4. package/client/web/dist/assets/{index-Cz89G4we.js → index-BlLut5xn.js} +121 -129
  5. package/client/web/dist/assets/index-fW_zcub3.css +1 -0
  6. package/client/web/dist/assets/pinyin-Dsq98249.js +157657 -0
  7. package/client/web/dist/index.html +2 -2
  8. package/client/web/dist/notice_local.json +1 -0
  9. package/client/web/package-lock.json +49 -0
  10. package/client/web/package.json +1 -0
  11. package/client/web/public/notice_local.json +1 -0
  12. package/client/web/src/components/EmojiFace.vue +0 -6
  13. package/client/web/src/components/FacePan.vue +0 -12
  14. package/client/web/src/components/MsgBody.vue +3 -4
  15. package/client/web/src/components/SettingsTab.vue +133 -0
  16. package/client/web/src/function/connect.ts +34 -14
  17. package/client/web/src/function/model/emoji.ts +7 -67
  18. package/client/web/src/function/msg.ts +68 -26
  19. package/client/web/src/function/utils/appUtil.ts +1 -4
  20. package/client/web/src/function/utils/msgUtil.ts +86 -52
  21. package/client/web/src/function/utils/pinyin.ts +25 -65
  22. package/client/web/src/function/utils/sessionUtil.ts +90 -14
  23. package/client/web/src/pages/Friends.vue +49 -13
  24. package/client/web/src/pages/Messages.vue +39 -12
  25. package/client/web/src/pages/Options.vue +4 -4
  26. package/client/web/src/pages/options/OptDev.vue +1 -14
  27. package/package.json +1 -1
  28. package/client/web/dist/assets/Chat-BoNiWGxb.js +0 -28
  29. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CGjy4Jzz.js +0 -67
  30. package/client/web/dist/assets/index-BgOX_tNa.css +0 -1
@@ -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
  ) {
@@ -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(),
@@ -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
  }))
@@ -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