koishi-plugin-chat-patch 3.0.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/client/vue/index.vue +60 -634
  2. package/client/web/env.d.ts +56 -0
  3. package/client/web/index.html +21 -0
  4. package/client/web/package-lock.json +3419 -0
  5. package/client/web/package.json +62 -0
  6. package/client/web/public/bcui/css/color-dark.css +35 -0
  7. package/client/web/public/bcui/css/color-light.css +35 -0
  8. package/client/web/public/bcui/css/style.css +468 -0
  9. package/client/web/public/bcui/js/main.js +193 -0
  10. package/client/web/public/css/append-dark.css +13 -0
  11. package/client/web/public/css/append-light.css +13 -0
  12. package/client/web/public/css/append-napcat-dark.css +14 -0
  13. package/client/web/public/css/append-napcat-light.css +14 -0
  14. package/client/web/public/favicon.ico +0 -0
  15. package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
  16. package/client/web/public/img/icons/icon-maskable.png +0 -0
  17. package/client/web/public/img/icons/icon.png +0 -0
  18. package/client/web/public/img/icons/icon.svg +10 -0
  19. package/client/web/public/robots.txt +2 -0
  20. package/client/web/public/sw.js +58 -0
  21. package/client/web/scripts/copy-bcui.mjs +17 -0
  22. package/client/web/src/App.vue +1127 -0
  23. package/client/web/src/assets/css/append/append_darwin.css +68 -0
  24. package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
  25. package/client/web/src/assets/css/append/append_linux.css +125 -0
  26. package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
  27. package/client/web/src/assets/css/append/append_napcat.css +35 -0
  28. package/client/web/src/assets/css/append/append_new.css +843 -0
  29. package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
  30. package/client/web/src/assets/css/append/append_win32.css +109 -0
  31. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
  32. package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
  33. package/client/web/src/assets/css/chat.css +2104 -0
  34. package/client/web/src/assets/css/msg.css +941 -0
  35. package/client/web/src/assets/css/options.css +811 -0
  36. package/client/web/src/assets/css/sys_notice.css +62 -0
  37. package/client/web/src/assets/css/view.css +2105 -0
  38. package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
  39. package/client/web/src/assets/img/Vue.png +0 -0
  40. package/client/web/src/assets/img/cakes.svg +189 -0
  41. package/client/web/src/assets/img/icons/AppIcon.png +0 -0
  42. package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
  43. package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
  44. package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
  45. package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
  46. package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
  47. package/client/web/src/assets/img/icons/exit.png +0 -0
  48. package/client/web/src/assets/img/icons/pen.png +0 -0
  49. package/client/web/src/assets/img/stars.svg +89 -0
  50. package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
  51. package/client/web/src/assets/l10n/_About I10n.md +35 -0
  52. package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
  53. package/client/web/src/assets/l10n/zh-CN.po +1941 -0
  54. package/client/web/src/assets/logo.png +0 -0
  55. package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
  56. package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
  57. package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
  58. package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
  59. package/client/web/src/assets/qed.txt +1 -0
  60. package/client/web/src/components/AboutPan.vue +197 -0
  61. package/client/web/src/components/BulletinBody.vue +129 -0
  62. package/client/web/src/components/DepPan.vue +310 -0
  63. package/client/web/src/components/EmojiFace.vue +32 -0
  64. package/client/web/src/components/FacePan.vue +461 -0
  65. package/client/web/src/components/FileBody.vue +129 -0
  66. package/client/web/src/components/FileManager.vue +532 -0
  67. package/client/web/src/components/FriendBody.vue +54 -0
  68. package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
  69. package/client/web/src/components/History.vue +49 -0
  70. package/client/web/src/components/LazyLottie.vue +82 -0
  71. package/client/web/src/components/MergePan.vue +127 -0
  72. package/client/web/src/components/MsgBody.vue +1478 -0
  73. package/client/web/src/components/MusicPlayer.vue +549 -0
  74. package/client/web/src/components/NoticeBody.vue +168 -0
  75. package/client/web/src/components/QzonePermissionPan.vue +150 -0
  76. package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
  77. package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
  78. package/client/web/src/components/TinySessionBody.vue +51 -0
  79. package/client/web/src/components/UmamiInfoPan.vue +1549 -0
  80. package/client/web/src/components/UpdatePan.vue +263 -0
  81. package/client/web/src/components/ViewerCom.vue +1360 -0
  82. package/client/web/src/components/VoiceMsg.vue +626 -0
  83. package/client/web/src/components/WelPan.vue +894 -0
  84. package/client/web/src/components/msg-body/.gitkeep +0 -0
  85. package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
  86. package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
  87. package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
  88. package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
  89. package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
  90. package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
  91. package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
  92. package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
  93. package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
  94. package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
  95. package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
  96. package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
  97. package/client/web/src/components/notice-component/.gitkeep +0 -0
  98. package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
  99. package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
  100. package/client/web/src/components/tooltip/Tooltip.vue +50 -0
  101. package/client/web/src/components/tooltip/Tooltips.vue +14 -0
  102. package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
  103. package/client/web/src/function/base.ts +238 -0
  104. package/client/web/src/function/connect.ts +2042 -0
  105. package/client/web/src/function/debug.ts +10 -0
  106. package/client/web/src/function/elements/information.ts +141 -0
  107. package/client/web/src/function/elements/system.ts +101 -0
  108. package/client/web/src/function/elements/vueComp.ts +62 -0
  109. package/client/web/src/function/favicon.ts +76 -0
  110. package/client/web/src/function/input.ts +69 -0
  111. package/client/web/src/function/model/emoji.ts +127 -0
  112. package/client/web/src/function/model/img.ts +150 -0
  113. package/client/web/src/function/model/msg-body.ts +51 -0
  114. package/client/web/src/function/msg.ts +2674 -0
  115. package/client/web/src/function/notify.ts +225 -0
  116. package/client/web/src/function/option.ts +749 -0
  117. package/client/web/src/function/satori-model.ts +818 -0
  118. package/client/web/src/function/satori.ts +325 -0
  119. package/client/web/src/function/sender.ts +178 -0
  120. package/client/web/src/function/tooltip.ts +49 -0
  121. package/client/web/src/function/utils/appUtil.ts +2288 -0
  122. package/client/web/src/function/utils/avatarUtil.ts +31 -0
  123. package/client/web/src/function/utils/backgroundUtil.ts +110 -0
  124. package/client/web/src/function/utils/linkViewUtil.ts +101 -0
  125. package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
  126. package/client/web/src/function/utils/msgUtil.ts +1242 -0
  127. package/client/web/src/function/utils/pinyin.ts +141 -0
  128. package/client/web/src/function/utils/recordUtil.ts +187 -0
  129. package/client/web/src/function/utils/sessionUtil.ts +114 -0
  130. package/client/web/src/function/utils/systemUtil.ts +664 -0
  131. package/client/web/src/main.ts +91 -0
  132. package/client/web/src/pages/Chat.vue +3453 -0
  133. package/client/web/src/pages/Friends.vue +636 -0
  134. package/client/web/src/pages/Info.vue +717 -0
  135. package/client/web/src/pages/Messages.vue +875 -0
  136. package/client/web/src/pages/Options.vue +82 -0
  137. package/client/web/src/pages/Qzone.vue +1178 -0
  138. package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
  139. package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
  140. package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
  141. package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
  142. package/client/web/src/pages/options/OptAccount.vue +107 -0
  143. package/client/web/src/pages/options/OptAddon.vue +185 -0
  144. package/client/web/src/pages/options/OptDev.vue +704 -0
  145. package/client/web/src/pages/options/OptFunction.vue +403 -0
  146. package/client/web/src/pages/options/OptInfo.vue +165 -0
  147. package/client/web/src/pages/options/OptView.vue +846 -0
  148. package/client/web/src/runtime/backend.ts +310 -0
  149. package/client/web/src/state/auth.ts +14 -0
  150. package/client/web/src/state/chat.ts +36 -0
  151. package/client/web/src/state/connection.ts +20 -0
  152. package/client/web/src/state/contact.ts +37 -0
  153. package/client/web/src/state/qzone.ts +18 -0
  154. package/client/web/src/state/sessionHistory.ts +25 -0
  155. package/client/web/src/state/settings.ts +18 -0
  156. package/client/web/src/state/sticker.ts +10 -0
  157. package/client/web/src/state/ui.ts +56 -0
  158. package/client/web/tsconfig.json +27 -0
  159. package/client/web/tsconfig.tsbuildinfo +1 -0
  160. package/client/web/vite.config.ts +33 -0
  161. package/dist/index.js +1 -12
  162. package/dist/style.css +1 -1
  163. package/lib/index.js +2070 -2077
  164. package/package.json +15 -2
  165. package/readme.md +0 -22
  166. package/src/bootstrap.ts +71 -0
  167. package/src/cache.ts +319 -0
  168. package/src/config.ts +23 -35
  169. package/src/database.ts +650 -0
  170. package/src/index.ts +27 -59
  171. package/src/media.ts +302 -0
  172. package/src/recorder.ts +175 -0
  173. package/src/self-message.ts +456 -0
  174. package/src/server.d.ts +26 -0
  175. package/src/types.ts +108 -48
  176. package/src/web.ts +712 -0
  177. package/client/vue/chat-logic.ts +0 -683
  178. package/client/vue/composables/useChatActions.ts +0 -61
  179. package/client/vue/composables/useChatData.ts +0 -185
  180. package/client/vue/composables/useImageCache.ts +0 -281
  181. package/client/vue/composables/useVideoCache.ts +0 -125
  182. package/client/vue/types.ts +0 -72
  183. package/lib/api-handlers.d.ts +0 -25
  184. package/lib/config.d.ts +0 -14
  185. package/lib/file-manager.d.ts +0 -124
  186. package/lib/index.d.ts +0 -17
  187. package/lib/logger.d.ts +0 -9
  188. package/lib/message-handler.d.ts +0 -35
  189. package/lib/types.d.ts +0 -57
  190. package/lib/utils.d.ts +0 -18
  191. package/src/api-handlers.ts +0 -646
  192. package/src/file-manager.ts +0 -1436
  193. package/src/message-handler.ts +0 -507
  194. package/src/utils.ts +0 -189
