koishi-plugin-chat-patch 1.1.0 → 1.2.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/chat-logic.ts +442 -69
- package/client/vue/index.vue +44 -14
- package/client/vue/style.css +75 -46
- package/dist/index.js +8 -8
- package/dist/style.css +1 -1
- package/lib/config.d.ts +14 -0
- package/lib/index.js +44 -5
- package/package.json +1 -1
- package/src/api-handlers.ts +42 -4
- package/src/config.ts +6 -1
- package/src/index.ts +2 -2
- package/lib/index.d.ts +0 -17
package/client/vue/chat-logic.ts
CHANGED
|
@@ -601,7 +601,7 @@ export function useChatLogic() {
|
|
|
601
601
|
return h('span', {
|
|
602
602
|
class: 'message-at',
|
|
603
603
|
title: element.attrs.name
|
|
604
|
-
},
|
|
604
|
+
}, `@${(element.attrs.name || element.attrs.id).replace("@", '')}`)
|
|
605
605
|
|
|
606
606
|
case 'json':
|
|
607
607
|
return h('div', { class: 'message-image-container' }, [
|
|
@@ -661,6 +661,7 @@ export function useChatLogic() {
|
|
|
661
661
|
loading: boolean
|
|
662
662
|
}>>({})
|
|
663
663
|
|
|
664
|
+
// Config默认值,实际值将从后端获取
|
|
664
665
|
const pluginConfig = ref<{
|
|
665
666
|
maxMessagesPerChannel: number
|
|
666
667
|
keepMessagesOnClear: number
|
|
@@ -670,12 +671,14 @@ export function useChatLogic() {
|
|
|
670
671
|
exactMatch: boolean
|
|
671
672
|
}>
|
|
672
673
|
chatContainerHeight: number
|
|
674
|
+
clearIndexedDBOnStart: boolean
|
|
673
675
|
}>({
|
|
674
676
|
maxMessagesPerChannel: 1000,
|
|
675
677
|
keepMessagesOnClear: 50,
|
|
676
678
|
loggerinfo: false,
|
|
677
679
|
blockedPlatforms: [],
|
|
678
|
-
chatContainerHeight: 80
|
|
680
|
+
chatContainerHeight: 80,
|
|
681
|
+
clearIndexedDBOnStart: true
|
|
679
682
|
})
|
|
680
683
|
|
|
681
684
|
// 图片缓存 - IndexedDB
|
|
@@ -745,7 +748,7 @@ export function useChatLogic() {
|
|
|
745
748
|
const showScrollButton = ref<boolean>(false)
|
|
746
749
|
const isUserScrolling = ref<boolean>(false)
|
|
747
750
|
const isSending = ref<boolean>(false)
|
|
748
|
-
const isLoadingMore = ref<boolean>(false)
|
|
751
|
+
const isLoadingMore = ref<boolean>(false)
|
|
749
752
|
|
|
750
753
|
// 拖拽相关状态
|
|
751
754
|
const draggingChannel = ref<string>('')
|
|
@@ -764,16 +767,18 @@ export function useChatLogic() {
|
|
|
764
767
|
show: boolean
|
|
765
768
|
x: number
|
|
766
769
|
y: number
|
|
767
|
-
type: 'bot' | 'channel' |
|
|
770
|
+
type: 'bot' | 'channel' | 'message'
|
|
768
771
|
targetId: string
|
|
769
772
|
isSecondClick: boolean
|
|
773
|
+
message?: MessageInfo
|
|
770
774
|
}>({
|
|
771
775
|
show: false,
|
|
772
776
|
x: 0,
|
|
773
777
|
y: 0,
|
|
774
|
-
type:
|
|
778
|
+
type: 'bot',
|
|
775
779
|
targetId: '',
|
|
776
|
-
isSecondClick: false
|
|
780
|
+
isSecondClick: false,
|
|
781
|
+
message: undefined
|
|
777
782
|
})
|
|
778
783
|
|
|
779
784
|
// 置顶状态管理
|
|
@@ -931,6 +936,125 @@ export function useChatLogic() {
|
|
|
931
936
|
}
|
|
932
937
|
}
|
|
933
938
|
|
|
939
|
+
// 解析内联引用消息
|
|
940
|
+
function parseQuoteMessage(content: string, allMessages: MessageInfo[]): { quotedMessage: MessageInfo | null, restContent: string } {
|
|
941
|
+
// 匹配 <quote id="..."/> 格式
|
|
942
|
+
const quoteRegex = /^<quote\s+id="([^"]+)"\s*\/>(.*)/;
|
|
943
|
+
const match = content.match(quoteRegex);
|
|
944
|
+
|
|
945
|
+
if (match) {
|
|
946
|
+
const quotedMessageId = match[1];
|
|
947
|
+
const restContent = match[2];
|
|
948
|
+
|
|
949
|
+
// 在所有消息中查找被引用的消息
|
|
950
|
+
const quotedMessage = allMessages.find(msg => msg.id === quotedMessageId) || null;
|
|
951
|
+
|
|
952
|
+
return { quotedMessage, restContent };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return { quotedMessage: null, restContent: content };
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// 在计算属性中添加解析内联引用消息的函数
|
|
959
|
+
function parseInlineQuote(content: string, allMessages: MessageInfo[]): { quotedMessage: MessageInfo | null, restContent: string } {
|
|
960
|
+
return parseQuoteMessage(content, allMessages);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// 获取内联引用消息
|
|
964
|
+
function getInlineQuoteMessage(message: MessageInfo, allMessages: MessageInfo[]): MessageInfo | null {
|
|
965
|
+
if (!message || !message.content) return null;
|
|
966
|
+
|
|
967
|
+
// 使用 parseInlineQuote 函数解析内联引用
|
|
968
|
+
const parsed = parseInlineQuote(message.content, allMessages);
|
|
969
|
+
return parsed.quotedMessage;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// 获取引用消息的用户信息
|
|
973
|
+
function getQuoteUser(message: MessageInfo, allMessages: MessageInfo[]): { avatar: string, username: string } {
|
|
974
|
+
// 如果有现成的引用消息,直接使用
|
|
975
|
+
if (message.quote && message.quote.user) {
|
|
976
|
+
return {
|
|
977
|
+
avatar: message.quote.user.avatar || '',
|
|
978
|
+
username: message.quote.user.username || ''
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// 检查是否有内联引用消息
|
|
983
|
+
const inlineQuote = getInlineQuoteMessage(message, allMessages);
|
|
984
|
+
if (inlineQuote) {
|
|
985
|
+
return {
|
|
986
|
+
avatar: inlineQuote.avatar || '',
|
|
987
|
+
username: inlineQuote.username || ''
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// 默认返回空对象
|
|
992
|
+
return {
|
|
993
|
+
avatar: '',
|
|
994
|
+
username: ''
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// 获取引用消息的时间戳
|
|
999
|
+
function getQuoteTimestamp(message: MessageInfo, allMessages: MessageInfo[]): number {
|
|
1000
|
+
// 如果有现成的引用消息,直接使用
|
|
1001
|
+
if (message.quote && message.quote.timestamp) {
|
|
1002
|
+
return message.quote.timestamp;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// 检查是否有内联引用消息
|
|
1006
|
+
const inlineQuote = getInlineQuoteMessage(message, allMessages);
|
|
1007
|
+
if (inlineQuote) {
|
|
1008
|
+
return inlineQuote.timestamp;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// 默认返回当前时间
|
|
1012
|
+
return Date.now();
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// 获取引用消息的内容
|
|
1016
|
+
function getQuoteContent(message: MessageInfo, allMessages: MessageInfo[]): string {
|
|
1017
|
+
// 如果有现成的引用消息,直接使用
|
|
1018
|
+
if (message.quote && message.quote.content) {
|
|
1019
|
+
return message.quote.content;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// 检查是否有内联引用消息
|
|
1023
|
+
const inlineQuote = getInlineQuoteMessage(message, allMessages);
|
|
1024
|
+
if (inlineQuote) {
|
|
1025
|
+
return inlineQuote.content;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// 默认返回空字符串
|
|
1029
|
+
return '';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// 获取引用消息的元素
|
|
1033
|
+
function getQuoteElements(message: MessageInfo, allMessages: MessageInfo[]): MessageElement[] {
|
|
1034
|
+
// 如果有现成的引用消息,直接使用
|
|
1035
|
+
if (message.quote && message.quote.elements) {
|
|
1036
|
+
return message.quote.elements;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 检查是否有内联引用消息
|
|
1040
|
+
const inlineQuote = getInlineQuoteMessage(message, allMessages);
|
|
1041
|
+
if (inlineQuote) {
|
|
1042
|
+
return inlineQuote.elements || [];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// 默认返回空数组
|
|
1046
|
+
return [];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// 获取不包含引用部分的消息内容
|
|
1050
|
+
function getMessageContentWithoutQuote(message: MessageInfo, allMessages: MessageInfo[]): string {
|
|
1051
|
+
if (!message || !message.content) return '';
|
|
1052
|
+
|
|
1053
|
+
// 使用 parseInlineQuote 函数解析内联引用
|
|
1054
|
+
const parsed = parseInlineQuote(message.content, allMessages);
|
|
1055
|
+
return parsed.restContent || message.content;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
934
1058
|
// 右键菜单相关方法
|
|
935
1059
|
function handleBotRightClick(event: MouseEvent, botId: string) {
|
|
936
1060
|
event.preventDefault()
|
|
@@ -966,6 +1090,23 @@ export function useChatLogic() {
|
|
|
966
1090
|
showContextMenu(event, 'channel', channelId)
|
|
967
1091
|
}
|
|
968
1092
|
|
|
1093
|
+
function handleMessageRightClick(event: MouseEvent, message: MessageInfo) {
|
|
1094
|
+
event.preventDefault()
|
|
1095
|
+
event.stopPropagation()
|
|
1096
|
+
|
|
1097
|
+
// 检查是否是第二次右键点击同一个目标
|
|
1098
|
+
const isSecondClick = contextMenu.value.show &&
|
|
1099
|
+
contextMenu.value.type === 'message' &&
|
|
1100
|
+
contextMenu.value.message?.id === message.id
|
|
1101
|
+
|
|
1102
|
+
if (isSecondClick) {
|
|
1103
|
+
// 第二次右键,隐藏自定义菜单,让浏览器显示原生菜单
|
|
1104
|
+
hideContextMenu()
|
|
1105
|
+
return
|
|
1106
|
+
}
|
|
1107
|
+
showMessageContextMenu(event, message)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
969
1110
|
function showContextMenu(event: MouseEvent, type: 'bot' | 'channel', targetId: string) {
|
|
970
1111
|
// 确保菜单不会超出屏幕边界
|
|
971
1112
|
const menuWidth = 180
|
|
@@ -994,6 +1135,35 @@ export function useChatLogic() {
|
|
|
994
1135
|
document.addEventListener('keydown', handleKeyDown)
|
|
995
1136
|
}
|
|
996
1137
|
|
|
1138
|
+
function showMessageContextMenu(event: MouseEvent, message: MessageInfo) {
|
|
1139
|
+
// 确保菜单不会超出屏幕边界
|
|
1140
|
+
const menuWidth = 180
|
|
1141
|
+
const menuHeight = 120
|
|
1142
|
+
let x = event.clientX
|
|
1143
|
+
let y = event.clientY
|
|
1144
|
+
|
|
1145
|
+
if (x + menuWidth > window.innerWidth) {
|
|
1146
|
+
x = window.innerWidth - menuWidth - 10
|
|
1147
|
+
}
|
|
1148
|
+
if (y + menuHeight > window.innerHeight) {
|
|
1149
|
+
y = window.innerHeight - menuHeight - 10
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
contextMenu.value = {
|
|
1153
|
+
show: true,
|
|
1154
|
+
x,
|
|
1155
|
+
y,
|
|
1156
|
+
type: 'message',
|
|
1157
|
+
targetId: message.id,
|
|
1158
|
+
isSecondClick: false,
|
|
1159
|
+
message: message
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// 添加全局事件监听器来隐藏菜单
|
|
1163
|
+
document.addEventListener('click', hideContextMenu, { once: true })
|
|
1164
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
1165
|
+
}
|
|
1166
|
+
|
|
997
1167
|
function hideContextMenu() {
|
|
998
1168
|
contextMenu.value.show = false
|
|
999
1169
|
document.removeEventListener('click', hideContextMenu)
|
|
@@ -1147,6 +1317,9 @@ export function useChatLogic() {
|
|
|
1147
1317
|
const messageContent = inputMessage.value.trim()
|
|
1148
1318
|
if (!messageContent && uploadedImages.value.length === 0) return
|
|
1149
1319
|
|
|
1320
|
+
// 保存当前频道信息,用于发送后重新加载
|
|
1321
|
+
const currentChannelId = selectedChannel.value
|
|
1322
|
+
|
|
1150
1323
|
// 设置发送状态
|
|
1151
1324
|
isSending.value = true
|
|
1152
1325
|
|
|
@@ -1176,6 +1349,15 @@ export function useChatLogic() {
|
|
|
1176
1349
|
uploadedImages.value = []
|
|
1177
1350
|
showActionMenu.value = false
|
|
1178
1351
|
|
|
1352
|
+
// 消息发送成功后,重新选择当前频道以完全重新渲染
|
|
1353
|
+
if (currentChannelId) {
|
|
1354
|
+
// 临时清空选择
|
|
1355
|
+
selectedChannel.value = ''
|
|
1356
|
+
await nextTick()
|
|
1357
|
+
// 重新选择当前频道,触发完整的重新渲染流程
|
|
1358
|
+
selectChannel(currentChannelId)
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1179
1361
|
// 消息发送成功后,主动通知后端清理临时文件
|
|
1180
1362
|
if (result.tempImageIds && result.tempImageIds.length > 0) {
|
|
1181
1363
|
try {
|
|
@@ -1212,7 +1394,6 @@ export function useChatLogic() {
|
|
|
1212
1394
|
}
|
|
1213
1395
|
showActionMenu.value = false
|
|
1214
1396
|
}
|
|
1215
|
-
|
|
1216
1397
|
async function handleFileSelect(event: Event) {
|
|
1217
1398
|
const target = event.target as HTMLInputElement
|
|
1218
1399
|
const files = target.files
|
|
@@ -1344,6 +1525,155 @@ export function useChatLogic() {
|
|
|
1344
1525
|
})
|
|
1345
1526
|
}
|
|
1346
1527
|
|
|
1528
|
+
// 消息右键菜单处理函数
|
|
1529
|
+
async function handlePlusOne(message: MessageInfo | undefined) {
|
|
1530
|
+
if (!message) return;
|
|
1531
|
+
hideContextMenu()
|
|
1532
|
+
|
|
1533
|
+
try {
|
|
1534
|
+
// 构造要发送的内容,保持原始类型
|
|
1535
|
+
let content = ''
|
|
1536
|
+
const elements = message.elements || []
|
|
1537
|
+
|
|
1538
|
+
// 如果有elements,处理各种元素类型
|
|
1539
|
+
if (elements.length > 0) {
|
|
1540
|
+
for (const element of elements) {
|
|
1541
|
+
switch (element.type) {
|
|
1542
|
+
case 'img':
|
|
1543
|
+
case 'image':
|
|
1544
|
+
// 对于图片类型,转换为img标签
|
|
1545
|
+
const imageUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
1546
|
+
if (imageUrl) {
|
|
1547
|
+
content += `<img src="${imageUrl}"/>`
|
|
1548
|
+
}
|
|
1549
|
+
break
|
|
1550
|
+
case 'mface':
|
|
1551
|
+
// 对于mface,转换为img标签
|
|
1552
|
+
const mfaceUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
1553
|
+
if (mfaceUrl) {
|
|
1554
|
+
content += `<img src="${mfaceUrl}"/>`
|
|
1555
|
+
}
|
|
1556
|
+
break
|
|
1557
|
+
case 'face':
|
|
1558
|
+
if (element.children && element.children[0]?.attrs?.src) {
|
|
1559
|
+
const faceUrl = element.children[0].attrs.src
|
|
1560
|
+
content += `<img src="${faceUrl}"/>`
|
|
1561
|
+
} else {
|
|
1562
|
+
content += `[${element.attrs.name || element.attrs.id}]`
|
|
1563
|
+
}
|
|
1564
|
+
break
|
|
1565
|
+
case 'at':
|
|
1566
|
+
// 对于@消息,保持原始格式
|
|
1567
|
+
content += `<at id="${element.attrs.id}" name="${element.attrs.name}"/>`
|
|
1568
|
+
break
|
|
1569
|
+
case 'text':
|
|
1570
|
+
// 对于文本元素,直接添加文本内容
|
|
1571
|
+
content += element.attrs.content || ''
|
|
1572
|
+
break
|
|
1573
|
+
default:
|
|
1574
|
+
// 其他类型保持原始内容
|
|
1575
|
+
content += message.content
|
|
1576
|
+
break
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
} else {
|
|
1580
|
+
// 如果没有elements,使用原始内容
|
|
1581
|
+
content = message.content
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// 设置输入框内容
|
|
1585
|
+
inputMessage.value = content
|
|
1586
|
+
|
|
1587
|
+
// 触发发送
|
|
1588
|
+
await sendMessage()
|
|
1589
|
+
} catch (error: any) {
|
|
1590
|
+
console.error('+1操作失败:', error)
|
|
1591
|
+
showNotification('操作失败: ' + (error?.message || String(error)), 'error')
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function handleCopyMessage(message: MessageInfo | undefined) {
|
|
1596
|
+
if (!message) return;
|
|
1597
|
+
hideContextMenu()
|
|
1598
|
+
|
|
1599
|
+
try {
|
|
1600
|
+
// 复制消息内容到剪贴板
|
|
1601
|
+
let content = ''
|
|
1602
|
+
const elements = message.elements || []
|
|
1603
|
+
|
|
1604
|
+
// 如果有elements,处理各种元素类型
|
|
1605
|
+
if (elements.length > 0) {
|
|
1606
|
+
for (const element of elements) {
|
|
1607
|
+
switch (element.type) {
|
|
1608
|
+
case 'img':
|
|
1609
|
+
case 'image':
|
|
1610
|
+
const imageUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
1611
|
+
if (imageUrl) {
|
|
1612
|
+
content += `<img src="${imageUrl}"/>`
|
|
1613
|
+
}
|
|
1614
|
+
break
|
|
1615
|
+
case 'mface':
|
|
1616
|
+
const mfaceUrl = element.attrs.src || element.attrs.url || element.attrs.file
|
|
1617
|
+
if (mfaceUrl) {
|
|
1618
|
+
content += `<img src="${mfaceUrl}"/>`
|
|
1619
|
+
}
|
|
1620
|
+
break
|
|
1621
|
+
case 'face':
|
|
1622
|
+
if (element.children && element.children[0]?.attrs?.src) {
|
|
1623
|
+
const faceUrl = element.children[0].attrs.src
|
|
1624
|
+
content += `<img src="${faceUrl}"/>`
|
|
1625
|
+
} else {
|
|
1626
|
+
content += `[${element.attrs.name || element.attrs.id}]`
|
|
1627
|
+
}
|
|
1628
|
+
break
|
|
1629
|
+
case 'at':
|
|
1630
|
+
// 对于@消息,保持原始格式
|
|
1631
|
+
content += `<at id="${element.attrs.id}" name="${element.attrs.name}"/>`
|
|
1632
|
+
break
|
|
1633
|
+
case 'text':
|
|
1634
|
+
// 对于文本元素,直接添加文本内容
|
|
1635
|
+
content += element.attrs.content || ''
|
|
1636
|
+
break
|
|
1637
|
+
default:
|
|
1638
|
+
// 其他类型保持原始内容
|
|
1639
|
+
content += message.content
|
|
1640
|
+
break
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
} else {
|
|
1644
|
+
// 如果没有elements,使用原始内容
|
|
1645
|
+
content = message.content
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
await navigator.clipboard.writeText(content)
|
|
1649
|
+
showNotification('已复制到剪贴板', 'success')
|
|
1650
|
+
} catch (error: any) {
|
|
1651
|
+
console.error('复制失败:', error)
|
|
1652
|
+
showNotification('复制失败: ' + (error?.message || String(error)), 'error')
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function handleReplyMessage(message: MessageInfo | undefined) {
|
|
1657
|
+
if (!message) return;
|
|
1658
|
+
hideContextMenu()
|
|
1659
|
+
|
|
1660
|
+
try {
|
|
1661
|
+
// 在输入框中添加引用
|
|
1662
|
+
const quote = `<quote id="${(message.id).replace("bot-msg-", "")}"/>`
|
|
1663
|
+
inputMessage.value = quote + inputMessage.value
|
|
1664
|
+
|
|
1665
|
+
// 聚焦输入框
|
|
1666
|
+
nextTick(() => {
|
|
1667
|
+
if (messageInput.value) {
|
|
1668
|
+
messageInput.value.focus()
|
|
1669
|
+
}
|
|
1670
|
+
})
|
|
1671
|
+
} catch (error: any) {
|
|
1672
|
+
console.error('回复操作失败:', error)
|
|
1673
|
+
showNotification('操作失败: ' + (error?.message || String(error)), 'error')
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1347
1677
|
function getChannelTypeText(type: number | string): string {
|
|
1348
1678
|
if (typeof type === 'number') {
|
|
1349
1679
|
switch (type) {
|
|
@@ -1380,7 +1710,7 @@ export function useChatLogic() {
|
|
|
1380
1710
|
// 滚动到底部时,重置滚动状态
|
|
1381
1711
|
isUserScrolling.value = false
|
|
1382
1712
|
}
|
|
1383
|
-
|
|
1713
|
+
|
|
1384
1714
|
// 检查是否滚动到顶部,如果是则加载更多消息
|
|
1385
1715
|
if (scrollTop <= 10 && selectedBot.value && selectedChannel.value) {
|
|
1386
1716
|
loadMoreMessages()
|
|
@@ -1391,13 +1721,13 @@ export function useChatLogic() {
|
|
|
1391
1721
|
// 加载更多消息的函数
|
|
1392
1722
|
async function loadMoreMessages() {
|
|
1393
1723
|
if (!selectedBot.value || !selectedChannel.value) return
|
|
1394
|
-
|
|
1724
|
+
|
|
1395
1725
|
const channelKey = `${selectedBot.value}:${selectedChannel.value}`
|
|
1396
|
-
|
|
1726
|
+
|
|
1397
1727
|
// 检查是否正在加载或没有更多消息
|
|
1398
1728
|
const pagination = channelPagination.value[channelKey]
|
|
1399
1729
|
if (isLoadingMore.value || (pagination && !pagination.hasMore)) return
|
|
1400
|
-
|
|
1730
|
+
|
|
1401
1731
|
// 更新加载状态
|
|
1402
1732
|
if (!channelPagination.value[channelKey]) {
|
|
1403
1733
|
channelPagination.value[channelKey] = {
|
|
@@ -1406,18 +1736,18 @@ export function useChatLogic() {
|
|
|
1406
1736
|
loading: false
|
|
1407
1737
|
}
|
|
1408
1738
|
}
|
|
1409
|
-
|
|
1739
|
+
|
|
1410
1740
|
// 设置为加载中状态
|
|
1411
1741
|
isLoadingMore.value = true
|
|
1412
1742
|
channelPagination.value[channelKey].loading = true
|
|
1413
|
-
|
|
1743
|
+
|
|
1414
1744
|
try {
|
|
1415
1745
|
// 获取当前offset
|
|
1416
1746
|
const currentOffset = pagination?.offset || 0
|
|
1417
|
-
|
|
1747
|
+
|
|
1418
1748
|
// 加载下一批消息(50条)
|
|
1419
1749
|
const result = await loadHistoryMessages(selectedBot.value, selectedChannel.value, 50, currentOffset)
|
|
1420
|
-
|
|
1750
|
+
|
|
1421
1751
|
if (result) {
|
|
1422
1752
|
// 更新分页状态已在loadHistoryMessages中处理
|
|
1423
1753
|
} else {
|
|
@@ -2669,64 +2999,83 @@ export function useChatLogic() {
|
|
|
2669
2999
|
chatData.value.messages[channelKey] = []
|
|
2670
3000
|
}
|
|
2671
3001
|
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
3002
|
+
const messages = chatData.value.messages[channelKey];
|
|
3003
|
+
|
|
3004
|
+
// 查找是否有使用临时ID的消息(以bot-msg-开头)
|
|
3005
|
+
const tempMessageIndex = messages.findIndex(m => m.id.startsWith('bot-msg-') && Math.abs(m.timestamp - sentEvent.timestamp) < 5000);
|
|
3006
|
+
|
|
3007
|
+
if (tempMessageIndex !== -1) {
|
|
3008
|
+
// 找到了临时消息,更新它的ID和其他信息
|
|
3009
|
+
const tempMessage = messages[tempMessageIndex];
|
|
3010
|
+
tempMessage.id = sentEvent.messageId; // 更新为真实ID
|
|
3011
|
+
tempMessage.content = sentEvent.content;
|
|
3012
|
+
tempMessage.userId = sentEvent.selfId;
|
|
3013
|
+
tempMessage.username = sentEvent.botUsername;
|
|
3014
|
+
tempMessage.avatar = sentEvent.botAvatar;
|
|
3015
|
+
tempMessage.channelId = sentEvent.channelId;
|
|
3016
|
+
tempMessage.selfId = sentEvent.selfId;
|
|
3017
|
+
tempMessage.elements = sentEvent.elements;
|
|
3018
|
+
tempMessage.isBot = true;
|
|
3019
|
+
tempMessage.quote = sentEvent.quote;
|
|
3020
|
+
} else {
|
|
3021
|
+
// 没有找到临时消息,检查是否已存在真实ID的消息
|
|
3022
|
+
const exists = messages.find(m => m.id === sentEvent.messageId);
|
|
3023
|
+
if (!exists) {
|
|
3024
|
+
const botMessage: MessageInfo = {
|
|
3025
|
+
id: sentEvent.messageId,
|
|
3026
|
+
content: sentEvent.content,
|
|
3027
|
+
userId: sentEvent.selfId,
|
|
3028
|
+
username: sentEvent.botUsername,
|
|
3029
|
+
avatar: sentEvent.botAvatar,
|
|
3030
|
+
timestamp: sentEvent.timestamp,
|
|
3031
|
+
channelId: sentEvent.channelId,
|
|
3032
|
+
selfId: sentEvent.selfId,
|
|
3033
|
+
elements: sentEvent.elements,
|
|
3034
|
+
isBot: true, // 标记为机器人发送的消息
|
|
3035
|
+
quote: sentEvent.quote
|
|
3036
|
+
};
|
|
2688
3037
|
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
let insertIndex = messages.length
|
|
3038
|
+
// 按时间戳排序插入消息
|
|
3039
|
+
let insertIndex = messages.length;
|
|
2692
3040
|
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
3041
|
+
// 找到正确的插入位置(按时间戳排序)
|
|
3042
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3043
|
+
if (messages[i].timestamp <= sentEvent.timestamp) {
|
|
3044
|
+
insertIndex = i + 1;
|
|
3045
|
+
break;
|
|
3046
|
+
}
|
|
3047
|
+
if (i === 0) {
|
|
3048
|
+
insertIndex = 0;
|
|
3049
|
+
}
|
|
2701
3050
|
}
|
|
2702
|
-
}
|
|
2703
3051
|
|
|
2704
|
-
|
|
3052
|
+
messages.splice(insertIndex, 0, botMessage);
|
|
2705
3053
|
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
3054
|
+
// 保持消息数量限制
|
|
3055
|
+
if (messages.length > 100) {
|
|
3056
|
+
chatData.value.messages[channelKey] = messages.slice(-100);
|
|
3057
|
+
}
|
|
2710
3058
|
|
|
2711
|
-
|
|
2712
|
-
|
|
3059
|
+
// 更新频道消息数量缓存
|
|
3060
|
+
channelMessageCounts.value[channelKey] = messages.length;
|
|
2713
3061
|
|
|
2714
|
-
|
|
2715
|
-
|
|
3062
|
+
// 在添加新消息前检查是否在底部附近
|
|
3063
|
+
const wasNearBottom = isNearBottom();
|
|
2716
3064
|
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
3065
|
+
// 智能滚动:基于添加消息前的位置状态来决定是否滚动
|
|
3066
|
+
nextTick(() => {
|
|
3067
|
+
// 再次等待,确保新消息的DOM已经渲染
|
|
3068
|
+
setTimeout(() => {
|
|
3069
|
+
if (wasNearBottom) {
|
|
3070
|
+
scrollToBottom();
|
|
3071
|
+
}
|
|
3072
|
+
}, 10);
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
2726
3075
|
}
|
|
2727
3076
|
|
|
2728
3077
|
// 触发响应式更新
|
|
2729
|
-
chatData.value = { ...chatData.value }
|
|
3078
|
+
chatData.value = { ...chatData.value };
|
|
2730
3079
|
}
|
|
2731
3080
|
|
|
2732
3081
|
// 处理机器人消息事件
|
|
@@ -2957,7 +3306,7 @@ export function useChatLogic() {
|
|
|
2957
3306
|
selfId: botId,
|
|
2958
3307
|
channelId: channelId
|
|
2959
3308
|
}
|
|
2960
|
-
|
|
3309
|
+
|
|
2961
3310
|
// 如果提供了分页参数,则添加到请求中
|
|
2962
3311
|
if (limit !== undefined) {
|
|
2963
3312
|
requestData.limit = limit
|
|
@@ -2991,7 +3340,7 @@ export function useChatLogic() {
|
|
|
2991
3340
|
if (limit !== undefined) {
|
|
2992
3341
|
// 按时间戳排序
|
|
2993
3342
|
messages.sort((a, b) => a.timestamp - b.timestamp)
|
|
2994
|
-
|
|
3343
|
+
|
|
2995
3344
|
// 如果是第一页(offset为0),则替换现有消息
|
|
2996
3345
|
if (offset === 0) {
|
|
2997
3346
|
chatData.value.messages[channelKey] = messages
|
|
@@ -3016,7 +3365,7 @@ export function useChatLogic() {
|
|
|
3016
3365
|
} else {
|
|
3017
3366
|
// 按时间戳排序
|
|
3018
3367
|
messages.sort((a, b) => a.timestamp - b.timestamp)
|
|
3019
|
-
|
|
3368
|
+
|
|
3020
3369
|
// 设置历史消息
|
|
3021
3370
|
chatData.value.messages[channelKey] = messages
|
|
3022
3371
|
// 初始化分页状态
|
|
@@ -3199,6 +3548,20 @@ export function useChatLogic() {
|
|
|
3199
3548
|
// 添加点击外部关闭菜单的监听器
|
|
3200
3549
|
document.addEventListener('click', handleClickOutside)
|
|
3201
3550
|
|
|
3551
|
+
// 首先加载插件配置
|
|
3552
|
+
await loadPluginConfig()
|
|
3553
|
+
|
|
3554
|
+
// 检查是否需要清空 IndexedDB
|
|
3555
|
+
if (pluginConfig.value.clearIndexedDBOnStart) {
|
|
3556
|
+
console.log('启动时清空 IndexedDB 缓存...')
|
|
3557
|
+
const clearResult = await clearAllIndexedDBData()
|
|
3558
|
+
if (clearResult) {
|
|
3559
|
+
console.log('IndexedDB 缓存已清空')
|
|
3560
|
+
} else {
|
|
3561
|
+
console.warn('清空 IndexedDB 缓存失败')
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3202
3565
|
// 初始化IndexedDB
|
|
3203
3566
|
const dbInitialized = await initImageDB()
|
|
3204
3567
|
if (!dbInitialized) {
|
|
@@ -3217,9 +3580,6 @@ export function useChatLogic() {
|
|
|
3217
3580
|
}, 5 * 60 * 1000)
|
|
3218
3581
|
}
|
|
3219
3582
|
|
|
3220
|
-
// 首先加载插件配置
|
|
3221
|
-
await loadPluginConfig()
|
|
3222
|
-
|
|
3223
3583
|
// 然后加载历史数据
|
|
3224
3584
|
await loadChatData()
|
|
3225
3585
|
|
|
@@ -3307,7 +3667,7 @@ export function useChatLogic() {
|
|
|
3307
3667
|
// 响应式数据
|
|
3308
3668
|
chatData,
|
|
3309
3669
|
channelMessageCounts,
|
|
3310
|
-
channelPagination,
|
|
3670
|
+
channelPagination,
|
|
3311
3671
|
pluginConfig,
|
|
3312
3672
|
selectedBot,
|
|
3313
3673
|
selectedChannel,
|
|
@@ -3357,6 +3717,7 @@ export function useChatLogic() {
|
|
|
3357
3717
|
selectChannel,
|
|
3358
3718
|
handleBotRightClick,
|
|
3359
3719
|
handleChannelRightClick,
|
|
3720
|
+
handleMessageRightClick,
|
|
3360
3721
|
showContextMenu,
|
|
3361
3722
|
hideContextMenu,
|
|
3362
3723
|
handleKeyDown,
|
|
@@ -3394,11 +3755,22 @@ export function useChatLogic() {
|
|
|
3394
3755
|
handleTouchEnd,
|
|
3395
3756
|
handleInputFocus,
|
|
3396
3757
|
loadMoreMessages,
|
|
3758
|
+
handlePlusOne,
|
|
3759
|
+
handleCopyMessage,
|
|
3760
|
+
handleReplyMessage,
|
|
3761
|
+
parseInlineQuote,
|
|
3762
|
+
getInlineQuoteMessage,
|
|
3763
|
+
getQuoteUser,
|
|
3764
|
+
getQuoteTimestamp,
|
|
3765
|
+
getQuoteContent,
|
|
3766
|
+
getQuoteElements,
|
|
3767
|
+
getMessageContentWithoutQuote,
|
|
3397
3768
|
|
|
3398
3769
|
// 图片缓存相关
|
|
3399
3770
|
getCachedImageUrl,
|
|
3400
3771
|
cacheImage,
|
|
3401
3772
|
clearChannelImageCache,
|
|
3773
|
+
clearAllIndexedDBData,
|
|
3402
3774
|
getMemoryStats,
|
|
3403
3775
|
getCacheStats,
|
|
3404
3776
|
|
|
@@ -3406,6 +3778,7 @@ export function useChatLogic() {
|
|
|
3406
3778
|
isFileUrl,
|
|
3407
3779
|
loadHistoryMessages,
|
|
3408
3780
|
handleMessageEvent,
|
|
3781
|
+
handleBotMessageSentEvent,
|
|
3409
3782
|
saveSelectionState,
|
|
3410
3783
|
restoreSelectionState
|
|
3411
3784
|
}
|