koishi-plugin-chat-patch 3.0.2 → 5.0.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 (194) hide show
  1. package/client/vue/index.vue +60 -634
  2. package/client/web/env.d.ts +56 -0
  3. package/client/web/index.html +21 -0
  4. package/client/web/package-lock.json +3419 -0
  5. package/client/web/package.json +62 -0
  6. package/client/web/public/bcui/css/color-dark.css +35 -0
  7. package/client/web/public/bcui/css/color-light.css +35 -0
  8. package/client/web/public/bcui/css/style.css +468 -0
  9. package/client/web/public/bcui/js/main.js +193 -0
  10. package/client/web/public/css/append-dark.css +13 -0
  11. package/client/web/public/css/append-light.css +13 -0
  12. package/client/web/public/css/append-napcat-dark.css +14 -0
  13. package/client/web/public/css/append-napcat-light.css +14 -0
  14. package/client/web/public/favicon.ico +0 -0
  15. package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
  16. package/client/web/public/img/icons/icon-maskable.png +0 -0
  17. package/client/web/public/img/icons/icon.png +0 -0
  18. package/client/web/public/img/icons/icon.svg +10 -0
  19. package/client/web/public/robots.txt +2 -0
  20. package/client/web/public/sw.js +58 -0
  21. package/client/web/scripts/copy-bcui.mjs +17 -0
  22. package/client/web/src/App.vue +1127 -0
  23. package/client/web/src/assets/css/append/append_darwin.css +68 -0
  24. package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
  25. package/client/web/src/assets/css/append/append_linux.css +125 -0
  26. package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
  27. package/client/web/src/assets/css/append/append_napcat.css +35 -0
  28. package/client/web/src/assets/css/append/append_new.css +843 -0
  29. package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
  30. package/client/web/src/assets/css/append/append_win32.css +109 -0
  31. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
  32. package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
  33. package/client/web/src/assets/css/chat.css +2104 -0
  34. package/client/web/src/assets/css/msg.css +941 -0
  35. package/client/web/src/assets/css/options.css +811 -0
  36. package/client/web/src/assets/css/sys_notice.css +62 -0
  37. package/client/web/src/assets/css/view.css +2105 -0
  38. package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
  39. package/client/web/src/assets/img/Vue.png +0 -0
  40. package/client/web/src/assets/img/cakes.svg +189 -0
  41. package/client/web/src/assets/img/icons/AppIcon.png +0 -0
  42. package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
  43. package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
  44. package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
  45. package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
  46. package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
  47. package/client/web/src/assets/img/icons/exit.png +0 -0
  48. package/client/web/src/assets/img/icons/pen.png +0 -0
  49. package/client/web/src/assets/img/stars.svg +89 -0
  50. package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
  51. package/client/web/src/assets/l10n/_About I10n.md +35 -0
  52. package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
  53. package/client/web/src/assets/l10n/zh-CN.po +1941 -0
  54. package/client/web/src/assets/logo.png +0 -0
  55. package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
  56. package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
  57. package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
  58. package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
  59. package/client/web/src/assets/qed.txt +1 -0
  60. package/client/web/src/components/AboutPan.vue +197 -0
  61. package/client/web/src/components/BulletinBody.vue +129 -0
  62. package/client/web/src/components/DepPan.vue +310 -0
  63. package/client/web/src/components/EmojiFace.vue +32 -0
  64. package/client/web/src/components/FacePan.vue +461 -0
  65. package/client/web/src/components/FileBody.vue +129 -0
  66. package/client/web/src/components/FileManager.vue +532 -0
  67. package/client/web/src/components/FriendBody.vue +54 -0
  68. package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
  69. package/client/web/src/components/History.vue +49 -0
  70. package/client/web/src/components/LazyLottie.vue +82 -0
  71. package/client/web/src/components/MergePan.vue +127 -0
  72. package/client/web/src/components/MsgBody.vue +1478 -0
  73. package/client/web/src/components/MusicPlayer.vue +549 -0
  74. package/client/web/src/components/NoticeBody.vue +168 -0
  75. package/client/web/src/components/QzonePermissionPan.vue +150 -0
  76. package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
  77. package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
  78. package/client/web/src/components/TinySessionBody.vue +51 -0
  79. package/client/web/src/components/UmamiInfoPan.vue +1549 -0
  80. package/client/web/src/components/UpdatePan.vue +263 -0
  81. package/client/web/src/components/ViewerCom.vue +1360 -0
  82. package/client/web/src/components/VoiceMsg.vue +626 -0
  83. package/client/web/src/components/WelPan.vue +894 -0
  84. package/client/web/src/components/msg-body/.gitkeep +0 -0
  85. package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
  86. package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
  87. package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
  88. package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
  89. package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
  90. package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
  91. package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
  92. package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
  93. package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
  94. package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
  95. package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
  96. package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
  97. package/client/web/src/components/notice-component/.gitkeep +0 -0
  98. package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
  99. package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
  100. package/client/web/src/components/tooltip/Tooltip.vue +50 -0
  101. package/client/web/src/components/tooltip/Tooltips.vue +14 -0
  102. package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
  103. package/client/web/src/function/base.ts +238 -0
  104. package/client/web/src/function/connect.ts +2042 -0
  105. package/client/web/src/function/debug.ts +10 -0
  106. package/client/web/src/function/elements/information.ts +141 -0
  107. package/client/web/src/function/elements/system.ts +101 -0
  108. package/client/web/src/function/elements/vueComp.ts +62 -0
  109. package/client/web/src/function/favicon.ts +76 -0
  110. package/client/web/src/function/input.ts +69 -0
  111. package/client/web/src/function/model/emoji.ts +127 -0
  112. package/client/web/src/function/model/img.ts +150 -0
  113. package/client/web/src/function/model/msg-body.ts +51 -0
  114. package/client/web/src/function/msg.ts +2674 -0
  115. package/client/web/src/function/notify.ts +225 -0
  116. package/client/web/src/function/option.ts +749 -0
  117. package/client/web/src/function/satori-model.ts +818 -0
  118. package/client/web/src/function/satori.ts +325 -0
  119. package/client/web/src/function/sender.ts +178 -0
  120. package/client/web/src/function/tooltip.ts +49 -0
  121. package/client/web/src/function/utils/appUtil.ts +2288 -0
  122. package/client/web/src/function/utils/avatarUtil.ts +31 -0
  123. package/client/web/src/function/utils/backgroundUtil.ts +110 -0
  124. package/client/web/src/function/utils/linkViewUtil.ts +101 -0
  125. package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
  126. package/client/web/src/function/utils/msgUtil.ts +1242 -0
  127. package/client/web/src/function/utils/pinyin.ts +141 -0
  128. package/client/web/src/function/utils/recordUtil.ts +187 -0
  129. package/client/web/src/function/utils/sessionUtil.ts +114 -0
  130. package/client/web/src/function/utils/systemUtil.ts +664 -0
  131. package/client/web/src/main.ts +91 -0
  132. package/client/web/src/pages/Chat.vue +3453 -0
  133. package/client/web/src/pages/Friends.vue +636 -0
  134. package/client/web/src/pages/Info.vue +717 -0
  135. package/client/web/src/pages/Messages.vue +875 -0
  136. package/client/web/src/pages/Options.vue +82 -0
  137. package/client/web/src/pages/Qzone.vue +1178 -0
  138. package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
  139. package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
  140. package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
  141. package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
  142. package/client/web/src/pages/options/OptAccount.vue +107 -0
  143. package/client/web/src/pages/options/OptAddon.vue +185 -0
  144. package/client/web/src/pages/options/OptDev.vue +704 -0
  145. package/client/web/src/pages/options/OptFunction.vue +403 -0
  146. package/client/web/src/pages/options/OptInfo.vue +165 -0
  147. package/client/web/src/pages/options/OptView.vue +846 -0
  148. package/client/web/src/runtime/backend.ts +310 -0
  149. package/client/web/src/state/auth.ts +14 -0
  150. package/client/web/src/state/chat.ts +36 -0
  151. package/client/web/src/state/connection.ts +20 -0
  152. package/client/web/src/state/contact.ts +37 -0
  153. package/client/web/src/state/qzone.ts +18 -0
  154. package/client/web/src/state/sessionHistory.ts +25 -0
  155. package/client/web/src/state/settings.ts +18 -0
  156. package/client/web/src/state/sticker.ts +10 -0
  157. package/client/web/src/state/ui.ts +56 -0
  158. package/client/web/tsconfig.json +27 -0
  159. package/client/web/tsconfig.tsbuildinfo +1 -0
  160. package/client/web/vite.config.ts +33 -0
  161. package/dist/index.js +1 -12
  162. package/dist/style.css +1 -1
  163. package/lib/index.js +2070 -2077
  164. package/package.json +15 -2
  165. package/readme.md +0 -22
  166. package/src/bootstrap.ts +71 -0
  167. package/src/cache.ts +319 -0
  168. package/src/config.ts +23 -35
  169. package/src/database.ts +650 -0
  170. package/src/index.ts +27 -59
  171. package/src/media.ts +302 -0
  172. package/src/recorder.ts +175 -0
  173. package/src/self-message.ts +456 -0
  174. package/src/server.d.ts +26 -0
  175. package/src/types.ts +108 -48
  176. package/src/web.ts +712 -0
  177. package/client/vue/chat-logic.ts +0 -683
  178. package/client/vue/composables/useChatActions.ts +0 -61
  179. package/client/vue/composables/useChatData.ts +0 -185
  180. package/client/vue/composables/useImageCache.ts +0 -281
  181. package/client/vue/composables/useVideoCache.ts +0 -125
  182. package/client/vue/types.ts +0 -72
  183. package/lib/api-handlers.d.ts +0 -25
  184. package/lib/config.d.ts +0 -14
  185. package/lib/file-manager.d.ts +0 -124
  186. package/lib/index.d.ts +0 -17
  187. package/lib/logger.d.ts +0 -9
  188. package/lib/message-handler.d.ts +0 -35
  189. package/lib/types.d.ts +0 -57
  190. package/lib/utils.d.ts +0 -18
  191. package/src/api-handlers.ts +0 -646
  192. package/src/file-manager.ts +0 -1436
  193. package/src/message-handler.ts +0 -507
  194. package/src/utils.ts +0 -189
