koishi-plugin-chat-patch 5.3.0 → 5.5.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 (77) hide show
  1. package/client/web/dist/assets/Chat-Clqxn7CB.js +28 -0
  2. package/client/web/dist/assets/{Chat-CTdyeaX2.css → Chat-DTEsebnY.css} +1 -1
  3. package/client/web/dist/assets/{MsgBody-DDacjWJ9.js → MsgBody-C0TKsXhC.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-CCdBwlN_.js} +2 -2
  5. package/client/web/dist/assets/{append_darwin-CqBNU62J.css → append_darwin-Cql-dV6d.css} +1 -1
  6. package/client/web/dist/assets/{append_full_vibrancy-j0D2LM9m.css → append_full_vibrancy-ih3loy1O.css} +1 -1
  7. package/client/web/dist/assets/{append_linux-uhxeBqeC.css → append_linux-B3LvR8KB.css} +1 -1
  8. package/client/web/dist/assets/{append_new-407Lo5_V.css → append_new-DfhN_NX9.css} +1 -1
  9. package/client/web/dist/assets/{append_vibrancy-6FHctkOY.css → append_vibrancy-D3pVsmAa.css} +1 -1
  10. package/client/web/dist/assets/{append_win32-srmD-78y.css → append_win32-Dh9JlmlE.css} +1 -1
  11. package/client/web/dist/assets/index-B71pPwae.css +1 -0
  12. package/client/web/dist/assets/{index-BlLut5xn.js → index-BA37EQGa.js} +97 -76
  13. package/client/web/dist/index.html +2 -2
  14. package/client/web/src/App.vue +2 -2
  15. package/client/web/src/assets/css/append/append_darwin.css +2 -2
  16. package/client/web/src/assets/css/append/append_full_vibrancy.css +2 -2
  17. package/client/web/src/assets/css/append/append_linux.css +2 -2
  18. package/client/web/src/assets/css/append/append_new.css +2 -2
  19. package/client/web/src/assets/css/append/append_vibrancy.css +1 -1
  20. package/client/web/src/assets/css/append/append_win32.css +2 -2
  21. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +1 -1
  22. package/client/web/src/assets/css/chat.css +4 -3
  23. package/client/web/src/assets/css/msg.css +7 -3
  24. package/client/web/src/assets/css/options.css +2 -2
  25. package/client/web/src/assets/css/view.css +14 -3
  26. package/client/web/src/assets/l10n/zh-CN.po +21 -0
  27. package/client/web/src/components/AboutPan.vue +7 -4
  28. package/client/web/src/components/BulletinBody.vue +1 -1
  29. package/client/web/src/components/DepPan.vue +1 -1
  30. package/client/web/src/components/FriendBody.vue +1 -0
  31. package/client/web/src/components/MsgBody.vue +1 -3
  32. package/client/web/src/components/ThemeColorPickerPan.vue +1 -1
  33. package/client/web/src/components/TinySessionBody.vue +1 -0
  34. package/client/web/src/components/UmamiInfoPan.vue +1 -1
  35. package/client/web/src/components/UpdatePan.vue +2 -2
  36. package/client/web/src/components/WelPan.vue +1 -1
  37. package/client/web/src/components/tooltip/UserInfoTooltip.vue +1 -1
  38. package/client/web/src/function/connect.ts +279 -86
  39. package/client/web/src/function/elements/information.ts +1 -0
  40. package/client/web/src/function/msg.ts +42 -23
  41. package/client/web/src/function/satori-model.ts +35 -13
  42. package/client/web/src/function/satori.ts +33 -2
  43. package/client/web/src/function/utils/appUtil.ts +3 -11
  44. package/client/web/src/function/utils/avatarUtil.ts +18 -3
  45. package/client/web/src/function/utils/localHistoryUtil.ts +36 -12
  46. package/client/web/src/function/utils/msgUtil.ts +32 -21
  47. package/client/web/src/function/utils/sessionUtil.ts +20 -18
  48. package/client/web/src/main.ts +5 -0
  49. package/client/web/src/pages/Chat.vue +50 -47
  50. package/client/web/src/pages/Friends.vue +28 -24
  51. package/client/web/src/pages/Info.vue +5 -5
  52. package/client/web/src/pages/Messages.vue +57 -36
  53. package/client/web/src/pages/Qzone.vue +2 -2
  54. package/client/web/src/pages/chat-view/ChatTerminal.vue +4 -1
  55. package/client/web/src/pages/chat-view/SystemNotice.vue +3 -3
  56. package/client/web/src/pages/options/OptAccount.vue +6 -4
  57. package/client/web/src/pages/options/OptDev.vue +45 -2
  58. package/lib/database.d.ts +58 -0
  59. package/lib/index.js +234 -82
  60. package/lib/media.d.ts +24 -0
  61. package/lib/recorder.d.ts +21 -0
  62. package/lib/self-message.d.ts +27 -0
  63. package/lib/types.d.ts +121 -0
  64. package/lib/web.d.ts +7 -0
  65. package/package.json +1 -1
  66. package/src/bootstrap.ts +1 -0
  67. package/src/cache.ts +1 -5
  68. package/src/config.ts +1 -1
  69. package/src/database.ts +52 -8
  70. package/src/index.ts +4 -4
  71. package/src/media.ts +97 -0
  72. package/src/recorder.ts +35 -26
  73. package/src/self-message.ts +68 -19
  74. package/src/types.ts +10 -0
  75. package/src/web.ts +57 -48
  76. package/client/web/dist/assets/Chat-B448YOeN.js +0 -28
  77. package/client/web/dist/assets/index-fW_zcub3.css +0 -1
