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,1242 @@
1
+ import jp from 'jsonpath'
2
+ import app from '../../main'
3
+ import anime from 'animejs'
4
+ import option from '../../function/option'
5
+
6
+ import { Logger, PopInfo, PopType } from '../../function/base'
7
+ import { useSettingsStore } from '../../state/settings'
8
+ import { v4 as uuid } from 'uuid'
9
+ import { Connector } from '../../function/connect'
10
+ import { getBootstrap, getLogins } from '../../function/satori'
11
+ import {
12
+ BotMsgType,
13
+ MsgItemElem,
14
+ UserFriendElem,
15
+ UserGroupElem,
16
+ } from '../elements/information'
17
+ import { sendStatEvent } from './appUtil'
18
+ import { backend } from '../../runtime/backend'
19
+ import { useContactStore } from '../../state/contact'
20
+ import { useUIStore } from '../../state/ui'
21
+ import { useAuthStore } from '../../state/auth'
22
+ import { useChatStore } from '../../state/chat'
23
+ import {
24
+ findSessionContact,
25
+ getSessionId,
26
+ normalizeSessionId,
27
+ } from './sessionUtil'
28
+
29
+ const logger = new Logger()
30
+
31
+ /**
32
+ * 根据 JSON Path 映射数据返回需要的内容体
33
+ * @param msg
34
+ * @param map
35
+ * @returns
36
+ */
37
+ export function getMsgData(
38
+ name: string,
39
+ msg: { [key: string]: any },
40
+ map: string | { [key: string]: any },
41
+ ) {
42
+ let back = undefined as any
43
+ // 解析数据
44
+ if (map != undefined) {
45
+ if (typeof map == 'string' || map.source != undefined) {
46
+ try {
47
+ if (msg == null || typeof msg != 'object' || Array.isArray(msg)) {
48
+ return back
49
+ }
50
+ back = jp.query(
51
+ msg,
52
+ replaceJPValue(typeof map == 'string' ? map : map.source),
53
+ )
54
+ if (back && typeof map != 'string' && map.list != undefined) {
55
+ const backList = [] as any[]
56
+ back.forEach((item) => {
57
+ if (item == null || typeof item != 'object') return
58
+ const itemObj = {} as any
59
+ Object.keys(map.list).forEach((key: string) => {
60
+ if (typeof map.list[key] == 'string' && map.list[key] != '') {
61
+ if (map.list[key].startsWith('/'))
62
+ itemObj[key] =
63
+ item[map.list[key].substring(1)]
64
+ else {
65
+ let nameKey = map.list[key]
66
+ let regexKey = null
67
+ if (nameKey.indexOf('@') > -1) {
68
+ const [name, key] = nameKey.split('@')
69
+ nameKey = name
70
+ regexKey = key
71
+ }
72
+ itemObj[key] = jp.query(
73
+ item,
74
+ replaceJPValue(nameKey),
75
+ )
76
+ if (regexKey != null) {
77
+ const regex = new RegExp(regexKey)
78
+ const match = itemObj[key].match(regex)
79
+ if (match != null) {
80
+ itemObj[key] = match[0]
81
+ }
82
+ }
83
+ }
84
+ }
85
+ })
86
+ backList.push(itemObj)
87
+ })
88
+ back = backList
89
+ }
90
+ } catch (ex) {
91
+ logger.error(
92
+ ex as Error,
93
+ `解析消息 JSON 错误:${name} -> ${map}`,
94
+ )
95
+ }
96
+ } else {
97
+ const data = {} as { [key: string]: any }
98
+ Object.keys(map).forEach((key) => {
99
+ if (
100
+ map[key] != undefined &&
101
+ map[key] !== '' &&
102
+ !key.startsWith('_')
103
+ ) {
104
+ if (msg == null || typeof msg != 'object' || Array.isArray(msg)) {
105
+ return
106
+ }
107
+ try {
108
+ data[key] = jp.query(msg, replaceJPValue(map[key]))[0]
109
+ } catch (ex) {
110
+ logger.error(
111
+ ex as Error,
112
+ `解析 JSON 错误:${name} -> ${map}`,
113
+ )
114
+ }
115
+ }
116
+ })
117
+ back = [data]
118
+ }
119
+ }
120
+ return back
121
+ }
122
+ function replaceJPValue(jpStr: string) {
123
+ const authStore = useAuthStore()
124
+ return jpStr.replaceAll('<uin>', authStore.loginInfo.uin)
125
+ }
126
+
127
+ /**
128
+ * 将一个消息体列表组装为基础消息列表便于解析(message 消息体可能不正确)
129
+ * @param msgList
130
+ * @param map
131
+ * @returns
132
+ */
133
+ export function buildMsgList(msgList: { [key: string]: any }): {
134
+ [key: string]: any
135
+ } {
136
+ const authStore = useAuthStore()
137
+ const path = jp.parse(authStore.jsonMap.message_list.source)
138
+ const keys = [] as string[]
139
+ path.forEach((item) => {
140
+ if (item.expression.value != '*' && item.expression.value != '$') {
141
+ keys.push(item.expression.value)
142
+ }
143
+ })
144
+ const result = {} as any
145
+ let acc = result
146
+ keys.forEach((key, index) => {
147
+ if (index === keys.length - 1) {
148
+ acc[key] = msgList
149
+ } else {
150
+ acc[key] = {}
151
+ }
152
+ acc = acc[key]
153
+ })
154
+ return result
155
+ }
156
+
157
+ export function parseMsgList(
158
+ list: any,
159
+ map: string,
160
+ valueMap: { [key: string]: any },
161
+ ): any[] {
162
+ const uiStore = useUIStore()
163
+ const authStore = useAuthStore()
164
+ if (!Array.isArray(list) || list.length === 0 || !list[0]) {
165
+ return []
166
+ }
167
+ // 判断消息类型
168
+ if (typeof list[0].message == 'string') {
169
+ uiStore.msgType = BotMsgType.CQCode
170
+ } else {
171
+ uiStore.msgType = BotMsgType.Array
172
+ }
173
+ // 消息类型的特殊处理
174
+ switch (uiStore.msgType) {
175
+ case BotMsgType.CQCode: {
176
+ // 这儿会默认处理成 oicq2 的格式,所以 CQCode 消息请使用 oicq2 配置文件修改
177
+ for (let i = 0; i < list.length; i++) {
178
+ list[i] = parseCQ(list[i])
179
+ }
180
+ break
181
+ }
182
+ case BotMsgType.Array: {
183
+ // 非扁平化消息体,这儿会取 _type 后半段的 JSON Path 将结果并入 message
184
+ for (let i = 0; i < list.length; i++) {
185
+ let msgList = list[i].message
186
+ if (msgList == undefined) {
187
+ msgList = list[i].content
188
+ }
189
+ if (!Array.isArray(msgList)) {
190
+ continue
191
+ }
192
+ for (let j = 0; j < msgList.length; j++) {
193
+ const data = getMsgData(
194
+ 'message_list_message',
195
+ msgList[j],
196
+ map,
197
+ )
198
+ // 如果 data 里有 type 字段,改成 type_item
199
+ if (data[0] && data[0]['type'] != undefined) {
200
+ data[0]['type_item'] = data[0]['type']
201
+ delete data[0]['type']
202
+ }
203
+ if (data != undefined && data.length == 1) {
204
+ msgList[j] = Object.assign(msgList[j], data[0])
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ // 消息字段的标准化特殊处理
211
+ if (valueMap != undefined) {
212
+ for (let i = 0; i < list.length; i++) {
213
+ Object.entries(valueMap).forEach(([type, values]) => {
214
+ Object.entries(values).forEach(([key, value]) => {
215
+ let content = list[i].message
216
+ if (content == undefined) {
217
+ content = list[i].content
218
+ }
219
+ if (!Array.isArray(content)) {
220
+ return
221
+ }
222
+ content.forEach((item: any) => {
223
+ if (item.type == type) {
224
+ item[key] = jp.query(item, value as string)[0]
225
+ }
226
+ // 顺便把没用的 data 删了,这边要注意 item.data 必须是个对象
227
+ // 因为有些消息类型的 data 就叫 data
228
+ if (typeof item.data == 'object') {
229
+ delete item.data
230
+ }
231
+ })
232
+ // 其他处理
233
+ if (list[i].content != undefined) {
234
+ // 把 content 改成 message
235
+ list[i].message = content
236
+ delete list[i].content
237
+ // 添加一个 sender.user_id 为 user_id
238
+ list[i].sender = {
239
+ user_id: list[i].user_id,
240
+ nickname: list[i].nickname,
241
+ }
242
+ }
243
+ })
244
+ })
245
+ // 补充 infoList
246
+ const infoList = getMsgData('message_info', list[i], authStore.jsonMap.message_info)
247
+ if (infoList != undefined) {
248
+ list[i].infoList = infoList[0]
249
+ }
250
+ }
251
+ }
252
+ return list
253
+ }
254
+
255
+ /**
256
+ * 将消息对象处理为扁平字符串
257
+ * @param message 待处理的消息对象
258
+ * @returns 字符串
259
+ */
260
+ export function getMsgRawTxt(data: any): string {
261
+ const { $t } = app.config.globalProperties
262
+ const chatStore = useChatStore()
263
+ const authStore = useAuthStore()
264
+
265
+ const message = data.message as [{ [key: string]: any }]
266
+ if (!Array.isArray(message)) {
267
+ return data.raw_message ?? ''
268
+ }
269
+ const fromId = data.group_id ?? data.user_id
270
+ let back = ''
271
+ for (let i = 0; i < message.length; i++) {
272
+ try {
273
+ switch (message[i].type) {
274
+ case 'at':
275
+ if (message[i].text == undefined) {
276
+ // 群内才可以 at,如果 at 消息中没有 text 字段
277
+ // 尝试去群成员列表中找到对应的昵称,群成员列表只在当前打开的群才有
278
+ if (
279
+ chatStore.chatInfo.show.id == fromId &&
280
+ chatStore.chatInfo.info.group_members
281
+ ) {
282
+ const user =
283
+ chatStore.chatInfo.info.group_members.find(
284
+ (item) => item.user_id == message[i].qq,
285
+ )
286
+ if (user) {
287
+ back +=
288
+ '@' +
289
+ (user.card && user.card != '' ? user.card : user.nickname)
290
+ break
291
+ }
292
+ }
293
+ break
294
+ }
295
+ const atText = String(message[i].text)
296
+ back += atText.startsWith('@') ? atText : '@' + atText
297
+ break
298
+ case 'text':
299
+ back += message[i].text
300
+ .replaceAll('\n', ' ')
301
+ .replaceAll('\r', ' ')
302
+ break
303
+ case 'i18n':
304
+ back += message[i].path || '[i18n]'
305
+ break
306
+ case 'forward':
307
+ back += '[' + $t('聊天记录') + ']'
308
+ break
309
+ case 'face':
310
+ back += '[' + $t('表情') + ']'
311
+ break
312
+ case 'bface':
313
+ back += message[i].text
314
+ break
315
+ case 'image':
316
+ back += '[' + $t('图片') + ']'
317
+ break
318
+ case 'record':
319
+ back += '[' + $t('语音') + ']'
320
+ break
321
+ case 'video':
322
+ back += '[' + $t('视频') + ']'
323
+ break
324
+ case 'file':
325
+ back += '[' + $t('文件') + ']'
326
+ break
327
+ case 'json': {
328
+ try {
329
+ back += JSON.parse(message[i].data).prompt
330
+ } catch (error) {
331
+ back += '[' + $t('卡片消息') + ']'
332
+ }
333
+ break
334
+ }
335
+ case 'xml': {
336
+ let name = message[i].data.substring(
337
+ message[i].data.indexOf('<source name="') + 14,
338
+ )
339
+ name = name.substring(0, name.indexOf('"'))
340
+ back += '[' + name + ']'
341
+ break
342
+ }
343
+ }
344
+ } catch (error) {
345
+ logger.error(
346
+ error as Error,
347
+ '解析消息短格式错误:' + JSON.stringify(message[i]),
348
+ )
349
+ }
350
+ }
351
+ return back
352
+ }
353
+
354
+ export function hasAtMe(data: any): boolean {
355
+ const message = data?.message
356
+ if (!Array.isArray(message)) return false
357
+ const authStore = useAuthStore()
358
+ const selfId = String(authStore.loginInfo.uin ?? '')
359
+ if (!selfId) return false
360
+ return message.some((item) => item?.type === 'at' && String(item.qq) === selfId)
361
+ }
362
+
363
+ /**
364
+ * 将消息对象转换为 CQCode
365
+ * @param data
366
+ * @returns CQCode 字符串
367
+ */
368
+ export function parseJSONCQCode(data: any) {
369
+ let back = ''
370
+ data.forEach((item: any) => {
371
+ if (item.type != 'text') {
372
+ let body = '[CQ:' + item.type + ','
373
+ Object.keys(item).forEach((key: any) => {
374
+ body += `${key}=${item[key]},`
375
+ })
376
+ body = body.substring(0, body.length - 1) + ']'
377
+ back += body
378
+ } else {
379
+ back += item.text
380
+ }
381
+ })
382
+ return back
383
+ }
384
+
385
+ /**
386
+ * 将扁平的 CQCode 消息处理成消息对象
387
+ * @param msg CQCode 消息
388
+ * @returns 消息对象
389
+ */
390
+ export function parseCQ(data: any) {
391
+ let msg = data.message as string
392
+ // 将纯文本也处理为 CQCode 格式
393
+ // PS:这儿不用担心方括号本身,go-cqhttp 会把它转义掉
394
+ let reg = /^[^\]]+?\[|\].+\[|\][^[]+$|^[^[\]]+$/g
395
+ const textList = msg.match(reg)
396
+ if (textList !== null) {
397
+ textList.forEach((item) => {
398
+ item = item.replace(']', '').replace('[', '')
399
+ msg = msg.replace(item, `[CQ:text,text=${item}]`)
400
+ })
401
+ }
402
+ // 拆分 CQCode
403
+ reg = /\[.+?\]/g
404
+ msg = msg.replaceAll('\n', '\\n')
405
+ const list = msg.match(reg)
406
+ // 处理为 object
407
+ const back: { [ket: string]: any }[] = []
408
+ reg = /\[CQ:([^,]+),(.*)\]/g
409
+ if (list !== null) {
410
+ list.forEach((item) => {
411
+ if (item.match(reg) !== null) {
412
+ const info: { [key: string]: any } = { type: RegExp.$1 }
413
+ RegExp.$2.split(',').forEach((key: string) => {
414
+ const kv = [] as string[]
415
+ kv.push(key.substring(0, key.indexOf('=')))
416
+ // 对 html 转义字符进行反转义
417
+ const a = document.createElement('a')
418
+ a.innerHTML = key.substring(key.indexOf('=') + 1)
419
+ kv.push(a.innerText)
420
+ info[kv[0]] = kv[1]
421
+ })
422
+ // 对文本消息特殊处理
423
+ if (info.type == 'text') {
424
+ info.text = RegExp.$2
425
+ .substring(RegExp.$2.lastIndexOf('=') + 1)
426
+ .replaceAll('\\n', '\n')
427
+ // 对 html 转义字符进行反转义
428
+ const a = document.createElement('a')
429
+ a.innerHTML = info.text
430
+ info.text = a.innerText
431
+ }
432
+ // 对回复消息进行特殊处理
433
+ if (info.type == 'reply') {
434
+ data.source = {
435
+ user_id: info.user_id,
436
+ seq: info.seq,
437
+ message: info.message,
438
+ }
439
+ } else {
440
+ back.push(info)
441
+ }
442
+ }
443
+ })
444
+ }
445
+ logger.debug('解析 CQ 消息结果: ' + JSON.stringify(back))
446
+ data.message = back
447
+ return data
448
+ }
449
+
450
+ /**
451
+ * 发送消息
452
+ * @param id 发送对象的 id
453
+ * @param type 发送对象的类型
454
+ * @param msg 消息体
455
+ * @param preShow 是否消息预显
456
+ * @param echo 回显的事件名
457
+ */
458
+ function parsePreviewMarkup(source: string): MsgItemElem[] {
459
+ const result: MsgItemElem[] = []
460
+ const tagPattern = /<(img|image|face|mface|file|audio|video|at|quote|sharp)\s+([^>]*?)\/?>/gi
461
+ let last = 0
462
+ let match: RegExpExecArray | null
463
+ while ((match = tagPattern.exec(source)) !== null) {
464
+ if (match.index > last) {
465
+ result.push({ type: 'text', text: source.slice(last, match.index) })
466
+ }
467
+ const attrs = match[2] ?? ''
468
+ const src = attrs.match(/src\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[1]
469
+ ?? attrs.match(/src\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[2]
470
+ ?? ''
471
+ const name = attrs.match(/name\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[1]
472
+ ?? attrs.match(/name\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[2]
473
+ ?? ''
474
+ const id = attrs.match(/id\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[1]
475
+ ?? attrs.match(/id\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[2]
476
+ ?? ''
477
+ const tag = String(match[1] ?? '').toLowerCase()
478
+ if (tag === 'at') {
479
+ result.push({ type: 'at', qq: id, text: name || id })
480
+ } else if (tag === 'quote') {
481
+ result.push({ type: 'reply', id })
482
+ } else if (tag === 'sharp') {
483
+ result.push({ type: 'text', text: name ? `#${name}` : `#${id}` })
484
+ } else {
485
+ const type = tag === 'file'
486
+ ? 'file'
487
+ : tag === 'audio'
488
+ ? 'record'
489
+ : tag === 'video'
490
+ ? 'video'
491
+ : 'image'
492
+ result.push({
493
+ type,
494
+ file: src,
495
+ url: src,
496
+ ...(name ? { name } : {}),
497
+ })
498
+ }
499
+ last = match.index + match[0].length
500
+ }
501
+ if (last < source.length) {
502
+ result.push({ type: 'text', text: source.slice(last) })
503
+ }
504
+ return result.length > 0 ? result : [{ type: 'text', text: source }]
505
+ }
506
+
507
+ function getBase64Source(item: any): string {
508
+ const file = String(item?.file ?? '')
509
+ const url = String(item?.url ?? '')
510
+ if (file.startsWith('base64://')) return file.slice(9)
511
+ if (file.startsWith('data:')) return file
512
+ if (url.startsWith('data:')) return url
513
+ if (url.startsWith('base64://')) return url.slice(9)
514
+ return ''
515
+ }
516
+
517
+ function isBase64Source(source: string): boolean {
518
+ return source.startsWith('base64://') || source.startsWith('data:')
519
+ }
520
+
521
+ export function getLocalMediaUrl(value: string): string {
522
+ if (!value) return value
523
+ const isLocalPath = value.startsWith('file:') ||
524
+ /^[a-z]:[\\/]/i.test(value) ||
525
+ value.startsWith('\\\\')
526
+ if (!isLocalPath) return value
527
+ const fileName = value
528
+ .replace(/^file:\/\//i, '')
529
+ .split(/[\\/]/)
530
+ .pop() || value
531
+ return `${location.origin}/chat-patch/api/media?file=${encodeURIComponent(fileName)}`
532
+ }
533
+
534
+ function escapeMarkupAttr(value: string): string {
535
+ return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;')
536
+ }
537
+
538
+ async function uploadBase64Source(source: string, name = ''): Promise<string> {
539
+ try {
540
+ const info = await getBootstrap()
541
+ const basePath = info.basePath || '/chat-patch'
542
+ const normalized = source.startsWith('base64://') ? source.slice(9) : source
543
+ const base64 = normalized.includes('base64,')
544
+ ? normalized.slice(normalized.indexOf('base64,') + 7)
545
+ : normalized
546
+ const binary = atob(base64)
547
+ const bytes = new Uint8Array(binary.length)
548
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
549
+ const mime = normalized.startsWith('data:')
550
+ ? normalized.slice(5, normalized.indexOf(';')).toLowerCase()
551
+ : 'application/octet-stream'
552
+ const query = name ? `?name=${encodeURIComponent(name)}` : ''
553
+ const response = await fetch(`${location.origin}${basePath}/api/upload-media${query}`, {
554
+ method: 'POST',
555
+ headers: { 'Content-Type': mime || 'application/octet-stream' },
556
+ body: bytes,
557
+ })
558
+ if (!response.ok) return ''
559
+ const result = await response.json() as Record<string, unknown>
560
+ return typeof result.path === 'string' ? result.path : ''
561
+ } catch {
562
+ return ''
563
+ }
564
+ }
565
+
566
+ async function persistBase64Markup(markup: string): Promise<string> {
567
+ const pattern = /<(img|audio|video|file)\b([^>]*)>/gi
568
+ let output = ''
569
+ let last = 0
570
+ let match: RegExpExecArray | null
571
+ while ((match = pattern.exec(markup)) !== null) {
572
+ const tag = match[1]
573
+ let attrs = match[2]
574
+ const selfClosing = /\/\s*$/.test(attrs)
575
+ if (selfClosing) attrs = attrs.replace(/\/\s*$/, '')
576
+ const srcMatch = /\bsrc\s*=\s*(["'])(.*?)\1/i.exec(attrs)
577
+ const nameMatch = /\bname\s*=\s*(["'])(.*?)\1/i.exec(attrs)
578
+ if (srcMatch && isBase64Source(srcMatch[2])) {
579
+ const localUrl = await uploadBase64Source(srcMatch[2], nameMatch?.[2] ?? '')
580
+ if (!localUrl) throw new Error('媒体上传失败,未发送原始 Base64')
581
+ attrs = attrs.replace(srcMatch[0], `src="${escapeMarkupAttr(localUrl)}"`)
582
+ }
583
+ output += markup.slice(last, match.index)
584
+ output += `<${tag}${attrs}${selfClosing ? '/>' : '>'}`
585
+ last = match.index + match[0].length
586
+ }
587
+ return output + markup.slice(last)
588
+ }
589
+
590
+ async function persistBase64Media(msg: string | any[] | undefined): Promise<string | any[] | undefined> {
591
+ if (typeof msg === 'string') return persistBase64Markup(msg)
592
+ if (!Array.isArray(msg)) return msg
593
+ return Promise.all(msg.map(async (item) => {
594
+ const source = getBase64Source(item)
595
+ if (!source) return item
596
+ const localUrl = await uploadBase64Source(source, item?.fileName ?? item?.name ?? '')
597
+ if (!localUrl) throw new Error('媒体上传失败,未发送原始 Base64')
598
+ return {
599
+ ...item,
600
+ file: localUrl,
601
+ url: localUrl,
602
+ localPath: localUrl,
603
+ }
604
+ }))
605
+ }
606
+
607
+ export async function sendMsgRaw(
608
+ id: string,
609
+ type: string,
610
+ msg: string | any[] | undefined,
611
+ preShow = false,
612
+ echo = 'sendMsgBack',
613
+ targetChannelId?: string,
614
+ targetGuildId?: string,
615
+ ) {
616
+ const chatStore = useChatStore()
617
+ const authStore = useAuthStore()
618
+ const contactStore = useContactStore()
619
+ const uiStore = useUIStore()
620
+ if (id === undefined || id === null || String(id) === '' || String(id) === '0') return
621
+ // 如果消息为空则不发送
622
+ if (msg == undefined || msg == '' || (Array.isArray(msg) && msg.length == 0)) {
623
+ return
624
+ }
625
+ // 预发送消息
626
+ // 将消息构建为完整消息体先显示出去
627
+ const msgUUID = uuid()
628
+ if (preShow) {
629
+ const botLogin = getLogins().find((item) => {
630
+ return item.platform === String(authStore.loginInfo.platform ?? '') &&
631
+ item.selfId === String(authStore.loginInfo.uin ?? '')
632
+ })
633
+ const preShowMsg = typeof msg === 'string'
634
+ ? parsePreviewMarkup(msg)
635
+ : JSON.parse(JSON.stringify(msg));
636
+ preShowMsg.forEach((item: any) => {
637
+ // 对 base64 图片做特殊处理
638
+ if (item.type == 'image') {
639
+ if (item.file.startsWith('base64://')) {
640
+ const b64Str = (item.file as string).substring(9)
641
+ item.url = 'data:image/png;base64,' + b64Str
642
+ } else {
643
+ item.url = item.file
644
+ }
645
+ }
646
+ if (item.type == 'record' && item.file.startsWith('base64://')) {
647
+ const b64Str = (item.file as string).substring(9)
648
+ item.url = 'data:audio/webm;base64,' + b64Str
649
+ }
650
+ })
651
+ const showMsg = {
652
+ revoke: true,
653
+ fake_msg: true,
654
+ message_id: msgUUID,
655
+ fake_message_id: msgUUID, // 用来作为这条消息的唯一标识,防止 message_id 刷新导致的闪烁
656
+ message_type: chatStore.chatInfo.show.type,
657
+ time: parseInt(String(new Date().getTime() / 1000)),
658
+ local_time: Date.now(),
659
+ timestamp_ms: Date.now(),
660
+ time_ms: Date.now(),
661
+ post_type: 'message',
662
+ sender: {
663
+ user_id: authStore.loginInfo.uin,
664
+ nickname: authStore.loginInfo.nickname,
665
+ avatar: authStore.loginInfo.avatar || botLogin?.avatar,
666
+ },
667
+ message: preShowMsg,
668
+ } as { [key: string]: any }
669
+ showMsg.raw_message = getMsgRawTxt(showMsg)
670
+
671
+ const isGroupChat = chatStore.chatInfo.show.type === 'group'
672
+ showMsg.channel_type = isGroupChat ? 0 : 1
673
+ if (isGroupChat) {
674
+ showMsg.group_id = chatStore.chatInfo.show.id
675
+ } else {
676
+ showMsg.user_id = chatStore.chatInfo.show.id
677
+ }
678
+ chatStore.messageList = chatStore.messageList.concat([showMsg])
679
+
680
+ // 发送方不一定会上报自身消息事件,先用预发送消息同步会话预览。
681
+ const sessionId = normalizeSessionId(String(id).split('/')[0])
682
+ const session = contactStore.baseOnMsgList.get(sessionId) ??
683
+ findSessionContact(contactStore.userList, sessionId)
684
+ if (session) {
685
+ const raw = getMsgRawTxt(showMsg)
686
+ const senderName = authStore.loginInfo.nickname
687
+ Object.assign(session, {
688
+ message_id: showMsg.message_id,
689
+ raw_msg: type === 'group' && senderName ? `${senderName}: ${raw}` : raw,
690
+ raw_msg_base: raw,
691
+ time: showMsg.time * 1000,
692
+ })
693
+ contactStore.baseOnMsgList.set(normalizeSessionId(sessionId), session)
694
+ updateBaseOnMsgList()
695
+ }
696
+ }
697
+ // 预发送保留 Base64 用于本地预览;真正发送前先写入后端临时目录
698
+ try {
699
+ msg = await persistBase64Media(msg)
700
+ } catch (error) {
701
+ logger.error(error as Error, '媒体上传失败,已阻止发送原始 Base64')
702
+ return
703
+ }
704
+ const fakeMessage = chatStore.messageList.find((item) => {
705
+ return String(item.fake_message_id ?? '') === msgUUID
706
+ })
707
+ if (fakeMessage) {
708
+ if (typeof msg === 'string') {
709
+ fakeMessage.message = parsePreviewMarkup(msg)
710
+ } else if (Array.isArray(msg)) {
711
+ fakeMessage.message = msg
712
+ }
713
+ fakeMessage.raw_message = getMsgRawTxt(fakeMessage)
714
+ }
715
+ // 检查消息体是否需要处理
716
+ if (uiStore.msgType == BotMsgType.Array) {
717
+ if (msg && typeof msg != 'string') {
718
+ const newMsg = [] as any
719
+ msg.forEach((item) => {
720
+ const newResult = {} as { [key: string]: any }
721
+ newResult.type = item.type
722
+ newResult.data = item
723
+ delete newResult.data.type
724
+ // 特殊处理,如果 newResult.data 里有 _type 字段,给它改成 type
725
+ if (newResult.data._type != undefined) {
726
+ newResult.data.type = newResult.data._type
727
+ delete newResult.data._type
728
+ }
729
+ newMsg.push(newResult)
730
+ })
731
+ msg = newMsg
732
+ }
733
+ }
734
+ if (msg !== undefined && msg.length > 0) {
735
+ if (authStore.jsonMap.name === 'Lagrange.OneBot') {
736
+ lgrSendMsg(id, msg, type, echo + '_uuid_' + msgUUID)
737
+ sendStatEvent('send_msg', { type: type })
738
+ return
739
+ }
740
+ switch (type) {
741
+ case 'group':
742
+ Connector.send(
743
+ authStore.jsonMap.message_list.name_group_send ??
744
+ 'send_msg',
745
+ {
746
+ group_id: id,
747
+ guild_id: String(targetGuildId ?? chatStore.chatInfo.show.guild_id ?? id),
748
+ channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ?? id),
749
+ message: msg,
750
+ },
751
+ echo + '_uuid_' + msgUUID,
752
+ )
753
+ break
754
+ case 'user': {
755
+ if (String(id).indexOf('/') > 1) {
756
+ Connector.send(
757
+ authStore.jsonMap.message_list.name_temp_send ??
758
+ 'send_temp_msg',
759
+ {
760
+ user_id: id.split('/')[0],
761
+ group_id: id.split('/')[1],
762
+ channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ?? `private:${String(id).split('/')[0]}`),
763
+ message: msg,
764
+ },
765
+ echo + '_uuid_' + msgUUID,
766
+ )
767
+ } else {
768
+ Connector.send(
769
+ authStore.jsonMap.message_list.name_user_send ??
770
+ 'send_msg',
771
+ {
772
+ user_id: id,
773
+ channel_id: String(targetChannelId ?? chatStore.chatInfo.show.channel_id ?? (/^(?:group|room|chat|channel|guild|private):/i.test(String(id)) ? String(id) : `private:${String(id)}`)),
774
+ message: msg,
775
+ },
776
+ echo + '_uuid_' + msgUUID,
777
+ )
778
+ }
779
+ break
780
+ }
781
+ }
782
+ sendStatEvent('send_msg', { type: type })
783
+ }
784
+ }
785
+
786
+ export function updateLastestHistory(item: UserFriendElem & UserGroupElem) {
787
+ const authStore = useAuthStore()
788
+ // 发起获取历史消息请求
789
+ const type = item.user_id ? 'user' : 'group'
790
+ const id = item.user_id ? item.user_id : item.group_id
791
+ let name
792
+ if (authStore.jsonMap.message_list && type != 'group') {
793
+ name = authStore.jsonMap.message_list.private_name
794
+ } else {
795
+ name = authStore.jsonMap.message_list.name
796
+ }
797
+ Connector.send(
798
+ name ?? 'get_chat_history',
799
+ {
800
+ message_type: authStore.jsonMap.message_list.message_type[type],
801
+ group_id: id,
802
+ user_id: id,
803
+ message_seq: 0,
804
+ message_id: 0,
805
+ count: 1,
806
+ },
807
+ 'getChatHistoryOnMsg_' + id,
808
+ )
809
+ }
810
+
811
+ function getSessionTime(item: UserFriendElem & UserGroupElem) {
812
+ const time = Number(item.time ?? 0)
813
+ return Number.isFinite(time) ? time : 0
814
+ }
815
+
816
+ function getSessionSortName(item: UserFriendElem & UserGroupElem) {
817
+ return item.py_start ?? getShowName(item.group_name ?? item.nickname ?? '', item.remark ?? '')
818
+ }
819
+
820
+ function normalizeBaseSessionMap() {
821
+ const contactStore = useContactStore()
822
+ let needNormalize = false
823
+ for (const id of contactStore.baseOnMsgList.keys()) {
824
+ if (typeof id !== 'string') {
825
+ needNormalize = true
826
+ break
827
+ }
828
+ }
829
+ if (!needNormalize) return
830
+
831
+ const normalized = new Map<string, UserFriendElem & UserGroupElem>()
832
+ contactStore.baseOnMsgList.forEach((item, id) => {
833
+ normalized.set(normalizeSessionId(id), item)
834
+ })
835
+ contactStore.baseOnMsgList.clear()
836
+ for (const [id, item] of normalized) {
837
+ contactStore.baseOnMsgList.set(id, item)
838
+ }
839
+ }
840
+
841
+ function getSessionList() {
842
+ const contactStore = useContactStore()
843
+ const settingsStore = useSettingsStore()
844
+ const sessionMap = new Map<string, UserFriendElem & UserGroupElem>()
845
+
846
+ if (settingsStore.sysConfig.session_display_mode === 'all') {
847
+ contactStore.userList.forEach((item) => {
848
+ const id = normalizeSessionId(getSessionId(item))
849
+ if (id !== '0' && id !== '') {
850
+ sessionMap.set(id, item)
851
+ }
852
+ })
853
+ }
854
+
855
+ contactStore.baseOnMsgList.forEach((item, id) => {
856
+ sessionMap.set(normalizeSessionId(id), item)
857
+ })
858
+
859
+ return [...sessionMap.values()]
860
+ }
861
+
862
+ /**
863
+ * 刷新消息列表排序
864
+ */
865
+ export function updateBaseOnMsgList() {
866
+ const contactStore = useContactStore()
867
+ const settingsStore = useSettingsStore()
868
+ normalizeBaseSessionMap()
869
+ const allList = getSessionList()
870
+ // 先更具 item.always_top 是不是 true 拆为两个数组
871
+ const topList = allList.filter((item) => item.always_top)
872
+ const normalList = allList.filter((item) => !item.always_top)
873
+ // 将两个数组按照 item.time 降序排序
874
+ // item.time 不存在或者相同时按照 item.py_start 降序排序
875
+
876
+ const sortFun = (
877
+ a: UserFriendElem & UserGroupElem,
878
+ b: UserFriendElem & UserGroupElem,
879
+ ) => {
880
+ const timeA = getSessionTime(a)
881
+ const timeB = getSessionTime(b)
882
+ if (timeA !== timeB) return timeB - timeA
883
+
884
+ return getSessionSortName(b).localeCompare(getSessionSortName(a))
885
+ }
886
+ topList.sort(sortFun)
887
+ normalList.sort(sortFun)
888
+
889
+ let onMsgList = [] as any[]
890
+ let groupAssistList = [] as any[]
891
+ if (settingsStore.sysConfig.bubble_sort_user) {
892
+ // 将 normalList 进行拆分
893
+ const shouldShowInMainList = (item: UserFriendElem & UserGroupElem) => {
894
+ return item.user_id || item.always_top
895
+ }
896
+ onMsgList = topList.concat(normalList.filter((item) => {
897
+ return shouldShowInMainList(item)
898
+ }))
899
+ groupAssistList = normalList.filter((item) => {
900
+ return item.group_id && !shouldShowInMainList(item)
901
+ })
902
+ } else {
903
+ onMsgList = topList.concat(normalList)
904
+ }
905
+
906
+ contactStore.onMsgList = onMsgList
907
+ contactStore.groupAssistList = groupAssistList
908
+ }
909
+
910
+ /**
911
+ * 判断当前消息是否可以通知
912
+ * @param id 群号
913
+ * @returns 是否可以通知
914
+ */
915
+ export function canGroupNotice(id: number | string) {
916
+ const authStore = useAuthStore()
917
+ const noticeInfo = option.get('notice_group') ?? {}
918
+ const list = noticeInfo[authStore.loginInfo.uin]
919
+ if (list) {
920
+ return list.indexOf(id) >= 0
921
+ }
922
+ return false
923
+ }
924
+
925
+ /**
926
+ * 戳一戳触发动画
927
+ * @param animeBody 动画作用的元素
928
+ * @param windowInfo 窗口信息,在 electron 中使用
929
+ */
930
+ export function pokeAnime(animeBody: HTMLElement | null, windowInfo = null as {
931
+ x: number
932
+ y: number
933
+ width: number
934
+ height: number
935
+ } | null) {
936
+ if (animeBody) {
937
+ const timeLine = anime.timeline({ targets: animeBody })
938
+ // 如果窗口小于 500px 播放完整的动画(手机端样式)
939
+ if (
940
+ (document.getElementById('app')?.offsetWidth ?? 500) <
941
+ 500
942
+ ) {
943
+ navigator.vibrate([10, 740, 10])
944
+ timeLine.add({ translateX: 30, duration: 600, easing: 'cubicBezier(.44,.09,.53,1)' })
945
+ .add({ translateX: 0, duration: 150, easing: 'cubicBezier(.44,.09,.53,1)' })
946
+ .add({ translateX: [0, 25, 0], duration: 500, easing: 'cubicBezier(.21,.27,.82,.67)' })
947
+ .add({ targets: {}, duration: 1000 })
948
+ .add({ translateX: 70, duration: 1300, easing: 'cubicBezier(.89,.72,.72,1.13)' })
949
+ .add({ translateX: 0, duration: 100, easing: 'easeOutSine' })
950
+ }
951
+ timeLine.add({ translateX: [-10, 10, -5, 5, 0], duration: 500, easing: 'cubicBezier(.44,.09,.53,1)' })
952
+ timeLine.change = async () => {
953
+ if (animeBody) {
954
+ animeBody.parentElement?.parentElement?.classList.add('poking')
955
+ const teansformX = animeBody.style.transform
956
+ // teansformX 的数字可能是科学计数法,需要转换为普通数字
957
+ let num = Number((teansformX.match(/-?\d+\.?\d*/g) ?? [0])[0])
958
+ // 取整
959
+ num = Math.round(num)
960
+ // 输出 translateX
961
+ if (backend.isDesktop() && windowInfo) {
962
+ await backend.call(undefined, 'win:move', false, {
963
+ x: windowInfo.x + num,
964
+ y: windowInfo.y,
965
+ })
966
+ }
967
+ }
968
+ }
969
+ timeLine.changeComplete = () => {
970
+ if (animeBody) {
971
+ animeBody.parentElement?.parentElement?.classList.remove('poking')
972
+ }
973
+ }
974
+ }
975
+ }
976
+
977
+ export function sendMsgAppendInfo(msg: any) {
978
+ if (msg.message) {
979
+ msg.message.forEach(() => {
980
+ // TODO: 消息附加功能,暂时没用到
981
+ })
982
+ }
983
+ }
984
+
985
+ /**
986
+ *
987
+ * @param base group_name 或者 nickname
988
+ * @param remark remark
989
+ * @returns 显示的名称
990
+ */
991
+ export function getShowName(base: string, remark: string) {
992
+ if (!remark || remark == '' || remark == base) {
993
+ return base.replace(/[\u202A-\u202E\u2066-\u2069]/g, '')
994
+ } else {
995
+ return (remark + '(' + base + ')').replace(/[\u202A-\u202E\u2066-\u2069]/g, '')
996
+ }
997
+ }
998
+
999
+ /**
1000
+ * 判断是否需要显示时间戳(上下超过五分钟的消息)
1001
+ * @param timePrv 上条消息的时间戳(10 位)
1002
+ * @param timeNow 当前消息的时间戳(10 位)
1003
+ */
1004
+ export function isShowTime(
1005
+ timePrv: number | undefined,
1006
+ timeNow: number,
1007
+ alwaysShow = false,
1008
+ ): boolean {
1009
+ if (alwaysShow) return true
1010
+ if (timePrv == undefined) return false
1011
+ // 五分钟 10 位时间戳相差 300
1012
+ return timeNow - timePrv >= 300
1013
+ }
1014
+
1015
+ /**
1016
+ * 计算 QQ 等级图标
1017
+ * @param level QQ 等级
1018
+ * @returns 图标数量
1019
+ */
1020
+ export function qqLevelIcons(level) {
1021
+ const result = {
1022
+ crown: 0, // 皇冠
1023
+ sun: 0, // 太阳
1024
+ moon: 0, // 月亮
1025
+ star: 0 // 星星
1026
+ };
1027
+
1028
+ result.crown = Math.floor(level / 64);
1029
+ level %= 64;
1030
+
1031
+ result.sun = Math.floor(level / 16);
1032
+ level %= 16;
1033
+
1034
+ result.moon = Math.floor(level / 4);
1035
+ level %= 4;
1036
+
1037
+ result.star = level;
1038
+
1039
+ return result;
1040
+ }
1041
+
1042
+ /**
1043
+ * 计算 QQ 等级表情
1044
+ * @param level QQ 等级
1045
+ * @returns 表情字符串
1046
+ */
1047
+ export function qqLevelToEmoji(level) {
1048
+ const rawLevel = level
1049
+ if (level <= 0) return level
1050
+
1051
+ const crown = Math.floor(level / 64);
1052
+ level %= 64;
1053
+
1054
+ const sun = Math.floor(level / 16);
1055
+ level %= 16;
1056
+
1057
+ const moon = Math.floor(level / 4);
1058
+ level %= 4;
1059
+
1060
+ const star = level;
1061
+
1062
+ return '👑'.repeat(crown) + '☀️'.repeat(sun) + '🌙'.repeat(moon) + '⭐️'.repeat(star) + '(' + rawLevel + ')';
1063
+ }
1064
+
1065
+ /**
1066
+ * 将图片 URL 转换为 PNG 格式的 Uint8Array
1067
+ * 支持 base64 和 HTTP URL 格式的图片
1068
+ * @param imageUrl 图片 URL
1069
+ */
1070
+ export async function getImageUrlData(imageUrl: string): Promise<{ buffer: Uint8Array, blob: Blob }> {
1071
+ return new Promise((resolve, reject) => {
1072
+ const img = new Image()
1073
+
1074
+ img.onload = () => {
1075
+ try {
1076
+ // 创建 canvas 并设置尺寸
1077
+ const canvas = document.createElement('canvas')
1078
+ canvas.width = img.width
1079
+ canvas.height = img.height
1080
+
1081
+ // 获取 2D 上下文并绘制图片
1082
+ const ctx = canvas.getContext('2d')
1083
+ if (!ctx) {
1084
+ reject(new Error('无法获取 Canvas 上下文'))
1085
+ return
1086
+ }
1087
+
1088
+ ctx.drawImage(img, 0, 0)
1089
+
1090
+ // 将 canvas 转换为 PNG 格式的 blob
1091
+ canvas.toBlob((blob) => {
1092
+ if (!blob) {
1093
+ reject(new Error('图片转换失败'))
1094
+ return
1095
+ }
1096
+
1097
+ // 读取 blob 为 ArrayBuffer,然后转换为 Uint8Array
1098
+ const reader = new FileReader()
1099
+ reader.onload = () => {
1100
+ const arrayBuffer = reader.result as ArrayBuffer
1101
+ resolve({
1102
+ buffer: new Uint8Array(arrayBuffer),
1103
+ blob: blob
1104
+ }
1105
+ )
1106
+ }
1107
+ reader.onerror = () => {
1108
+ reject(new Error('读取图片数据失败'))
1109
+ }
1110
+ reader.readAsArrayBuffer(blob)
1111
+ }, 'image/png') // 强制转换为 PNG 格式
1112
+ } catch (error) {
1113
+ reject(error)
1114
+ }
1115
+ }
1116
+
1117
+ img.onerror = () => {
1118
+ reject(new Error('图片加载失败'))
1119
+ }
1120
+
1121
+ // 处理跨域问题
1122
+ img.crossOrigin = 'anonymous'
1123
+ img.src = imageUrl
1124
+ })
1125
+ }
1126
+
1127
+ /**
1128
+ * 判断这个消息是不是[已删除]
1129
+ * @param msg
1130
+ */
1131
+ export function isDeleteMsg(msg: any): boolean {
1132
+ const authStore = useAuthStore()
1133
+ if (!['message', 'message_sent'].includes(msg.post_type)) return false
1134
+ if (msg.sender.user_id !== authStore.loginInfo.uin) return false
1135
+ if (msg.raw_message !== '&#91;已删除&#93;') return false
1136
+ return true
1137
+ }
1138
+
1139
+ /**
1140
+ * 获取两个字符串之间的差异
1141
+ * @param a 原字符串
1142
+ * @param b 新字符串
1143
+ * @returns 差异列表,包含差异的起始位置、结束位置和差异内容
1144
+ */
1145
+ export function getDifferencesWithRanges(a: string, b: string) {
1146
+ let i = 0; // a 的指针
1147
+ let j = 0; // b 的指针
1148
+ const diffs = [] as { start: number; end: number; str: string }[]
1149
+ let currentDiffStart = null as number | null
1150
+ let currentDiffStr = ''
1151
+
1152
+ while (j < b.length) {
1153
+ if (i < a.length && a[i] === b[j]) {
1154
+ // 遇到匹配字符,先保存上一个差异块
1155
+ if (currentDiffStr) {
1156
+ diffs.push({
1157
+ start: currentDiffStart!,
1158
+ end: j - 1,
1159
+ str: currentDiffStr
1160
+ })
1161
+ currentDiffStr = ''
1162
+ currentDiffStart = null
1163
+ }
1164
+ i++;
1165
+ } else {
1166
+ // 遇到差异字符,记录
1167
+ if (currentDiffStart === null) currentDiffStart = j;
1168
+ currentDiffStr += b[j];
1169
+ }
1170
+ j++;
1171
+ }
1172
+
1173
+ // 遍历结束,如果还有未保存的差异块
1174
+ if (currentDiffStr) {
1175
+ diffs.push({
1176
+ start: currentDiffStart!,
1177
+ end: j - 1,
1178
+ str: currentDiffStr
1179
+ });
1180
+ }
1181
+
1182
+ return diffs;
1183
+ }
1184
+
1185
+ /**
1186
+ * lgr专用发送消息,懒得写了,不做通用适配,胡乱应付下吧
1187
+ * @param msg 消息内容
1188
+ */
1189
+ function lgrSendMsg(id: string, msg: any, type: string, cb: string) {
1190
+ if (msg[0].type === 'node') {
1191
+ const sendMsgs = [] as any[]
1192
+ msg.forEach((item) => {
1193
+ const msg = {
1194
+ type: item.type,
1195
+ data: {
1196
+ user_id: item.data.user_id.toString(),
1197
+ nickname: item.data.nickname,
1198
+ content: item.data.content.map((item) => {
1199
+ const copy = { ...item }
1200
+ delete copy.type
1201
+ return {
1202
+ type: item.type,
1203
+ data: { ...copy }
1204
+ }
1205
+ }),
1206
+ },
1207
+ }
1208
+ sendMsgs.push(msg)
1209
+ })
1210
+ if (type === 'group') {
1211
+ Connector.send(
1212
+ 'send_group_forward_msg',
1213
+ { group_id: id, messages: sendMsgs },
1214
+ cb,
1215
+ )
1216
+ } else if (type === 'user') {
1217
+ Connector.send(
1218
+ 'send_private_forward_msg',
1219
+ { user_id: id, messages: sendMsgs },
1220
+ cb,
1221
+ )
1222
+ } else {
1223
+ new PopInfo().add(PopType.ERR, 'lgr不支持匿名聊天')
1224
+ }
1225
+ } else {
1226
+ if (type === 'group') {
1227
+ Connector.send(
1228
+ 'send_group_msg',
1229
+ { group_id: id, message: msg },
1230
+ cb,
1231
+ )
1232
+ } else if (type === 'user') {
1233
+ Connector.send(
1234
+ 'send_private_msg',
1235
+ { user_id: id, message: msg },
1236
+ cb,
1237
+ )
1238
+ } else {
1239
+ new PopInfo().add(PopType.ERR, 'lgr不支持匿名聊天')
1240
+ }
1241
+ }
1242
+ }