koishi-plugin-chat-patch 1.3.0 → 2.0.1

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.
@@ -0,0 +1,61 @@
1
+ import { ref } from 'vue'
2
+ import { send } from '@koishijs/client'
3
+ import type { MessageInfo } from '../types'
4
+
5
+ export function useChatActions() {
6
+ const isSending = ref(false)
7
+
8
+ async function sendMessage(botId: string, channelId: string, content: string, images: any[]) {
9
+ if (isSending.value) return
10
+ isSending.value = true
11
+ try {
12
+ const result = await (send as any)('send-message', {
13
+ selfId: botId,
14
+ channelId: channelId,
15
+ content: content,
16
+ images: images.map(img => ({
17
+ tempId: img.tempId,
18
+ filename: img.filename
19
+ }))
20
+ })
21
+ return result
22
+ } finally {
23
+ isSending.value = false
24
+ }
25
+ }
26
+
27
+ async function deleteBotData(botId: string) {
28
+ return await (send as any)('delete-bot-data', { selfId: botId })
29
+ }
30
+
31
+ async function deleteChannelData(botId: string, channelId: string) {
32
+ return await (send as any)('delete-channel-data', { selfId: botId, channelId: channelId })
33
+ }
34
+
35
+ async function clearHistory(botId: string, channelId: string) {
36
+ return await (send as any)('clear-channel-history', { selfId: botId, channelId: channelId })
37
+ }
38
+
39
+ async function togglePinBot(botId: string, pinned: boolean, pinnedBots: Set<string>) {
40
+ if (pinned) pinnedBots.delete(botId)
41
+ else pinnedBots.add(botId)
42
+ await (send as any)('set-pinned-bots', { pinnedBots: Array.from(pinnedBots) })
43
+ }
44
+
45
+ async function togglePinChannel(botId: string, channelId: string, pinned: boolean, pinnedChannels: Set<string>) {
46
+ const key = `${botId}:${channelId}`
47
+ if (pinned) pinnedChannels.delete(key)
48
+ else pinnedChannels.add(key)
49
+ await (send as any)('set-pinned-channels', { pinnedChannels: Array.from(pinnedChannels) })
50
+ }
51
+
52
+ return {
53
+ isSending,
54
+ sendMessage,
55
+ deleteBotData,
56
+ deleteChannelData,
57
+ clearHistory,
58
+ togglePinBot,
59
+ togglePinChannel
60
+ }
61
+ }
@@ -0,0 +1,184 @@
1
+ import { ref, computed } from 'vue'
2
+ import { receive, send } from '@koishijs/client'
3
+ import type { ChatData, BotInfo, ChannelInfo, MessageInfo, PluginConfig } from '../types'
4
+
5
+ export function useChatData() {
6
+ const chatData = ref<ChatData>({
7
+ bots: {},
8
+ channels: {},
9
+ messages: {}
10
+ })
11
+
12
+ const pluginConfig = ref<PluginConfig>({
13
+ maxMessagesPerChannel: 1000,
14
+ keepMessagesOnClear: 50,
15
+ loggerinfo: false,
16
+ blockedPlatforms: [],
17
+ chatContainerHeight: 80,
18
+ clearIndexedDBOnStart: true
19
+ })
20
+
21
+ const pinnedBots = ref<Set<string>>(new Set())
22
+ const pinnedChannels = ref<Set<string>>(new Set())
23
+
24
+ // 记录每个频道的分页状态
25
+ const channelPagination = ref<Record<string, { offset: number, hasMore: boolean }>>({})
26
+
27
+ // 计算属性
28
+ const bots = computed(() => {
29
+ return Object.values(chatData.value.bots).sort((a, b) => {
30
+ const aPinned = pinnedBots.value.has(a.selfId)
31
+ const bPinned = pinnedBots.value.has(b.selfId)
32
+ return aPinned === bPinned ? 0 : aPinned ? -1 : 1
33
+ })
34
+ })
35
+
36
+ const getChannels = (botId: string) => {
37
+ if (!botId || !chatData.value.channels[botId]) return []
38
+ return Object.values(chatData.value.channels[botId]).sort((a, b) => {
39
+ const aPinned = pinnedChannels.value.has(`${botId}:${a.id}`)
40
+ const bPinned = pinnedChannels.value.has(`${botId}:${b.id}`)
41
+ return aPinned === bPinned ? 0 : aPinned ? -1 : 1
42
+ })
43
+ }
44
+
45
+ const getMessages = (botId: string, channelId: string) => {
46
+ const key = `${botId}:${channelId}`
47
+ return chatData.value.messages[key] || []
48
+ }
49
+
50
+ const getPagination = (botId: string, channelId: string) => {
51
+ const key = `${botId}:${channelId}`
52
+ return channelPagination.value[key] || { offset: 0, hasMore: true }
53
+ }
54
+
55
+ // 加载数据
56
+ async function loadInitialData() {
57
+ const result = await (send as any)('get-chat-data')
58
+ if (result.success && result.data) {
59
+ chatData.value = {
60
+ bots: result.data.bots || {},
61
+ channels: result.data.channels || {},
62
+ messages: result.data.messages || {}
63
+ }
64
+ pinnedBots.value = new Set(result.data.pinnedBots || [])
65
+ pinnedChannels.value = new Set(result.data.pinnedChannels || [])
66
+ }
67
+ }
68
+
69
+ async function loadConfig() {
70
+ const result = await (send as any)('get-plugin-config')
71
+ if (result.success && result.config) {
72
+ pluginConfig.value = result.config
73
+ }
74
+ }
75
+
76
+ // 消息处理
77
+ function addMessage(msg: any) {
78
+ const selfId = msg.selfId
79
+ const channelId = msg.channelId
80
+ const key = `${selfId}:${channelId}`
81
+
82
+ // 1. 确保机器人存在
83
+ if (!chatData.value.bots[selfId]) {
84
+ chatData.value.bots[selfId] = {
85
+ selfId,
86
+ platform: msg.platform || 'unknown',
87
+ username: msg.bot?.name || `Bot-${selfId}`,
88
+ avatar: msg.bot?.avatar,
89
+ status: 'online'
90
+ }
91
+ }
92
+
93
+ // 2. 确保频道存在
94
+ if (!chatData.value.channels[selfId]) {
95
+ chatData.value.channels[selfId] = {}
96
+ }
97
+ if (!chatData.value.channels[selfId][channelId]) {
98
+ chatData.value.channels[selfId][channelId] = {
99
+ id: channelId,
100
+ name: msg.guildName || msg.username || channelId,
101
+ type: msg.channelType || 0
102
+ }
103
+ }
104
+
105
+ // 3. 消息去重与添加
106
+ if (!chatData.value.messages[key]) chatData.value.messages[key] = []
107
+
108
+ const messages = chatData.value.messages[key]
109
+ // 严格去重:检查 ID,如果是机器人发送的临时消息,则通过内容和时间戳近似匹配
110
+ const isDuplicate = messages.some(m => {
111
+ if (m.id === msg.messageId || m.id === msg.id) return true
112
+ // 针对机器人发送消息的特殊去重逻辑
113
+ if ((msg.type === 'bot-message' || msg.type === 'bot-message-sent' || msg.type === 'bot') && m.isBot && Math.abs(m.timestamp - msg.timestamp) < 2000 && m.content === msg.content) return true
114
+ return false
115
+ })
116
+
117
+ if (!isDuplicate) {
118
+ const newMsg: MessageInfo = {
119
+ id: msg.messageId || msg.id,
120
+ content: msg.content,
121
+ userId: msg.userId,
122
+ username: msg.username,
123
+ avatar: msg.avatar,
124
+ timestamp: msg.timestamp,
125
+ channelId: channelId,
126
+ selfId: selfId,
127
+ elements: msg.elements,
128
+ isBot: msg.isBot || msg.type === 'bot-message' || msg.type === 'bot',
129
+ quote: msg.quote
130
+ }
131
+ messages.push(newMsg)
132
+ messages.sort((a, b) => a.timestamp - b.timestamp)
133
+ }
134
+
135
+ // 强制触发响应式更新,确保列表实时刷新
136
+ chatData.value = { ...chatData.value }
137
+ }
138
+
139
+ async function loadHistory(botId: string, channelId: string, limit = 50) {
140
+ const key = `${botId}:${channelId}`
141
+ const pagination = getPagination(botId, channelId)
142
+ if (!pagination.hasMore) return
143
+
144
+ const result = await (send as any)('get-history-messages', {
145
+ selfId: botId,
146
+ channelId: channelId,
147
+ limit,
148
+ offset: pagination.offset
149
+ })
150
+
151
+ if (result.success && result.messages) {
152
+ if (!chatData.value.messages[key]) chatData.value.messages[key] = []
153
+ const existing = chatData.value.messages[key]
154
+
155
+ // 合并并去重
156
+ const newMsgs = result.messages.filter((m: any) => !existing.find(e => e.id === m.id))
157
+ chatData.value.messages[key] = [...newMsgs, ...existing].sort((a, b) => a.timestamp - b.timestamp)
158
+
159
+ channelPagination.value[key] = {
160
+ offset: pagination.offset + result.messages.length,
161
+ hasMore: result.messages.length >= limit && result.total > (pagination.offset + result.messages.length)
162
+ }
163
+
164
+ chatData.value = { ...chatData.value }
165
+ return result.messages.length
166
+ }
167
+ return 0
168
+ }
169
+
170
+ return {
171
+ chatData,
172
+ pluginConfig,
173
+ pinnedBots,
174
+ pinnedChannels,
175
+ bots,
176
+ getChannels,
177
+ getMessages,
178
+ getPagination,
179
+ loadInitialData,
180
+ loadConfig,
181
+ addMessage,
182
+ loadHistory
183
+ }
184
+ }
@@ -0,0 +1,140 @@
1
+ import { ref, onMounted, onUnmounted } from 'vue'
2
+ import { send } from '@koishijs/client'
3
+
4
+ // 图片缓存 - IndexedDB
5
+ interface ImageCacheItem {
6
+ url: string
7
+ blob: Blob
8
+ timestamp: number
9
+ size: number
10
+ channelKey: string
11
+ }
12
+
13
+ export function useImageCache() {
14
+ const imageBlobUrls = ref<Record<string, string>>({})
15
+ const loadingImages = new Map<string, Promise<string | null>>()
16
+
17
+ // 内存管理配置
18
+ const MAX_MEMORY_USAGE = 100 * 1024 * 1024 // 100MB
19
+ const MAX_BLOB_COUNT = 50
20
+ let currentMemoryUsage = 0
21
+
22
+ // IndexedDB 配置
23
+ let imageDB: IDBDatabase | null = null
24
+ const DB_NAME = 'ChatImageCache'
25
+ const DB_VERSION = 2
26
+ const STORE_NAME = 'images'
27
+ const MAX_DB_SIZE = 50 * 1024 * 1024 // 50MB
28
+ const MAX_TOTAL_IMAGES = 500
29
+ const MAX_IMAGE_SIZE = 12 * 1024 * 1024 // 12MB
30
+
31
+ // 初始化数据库
32
+ async function initDB(): Promise<boolean> {
33
+ return new Promise((resolve) => {
34
+ const request = indexedDB.open(DB_NAME, DB_VERSION)
35
+ request.onupgradeneeded = (event) => {
36
+ const db = (event.target as IDBOpenDBRequest).result
37
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
38
+ const store = db.createObjectStore(STORE_NAME, { keyPath: 'url' })
39
+ store.createIndex('channelKey', 'channelKey', { unique: false })
40
+ store.createIndex('timestamp', 'timestamp', { unique: false })
41
+ }
42
+ }
43
+ request.onsuccess = () => {
44
+ imageDB = request.result
45
+ resolve(true)
46
+ }
47
+ request.onerror = () => resolve(false)
48
+ })
49
+ }
50
+
51
+ // 获取缓存图片
52
+ async function getCachedImageUrl(channelKey: string, url: string): Promise<string | null> {
53
+ if (imageBlobUrls.value[url]) return imageBlobUrls.value[url]
54
+ if (loadingImages.has(url)) return loadingImages.get(url)!
55
+
56
+ const promise = (async () => {
57
+ try {
58
+ if (!imageDB) await initDB()
59
+ const item = await getImageFromDB(url)
60
+ if (item) {
61
+ const blobUrl = URL.createObjectURL(item.blob)
62
+ imageBlobUrls.value[url] = blobUrl
63
+ return blobUrl
64
+ }
65
+ return null
66
+ } finally {
67
+ loadingImages.delete(url)
68
+ }
69
+ })()
70
+
71
+ loadingImages.set(url, promise)
72
+ return promise
73
+ }
74
+
75
+ async function getImageFromDB(url: string): Promise<ImageCacheItem | null> {
76
+ if (!imageDB) return null
77
+ return new Promise((resolve) => {
78
+ const transaction = imageDB!.transaction([STORE_NAME], 'readonly')
79
+ const store = transaction.objectStore(STORE_NAME)
80
+ const request = store.get(url)
81
+ request.onsuccess = () => resolve(request.result || null)
82
+ request.onerror = () => resolve(null)
83
+ })
84
+ }
85
+
86
+ async function cacheImage(channelKey: string, url: string): Promise<string | null> {
87
+ const existing = await getCachedImageUrl(channelKey, url)
88
+ if (existing) return existing
89
+
90
+ try {
91
+ const result = await (send as any)('fetch-image', { url })
92
+ if (!result.success) return null
93
+
94
+ const response = await fetch(result.dataUrl)
95
+ const blob = await response.blob()
96
+
97
+ if (blob.size > MAX_IMAGE_SIZE) return null
98
+
99
+ const item: ImageCacheItem = {
100
+ url,
101
+ blob,
102
+ timestamp: Date.now(),
103
+ size: blob.size,
104
+ channelKey
105
+ }
106
+
107
+ await saveToDB(item)
108
+ const blobUrl = URL.createObjectURL(blob)
109
+ imageBlobUrls.value[url] = blobUrl
110
+ return blobUrl
111
+ } catch (e) {
112
+ return null
113
+ }
114
+ }
115
+
116
+ async function saveToDB(item: ImageCacheItem) {
117
+ if (!imageDB) return
118
+ const transaction = imageDB.transaction([STORE_NAME], 'readwrite')
119
+ transaction.objectStore(STORE_NAME).put(item)
120
+ }
121
+
122
+ async function clearChannelCache(channelKey: string) {
123
+ // 简化实现
124
+ Object.keys(imageBlobUrls.value).forEach(url => {
125
+ URL.revokeObjectURL(imageBlobUrls.value[url])
126
+ delete imageBlobUrls.value[url]
127
+ })
128
+ }
129
+
130
+ onUnmounted(() => {
131
+ Object.values(imageBlobUrls.value).forEach(URL.revokeObjectURL)
132
+ if (imageDB) imageDB.close()
133
+ })
134
+
135
+ return {
136
+ getCachedImageUrl,
137
+ cacheImage,
138
+ clearChannelCache
139
+ }
140
+ }