koishi-plugin-chat-patch 3.0.2 → 5.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.
Files changed (237) hide show
  1. package/client/vue/index.vue +60 -634
  2. package/client/web/dist/assets/AppIcon-DIku6-h0.png +0 -0
  3. package/client/web/dist/assets/AppIconDark-BYymN71t.png +0 -0
  4. package/client/web/dist/assets/CapacitorAppIcon-CSZwNhvi.png +0 -0
  5. package/client/web/dist/assets/CapacitorAppIconDark-BMvMOzsg.png +0 -0
  6. package/client/web/dist/assets/Chat-CTdyeaX2.css +1 -0
  7. package/client/web/dist/assets/Chat-DkZvD7d8.js +28 -0
  8. package/client/web/dist/assets/MealHungryPan-Beos3lRI.js +1 -0
  9. package/client/web/dist/assets/MsgBody-aZ4_aija.js +1 -0
  10. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CPZjRLp6.js +67 -0
  11. package/client/web/dist/assets/NapcatAppIcon-C7VOAkW9.png +0 -0
  12. package/client/web/dist/assets/Vue-lEhHouxB.png +0 -0
  13. package/client/web/dist/assets/append_darwin-CqBNU62J.css +1 -0
  14. package/client/web/dist/assets/append_full_vibrancy-j0D2LM9m.css +1 -0
  15. package/client/web/dist/assets/append_linux-uhxeBqeC.css +1 -0
  16. package/client/web/dist/assets/append_linux_vibrancy-COiZq8MS.css +1 -0
  17. package/client/web/dist/assets/append_napcat-DV8CAyAd.css +1 -0
  18. package/client/web/dist/assets/append_new-407Lo5_V.css +1 -0
  19. package/client/web/dist/assets/append_vibrancy-6FHctkOY.css +1 -0
  20. package/client/web/dist/assets/append_win32-srmD-78y.css +1 -0
  21. package/client/web/dist/assets/cakes-CxTVKdnK.svg +189 -0
  22. package/client/web/dist/assets/core-DhEqZVGG.js +1 -0
  23. package/client/web/dist/assets/event-Cadp6m6J.js +1 -0
  24. package/client/web/dist/assets/index-CPTXiemn.js +4045 -0
  25. package/client/web/dist/assets/index-Cg_esKB0.js +1 -0
  26. package/client/web/dist/assets/index-Cln012sP.css +1 -0
  27. package/client/web/dist/assets//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223-CJKIKBn3.ttf +0 -0
  28. package/client/web/dist/assets//346/234/237/345/276/205-C90_OY60.png +0 -0
  29. package/client/web/dist/bcui/css/color-dark.css +35 -0
  30. package/client/web/dist/bcui/css/color-light.css +35 -0
  31. package/client/web/dist/bcui/css/style.css +468 -0
  32. package/client/web/dist/bcui/js/main.js +193 -0
  33. package/client/web/dist/css/append-dark.css +13 -0
  34. package/client/web/dist/css/append-light.css +13 -0
  35. package/client/web/dist/css/append-napcat-dark.css +14 -0
  36. package/client/web/dist/css/append-napcat-light.css +14 -0
  37. package/client/web/dist/favicon.ico +0 -0
  38. package/client/web/dist/img/icons/icon-for-fuck-apple.png +0 -0
  39. package/client/web/dist/img/icons/icon-maskable.png +0 -0
  40. package/client/web/dist/img/icons/icon.png +0 -0
  41. package/client/web/dist/img/icons/icon.svg +10 -0
  42. package/client/web/dist/index.html +22 -0
  43. package/client/web/dist/robots.txt +2 -0
  44. package/client/web/dist/sw.js +58 -0
  45. package/client/web/env.d.ts +56 -0
  46. package/client/web/index.html +21 -0
  47. package/client/web/package-lock.json +3419 -0
  48. package/client/web/package.json +62 -0
  49. package/client/web/public/bcui/css/color-dark.css +35 -0
  50. package/client/web/public/bcui/css/color-light.css +35 -0
  51. package/client/web/public/bcui/css/style.css +468 -0
  52. package/client/web/public/bcui/js/main.js +193 -0
  53. package/client/web/public/css/append-dark.css +13 -0
  54. package/client/web/public/css/append-light.css +13 -0
  55. package/client/web/public/css/append-napcat-dark.css +14 -0
  56. package/client/web/public/css/append-napcat-light.css +14 -0
  57. package/client/web/public/favicon.ico +0 -0
  58. package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
  59. package/client/web/public/img/icons/icon-maskable.png +0 -0
  60. package/client/web/public/img/icons/icon.png +0 -0
  61. package/client/web/public/img/icons/icon.svg +10 -0
  62. package/client/web/public/robots.txt +2 -0
  63. package/client/web/public/sw.js +58 -0
  64. package/client/web/scripts/copy-bcui.mjs +17 -0
  65. package/client/web/src/App.vue +1127 -0
  66. package/client/web/src/assets/css/append/append_darwin.css +68 -0
  67. package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
  68. package/client/web/src/assets/css/append/append_linux.css +125 -0
  69. package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
  70. package/client/web/src/assets/css/append/append_napcat.css +35 -0
  71. package/client/web/src/assets/css/append/append_new.css +843 -0
  72. package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
  73. package/client/web/src/assets/css/append/append_win32.css +109 -0
  74. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
  75. package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
  76. package/client/web/src/assets/css/chat.css +2104 -0
  77. package/client/web/src/assets/css/msg.css +941 -0
  78. package/client/web/src/assets/css/options.css +811 -0
  79. package/client/web/src/assets/css/sys_notice.css +62 -0
  80. package/client/web/src/assets/css/view.css +2105 -0
  81. package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
  82. package/client/web/src/assets/img/Vue.png +0 -0
  83. package/client/web/src/assets/img/cakes.svg +189 -0
  84. package/client/web/src/assets/img/icons/AppIcon.png +0 -0
  85. package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
  86. package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
  87. package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
  88. package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
  89. package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
  90. package/client/web/src/assets/img/icons/exit.png +0 -0
  91. package/client/web/src/assets/img/icons/pen.png +0 -0
  92. package/client/web/src/assets/img/stars.svg +89 -0
  93. package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
  94. package/client/web/src/assets/l10n/_About I10n.md +35 -0
  95. package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
  96. package/client/web/src/assets/l10n/zh-CN.po +1941 -0
  97. package/client/web/src/assets/logo.png +0 -0
  98. package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
  99. package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
  100. package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
  101. package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
  102. package/client/web/src/assets/qed.txt +1 -0
  103. package/client/web/src/components/AboutPan.vue +197 -0
  104. package/client/web/src/components/BulletinBody.vue +129 -0
  105. package/client/web/src/components/DepPan.vue +310 -0
  106. package/client/web/src/components/EmojiFace.vue +32 -0
  107. package/client/web/src/components/FacePan.vue +461 -0
  108. package/client/web/src/components/FileBody.vue +129 -0
  109. package/client/web/src/components/FileManager.vue +532 -0
  110. package/client/web/src/components/FriendBody.vue +54 -0
  111. package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
  112. package/client/web/src/components/History.vue +49 -0
  113. package/client/web/src/components/LazyLottie.vue +82 -0
  114. package/client/web/src/components/MergePan.vue +127 -0
  115. package/client/web/src/components/MsgBody.vue +1478 -0
  116. package/client/web/src/components/MusicPlayer.vue +549 -0
  117. package/client/web/src/components/NoticeBody.vue +168 -0
  118. package/client/web/src/components/QzonePermissionPan.vue +150 -0
  119. package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
  120. package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
  121. package/client/web/src/components/TinySessionBody.vue +51 -0
  122. package/client/web/src/components/UmamiInfoPan.vue +1549 -0
  123. package/client/web/src/components/UpdatePan.vue +263 -0
  124. package/client/web/src/components/ViewerCom.vue +1360 -0
  125. package/client/web/src/components/VoiceMsg.vue +626 -0
  126. package/client/web/src/components/WelPan.vue +894 -0
  127. package/client/web/src/components/msg-body/.gitkeep +0 -0
  128. package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
  129. package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
  130. package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
  131. package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
  132. package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
  133. package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
  134. package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
  135. package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
  136. package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
  137. package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
  138. package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
  139. package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
  140. package/client/web/src/components/notice-component/.gitkeep +0 -0
  141. package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
  142. package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
  143. package/client/web/src/components/tooltip/Tooltip.vue +50 -0
  144. package/client/web/src/components/tooltip/Tooltips.vue +14 -0
  145. package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
  146. package/client/web/src/function/base.ts +238 -0
  147. package/client/web/src/function/connect.ts +2042 -0
  148. package/client/web/src/function/debug.ts +10 -0
  149. package/client/web/src/function/elements/information.ts +141 -0
  150. package/client/web/src/function/elements/system.ts +101 -0
  151. package/client/web/src/function/elements/vueComp.ts +62 -0
  152. package/client/web/src/function/favicon.ts +76 -0
  153. package/client/web/src/function/input.ts +69 -0
  154. package/client/web/src/function/model/emoji.ts +127 -0
  155. package/client/web/src/function/model/img.ts +150 -0
  156. package/client/web/src/function/model/msg-body.ts +51 -0
  157. package/client/web/src/function/msg.ts +2674 -0
  158. package/client/web/src/function/notify.ts +225 -0
  159. package/client/web/src/function/option.ts +749 -0
  160. package/client/web/src/function/satori-model.ts +818 -0
  161. package/client/web/src/function/satori.ts +325 -0
  162. package/client/web/src/function/sender.ts +178 -0
  163. package/client/web/src/function/tooltip.ts +49 -0
  164. package/client/web/src/function/utils/appUtil.ts +2288 -0
  165. package/client/web/src/function/utils/avatarUtil.ts +31 -0
  166. package/client/web/src/function/utils/backgroundUtil.ts +110 -0
  167. package/client/web/src/function/utils/linkViewUtil.ts +101 -0
  168. package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
  169. package/client/web/src/function/utils/msgUtil.ts +1242 -0
  170. package/client/web/src/function/utils/pinyin.ts +141 -0
  171. package/client/web/src/function/utils/recordUtil.ts +187 -0
  172. package/client/web/src/function/utils/sessionUtil.ts +114 -0
  173. package/client/web/src/function/utils/systemUtil.ts +664 -0
  174. package/client/web/src/main.ts +91 -0
  175. package/client/web/src/pages/Chat.vue +3453 -0
  176. package/client/web/src/pages/Friends.vue +636 -0
  177. package/client/web/src/pages/Info.vue +717 -0
  178. package/client/web/src/pages/Messages.vue +875 -0
  179. package/client/web/src/pages/Options.vue +82 -0
  180. package/client/web/src/pages/Qzone.vue +1178 -0
  181. package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
  182. package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
  183. package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
  184. package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
  185. package/client/web/src/pages/options/OptAccount.vue +107 -0
  186. package/client/web/src/pages/options/OptAddon.vue +185 -0
  187. package/client/web/src/pages/options/OptDev.vue +704 -0
  188. package/client/web/src/pages/options/OptFunction.vue +403 -0
  189. package/client/web/src/pages/options/OptInfo.vue +165 -0
  190. package/client/web/src/pages/options/OptView.vue +846 -0
  191. package/client/web/src/runtime/backend.ts +310 -0
  192. package/client/web/src/state/auth.ts +14 -0
  193. package/client/web/src/state/chat.ts +36 -0
  194. package/client/web/src/state/connection.ts +20 -0
  195. package/client/web/src/state/contact.ts +37 -0
  196. package/client/web/src/state/qzone.ts +18 -0
  197. package/client/web/src/state/sessionHistory.ts +25 -0
  198. package/client/web/src/state/settings.ts +18 -0
  199. package/client/web/src/state/sticker.ts +10 -0
  200. package/client/web/src/state/ui.ts +56 -0
  201. package/client/web/tsconfig.json +27 -0
  202. package/client/web/tsconfig.tsbuildinfo +1 -0
  203. package/client/web/vite.config.ts +33 -0
  204. package/dist/index.js +1 -12
  205. package/dist/style.css +1 -1
  206. package/lib/index.js +2070 -2077
  207. package/package.json +15 -2
  208. package/readme.md +0 -22
  209. package/src/bootstrap.ts +71 -0
  210. package/src/cache.ts +319 -0
  211. package/src/config.ts +23 -35
  212. package/src/database.ts +650 -0
  213. package/src/index.ts +27 -59
  214. package/src/media.ts +302 -0
  215. package/src/recorder.ts +175 -0
  216. package/src/self-message.ts +456 -0
  217. package/src/server.d.ts +26 -0
  218. package/src/types.ts +108 -48
  219. package/src/web.ts +712 -0
  220. package/client/vue/chat-logic.ts +0 -683
  221. package/client/vue/composables/useChatActions.ts +0 -61
  222. package/client/vue/composables/useChatData.ts +0 -185
  223. package/client/vue/composables/useImageCache.ts +0 -281
  224. package/client/vue/composables/useVideoCache.ts +0 -125
  225. package/client/vue/types.ts +0 -72
  226. package/lib/api-handlers.d.ts +0 -25
  227. package/lib/config.d.ts +0 -14
  228. package/lib/file-manager.d.ts +0 -124
  229. package/lib/index.d.ts +0 -17
  230. package/lib/logger.d.ts +0 -9
  231. package/lib/message-handler.d.ts +0 -35
  232. package/lib/types.d.ts +0 -57
  233. package/lib/utils.d.ts +0 -18
  234. package/src/api-handlers.ts +0 -646
  235. package/src/file-manager.ts +0 -1436
  236. package/src/message-handler.ts +0 -507
  237. package/src/utils.ts +0 -189
