koishi-plugin-chat-patch 5.3.1 → 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 (65) hide show
  1. package/client/web/dist/assets/Chat-Clqxn7CB.js +28 -0
  2. package/client/web/dist/assets/{Chat-C-rmzMIw.css → Chat-DTEsebnY.css} +1 -1
  3. package/client/web/dist/assets/{MsgBody-h4C1nnBQ.js → MsgBody-C0TKsXhC.js} +1 -1
  4. package/client/web/dist/assets/{MsgBody.vue_vue_type_script_setup_true_lang-a902ps2r.js → MsgBody.vue_vue_type_script_setup_true_lang-CCdBwlN_.js} +1 -1
  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-CdTmWcO4.js → index-BA37EQGa.js} +96 -75
  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 +5 -3
  28. package/client/web/src/components/DepPan.vue +1 -1
  29. package/client/web/src/components/MsgBody.vue +0 -3
  30. package/client/web/src/components/ThemeColorPickerPan.vue +1 -1
  31. package/client/web/src/components/UmamiInfoPan.vue +1 -1
  32. package/client/web/src/components/WelPan.vue +1 -1
  33. package/client/web/src/function/connect.ts +232 -72
  34. package/client/web/src/function/elements/information.ts +1 -0
  35. package/client/web/src/function/msg.ts +42 -23
  36. package/client/web/src/function/satori-model.ts +27 -13
  37. package/client/web/src/function/satori.ts +2 -1
  38. package/client/web/src/function/utils/appUtil.ts +3 -11
  39. package/client/web/src/function/utils/localHistoryUtil.ts +36 -12
  40. package/client/web/src/function/utils/msgUtil.ts +32 -21
  41. package/client/web/src/function/utils/sessionUtil.ts +20 -18
  42. package/client/web/src/pages/Chat.vue +47 -43
  43. package/client/web/src/pages/Friends.vue +5 -7
  44. package/client/web/src/pages/Info.vue +2 -2
  45. package/client/web/src/pages/Messages.vue +37 -22
  46. package/client/web/src/pages/chat-view/ChatTerminal.vue +4 -1
  47. package/client/web/src/pages/options/OptDev.vue +45 -2
  48. package/lib/database.d.ts +58 -0
  49. package/lib/index.js +220 -68
  50. package/lib/media.d.ts +24 -0
  51. package/lib/recorder.d.ts +21 -0
  52. package/lib/self-message.d.ts +27 -0
  53. package/lib/types.d.ts +6 -0
  54. package/lib/web.d.ts +7 -0
  55. package/package.json +1 -1
  56. package/src/cache.ts +1 -5
  57. package/src/database.ts +52 -8
  58. package/src/index.ts +4 -4
  59. package/src/media.ts +97 -0
  60. package/src/recorder.ts +28 -19
  61. package/src/self-message.ts +68 -19
  62. package/src/types.ts +6 -0
  63. package/src/web.ts +49 -39
  64. package/client/web/dist/assets/Chat-Bo5JdcNg.js +0 -28
  65. package/client/web/dist/assets/index-wjp_A4n1.css +0 -1
@@ -31,6 +31,7 @@ import {
31
31
  getSessionDedupKey,
32
32
  normalizeSessionId,
33
33
  setSessionContact,
34
+ upsertSessionContact,
34
35
  } from './utils/sessionUtil'
35
36
  import { buildQqGroupAvatar } from './utils/avatarUtil'
36
37
  import type { ConnectionHistoryItem, LoginCacheElem } from './elements/system'
@@ -504,7 +505,7 @@ async function fetchUserIdentity(
504
505
  const guildId = getString(msg.guild_id) || getString(msg.group_id)
505
506
  const isPrivate = isPrivateMessage(msg)
506
507
  const channelId = isPrivate
507
- ? (getString(msg.channel_id) || `private:${userId}`)
508
+ ? (getString(msg.channel_id) || userId)
508
509
  : ''
509
510
  const sender = getObject(msg.sender)
510
511
  const senderName = getString(sender.nickname) || getString(sender.card) || getString(sender.name)
@@ -555,10 +556,11 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
555
556
  })
556
557
  const isGroup = isGroupMessage(msg)
557
558
  const channelId = typeof msg.channel_id === 'string' ? msg.channel_id : ''