@@ -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,
@@ -29,6 +31,7 @@ import {
29
31
  getSessionDedupKey,
30
32
  normalizeSessionId,
31
33
  setSessionContact,
34
+ upsertSessionContact,
32
35
  } from './utils/sessionUtil'
33
36
  import { buildQqGroupAvatar } from './utils/avatarUtil'
34
37
  import type { ConnectionHistoryItem, LoginCacheElem } from './elements/system'
@@ -42,6 +45,14 @@ function encodeKeyPart(value: string): string {
42
45
  return encodeURIComponent(value)
43
46
  }
44
47
 
48
+ function isBlockedPlatform(platform: string): boolean {
49
+ return getBlockedPlatforms().some((item) => {
50
+ return item.exactMatch
51
+ ? platform === item.platformName
52
+ : platform.includes(item.platformName)
53
+ })
54
+ }
55
+
45
56
  function getChannelKind(value: unknown): 'group' | 'direct' | 'unknown' {
46
57
  if (typeof value === 'number') {
47
58
  if (value === 0) return 'group'
@@ -350,7 +361,7 @@ function updateBotStateGroup(
350
361
  apply: (target: Record<string, unknown>) => void,
351
362
  ) {
352
363
  const contactStore = useContactStore()
353
- const state = contactStore.botStates.get(selfId)
364
+ const state = contactStore.botStates.get(botKey(platform, selfId))
354
365
  const session = state?.onMsgList.find((item) => String(item.group_id) === sessionId)
355
366
  if (!state || !session) return
356
367
  apply(session as unknown as Record<string, unknown>)
@@ -363,7 +374,7 @@ function updateBotStateGroup(
363
374
  } else {
364
375
  baseList.push([key, typedSession])
365
376
  }
366
- contactStore.botStates.set(selfId, {
377
+ contactStore.botStates.set(botKey(platform, selfId), {
367
378
  userList: [...state.userList],
368
379
  baseList,
369
380
  onMsgList: [...state.onMsgList],
@@ -494,7 +505,7 @@ async function fetchUserIdentity(
494
505
  const guildId = getString(msg.guild_id) || getString(msg.group_id)
495
506
  const isPrivate = isPrivateMessage(msg)
496
507
  const channelId = isPrivate
497
- ? (getString(msg.channel_id) || `private:${userId}`)
508
+ ? (getString(msg.channel_id) || userId)
498
509
  : ''
499
510
  const sender = getObject(msg.sender)
500
511
  const senderName = getString(sender.nickname) || getString(sender.card) || getString(sender.name)
@@ -545,10 +556,11 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
545
556
  })
546
557
  const isGroup = isGroupMessage(msg)
547
558
  const channelId = typeof msg.channel_id === 'string' ? msg.channel_id : ''
559
+ const guildId = typeof msg.guild_id === 'string' ? msg.guild_id : ''
548
560
  const sessionId = String(
549
- msg.group_id
550
- || msg.user_id
551
- || (isGroup ? '' : channelId.replace(/^private:/, '')),
561
+ isGroup
562
+ ? (msg.group_id || channelId || guildId || '')
563
+ : (msg.user_id || channelId || ''),
552
564
  )
553
565
  if (!sessionId || sessionId === '0') {
554
566
  identityDebug('recordBotMessage: skipped invalid session', { sessionId, msg })
@@ -561,7 +573,8 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
561
573
  const senderId = String(
562
574
  sender.user_id
563
575
  || msg.user_id
564
- || (isGroup ? '' : channelId.replace(/^private:/, '')),
576
+ || channelId
577
+ || '',
565
578
  )
566
579
  const raw = getMsgRawTxt(msg) || (typeof msg.raw_message === 'string' ? msg.raw_message : '')
567
580
  const senderName = String(sender.card || sender.nickname || (isGroup ? senderId : ''))
@@ -578,7 +591,7 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
578
591
  ? (eventGroupAvatar || cachedGroupInfo?.avatar || buildQqGroupAvatar(platform, sessionId) || undefined)
579
592
  : undefined
580
593
  const contactStore = useContactStore()
581
- const state = contactStore.botStates.get(selfId) ?? {
594
+ const state = contactStore.botStates.get(botKey(platform, selfId)) ?? {
582
595
  userList: [],
583
596
  baseList: [],
584
597
  onMsgList: [],
@@ -601,60 +614,108 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
601
614
  channel_id: typeof msg.channel_id === 'string' ? msg.channel_id : undefined,
602
615
  guild_id: typeof msg.guild_id === 'string' ? msg.guild_id : undefined,
603
616
  }
604
- const existing = state.onMsgList.find((item) => {
605
- const itemChannelId = String(item.channel_id || item.channelId || '')
606
- if (itemChannelId) {
607
- if (isGroup) {
617
+ const typedSession = session as unknown as UserFriendElem & UserGroupElem
618
+ const memberContact = isGroup && senderId && senderId !== '0'
619
+ ? {
620
+ user_id: senderId,
621
+ nickname: String(sender.nickname || sender.card || senderName || senderId),
622
+ remark: String(sender.nickname || sender.card || senderName || senderId),
623
+ avatar: hasUsableAvatar(sender.avatar) ? getString(sender.avatar) : undefined,
624
+ class_id: 0,
625
+ class_name: '我的好友',
626
+ _groupMember: true,
627
+ raw_msg: senderName ? `${senderName}: ${raw}` : raw,
628
+ raw_msg_base: raw,
629
+ time: sessionTime,
630
+ message_id: String(msg.message_id ?? ''),
631
+ guild_id: typeof msg.guild_id === 'string' ? msg.guild_id : undefined,
632
+ } as UserFriendElem & UserGroupElem
633
+ : undefined
634
+ let baseList = [...state.baseList]
635
+ if (isGroup) {
636
+ // 群消息进入群会话列表,群成员只进联系人列表
637
+ state.userList = upsertSessionContact(state.userList, typedSession)
638
+ if (memberContact) {
639
+ state.userList = upsertSessionContact(state.userList, memberContact)
640
+ }
641
+ const existing = state.onMsgList.find((item) => {
642
+ const itemChannelId = String(item.channel_id || item.channelId || '')
643
+ if (itemChannelId) {
608
644
  return normalizeGroupId(itemChannelId) === normalizeGroupId(channelId || sessionId)
609
645
  }
610
- return itemChannelId.replace(/^(?:private|direct):/i, '')
611
- === String(sessionId).replace(/^(?:private|direct):/i, '')
612
- }
613
- return isGroup
614
- ? String(item.group_id) === sessionId
615
- : String(item.user_id) === sessionId
616
- })
617
- if (existing) {
618
- const previousAvatar = existing.avatar
619
- const previousGroupName = existing.group_name
620
- const previousNickname = existing.nickname
621
- const previousRemark = existing.remark
622
- Object.assign(existing, session)
623
- if (!hasUsableAvatar(session.avatar) && hasUsableAvatar(previousAvatar)) {
624
- existing.avatar = previousAvatar
646
+ return String(item.group_id) === sessionId
647
+ })
648
+ if (existing) {
649
+ const previousAvatar = existing.avatar
650
+ const previousGroupName = existing.group_name
651
+ Object.assign(existing, session)
652
+ if (!hasUsableAvatar(session.avatar) && hasUsableAvatar(previousAvatar)) {
653
+ existing.avatar = previousAvatar
654
+ }
655
+ if (
656
+ !msg.group_name &&
657
+ previousGroupName &&
658
+ String(previousGroupName) !== String(sessionId)
659
+ ) {
660
+ existing.group_name = previousGroupName
661
+ }
662
+ session = existing
663
+ } else {
664
+ state.onMsgList.unshift(session)
625
665
  }
626
- if (
627
- isGroup &&
628
- !msg.group_name &&
629
- previousGroupName &&
630
- String(previousGroupName) !== String(sessionId)
631
- ) {
632
- existing.group_name = previousGroupName
666
+ const baseKey = normalizeSessionId(sessionKey)
667
+ const baseIndex = baseList.findIndex(([, value]) => value === session)
668
+ if (baseIndex >= 0) {
669
+ baseList[baseIndex] = [baseKey, typedSession]
670
+ } else {
671
+ baseList.push([baseKey, typedSession])
633
672
  }
634
- if (!isGroup && !sender.nickname && previousNickname) {
635
- existing.nickname = previousNickname
673
+ } else {
674
+ const existing = state.onMsgList.find((item) => {
675
+ const itemChannelId = String(item.channel_id || item.channelId || '')
676
+ if (itemChannelId) {
677
+ return itemChannelId === String(sessionId)
678
+ }
679
+ return String(item.user_id) === sessionId
680
+ })
681
+ if (existing) {
682
+ const previousAvatar = existing.avatar
683
+ const previousNickname = existing.nickname
684
+ const previousRemark = existing.remark
685
+ Object.assign(existing, session)
686
+ if (existing._groupMember) existing._groupMember = false
687
+ if (!hasUsableAvatar(session.avatar) && hasUsableAvatar(previousAvatar)) {
688
+ existing.avatar = previousAvatar
689
+ }
690
+ if (!isGroup && !sender.nickname && previousNickname) {
691
+ existing.nickname = previousNickname
692
+ }
693
+ if (!isGroup && !existing.remark && previousRemark) {
694
+ existing.remark = previousRemark
695
+ }
696
+ session = existing
697
+ } else {
698
+ state.onMsgList.unshift(session)
636
699
  }
637
- if (!isGroup && !existing.remark && previousRemark) {
638
- existing.remark = previousRemark
700
+ const baseKey = normalizeSessionId(sessionKey)
701
+ const baseIndex = baseList.findIndex(([, value]) => value === session)
702
+ if (baseIndex >= 0) {
703
+ baseList[baseIndex] = [baseKey, typedSession]
704
+ } else {
705
+ baseList.push([baseKey, typedSession])
639
706
  }
640
- session = existing
641
- } else {
642
- state.onMsgList.unshift(session)
643
- }
644
- const baseKey = normalizeSessionId(sessionKey)
645
- const baseList = [...state.baseList]
646
- const baseIndex = baseList.findIndex(([, value]) => value === session)
647
- const typedSession = session as unknown as UserFriendElem & UserGroupElem
648
- if (baseIndex >= 0) {
649
- baseList[baseIndex] = [baseKey, typedSession]
650
- } else {
651
- baseList.push([baseKey, typedSession])
652
707
  }
653
- contactStore.botStates.set(selfId, {
708
+ contactStore.botStates.set(botKey(platform, selfId), {
654
709
  userList: [...state.userList],
655
710
  baseList,
656
711
  onMsgList: [...state.onMsgList],
657
712
  })
713
+ if (isGroup && isActiveBot({ platform, selfId })) {
714
+ contactStore.userList = upsertSessionContact(contactStore.userList, typedSession)
715
+ if (memberContact) {
716
+ contactStore.userList = upsertSessionContact(contactStore.userList, memberContact)
717
+ }
718
+ }
658
719
  if (
659
720
  isActiveBot({ platform, selfId }) &&
660
721
  !contactStore.baseOnMsgList.has(normalizeSessionId(sessionKey))
@@ -664,7 +725,7 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
664
725
  }
665
726
 
666
727
  const chatStore = useChatStore()
667
- const cacheKey = [platform, selfId, sessionId].map(encodeKeyPart).join(':')
728
+ const cacheKey = [platform, selfId, sessionKey].map(encodeKeyPart).join(':')
668
729
  const messages = chatStore.sessionMessageCache.get(cacheKey) ?? []
669
730
  const messageId = String(msg.message_id ?? '')
670
731
  if (messageId && !messages.some((item) => String(item.message_id) === messageId)) {
@@ -726,6 +787,26 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
726
787
  )
727
788
  if (senderId && senderId !== '0') {
728
789
  void fetchUserIdentity(platform, selfId, senderId, null, msg)
790
+ if (isCurrentChat) {
791
+ const members = chatStore.chatInfo.info.group_members
792
+ const member = {
793
+ user_id: senderId,
794
+ nickname: String(sender.nickname || sender.card || senderName || senderId),
795
+ card: String(sender.card || ''),
796
+ role: String(sender.role || ''),
797
+ avatar: hasUsableAvatar(sender.avatar) ? getString(sender.avatar) : undefined,
798
+ title: '',
799
+ join_time: 0,
800
+ last_sent_time: Math.floor(sessionTime),
801
+ level: 0,
802
+ rank: '',
803
+ sex: '',
804
+ shutup_time: 0,
805
+ }
806
+ const index = members.findIndex((item) => String(item.user_id) === senderId)
807
+ if (index >= 0) Object.assign(members[index], member)
808
+ else members.push(member)
809
+ }
729
810
  }
730
811
  } else if (senderId && senderId !== '0') {
731
812
  identityDebug('recordBotMessage: private identity branch', {
@@ -748,7 +829,9 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
748
829
  const msg = messages[messages.length - 1]
749
830
  if (!msg) continue
750
831
  const isGroup = isGroupMessage(msg)
751
- const id = String(isGroup ? msg.group_id : msg.user_id)
832
+ const id = String(isGroup
833
+ ? (msg.group_id || msg.channel_id || msg.guild_id)
834
+ : (msg.user_id || msg.channel_id))
752
835
  if (!id || id === '0') continue
753
836
  const sender = getObject(msg.sender)
754
837
  const senderId = getString(sender.user_id) || getString(msg.user_id) || ''
@@ -780,7 +863,7 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
780
863
  }
781
864
  if (!sessions.length) return
782
865
 
783
- const state = contactStore.botStates.get(selfId) ?? {
866
+ const state = contactStore.botStates.get(botKey(platform, selfId)) ?? {
784
867
  userList: [],
785
868
  baseList: [],
786
869
  onMsgList: [],
@@ -788,28 +871,28 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
788
871
  const baseMap = new Map<string, UserFriendElem & UserGroupElem>()
789
872
  for (const [, value] of state.baseList) {
790
873
  const id = getSessionDedupKey(value)
791
- if (!id.endsWith(':0') && !id.endsWith(':')) baseMap.set(id, value)
874
+ if (id !== '0' && id !== '') baseMap.set(id, value)
792
875
  }
793
876
  const baseList = [...baseMap.entries()]
794
877
  const baseKeys = new Set(baseList.map(([key]) => key))
795
878
  const existing = new Set(
796
879
  state.onMsgList.map((item) => {
797
880
  const id = getSessionDedupKey(item)
798
- return !id.endsWith(':0') && !id.endsWith(':') ? id : ''
881
+ return id !== '0' && id !== '' ? id : ''
799
882
  }).filter(Boolean),
800
883
  )
801
884
  for (const session of sessions) {
802
885
  const id = getSessionDedupKey(session)
803
- if (!id.endsWith(':0') && !id.endsWith(':') && !existing.has(id)) {
886
+ if (id !== '0' && id !== '' && !existing.has(id)) {
804
887
  state.onMsgList.unshift(session)
805
888
  existing.add(id)
806
889
  }
807
- if (!id.endsWith(':0') && !id.endsWith(':') && !baseKeys.has(id)) {
890
+ if (id !== '0' && id !== '' && !baseKeys.has(id)) {
808
891
  baseList.push([normalizeSessionId(String(session.channel_id || session.channelId || session.user_id || session.group_id || '')), session])
809
892
  baseKeys.add(id)
810
893
  }
811
894
  }
812
- contactStore.botStates.set(selfId, {
895
+ contactStore.botStates.set(botKey(platform, selfId), {
813
896
  userList: [...state.userList],
814
897
  baseList,
815
898
  onMsgList: [...state.onMsgList],
@@ -817,6 +900,7 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
817
900
  }
818
901
 
819
902
  function onSatoriEvent(event: SatoriEvent) {
903
+ if (isBlockedPlatform(event.platform)) return
820
904
  const oneBot = satoriEventToOneBot(event.body, String(event.platform || ''))
821
905
  if (oneBot) {
822
906
  oneBot._rawSatori = {
@@ -865,6 +949,7 @@ export function flushPendingBotEvents(platform: string, selfId: string) {
865
949
  if (!events?.length) return
866
950
  pendingBotEvents.delete(key)
867
951
  for (const event of events) {
952
+ if (isBlockedPlatform(event.platform)) continue
868
953
  const oneBot = satoriEventToOneBot(event.body, String(event.platform || ''))
869
954
  if (oneBot) {
870
955
  oneBot._rawSatori = {
@@ -893,7 +978,8 @@ function onSatoriReady(logins: Array<{ platform: string; selfId: string; name: s
893
978
  login.creating = false
894
979
  login.status = true
895
980
  const authStore = useAuthStore()
896
- for (const item of logins) {
981
+ const visibleLogins = logins.filter((item) => !isBlockedPlatform(item.platform))
982
+ for (const item of visibleLogins) {
897
983
  if (!item.platform || !item.selfId) continue
898
984
  identityInfoCache.set(identityCacheKey('user', item.platform, item.selfId, item.selfId), {
899
985
  name: item.name || item.selfId,
@@ -907,7 +993,7 @@ function onSatoriReady(logins: Array<{ platform: string; selfId: string; name: s
907
993
  },
908
994
  })
909
995
  }
910
- const first = logins[0]
996
+ const first = visibleLogins[0]
911
997
  if (!first) return
912
998
 
913
999
  setActiveBot(first.platform, first.selfId)
@@ -915,16 +1001,16 @@ function onSatoriReady(logins: Array<{ platform: string; selfId: string; name: s
915
1001
  login.uin = first.selfId
916
1002
  login.nickname = first.name
917
1003
  login.platform = first.platform
918
- login.satoriLogins = logins
919
- login.selectedSatoriBot = first.selfId
1004
+ login.satoriLogins = visibleLogins
1005
+ login.selectedSatoriBot = botKey(first.platform, first.selfId)
920
1006
  authStore.loginInfo = {
921
1007
  uin: first.selfId,
922
1008
  user_id: first.selfId,
923
1009
  nickname: first.name,
924
1010
  platform: first.platform,
925
1011
  avatar: first.avatar,
926
- satoriLogins: logins,
927
- selectedSatoriBot: first.selfId,
1012
+ satoriLogins: visibleLogins,
1013
+ selectedSatoriBot: botKey(first.platform, first.selfId),
928
1014
  }
929
1015
  authStore.botInfo = {
930
1016
  app_name: 'Satori',
@@ -944,9 +1030,7 @@ function contactId(item: unknown): string {
944
1030
  }
945
1031
 
946
1032
  function normalizeGroupId(value: string): string {
947
- const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, '').trim()
948
- const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/)
949
- return wrapped ? wrapped[1] : raw || value
1033
+ return String(value)
950
1034
  }
951
1035
 
952
1036
  function contactName(item: unknown): string {
@@ -1092,6 +1176,7 @@ export interface SendForwardResult {
1092
1176
  ok: boolean
1093
1177
  native: boolean
1094
1178
  messageId?: string
1179
+ channelId?: string
1095
1180
  }
1096
1181
 
1097
1182
  function extractFirstMessageId(data: unknown): string | undefined {
@@ -1102,7 +1187,56 @@ function extractFirstMessageId(data: unknown): string | undefined {
1102
1187
  ? root.data as unknown[]
1103
1188
  : []
1104
1189
  const first = list.length > 0 ? getObject(list[0]) : root
1105
- return getString(first.id) || getString(first.message_id) || undefined
1190
+ const id = first.id ?? first.message_id
1191
+ return id == null ? undefined : String(id)
1192
+ }
1193
+
1194
+ function extractDirectChannelId(data: unknown): string {
1195
+ const root = getObject(data)
1196
+ const nested = getObject(root.data)
1197
+ const channel = getObject(root.channel)
1198
+ const id = getString(channel.id)
1199
+ || getString(nested.id)
1200
+ || getString(root.id)
1201
+ if (!id) throw new Error('user.channel.create 未返回频道 ID')
1202
+ return id
1203
+ }
1204
+
1205
+ function getPrivateSendUserId(action: string, params: Record<string, unknown>): string {
1206
+ const normalized = action.replace(/^_/, '')
1207
+ const userId = params.user_id == null ? '' : String(params.user_id)
1208
+ const groupId = params.group_id == null ? '' : String(params.group_id)
1209
+ const guildId = params.guild_id == null ? '' : String(params.guild_id)
1210
+ if (normalized === 'send_private_msg') return userId
1211
+ if (normalized === 'send_msg'
1212
+ && userId
1213
+ && !groupId
1214
+ && !guildId) {
1215
+ return userId
1216
+ }
1217
+ return ''
1218
+ }
1219
+
1220
+ async function sendPrivateSatoriMessage(
1221
+ userId: string,
1222
+ content: string,
1223
+ guildId: string | undefined,
1224
+ active: ReturnType<typeof getActiveBot>,
1225
+ channelIdHint?: string,
1226
+ ): Promise<{ data: unknown; channelId: string }> {
1227
+ // 不构造前缀,优先透传前端已有的完整频道 ID
1228
+ let channelId = channelIdHint || userId
1229
+ try {
1230
+ const direct = await request('user.channel.create', { user_id: userId, guild_id: guildId }, active)
1231
+ channelId = extractDirectChannelId(direct)
1232
+ } catch {
1233
+ // 兼容未实现 createDirectChannel 的适配器,无完整频道 ID 时退回 private: 前缀私聊
1234
+ channelId = channelIdHint || (/^(?:private|direct):/i.test(userId) ? userId : `private:${userId}`)
1235
+ }
1236
+ return {
1237
+ data: await request('message.create', { channel_id: channelId, content }, active),
1238
+ channelId,
1239
+ }
1106
1240
  }
1107
1241
 
1108
1242
  export async function sendForwardMessage(
@@ -1111,29 +1245,33 @@ export async function sendForwardMessage(
1111
1245
  type: 'group' | 'user',
1112
1246
  id: string,
1113
1247
  messages: unknown[],
1248
+ channelIdHint?: string,
1114
1249
  ): Promise<SendForwardResult> {
1115
1250
  if (!platform || !selfId || !id || messages.length === 0) {
1116
1251
  return { ok: false, native: false }
1117
1252
  }
1118
1253
  // 优先走 Satori 原生合并转发,避免依赖 onebot 专属的 send-forward API
1119
1254
  if (platform) {
1120
- const channelId = type === 'group'
1121
- ? /^(?:group|room|chat|channel|guild):/i.test(id)
1122
- ? id
1123
- : platform === 'yunhu'
1124
- ? `group:${id}`
1125
- : id
1126
- : /^(?:private|direct):/i.test(id)
1127
- ? id
1128
- : `private:${id}`
1255
+ // Satori ID 是完整 string,调用方传什么就原样发什么
1256
+ const channelId = channelIdHint || id
1129
1257
  const content = buildForwardMessage(messages)
1130
1258
  if (content) {
1131
1259
  try {
1132
- const data = await request('message.create', { channel_id: channelId, content }, { platform, selfId })
1260
+ let finalChannelId = channelId
1261
+ if (type === 'user') {
1262
+ try {
1263
+ const direct = await request('user.channel.create', { user_id: id }, { platform, selfId })
1264
+ finalChannelId = extractDirectChannelId(direct)
1265
+ } catch {
1266
+ finalChannelId = channelIdHint || (/^(?:private|direct):/i.test(id) ? id : `private:${id}`)
1267
+ }
1268
+ }
1269
+ const data = await request('message.create', { channel_id: finalChannelId, content }, { platform, selfId })
1133
1270
  return {
1134
1271
  ok: true,
1135
1272
  native: true,
1136
1273
  messageId: extractFirstMessageId(data),
1274
+ channelId: finalChannelId,
1137
1275
  }
1138
1276
  } catch {
1139
1277
  // 原生路径失败时回退到旧 API,方便兼容尚未热重载的 Koishi 后端
@@ -1197,10 +1335,8 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1197
1335
  const sentAt = Number(obj.sentAt ?? Date.now())
1198
1336
  const messageId = getString(obj.messageId)
1199
1337
  const localId = getString(obj.id) || `self-${sentAt}`
1200
- const channelType = getString(obj.channelType) === 'user'
1201
- || /^(?:private|direct):/i.test(channelId)
1202
- ? 'user'
1203
- : 'group'
1338
+ // 只按记录里的显式 channelType 判断,不根据频道 ID 前缀推断
1339
+ const channelType = getString(obj.channelType) === 'user' ? 'user' : 'group'
1204
1340
  if (!platform || !selfId || !channelId) return null
1205
1341
 
1206
1342
  const login = getLogins().find((item) => {
@@ -1251,7 +1387,7 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1251
1387
  if (message.length === 0) return null
1252
1388
 
1253
1389
  const groupId = channelType === 'group' ? normalizeGroupId(channelId) : ''
1254
- const userId = channelType === 'user' ? channelId.replace(/^(?:private|direct):/i, '') : ''
1390
+ const userId = channelType === 'user' ? channelId : ''
1255
1391
  const guildId = getString(obj.guildId)
1256
1392
  const fallbackId = messageId || `self-${localId}`
1257
1393
  const rawMessage = getString(obj.content) || getMsgRawTxt({ message })
@@ -1288,9 +1424,26 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1288
1424
  },
1289
1425
  _from_self_cache: true,
1290
1426
  }
1427
+ if (obj.revoked === true) {
1428
+ msg.revoke = true
1429
+ msg.revoked = true
1430
+ msg.revokeTime = Number(obj.revokedAt) || Date.now() / 1000
1431
+ }
1291
1432
  return msg
1292
1433
  }
1293
1434
 
1435
+ export async function clearAllCache(): Promise<boolean> {
1436
+ try {
1437
+ const info = await getBootstrap()
1438
+ const response = await fetch(`${location.origin}${info.basePath}/api/clear-all`, {
1439
+ method: 'POST',
1440
+ })
1441
+ return response.ok
1442
+ } catch {
1443
+ return false
1444
+ }
1445
+ }
1446
+
1294
1447
  export async function loadChatHistoryFromCache(params: {
1295
1448
  platform: string
1296
1449
  selfId: string
@@ -1336,6 +1489,11 @@ export async function loadChatHistoryFromCache(params: {
1336
1489
  target_id: getString(msg.target_id),
1337
1490
  sender: userId,
1338
1491
  }
1492
+ if (recordObj.revoked === true) {
1493
+ msg.revoke = true
1494
+ msg.revoked = true
1495
+ msg.revokeTime = Number(recordObj.revokedAt) || Date.now() / 1000
1496
+ }
1339
1497
  messages.push(msg)
1340
1498
  }
1341
1499
  const existingIds = new Set(messages.map((item) => getString(item.message_id)))
@@ -1406,9 +1564,9 @@ async function loadAllBotsCache() {
1406
1564
  }
1407
1565
  const userList = [...groupRaws, ...friendRaws] as unknown as (UserFriendElem & UserGroupElem)[]
1408
1566
 
1409
- const state = contactStore.botStates.get(selfId)
1567
+ const state = contactStore.botStates.get(botKey(platform, selfId))
1410
1568
  if (!state || state.userList.length === 0) {
1411
- contactStore.botStates.set(selfId, {
1569
+ contactStore.botStates.set(botKey(platform, selfId), {
1412
1570
  userList,
1413
1571
  baseList: state?.baseList ?? [],
1414
1572
  onMsgList: state?.onMsgList ?? [],
@@ -1469,6 +1627,7 @@ function buildFriendItem(raw: unknown, data: unknown): Record<string, unknown> {
1469
1627
  const item: Record<string, unknown> = { ...getObject(raw) }
1470
1628
  if (!item.channel_id && item.channelId) item.channel_id = item.channelId
1471
1629
  if (!item.guild_id && item.guildId) item.guild_id = item.guildId
1630
+ if (getString(item.guild_id) || getString(item.guildId)) item._groupMember = true
1472
1631
  if (name) {
1473
1632
  item.nickname = name
1474
1633
  item.remark = name
@@ -1978,6 +2137,20 @@ export class Connector {
1978
2137
  const mapped = mapAction(api, args)
1979
2138
  if (!mapped) return undefined
1980
2139
  const active = getActiveBot()
2140
+ const privateUserId = getPrivateSendUserId(api, args)
2141
+ if (privateUserId) {
2142
+ const result = await sendPrivateSatoriMessage(
2143
+ privateUserId,
2144
+ getString(mapped.params.content),
2145
+ getString(args.group_id) || getString(args.guild_id) || undefined,
2146
+ active,
2147
+ getString(mapped.params.channel_id) || getString(args.channel_id),
2148
+ )
2149
+ const response = satoriResponseToOneBot(api, result.data, active.platform)
2150
+ response.channel_id = result.channelId
2151
+ response.channelId = result.channelId
2152
+ return response.data
2153
+ }
1981
2154
  const data = await request(mapped.method, mapped.params, active)
1982
2155
  return satoriResponseToOneBot(api, data, active.platform).data
1983
2156
  }
@@ -1989,6 +2162,26 @@ export class Connector {
1989
2162
  return
1990
2163
  }
1991
2164
  const active = getActiveBot()
2165
+ const privateUserId = getPrivateSendUserId(action, value)
2166
+ if (privateUserId) {
2167
+ void sendPrivateSatoriMessage(
2168
+ privateUserId,
2169
+ getString(mapped.params.content),
2170
+ getString(value.group_id) || getString(value.guild_id) || undefined,
2171
+ active,
2172
+ getString(mapped.params.channel_id) || getString(value.channel_id),
2173
+ )
2174
+ .then((result) => {
2175
+ const response = satoriResponseToOneBot(action, result.data, active.platform)
2176
+ response.channel_id = result.channelId
2177
+ response.channelId = result.channelId
2178
+ dispatch(response, echo)
2179
+ })
2180
+ .catch((error: unknown) => {
2181
+ dispatch({ retcode: -1, data: null, error: String(error) }, echo)
2182
+ })
2183
+ return
2184
+ }
1992
2185
  if (active && (action === 'get_friend_list' || action === 'get_group_list')) {
1993
2186
  const type = action === 'get_friend_list' ? 'friend' : 'group'
1994
2187
  void loadContactType(active, { type, action, echo })
@@ -43,6 +43,7 @@ export interface UserElem {
43
43
  always_top?: boolean
44
44
  message_id?: string
45
45
  highlight?: string
46
+ _groupMember?: boolean
46
47
  }
47
48
 
48
49
  export interface UserFriendElem extends UserElem {