@@ -1,507 +0,0 @@
1
- import { BotInfo, ChannelInfo, MessageInfo, QuoteInfo } from './types'
2
- import { Context, Session, h } from 'koishi'
3
- import { FileManager } from './file-manager'
4
- import { Config } from './config'
5
- import { Utils } from './utils'
6
- import { PluginLogger } from './logger'
7
- import { } from '@koishijs/plugin-console'
8
- import { promises as fs } from 'node:fs'
9
- import path from 'node:path'
10
- import { createHash } from 'node:crypto'
11
-
12
- export class MessageHandler {
13
- private utils: Utils
14
-
15
- private correctChannelIds: Map<string, string> = new Map()
16
-
17
- private scheduledTasks: Set<() => void> = new Set()
18
-
19
- private channelRefreshInFlight: Set<string> = new Set()
20
-
21
- private lastChannelRefreshAt: Map<string, number> = new Map()
22
-
23
- private readonly CHANNEL_REFRESH_TTL_MS = 10 * 60 * 1000
24
-
25
- constructor(
26
- private ctx: Context,
27
- private config: Config,
28
- private fileManager: FileManager,
29
- private logger: PluginLogger
30
- ) {
31
- this.utils = new Utils(config)
32
- }
33
-
34
- recordUserMessage(session: Session, timestamp: number) {
35
- this.scheduleTask('记录用户消息', async () => {
36
- await this.processUserMessage(session, timestamp)
37
- })
38
- }
39
-
40
- recordBotMessage(session: Session, timestamp: number) {
41
- this.scheduleTask('记录机器人消息', async () => {
42
- await this.processBotMessage(session, timestamp)
43
- })
44
- }
45
-
46
- setCorrectChannelId(selfId: string, channelId: string) {
47
- this.correctChannelIds.set(selfId, channelId)
48
- this.logger.logInfo('设置正确的 channelId:', { selfId, channelId })
49
- }
50
-
51
- getCorrectChannelId(selfId: string): string | undefined {
52
- return this.correctChannelIds.get(selfId)
53
- }
54
-
55
- updateBotInfoToFile(session: Session) {
56
- this.scheduleTask('更新机器人信息', async () => {
57
- const botInfo: BotInfo = {
58
- selfId: session.selfId,
59
- platform: session.platform || 'unknown',
60
- username: session.bot.user?.name || `Bot-${session.selfId}`,
61
- avatar: session.bot.user?.avatar,
62
- status: 'online'
63
- }
64
-
65
- await this.fileManager.upsertBotInfo(botInfo)
66
- this.logger.logInfo('更新机器人信息到文件:', botInfo.username)
67
- })
68
- }
69
-
70
- updateChannelInfoToFile(session: Session): string {
71
- const isDirect = session.isDirect || session.channelId?.includes('private')
72
- const directUserName = session.username || session.event?.user?.name || session.userId
73
-
74
- const existingChannel = this.fileManager.getCachedChannelInfo(session.selfId, session.channelId)
75
-
76
- let immediateName = session.channelId
77
- if (isDirect) {
78
- if (directUserName && directUserName !== session.userId) {
79
- immediateName = `私聊(${directUserName})`
80
- } else if (existingChannel?.name && !existingChannel.name.includes('未知')) {
81
- immediateName = existingChannel.name
82
- } else if (session.platform && session.platform.toLowerCase().includes('sandbox')) {
83
- immediateName = `私聊(${session.userId})`
84
- } else {
85
- immediateName = '私聊(未知用户)'
86
- }
87
- } else if (existingChannel?.guildName) {
88
- immediateName = existingChannel.guildName
89
- }
90
-
91
- const channelKey = `${session.selfId}:${session.channelId}`
92
- if (this.shouldRefreshChannelInfo(channelKey, existingChannel, isDirect, session.channelId, directUserName)) {
93
- this.scheduleTask('更新频道信息', async () => {
94
- await this.refreshChannelInfo(session, existingChannel, isDirect, directUserName)
95
- })
96
- }
97
-
98
- return immediateName
99
- }
100
-
101
- public async downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar') {
102
- try {
103
- if (!url || url.startsWith('data:')) return url
104
-
105
- // 如果已经是 Vite @fs 路径,直接返回
106
- if (url.includes('/vite/@fs/')) return url
107
-
108
- let folder = 'media'
109
- if (type === 'image') folder = 'images'
110
- else if (type === 'avatar') folder = 'avatars'
111
-
112
- const dir = path.join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', folder)
113
- await fs.mkdir(dir, { recursive: true })
114
-
115
- const hash = createHash('md5').update(url).digest('hex')
116
- const ext = path.extname(new URL(url).pathname) || (type === 'image' ? '.jpg' : '.mp4')
117
- const filename = `${hash}${ext}`
118
- const filePath = path.join(dir, filename)
119
-
120
- if (!(await this.fileExists(filePath))) {
121
- const buffer = await this.ctx.http.get(url, { responseType: 'arraybuffer' })
122
- await fs.writeFile(filePath, Buffer.from(buffer))
123
- }
124
-
125
- // 返回 Vite @fs 路径格式,让浏览器通过 Vite 开发服务器加载本地文件
126
- // Windows 路径需要转换为正斜杠
127
- const normalizedPath = filePath.replace(/\\/g, '/')
128
- return `/vite/@fs/${normalizedPath}`
129
- } catch (e) {
130
- this.logger.warn('下载并缓存媒体失败:', e)
131
- return url
132
- }
133
- }
134
-
135
- private processMediaElementsAsync(elements: h[], isUserMessage: boolean = true) {
136
- if (!elements) return
137
-
138
- this.scheduleTask('处理媒体元素', async () => {
139
- await this.processMediaElements(elements, isUserMessage)
140
- })
141
- }
142
-
143
- private async processUserMessage(session: Session, timestamp?: number) {
144
- try {
145
- if (!timestamp) timestamp = Date.now()
146
-
147
- this.updateBotInfoToFile(session)
148
-
149
- const guildName = this.updateChannelInfoToFile(session)
150
- const isDirect = session.isDirect || session.channelId?.includes('private')
151
-
152
- if (session.elements) {
153
- this.processMediaElementsAsync(session.elements, true)
154
- }
155
- if (session.quote?.elements) {
156
- this.processMediaElementsAsync(session.quote.elements, true)
157
- }
158
-
159
- let quoteInfo: QuoteInfo | undefined = undefined
160
- if (session.quote) {
161
- quoteInfo = {
162
- messageId: session.quote.messageId || session.quote.id,
163
- id: session.quote.id,
164
- content: session.quote.content || '',
165
- elements: session.quote.elements,
166
- user: {
167
- id: session.quote.user?.id || session.quote.user?.userId || 'unknown',
168
- name: session.quote.user?.name || session.quote.user?.username || 'unknown',
169
- userId: session.quote.user?.userId || session.quote.user?.id || 'unknown',
170
- avatar: session.quote.user?.avatar,
171
- username: session.quote.user?.username || session.quote.user?.name || 'unknown'
172
- },
173
- timestamp: session.quote.timestamp || Date.now()
174
- }
175
- }
176
-
177
- let content = ''
178
- let elements: h[] = []
179
-
180
- if (session.content) {
181
- content = session.content
182
- } else if (session.stripped?.content) {
183
- content = session.stripped.content
184
- }
185
-
186
- if (session.elements) {
187
- elements = session.elements
188
- if (!content) {
189
- content = session.elements
190
- .filter((element: any) => element.type === 'text')
191
- .map((element: any) => element.attrs?.content || '')
192
- .join('')
193
- }
194
- }
195
-
196
- const messageInfo: MessageInfo = {
197
- id: session.event?.message?.id || `msg-${timestamp}`,
198
- content: content || session.content || '',
199
- userId: session.userId || session.event?.user?.id || 'unknown',
200
- username: session.username || session.event?.user?.name || session.userId || 'unknown',
201
- avatar: session.event?.user?.avatar,
202
- timestamp: timestamp,
203
- channelId: session.channelId,
204
- selfId: session.selfId,
205
- elements: elements,
206
- type: 'user',
207
- guildName: guildName,
208
- platform: session.platform || 'unknown',
209
- quote: quoteInfo,
210
- isDirect: !!isDirect
211
- }
212
-
213
- messageInfo.elements = await this.utils.cleanBase64ContentAsync(messageInfo.elements, false)
214
- messageInfo.quote = messageInfo.quote ? await this.utils.cleanBase64ContentAsync(messageInfo.quote, false) : undefined
215
-
216
- await this.fileManager.addMessageToFile(messageInfo)
217
-
218
- const eventElements = await this.utils.cleanBase64ContentAsync(elements, false)
219
- const eventQuote = quoteInfo ? await this.utils.cleanBase64ContentAsync(quoteInfo, false) : undefined
220
-
221
- const messageEvent = {
222
- type: 'message',
223
- selfId: session.selfId,
224
- platform: session.platform || 'unknown',
225
- channelId: session.channelId,
226
- messageId: session.event?.message?.id || `msg-${timestamp}`,
227
- content: content || session.content || '',
228
- userId: session.userId || session.event?.user?.id || 'unknown',
229
- username: session.username || session.event?.user?.name || session.userId || 'unknown',
230
- avatar: session.event?.user?.avatar,
231
- timestamp: timestamp,
232
- guildName: guildName,
233
- channelType: session.type || 0,
234
- elements: eventElements,
235
- quote: eventQuote,
236
- isDirect: session.isDirect,
237
- bot: {
238
- avatar: session.bot.user?.avatar,
239
- name: session.bot.user?.name,
240
- }
241
- }
242
-
243
- this.ctx.console.broadcast('chat-message-event', messageEvent)
244
- } catch (error) {
245
- this.logger.error('处理用户消息失败:', error)
246
- }
247
- }
248
-
249
- private async processBotMessage(session: Session, timestamp?: number) {
250
- try {
251
- if (!timestamp) timestamp = Date.now()
252
-
253
- const correctChannelId = this.getCorrectChannelId(session.selfId)
254
- const finalChannelId = correctChannelId || session.channelId
255
-
256
- this.updateBotInfoToFile(session)
257
-
258
- const guildName = this.updateChannelInfoToFile(session)
259
- const isDirect = session.isDirect || finalChannelId?.includes('private')
260
-
261
- let content = session.content || ''
262
-
263
- if (!content && session.event?.message?.elements) {
264
- content = this.utils.extractTextContent(session.event.message.elements).trim()
265
- }
266
-
267
- let quoteInfo: QuoteInfo | undefined = undefined
268
- const quoteMatch = content.match(/<quote id="([^"]+)"\/>/)
269
- if (quoteMatch) {
270
- const quoteId = quoteMatch[1]
271
- const quotedMsg = await this.fileManager.findChannelMessageById(session.selfId, finalChannelId, quoteId)
272
-
273
- if (quotedMsg) {
274
- const realId = quotedMsg.id.startsWith('bot-msg-') ? quotedMsg.realId : quotedMsg.id
275
-
276
- quoteInfo = {
277
- messageId: realId || quotedMsg.id,
278
- id: realId || quotedMsg.id,
279
- content: quotedMsg.content,
280
- elements: quotedMsg.elements,
281
- user: {
282
- id: quotedMsg.userId,
283
- name: quotedMsg.username,
284
- userId: quotedMsg.userId,
285
- avatar: quotedMsg.avatar,
286
- username: quotedMsg.username
287
- },
288
- timestamp: quotedMsg.timestamp
289
- }
290
- content = content.replace(/<quote id="[^"]+"\/>\s*/, '')
291
-
292
- if (realId) {
293
- session.content = session.content.replace(/id="[^"]+"/, `id="${realId}"`)
294
- }
295
- }
296
- }
297
-
298
- // 创建机器人消息信息对象
299
- const messageInfo: MessageInfo = {
300
- id: `bot-msg-${timestamp}`,
301
- content: content,
302
- userId: session.selfId,
303
- username: session.bot.user?.name || `Bot-${session.selfId}`,
304
- avatar: session.bot.user?.avatar,
305
- timestamp: timestamp,
306
- channelId: finalChannelId,
307
- selfId: session.selfId,
308
- elements: await this.utils.cleanBase64ContentAsync(session.event?.message?.elements, true),
309
- type: 'bot',
310
- guildName: guildName,
311
- platform: session.platform || 'unknown',
312
- quote: quoteInfo,
313
- isDirect: !!isDirect,
314
- sending: true // 标记为正在发送
315
- }
316
-
317
- // 异步保存消息(不阻塞)
318
- await this.fileManager.addMessageToFile(messageInfo)
319
-
320
- const eventElements = await this.utils.cleanBase64ContentAsync(session.event?.message?.elements, true)
321
-
322
- const messageEvent = {
323
- type: 'bot-message',
324
- selfId: session.selfId,
325
- platform: session.platform || 'unknown',
326
- channelId: finalChannelId,
327
- messageId: `bot-msg-${timestamp}`,
328
- content: content,
329
- userId: session.selfId,
330
- username: session.bot.user?.name || `Bot-${session.selfId}`,
331
- avatar: session.bot.user?.avatar,
332
- timestamp: timestamp,
333
- guildName: guildName,
334
- channelType: session.event?.channel?.type || session.type || 0,
335
- elements: eventElements,
336
- quote: quoteInfo,
337
- isDirect: !!isDirect,
338
- sending: true,
339
- bot: {
340
- avatar: session.bot.user?.avatar,
341
- name: session.bot.user?.name,
342
- }
343
- }
344
-
345
- this.ctx.console.broadcast('chat-bot-message-event', messageEvent)
346
- } catch (error) {
347
- this.logger.error('处理机器人消息失败:', error)
348
- }
349
- }
350
-
351
- dispose() {
352
- void this.utils.dispose()
353
- for (const dispose of this.scheduledTasks) {
354
- dispose()
355
- }
356
- this.scheduledTasks.clear()
357
- }
358
-
359
- private scheduleTask(label: string, task: () => Promise<void>) {
360
- const dispose = this.ctx.setTimeout(() => {
361
- this.scheduledTasks.delete(dispose)
362
- void task().catch((error) => {
363
- this.logger.error(`${label}失败:`, error)
364
- })
365
- }, 0)
366
-
367
- this.scheduledTasks.add(dispose)
368
- }
369
-
370
- private shouldRefreshChannelInfo(
371
- channelKey: string,
372
- existingChannel: ChannelInfo | undefined,
373
- isDirect: boolean,
374
- channelId: string,
375
- directUserName?: string
376
- ) {
377
- if (this.channelRefreshInFlight.has(channelKey)) {
378
- return false
379
- }
380
-
381
- const lastRefresh = this.lastChannelRefreshAt.get(channelKey) || 0
382
- if (Date.now() - lastRefresh < this.CHANNEL_REFRESH_TTL_MS) {
383
- return false
384
- }
385
-
386
- if (isDirect) {
387
- return !directUserName && (!existingChannel || existingChannel.name.includes('未知'))
388
- }
389
-
390
- return !existingChannel?.guildName || existingChannel.guildName === channelId
391
- }
392
-
393
- private async refreshChannelInfo(
394
- session: Session,
395
- existingChannel: ChannelInfo | undefined,
396
- isDirect: boolean,
397
- directUserName?: string
398
- ) {
399
- const channelKey = `${session.selfId}:${session.channelId}`
400
- this.channelRefreshInFlight.add(channelKey)
401
-
402
- try {
403
- let guildName = existingChannel?.guildName || session.channelId
404
-
405
- if (!isDirect) {
406
- guildName = await this.resolveGuildName(session)
407
- }
408
-
409
- const finalName = this.buildChannelName(session, existingChannel, isDirect, directUserName, guildName)
410
- const channelInfo: ChannelInfo = {
411
- id: session.channelId,
412
- name: finalName,
413
- type: session.type || 0,
414
- channelId: session.channelId,
415
- guildName,
416
- isDirect: !!isDirect
417
- }
418
-
419
- await this.fileManager.upsertChannelInfo(session.selfId, session.channelId, channelInfo)
420
- this.lastChannelRefreshAt.set(channelKey, Date.now())
421
- this.logger.logInfo('更新频道信息到文件:', channelInfo.name)
422
- } finally {
423
- this.channelRefreshInFlight.delete(channelKey)
424
- }
425
- }
426
-
427
- private async resolveGuildName(session: Session): Promise<string> {
428
- try {
429
- if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === 'function') {
430
- const guild = await session.bot.getGuild(session.guildId)
431
- return guild?.name || session.channelId
432
- }
433
-
434
- if (session.guildId && session.bot.getChannel && typeof session.bot.getChannel === 'function') {
435
- const channel = await session.bot.getChannel(session.guildId)
436
- return channel?.name || session.channelId
437
- }
438
- } catch (error) {
439
- this.logger.logInfo('获取频道信息失败,使用频道ID作为备用:', error)
440
- }
441
-
442
- return session.channelId
443
- }
444
-
445
- private buildChannelName(
446
- session: Session,
447
- existingChannel: ChannelInfo | undefined,
448
- isDirect: boolean,
449
- directUserName: string | undefined,
450
- guildName: string
451
- ) {
452
- if (isDirect) {
453
- if (directUserName && directUserName !== session.userId) {
454
- return `私聊(${directUserName})`
455
- }
456
-
457
- if (existingChannel?.name && !existingChannel.name.includes('未知')) {
458
- return existingChannel.name
459
- }
460
-
461
- if (session.platform && session.platform.toLowerCase().includes('sandbox')) {
462
- return `私聊(${session.userId})`
463
- }
464
-
465
- return '私聊(未知用户)'
466
- }
467
-
468
- return guildName || session.channelId
469
- }
470
-
471
- private async processMediaElements(elements: h[], isUserMessage: boolean) {
472
- for (const el of elements) {
473
- if (['image', 'img', 'mface'].includes(el.type)) {
474
- const src = el.attrs.src || el.attrs.url || el.attrs.file
475
- if (src && isUserMessage) {
476
- try {
477
- await this.downloadAndCacheMedia(src, 'image')
478
- } catch (error) {
479
- this.logger.warn('缓存图片失败:', error)
480
- }
481
- }
482
- } else if (el.type === 'audio') {
483
- const src = el.attrs.src || el.attrs.url || el.attrs.file
484
- if (src && isUserMessage) {
485
- try {
486
- await this.downloadAndCacheMedia(src, 'media')
487
- } catch (error) {
488
- this.logger.warn('缓存语音失败:', error)
489
- }
490
- }
491
- }
492
-
493
- if (el.children?.length) {
494
- await this.processMediaElements(el.children, isUserMessage)
495
- }
496
- }
497
- }
498
-
499
- private async fileExists(filePath: string) {
500
- try {
501
- await fs.access(filePath)
502
- return true
503
- } catch {
504
- return false
505
- }
506
- }
507
- }
package/src/utils.ts DELETED
@@ -1,189 +0,0 @@
1
- import { Config } from './config'
2
- import { Context } from 'koishi'
3
- import { join } from 'node:path'
4
- import { createHash } from 'node:crypto'
5
- import { pathToFileURL } from 'node:url'
6
- import { promises as fs } from 'node:fs'
7
-
8
- export class Utils {
9
- private persistImageWriteCount = 0
10
-
11
- private pendingTasks: Set<Promise<void>> = new Set()
12
-
13
- constructor(private config: Config, private ctx?: Context) { }
14
-
15
- isPlatformBlocked(platform: string): boolean {
16
- if (!this.config.blockedPlatforms || this.config.blockedPlatforms.length === 0) {
17
- return false
18
- }
19
-
20
- for (const blockedPlatform of this.config.blockedPlatforms) {
21
- if (blockedPlatform.exactMatch) {
22
- if (platform === blockedPlatform.platformName) {
23
- return true
24
- }
25
- } else {
26
- if (platform.includes(blockedPlatform.platformName)) {
27
- return true
28
- }
29
- }
30
- }
31
-
32
- return false
33
- }
34
-
35
- extractTextContent(elements: any[]): string {
36
- let text = ''
37
- for (const element of elements) {
38
- if (element.type === 'text') {
39
- text += element.attrs?.content || ''
40
- } else if (element.type === 'p') {
41
- if (element.children && element.children.length > 0) {
42
- text += this.extractTextContent(element.children) + '\n'
43
- }
44
- } else if (element.children && element.children.length > 0) {
45
- text += this.extractTextContent(element.children)
46
- }
47
- }
48
- return text
49
- }
50
-
51
- private isBase64(str: string): boolean {
52
- if (!str || typeof str !== 'string') return false
53
-
54
- if (str.startsWith('data:')) {
55
- return str.includes('base64,')
56
- }
57
-
58
- if (str.length > 100) {
59
- const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/
60
- return base64Regex.test(str)
61
- }
62
-
63
- return false
64
- }
65
-
66
- async persistBase64ImageAsync(base64Data: string): Promise<string> {
67
- if (!this.ctx || !base64Data.startsWith('data:image/')) return base64Data
68
-
69
- try {
70
- const dir = join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-images')
71
- await fs.mkdir(dir, { recursive: true })
72
-
73
- const hash = createHash('md5').update(base64Data).digest('hex')
74
- const ext = base64Data.split(';')[0].split('/')[1] || 'png'
75
- const filename = `${hash}.${ext}`
76
- const filePath = join(dir, filename)
77
-
78
- if (!(await this.fileExists(filePath))) {
79
- const base64Content = base64Data.split(',')[1]
80
- await fs.writeFile(filePath, Buffer.from(base64Content, 'base64'))
81
- }
82
-
83
- this.persistImageWriteCount += 1
84
- if (this.persistImageWriteCount % 20 === 0) {
85
- this.trackTask(this.cleanupPersistImagesAsync(dir))
86
- }
87
-
88
- return pathToFileURL(filePath).href
89
- } catch {
90
- return base64Data
91
- }
92
- }
93
-
94
- private async cleanupPersistImagesAsync(dir: string) {
95
- try {
96
- const fileNames = await fs.readdir(dir)
97
- const files = await Promise.all(fileNames.map(async (name) => {
98
- const filePath = join(dir, name)
99
- const stats = await fs.stat(filePath)
100
- return { path: filePath, mtime: stats.mtimeMs }
101
- }))
102
-
103
- files.sort((a, b) => b.mtime - a.mtime)
104
-
105
- for (const file of files.slice(this.config.maxPersistImages)) {
106
- try {
107
- await fs.unlink(file.path)
108
- } catch {
109
- continue
110
- }
111
- }
112
- } catch { }
113
- }
114
-
115
- async cleanBase64ContentAsync<T>(obj: T, isBotMessage: boolean = false): Promise<T> {
116
- if (obj === null || obj === undefined) {
117
- return obj
118
- }
119
-
120
- if (typeof obj === 'string') {
121
- if (this.isBase64(obj)) {
122
- if (isBotMessage && !obj.startsWith('data:image/')) {
123
- return '[富媒体内容已省略]' as T
124
- }
125
- return await this.persistBase64ImageAsync(obj) as T
126
- }
127
- return obj
128
- }
129
-
130
- if (Array.isArray(obj)) {
131
- const cleanedItems = await Promise.all(obj.map(item => this.cleanBase64ContentAsync(item, isBotMessage)))
132
- return cleanedItems as T
133
- }
134
-
135
- if (typeof obj === 'object') {
136
- const cleaned: Record<string, unknown> = {}
137
- const source = obj as Record<string, unknown>
138
-
139
- for (const [key, value] of Object.entries(source)) {
140
- const type = typeof source.type === 'string' ? source.type : undefined
141
-
142
- if (isBotMessage && type && !['text', 'image', 'img'].includes(type)) {
143
- if (typeof value === 'string' && (key === 'src' || key === 'url' || key === 'file') && this.isBase64(value)) {
144
- cleaned[key] = '[富媒体内容已省略]'
145
- continue
146
- }
147
- }
148
-
149
- if (
150
- typeof value === 'string'
151
- && (key === 'src' || key === 'url' || key === 'file' || key === 'data' || key === 'content')
152
- && this.isBase64(value)
153
- ) {
154
- if (isBotMessage && !value.startsWith('data:image/')) {
155
- cleaned[key] = '[富媒体内容已省略]'
156
- } else {
157
- cleaned[key] = await this.persistBase64ImageAsync(value)
158
- }
159
- } else {
160
- cleaned[key] = await this.cleanBase64ContentAsync(value, isBotMessage)
161
- }
162
- }
163
-
164
- return cleaned as T
165
- }
166
-
167
- return obj
168
- }
169
-
170
- async dispose() {
171
- await Promise.allSettled([...this.pendingTasks])
172
- }
173
-
174
- private trackTask(task: Promise<void>) {
175
- this.pendingTasks.add(task)
176
- void task.finally(() => {
177
- this.pendingTasks.delete(task)
178
- })
179
- }
180
-
181
- private async fileExists(filePath: string) {
182
- try {
183
- await fs.access(filePath)
184
- return true
185
- } catch {
186
- return false
187
- }
188
- }
189
- }