559
+ const guildId = typeof msg.guild_id === 'string' ? msg.guild_id : ''
558
560
  const sessionId = String(
559
- msg.group_id
560
- || msg.user_id
561
- || (isGroup ? '' : channelId.replace(/^private:/, '')),
561
+ isGroup
562
+ ? (msg.group_id || channelId || guildId || '')
563
+ : (msg.user_id || channelId || ''),
562
564
  )
563
565
  if (!sessionId || sessionId === '0') {
564
566
  identityDebug('recordBotMessage: skipped invalid session', { sessionId, msg })
@@ -571,7 +573,8 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
571
573
  const senderId = String(
572
574
  sender.user_id
573
575
  || msg.user_id
574
- || (isGroup ? '' : channelId.replace(/^private:/, '')),
576
+ || channelId
577
+ || '',
575
578
  )
576
579
  const raw = getMsgRawTxt(msg) || (typeof msg.raw_message === 'string' ? msg.raw_message : '')
577
580
  const senderName = String(sender.card || sender.nickname || (isGroup ? senderId : ''))
@@ -611,60 +614,108 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
611
614
  channel_id: typeof msg.channel_id === 'string' ? msg.channel_id : undefined,
612
615
  guild_id: typeof msg.guild_id === 'string' ? msg.guild_id : undefined,
613
616
  }
614
- const existing = state.onMsgList.find((item) => {
615
- const itemChannelId = String(item.channel_id || item.channelId || '')
616
- if (itemChannelId) {
617
- 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) {
618
644
  return normalizeGroupId(itemChannelId) === normalizeGroupId(channelId || sessionId)
619
645
  }
620
- return itemChannelId.replace(/^(?:private|direct):/i, '')
621
- === String(sessionId).replace(/^(?:private|direct):/i, '')
622
- }
623
- return isGroup
624
- ? String(item.group_id) === sessionId
625
- : String(item.user_id) === sessionId
626
- })
627
- if (existing) {
628
- const previousAvatar = existing.avatar
629
- const previousGroupName = existing.group_name
630
- const previousNickname = existing.nickname
631
- const previousRemark = existing.remark
632
- Object.assign(existing, session)
633
- if (!hasUsableAvatar(session.avatar) && hasUsableAvatar(previousAvatar)) {
634
- 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)
635
665
  }
636
- if (
637
- isGroup &&
638
- !msg.group_name &&
639
- previousGroupName &&
640
- String(previousGroupName) !== String(sessionId)
641
- ) {
642
- 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])
643
672
  }
644
- if (!isGroup && !sender.nickname && previousNickname) {
645
- 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)
646
699
  }
647
- if (!isGroup && !existing.remark && previousRemark) {
648
- 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])
649
706
  }
650
- session = existing
651
- } else {
652
- state.onMsgList.unshift(session)
653
- }
654
- const baseKey = normalizeSessionId(sessionKey)
655
- const baseList = [...state.baseList]
656
- const baseIndex = baseList.findIndex(([, value]) => value === session)
657
- const typedSession = session as unknown as UserFriendElem & UserGroupElem
658
- if (baseIndex >= 0) {
659
- baseList[baseIndex] = [baseKey, typedSession]
660
- } else {
661
- baseList.push([baseKey, typedSession])
662
707
  }
663
708
  contactStore.botStates.set(botKey(platform, selfId), {
664
709
  userList: [...state.userList],
665
710
  baseList,
666
711
  onMsgList: [...state.onMsgList],
667
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
+ }
668
719
  if (
669
720
  isActiveBot({ platform, selfId }) &&
670
721
  !contactStore.baseOnMsgList.has(normalizeSessionId(sessionKey))
@@ -674,7 +725,7 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
674
725
  }
675
726
 
676
727
  const chatStore = useChatStore()
677
- const cacheKey = [platform, selfId, sessionId].map(encodeKeyPart).join(':')
728
+ const cacheKey = [platform, selfId, sessionKey].map(encodeKeyPart).join(':')
678
729
  const messages = chatStore.sessionMessageCache.get(cacheKey) ?? []
679
730
  const messageId = String(msg.message_id ?? '')
680
731
  if (messageId && !messages.some((item) => String(item.message_id) === messageId)) {
@@ -778,7 +829,9 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
778
829
  const msg = messages[messages.length - 1]
779
830
  if (!msg) continue
780
831
  const isGroup = isGroupMessage(msg)
781
- 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))
782
835
  if (!id || id === '0') continue
