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.
- package/client/vue/index.vue +60 -634
- package/client/web/env.d.ts +56 -0
- package/client/web/index.html +21 -0
- package/client/web/package-lock.json +3419 -0
- package/client/web/package.json +62 -0
- package/client/web/public/bcui/css/color-dark.css +35 -0
- package/client/web/public/bcui/css/color-light.css +35 -0
- package/client/web/public/bcui/css/style.css +468 -0
- package/client/web/public/bcui/js/main.js +193 -0
- package/client/web/public/css/append-dark.css +13 -0
- package/client/web/public/css/append-light.css +13 -0
- package/client/web/public/css/append-napcat-dark.css +14 -0
- package/client/web/public/css/append-napcat-light.css +14 -0
- package/client/web/public/favicon.ico +0 -0
- package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
- package/client/web/public/img/icons/icon-maskable.png +0 -0
- package/client/web/public/img/icons/icon.png +0 -0
- package/client/web/public/img/icons/icon.svg +10 -0
- package/client/web/public/robots.txt +2 -0
- package/client/web/public/sw.js +58 -0
- package/client/web/scripts/copy-bcui.mjs +17 -0
- package/client/web/src/App.vue +1127 -0
- package/client/web/src/assets/css/append/append_darwin.css +68 -0
- package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
- package/client/web/src/assets/css/append/append_linux.css +125 -0
- package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
- package/client/web/src/assets/css/append/append_napcat.css +35 -0
- package/client/web/src/assets/css/append/append_new.css +843 -0
- package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
- package/client/web/src/assets/css/append/append_win32.css +109 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
- package/client/web/src/assets/css/chat.css +2104 -0
- package/client/web/src/assets/css/msg.css +941 -0
- package/client/web/src/assets/css/options.css +811 -0
- package/client/web/src/assets/css/sys_notice.css +62 -0
- package/client/web/src/assets/css/view.css +2105 -0
- package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
- package/client/web/src/assets/img/Vue.png +0 -0
- package/client/web/src/assets/img/cakes.svg +189 -0
- package/client/web/src/assets/img/icons/AppIcon.png +0 -0
- package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/exit.png +0 -0
- package/client/web/src/assets/img/icons/pen.png +0 -0
- package/client/web/src/assets/img/stars.svg +89 -0
- package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
- package/client/web/src/assets/l10n/_About I10n.md +35 -0
- package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
- package/client/web/src/assets/l10n/zh-CN.po +1941 -0
- package/client/web/src/assets/logo.png +0 -0
- package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
- package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
- package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
- package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
- package/client/web/src/assets/qed.txt +1 -0
- package/client/web/src/components/AboutPan.vue +197 -0
- package/client/web/src/components/BulletinBody.vue +129 -0
- package/client/web/src/components/DepPan.vue +310 -0
- package/client/web/src/components/EmojiFace.vue +32 -0
- package/client/web/src/components/FacePan.vue +461 -0
- package/client/web/src/components/FileBody.vue +129 -0
- package/client/web/src/components/FileManager.vue +532 -0
- package/client/web/src/components/FriendBody.vue +54 -0
- package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
- package/client/web/src/components/History.vue +49 -0
- package/client/web/src/components/LazyLottie.vue +82 -0
- package/client/web/src/components/MergePan.vue +127 -0
- package/client/web/src/components/MsgBody.vue +1478 -0
- package/client/web/src/components/MusicPlayer.vue +549 -0
- package/client/web/src/components/NoticeBody.vue +168 -0
- package/client/web/src/components/QzonePermissionPan.vue +150 -0
- package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
- package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
- package/client/web/src/components/TinySessionBody.vue +51 -0
- package/client/web/src/components/UmamiInfoPan.vue +1549 -0
- package/client/web/src/components/UpdatePan.vue +263 -0
- package/client/web/src/components/ViewerCom.vue +1360 -0
- package/client/web/src/components/VoiceMsg.vue +626 -0
- package/client/web/src/components/WelPan.vue +894 -0
- package/client/web/src/components/msg-body/.gitkeep +0 -0
- package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
- package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
- package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
- package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
- package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
- package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
- package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
- package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
- package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
- package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
- package/client/web/src/components/notice-component/.gitkeep +0 -0
- package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
- package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
- package/client/web/src/components/tooltip/Tooltip.vue +50 -0
- package/client/web/src/components/tooltip/Tooltips.vue +14 -0
- package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
- package/client/web/src/function/base.ts +238 -0
- package/client/web/src/function/connect.ts +2042 -0
- package/client/web/src/function/debug.ts +10 -0
- package/client/web/src/function/elements/information.ts +141 -0
- package/client/web/src/function/elements/system.ts +101 -0
- package/client/web/src/function/elements/vueComp.ts +62 -0
- package/client/web/src/function/favicon.ts +76 -0
- package/client/web/src/function/input.ts +69 -0
- package/client/web/src/function/model/emoji.ts +127 -0
- package/client/web/src/function/model/img.ts +150 -0
- package/client/web/src/function/model/msg-body.ts +51 -0
- package/client/web/src/function/msg.ts +2674 -0
- package/client/web/src/function/notify.ts +225 -0
- package/client/web/src/function/option.ts +749 -0
- package/client/web/src/function/satori-model.ts +818 -0
- package/client/web/src/function/satori.ts +325 -0
- package/client/web/src/function/sender.ts +178 -0
- package/client/web/src/function/tooltip.ts +49 -0
- package/client/web/src/function/utils/appUtil.ts +2288 -0
- package/client/web/src/function/utils/avatarUtil.ts +31 -0
- package/client/web/src/function/utils/backgroundUtil.ts +110 -0
- package/client/web/src/function/utils/linkViewUtil.ts +101 -0
- package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
- package/client/web/src/function/utils/msgUtil.ts +1242 -0
- package/client/web/src/function/utils/pinyin.ts +141 -0
- package/client/web/src/function/utils/recordUtil.ts +187 -0
- package/client/web/src/function/utils/sessionUtil.ts +114 -0
- package/client/web/src/function/utils/systemUtil.ts +664 -0
- package/client/web/src/main.ts +91 -0
- package/client/web/src/pages/Chat.vue +3453 -0
- package/client/web/src/pages/Friends.vue +636 -0
- package/client/web/src/pages/Info.vue +717 -0
- package/client/web/src/pages/Messages.vue +875 -0
- package/client/web/src/pages/Options.vue +82 -0
- package/client/web/src/pages/Qzone.vue +1178 -0
- package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
- package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
- package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
- package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
- package/client/web/src/pages/options/OptAccount.vue +107 -0
- package/client/web/src/pages/options/OptAddon.vue +185 -0
- package/client/web/src/pages/options/OptDev.vue +704 -0
- package/client/web/src/pages/options/OptFunction.vue +403 -0
- package/client/web/src/pages/options/OptInfo.vue +165 -0
- package/client/web/src/pages/options/OptView.vue +846 -0
- package/client/web/src/runtime/backend.ts +310 -0
- package/client/web/src/state/auth.ts +14 -0
- package/client/web/src/state/chat.ts +36 -0
- package/client/web/src/state/connection.ts +20 -0
- package/client/web/src/state/contact.ts +37 -0
- package/client/web/src/state/qzone.ts +18 -0
- package/client/web/src/state/sessionHistory.ts +25 -0
- package/client/web/src/state/settings.ts +18 -0
- package/client/web/src/state/sticker.ts +10 -0
- package/client/web/src/state/ui.ts +56 -0
- package/client/web/tsconfig.json +27 -0
- package/client/web/tsconfig.tsbuildinfo +1 -0
- package/client/web/vite.config.ts +33 -0
- package/dist/index.js +1 -12
- package/dist/style.css +1 -1
- package/lib/index.js +2070 -2077
- package/package.json +15 -2
- package/readme.md +0 -22
- package/src/bootstrap.ts +71 -0
- package/src/cache.ts +319 -0
- package/src/config.ts +23 -35
- package/src/database.ts +650 -0
- package/src/index.ts +27 -59
- package/src/media.ts +302 -0
- package/src/recorder.ts +175 -0
- package/src/self-message.ts +456 -0
- package/src/server.d.ts +26 -0
- package/src/types.ts +108 -48
- package/src/web.ts +712 -0
- package/client/vue/chat-logic.ts +0 -683
- package/client/vue/composables/useChatActions.ts +0 -61
- package/client/vue/composables/useChatData.ts +0 -185
- package/client/vue/composables/useImageCache.ts +0 -281
- package/client/vue/composables/useVideoCache.ts +0 -125
- package/client/vue/types.ts +0 -72
- package/lib/api-handlers.d.ts +0 -25
- package/lib/config.d.ts +0 -14
- package/lib/file-manager.d.ts +0 -124
- package/lib/index.d.ts +0 -17
- package/lib/logger.d.ts +0 -9
- package/lib/message-handler.d.ts +0 -35
- package/lib/types.d.ts +0 -57
- package/lib/utils.d.ts +0 -18
- package/src/api-handlers.ts +0 -646
- package/src/file-manager.ts +0 -1436
- package/src/message-handler.ts +0 -507
- 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, '&').replace(/"/g, '"')
|
|
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 !== '[已删除]') 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
|
+
}
|