@@ -1,61 +0,0 @@
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
- }
@@ -1,185 +0,0 @@
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
- loggerinfo: false,
15
- blockedPlatforms: [],
16
- chatContainerHeight: 80,
17
- clearIndexedDBOnStart: true
18
- })
19
-
20
- const pinnedBots = ref<Set<string>>(new Set())
21
- const pinnedChannels = ref<Set<string>>(new Set())
22
-
23
- // 记录每个频道的分页状态
24
- const channelPagination = ref<Record<string, { offset: number, hasMore: boolean }>>({})
25
-
26
- // 计算属性
27
- const bots = computed(() => {
28
- return Object.values(chatData.value.bots).sort((a, b) => {
29
- const aPinned = pinnedBots.value.has(a.selfId)
30
- const bPinned = pinnedBots.value.has(b.selfId)
31
- return aPinned === bPinned ? 0 : aPinned ? -1 : 1
32
- })
33
- })
34
-
35
- const getChannels = (botId: string) => {
36
- if (!botId || !chatData.value.channels[botId]) return []
37
- return Object.values(chatData.value.channels[botId]).sort((a, b) => {
38
- const aPinned = pinnedChannels.value.has(`${botId}:${a.id}`)
39
- const bPinned = pinnedChannels.value.has(`${botId}:${b.id}`)
40
- return aPinned === bPinned ? 0 : aPinned ? -1 : 1
41
- })
42
- }
43
-
44
- const getMessages = (botId: string, channelId: string) => {
45
- const key = `${botId}:${channelId}`
46
- return chatData.value.messages[key] || []
47
- }
48
-
49
- const getPagination = (botId: string, channelId: string) => {
50
- const key = `${botId}:${channelId}`
51
- return channelPagination.value[key] || { offset: 0, hasMore: true }
52
- }
53
-
54
- // 加载数据
55
- async function loadInitialData() {
56
- const result = await (send as any)('get-chat-data')
57
- if (result.success && result.data) {
58
- chatData.value = {
59
- bots: result.data.bots || {},
60
- channels: result.data.channels || {},
61
- messages: result.data.messages || {}
62
- }
63
- pinnedBots.value = new Set(result.data.pinnedBots || [])
64
- pinnedChannels.value = new Set(result.data.pinnedChannels || [])
65
- }
66
- }
67
-
68
- async function loadConfig() {
69
- const result = await (send as any)('get-plugin-config')
70
- if (result.success && result.config) {
71
- pluginConfig.value = result.config
72
- }
73
- }
74
-
75
- // 消息处理
76
- function addMessage(msg: any) {
77
- const selfId = msg.selfId
78
- const channelId = msg.channelId
79
- const key = `${selfId}:${channelId}`
80
-
81
- // 1. 确保机器人存在
82
- if (!chatData.value.bots[selfId]) {
83
- chatData.value.bots[selfId] = {
84
- selfId,
85
- platform: msg.platform || 'unknown',
86
- username: msg.bot?.name || `Bot-${selfId}`,
87
- avatar: msg.bot?.avatar,
88
- status: 'online'
89
- }
90
- }
91
-
92
- // 2. 确保频道存在
93
- if (!chatData.value.channels[selfId]) {
94
- chatData.value.channels[selfId] = {}
95
- }
96
- if (!chatData.value.channels[selfId][channelId]) {
97
- const isDirect = msg.isDirect || channelId.includes('private')
98
- chatData.value.channels[selfId][channelId] = {
99
- id: channelId,
100
- name: isDirect ? `私聊(${msg.username || channelId})` : (msg.guildName || channelId),
101
- type: msg.channelType || 0,
102
- isDirect
103
- }
104
- }
105
-
106
- // 3. 消息去重与添加
107
- if (!chatData.value.messages[key]) chatData.value.messages[key] = []
108
-
109
- const messages = chatData.value.messages[key]
110
- // 严格去重:检查 ID,如果是机器人发送的临时消息,则通过内容和时间戳近似匹配
111
- const isDuplicate = messages.some(m => {
112
- if (m.id === msg.messageId || m.id === msg.id) return true
113
- // 针对机器人发送消息的特殊去重逻辑
114
- 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
115
- return false
116
- })
117
-
118
- if (!isDuplicate) {
119
- const newMsg: MessageInfo = {
120
- id: msg.messageId || msg.id,
121
- content: msg.content,
122
- userId: msg.userId,
123
- username: msg.username,
124
- avatar: msg.avatar,
125
- timestamp: msg.timestamp,
126
- channelId: channelId,
127
- selfId: selfId,
128
- elements: msg.elements,
129
- isBot: msg.isBot || msg.type === 'bot-message' || msg.type === 'bot',
130
- quote: msg.quote
131
- }
132
- messages.push(newMsg)
133
- messages.sort((a, b) => a.timestamp - b.timestamp)
134
- }
135
-
136
- // 强制触发响应式更新,确保列表实时刷新
137
- chatData.value = { ...chatData.value }
138
- }
139
-
140
- async function loadHistory(botId: string, channelId: string, limit = 50) {
141
- const key = `${botId}:${channelId}`
142
- const pagination = getPagination(botId, channelId)
143
- if (!pagination.hasMore) return
144
-
145
- const result = await (send as any)('get-history-messages', {
146
- selfId: botId,
147
- channelId: channelId,
148
- limit,
149
- offset: pagination.offset
150
- })
151
-
152
- if (result.success && result.messages) {
153
- if (!chatData.value.messages[key]) chatData.value.messages[key] = []
154
- const existing = chatData.value.messages[key]
155
-
156
- // 合并并去重
157
- const newMsgs = result.messages.filter((m: any) => !existing.find(e => e.id === m.id))
158
- chatData.value.messages[key] = [...newMsgs, ...existing].sort((a, b) => a.timestamp - b.timestamp)
159
-
160
- channelPagination.value[key] = {
161
- offset: pagination.offset + result.messages.length,
162
- hasMore: result.messages.length >= limit && result.total > (pagination.offset + result.messages.length)
163
- }
164
-
165
- chatData.value = { ...chatData.value }
166
- return result.messages.length
167
- }
168
- return 0
169
- }
170
-
171
- return {
172
- chatData,
173
- pluginConfig,
174
- pinnedBots,
175
- pinnedChannels,
176
- bots,
177
- getChannels,
178
- getMessages,
179
- getPagination,
180
- loadInitialData,
181
- loadConfig,
182
- addMessage,
183
- loadHistory
184
- }
185
- }
@@ -1,281 +0,0 @@
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 = 50 * 1024 * 1024 // 50MB(从100MB降低)
19
- const MAX_BLOB_COUNT = 30 // 从50降低到30
20
- let currentMemoryUsage = 0
21
- let blobCount = 0
22
-
23
- // IndexedDB 配置
24
- let imageDB: IDBDatabase | null = null
25
- const DB_NAME = 'ChatImageCache'
26
- const DB_VERSION = 2
27
- const STORE_NAME = 'images'
28
- const MAX_DB_SIZE = 50 * 1024 * 1024 // 50MB
29
- const MAX_TOTAL_IMAGES = 500
30
- const MAX_IMAGE_SIZE = 12 * 1024 * 1024 // 12MB
31
-
32
- // 清理 IndexedDB
33
- async function clearIndexedDB(): Promise<boolean> {
34
- return new Promise((resolve) => {
35
- const request = indexedDB.deleteDatabase(DB_NAME)
36
- request.onsuccess = () => {
37
- console.log('[ImageCache] IndexedDB 已清空')
38
- resolve(true)
39
- }
40
- request.onerror = () => {
41
- console.error('[ImageCache] 清空 IndexedDB 失败')
42
- resolve(false)
43
- }
44
- })
45
- }
46
-
47
- // 初始化数据库
48
- async function initDB(shouldClear: boolean = false): Promise<boolean> {
49
- // 如果需要清空,先删除数据库
50
- if (shouldClear) {
51
- await clearIndexedDB()
52
- }
53
-
54
- return new Promise((resolve) => {
55
- const request = indexedDB.open(DB_NAME, DB_VERSION)
56
- request.onupgradeneeded = (event) => {
57
- const db = (event.target as IDBOpenDBRequest).result
58
- if (!db.objectStoreNames.contains(STORE_NAME)) {
59
- const store = db.createObjectStore(STORE_NAME, { keyPath: 'url' })
60
- store.createIndex('channelKey', 'channelKey', { unique: false })
61
- store.createIndex('timestamp', 'timestamp', { unique: false })
62
- }
63
- }
64
- request.onsuccess = () => {
65
- imageDB = request.result
66
- resolve(true)
67
- }
68
- request.onerror = () => resolve(false)
69
- })
70
- }
71
-
72
- // 清理内存中的 Blob URLs
73
- function cleanupMemoryBlobs() {
74
- if (blobCount > MAX_BLOB_COUNT || currentMemoryUsage > MAX_MEMORY_USAGE) {
75
- // 按时间戳排序,删除最旧的
76
- const entries = Object.entries(imageBlobUrls.value)
77
- const toRemove = Math.ceil(entries.length * 0.3) // 删除30%最旧的
78
-
79
- for (let i = 0; i < toRemove && entries.length > 0; i++) {
80
- const [url, blobUrl] = entries[i]
81
- URL.revokeObjectURL(blobUrl)
82
- delete imageBlobUrls.value[url]
83
- blobCount--
84
- }
85
-
86
- currentMemoryUsage = Math.floor(currentMemoryUsage * 0.7)
87
- console.log(`[ImageCache] 清理内存,剩余 ${blobCount} 个图片`)
88
- }
89
- }
90
-
91
- // 获取缓存图片
92
- async function getCachedImageUrl(channelKey: string, url: string): Promise<string | null> {
93
- // 先检查内存缓存
94
- if (imageBlobUrls.value[url]) return imageBlobUrls.value[url]
95
- if (loadingImages.has(url)) return loadingImages.get(url)!
96
-
97
- const promise = (async () => {
98
- try {
99
- if (!imageDB) await initDB()
100
- const item = await getImageFromDB(url)
101
- if (item) {
102
- // 从 IndexedDB 加载到内存
103
- const blobUrl = URL.createObjectURL(item.blob)
104
- imageBlobUrls.value[url] = blobUrl
105
- currentMemoryUsage += item.size
106
- blobCount++
107
- cleanupMemoryBlobs() // 检查是否需要清理
108
- return blobUrl
109
- }
110
- return null
111
- } finally {
112
- loadingImages.delete(url)
113
- }
114
- })()
115
-
116
- loadingImages.set(url, promise)
117
- return promise
118
- }
119
-
120
- async function getImageFromDB(url: string): Promise<ImageCacheItem | null> {
121
- if (!imageDB) return null
122
- return new Promise((resolve) => {
123
- const transaction = imageDB!.transaction([STORE_NAME], 'readonly')
124
- const store = transaction.objectStore(STORE_NAME)
125
- const request = store.get(url)
126
- request.onsuccess = () => resolve(request.result || null)
127
- request.onerror = () => resolve(null)
128
- })
129
- }
130
-
131
- async function cacheImage(channelKey: string, url: string): Promise<string | null> {
132
- // 先检查是否已缓存
133
- const existing = await getCachedImageUrl(channelKey, url)
134
- if (existing) return existing
135
-
136
- try {
137
- // 通过后端获取图片(现在返回 Vite @fs 路径)
138
- const result = await (send as any)('fetch-image', { url })
139
- if (!result.success) return null
140
-
141
- // 如果后端返回 Vite @fs 路径,直接使用,不需要缓存到 IndexedDB
142
- if (result.viteUrl) {
143
- // 将 Vite 路径缓存到内存中,避免重复请求后端
144
- imageBlobUrls.value[url] = result.viteUrl
145
- return result.viteUrl
146
- }
147
-
148
- // 兼容旧的 base64 返回格式(如果有的话)
149
- if (result.dataUrl) {
150
- const response = await fetch(result.dataUrl)
151
- const blob = await response.blob()
152
-
153
- // 检查图片大小
154
- if (blob.size > MAX_IMAGE_SIZE) {
155
- console.warn(`[ImageCache] 图片过大 (${blob.size} bytes),不缓存`)
156
- return result.dataUrl
157
- }
158
-
159
- const item: ImageCacheItem = {
160
- url,
161
- blob,
162
- timestamp: Date.now(),
163
- size: blob.size,
164
- channelKey
165
- }
166
-
167
- await saveToDB(item)
168
-
169
- const blobUrl = URL.createObjectURL(blob)
170
- imageBlobUrls.value[url] = blobUrl
171
- currentMemoryUsage += blob.size
172
- blobCount++
173
- cleanupMemoryBlobs()
174
-
175
- return blobUrl
176
- }
177
-
178
- return null
179
- } catch (e) {
180
- console.error('[ImageCache] 缓存图片失败:', e)
181
- return null
182
- }
183
- }
184
-
185
- async function saveToDB(item: ImageCacheItem) {
186
- if (!imageDB) return
187
-
188
- try {
189
- // 检查数据库大小,如果超过限制则清理旧数据
190
- const count = await getDBCount()
191
- if (count >= MAX_TOTAL_IMAGES) {
192
- await cleanupOldestImages(Math.floor(MAX_TOTAL_IMAGES * 0.2)) // 清理20%最旧的
193
- }
194
-
195
- const transaction = imageDB.transaction([STORE_NAME], 'readwrite')
196
- transaction.objectStore(STORE_NAME).put(item)
197
- } catch (e) {
198
- console.error('[ImageCache] 保存到 IndexedDB 失败:', e)
199
- }
200
- }
201
-
202
- // 获取数据库中的图片数量
203
- async function getDBCount(): Promise<number> {
204
- if (!imageDB) return 0
205
- return new Promise((resolve) => {
206
- const transaction = imageDB!.transaction([STORE_NAME], 'readonly')
207
- const store = transaction.objectStore(STORE_NAME)
208
- const request = store.count()
209
- request.onsuccess = () => resolve(request.result)
210
- request.onerror = () => resolve(0)
211
- })
212
- }
213
-
214
- // 清理最旧的图片
215
- async function cleanupOldestImages(count: number) {
216
- if (!imageDB) return
217
-
218
- return new Promise<void>((resolve) => {
219
- const transaction = imageDB!.transaction([STORE_NAME], 'readwrite')
220
- const store = transaction.objectStore(STORE_NAME)
221
- const index = store.index('timestamp')
222
- const request = index.openCursor()
223
-
224
- let deleted = 0
225
- request.onsuccess = (event) => {
226
- const cursor = (event.target as IDBRequest).result
227
- if (cursor && deleted < count) {
228
- cursor.delete()
229
- deleted++
230
- cursor.continue()
231
- } else {
232
- console.log(`[ImageCache] 清理了 ${deleted} 张旧图片`)
233
- resolve()
234
- }
235
- }
236
- request.onerror = () => resolve()
237
- })
238
- }
239
-
240
- async function clearChannelCache(channelKey: string) {
241
- // 清理内存中的 Blob URLs
242
- Object.keys(imageBlobUrls.value).forEach(url => {
243
- URL.revokeObjectURL(imageBlobUrls.value[url])
244
- delete imageBlobUrls.value[url]
245
- })
246
- blobCount = 0
247
- currentMemoryUsage = 0
248
- }
249
-
250
- // 初始化时检查是否需要清空 IndexedDB
251
- onMounted(async () => {
252
- try {
253
- // 获取插件配置
254
- const configResult = await (send as any)('get-plugin-config')
255
- const shouldClear = configResult?.success && configResult?.config?.clearIndexedDBOnStart
256
-
257
- if (shouldClear) {
258
- console.log('[ImageCache] 配置要求清空 IndexedDB,正在清理...')
259
- await initDB(true) // 清空并重新初始化
260
- } else {
261
- await initDB(false) // 正常初始化
262
- }
263
- } catch (e) {
264
- console.error('[ImageCache] 初始化失败:', e)
265
- await initDB(false)
266
- }
267
- })
268
-
269
- onUnmounted(() => {
270
- // 清理所有内存中的 Blob URLs
271
- Object.values(imageBlobUrls.value).forEach(URL.revokeObjectURL)
272
- if (imageDB) imageDB.close()
273
- })
274
-
275
- return {
276
- getCachedImageUrl,
277
- cacheImage,
278
- clearChannelCache,
279
- clearIndexedDB // 导出清理函数供外部使用
280
- }
281
- }
@@ -1,125 +0,0 @@
1
- import { ref, onUnmounted } from 'vue'
2
- import { send } from '@koishijs/client'
3
-
4
- /**
5
- * 视频按需加载 composable
6
- * 只在用户点击播放时才加载视频,避免内存占用
7
- */
8
- export function useVideoCache() {
9
- // 当前加载的视频 URL(原始 URL -> blob URL)
10
- const loadedVideos = ref<Record<string, string>>({})
11
- // 正在加载的视频
12
- const loadingVideos = ref<Set<string>>(new Set())
13
- // 当前的 blob URL,用于清理
14
- const currentBlobUrl = ref<string | null>(null)
15
-
16
- /**
17
- * 按需加载视频
18
- * @param url 原始视频 URL
19
- * @returns blob URL 或 null
20
- */
21
- async function loadVideo(url: string): Promise<string | null> {
22
- // 如果已经加载过,直接返回
23
- if (loadedVideos.value[url]) {
24
- return loadedVideos.value[url]
25
- }
26
-
27
- // 如果正在加载,等待加载完成
28
- if (loadingVideos.value.has(url)) {
29
- // 等待加载完成
30
- return new Promise((resolve) => {
31
- const checkInterval = setInterval(() => {
32
- if (loadedVideos.value[url]) {
33
- clearInterval(checkInterval)
34
- resolve(loadedVideos.value[url])
35
- }
36
- if (!loadingVideos.value.has(url)) {
37
- clearInterval(checkInterval)
38
- resolve(null)
39
- }
40
- }, 100)
41
- })
42
- }
43
-
44
- // 开始加载
45
- loadingVideos.value.add(url)
46
-
47
- try {
48
- const result = await (send as any)('fetch-video-temp', { url })
49
-
50
- if (result.success) {
51
- // 如果返回 Vite @fs 路径,直接使用,不需要转换为 blob
52
- if (result.viteUrl) {
53
- loadedVideos.value[url] = result.viteUrl
54
- return result.viteUrl
55
- }
56
-
57
- // 兼容旧的 dataUrl 格式(如果有的话)
58
- if (result.dataUrl) {
59
- // 清理上一个 blob URL
60
- if (currentBlobUrl.value) {
61
- URL.revokeObjectURL(currentBlobUrl.value)
62
- }
63
-
64
- // 从 data URL 创建 blob
65
- const response = await fetch(result.dataUrl)
66
- const blob = await response.blob()
67
- const blobUrl = URL.createObjectURL(blob)
68
-
69
- currentBlobUrl.value = blobUrl
70
- loadedVideos.value[url] = blobUrl
71
- return blobUrl
72
- }
73
-
74
- return null
75
- }
76
-
77
- return null
78
- } catch (error) {
79
- console.error('加载视频失败:', error)
80
- return null
81
- } finally {
82
- loadingVideos.value.delete(url)
83
- }
84
- }
85
-
86
- /**
87
- * 检查视频是否正在加载
88
- */
89
- function isVideoLoading(url: string): boolean {
90
- return loadingVideos.value.has(url)
91
- }
92
-
93
- /**
94
- * 检查视频是否已加载
95
- */
96
- function isVideoLoaded(url: string): boolean {
97
- return !!loadedVideos.value[url]
98
- }
99
-
100
- /**
101
- * 清空所有已加载的视频
102
- */
103
- function clearAllVideos() {
104
- // 清理所有 blob URLs
105
- Object.values(loadedVideos.value).forEach(blobUrl => {
106
- if (blobUrl.startsWith('blob:')) {
107
- URL.revokeObjectURL(blobUrl)
108
- }
109
- })
110
- loadedVideos.value = {}
111
- currentBlobUrl.value = null
112
- }
113
-
114
- // 组件卸载时清理
115
- onUnmounted(() => {
116
- clearAllVideos()
117
- })
118
-
119
- return {
120
- loadVideo,
121
- isVideoLoading,
122
- isVideoLoaded,
123
- clearAllVideos
124
- }
125
- }