783
836
  const sender = getObject(msg.sender)
784
837
  const senderId = getString(sender.user_id) || getString(msg.user_id) || ''
@@ -818,23 +871,23 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
818
871
  const baseMap = new Map<string, UserFriendElem & UserGroupElem>()
819
872
  for (const [, value] of state.baseList) {
820
873
  const id = getSessionDedupKey(value)
821
- if (!id.endsWith(':0') && !id.endsWith(':')) baseMap.set(id, value)
874
+ if (id !== '0' && id !== '') baseMap.set(id, value)
822
875
  }
823
876
  const baseList = [...baseMap.entries()]
824
877
  const baseKeys = new Set(baseList.map(([key]) => key))
825
878
  const existing = new Set(
826
879
  state.onMsgList.map((item) => {
827
880
  const id = getSessionDedupKey(item)
828
- return !id.endsWith(':0') && !id.endsWith(':') ? id : ''
881
+ return id !== '0' && id !== '' ? id : ''
829
882
  }).filter(Boolean),
830
883
  )
831
884
  for (const session of sessions) {
832
885
  const id = getSessionDedupKey(session)
833
- if (!id.endsWith(':0') && !id.endsWith(':') && !existing.has(id)) {
886
+ if (id !== '0' && id !== '' && !existing.has(id)) {
834
887
  state.onMsgList.unshift(session)
835
888
  existing.add(id)
836
889
  }
837
- if (!id.endsWith(':0') && !id.endsWith(':') && !baseKeys.has(id)) {
890
+ if (id !== '0' && id !== '' && !baseKeys.has(id)) {
838
891
  baseList.push([normalizeSessionId(String(session.channel_id || session.channelId || session.user_id || session.group_id || '')), session])
839
892
  baseKeys.add(id)
840
893
  }
@@ -977,9 +1030,7 @@ function contactId(item: unknown): string {
977
1030
  }
978
1031
 
979
1032
  function normalizeGroupId(value: string): string {
980
- const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, '').trim()
981
- const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/)
982
- return wrapped ? wrapped[1] : raw || value
1033
+ return String(value)
983
1034
  }
984
1035
 
985
1036
  function contactName(item: unknown): string {
@@ -1125,6 +1176,7 @@ export interface SendForwardResult {
1125
1176
  ok: boolean
1126
1177
  native: boolean
1127
1178
  messageId?: string
1179
+ channelId?: string
1128
1180
  }
1129
1181
 
1130
1182
  function extractFirstMessageId(data: unknown): string | undefined {
@@ -1135,7 +1187,56 @@ function extractFirstMessageId(data: unknown): string | undefined {
1135
1187
  ? root.data as unknown[]
1136
1188
  : []
1137
1189
  const first = list.length > 0 ? getObject(list[0]) : root
1138
- 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
+ }
1139
1240
  }
1140
1241
 
1141
1242
  export async function sendForwardMessage(
@@ -1144,29 +1245,33 @@ export async function sendForwardMessage(
1144
1245
  type: 'group' | 'user',
1145
1246
  id: string,
1146
1247
  messages: unknown[],
1248
+ channelIdHint?: string,
1147
1249
  ): Promise<SendForwardResult> {
1148
1250
  if (!platform || !selfId || !id || messages.length === 0) {
1149
1251
  return { ok: false, native: false }
1150
1252
  }
1151
1253
  // 优先走 Satori 原生合并转发,避免依赖 onebot 专属的 send-forward API
1152
1254
  if (platform) {
1153
- const channelId = type === 'group'
1154
- ? /^(?:group|room|chat|channel|guild):/i.test(id)
1155
- ? id
1156
- : platform === 'yunhu'
1157
- ? `group:${id}`
1158
- : id
1159
- : /^(?:private|direct):/i.test(id)
1160
- ? id
1161
- : `private:${id}`
1255
+ // Satori ID 是完整 string,调用方传什么就原样发什么
1256
+ const channelId = channelIdHint || id
1162
1257
  const content = buildForwardMessage(messages)
1163
1258
  if (content) {
1164
1259
  try {
1165
- 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 })
1166
1270
  return {
1167
1271
  ok: true,
1168
1272
  native: true,
1169
1273
  messageId: extractFirstMessageId(data),
1274
+ channelId: finalChannelId,
1170
1275
  }
1171
1276
  } catch {
1172
1277
  // 原生路径失败时回退到旧 API,方便兼容尚未热重载的 Koishi 后端
@@ -1230,10 +1335,8 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1230
1335
  const sentAt = Number(obj.sentAt ?? Date.now())
1231
1336
  const messageId = getString(obj.messageId)
1232
1337
  const localId = getString(obj.id) || `self-${sentAt}`
1233
- const channelType = getString(obj.channelType) === 'user'
1234
- || /^(?:private|direct):/i.test(channelId)
1235
- ? 'user'
1236
- : 'group'
1338
+ // 只按记录里的显式 channelType 判断,不根据频道 ID 前缀推断
1339
+ const channelType = getString(obj.channelType) === 'user' ? 'user' : 'group'
1237
1340
  if (!platform || !selfId || !channelId) return null
1238
1341
 
1239
1342
  const login = getLogins().find((item) => {
@@ -1284,7 +1387,7 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1284
1387
  if (message.length === 0) return null
1285
1388
 
1286
1389
  const groupId = channelType === 'group' ? normalizeGroupId(channelId) : ''
1287
- const userId = channelType === 'user' ? channelId.replace(/^(?:private|direct):/i, '') : ''
1390
+ const userId = channelType === 'user' ? channelId : ''
1288
1391
  const guildId = getString(obj.guildId)
1289
1392
  const fallbackId = messageId || `self-${localId}`
1290
1393
  const rawMessage = getString(obj.content) || getMsgRawTxt({ message })
@@ -1321,9 +1424,26 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1321
1424
  },
1322
1425
  _from_self_cache: true,
1323
1426
  }
1427
+ if (obj.revoked === true) {
1428
+ msg.revoke = true
1429
+ msg.revoked = true
1430
+ msg.revokeTime = Number(obj.revokedAt) || Date.now() / 1000
1431
+ }
1324
1432
  return msg
1325
1433
  }
1326
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
+
1327
1447
  export async function loadChatHistoryFromCache(params: {
1328
1448
  platform: string
1329
1449
  selfId: string
@@ -1369,6 +1489,11 @@ export async function loadChatHistoryFromCache(params: {
1369
1489
  target_id: getString(msg.target_id),
1370
1490
  sender: userId,
1371
1491
  }
1492
+ if (recordObj.revoked === true) {
1493
+ msg.revoke = true
1494
+ msg.revoked = true
1495
+ msg.revokeTime = Number(recordObj.revokedAt) || Date.now() / 1000
1496
+ }
1372
1497
  messages.push(msg)
1373
1498
  }
1374
1499
  const existingIds = new Set(messages.map((item) => getString(item.message_id)))
@@ -1502,6 +1627,7 @@ function buildFriendItem(raw: unknown, data: unknown): Record<string, unknown> {
1502
1627
  const item: Record<string, unknown> = { ...getObject(raw) }
1503
1628
  if (!item.channel_id && item.channelId) item.channel_id = item.channelId
1504
1629
  if (!item.guild_id && item.guildId) item.guild_id = item.guildId
1630
+ if (getString(item.guild_id) || getString(item.guildId)) item._groupMember = true
1505
1631
  if (name) {
1506
1632
  item.nickname = name
1507
1633
  item.remark = name
@@ -2011,6 +2137,20 @@ export class Connector {
2011
2137
  const mapped = mapAction(api, args)
2012
2138
  if (!mapped) return undefined
2013
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
+ }
2014
2154
  const data = await request(mapped.method, mapped.params, active)
2015
2155
  return satoriResponseToOneBot(api, data, active.platform).data
2016
2156
  }
@@ -2022,6 +2162,26 @@ export class Connector {
2022
2162
  return
2023
2163
  }
2024
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
+ }
2025
2185
  if (active && (action === 'get_friend_list' || action === 'get_group_list')) {
2026
2186
  const type = action === 'get_friend_list' ? 'friend' : 'group'
2027
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 {
@@ -95,6 +95,7 @@ import {
95
95
  normalizeSessionId,
96
96
  resolveIncomingSession,
97
97
  setSessionContact,
98
+ upsertSessionContact,
98
99
  } from './utils/sessionUtil'
99
100
 
100
101
  const popInfo = new PopInfo()
@@ -120,11 +121,6 @@ function getString(value: unknown): string {
120
121
  return typeof value === 'string' ? value : String(value)
121
122
  }
122
123
 
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
-
128
124
  // 其他 tag
129
125
  let listLoadTimes = 0
130
126
  const logger = new Logger()
@@ -995,10 +991,13 @@ const msgFunctions = {
995
991
  const chatStore = useChatStore()
996
992
  const rawData = msg.data
997
993
  const firstData = Array.isArray(rawData) ? rawData[0] : rawData
994
+ const firstRawId = getDataObject(firstData).id
995
+ ?? getDataObject(rawData).id
998
996
  const realMessageId = getString(msg.message_id)
999
997
  || getString(getDataObject(rawData).message_id)
1000
998
  || getString(getDataObject(firstData).id)
1001
999
  || getString(getDataObject(firstData).message_id)
1000
+ || (firstRawId == null ? '' : String(firstRawId))
1002
1001
  if (realMessageId) msg.message_id = realMessageId
1003
1002
  if (echoList[1] == 'forward') {
1004
1003
  // PS:这儿写是写了转发成功,事实上不确定消息有没有真的发送出去(x
@@ -1341,8 +1340,16 @@ const msgFunctions = {
1341
1340
  )
1342
1341
  getMessageList(trueMsg).then((trueMsg) => {
1343
1342
  if (trueMsg?.length == 1) {
1343
+ const nextMessage = Array.isArray(trueMsg[0].message)
1344
+ && trueMsg[0].message.some((item: any) => {
1345
+ if (item?.type !== 'text') return true
1346
+ const text = String(item?.text ?? item?.content ?? '')
1347
+ return text.trim() !== ''
1348
+ })
1349
+ ? trueMsg[0].message
1350
+ : fakeMsg.message
1344
1351
  // 使用消息对象引用直接更新,避免索引问题
1345
- fakeMsg.message = trueMsg[0].message
1352
+ if (Array.isArray(nextMessage)) fakeMsg.message = nextMessage
1346
1353
  fakeMsg.raw_message = trueMsg[0].raw_message
1347
1354
  fakeMsg.time = trueMsg[0].time
1348
1355
  fakeMsg.fake_msg = undefined
@@ -1699,8 +1706,9 @@ function saveUser(msg: { [key: string]: any }, type: string) {
1699
1706
  const channel = String(item.channel_id ?? item.channelId ?? '')
1700
1707
  const group = String(item.group_id ?? '')
1701
1708
  const user = String(item.user_id ?? '')
1702
- if (group) return `group:${normalizeGroupId(channel || group)}`
1703
- return `user:${String(user).replace(/^(?:private|direct):/i, '')}`
1709
+ if (channel) return normalizeSessionId(channel)
1710
+ if (group) return normalizeSessionId(group)
1711
+ return normalizeSessionId(user)
1704
1712
  }
1705
1713
  const existingIds = new Set(contactStore.userList.map(sessionDedupKey))
1706
1714
  const freshList = list.filter((item) => {
@@ -1891,19 +1899,28 @@ async function saveMsg(msg: any, append = undefined as undefined | string) {
1891
1899
  const lastMsg =
1892
1900
  chatStore.messageList[chatStore.messageList.length - 1]
1893
1901
  if (lastMsg) {
1902
+ const isGroup = chatStore.chatInfo.show.type === 'group'
1894
1903
  const user = findSessionContact(contactStore.userList, chatStore.chatInfo.show.id)
1895
1904
  const sessionId = normalizeSessionId(
1896
1905
  chatStore.chatInfo.show.channel_id
1897
1906
  ?? chatStore.chatInfo.show.id,
1898
1907
  )
1899
- const session = contactStore.baseOnMsgList.get(sessionId) ?? user
1908
+ const session = isGroup
1909
+ ? user
1910
+ : contactStore.baseOnMsgList.get(sessionId) ?? user
1900
1911
  if (session) {
1901
1912
  const preview = formatMessageData(
1902
1913
  lastMsg,
1903
- chatStore.chatInfo.show.type == 'group',
1914
+ isGroup,
1904
1915
  )
1905
1916
  if (user) Object.assign(user, preview)
1906
1917
  Object.assign(session, preview)
1918
+ if (isGroup) {
1919
+ // 群历史同步到联系人列表,同时进入群会话列表
1920
+ contactStore.userList = upsertSessionContact(contactStore.userList, session)
1921
+ } else if (session._groupMember) {
1922
+ session._groupMember = false
1923
+ }
1907
1924
  contactStore.baseOnMsgList.set(sessionId, session)
1908
1925
  updateBaseOnMsgList()
1909
1926
  }
@@ -2326,7 +2343,7 @@ function newMsg(_: string, data: any) {
2326
2343
  }
2327
2344
  // 消息基础信息:优先使用 Satori 转换层生成的 infoList,避免 OneBot pathMap 误解析。
2328
2345
  const rawInfo = getDataObject(data.infoList ?? data)
2329
- const id = getString(rawInfo.group_id) ||
2346
+ const rawId = getString(rawInfo.group_id) ||
2330
2347
  getString(rawInfo.private_id) ||
2331
2348
  getString(data.group_id) ||
2332
2349
  getString(data.user_id) ||
@@ -2354,12 +2371,16 @@ function newMsg(_: string, data: any) {
2354
2371
  : data.message_type === 'group' || Boolean(data.group_id) || Boolean(rawInfo.group_id)
2355
2372
  const sessionChannelId = getString(rawInfo.channel_id) || getString(data.channel_id)
2356
2373
  const sessionGuildId = getString(rawInfo.guild_id) || getString(data.guild_id)
2374
+ // 群消息必须用群/频道 ID 作为会话,不能退化成发送者用户 ID
2375
+ const id = isGroupMessage
2376
+ ? getString(rawInfo.group_id) || getString(data.group_id) || sessionChannelId || sessionGuildId || ''
2377
+ : rawId
2357
2378
  if (!id || id === '0') return
2358
2379
  const isCurrentSession = Boolean(
2359
2380
  (
2360
2381
  sessionChannelId &&
2361
2382
  showChannelId &&
2362
- normalizeChannelForMatch(sessionChannelId) === normalizeChannelForMatch(showChannelId)
2383
+ sessionChannelId === showChannelId
2363
2384
  ) || (
2364
2385
  (!sessionChannelId || !showChannelId) &&
2365
2386
  (id === showId || targetId === showId || String(sessionChannelId || sessionGuildId || '') === showId)
@@ -2572,6 +2593,13 @@ function newMsg(_: string, data: any) {
2572
2593
  if (data.atall) { session.highlight = $t('[@全体]') }
2573
2594
  if (isImportant) { session.highlight = $t('[特別关心]') }
2574
2595
  }
2596
+ if (!isGroupMessage && session._groupMember) {
2597
+ session._groupMember = false
2598
+ }
2599
+ if (isGroupMessage) {
2600
+ // 群消息同步到联系人列表,同时进入群会话列表
2601
+ contactStore.userList = upsertSessionContact(contactStore.userList, session)
2602
+ }
2575
2603
  setSessionContact(contactStore.baseOnMsgList, session)
2576
2604
  updateBaseOnMsgList()
2577
2605
  }
@@ -2589,17 +2617,8 @@ function newMsg(_: string, data: any) {
2589
2617
  hidden: document.hidden,
2590
2618
  isImportant: isImportant
2591
2619
  })
2592
- // (发送者没有被打开 || 窗口没有焦点 || 窗口被最小化 || 在特别关心列表里) 这些情况需要进行消息通知
2593
- const forceGroupSystemNotice = isGroupMessage && groupNoticeType === 'all'
2594
- const forceImportantNotice = isImportant ||
2595
- (isGroupMessage && (data.atme || data.atall || isGroupNotice))
2596
- if (
2597
- forceGroupSystemNotice ||
2598
- forceImportantNotice ||
2599
- !isCurrentSession ||
2600
- !document.hasFocus() ||
2601
- document.hidden
2602
- ) {
2620
+ // 当前会话已打开时不弹系统通知,新消息交给右下角置底箭头提示
2621
+ if (!isCurrentSession) {
2603
2622
  // 准备消息内容
2604
2623
  let raw = getMsgRawTxt(data)
2605
2624
  raw = raw === '' ? data.raw_message : raw