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
@@ -21,9 +21,7 @@ function getString(value: unknown): string {
21
21
  }
22
22
 
23
23
  function normalizeGroupId(value: string): string {
24
- const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, '').trim()
25
- const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/)
26
- return wrapped ? wrapped[1] : raw || value
24
+ return String(value)
27
25
  }
28
26
 
29
27
  function isDirectChannelType(channel: SatoriObject): boolean {
@@ -449,11 +447,11 @@ export function satoriEventToOneBot(
449
447
  if (type === 'message' || type === 'message-created' || type === 'send') {
450
448
  const isGroup = isGroupChannel(channel, guild)
451
449
  const userId = getString(user.id)
452
- || (isGroup ? '' : getString(channel.id).replace(/^private:/, ''))
450
+ || (isGroup ? '' : getString(channel.id))
453
451
  const groupId = isGroup
454
452
  ? normalizeGroupId(getString(guild.id)) || normalizeGroupId(getString(channel.id))
455
453
  : ''
456
- const directChannelId = isGroup ? groupId : userId
454
+ const directChannelId = isGroup ? groupId : getString(channel.id) || userId
457
455
  const nickname = getString(user.name) || getString(user.nick) || userId
458
456
  const card = getString(member.nick) || getString(member.name) || ''
459
457
  return {
@@ -470,7 +468,7 @@ export function satoriEventToOneBot(
470
468
  group_avatar: isGroup
471
469
  ? (usableAvatar(guild.avatar) || usableAvatar(channel.avatar) || buildQqGroupAvatar(platform, groupId) || undefined)
472
470
  : undefined,
473
- channel_id: getString(channel.id) || (isGroup ? groupId : `private:${userId}`),
471
+ channel_id: getString(channel.id) || (isGroup ? groupId : userId),
474
472
  guild_id: getString(guild.id),
475
473
  target_id: selfId,
476
474
  message: messageSegments(message),
@@ -486,7 +484,7 @@ export function satoriEventToOneBot(
486
484
  message_id: getString(message.id),
487
485
  private_id: isGroup ? '' : directChannelId,
488
486
  group_id: groupId,
489
- channel_id: getString(channel.id) || (isGroup ? groupId : `private:${userId}`),
487
+ channel_id: getString(channel.id) || (isGroup ? groupId : userId),
490
488
  guild_id: getString(guild.id),
491
489
  target_id: selfId,
492
490
  sender: userId,
@@ -533,6 +531,24 @@ export function satoriEventToOneBot(
533
531
  }
534
532
  }
535
533
 
534
+ if (type === 'message-deleted') {
535
+ const isGroup = isGroupChannel(channel, guild)
536
+ const userId = getString(user.id) || (isGroup ? '' : getString(channel.id))
537
+ const groupId = isGroup
538
+ ? normalizeGroupId(getString(guild.id)) || normalizeGroupId(getString(channel.id))
539
+ : ''
540
+ return {
541
+ ...base,
542
+ post_type: 'notice',
543
+ notice_type: isGroup ? 'group_recall' : 'friend_recall',
544
+ group_id: groupId,
545
+ user_id: userId,
546
+ message_id: getString(message.id),
547
+ channel_id: getString(channel.id) || (isGroup ? groupId : userId),
548
+ guild_id: getString(guild.id),
549
+ }
550
+ }
551
+
536
552
  return null
537
553
  }
538
554
 
@@ -637,9 +653,7 @@ export function mapAction(action: string, params: Record<string, unknown>): {
637
653
  const guildId = getString(params.group_id) || getString(params.guild_id)
638
654
  const userId = getString(params.user_id)
639
655
  const messageId = getString(params.message_id)
640
- const directChannel = userId && !/^(?:group|room|chat|channel|guild|private):/i.test(userId)
641
- ? `private:${userId}`
642
- : userId
656
+ const directChannel = userId
643
657
  const channelId = getString(params.channel_id) || guildId || directChannel || userId
644
658
  const content = Array.isArray(params.message)
645
659
  ? serializeSatoriContent(params.message)
@@ -696,8 +710,8 @@ function messageListFromResponse(data: unknown): unknown[] {
696
710
  ? normalizeGroupId(getString(guild.id)) || normalizeGroupId(getString(channel.id))
697
711
  : ''
698
712
  const userId = getString(user.id)
699
- || (isGroup ? '' : getString(channel.id).replace(/^private:/, ''))
700
- const directChannelId = isGroup ? groupId : userId
713
+ || (isGroup ? '' : getString(channel.id))
714
+ const directChannelId = isGroup ? groupId : getString(channel.id) || userId
701
715
  return {
702
716
  message_id: getString(message.id),
703
717
  sn: getNumber(message.sn),
@@ -709,7 +723,7 @@ function messageListFromResponse(data: unknown): unknown[] {
709
723
  channel_type: channel.type,
710
724
  group_id: groupId,
711
725
  user_id: groupId ? userId : directChannelId,
712
- channel_id: getString(channel.id) || (isGroup ? groupId : `private:${userId}`),
726
+ channel_id: getString(channel.id) || (isGroup ? groupId : userId),
713
727
  guild_id: getString(guild.id),
714
728
  sender: {
715
729
  user_id: userId,
@@ -75,7 +75,8 @@ async function parseJsonResponse(response: Response): Promise<unknown> {
75
75
  try {
76
76
  return JSON.parse(text) as unknown
77
77
  } catch {
78
- throw new Error(`Satori API 返回了非 JSON 内容: ${text.slice(0, 120)}`)
78
+ // 部分平台对 delete 等操作只回显纯文本(例如 OK),HTTP 200 即视为成功
79
+ return text
79
80
  }
80
81
  }
81
82
 
@@ -53,11 +53,6 @@ function encodeKeyPart(value: unknown): string {
53
53
  return encodeURIComponent(String(value ?? ''))
54
54
  }
55
55
 
56
- function ensureChannelPrefix(channelId: string, prefix: string): string {
57
- if (/^(?:group|room|chat|channel|guild|private):/i.test(channelId)) return channelId
58
- return `${prefix}:${channelId}`
59
- }
60
-
61
56
  /**
62
57
  * 滚动到目标消息(不自动加载)
63
58
  * @param seqName DOM 名(chat-xx)
@@ -121,7 +116,8 @@ export async function loadHistory(info: BaseChatInfoElem) {
121
116
  uiStore.nowGetHistory = false
122
117
  uiStore.historyBeforeTime = undefined
123
118
  chatStore.messageList = []
124
- const cacheKey = [authStore.loginInfo.platform, authStore.loginInfo.uin, info.id].map(encodeKeyPart).join(':')
119
+ const channelId = String(info.channel_id ?? info.id ?? '')
120
+ const cacheKey = [authStore.loginInfo.platform, authStore.loginInfo.uin, channelId].map(encodeKeyPart).join(':')
125
121
  const cachedMessages = chatStore.sessionMessageCache.get(cacheKey)
126
122
  if (cachedMessages?.length) {
127
123
  chatStore.messageList = [...cachedMessages]
@@ -133,17 +129,13 @@ export async function loadHistory(info: BaseChatInfoElem) {
133
129
  ) {
134
130
  const localMsgs = await dbGetLatest(
135
131
  authStore.loginInfo.uin,
136
- info.id,
132
+ channelId,
137
133
  20,
138
134
  )
139
135
  if (localMsgs.length > 0) {
140
136
  chatStore.messageList = localMsgs
141
137
  }
142
138
  }
143
- const rawChannelId = String(info.channel_id ?? '')
144
- const channelId = info.type === 'group'
145
- ? ensureChannelPrefix(rawChannelId || String(info.id), 'group')
146
- : ensureChannelPrefix(rawChannelId || String(info.id), 'private')
147
139
  try {
148
140
  const cachedMessages = await loadChatHistoryFromCache({
149
141
  platform: String(authStore.loginInfo.platform ?? ''),
@@ -18,7 +18,7 @@ const logger = new Logger()
18
18
  export interface LocalMsgRecord {
19
19
  /** Bot 侧的消息 ID */
20
20
  message_id: string
21
- /** 会话 ID(group_id user_id) */
21
+ /** 会话 ID:保存完整 channel_id,不剥离前缀 */
22
22
  chat_id: number | string
23
23
  /** 会话类型:"group" | "private" */
24
24
  chat_type: string
@@ -115,18 +115,28 @@ function computeRawMessage(msg: any): string | null {
115
115
  }
116
116
  }
117
117
 
118
+ function firstDefinedId(value: any, keys: string[]): number | string | undefined {
119
+ for (const key of keys) {
120
+ const current = value?.[key]
121
+ if (current != null && String(current) !== '') return current
122
+ }
123
+ return undefined
124
+ }
125
+
118
126
  function deriveChatId(selfId: string | number, msgs: any[]): number | string | undefined {
119
127
  const firstMsg = msgs[0]
120
- let chatId: number | string | undefined = firstMsg?.infoList?.group_id ?? firstMsg?.infoList?.target_id
128
+ const firstInfo = firstMsg?.infoList ?? firstMsg
129
+ const chatId = firstDefinedId(firstInfo, ['channel_id', 'channelId', 'private_id', 'group_id', 'user_id'])
130
+ ?? firstDefinedId(firstMsg, ['channel_id', 'channelId', 'private_id', 'group_id', 'user_id'])
121
131
  if (chatId != null) return chatId
122
132
 
123
133
  for (const item of msgs) {
124
- if (item?.infoList?.sender != null && String(item.infoList.sender) !== String(selfId)) {
125
- chatId = item.infoList.sender
126
- break
134
+ const info = item?.infoList ?? item
135
+ if (info?.sender != null && String(info.sender) !== String(selfId)) {
136
+ return info.sender
127
137
  }
128
138
  }
129
- return chatId
139
+ return undefined
130
140
  }
131
141
 
132
142
  export function ensureChatIdOnMsgs(selfId: string | number, msgs: any[]): any[] {
@@ -135,14 +145,21 @@ export function ensureChatIdOnMsgs(selfId: string | number, msgs: any[]): any[]
135
145
 
136
146
  return msgs.map((item: any) => {
137
147
  if (!item?.infoList) return item
138
- if (item.infoList.group_id != null || item.infoList.target_id != null) {
148
+ if (
149
+ item.infoList.channel_id != null
150
+ || item.infoList.channelId != null
151
+ || item.infoList.private_id != null
152
+ || item.infoList.group_id != null
153
+ || item.infoList.user_id != null
154
+ ) {
139
155
  return item
140
156
  }
141
157
  return {
142
158
  ...item,
143
159
  infoList: {
144
160
  ...item.infoList,
145
- target_id: chatId,
161
+ channel_id: chatId,
162
+ channelId: chatId,
146
163
  },
147
164
  }
148
165
  })
@@ -158,12 +175,14 @@ export function msgToRecord(msg: any): LocalMsgRecord | null {
158
175
  const messageId = msg.message_id
159
176
  if (!messageId) return null
160
177
 
161
- const chatId: number | string = msg.infoList.group_id ?? msg.infoList.target_id
178
+ const info = msg.infoList ?? msg
179
+ const chatId = firstDefinedId(info, ['channel_id', 'channelId', 'private_id', 'group_id', 'user_id'])
180
+ ?? firstDefinedId(msg, ['channel_id', 'channelId', 'private_id', 'group_id', 'user_id'])
162
181
  if (chatId == null) return null
163
182
 
164
183
  const chatType: string =
165
184
  msg.message_type ?? (msg.group_id != null ? 'group' : 'private')
166
- const senderId: number | string = msg.infoList.sender
185
+ const senderId: number | string = info.sender ?? msg.sender?.user_id
167
186
  if (senderId == null) return null
168
187
 
169
188
  const senderName: string | null =
@@ -490,7 +509,8 @@ function deserializeRecord(record: LocalMsgRecord): any {
490
509
  message_id: record.message_id,
491
510
  private_id: isGroup ? undefined : record.chat_id,
492
511
  group_id: isGroup ? record.chat_id : undefined,
493
- target_id: isGroup ? undefined : record.chat_id,
512
+ channel_id: record.chat_id,
513
+ channelId: record.chat_id,
494
514
  sender: record.sender_id,
495
515
  }
496
516
 
@@ -499,7 +519,9 @@ function deserializeRecord(record: LocalMsgRecord): any {
499
519
  message_id: record.message_id,
500
520
  message_type: record.chat_type,
501
521
  // 根据 chat_type 恢复对应的 id 字段
502
- ...(isGroup ? { group_id: record.chat_id } : { user_id: record.chat_id }),
522
+ ...(isGroup ? { group_id: record.chat_id } : { user_id: record.sender_id }),
523
+ channel_id: record.chat_id,
524
+ channelId: record.chat_id,
503
525
  sender,
504
526
  time: record.time,
505
527
  timestamp_ms: timeMs || undefined,
@@ -509,6 +531,8 @@ function deserializeRecord(record: LocalMsgRecord): any {
509
531
  infoList,
510
532
  raw_message: record.raw_message ?? '',
511
533
  revoked: record.revoked,
534
+ revoke: record.revoked,
535
+ revokeTime: record.revoked ? Date.now() / 1000 : undefined,
512
536
  // 消息序列号(并非所有 Bot 都提供,可为 null)
513
537
  ...(record.seq != null ? { message_seq: record.seq, seq_id: record.seq } : {}),
514
538
  // 标记来源为本地缓存,业务层可按需用此字段区分
@@ -27,6 +27,7 @@ import {
27
27
  getSessionId,
28
28
  mergeSessionState,
29
29
  normalizeSessionId,
30
+ upsertSessionContact,
30
31
  } from './sessionUtil'
31
32
 
32
33
  const logger = new Logger()
@@ -681,21 +682,31 @@ export async function sendMsgRaw(
681
682
  chatStore.messageList = chatStore.messageList.concat([showMsg])
682
683
 
683
684
  // 发送方不一定会上报自身消息事件,先用预发送消息同步会话预览。
684
- const sessionId = normalizeSessionId(String(id).split('/')[0])
685
+ const sessionId = normalizeSessionId(
686
+ targetChannelId
687
+ || chatStore.chatInfo.show.channel_id
688
+ || String(id).split('/')[0],
689
+ )
685
690
  const session = contactStore.baseOnMsgList.get(sessionId) ??
686
691
  findSessionContact(contactStore.userList, sessionId)
687
692
  if (session) {
688
693
  const raw = getMsgRawTxt(showMsg)
689
694
  const senderName = authStore.loginInfo.nickname
690
- Object.assign(session, {
691
- message_id: showMsg.message_id,
692
- raw_msg: type === 'group' && senderName ? `${senderName}: ${raw}` : raw,
693
- raw_msg_base: raw,
694
- time: showMsg.time * 1000,
695
- })
696
- contactStore.baseOnMsgList.set(normalizeSessionId(sessionId), session)
697
- updateBaseOnMsgList()
698
- }
695
+ Object.assign(session, {
696
+ message_id: showMsg.message_id,
697
+ raw_msg: type === 'group' && senderName ? `${senderName}: ${raw}` : raw,
698
+ raw_msg_base: raw,
699
+ time: showMsg.time * 1000,
700
+ })
701
+ if (type === 'group') {
702
+ // 群发送预览同步到联系人列表,同时进入群会话列表
703
+ contactStore.userList = upsertSessionContact(contactStore.userList, session)
704
+ } else if (session._groupMember) {
705
+ session._groupMember = false
706
+ }
707
+ contactStore.baseOnMsgList.set(normalizeSessionId(sessionId), session)
708
+ updateBaseOnMsgList()
709
+ }
699
710
  }
700
711
  // 预发送保留 Base64 用于本地预览;真正发送前先写入后端临时目录
701
712
  try {
@@ -762,7 +773,7 @@ export async function sendMsgRaw(
762
773
  {
763
774
  user_id: id.split('/')[0],
764
775
  group_id: id.split('/')[1],
765
- channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ?? `private:${String(id).split('/')[0]}`),
776
+ channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ?? String(id).split('/')[0]),
766
777
  message: msg,
767
778
  },
768
779
  echo + '_uuid_' + msgUUID,
@@ -773,7 +784,7 @@ export async function sendMsgRaw(
773
784
  'send_msg',
774
785
  {
775
786
  user_id: id,
776
- channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ?? (/^(?:group|room|chat|channel|guild|private):/i.test(String(id)) ? String(id) : `private:${String(id)}`)),
787
+ channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ?? String(id)),
777
788
  message: msg,
778
789
  },
779
790
  echo + '_uuid_' + msgUUID,
@@ -827,7 +838,7 @@ function normalizeBaseSessionMap() {
827
838
  const seen = new Map<string, UserFriendElem & UserGroupElem>()
828
839
  for (const [rawKey, item] of contactStore.baseOnMsgList) {
829
840
  const id = getSessionDedupKey(item)
830
- if (id.endsWith(':0') || id.endsWith(':')) continue
841
+ if (id === '0' || id === '') continue
831
842
  const existing = seen.get(id)
832
843
  if (existing && existing !== item) {
833
844
  needNormalize = true
@@ -846,7 +857,7 @@ function normalizeBaseSessionMap() {
846
857
  const canonical = new Map<string, UserFriendElem & UserGroupElem>()
847
858
  contactStore.baseOnMsgList.forEach((item) => {
848
859
  const id = getSessionDedupKey(item)
849
- if (id === 'group:0' || id === 'group:' || id === 'user:0' || id === 'user:') return
860
+ if (id === '0' || id === '') return
850
861
  const existing = canonical.get(id)
851
862
  if (existing && existing !== item) {
852
863
  canonical.set(id, mergeSessionState(existing, item))
@@ -869,7 +880,7 @@ function getSessionList() {
869
880
 
870
881
  const addSession = (item: UserFriendElem & UserGroupElem) => {
871
882
  const id = getSessionDedupKey(item)
872
- if (id === 'group:0' || id === 'group:' || id === 'user:0' || id === 'user:') return
883
+ if (id === '0' || id === '') return
873
884
  const existing = sessionMap.get(id)
874
885
  if (existing && existing !== item) {
875
886
  sessionMap.set(id, mergeSessionState(existing, item))
@@ -880,7 +891,7 @@ function getSessionList() {
880
891
 
881
892
  if (settingsStore.sysConfig.session_display_mode === 'all') {
882
893
  contactStore.userList.forEach((item) => {
883
- addSession(item)
894
+ if (!item._groupMember) addSession(item)
884
895
  })
885
896
  }
886
897
 
@@ -894,11 +905,11 @@ function getSessionList() {
894
905
  /**
895
906
  * 刷新消息列表排序
896
907
  */
897
- export function updateBaseOnMsgList() {
898
- const contactStore = useContactStore()
899
- const settingsStore = useSettingsStore()
900
- normalizeBaseSessionMap()
901
- const allList = getSessionList()
908
+ export function updateBaseOnMsgList() {
909
+ const contactStore = useContactStore()
910
+ const settingsStore = useSettingsStore()
911
+ normalizeBaseSessionMap()
912
+ const allList = getSessionList()
902
913
  // 先更具 item.always_top 是不是 true 拆为两个数组
903
914
  const topList = allList.filter((item) => item.always_top)
904
915
  const normalList = allList.filter((item) => !item.always_top)
@@ -1,9 +1,7 @@
1
1
  import type { Session } from '../elements/information'
2
2
 
3
3
  export function normalizeGroupId(value: string): string {
4
- const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, '').trim()
5
- const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/)
6
- return wrapped ? wrapped[1] : raw || value
4
+ return String(value)
7
5
  }
8
6
 
9
7
  export function normalizeSessionId(value: number | string): string {
@@ -27,12 +25,6 @@ export function getSessionAliases(item: Session): string[] {
27
25
  getSessionId(item),
28
26
  getLegacySessionId(item),
29
27
  ]
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
28
  const aliases = rawAliases.filter((value): value is number | string => {
37
29
  const text = String(value ?? '')
38
30
  return text !== '' && text !== '0'
@@ -44,11 +36,10 @@ export function getSessionDedupKey(item: Session): string {
44
36
  const channelId = String(item.channel_id ?? item.channelId ?? '')
45
37
  const groupId = String(item.group_id ?? '')
46
38
  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)}`
39
+ if (channelId) return normalizeSessionId(channelId)
40
+ if (groupId) return normalizeSessionId(groupId)
41
+ if (userId) return normalizeSessionId(userId)
42
+ return ''
52
43
  }
53
44
 
54
45
  export function setSessionContact(
@@ -60,6 +51,21 @@ export function setSessionContact(
60
51
  }
61
52
  }
62
53
 
54
+ export function upsertSessionContact(
55
+ contacts: Session[],
56
+ item: Session,
57
+ ): Session[] {
58
+ const key = getSessionDedupKey(item)
59
+ if (!key) return contacts
60
+ const index = contacts.findIndex((current) => getSessionDedupKey(current) === key)
61
+ if (index >= 0) {
62
+ const next = [...contacts]
63
+ Object.assign(next[index], item)
64
+ return next
65
+ }
66
+ return [...contacts, item]
67
+ }
68
+
63
69
  export function findSessionContact(
64
70
  contacts: Session[],
65
71
  sessionId: number | string,
@@ -67,10 +73,6 @@ export function findSessionContact(
67
73
  const target = normalizeSessionId(sessionId)
68
74
  return contacts.find((item) => {
69
75
  return getSessionAliases(item).some((alias) => alias === target)
70
- || (
71
- Boolean(item.group_id) &&
72
- normalizeGroupId(String(item.group_id)) === normalizeGroupId(target)
73
- )
74
76
  })
75
77
  }
76
78
 
@@ -738,12 +738,6 @@ const searchRequestId = ref(0)
738
738
  const forwardList = ref(contactStore.userList)
739
739
  const chatImg = ref<any>(undefined)
740
740
  const trueLang = getTrueLang()
741
- let selfRefreshTimer: number | null = null
742
-
743
- function ensureChannelPrefix(channelId: string, prefix: string): string {
744
- if (/^(?:group|room|chat|channel|guild|private):/i.test(channelId)) return channelId
745
- return `${prefix}:${channelId}`
746
- }
747
741
 
748
742
  function messageLocalTimeMs(item: any): number {
749
743
  const local = Number(item?.local_time ?? item?.timestamp_ms ?? item?.time_ms ?? 0)
@@ -756,8 +750,8 @@ async function refreshSelfHistory() {
756
750
  const id = chat.show.id
757
751
  if (!id || id === 0 || details.value[3].open || tags.value.showForwardPan) return
758
752
  const channelId = chat.show.type === 'group'
759
- ? ensureChannelPrefix(String(chat.show.channel_id ?? id), 'group')
760
- : ensureChannelPrefix(String(chat.show.channel_id ?? id), 'private')
753
+ ? String(chat.show.channel_id ?? id)
754
+ : String(chat.show.channel_id ?? id)
761
755
  let cached: Record<string, unknown>[] = []
762
756
  try {
763
757
  cached = await loadChatHistoryFromCache({
@@ -798,22 +792,8 @@ async function refreshSelfHistory() {
798
792
  }
799
793
 
800
794
  function startSelfRefresh() {
801
- if (selfRefreshTimer !== null) {
802
- window.clearInterval(selfRefreshTimer)
803
- selfRefreshTimer = null
804
- }
805
795
  if (!chat.show.id) return
806
796
  void refreshSelfHistory()
807
- selfRefreshTimer = window.setInterval(() => {
808
- void refreshSelfHistory()
809
- }, 2500)
810
- }
811
-
812
- function stopSelfRefresh() {
813
- if (selfRefreshTimer !== null) {
814
- window.clearInterval(selfRefreshTimer)
815
- selfRefreshTimer = null
816
- }
817
797
  }
818
798
 
819
799
  //#region == 窗口移动相关 ==================================================
@@ -973,7 +953,6 @@ onMounted(() => {
973
953
  })
974
954
 
975
955
  onBeforeUnmount(() => {
976
- stopSelfRefresh()
977
956
  if (resizeMainInputFrame !== null) {
978
957
  cancelAnimationFrame(resizeMainInputFrame)
979
958
  resizeMainInputFrame = null
@@ -1217,19 +1196,24 @@ async function loadMoreHistory() {
1217
1196
  }
1218
1197
  uiStore.loadHistoryFail = false
1219
1198
 
1199
+ const channelId = String(
1200
+ chatStore.chatInfo.show.channel_id
1201
+ ?? chatStore.chatInfo.show.id
1202
+ ?? '',
1203
+ )
1220
1204
  if (useMixedHistory) {
1221
1205
  let localMsgs = [] as any[]
1222
1206
  if (Number.isFinite(firstMsgTime)) {
1223
1207
  localMsgs = await dbGetBeforeByTime(
1224
1208
  authStore.loginInfo.uin,
1225
- chatStore.chatInfo.show.id,
1209
+ channelId,
1226
1210
  firstMsgTime,
1227
1211
  20,
1228
1212
  )
1229
1213
  } else {
1230
1214
  localMsgs = await dbGetBefore(
1231
1215
  authStore.loginInfo.uin,
1232
- chatStore.chatInfo.show.id,
1216
+ channelId,
1233
1217
  firstMsgId,
1234
1218
  20,
1235
1219
  )
@@ -1251,12 +1235,6 @@ async function loadMoreHistory() {
1251
1235
  }
1252
1236
  }
1253
1237
 
1254
- const type = chatStore.chatInfo.show.type
1255
- const id = chatStore.chatInfo.show.id
1256
- const rawChannelId = String(chatStore.chatInfo.show.channel_id ?? '')
1257
- const channelId = type === 'group'
1258
- ? ensureChannelPrefix(rawChannelId || String(id), 'group')
1259
- : ensureChannelPrefix(rawChannelId || String(id), 'private')
1260
1238
  const cached = await loadChatHistoryFromCache({
1261
1239
  platform: String(authStore.loginInfo.platform ?? ''),
1262
1240
  selfId: String(authStore.loginInfo.uin ?? ''),
@@ -2030,6 +2008,7 @@ function forwardMsg(data: UserFriendElem & UserGroupElem) {
2030
2008
  targetType as 'group' | 'user',
2031
2009
  targetId,
2032
2010
  msgBody,
2011
+ targetChannelId || undefined,
2033
2012
  )
2034
2013
  if (!result.ok) {
2035
2014
  new PopInfo().add(PopType.ERR, $t('合并转发发送失败'))
@@ -2072,11 +2051,7 @@ function forwardMsg(data: UserFriendElem & UserGroupElem) {
2072
2051
  scrollToMsg('chat-' + sentMsg.message_id, true)
2073
2052
  })
2074
2053
  }
2075
- const sentChannelId = targetType === 'group'
2076
- ? (targetChannelId || targetId)
2077
- : /^(?:private|direct):/i.test(targetChannelId || '')
2078
- ? targetChannelId
2079
- : `private:${targetId}`
2054
+ const sentChannelId = result.channelId || targetChannelId || targetId
2080
2055
  if (result.native && !result.messageId) {
2081
2056
  // 原生路径已经由后端 before-send/发送拦截写入,缺少消息 id 时不再重复写入
2082
2057
  } else {
@@ -2096,7 +2071,8 @@ function forwardMsg(data: UserFriendElem & UserGroupElem) {
2096
2071
  kind: 'forward',
2097
2072
  })
2098
2073
  }
2099
- const targetSession = contactStore.baseOnMsgList.get(normalizeSessionId(targetId))
2074
+ const targetSessionKey = targetChannelId || targetId
2075
+ const targetSession = contactStore.baseOnMsgList.get(normalizeSessionId(targetSessionKey))
2100
2076
  if (targetSession) {
2101
2077
  const preview = `[${$t('聊天记录')}]`
2102
2078
  targetSession.raw_msg_base = preview
@@ -2104,7 +2080,7 @@ function forwardMsg(data: UserFriendElem & UserGroupElem) {
2104
2080
  ? `${String(authStore.loginInfo.nickname ?? '')}: ${preview}`
2105
2081
  : preview
2106
2082
  targetSession.time = Math.floor(sentTime / 1000)
2107
- contactStore.baseOnMsgList.set(normalizeSessionId(targetId), targetSession)
2083
+ contactStore.baseOnMsgList.set(normalizeSessionId(targetSessionKey), targetSession)
2108
2084
  updateBaseOnMsgList()
2109
2085
  }
2110
2086
  }
@@ -2153,8 +2129,9 @@ function forwardMsg(data: UserFriendElem & UserGroupElem) {
2153
2129
 
2154
2130
  function finishForward(data: UserFriendElem & UserGroupElem, id: string | number) {
2155
2131
  cancelForward()
2156
- if(contactStore.baseOnMsgList.get(normalizeSessionId(id)) == undefined) {
2157
- contactStore.baseOnMsgList.set(normalizeSessionId(id), data)
2132
+ const sessionKey = String(data.channel_id ?? data.channelId ?? id ?? '')
2133
+ if(contactStore.baseOnMsgList.get(normalizeSessionId(sessionKey)) == undefined) {
2134
+ contactStore.baseOnMsgList.set(normalizeSessionId(sessionKey), data)
2158
2135
  }
2159
2136
  nextTick(() => {
2160
2137
  const user = document.getElementById('user-' + id)
@@ -2267,7 +2244,15 @@ async function revokeMsg() {
2267
2244
  ?? chatStore.chatInfo.show.channel_id
2268
2245
  ?? chat.show.id,
2269
2246
  )
2270
- await Connector.callApi('delete_msg', { channel_id: channelId, message_id: msgId })
2247
+ try {
2248
+ await Connector.callApi('delete_msg', { channel_id: channelId, message_id: msgId })
2249
+ } catch {
2250
+ new PopInfo().add(PopType.ERR, $t('撤回失败'))
2251
+ return
2252
+ }
2253
+ msgData.revoke = true
2254
+ msgData.revoked = true
2255
+ msgData.revokeTime = Date.now() / 1000
2271
2256
  }
2272
2257
 
2273
2258
  async function reeditMsg() {
@@ -2629,13 +2614,31 @@ function sendVideo(file: File, fileName: string | null) {
2629
2614
  sendMedia(file, fileName, 'video')
2630
2615
  }
2631
2616
 
2617
+ async function isGifImage(file: File): Promise<boolean> {
2618
+ if (file.type === 'image/gif' || /\.gif$/i.test(file.name || '')) return true
2619
+ try {
2620
+ const buffer = await file.slice(0, 6).arrayBuffer()
2621
+ const bytes = new Uint8Array(buffer)
2622
+ return bytes.length >= 6
2623
+ && bytes[0] === 0x47
2624
+ && bytes[1] === 0x49
2625
+ && bytes[2] === 0x46
2626
+ && bytes[3] === 0x38
2627
+ && (bytes[4] === 0x37 || bytes[4] === 0x39)
2628
+ && bytes[5] === 0x61
2629
+ } catch {
2630
+ return false
2631
+ }
2632
+ }
2633
+
2632
2634
  async function setImg(file: File | null) {
2633
2635
  const popInfo = new PopInfo()
2634
2636
  if (!file) return
2635
2637
  if (!file.type.includes('image/')) return
2636
2638
  if (file.size === 0) return
2639
+ const isGif = await isGifImage(file)
2637
2640
 
2638
- if (file.size > 3145728) {
2641
+ if (file.size > 3145728 && !isGif) {
2639
2642
  const options = { maxSizeMB: 3, useWebWorker: true }
2640
2643
  try {
2641
2644
  popInfo.add(PopType.INFO, $t('正在压缩图片 ……'))
@@ -3170,7 +3173,8 @@ async function handleInput(event: Event) {
3170
3173
  searchDebounceTimer.value = setTimeout(async () => {
3171
3174
  const results = await dbSearchMessages(
3172
3175
  authStore.loginInfo.uin,
3173
- chatStore.chatInfo.show.id,
3176
+ chatStore.chatInfo.show.channel_id
3177
+ ?? chatStore.chatInfo.show.id,
3174
3178
  value,
3175
3179
  )
3176
3180
  if (requestId !== searchRequestId.value || !details.value[3].open) return