@@ -0,0 +1,2674 @@
1
+ /*
2
+ * @FileDescription: 消息处理模块
3
+ * @Author: Stapxs
4
+ * @Date:
5
+ * 2022/11/1
6
+ * 2022/12/7
7
+ * 2024/9/2
8
+ * @Version:
9
+ * 1.0 - 初始版本
10
+ * 2.0 - 重构为 ts 版本,修改 Vue3 相关变更
11
+ * 3.0 - 优化更优雅的代码结构
12
+ * @Description: 此模块用于拆分和保存/处理 bot 返回的各类信息,整个运行时数据也保存在这儿。
13
+ */
14
+ import qed from '../assets/qed.txt?raw'
15
+
16
+ import app from '../main'
17
+ import Option from './option'
18
+
19
+
20
+ import {
21
+ buildMsgList,
22
+ getMsgData,
23
+ parseMsgList,
24
+ getMsgRawTxt,
25
+ hasAtMe,
26
+ parseCQ,
27
+ updateBaseOnMsgList,
28
+ updateLastestHistory,
29
+ sendMsgAppendInfo,
30
+ } from '../function/utils/msgUtil'
31
+ import { parseSatoriMarkup } from '../function/satori-model'
32
+ import {
33
+ delay,
34
+ getViewTime,
35
+ randomNum,
36
+ } from '../function/utils/systemUtil'
37
+ import {
38
+ reloadUsers,
39
+ reloadCookies,
40
+ updateMenu,
41
+ loadJsonMap,
42
+ sendIdentifyData,
43
+ sendStatEvent,
44
+ } from '../function/utils/appUtil'
45
+
46
+ const SATORI_MARKUP_PATTERN = /<(?:img|image|audio|video|file|mface|face|quote|at|forward|json|xml|markdown|keyboard|p|br|i18n|a|text|sharp)(?:\s|\/|>)/i
47
+
48
+ function splitSatoriMarkupText(message: any[]): any[] {
49
+ return message.flatMap((segment) => {
50
+ if (
51
+ segment?.type === 'text' &&
52
+ typeof segment.text === 'string' &&
53
+ SATORI_MARKUP_PATTERN.test(segment.text)
54
+ ) {
55
+ return parseSatoriMarkup(segment.text)
56
+ }
57
+ return [segment]
58
+ })
59
+ }
60
+ import { reactive, markRaw, nextTick } from 'vue'
61
+ import { PopInfo, PopType, Logger, LogType } from './base'
62
+ import { isDebugMode } from './debug'
63
+ import { Connector, fetchForwardMessage, getCachedUserAvatar, loadGroupMembersFromCache, login, requestUserAvatar, saveConnectionToHistory, saveSentSelfMessage } from './connect'
64
+ import {
65
+ GroupFileElem,
66
+ GroupFileFolderElem,
67
+ GroupMemberInfoElem,
68
+ UserFriendElem,
69
+ UserGroupElem,
70
+ MsgItemElem,
71
+ type Session,
72
+ } from './elements/information'
73
+ import { NotifyInfo } from './elements/system'
74
+ import { Notify } from './notify'
75
+ import { backend } from '../runtime/backend'
76
+ import { dbRevokeMessage, saveMessagesWithSideEffects } from './utils/localHistoryUtil'
77
+ import { addDownloadTask, completeUploadTask } from '../components/FileManager.vue'
78
+ import { refreshFavicon } from './favicon'
79
+ import { Img } from './model/img'
80
+ import { ensurePinyinLoaded, getPinyin, isPinyinReady } from './utils/pinyin'
81
+ import { useAuthStore } from '../state/auth'
82
+ import { useContactStore } from '../state/contact'
83
+ import { useChatStore } from '../state/chat'
84
+ import { useConnectionStore } from '../state/connection'
85
+ import { useStickerStore } from '../state/sticker'
86
+ import { useUIStore } from '../state/ui'
87
+ import { useSettingsStore } from '../state/settings'
88
+ import { useQzoneStore } from '../state/qzone'
89
+ import {
90
+ getSessionId,
91
+ getMissingGroupPreviewSessions,
92
+ mergeEarlySessionContacts,
93
+ normalizeSessionId,
94
+ resolveIncomingSession,
95
+ } from './utils/sessionUtil'
96
+
97
+ const popInfo = new PopInfo()
98
+ // eslint-disable-next-line
99
+ const msgPaths = import.meta.glob("../assets/pathMap/*.yaml", { eager: true })
100
+ // 取出包含 Lagrange.OneBot.yaml 的那条
101
+ const msgPathAt = Object.keys(msgPaths).find((item) => {
102
+ return item.indexOf('Lagrange.OneBot.yaml') > 0
103
+ })
104
+ let msgPath = {} as { [key: string]: any }
105
+ if (msgPathAt != undefined) {
106
+ msgPath = (msgPaths[msgPathAt] as any).default
107
+ }
108
+
109
+ function getDataObject(value: unknown): Record<string, unknown> {
110
+ return typeof value === 'object' && value !== null
111
+ ? value as Record<string, unknown>
112
+ : {}
113
+ }
114
+
115
+ function getString(value: unknown): string {
116
+ if (value === null || value === undefined) return ''
117
+ return typeof value === 'string' ? value : String(value)
118
+ }
119
+
120
+ // 其他 tag
121
+ let listLoadTimes = 0
122
+ const logger = new Logger()
123
+ let firstHeartbeatTime = -1
124
+ let heartbeatTime = -1
125
+ const MILLISECONDS_PER_SECOND = 1000
126
+ const META_EVENT_WATCHDOG = {
127
+ // Give one missed heartbeat plus a small network/main-thread grace window before forcing a disconnect.
128
+ timeoutMultiplier: 2,
129
+ graceSeconds: 5,
130
+ }
131
+ let loginWaveTimer: any = null
132
+
133
+ export function setLoginWaveTimer(timer: any) {
134
+ loginWaveTimer = timer
135
+ }
136
+
137
+ export function clearLoginWaveTimer() {
138
+ if (loginWaveTimer !== null) {
139
+ clearInterval(loginWaveTimer)
140
+ loginWaveTimer = null
141
+ }
142
+ }
143
+
144
+ const groupPreviewHydrator = (() => {
145
+ const intervalMs = 150
146
+ let queue: Session[] = []
147
+ let timer: ReturnType<typeof setTimeout> | undefined
148
+
149
+ function stop() {
150
+ if (timer) clearTimeout(timer)
151
+ timer = undefined
152
+ }
153
+
154
+ function tick() {
155
+ timer = undefined
156
+ const contactStore = useContactStore()
157
+ const session = queue.shift()
158
+ if (session) {
159
+ const sessionId = getSessionId(session)
160
+ if (
161
+ sessionId !== 0 &&
162
+ sessionId !== '' &&
163
+ !session.time &&
164
+ !session.raw_msg &&
165
+ !contactStore.baseOnMsgList.has(normalizeSessionId(sessionId))
166
+ ) {
167
+ // userList 与 baseOnMsgList 共享同一个会话对象;历史响应会原地补全预览。
168
+ contactStore.baseOnMsgList.set(normalizeSessionId(sessionId), session)
169
+ updateLastestHistory(session)
170
+ }
171
+ }
172
+
173
+ if (queue.length > 0) {
174
+ timer = setTimeout(tick, intervalMs)
175
+ }
176
+ }
177
+
178
+ function start() {
179
+ if (!timer && queue.length > 0) tick()
180
+ }
181
+
182
+ return {
183
+ scheduleMissingSessions() {
184
+ const contactStore = useContactStore()
185
+ const settingsStore = useSettingsStore()
186
+ if (settingsStore.sysConfig.session_display_mode !== 'all') return
187
+
188
+ const queuedIds = new Set(queue.map((item) => normalizeSessionId(getSessionId(item))))
189
+ getMissingGroupPreviewSessions(
190
+ contactStore.userList,
191
+ contactStore.baseOnMsgList,
192
+ ).forEach((item) => {
193
+ const sessionId = normalizeSessionId(getSessionId(item))
194
+ if (!queuedIds.has(sessionId)) {
195
+ queue.push(item)
196
+ queuedIds.add(sessionId)
197
+ }
198
+ })
199
+ start()
200
+ },
201
+ reset() {
202
+ stop()
203
+ queue = []
204
+ },
205
+ }
206
+ })()
207
+
208
+ function resolveContactPinyinName(item: UserFriendElem | UserGroupElem) {
209
+ if ((item as UserFriendElem).group_id) {
210
+ return (item as UserFriendElem).group_name ?? ''
211
+ }
212
+ return `${(item as UserGroupElem).nickname ?? ''},${(item as UserGroupElem).remark ?? ''}`
213
+ }
214
+
215
+ function resolvePinyinFirstChar(value: string) {
216
+ return getPinyin(value)
217
+ .main
218
+ .at(0)
219
+ ?.substring(0, 1)
220
+ .toUpperCase() ?? ' '
221
+ }
222
+
223
+ function sortContactListByPinyin<T extends UserFriendElem | UserGroupElem>(list: T[]) {
224
+ list.sort((a, b) => {
225
+ if (a.py_start && b.py_start) {
226
+ return a.py_start.charCodeAt(0) - b.py_start.charCodeAt(0)
227
+ }
228
+ return 0
229
+ })
230
+ }
231
+
232
+ function buildPinyinForContacts(
233
+ list: (UserFriendElem | UserGroupElem)[],
234
+ startIndex = 0,
235
+ onDone?: () => void,
236
+ ) {
237
+ if (!isPinyinReady()) {
238
+ onDone?.()
239
+ return
240
+ }
241
+
242
+ const batchSize = 100
243
+ const endIndex = Math.min(startIndex + batchSize, list.length)
244
+
245
+ for (let index = startIndex; index < endIndex; index++) {
246
+ const item = list[index]
247
+ item.py_name = getPinyin(resolveContactPinyinName(item))
248
+ item.py_start = item.py_name.main.at(0)?.substring(0, 1).toUpperCase() ?? ' '
249
+ }
250
+
251
+ if (endIndex >= list.length) {
252
+ onDone?.()
253
+ return
254
+ }
255
+
256
+ setTimeout(() => {
257
+ buildPinyinForContacts(list, endIndex, onDone)
258
+ }, 0)
259
+ }
260
+
261
+ function hydrateContactPinyinLater(list: (UserFriendElem | UserGroupElem)[]) {
262
+ const contactStore = useContactStore()
263
+
264
+ const applyHydration = () => {
265
+ buildPinyinForContacts(list, 0, () => {
266
+ sortContactListByPinyin(list)
267
+ contactStore.userList = [...contactStore.userList]
268
+ })
269
+ }
270
+
271
+ if (isPinyinReady()) {
272
+ applyHydration()
273
+ return
274
+ }
275
+
276
+ void ensurePinyinLoaded().then((loaded) => {
277
+ if (!loaded) return
278
+ applyHydration()
279
+ })
280
+ }
281
+
282
+ function clearMetaEventWatchdog() {
283
+ const connectionStore = useConnectionStore()
284
+ if (connectionStore.metaEventWatchTimer) {
285
+ clearTimeout(connectionStore.metaEventWatchTimer)
286
+ connectionStore.metaEventWatchTimer = undefined
287
+ }
288
+ connectionStore.metaEventTimeoutTriggered = false
289
+ }
290
+
291
+ function refreshMetaEventWatchdog(intervalSeconds: number) {
292
+ if (intervalSeconds <= 0) return
293
+
294
+ const connectionStore = useConnectionStore()
295
+ if (connectionStore.metaEventWatchTimer) {
296
+ clearTimeout(connectionStore.metaEventWatchTimer)
297
+ }
298
+
299
+ const timeoutSeconds = Math.ceil(
300
+ Math.max(
301
+ intervalSeconds * META_EVENT_WATCHDOG.timeoutMultiplier,
302
+ intervalSeconds + META_EVENT_WATCHDOG.graceSeconds,
303
+ ),
304
+ )
305
+
306
+ connectionStore.metaEventTimeoutTriggered = false
307
+ connectionStore.metaEventWatchTimer = setTimeout(() => {
308
+ if (connectionStore.metaEventTimeoutTriggered) return
309
+ connectionStore.metaEventTimeoutTriggered = true
310
+ connectionStore.metaEventWatchTimer = undefined
311
+ logger.add(LogType.WS, '心跳包超时,准备断开连接')
312
+ Connector.forceDisconnect('心跳包超时')
313
+ }, timeoutSeconds * MILLISECONDS_PER_SECOND)
314
+ }
315
+
316
+ function getObservedHeartbeatIntervalSeconds(msg: { [key: string]: any }) {
317
+ const currentHeartbeatTimeSeconds = Number(msg.time)
318
+ if (
319
+ firstHeartbeatTime > 0 &&
320
+ Number.isFinite(currentHeartbeatTimeSeconds) &&
321
+ currentHeartbeatTimeSeconds > firstHeartbeatTime
322
+ ) {
323
+ return currentHeartbeatTimeSeconds - firstHeartbeatTime
324
+ }
325
+
326
+ return -1
327
+ }
328
+
329
+ function getHeartbeatIntervalSeconds(msg: { [key: string]: any }) {
330
+ // OneBot heartbeat `interval` is reported in milliseconds; `time` is a Unix timestamp in seconds.
331
+ const reportedIntervalMilliseconds = Number(msg.interval)
332
+ if (Number.isFinite(reportedIntervalMilliseconds) && reportedIntervalMilliseconds > 0) {
333
+ return reportedIntervalMilliseconds / MILLISECONDS_PER_SECOND
334
+ }
335
+
336
+ return getObservedHeartbeatIntervalSeconds(msg)
337
+ }
338
+
339
+ export function dispatch(raw: string | { [k: string]: any }, echo?: string) {
340
+ let msg: any;
341
+
342
+ // 1) 如有需要先 parse
343
+ if (typeof raw === 'string') {
344
+ try {
345
+ msg = JSON.parse(raw);
346
+ } catch {
347
+ if (!raw.includes('"meta_event_type":"heartbeat"')) {
348
+ logger.add(LogType.WS, 'GET:' + raw);
349
+ }
350
+ return;
351
+ }
352
+ } else {
353
+ msg = raw;
354
+ }
355
+
356
+ // 2) 決定 name/key
357
+ const name = echo ? echo.split('_')[0] : msg.post_type === 'notice' ? msg.sub_type ?? msg.notice_type : msg.post_type;
358
+
359
+ // 3) 安全調用 handler
360
+ try {
361
+ const fn = handlers[name];
362
+ if (!fn) throw new Error(`No handler for "${name}"`);
363
+ const metaArgs = echo ? echo.split('_') : undefined;
364
+ fn(msg, metaArgs);
365
+ } catch (e) {
366
+ logger.error(e as Error, `跳转事件处理错误 - ${name}:\n${JSON.stringify(msg)}`);
367
+ }
368
+ }
369
+
370
+ // ==============================================================
371
+ const noticeFunctions = {
372
+ /**
373
+ * 心跳包
374
+ */
375
+ meta_event: (_: string, msg: { [key: string]: any }) => {
376
+ const connectionStore = useConnectionStore()
377
+ if (firstHeartbeatTime == -1) {
378
+ firstHeartbeatTime = 0
379
+ connectionStore.heartbeatTime = 0
380
+ clearMetaEventWatchdog()
381
+ return
382
+ }
383
+ if (firstHeartbeatTime == 0) {
384
+ firstHeartbeatTime = msg.time
385
+ connectionStore.lastHeartbeatTime = msg.time
386
+ clearMetaEventWatchdog()
387
+ return
388
+ }
389
+ if (firstHeartbeatTime != -1 && heartbeatTime == -1) {
390
+ // 计算心跳时间
391
+ heartbeatTime = getHeartbeatIntervalSeconds(msg)
392
+ }
393
+ // 记录心跳状态
394
+ if (heartbeatTime != -1) {
395
+ connectionStore.heartbeatTime = heartbeatTime
396
+ connectionStore.oldHeartbeatTime = connectionStore.lastHeartbeatTime
397
+ connectionStore.lastHeartbeatTime = msg.time
398
+ refreshMetaEventWatchdog(heartbeatTime)
399
+ }
400
+ },
401
+
402
+ /**
403
+ * 新消息
404
+ */
405
+ message_sent: newMsg,
406
+ message: newMsg,
407
+
408
+ /**
409
+ * 请求
410
+ */
411
+ request: (_: string, msg: { [key: string]: any }) => {
412
+ const contactStore = useContactStore()
413
+ if (contactStore.systemNoticesList) {
414
+ contactStore.systemNoticesList.push(msg)
415
+ } else {
416
+ contactStore.systemNoticesList = [msg]
417
+ }
418
+ },
419
+
420
+ /**
421
+ * 好友变动
422
+ */
423
+ friend: (_: string, msg: { [key: string]: any }) => {
424
+ // 重新加载联系人列表
425
+ reloadUsers()
426
+ switch (msg.sub_type) {
427
+ case 'increase': {
428
+ // 添加系统通知
429
+ new PopInfo().add(
430
+ PopType.INFO,
431
+ app.config.globalProperties.$t('添加好友 {name} 成功!', {
432
+ name: msg.nickname,
433
+ }),
434
+ )
435
+ break
436
+ }
437
+ case 'decrease': {
438
+ if (isDebugMode()) {
439
+ // eslint-disable-next-line no-console
440
+ console.log(
441
+ '%c消失了一个好友:' +
442
+ msg.nickname +
443
+ '(' +
444
+ msg.user_id +
445
+ ')',
446
+ 'color:red;',
447
+ )
448
+ }
449
+ break
450
+ }
451
+ }
452
+ },
453
+
454
+ /**
455
+ * 消息撤回
456
+ */
457
+ group_recall: revokeMsg,
458
+ friend_recall: revokeMsg,
459
+ recall: revokeMsg,
460
+
461
+ /**
462
+ * 表情回应
463
+ */
464
+ group_msg_emoji_like: (_: string, msg: { [key: string]: any }) => {
465
+ const chatStore = useChatStore()
466
+ const msgId = msg.message_id
467
+ const emojiList = msg.likes
468
+ // 寻找消息
469
+ chatStore.messageList.forEach((item, index) => {
470
+ if (item.message_id === msgId) {
471
+ chatStore.messageList[index].emoji_like = emojiList
472
+ }
473
+ })
474
+ },
475
+
476
+ /**
477
+ * 群禁言
478
+ */
479
+ group_ban: (_: string, msg: { [key: string]: any }) => {
480
+ const authStore = useAuthStore()
481
+ const chatStore = useChatStore()
482
+ const groupId = msg.group_id
483
+ const userId = msg.user_id
484
+ const status = msg.sub_type === 'ban' ? true : false
485
+ const duration = msg.duration ?? 0 // 秒
486
+
487
+ // 如果是自己,更新禁言时间
488
+ if (
489
+ userId == authStore.loginInfo.uin &&
490
+ groupId == chatStore.chatInfo.show.id
491
+ ) {
492
+ if (status)
493
+ chatStore.chatInfo.info.me_info.shut_up_timestamp =
494
+ (new Date().getTime() + duration * 1000) / 1000
495
+ else chatStore.chatInfo.info.me_info.shut_up_timestamp = 0
496
+ }
497
+
498
+ // 只有在当前群才会显示
499
+ if (groupId == chatStore.chatInfo.show.id)
500
+ chatStore.messageList.push(msg)
501
+ },
502
+
503
+ /**
504
+ * 踢人
505
+ */
506
+ kick: (_: string, msg: { [key: string]: any }) => {
507
+ const chatStore = useChatStore()
508
+ const groupId = msg.group_id
509
+ if (groupId == chatStore.chatInfo.show.id) {
510
+ // 稍微等一下再刷新成员列表
511
+ delay(1000).then(() => {
512
+ void loadGroupMembersFromCache(String(chatStore.chatInfo.show.id))
513
+ return delay(1000)
514
+ }).then(() => {
515
+ void loadGroupMembersFromCache(String(chatStore.chatInfo.show.id))
516
+ })
517
+ }
518
+ },
519
+
520
+ /**
521
+ * 戳一戳
522
+ */
523
+ poke: (_: string, msg: { [key: string]: any }) => {
524
+ const { $t } = app.config.globalProperties
525
+ const authStore = useAuthStore()
526
+ const chatStore = useChatStore()
527
+
528
+ const groupId = msg.group_id
529
+ const userIds = [msg.user_id, msg.target_id]
530
+ const info = msg.raw_info
531
+
532
+ // 如果的当前打开的会话
533
+ if (groupId == chatStore.chatInfo.show.id) {
534
+ let str = ''
535
+ const userInfo = [] as { txt: string; isMe: boolean }[]
536
+ // 用户列表
537
+ userIds.forEach((id) => {
538
+ if (id == authStore.loginInfo.uin) {
539
+ userInfo.push({
540
+ txt: $t('你'),
541
+ isMe: true,
542
+ })
543
+ } else {
544
+ // 到群成员列表中去找这个人
545
+ const user = chatStore.chatInfo.info.group_members.find(
546
+ (item) => {
547
+ return item.user_id == id
548
+ },
549
+ )
550
+ if (user)
551
+ userInfo.push({
552
+ txt: `<span>${user.nickname}</span>`,
553
+ isMe: false,
554
+ })
555
+ }
556
+ })
557
+ // 遍历内容段
558
+ let getQQTimes = 0
559
+ info.forEach((item: any) => {
560
+ switch (item.type) {
561
+ case 'img':
562
+ str += `<img src="${backend.proxyUrl(item.src)}"/>`
563
+ break
564
+ case 'nor':
565
+ str += item.txt
566
+ break
567
+ case 'qq': {
568
+ str += userInfo[getQQTimes].txt
569
+ getQQTimes++
570
+ }
571
+ }
572
+ })
573
+ // 插入系统消息
574
+ msg.str = str
575
+ msg.pokeMe = userInfo[1].isMe
576
+ chatStore.messageList.push(msg)
577
+ }
578
+ },
579
+
580
+ approve: (_: string, msg: { [key: string]: any }) => {
581
+ const { $t } = app.config.globalProperties
582
+ const chatStore = useChatStore()
583
+
584
+ const groupId = msg.group_id
585
+ const userId = msg.user_id
586
+
587
+ // 如果的当前打开的会话
588
+ if (groupId == chatStore.chatInfo.show.id) {
589
+ // 刷新群成员列表
590
+ void loadGroupMembersFromCache(String(groupId))
591
+ // 获取到用户信息
592
+ const user = chatStore.chatInfo.info.group_members.find(
593
+ (item) => {
594
+ return item.user_id == userId
595
+ },
596
+ )
597
+ // 插入入群通知
598
+ if (user) {
599
+ const str = $t('{name} 加入了群聊', {
600
+ name: user.nickname,
601
+ })
602
+ msg.str = str
603
+ chatStore.messageList.push(msg)
604
+ }
605
+ }
606
+ },
607
+
608
+ input_status: (_: string, msg: { [key: string]: any }) => {
609
+ const { $t } = app.config.globalProperties
610
+ const chatStore = useChatStore()
611
+ const sender = msg.user_id
612
+ if (chatStore.chatInfo.show.id == sender) {
613
+ // 使用客户端返回的具体状态文本
614
+ if (msg.status_text) {
615
+ chatStore.chatInfo.show.appendInfo = $t(msg.status_text)
616
+ setTimeout(() => {
617
+ chatStore.chatInfo.show.appendInfo = undefined
618
+ }, 10000)
619
+ } else {
620
+ // 对方停止输入时,会有一个空的 input_status 消息
621
+ chatStore.chatInfo.show.appendInfo = undefined
622
+ }
623
+ }
624
+ },
625
+ } as { [key: string]: (name: string, msg: { [key: string]: any }) => void }
626
+
627
+ const msgFunctions = {
628
+ /**
629
+ * 修改群成员信息回调
630
+ */
631
+ updateGroupMemberInfo: () => {
632
+ const { $t } = app.config.globalProperties
633
+ const uiStore = useUIStore()
634
+ const chatStore = useChatStore()
635
+ const popInfo = {
636
+ title: $t('操作'),
637
+ html: `<span>${$t('正在确认操作……')}</span>`
638
+ }
639
+ uiStore.popBoxList.push(popInfo)
640
+ // 稍微等一下再刷新成员列表
641
+ delay(1000).then(() => {
642
+ void loadGroupMembersFromCache(String(chatStore.chatInfo.show.id))
643
+ return delay(1000)
644
+ }).then(() => {
645
+ void loadGroupMembersFromCache(String(chatStore.chatInfo.show.id))
646
+ uiStore.popBoxList.shift()
647
+ })
648
+ },
649
+
650
+ /**
651
+ * 保存 Bot 信息
652
+ */
653
+ getVersionInfo: (_: string, msg: { [key: string]: any }) => {
654
+ const data = getMsgData('version_info', msg, msgPath.version_info)[0]
655
+
656
+ if (data) {
657
+ // 如果 runtime 存在(即不是第一次连接),且 app_name 不同,重置 runtime
658
+ const authStore = useAuthStore()
659
+ resetRimtime(
660
+ authStore.botInfo.app_name != data.app_name && !login.status,
661
+ )
662
+
663
+ authStore.botInfo = data
664
+ if (Option.get('open_ga_bot') !== false) {
665
+ const appVersion = data.app_version ? ',' + data.app_version : ''
666
+ const appInfo = data.app_name ? data.app_name + appVersion : '(未知)'
667
+
668
+ sendStatEvent('connect', { method: data.app_name })
669
+ sendIdentifyData({ bot_version: appInfo })
670
+ }
671
+ if (!login.status) {
672
+ // 尝试动态载入对应的 pathMap
673
+ if (data.app_name !== undefined) {
674
+ const getMap = loadJsonMap(data.app_name)
675
+ if (getMap != null) msgPath = getMap
676
+ }
677
+ // 继续获取后续内容
678
+ Connector.send('get_login_info', {}, 'getLoginInfo')
679
+ }
680
+ }
681
+ },
682
+
683
+ /**
684
+ * 保存账号信息
685
+ */
686
+ getLoginInfo: (_: string, msg: { [key: string]: any }) => {
687
+ const msgBody = getMsgData('login_info', msg, msgPath.login_info)
688
+ if (msgBody) {
689
+ const data = msgBody[0]
690
+ const authStore = useAuthStore()
691
+
692
+ // 如果 runtime 存在(即不是第一次连接),且 uin 不同,重置 runtime
693
+ resetRimtime(authStore.loginInfo.uin != data.uin && !login.status)
694
+
695
+ // 完成登陆初始化
696
+ authStore.loginInfo = data
697
+ login.status = true
698
+
699
+ // 保存用户信息到连接历史
700
+ saveConnectionToHistory(login.address, login.token, data.uin, data.nickname)
701
+
702
+ // 显示账户菜单
703
+ updateMenu({
704
+ parent: 'account',
705
+ id: 'userName',
706
+ action: 'label',
707
+ value: data.nickname,
708
+ })
709
+ const title = `${data.nickname} `
710
+ if (backend.platform == 'web') {
711
+ document.title = title + '- Stapxs QQ Lite'
712
+ } else {
713
+ document.title = title
714
+ backend.call(undefined, 'win:setTitle', false, title)
715
+ }
716
+ // 结束登录页面的水波动画
717
+ clearLoginWaveTimer()
718
+ // 跳转标签卡
719
+ const barMsg = document.getElementById('bar-msg')
720
+ if (barMsg != null) barMsg.click()
721
+ // 加载列表消息
722
+ reloadUsers()
723
+ reloadCookies()
724
+ // 尝试加载 QZore 列表
725
+ if (authStore.jsonMap.get_qzone_feed) {
726
+ Connector.send(authStore.jsonMap.get_qzone_feed.name, {}, 'getQzoneFeed')
727
+ }
728
+ }
729
+ },
730
+
731
+ /**
732
+ * 补充登录信息
733
+ * @deprecated 此功能在 OICQ 后的 bot 中没有再实现,暂时保留
734
+ */
735
+ getMoreLoginInfo: (_: string, msg: { [key: string]: any }) => {
736
+ const authStore = useAuthStore()
737
+ authStore.loginInfo.info = msg.data.data.result.buddy.info_list[0]
738
+ },
739
+
740
+ /**
741
+ * 保存好友列表
742
+ */
743
+ getGroupList: (_: string, msg: { [key: string]: any }) => {
744
+ saveUser(msg, 'group')
745
+ },
746
+ getFriendList: (_: string, msg: { [key: string]: any }) => {
747
+ saveUser(msg, 'friend')
748
+ },
749
+
750
+ /**
751
+ * 保存分组信息(独立保存)
752
+ */
753
+ getFriendCategory: (_: string, msg: { [key: string]: any }) => {
754
+ const contactStore = useContactStore()
755
+ const list = getMsgData(
756
+ 'friend_category',
757
+ msg,
758
+ msgPath.friend_category,
759
+ ) as {
760
+ class_id: number
761
+ class_name: string
762
+ sort_id: number
763
+ users: number[]
764
+ }[]
765
+ if (list != undefined) {
766
+ saveClassInfo(list)
767
+ }
768
+ // 刷新用户列表的分类信息
769
+ list.forEach((item) => {
770
+ item.users.forEach((id) => {
771
+ contactStore.userList.forEach((user) => {
772
+ if (user.user_id == id && user.class_id == undefined) {
773
+ user.class_id = item.class_id
774
+ user.class_name = item.class_name
775
+ }
776
+ })
777
+ })
778
+ })
779
+ },
780
+
781
+ /**
782
+ * 获取群成员信息
783
+ */
784
+ getUserInfoInGroup: (_: string, msg: { [key: string]: any }) => {
785
+ const chatStore = useChatStore()
786
+ const payload = getDataObject(getDataObject(msg).data ?? msg)
787
+ const info: Record<string, unknown> = { ...payload }
788
+ const user = getDataObject(payload.user)
789
+ if (info.user_id == null && user.id != null) {
790
+ info.user_id = user.id
791
+ }
792
+ if (info.nickname == null && (user.name != null || user.nick != null)) {
793
+ info.nickname = user.name ?? user.nick
794
+ }
795
+ if (info.card == null && (payload.nick != null || payload.name != null)) {
796
+ info.card = payload.nick ?? payload.name
797
+ }
798
+ const shutUp = Number(info.shut_up_timestamp ?? info.shutup_time ?? 0)
799
+ if (shutUp * 1000 < Date.now()) {
800
+ info.shut_up_timestamp = 0
801
+ }
802
+ chatStore.chatInfo.info.me_info = info
803
+ },
804
+
805
+ /**
806
+ * 保存群成员列表
807
+ */
808
+ getGroupMemberList: (_: string, msg: { [key: string]: any }) => {
809
+ const chatStore = useChatStore()
810
+ const raw = getDataObject(msg).data
811
+ if (!Array.isArray(raw)) return
812
+ const data = raw as GroupMemberInfoElem[]
813
+ const sortAndSaveMembers = () => {
814
+ const adminList = data.filter((item: GroupMemberInfoElem) => {
815
+ return item.role === 'admin'
816
+ })
817
+ adminList.sort((a, b) => {
818
+ if (a.py_start && b.py_start) {
819
+ return a.py_start.charCodeAt(0) - b.py_start.charCodeAt(0)
820
+ }
821
+ return 0
822
+ })
823
+ const createrList = data.filter((item: GroupMemberInfoElem) => {
824
+ return item.role === 'owner'
825
+ })
826
+ const memberList = data.filter((item: GroupMemberInfoElem) => {
827
+ return item.role !== 'admin' && item.role !== 'owner'
828
+ })
829
+ memberList.sort((a, b) => {
830
+ if (a.py_start && b.py_start) {
831
+ return a.py_start.charCodeAt(0) - b.py_start.charCodeAt(0)
832
+ }
833
+ return 0
834
+ })
835
+ chatStore.chatInfo.info.group_members = createrList.concat(adminList.concat(memberList))
836
+ }
837
+
838
+ data.forEach((item: any) => {
839
+ let name: string
840
+ if (item.card != undefined && item.card != '') {
841
+ name = item.card
842
+ } else if (item.nickname != undefined && item.nickname != '') {
843
+ name = item.nickname
844
+ } else {
845
+ name = item.user_id.toString()
846
+ }
847
+
848
+ // 获取拼音首字母
849
+ item.py_start = resolvePinyinFirstChar(name.substring(0, 1))
850
+ })
851
+ sortAndSaveMembers()
852
+
853
+ if (!isPinyinReady()) {
854
+ void ensurePinyinLoaded().then((loaded) => {
855
+ if (!loaded) return
856
+ data.forEach((item: any) => {
857
+ let name: string
858
+ if (item.card != undefined && item.card != '') {
859
+ name = item.card
860
+ } else if (item.nickname != undefined && item.nickname != '') {
861
+ name = item.nickname
862
+ } else {
863
+ name = item.user_id.toString()
864
+ }
865
+ item.py_start = resolvePinyinFirstChar(name.substring(0, 1))
866
+ })
867
+ sortAndSaveMembers()
868
+ })
869
+ }
870
+ },
871
+
872
+ /**
873
+ * 保存聊天记录
874
+ */
875
+ getChatHistoryFist: (_: string, msg: { [key: string]: any }) => {
876
+ const uiStore = useUIStore()
877
+ if (msg.data === null) {
878
+ new PopInfo().add(
879
+ PopType.ERR,
880
+ app.config.globalProperties.$t('获取历史记录失败'),
881
+ )
882
+ uiStore.loadHistoryFail = true
883
+ return
884
+ }
885
+ // 无论是否有本地预填充,都以网络数据替换(保证最新消息不遗漏)
886
+ saveMsg(msg)
887
+ },
888
+ getChatHistoryGapFill: (
889
+ _: string,
890
+ msg: { [key: string]: any },
891
+ metaArgs?: string[],
892
+ ) => {
893
+ const authStore = useAuthStore()
894
+ const chatStore = useChatStore()
895
+ // echo 格式:getChatHistoryGapFill_<anchorMsgId>
896
+ // anchorMsgId 是 gap 之后第一条消息的 message_id(插入点)
897
+ const anchorMsgId = metaArgs?.[1]
898
+ if (!anchorMsgId || msg.data === null) return
899
+ const rawList = getMsgData('message_list', msg, msgPath.message_list)
900
+ getMessageList(rawList)
901
+ .then((list) => {
902
+ if (!list || list.length === 0) return
903
+ const inserted = insertHistorySegmentAtAnchor(
904
+ chatStore.messageList,
905
+ anchorMsgId,
906
+ list,
907
+ )
908
+ if (inserted.length === chatStore.messageList.length) return
909
+ replaceMessageListInPlace(inserted)
910
+ // 同步存入本地 DB,以便下次直接从本地加载
911
+ saveMessagesWithSideEffects(authStore.loginInfo.uin, list)
912
+ })
913
+ .catch(() => {})
914
+ },
915
+ getChatHistory: (_: string, msg: { [key: string]: any }) => {
916
+ const uiStore = useUIStore()
917
+ if (msg.data === null) {
918
+ new PopInfo().add(
919
+ PopType.ERR,
920
+ app.config.globalProperties.$t('获取历史记录失败'),
921
+ )
922
+ uiStore.loadHistoryFail = true
923
+ uiStore.historyBeforeTime = undefined
924
+ uiStore.nowGetHistory = false
925
+ return
926
+ }
927
+ const pan = document.getElementById('msgPan')
928
+ if (pan) {
929
+ const oldScrollHeight = pan.scrollHeight
930
+ saveMsg(msg, 'top').then(() => {
931
+ uiStore.nowGetHistory = false
932
+ nextTick(() => {
933
+ setTimeout(() => {
934
+ logger.debug(`滚动前高度:${oldScrollHeight},当前高度:${pan.scrollHeight},滚动位置:${pan.scrollHeight - oldScrollHeight}`)
935
+ pan.style.scrollBehavior = 'unset'
936
+ // 纠正滚动位置
937
+ pan.scrollTop = pan.scrollHeight - oldScrollHeight
938
+ pan.style.scrollBehavior = 'smooth'
939
+ }, 200);
940
+ })
941
+ })
942
+ }
943
+ },
944
+
945
+ getChatHistoryOnMsg: (
946
+ _: string,
947
+ msg: { [key: string]: any },
948
+ echoList: string[],
949
+ ) => {
950
+ const contactStore = useContactStore()
951
+ const id = String(echoList[1] ?? '')
952
+ if (id) {
953
+ try {
954
+ // 对消息进行一次格式化处理
955
+ let list = getMsgData('message_list', msg, msgPath.message_list)
956
+ if (list != undefined) {
957
+ list = parseMsgList(
958
+ list,
959
+ msgPath.message_list.type,
960
+ msgPath.message_value,
961
+ )
962
+ // 更新消息列表
963
+ const onmsg = contactStore.baseOnMsgList.get(normalizeSessionId(id))
964
+ if (onmsg && list[0]) {
965
+ Object.assign(onmsg, formatMessageData(list[0], Boolean(onmsg.group_id)))
966
+ contactStore.baseOnMsgList.set(normalizeSessionId(id), onmsg)
967
+ updateBaseOnMsgList()
968
+ }
969
+ }
970
+ } catch (e) {
971
+ // do nothing
972
+ }
973
+ }
974
+ },
975
+
976
+ /**
977
+ * 发送消息后处理
978
+ */
979
+ sendMsgBack: (
980
+ _: string,
981
+ msg: { [key: string]: any },
982
+ echoList: string[],
983
+ ) => {
984
+ const authStore = useAuthStore()
985
+ const chatStore = useChatStore()
986
+ const rawData = msg.data
987
+ const firstData = Array.isArray(rawData) ? rawData[0] : rawData
988
+ const realMessageId = getString(msg.message_id)
989
+ || getString(getDataObject(rawData).message_id)
990
+ || getString(getDataObject(firstData).id)
991
+ || getString(getDataObject(firstData).message_id)
992
+ if (realMessageId) msg.message_id = realMessageId
993
+ if (echoList[1] == 'forward') {
994
+ // PS:这儿写是写了转发成功,事实上不确定消息有没有真的发送出去(x
995
+ popInfo.add(
996
+ PopType.INFO,
997
+ app.config.globalProperties.$t('消息已转发'),
998
+ )
999
+ } else if (echoList[1] == 'uuid') {
1000
+ const messageId = echoList[2]
1001
+ // 去 messagelist 里找到这条消息
1002
+ chatStore.messageList.forEach((item) => {
1003
+ if (item.message_id == messageId) {
1004
+ item.message_id = msg.message_id
1005
+ item.fake_msg = false
1006
+ item.revoke = false
1007
+ return
1008
+ }
1009
+ })
1010
+ const sentItem = chatStore.messageList.find((item) => {
1011
+ return String(item.fake_message_id ?? item.message_id) === String(messageId)
1012
+ })
1013
+ const chatInfo = chatStore.chatInfo.show
1014
+ const channelType = chatInfo.type === 'group' ? 'group' as const : 'user' as const
1015
+ const channelId = getString(msg.channel_id)
1016
+ || getString(msg.channelId)
1017
+ || getString(chatInfo.channel_id)
1018
+ || String(chatInfo.id)
1019
+ const guildId = getString(msg.guild_id) || getString(msg.guildId) || getString(chatInfo.guild_id)
1020
+ if (realMessageId) {
1021
+ void saveSentSelfMessage({
1022
+ id: messageId,
1023
+ platform: getString(authStore.loginInfo.platform),
1024
+ selfId: getString(authStore.loginInfo.uin),
1025
+ channelId,
1026
+ guildId: guildId || undefined,
1027
+ channelType,
1028
+ messageId: realMessageId,
1029
+ content: getString(sentItem?.raw_message) || getMsgRawTxt(sentItem ?? {}),
1030
+ message: Array.isArray(sentItem?.message) ? sentItem.message : [],
1031
+ source: 'webui',
1032
+ sentAt: Number(sentItem?.local_time ?? sentItem?.timestamp_ms ?? Date.now()),
1033
+ kind: String(sentItem?.message?.[0]?.type ?? 'text'),
1034
+ })
1035
+ }
1036
+ // 请求消息内容
1037
+ // PS:其实有消息通知的情况下不需要再去主动获取了
1038
+ // 但是为了兼容没有开启自身消息通知的情况,还是保留了这个功能
1039
+ Connector.send(
1040
+ authStore.jsonMap.get_message?.name ?? 'get_msg',
1041
+ { message_id: msg.message_id },
1042
+ 'getSendMsg_' + msg.message_id,
1043
+ )
1044
+ }
1045
+ },
1046
+ sendFileBack: (
1047
+ _: string,
1048
+ msg: { [key: string]: any },
1049
+ echoList: string[],
1050
+ ) => {
1051
+ // 标记上传任务完成
1052
+ if (echoList[1] === 'task' && echoList[2] && echoList[3]) {
1053
+ const taskId = echoList[1] + '_' + echoList[2] + '_' + echoList[3]
1054
+ completeUploadTask(taskId)
1055
+ }
1056
+ const newEchoList = ['sendMsgBack', ...echoList.slice(4)]
1057
+ msgFunctions['sendMsgBack'](_, msg, newEchoList)
1058
+ },
1059
+ /**
1060
+ * 获取收藏表情
1061
+ */
1062
+ getRoamingStamp: (
1063
+ _: string,
1064
+ msg: { [key: string]: any },
1065
+ echoList: string[],
1066
+ ) => {
1067
+ const authStore = useAuthStore()
1068
+ const getCount = Number(echoList[1])
1069
+ const data = msg.data
1070
+ if (msgPath.roaming_stamp.reverse) {
1071
+ data.reverse()
1072
+ }
1073
+ const stickerStore = useStickerStore()
1074
+ const stickerCache = stickerStore.stickerCache ?? []
1075
+ if (stickerCache.length == 0) {
1076
+ stickerStore.stickerCache = data
1077
+ } else if (authStore.jsonMap.roaming_stamp.pagerType == 'full') {
1078
+ // 全量分页模式下不追加
1079
+ if (getCount > stickerCache.length + 48) {
1080
+ // 已经获取到所有内容了
1081
+ data.push('end')
1082
+ }
1083
+ stickerStore.stickerCache = data
1084
+ } else {
1085
+ stickerStore.stickerCache = stickerCache.concat(data)
1086
+ }
1087
+ },
1088
+
1089
+ /**
1090
+ * 保存群补充信息
1091
+ * @deprecated 功能在后期更新中未被重构检查,可能存在问题
1092
+ */
1093
+ getMoreGroupInfo: (_: string, msg: { [key: string]: any }) => {
1094
+ const chatStore = useChatStore()
1095
+ chatStore.chatInfo.info.group_info = msg.data.data
1096
+ },
1097
+
1098
+ /**
1099
+ * 保存好友补充信息
1100
+ * @deprecated 功能在后期更新中未被重构检查,可能存在问题
1101
+ */
1102
+ getMoreUserInfo: (_: string, msg: { [key: string]: any }) => {
1103
+ const chatStore = useChatStore()
1104
+ // chatStore.chatInfo.info.user_info =
1105
+ // msg.data.data.result.buddy.info_list[0]
1106
+ const data = getMsgData('friend_info', msg, msgPath.friend_info)[0]
1107
+ data.regTime = new Date(data.reg_time).getTime()
1108
+ if (data) {
1109
+ chatStore.chatInfo.info.user_info = data
1110
+ }
1111
+ },
1112
+
1113
+ /**
1114
+ * 获取群通知
1115
+ */
1116
+ getGroupNotices: (_: string, msg: { [key: string]: any }) => {
1117
+ const chatStore = useChatStore()
1118
+ const list = getMsgData('group_notices', msg, msgPath.group_notices)
1119
+ if (!list) return
1120
+
1121
+ // 组装img信息
1122
+ let lastImg: Img | undefined
1123
+ for (const notice of list) {
1124
+ if (!notice.img_id || notice.img_id.length == 0) continue
1125
+ const img = markRaw(new Img(
1126
+ `https://p.qlogo.cn/gdynamic/${notice.img_id}/0/`
1127
+ ))
1128
+ if (lastImg) img.insertPrev(lastImg)
1129
+ notice.img = img
1130
+ lastImg = img
1131
+ }
1132
+ chatStore.chatInfo.info.group_notices = list
1133
+ },
1134
+
1135
+ /**
1136
+ * 获取群文件列表
1137
+ */
1138
+ getGroupFiles: (_: string, msg: { [key: string]: any }) => {
1139
+ const chatStore = useChatStore()
1140
+ const list = getMsgData('group_files', msg, msgPath.group_files) as (GroupFileElem & GroupFileFolderElem)[]
1141
+ // 排序;文件夹在前,文件在后
1142
+ const folderList = list.filter((item) => {
1143
+ return item.folder_id
1144
+ })
1145
+ const fileList = list.filter((item) => {
1146
+ return item.file_id
1147
+ })
1148
+ // 对它们各自排序,文件夹按照 create_time 降序,文件按照 upload_time 降序
1149
+ folderList.sort((a, b) => {
1150
+ return b.create_time - a.create_time
1151
+ })
1152
+ fileList.sort((a, b) => {
1153
+ return b.upload_time - a.upload_time
1154
+ })
1155
+ // 合并
1156
+ chatStore.chatInfo.info.group_files = folderList.concat(fileList)
1157
+ },
1158
+
1159
+ /**
1160
+ * 获取群文件文件夹文件
1161
+ */
1162
+ getGroupDirFiles: (_: string, msg: { [key: string]: any }, echoList: string[]) => {
1163
+ const chatStore = useChatStore()
1164
+ // TODO: 有分页
1165
+
1166
+ // 默认使用主目录相同的结构,如果存在子目录结构的定义则使用子目录的结构
1167
+ let map = msgPath.group_files
1168
+ if (msgPath.group_folder_files.source) {
1169
+ map = msgPath.group_folder
1170
+ }
1171
+ const list = getMsgData('group_files', msg, map) as (GroupFileElem & GroupFileFolderElem)[]
1172
+ // 排序;文件夹在前,文件在后
1173
+ const folderList = list.filter((item) => {
1174
+ return item.folder_id
1175
+ })
1176
+ const fileList = list.filter((item) => {
1177
+ return item.file_id
1178
+ })
1179
+ // 对它们各自排序,文件夹按照 create_time 降序,文件按照 upload_time 降序
1180
+ folderList.sort((a, b) => {
1181
+ return b.create_time - a.create_time
1182
+ })
1183
+ fileList.sort((a, b) => {
1184
+ return b.upload_time - a.upload_time
1185
+ })
1186
+ // 寻找 item
1187
+ const folderId = echoList[1]
1188
+ const folder = chatStore.chatInfo.info.group_files.find((item) => {
1189
+ return item.folder_id == folderId
1190
+ })
1191
+ if (folder) {
1192
+ folder.items = fileList
1193
+ }
1194
+ },
1195
+
1196
+ /**
1197
+ * 下载文件(聊天中)
1198
+ */
1199
+ downloadFile: (_: string, msg: { [key: string]: any }, echoList: string[]) => {
1200
+ const data = getMsgData('file_download', msg, msgPath.file_download)[0]
1201
+ const url = data.file_url
1202
+
1203
+ const fileName = decodeURIComponent(atob(echoList[2]))
1204
+ const fileSize = data.file_size || 0
1205
+
1206
+ // 使用文件传输管理器下载
1207
+ addDownloadTask({
1208
+ fileName,
1209
+ fileSize,
1210
+ filePath: '',
1211
+ url
1212
+ })
1213
+ },
1214
+
1215
+ /**
1216
+ * 下载文件(群文件)
1217
+ */
1218
+ downloadGroupFile: (_: string, msg: { [key: string]: any }, echoList: string[]) => {
1219
+ const data = getMsgData('file_download', msg, msgPath.file_download)[0]
1220
+ const url = data.file_url
1221
+
1222
+ const fileName = decodeURIComponent(atob(echoList[2]))
1223
+ const fileSize = data.file_size || 0
1224
+
1225
+ // 使用文件传输管理器下载
1226
+ addDownloadTask({
1227
+ fileName,
1228
+ fileSize,
1229
+ filePath: '',
1230
+ url
1231
+ })
1232
+ },
1233
+
1234
+ /**
1235
+ * 文件预览下载
1236
+ */
1237
+ loadFileBase: (
1238
+ _: string,
1239
+ msg: { [key: string]: any },
1240
+ echoList: string[],
1241
+ ) => {
1242
+ const chatStore = useChatStore()
1243
+ const data = getMsgData('file_download', msg, msgPath.file_download)[0]
1244
+ let url = data.file_url
1245
+ const msgId = echoList[1]
1246
+ const ext = echoList[2]
1247
+ if (url) {
1248
+ // 寻找消息
1249
+ const msg = chatStore.messageList.find((item) => {
1250
+ return item.message_id == msgId
1251
+ })
1252
+ if (msg) {
1253
+ if (document.location.protocol == 'https:') {
1254
+ // 判断文件 URL 的协议
1255
+ // PS:Chrome 不会对 http 文件进行协议升级
1256
+ if (url.toLowerCase().startsWith('http:')) {
1257
+ url = 'https' + url.substring(url.indexOf('://'))
1258
+ }
1259
+ }
1260
+ msg.fileView.url = url
1261
+ msg.fileView.ext = ext
1262
+ }
1263
+ }
1264
+ },
1265
+
1266
+ /**
1267
+ * 保存精华消息
1268
+ */
1269
+ getJin: (_: string, msg: { [key: string]: any }) => {
1270
+ const chatStore = useChatStore()
1271
+ const jinList = getMsgData('group_essence', msg, msgPath.group_essence)
1272
+ const is_end = getMsgData(
1273
+ 'is_end',
1274
+ msg,
1275
+ msgPath.group_essence.is_end,
1276
+ ) ?? [true]
1277
+ if (jinList && is_end) {
1278
+ if (chatStore.chatInfo.info.jin_info.list.length == 0) {
1279
+ chatStore.chatInfo.info.jin_info.list = jinList
1280
+ } else {
1281
+ const now_page = chatStore.chatInfo.info.jin_info.pages ?? 0
1282
+
1283
+ chatStore.chatInfo.info.jin_info.list =
1284
+ chatStore.chatInfo.info.jin_info.list.concat(jinList)
1285
+ chatStore.chatInfo.info.jin_info.pages = now_page + 1
1286
+ }
1287
+ chatStore.chatInfo.info.jin_info.is_end = is_end[0]
1288
+ }
1289
+ },
1290
+
1291
+ /**
1292
+ * 获取发送的消息(消息发送后处理)
1293
+ * @deprecated 功能已被遗弃,暂时保留方法
1294
+ */
1295
+ getSendMsg: (
1296
+ _: string,
1297
+ msg: { [key: string]: any },
1298
+ echoList: string[],
1299
+ ) => {
1300
+ const authStore = useAuthStore()
1301
+ const chatStore = useChatStore()
1302
+ const msgInfo = getMsgData('message_info', msg.data, msgPath.message_info)
1303
+ if (msgInfo) {
1304
+ const info = msgInfo[0]
1305
+ if (echoList[1] !== info.message_id.toString()) {
1306
+ // 返回的不是这条消息,重新请求
1307
+ setTimeout(() => {
1308
+ Connector.send(
1309
+ authStore.jsonMap.get_message?.name ?? 'get_msg',
1310
+ { message_id: echoList[1] },
1311
+ 'getSendMsg_' + echoList[1]
1312
+ )
1313
+ }, 5000)
1314
+ } else {
1315
+ // 列表内最近的一条 fake_msg(倒序查找)
1316
+ let fakeMsg = null as any
1317
+ for (let i = chatStore.messageList.length - 1; i > 0; i--) {
1318
+ const msg = chatStore.messageList[i]
1319
+ if (msg.fake_msg != undefined && info.sender == authStore.loginInfo.uin) {
1320
+ fakeMsg = msg
1321
+ break
1322
+ }
1323
+ }
1324
+ // 预发送消息刷新
1325
+ if (fakeMsg != null) {
1326
+ // 将这条消息直接替换掉
1327
+ const trueMsg = getMsgData(
1328
+ 'message_list',
1329
+ buildMsgList([msg.data]),
1330
+ msgPath.message_list,
1331
+ )
1332
+ getMessageList(trueMsg).then((trueMsg) => {
1333
+ if (trueMsg?.length == 1) {
1334
+ // 使用消息对象引用直接更新,避免索引问题
1335
+ fakeMsg.message = trueMsg[0].message
1336
+ fakeMsg.raw_message = trueMsg[0].raw_message
1337
+ fakeMsg.time = trueMsg[0].time
1338
+ fakeMsg.fake_msg = undefined
1339
+ fakeMsg.revoke = false
1340
+ }
1341
+ })
1342
+ }
1343
+ }
1344
+ }
1345
+ },
1346
+
1347
+ /**
1348
+ * 设置消息已读
1349
+ */
1350
+ readMemberMessage: (_: string, msg: { [key: string]: any }) => {
1351
+ const authStore = useAuthStore()
1352
+ const data = msg.data[0]
1353
+ const msgName = authStore.jsonMap.set_message_read.private_name
1354
+ let private_name = authStore.jsonMap.set_message_read.private_name
1355
+ if (!private_name) private_name = msgName
1356
+ if (data.group_id != undefined) {
1357
+ Connector.send(
1358
+ msgName,
1359
+ {
1360
+ message_id: data.message_id,
1361
+ group_id: data.group_id,
1362
+ },
1363
+ 'setMessageRead',
1364
+ )
1365
+ } else {
1366
+ Connector.send(
1367
+ private_name,
1368
+ {
1369
+ message_id: data.message_id,
1370
+ user_id: data.self_id,
1371
+ },
1372
+ 'setMessageRead',
1373
+ )
1374
+ }
1375
+ // 关闭所有通知
1376
+ new Notify().closeAll(data.group_id ?? data.self_id)
1377
+ },
1378
+
1379
+ /**
1380
+ * 系统通知后处理
1381
+ */
1382
+ setFriendAdd: updateSysInfo,
1383
+ setGroupAdd: updateSysInfo,
1384
+
1385
+ /**
1386
+ * 获取会话历史
1387
+ */
1388
+ getRecentContact: (_: string, data: any) => {
1389
+ const authStore = useAuthStore()
1390
+ const contactStore = useContactStore()
1391
+ const settingsStore = useSettingsStore()
1392
+ const list = getMsgData('recent_contact', data, msgPath.recent_contact)
1393
+ if (list != undefined) {
1394
+ // user_id: /peerUin
1395
+ // time: /msgTime
1396
+ // chat_type: /chatType
1397
+ // 过滤掉 chatType 不是 1 和 2 的
1398
+ let back = list.filter((item) => {
1399
+ return item.chat_type == 1 || item.chat_type == 2
1400
+ })
1401
+ // 排除掉在置顶列表里的
1402
+ const topList = settingsStore.sysConfig.top_info as {
1403
+ [key: string]: number[]
1404
+ } | null
1405
+ if (topList != null) {
1406
+ const top = topList[authStore.loginInfo.uin]
1407
+ if (top != undefined) {
1408
+ back = back.filter((item) => {
1409
+ return top.indexOf(Number(item.user_id)) == -1
1410
+ })
1411
+ }
1412
+ }
1413
+ // 去重
1414
+ back = back.filter((item, index, arr) => {
1415
+ return (
1416
+ arr.findIndex((item2) => {
1417
+ return item2.user_id == item.user_id
1418
+ }) == index
1419
+ )
1420
+ })
1421
+ back.forEach((item) => {
1422
+ // 去消息列表里找一下它
1423
+ const user = contactStore.userList.find((user) => {
1424
+ return user.user_id == item.user_id || user.group_id == item.user_id
1425
+ })
1426
+ if (user) {
1427
+ contactStore.baseOnMsgList.set(String(item.user_id), user)
1428
+ updateLastestHistory(user)
1429
+ }
1430
+ })
1431
+ }
1432
+ // “显示全部会话”会包含 recent_contact 之外的群;限流补取这些群的最后一条历史。
1433
+ groupPreviewHydrator.scheduleMissingSessions()
1434
+ },
1435
+
1436
+ /**
1437
+ * 表情回应后处理
1438
+ */
1439
+ SendRespondBack: (
1440
+ _: string,
1441
+ __: { [key: string]: any },
1442
+ echoList: string[],
1443
+ ) => {
1444
+ const chatStore = useChatStore()
1445
+ const msgId = echoList[1]
1446
+ const id = Number(echoList[2])
1447
+ // 从消息列表中找到这条消息
1448
+ chatStore.messageList.forEach((item, index) => {
1449
+ if (item.message_id === msgId) {
1450
+ if (chatStore.messageList[index].emoji_like) {
1451
+ // 寻找有没有 emoji_id 相同的
1452
+ let hasAdd = false
1453
+ chatStore.messageList[index].emoji_like.forEach(
1454
+ (item: { emoji_id: number; count: number }) => {
1455
+ if (item.emoji_id == id) {
1456
+ item.count++
1457
+ hasAdd = true
1458
+ }
1459
+ },
1460
+ )
1461
+ if (!hasAdd) {
1462
+ chatStore.messageList[index].emoji_like.push({
1463
+ emoji_id: id,
1464
+ count: 1,
1465
+ })
1466
+ }
1467
+ } else {
1468
+ chatStore.messageList[index].emoji_like = [
1469
+ { emoji_id: id, count: 1 },
1470
+ ]
1471
+ }
1472
+ }
1473
+ })
1474
+ },
1475
+
1476
+ /**
1477
+ * 获取 cookie
1478
+ * @deprecated 暂时没用到他
1479
+ */
1480
+ getCookies: (
1481
+ _: string,
1482
+ msg: { [key: string]: any },
1483
+ echoList: string[],
1484
+ ) => {
1485
+ const authStore = useAuthStore()
1486
+ // 拆分 cookie
1487
+ const cookieObject = {} as { [key: string]: string }
1488
+ msg.data.cookies.split('; ').forEach((item: string) => {
1489
+ const key = item.split('=')[0]
1490
+ const value = item.split('=')[1]
1491
+ cookieObject[key] = value
1492
+ })
1493
+ // 计算 bkn
1494
+ const skey = cookieObject['skey'] || ''
1495
+ let hash = 5381
1496
+
1497
+ for (let i = 0; i < skey.length; i++) {
1498
+ hash += (hash << 5) + skey.charCodeAt(i)
1499
+ }
1500
+ // 保存 cookie 和 bkn
1501
+ const domain = echoList[1]
1502
+ if (!authStore.loginInfo.webapi) authStore.loginInfo.webapi = {}
1503
+ if (!authStore.loginInfo.webapi[domain])
1504
+ authStore.loginInfo.webapi[domain] = {}
1505
+ authStore.loginInfo.webapi[domain].cookie = cookieObject
1506
+ authStore.loginInfo.webapi[domain].bkn = (
1507
+ hash & 0x7fffffff
1508
+ ).toString()
1509
+ },
1510
+
1511
+ /**
1512
+ * 设置消息已读回调
1513
+ */
1514
+ setMessageRead() {
1515
+ // do nothing
1516
+ },
1517
+
1518
+ /**
1519
+ * 获取 QQ 空间推送列表
1520
+ * @param _
1521
+ * @param msg
1522
+ */
1523
+ getQzoneFeed: (_: string, msg: { [key: string]: any }) => {
1524
+ const qzoneStore = useQzoneStore()
1525
+ const list = getMsgData('get_qzone_feed', msg, msgPath.get_qzone_feed)
1526
+ if (list) {
1527
+ qzoneStore.state.currentView = 'feed'
1528
+ qzoneStore.qzoneFeedList = list
1529
+ }
1530
+ },
1531
+
1532
+ /**
1533
+ * 获取 QQ 空间“我的”列表
1534
+ * @param _
1535
+ * @param msg
1536
+ */
1537
+ getQzoneMsg: (_: string, msg: { [key: string]: any }) => {
1538
+ const qzoneStore = useQzoneStore()
1539
+ const list = getMsgData('get_qzone_msg', msg, msgPath.get_qzone_msg)
1540
+ if (list) {
1541
+ qzoneStore.state.currentView = 'my'
1542
+ if (qzoneStore.state.myPagePos === 0) {
1543
+ qzoneStore.qzoneFeedList = list
1544
+ } else {
1545
+ qzoneStore.qzoneFeedList = [
1546
+ ...qzoneStore.qzoneFeedList,
1547
+ ...list,
1548
+ ]
1549
+ }
1550
+ qzoneStore.state.myHasMore = list.length >= qzoneStore.state.myPageSize
1551
+ }
1552
+ qzoneStore.state.myLoading = false
1553
+ }
1554
+ } as {
1555
+ [key: string]: (
1556
+ name: string,
1557
+ msg: { [key: string]: any },
1558
+ echoList?: string[],
1559
+ ) => void
1560
+ }
1561
+
1562
+ const handlers: Record<string, (payload: any, metaArgs?: string[]) => void> = {
1563
+ ...(Object.entries(msgFunctions).reduce((acc, [key, fn]) => ({
1564
+ ...acc,
1565
+ [key]: (payload: any, metaArgs?: string[]) => fn(key, payload, metaArgs)
1566
+ }), {})),
1567
+ ...(Object.entries(noticeFunctions).reduce((acc, [key, fn]) => ({
1568
+ ...acc,
1569
+ [key]: (payload: any) => fn(key, payload)
1570
+ }), {}))
1571
+ };
1572
+
1573
+ // ==========================================
1574
+
1575
+ function saveUser(msg: { [key: string]: any }, type: string) {
1576
+ const authStore = useAuthStore()
1577
+ const contactStore = useContactStore()
1578
+ const settingsStore = useSettingsStore()
1579
+ listLoadTimes++
1580
+ let list: any[] | undefined
1581
+ const payload = getDataObject(msg)
1582
+ if (Array.isArray(payload.data)) {
1583
+ list = payload.data as any[]
1584
+ } else if (msgPath.user_list) {
1585
+ list = getMsgData('user_list', msg, msgPath.user_list)
1586
+ } else {
1587
+ switch (type) {
1588
+ case 'friend':
1589
+ list = getMsgData('friend_list', msg, msgPath.friend_list)
1590
+ if (list)
1591
+ // 根据 user_id 去重
1592
+ list = list.filter((item, index, arr) => {
1593
+ return (
1594
+ arr.findIndex((item2) => {
1595
+ return item2.user_id == item.user_id
1596
+ }) == index
1597
+ )
1598
+ })
1599
+ break
1600
+ case 'group':
1601
+ list = getMsgData('group_list', msg, msgPath.group_list)
1602
+ if (list)
1603
+ // 根据 group_id 去重
1604
+ list = list.filter((item, index, arr) => {
1605
+ return (
1606
+ arr.findIndex((item2) => {
1607
+ return item2.group_id == item.group_id
1608
+ }) == index
1609
+ )
1610
+ })
1611
+ break
1612
+ }
1613
+ }
1614
+ if (list != undefined) {
1615
+ list = list.filter((item) => {
1616
+ const id = item.user_id ?? item.group_id
1617
+ return id !== undefined && id !== null && String(id) !== '0'
1618
+ })
1619
+ }
1620
+ if (list != undefined) {
1621
+ const groupNames = {} as { [key: number]: string }
1622
+ list.forEach((item, index) => {
1623
+ if (item.group_name == null || item.group_name == undefined) {
1624
+ item.group_name = ''
1625
+ }
1626
+ if (list?.[index]) {
1627
+ list[index].py_name = { main: [], short: [] }
1628
+ list[index].py_start = ' '
1629
+ }
1630
+ // 构建分类
1631
+ if (type == 'friend') {
1632
+ if (item.class_id != undefined && item.class_name) {
1633
+ if (typeof item.class_name == 'string') {
1634
+ groupNames[item.class_id] = item.class_name
1635
+ } else {
1636
+ groupNames[item.class_id] = item.class_name[0]
1637
+ }
1638
+ }
1639
+ delete item.group_name
1640
+ } else {
1641
+ delete item.class_id
1642
+ delete item.class_name
1643
+ }
1644
+ })
1645
+ if (Object.keys(groupNames).length > 0) {
1646
+ // 把 groupNames 处理为 { class_id: number, class_name: string }[]
1647
+ const groupNamesList = [] as {
1648
+ class_id: number
1649
+ class_name: string
1650
+ }[]
1651
+ for (const key in groupNames) {
1652
+ groupNamesList.push({
1653
+ class_id: Number(key),
1654
+ class_name: groupNames[key],
1655
+ })
1656
+ }
1657
+ saveClassInfo(groupNamesList)
1658
+ } else if (type == 'friend') {
1659
+ // Satori 的好友接口不返回分组时,统一归入“我的好友”
1660
+ settingsStore.classes = [{
1661
+ class_id: 0,
1662
+ class_name: '我的好友',
1663
+ sort_id: 0,
1664
+ }]
1665
+ }
1666
+ if (isPinyinReady()) {
1667
+ buildPinyinForContacts(list)
1668
+ } else {
1669
+ hydrateContactPinyinLater(list)
1670
+ }
1671
+ sortContactListByPinyin(list)
1672
+ // 实时消息可能比联系人列表更早到达;用真实联系人资料接管临时会话,保留预览状态。
1673
+ const didMergeEarlySessions = mergeEarlySessionContacts(
1674
+ list,
1675
+ contactStore.baseOnMsgList,
1676
+ )
1677
+ const existingIds = new Set(
1678
+ contactStore.userList.map((item) => {
1679
+ return String(item.user_id ?? item.group_id ?? '')
1680
+ }),
1681
+ )
1682
+ const freshList = list.filter((item) => {
1683
+ return !existingIds.has(String(item.user_id ?? item.group_id ?? ''))
1684
+ })
1685
+ contactStore.userList = contactStore.userList.concat(freshList)
1686
+ if (
1687
+ settingsStore.sysConfig.session_display_mode === 'all' ||
1688
+ didMergeEarlySessions
1689
+ ) {
1690
+ updateBaseOnMsgList()
1691
+ }
1692
+ // 刷新置顶列表
1693
+ const info = settingsStore.sysConfig.top_info as {
1694
+ [key: string]: number[]
1695
+ } | null
1696
+ if (info != null) {
1697
+ const topList = info[authStore.loginInfo.uin]
1698
+ if (topList !== undefined) {
1699
+ list.forEach((item) => {
1700
+ const id = Number(
1701
+ item.user_id ? item.user_id : item.group_id,
1702
+ )
1703
+ if (topList.indexOf(id) >= 0) {
1704
+ item.always_top = true
1705
+ // 判断它在不在消息列表里
1706
+ if (contactStore.baseOnMsgList.get(normalizeSessionId(id)) == undefined) {
1707
+ contactStore.baseOnMsgList.set(normalizeSessionId(id), item)
1708
+ // 给它获取一下最新的一条消息
1709
+ // 给置顶的用户刷新最新一条的消息用于显示
1710
+ contactStore.userList.forEach((item) => {
1711
+ if (item.always_top) {
1712
+ updateLastestHistory(item)
1713
+ }
1714
+ })
1715
+ }
1716
+ }
1717
+ })
1718
+ }
1719
+ }
1720
+ // 更新菜单
1721
+ updateMenu({
1722
+ parent: 'account',
1723
+ id: 'userList',
1724
+ action: 'label',
1725
+ value: app.config.globalProperties.$t('用户列表({count})', {
1726
+ count: contactStore.userList.length,
1727
+ }),
1728
+ })
1729
+ }
1730
+ // 如果获取次数大于 0 并且是双数,刷新一下历史会话
1731
+ if (listLoadTimes > 0 && listLoadTimes % 2 == 0) {
1732
+ // 获取最近的会话
1733
+ if (authStore.jsonMap.recent_contact)
1734
+ Connector.send(
1735
+ authStore.jsonMap.recent_contact.name,
1736
+ {},
1737
+ 'getRecentContact',
1738
+ )
1739
+ }
1740
+ // 如果是分离式的好友列表,继续获取分类信息
1741
+ if (type == 'friend' && authStore.jsonMap.friend_category) {
1742
+ Connector.send(
1743
+ authStore.jsonMap.friend_category.name,
1744
+ {},
1745
+ 'getFriendCategory',
1746
+ )
1747
+ }
1748
+ }
1749
+
1750
+ function saveClassInfo(
1751
+ list: { class_id: number; class_name: string; sort_id?: number }[],
1752
+ ) {
1753
+ const settingsStore = useSettingsStore()
1754
+ if (list[0].sort_id != undefined) {
1755
+ // 如果有 sort_id,按 sort_id 排序,从小到大
1756
+ list.sort((a, b) => {
1757
+ if (a.sort_id && b.sort_id) return a.sort_id - b.sort_id
1758
+ else return 0
1759
+ })
1760
+ } else {
1761
+ // 按 class_id 排序
1762
+ list.sort((a, b) => {
1763
+ return a.class_id - b.class_id
1764
+ })
1765
+ }
1766
+
1767
+ settingsStore.classes = list
1768
+ }
1769
+
1770
+ async function saveMsg(msg: any, append = undefined as undefined | string) {
1771
+ const uiStore = useUIStore()
1772
+ const authStore = useAuthStore()
1773
+ const chatStore = useChatStore()
1774
+ const contactStore = useContactStore()
1775
+ const settingsStore = useSettingsStore()
1776
+ let list = await normalizeMessagesFromPayload(msg)
1777
+ if (list != undefined) {
1778
+ const historyBeforeTime = Number(uiStore.historyBeforeTime)
1779
+ const hasHistoryBeforeTime = Number.isFinite(historyBeforeTime)
1780
+
1781
+ // 检查消息是否是当前聊天的消息
1782
+ const firstMsg = list[0]
1783
+ const infoList = getMsgData(
1784
+ 'message_info',
1785
+ firstMsg,
1786
+ msgPath.message_info,
1787
+ )
1788
+ if (infoList != undefined) {
1789
+ const info = infoList[0]
1790
+ const id = info.group_id ?? info.private_id
1791
+ if (id != undefined && id != chatStore.chatInfo.show.id) {
1792
+ return
1793
+ }
1794
+ }
1795
+ // 将消息中 message 字段为空数组的消息过滤掉
1796
+ list = list.filter((item: any) => {
1797
+ return item.message.length > 0
1798
+ })
1799
+
1800
+ // 上拉历史时按时间戳作为边界(兼容增量/全量两种分页模式)。
1801
+ if (hasHistoryBeforeTime && append === 'top') {
1802
+ list = list.filter((item: any) => {
1803
+ const t = Number(item?.time)
1804
+ return Number.isFinite(t) && t <= historyBeforeTime
1805
+ })
1806
+ }
1807
+
1808
+ // 处于上拉边界过滤时,若结果为空则保留当前列表,避免误清空。
1809
+ if (hasHistoryBeforeTime && append === 'top' && list.length < 1) {
1810
+ uiStore.historyBeforeTime = undefined
1811
+ uiStore.nowGetHistory = false
1812
+ return
1813
+ }
1814
+
1815
+ // 保存到本地历史
1816
+ saveMessagesWithSideEffects(authStore.loginInfo.uin, list)
1817
+ // 如果分页不是增量的,就不使用追加
1818
+ if (
1819
+ append == 'top' &&
1820
+ authStore.jsonMap.message_list?.pagerType == 'full'
1821
+ ) {
1822
+ append = undefined
1823
+ }
1824
+ // 追加处理
1825
+ if (append != undefined) {
1826
+ // 没有更旧的消息能加载了,禁用允许加载标志
1827
+ if (list.length < 1) {
1828
+ uiStore.canLoadHistory = false
1829
+ uiStore.historyBeforeTime = undefined
1830
+ return
1831
+ }
1832
+ const merged = mergeMessagesByIdAndTime(chatStore.messageList, list)
1833
+ replaceMessageListInPlace(merged)
1834
+ } else {
1835
+ if (
1836
+ settingsStore.sysConfig.enable_local_history &&
1837
+ settingsStore.sysConfig.mixed_load_messages !== false
1838
+ ) {
1839
+ const merged = mergeMessagesByIdAndTime(chatStore.messageList, list)
1840
+ replaceMessageListInPlace(merged)
1841
+ } else {
1842
+ replaceMessageListInPlace(list)
1843
+ }
1844
+ }
1845
+ // 消息后处理
1846
+ // PS: 部分消息类型可能需要获取附加内容,在此处进行处理
1847
+ chatStore.messageList.forEach((item) => {
1848
+ sendMsgAppendInfo(item)
1849
+ })
1850
+ // 将最新消息同步到会话列表;通过会话 Map 更新以触发 shallowRef 列表刷新。
1851
+ const lastMsg =
1852
+ chatStore.messageList[chatStore.messageList.length - 1]
1853
+ if (lastMsg) {
1854
+ const user = contactStore.userList.find((item) => {
1855
+ return (
1856
+ item.group_id == chatStore.chatInfo.show.id ||
1857
+ item.user_id == chatStore.chatInfo.show.id
1858
+ )
1859
+ })
1860
+ const sessionId = normalizeSessionId(chatStore.chatInfo.show.id)
1861
+ const session = contactStore.baseOnMsgList.get(sessionId) ?? user
1862
+ if (session) {
1863
+ const preview = formatMessageData(
1864
+ lastMsg,
1865
+ chatStore.chatInfo.show.type == 'group',
1866
+ )
1867
+ if (user) Object.assign(user, preview)
1868
+ Object.assign(session, preview)
1869
+ contactStore.baseOnMsgList.set(sessionId, session)
1870
+ updateBaseOnMsgList()
1871
+ }
1872
+ }
1873
+
1874
+ if (hasHistoryBeforeTime) {
1875
+ uiStore.historyBeforeTime = undefined
1876
+ }
1877
+ }
1878
+ }
1879
+
1880
+ async function normalizeMessagesFromPayload(payload: any): Promise<any[] | undefined> {
1881
+ const rawList = getMsgData('message_list', payload, msgPath.message_list)
1882
+ return getMessageList(rawList)
1883
+ }
1884
+
1885
+ export async function normalizeMessagesForPreview(payload: any): Promise<any[]> {
1886
+ const authStore = useAuthStore()
1887
+ const map = authStore.jsonMap
1888
+
1889
+ if (
1890
+ payload &&
1891
+ payload.post_type === 'message' &&
1892
+ Array.isArray(payload.message)
1893
+ ) {
1894
+ const directList = parseMsgList(
1895
+ [payload],
1896
+ map?.message_list?.type ?? '$',
1897
+ map?.message_value,
1898
+ )
1899
+
1900
+ if (directList.length === 0) return []
1901
+ return Promise.all(directList.map((item) => msgPreprocess(item)))
1902
+ }
1903
+
1904
+ if (!map?.message_list) return []
1905
+
1906
+ let rawList = getMsgData('message_list', payload, map.message_list)
1907
+ if (rawList == undefined) {
1908
+ rawList = getMsgData(
1909
+ 'message_list',
1910
+ buildMsgList([payload]),
1911
+ map.message_list,
1912
+ )
1913
+ }
1914
+ if (rawList == undefined) return []
1915
+
1916
+ const list = parseMsgList(
1917
+ rawList,
1918
+ map.message_list.type,
1919
+ map.message_value,
1920
+ )
1921
+ if (list.length === 0) return []
1922
+
1923
+ if (map.message_list.order === 'reverse') {
1924
+ list.reverse()
1925
+ }
1926
+
1927
+ list.forEach((item: any) => {
1928
+ if (!item.post_type) {
1929
+ item.post_type = 'message'
1930
+ }
1931
+ })
1932
+
1933
+ return Promise.all(list.map((item) => msgPreprocess(item)))
1934
+ }
1935
+
1936
+ function normalizeNewIncomingMessage(data: any): any[] {
1937
+ let list = getMsgData(
1938
+ 'message_list',
1939
+ buildMsgList([data]),
1940
+ msgPath.message_list,
1941
+ )
1942
+
1943
+ if (list == undefined) return []
1944
+
1945
+ list = parseMsgList(
1946
+ list,
1947
+ msgPath.message_list.type,
1948
+ msgPath.message_value,
1949
+ )
1950
+ return list
1951
+ }
1952
+
1953
+ function insertHistorySegmentAtAnchor(
1954
+ current: any[],
1955
+ anchorMsgId: string,
1956
+ segment: any[],
1957
+ ): any[] {
1958
+ const insertIdx = current.findIndex((m) => m.message_id === anchorMsgId)
1959
+ if (insertIdx === -1) return current
1960
+
1961
+ const existingIds = new Set(current.map((m) => normalizeMessageId(m.message_id)))
1962
+ const newMsgs = segment.filter((m) => !existingIds.has(normalizeMessageId(m.message_id)))
1963
+ if (newMsgs.length === 0) return current
1964
+
1965
+ const merged = [
1966
+ ...current.slice(0, insertIdx),
1967
+ ...newMsgs,
1968
+ ...current.slice(insertIdx),
1969
+ ]
1970
+ return mergeMessagesByIdAndTime([], merged)
1971
+ }
1972
+
1973
+ function normalizeMessageId(id: unknown): string {
1974
+ if (id === null || id === undefined) return ''
1975
+ return String(id)
1976
+ }
1977
+
1978
+ function getMessageTimestamp(msg: any): number {
1979
+ const t = Number(msg?.time)
1980
+ return Number.isFinite(t) ? t : 0
1981
+ }
1982
+
1983
+ function buildFallbackMessageKey(msg: any): string {
1984
+ const local = msg?.time_ms ?? msg?.local_time ?? msg?.timestamp_ms ?? ''
1985
+ const seq = local || (msg?.message_seq ?? msg?.seq_id ?? msg?.seq ?? msg?.sn ?? '')
1986
+ const sender = msg?.sender?.user_id ?? msg?.user_id ?? msg?.sender_id ?? ''
1987
+ const ts = getMessageTimestamp(msg)
1988
+ return `${ts}|${sender}|${seq}`
1989
+ }
1990
+
1991
+ function compareMessageOrder(a: any, b: any): number {
1992
+ const localA = Number(a?.time_ms ?? a?.local_time ?? a?.timestamp_ms ?? 0)
1993
+ const localB = Number(b?.time_ms ?? b?.local_time ?? b?.timestamp_ms ?? 0)
1994
+ if (localA > 0 && localB > 0 && localA !== localB) {
1995
+ return localA - localB
1996
+ }
1997
+
1998
+ const ta = getMessageTimestamp(a)
1999
+ const tb = getMessageTimestamp(b)
2000
+ if (ta !== tb) return ta - tb
2001
+
2002
+ const sa = Number(a?.message_seq ?? a?.seq_id ?? a?.seq ?? a?.sn)
2003
+ const sb = Number(b?.message_seq ?? b?.seq_id ?? b?.seq ?? b?.sn)
2004
+ if (Number.isFinite(sa) && Number.isFinite(sb) && sa !== sb) {
2005
+ return sa - sb
2006
+ }
2007
+
2008
+ const ia = normalizeMessageId(a?.message_id)
2009
+ const ib = normalizeMessageId(b?.message_id)
2010
+ if (ia === ib) return 0
2011
+ const na = Number(ia)
2012
+ const nb = Number(ib)
2013
+ if (Number.isFinite(na) && Number.isFinite(nb) && na !== nb) {
2014
+ return na - nb
2015
+ }
2016
+ // 同秒且没有可靠序列时保持平台返回顺序,避免按随机 message_id 打乱
2017
+ return 0
2018
+ }
2019
+
2020
+ function getImageSegments(msg: any): any[] {
2021
+ if (!Array.isArray(msg?.message)) return []
2022
+ return msg.message.filter((seg: any) => seg?.type === 'image')
2023
+ }
2024
+
2025
+ function hasImageMessage(msg: any): boolean {
2026
+ return getImageSegments(msg).length > 0
2027
+ }
2028
+
2029
+ function hasResolvableImageSource(msg: any): boolean {
2030
+ const imgs = getImageSegments(msg)
2031
+ if (imgs.length === 0) return false
2032
+ return imgs.every((seg: any) => {
2033
+ const url = typeof seg?.url === 'string' ? seg.url : ''
2034
+ const file = typeof seg?.file === 'string' ? seg.file : ''
2035
+ if (url.length > 0) return true
2036
+ if (file.length > 0) return true
2037
+ return false
2038
+ })
2039
+ }
2040
+
2041
+ function shouldReplaceDuplicateMessage(existing: any, incoming: any): boolean {
2042
+ const settingsStore = useSettingsStore()
2043
+ if (!hasImageMessage(incoming)) return false
2044
+ if (existing?._from_local_db !== true) return false
2045
+
2046
+ // 关闭本地图片缓存时,在线同 id 图片消息应覆盖本地消息。
2047
+ if (settingsStore.sysConfig.disable_local_history_image_cache === true) {
2048
+ return true
2049
+ }
2050
+
2051
+ // 图片缓存开启但本地消息图片字段不完整时,也允许在线覆盖修复。
2052
+ return !hasResolvableImageSource(existing) && hasResolvableImageSource(incoming)
2053
+ }
2054
+
2055
+ function mergeMessagesByIdAndTime(current: any[], incoming: any[]): any[] {
2056
+ if (incoming.length === 0) return [...current]
2057
+ if (current.length === 0) {
2058
+ const firstPass = [...incoming]
2059
+ firstPass.sort(compareMessageOrder)
2060
+ return firstPass
2061
+ }
2062
+
2063
+ const idSet = new Set<string>()
2064
+ const idIndexMap = new Map<string, number>()
2065
+ const fallbackSet = new Set<string>()
2066
+ const merged = [] as any[]
2067
+
2068
+ for (const msg of current) {
2069
+ merged.push(msg)
2070
+ const id = normalizeMessageId(msg?.message_id)
2071
+ if (id) {
2072
+ idSet.add(id)
2073
+ idIndexMap.set(id, merged.length - 1)
2074
+ } else {
2075
+ fallbackSet.add(buildFallbackMessageKey(msg))
2076
+ }
2077
+ }
2078
+
2079
+ for (const msg of incoming) {
2080
+ const id = normalizeMessageId(msg?.message_id)
2081
+ if (id) {
2082
+ if (idSet.has(id)) {
2083
+ const idx = idIndexMap.get(id)
2084
+ if (idx !== undefined && shouldReplaceDuplicateMessage(merged[idx], msg)) {
2085
+ merged[idx] = msg
2086
+ }
2087
+ continue
2088
+ }
2089
+ idSet.add(id)
2090
+ merged.push(msg)
2091
+ idIndexMap.set(id, merged.length - 1)
2092
+ continue
2093
+ }
2094
+
2095
+ const fallbackKey = buildFallbackMessageKey(msg)
2096
+ if (fallbackSet.has(fallbackKey)) continue
2097
+ fallbackSet.add(fallbackKey)
2098
+ merged.push(msg)
2099
+ }
2100
+
2101
+ merged.sort(compareMessageOrder)
2102
+ return merged
2103
+ }
2104
+
2105
+ function replaceMessageListInPlace(next: any[]) {
2106
+ const chatStore = useChatStore()
2107
+ chatStore.messageList.splice(0, chatStore.messageList.length, ...next)
2108
+ }
2109
+
2110
+ export async function getMessageList(
2111
+ list: any[] | undefined,
2112
+ options?: { platform?: string; selfId?: string },
2113
+ ) {
2114
+ if (!list) return undefined
2115
+
2116
+ list = parseMsgList(
2117
+ list,
2118
+ msgPath.message_list.type,
2119
+ msgPath.message_value,
2120
+ )
2121
+ list.forEach((item) => {
2122
+ if (Array.isArray(item.message)) item.message = splitSatoriMarkupText(item.message)
2123
+ })
2124
+ // 倒序处理
2125
+ if (msgPath.message_list.order === 'reverse') {
2126
+ list.reverse()
2127
+ }
2128
+ list.sort(compareMessageOrder)
2129
+ // 检查必要字段
2130
+ list.forEach((item: any) => {
2131
+ if (!item.post_type) {
2132
+ item.post_type = 'message'
2133
+ }
2134
+ })
2135
+ const authStore = useAuthStore()
2136
+ const platform = String(options?.platform || authStore.loginInfo.platform || '')
2137
+ const selfId = String(options?.selfId || authStore.loginInfo.uin || '')
2138
+ list.forEach((item: any) => {
2139
+ const sender = item.sender
2140
+ const userId = String(sender?.user_id ?? item.user_id ?? item.sender_id ?? '')
2141
+ if (!userId) return
2142
+ const avatar = getCachedUserAvatar(platform, selfId, userId)
2143
+ if (avatar && sender) {
2144
+ sender.avatar = avatar
2145
+ } else {
2146
+ requestUserAvatar(platform, selfId, userId, item)
2147
+ }
2148
+ })
2149
+ return Promise.all(list.map((item) => msgPreprocess(item, options)))
2150
+ }
2151
+
2152
+ /**
2153
+ * 解析合并转发内容;实时消息和点击卡片都走这里,避免各自维护一套逻辑
2154
+ */
2155
+ export async function resolveForwardMessageContent(
2156
+ segment: { id?: unknown; content?: unknown },
2157
+ options?: { platform?: string; selfId?: string; channelId?: string },
2158
+ ): Promise<unknown[] | undefined> {
2159
+ const authStore = useAuthStore()
2160
+ const platform = String(options?.platform || authStore.loginInfo.platform || '')
2161
+ const selfId = String(options?.selfId || authStore.loginInfo.uin || '')
2162
+ if (Array.isArray(segment.content) && segment.content.length > 0) {
2163
+ return getMessageList(segment.content, options)
2164
+ }
2165
+ if (!segment.id) return undefined
2166
+ const originData = await fetchForwardMessage(
2167
+ platform,
2168
+ selfId,
2169
+ String(segment.id),
2170
+ options?.channelId,
2171
+ )
2172
+ if (!Array.isArray(originData)) return undefined
2173
+ const normalized = originData.map((raw) => {
2174
+ const item = typeof raw === 'object' && raw !== null
2175
+ ? raw as Record<string, unknown>
2176
+ : {}
2177
+ const rawSender = typeof item.sender === 'object' && item.sender !== null
2178
+ ? item.sender as Record<string, unknown>
2179
+ : {}
2180
+ const userId = String(rawSender.user_id ?? item.user_id ?? '')
2181
+ const nickname = String(rawSender.nickname ?? rawSender.name ?? item.nickname ?? '')
2182
+ const content = item.content ?? item.message
2183
+ const base = {
2184
+ ...item,
2185
+ user_id: userId,
2186
+ nickname,
2187
+ sender: { user_id: userId, nickname },
2188
+ }
2189
+ if (typeof content === 'string') {
2190
+ const parsed = parseCQ({ message: content }) as { message?: unknown }
2191
+ return { ...base, message: parsed.message ?? [] }
2192
+ }
2193
+ return base
2194
+ })
2195
+ return getMessageList(normalized, options)
2196
+ }
2197
+
2198
+ /**
2199
+ * 消息预处理
2200
+ * @param msg 要处理的消息
2201
+ */
2202
+ export async function msgPreprocess(
2203
+ msg: any,
2204
+ options?: { platform?: string; selfId?: string },
2205
+ ): Promise<any> {
2206
+ if (!Array.isArray(msg.message)) return msg
2207
+ msg.message = splitSatoriMarkupText(msg.message)
2208
+ //#region == json 合并转发 ============================
2209
+ if (msg.message.at(0)?.type === 'json') {
2210
+ try {
2211
+ const data = JSON.parse(msg.message.at(0).data)
2212
+ if (data['app'] === 'com.tencent.multimsg') {
2213
+ msg.message = [{
2214
+ type: 'forward',
2215
+ id: data['meta']['detail']['resid'],
2216
+ }]
2217
+ }
2218
+ } catch (e) {/**/ }
2219
+ }
2220
+ //#endregion
2221
+
2222
+ //#region == 合并转发解析 ==============================
2223
+ if (msg.message.at(0)?.type === 'forward') {
2224
+ try {
2225
+ const data = await resolveForwardMessageContent(msg.message.at(0), options)
2226
+ msg.message.at(0).content = data && data.length > 0 ? data : undefined
2227
+ } catch (e) {
2228
+ logger.error(e as unknown as Error, '合并转发解析失败')
2229
+ msg.message.at(0).content = undefined
2230
+ }
2231
+ }
2232
+ //#endregion
2233
+
2234
+ //#region == lgr 商场表情 =============================
2235
+ // 过滤掉mface后面尾随的字符串
2236
+ const filter: any[] = []
2237
+ for (let id = 0; id < msg.message.length; id++) {
2238
+ const seg = msg.message[id]
2239
+ filter.push(seg)
2240
+ if (seg.type === 'mface') id++
2241
+ }
2242
+ msg.message = filter
2243
+ //#endregion
2244
+ return msg
2245
+ }
2246
+
2247
+ function revokeMsg(_: string, msg: any) {
2248
+ const authStore = useAuthStore()
2249
+ const chatStore = useChatStore()
2250
+ // 清除通知
2251
+ const chatId = msg.notice_type.includes('group') ? msg.group_id : msg.user_id
2252
+ new Notify().closeAll(chatId)
2253
+
2254
+ // 在本地 DB 中标记撤回
2255
+ const msgId = msg.message_id
2256
+ dbRevokeMessage(authStore.loginInfo.uin, String(msgId))
2257
+
2258
+ // 寻找消息
2259
+ let msgGet = null as { [key: string]: any } | null
2260
+ for (const [index, msg] of chatStore.messageList.entries()) {
2261
+ if (msg.message_id === msgId) {
2262
+ msgGet = msg
2263
+ }
2264
+ }
2265
+
2266
+ if (!msgGet) {
2267
+ logger.add(LogType.UI, '没有找到这条被撤回的消息')
2268
+ return
2269
+ }
2270
+
2271
+ // 不删除消息,保留原文并标记为已撤回。
2272
+ msgGet.revoke = true
2273
+ msgGet.revoked = true
2274
+ msgGet.revokeTime = Date.now() / 1000
2275
+ }
2276
+
2277
+ let qed_try_times = 0
2278
+ function newMsg(_: string, data: any) {
2279
+ const { $t } = app.config.globalProperties
2280
+ const authStore = useAuthStore()
2281
+ const uiStore = useUIStore()
2282
+ const chatStore = useChatStore()
2283
+ const contactStore = useContactStore()
2284
+ const settingsStore = useSettingsStore()
2285
+ // 没有对频道的支持计划
2286
+ if (data.detail_type == 'guild') {
2287
+ return
2288
+ }
2289
+ // 消息基础信息:优先使用 Satori 转换层生成的 infoList,避免 OneBot pathMap 误解析。
2290
+ const rawInfo = getDataObject(data.infoList ?? data)
2291
+ const id = getString(rawInfo.group_id) ||
2292
+ getString(rawInfo.private_id) ||
2293
+ getString(data.group_id) ||
2294
+ getString(data.user_id) ||
2295
+ getString(getDataObject(data.channel).id)
2296
+ const loginId = String(authStore.loginInfo.uin ?? '')
2297
+ const showId = String(chatStore.chatInfo.show.id ?? '')
2298
+ const sender = getString(rawInfo.sender) ||
2299
+ getString(getDataObject(data.sender).user_id) ||
2300
+ getString(data.user_id)
2301
+ const senderObj = getDataObject(data.sender)
2302
+ const targetId = getString(rawInfo.target_id) || getString(data.target_id) || loginId
2303
+ const rawChannelType = rawInfo.channel_type ?? data.channel_type
2304
+ const channelTypeNum = Number(rawChannelType)
2305
+ const hasDirectType = Number.isFinite(channelTypeNum)
2306
+ ? channelTypeNum === 1
2307
+ : ['direct', 'private'].includes(String(rawChannelType ?? '').toLowerCase())
2308
+ const hasGroupType = Number.isFinite(channelTypeNum)
2309
+ ? channelTypeNum === 0
2310
+ : ['text', 'group', 'room', 'chat', 'channel'].includes(String(rawChannelType ?? '').toLowerCase())
2311
+ const isGroupMessage = hasDirectType
2312
+ ? false
2313
+ : hasGroupType
2314
+ ? true
2315
+ : data.message_type === 'group' || Boolean(data.group_id) || Boolean(rawInfo.group_id)
2316
+ const sessionChannelId = getString(rawInfo.channel_id) || getString(data.channel_id)
2317
+ const sessionGuildId = getString(rawInfo.guild_id) || getString(data.guild_id)
2318
+ if (!id || id === '0') return
2319
+ // 在好友列表里找一下他
2320
+ const senderInfo = contactStore.userList.find((item) => {
2321
+ return String(item.user_id ?? '') === sender
2322
+ })
2323
+ const isImportant = senderInfo?.class_id == 9999
2324
+
2325
+ // 预发送消息填充 ============================================
2326
+ // 列表内最近的一条 fake_msg(倒序查找)
2327
+ let fakeMsg = null as any
2328
+ for (let i = chatStore.messageList.length - 1; i > 0; i--) {
2329
+ const msg = chatStore.messageList[i]
2330
+ if (msg.fake_msg != undefined && sender == loginId) {
2331
+ fakeMsg = msg
2332
+ break
2333
+ }
2334
+ }
2335
+ // 预发送消息刷新
2336
+ if (fakeMsg != null) {
2337
+ // 将这条消息直接替换掉
2338
+ const trueMsg = getMsgData(
2339
+ 'message_list',
2340
+ buildMsgList([data]),
2341
+ msgPath.message_list,
2342
+ )
2343
+ getMessageList(trueMsg).then((trueMsg) => {
2344
+ if (trueMsg?.length == 1) {
2345
+ // 使用消息对象引用直接更新,避免索引问题
2346
+ fakeMsg.message = trueMsg[0].message
2347
+ fakeMsg.raw_message = trueMsg[0].raw_message
2348
+ fakeMsg.time = trueMsg[0].time
2349
+ fakeMsg.fake_msg = undefined
2350
+ fakeMsg.revoke = false
2351
+ }
2352
+ })
2353
+ // 移除最顶端的一条消息以被动刷新整个列表
2354
+ chatStore.messageList.shift()
2355
+ return
2356
+ }
2357
+
2358
+ // 刷新 favicon
2359
+ refreshFavicon()
2360
+
2361
+
2362
+
2363
+ // 对消息进行一次格式化处理
2364
+ const list = normalizeNewIncomingMessage(data)
2365
+
2366
+ if (list.length > 0) {
2367
+ // 保存到本地历史
2368
+ saveMessagesWithSideEffects(authStore.loginInfo.uin, list)
2369
+ data = list[0]
2370
+ }
2371
+
2372
+ // 显示消息 ============================================
2373
+ if (id && (id === showId || targetId === showId)) {
2374
+ // 如果有正在输入的提示,清除它
2375
+ chatStore.chatInfo.show.appendInfo = undefined
2376
+ const cachedAvatar = getCachedUserAvatar(
2377
+ String(authStore.loginInfo.platform ?? ''),
2378
+ String(authStore.loginInfo.uin ?? ''),
2379
+ sender,
2380
+ )
2381
+ if (
2382
+ cachedAvatar &&
2383
+ (!senderObj.avatar || senderObj.avatar === '/img/icons/icon.svg')
2384
+ ) {
2385
+ senderObj.avatar = cachedAvatar
2386
+ }
2387
+ // 保存消息:优先使用 Satori 转换后的消息结构,避免 pathMap 解析失败导致右侧不更新
2388
+ const incoming = Array.isArray(data.message)
2389
+ ? data
2390
+ : normalizeNewIncomingMessage(data)[0] ?? data
2391
+ if (!incoming.infoList) {
2392
+ const groupId = getString(incoming.group_id)
2393
+ const userId = getString(incoming.user_id)
2394
+ incoming.infoList = {
2395
+ message_id: getString(incoming.message_id),
2396
+ private_id: groupId ? '' : userId,
2397
+ group_id: groupId || undefined,
2398
+ channel_id: getString(incoming.channel_id),
2399
+ guild_id: getString(incoming.guild_id),
2400
+ target_id: getString(incoming.target_id),
2401
+ sender: userId,
2402
+ }
2403
+ }
2404
+ if (
2405
+ !chatStore.messageList.some((item) => {
2406
+ return String(item.message_id) === String(incoming.message_id)
2407
+ })
2408
+ ) {
2409
+ chatStore.messageList.push(incoming)
2410
+ sendMsgAppendInfo(incoming)
2411
+ saveMessagesWithSideEffects(authStore.loginInfo.uin, [incoming])
2412
+ }
2413
+ // 抽个签
2414
+ const num = randomNum(0, 10000)
2415
+ if (num >= 400 && num <= 500) {
2416
+ logger.add(
2417
+ LogType.INFO,
2418
+ num.toString() + ',这只是个神秘的数字...',
2419
+ undefined,
2420
+ true,
2421
+ )
2422
+ }
2423
+ if (num === 495) { // QED怎么能和芙兰无关?(◣_◢)吃我一发 QED [495年的波纹]
2424
+ const popInfo = {
2425
+ html: qed,
2426
+ button: [
2427
+ {
2428
+ text: '确定(O)',
2429
+ fun: () => {
2430
+ uiStore.popBoxList.shift()
2431
+ },
2432
+ },
2433
+ ],
2434
+ }
2435
+ uiStore.popBoxList.push(popInfo)
2436
+ }
2437
+ qed_try_times++
2438
+ }
2439
+
2440
+ // 通知判定预处理 ============================================
2441
+ // 对于其他不在消息里标记 atme、atall 的处理
2442
+ if (data.atme == undefined || data.atall == undefined) {
2443
+ if (!Array.isArray(data.message)) {
2444
+ data.message = []
2445
+ }
2446
+ data.message.forEach((item: any) => {
2447
+ if (item.type == 'at' && item.qq == loginId) {
2448
+ data.atme = true
2449
+ }
2450
+ })
2451
+ }
2452
+ // 临时会话名字的特殊处理
2453
+ if (data.sub_type === 'group' && senderObj.nickname !== undefined) {
2454
+ senderObj.nickname = senderObj.user_id
2455
+ }
2456
+ // 检查群组有没有开启通知
2457
+ let isGroupNotice = false
2458
+ if (isGroupMessage) {
2459
+ const noticeInfo = Option.get('notice_group') ?? {}
2460
+ const list = noticeInfo[authStore.loginInfo.uin]
2461
+ if (list) {
2462
+ isGroupNotice = list.indexOf(id) >= 0
2463
+ }
2464
+ }
2465
+
2466
+ const isTempGroupMessage = data.sub_type === 'group'
2467
+ const groupNoticeType = settingsStore.sysConfig.group_notice_type
2468
+ const hasForcedGroupInnerNotice = data.atme || data.atall || isImportant || isGroupNotice
2469
+ const allowGroupInnerNotice = !isGroupMessage ||
2470
+ groupNoticeType !== 'none' ||
2471
+ hasForcedGroupInnerNotice
2472
+ const allowGroupSystemNotice = !isGroupMessage ||
2473
+ groupNoticeType === 'all'
2474
+
2475
+ // 会话状态更新 ============================================
2476
+ const sessionId = isTempGroupMessage ? sender : id
2477
+ const normalizedSessionId = normalizeSessionId(sessionId)
2478
+ let session = contactStore.baseOnMsgList.get(normalizedSessionId)
2479
+ if (!session) {
2480
+ if (isTempGroupMessage) {
2481
+ session = {
2482
+ user_id: sender,
2483
+ nickname: app.config.globalProperties.$t('临时会话'),
2484
+ remark: senderObj.user_id,
2485
+ group_id: data.group_id,
2486
+ group_name: '',
2487
+ } as UserFriendElem & UserGroupElem
2488
+ } else {
2489
+ session = resolveIncomingSession(
2490
+ contactStore.userList,
2491
+ sessionId,
2492
+ isGroupMessage,
2493
+ getString(senderObj.nickname),
2494
+ )
2495
+ }
2496
+ }
2497
+ if (session) {
2498
+ if (isGroupMessage && data.group_name) {
2499
+ if (!session.group_name || session.group_name === String(sessionId)) {
2500
+ session.group_name = String(data.group_name)
2501
+ }
2502
+ }
2503
+ Object.assign(session, formatMessageData(data, isGroupMessage), {
2504
+ channel_id: sessionChannelId || undefined,
2505
+ guild_id: sessionGuildId || undefined,
2506
+ })
2507
+ if (
2508
+ sender != loginId &&
2509
+ sender !== '' &&
2510
+ sender !== '0' &&
2511
+ sessionId !== showId &&
2512
+ allowGroupInnerNotice
2513
+ ) {
2514
+ if (!session.new_msg) {
2515
+ session.new_msg = true
2516
+ contactStore.newMsgCount++
2517
+ }
2518
+ }
2519
+ if (sessionId !== showId && allowGroupInnerNotice) {
2520
+ if (data.atme) { session.highlight = $t('[有人@你]') }
2521
+ if (data.atall) { session.highlight = $t('[@全体]') }
2522
+ if (isImportant) { session.highlight = $t('[特別关心]') }
2523
+ }
2524
+ contactStore.baseOnMsgList.set(normalizedSessionId, session)
2525
+ updateBaseOnMsgList()
2526
+ }
2527
+
2528
+ // 通知判定 ============================================
2529
+ if (
2530
+ sender != loginId &&
2531
+ sender !== '' &&
2532
+ sender !== '0' &&
2533
+ allowGroupSystemNotice
2534
+ ) {
2535
+ logger.add(LogType.DEBUG, '通知判定:', {
2536
+ notShow: sessionId !== showId,
2537
+ notFocus: !document.hasFocus(),
2538
+ hidden: document.hidden,
2539
+ isImportant: isImportant
2540
+ })
2541
+ // (发送者没有被打开 || 窗口没有焦点 || 窗口被最小化 || 在特别关心列表里) 这些情况需要进行消息通知
2542
+ const forceGroupSystemNotice = isGroupMessage && groupNoticeType === 'all'
2543
+ const forceImportantNotice = isImportant ||
2544
+ (isGroupMessage && (data.atme || data.atall || isGroupNotice))
2545
+ if (
2546
+ forceGroupSystemNotice ||
2547
+ forceImportantNotice ||
2548
+ sessionId !== showId ||
2549
+ !document.hasFocus() ||
2550
+ document.hidden
2551
+ ) {
2552
+ // 准备消息内容
2553
+ let raw = getMsgRawTxt(data)
2554
+ raw = raw === '' ? data.raw_message : raw
2555
+ logger.add(LogType.INFO, '新消息通知:' + raw, undefined, true)
2556
+ if (data.group_name === undefined) {
2557
+ // 检查消息内是否有群名,去列表里寻找
2558
+ contactStore.userList.forEach((item) => {
2559
+ if (item.group_id == data.group_id) {
2560
+ data.group_name = item.group_name
2561
+ }
2562
+ })
2563
+ }
2564
+ const msgInfo = {
2565
+ base_type: 'msg',
2566
+
2567
+ title: data.group_name ?? senderObj.nickname,
2568
+ body:
2569
+ isGroupMessage ? String(senderObj.nickname ?? '') + ':' + raw : raw,
2570
+ tag: `${sessionId}/${data.message_id}`,
2571
+ icon: '/img/icons/icon.svg',
2572
+ image: undefined as any,
2573
+ type: isGroupMessage ? 'group' : 'user',
2574
+ is_important: isImportant,
2575
+ } as NotifyInfo
2576
+ if (Array.isArray(data.message)) {
2577
+ data.message.forEach((item: MsgItemElem) => {
2578
+ // 如果消息有图片,追加第一张图片
2579
+ if (item.type === 'image' && msgInfo.image === undefined) {
2580
+ msgInfo.image = item.url
2581
+ }
2582
+ })
2583
+ }
2584
+ const isGroupImportantNotice = isGroupMessage &&
2585
+ (data.atme || data.atall || isImportant || isGroupNotice)
2586
+ if (isGroupImportantNotice && groupNoticeType !== 'none') {
2587
+ new PopInfo().add(PopType.INFO, `${msgInfo.title}: ${msgInfo.body}`)
2588
+ }
2589
+ // 发送消息
2590
+ new Notify().notify(msgInfo)
2591
+ }
2592
+ }
2593
+ }
2594
+
2595
+ /**
2596
+ * 刷新系统通知和其他内容,给系统通知响应用的
2597
+ */
2598
+ function updateSysInfo(
2599
+ _: string,
2600
+ __: { [key: string]: any },
2601
+ echoList: string[],
2602
+ ) {
2603
+ const contactStore = useContactStore()
2604
+ const flag = echoList[1]
2605
+ // 从系统通知列表里删除这条消息
2606
+ if (flag !== undefined) {
2607
+ const index = contactStore.systemNoticesList?.findIndex((item: any) => {
2608
+ return item.flag == flag
2609
+ })
2610
+ if (index !== -1) {
2611
+ contactStore.systemNoticesList?.splice(index, 1)
2612
+ }
2613
+ }
2614
+ }
2615
+
2616
+ // ==============================================================
2617
+
2618
+ function formatMessageData(data: any, isGroup: boolean) {
2619
+ const name = data.sender?.card && data.sender.card !== '' ? data.sender.card : data.sender?.nickname
2620
+ const raw = getMsgRawTxt(data) || data.raw_message || ''
2621
+ const rawTime = Number(data.time)
2622
+ const time = Number.isFinite(rawTime) && rawTime > 0 ? getViewTime(rawTime) : Date.now()
2623
+
2624
+ return {
2625
+ message_id: data.message_id,
2626
+ raw_msg: isGroup && name ? `${name}: ${raw}` : raw,
2627
+ highlight: hasAtMe(data) ? '[有人@你]' : undefined,
2628
+ time,
2629
+ raw_msg_base: raw,
2630
+ }
2631
+ }
2632
+
2633
+ // 重置 Runtime,但是保留应用设置之类已经加载好的应用内容
2634
+ export function resetRimtime(resetAll = false) {
2635
+ firstHeartbeatTime = -1
2636
+ heartbeatTime = -1
2637
+ clearMetaEventWatchdog()
2638
+ groupPreviewHydrator.reset()
2639
+ if (resetAll) {
2640
+ // Reset auth store
2641
+ const authStore = useAuthStore()
2642
+ authStore.loginInfo = reactive({})
2643
+ authStore.botInfo = reactive({})
2644
+ // Reset contact store
2645
+ const contactStore = useContactStore()
2646
+ contactStore.userList = reactive([])
2647
+ contactStore.showList = reactive([])
2648
+ contactStore.systemNoticesList = reactive([])
2649
+ contactStore.baseOnMsgList = reactive(new Map())
2650
+ contactStore.onMsgList = reactive([])
2651
+ contactStore.groupAssistList = reactive([])
2652
+ // Reset chat store
2653
+ const chatStore = useChatStore()
2654
+ chatStore.chatInfo = reactive({
2655
+ show: { type: '', id: 0, name: '', avatar: '' },
2656
+ info: {
2657
+ group_info: {},
2658
+ user_info: {},
2659
+ me_info: {},
2660
+ group_members: [],
2661
+ group_files: {},
2662
+ group_sub_files: {},
2663
+ jin_info: { list: [] as { [key: string]: any }[], pages: 0 },
2664
+ },
2665
+ })
2666
+ chatStore.messageList = []
2667
+ // Reset connection store
2668
+ const connectionStore = useConnectionStore()
2669
+ connectionStore.heartbeatTime = -1
2670
+ connectionStore.oldHeartbeatTime = -1
2671
+ connectionStore.lastHeartbeatTime = -1
2672
+ connectionStore.backTimes = 0
2673
+ }
2674
+ }