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
@@ -0,0 +1,456 @@
1
+ import { Bot, Context, h, Session } from 'koishi'
2
+ import { createHash, randomUUID } from 'node:crypto'
3
+
4
+ import { Config } from './config'
5
+ import { ChatDatabase } from './database'
6
+ import { PluginLogger } from './logger'
7
+ import { SelfMessageChannelType, SelfMessageRecord, SelfMessageSource } from './types'
8
+
9
+ type MessageContent = Parameters<Bot['sendMessage']>[1]
10
+ type SendOptions = NonNullable<Parameters<Bot['sendMessage']>[3]>
11
+ type SendOptionsWithMeta = SendOptions & {
12
+ chatPatchId?: string
13
+ }
14
+ type BotSendMethod = Bot['sendMessage']
15
+ type BotCreateMethod = Bot['createMessage']
16
+ type BotPrivateMethod = Bot['sendPrivateMessage']
17
+
18
+ interface BotMethods {
19
+ createMessage: BotCreateMethod
20
+ sendMessage: BotSendMethod
21
+ sendPrivateMessage: BotPrivateMethod
22
+ }
23
+
24
+ interface SatoriElement {
25
+ type?: unknown
26
+ attrs?: Record<string, unknown>
27
+ children?: SatoriElement[]
28
+ }
29
+
30
+ const KOISHI_STATUS_I18N_FALLBACK: Record<string, string> = {
31
+ 'commands.status.messages.status.0': '离线',
32
+ 'commands.status.messages.status.1': '运行中',
33
+ 'commands.status.messages.status.2': '连接中',
34
+ 'commands.status.messages.status.3': '异常',
35
+ }
36
+
37
+ function getString(value: unknown): string {
38
+ return typeof value === 'string' ? value : ''
39
+ }
40
+
41
+ function getNumber(value: unknown): number {
42
+ const num = Number(value)
43
+ return Number.isFinite(num) ? num : 0
44
+ }
45
+
46
+ function getObject(value: unknown): Record<string, unknown> {
47
+ return typeof value === 'object' && value !== null
48
+ ? value as Record<string, unknown>
49
+ : {}
50
+ }
51
+
52
+ function toSatoriElement(value: unknown): SatoriElement {
53
+ const raw = getObject(value)
54
+ const attrs = typeof raw.attrs === 'object' && raw.attrs !== null
55
+ ? raw.attrs as Record<string, unknown>
56
+ : {}
57
+ const children = Array.isArray(raw.children)
58
+ ? raw.children.map(toSatoriElement)
59
+ : []
60
+ return {
61
+ type: raw.type,
62
+ attrs,
63
+ children,
64
+ }
65
+ }
66
+
67
+ function isForwardContainer(element: SatoriElement): boolean {
68
+ return element.type === 'figure'
69
+ || element.type === 'forward'
70
+ || (element.type === 'message'
71
+ && ('forward' in (element.attrs ?? {}) || element.attrs?.forward === true))
72
+ }
73
+
74
+ function isPrivateChannel(value: string): boolean {
75
+ return /^(?:private|direct):/i.test(value)
76
+ }
77
+
78
+ function toSegments(
79
+ elements: unknown[],
80
+ resolveI18n?: (attrs: Record<string, unknown>) => string,
81
+ ): unknown[] {
82
+ const result: unknown[] = []
83
+ for (const raw of elements) {
84
+ const element = toSatoriElement(raw)
85
+ const attrs = element.attrs ?? {}
86
+ const type = getString(element.type)
87
+ if (type === 'text') {
88
+ result.push({ type: 'text', text: getString(attrs.content) })
89
+ } else if (type === 'at') {
90
+ const id = getString(attrs.id)
91
+ const name = getString(attrs.name) || (attrs.type === 'all' ? '所有人' : id)
92
+ result.push({ type: 'at', qq: id, text: name.startsWith('@') ? name : `@${name}` })
93
+ } else if (type === 'img' || type === 'image') {
94
+ const src = getString(attrs.src) || getString(attrs.url) || getString(attrs.file)
95
+ result.push({ type: 'image', file: src, url: src, summary: getString(attrs.title) })
96
+ } else if (type === 'audio' || type === 'record') {
97
+ const src = getString(attrs.src) || getString(attrs.url) || getString(attrs.file)
98
+ result.push({ type: 'record', file: src, url: src })
99
+ } else if (type === 'video') {
100
+ const src = getString(attrs.src) || getString(attrs.url) || getString(attrs.file)
101
+ result.push({ type: 'video', file: src, url: src })
102
+ } else if (type === 'file') {
103
+ const src = getString(attrs.src) || getString(attrs.url) || getString(attrs.file)
104
+ result.push({ type: 'file', file: src, url: src, name: getString(attrs.name) })
105
+ } else if (type === 'quote') {
106
+ result.push({ type: 'reply', id: getString(attrs.id) })
107
+ } else if (type === 'json') {
108
+ result.push({ type: 'json', data: getString(attrs.data) || getString(attrs.content) })
109
+ } else if (type === 'xml') {
110
+ result.push({ type: 'xml', data: getString(attrs.data) || getString(attrs.content) })
111
+ } else if (type === 'markdown') {
112
+ result.push({ type: 'markdown', content: getString(attrs.content) })
113
+ } else if (isForwardContainer(element)) {
114
+ result.push({
115
+ type: 'forward',
116
+ id: getString(attrs.id),
117
+ content: toForwardNodes(element.children ?? [], resolveI18n) ?? [],
118
+ })
119
+ } else if (type === 'p') {
120
+ result.push(...toSegments(element.children ?? [], resolveI18n))
121
+ result.push({ type: 'text', text: '\n' })
122
+ } else if (type === 'br') {
123
+ result.push({ type: 'text', text: '\n' })
124
+ } else if (type === 'i18n') {
125
+ const path = getString(attrs.path)
126
+ const resolved = resolveI18n ? resolveI18n(attrs) : ''
127
+ result.push({ type: 'text', text: resolved || `[${path || 'i18n'}]` })
128
+ } else if (element.children?.length) {
129
+ result.push(...toSegments(element.children, resolveI18n))
130
+ } else if (type && type !== 'author' && type !== 'button' && type !== 'keyboard') {
131
+ result.push({ type: 'text', text: `[${type}]` })
132
+ }
133
+ }
134
+ return result
135
+ }
136
+
137
+ // 将 Satori figure/message 转换为前端可直接渲染的合并转发节点
138
+ function toForwardNodes(
139
+ elements: unknown[],
140
+ resolveI18n?: (attrs: Record<string, unknown>) => string,
141
+ ): unknown[] | undefined {
142
+ const nodes: unknown[] = []
143
+ const collect = (list: SatoriElement[]) => {
144
+ for (const element of list) {
145
+ if (element.type === 'figure' || element.type === 'forward') {
146
+ collect(element.children ?? [])
147
+ continue
148
+ }
149
+ if (element.type !== 'message') continue
150
+ if (isForwardContainer(element)) {
151
+ collect(element.children ?? [])
152
+ continue
153
+ }
154
+ const children = element.children ?? []
155
+ const author = children.find((item) => item.type === 'author')
156
+ const attrs = element.attrs ?? {}
157
+ const authorAttrs = author?.attrs ?? {}
158
+ const content = children.filter((item) => item.type !== 'author')
159
+ nodes.push({
160
+ message_id: getString(attrs.id),
161
+ time: getNumber(authorAttrs.time) || getNumber(attrs.time),
162
+ sender: {
163
+ user_id: getString(authorAttrs.id),
164
+ nickname: getString(authorAttrs.name),
165
+ avatar: getString(authorAttrs.avatar),
166
+ },
167
+ message: toSegments(content, resolveI18n),
168
+ })
169
+ }
170
+ }
171
+ collect(elements.map(toSatoriElement))
172
+ return nodes.length > 0 ? nodes : undefined
173
+ }
174
+
175
+ function detectKind(elements: unknown[]): string {
176
+ const stack = [...elements.map(toSatoriElement)]
177
+ while (stack.length) {
178
+ const element = stack.pop()
179
+ if (!element) continue
180
+ const type = getString(element.type)
181
+ if (isForwardContainer(element)) return 'forward'
182
+ if (type === 'img' || type === 'image' || type === 'mface') return 'image'
183
+ if (type === 'audio' || type === 'record') return 'voice'
184
+ if (type === 'video') return 'video'
185
+ if (type === 'file') return 'file'
186
+ stack.push(...(element.children ?? []))
187
+ }
188
+ return 'text'
189
+ }
190
+
191
+ function forwardId(elements: unknown[]): string | undefined {
192
+ const stack = [...elements.map(toSatoriElement)]
193
+ while (stack.length) {
194
+ const element = stack.pop()
195
+ if (!element) continue
196
+ if (isForwardContainer(element)) {
197
+ const id = getString(element.attrs?.id)
198
+ if (id) return id
199
+ }
200
+ stack.push(...(element.children ?? []))
201
+ }
202
+ return undefined
203
+ }
204
+
205
+ export class SelfMessageRecorder {
206
+ private wrapped = new Map<Bot, BotMethods>()
207
+ private sequence = 0
208
+
209
+ constructor(
210
+ private ctx: Context,
211
+ private config: Config,
212
+ private database: ChatDatabase,
213
+ private logger: PluginLogger,
214
+ ) {}
215
+
216
+ start() {
217
+ for (const bot of this.ctx.bots) this.wrapBot(bot)
218
+ this.ctx.on('bot-added', (bot) => this.wrapBot(bot))
219
+ this.ctx.on('bot-removed', (bot) => this.unwrapBot(bot))
220
+ this.ctx.on('send', (session) => {
221
+ void this.recordSendEvent(session).catch((error) => {
222
+ this.logger.warn('记录 send 事件失败:', error)
223
+ })
224
+ })
225
+ }
226
+
227
+ dispose() {
228
+ for (const bot of [...this.wrapped.keys()]) {
229
+ this.unwrapBot(bot)
230
+ }
231
+ }
232
+
233
+ private isBlocked(platform: string): boolean {
234
+ return (this.config.blockedPlatforms ?? []).some((item) => {
235
+ return item.exactMatch
236
+ ? platform === item.platformName
237
+ : platform.includes(item.platformName)
238
+ })
239
+ }
240
+
241
+ // 包装三个发送入口,覆盖 Satori message.create 和插件直接发送
242
+ private wrapBot(bot: Bot) {
243
+ if (this.wrapped.has(bot)) return
244
+ const methods: BotMethods = {
245
+ createMessage: bot.createMessage,
246
+ sendMessage: bot.sendMessage,
247
+ sendPrivateMessage: bot.sendPrivateMessage,
248
+ }
249
+ this.wrapped.set(bot, methods)
250
+
251
+ bot.createMessage = async (channelId, content, referrer, options) => {
252
+ const sendOptions = this.prepareOptions(options)
253
+ if (sendOptions.chatPatchId) {
254
+ return methods.createMessage.call(bot, channelId, content, referrer, sendOptions)
255
+ }
256
+ const localId = randomUUID()
257
+ sendOptions.chatPatchId = localId
258
+ try {
259
+ const messages = await methods.createMessage.call(bot, channelId, content, referrer, sendOptions)
260
+ const ids = Array.isArray(messages)
261
+ ? messages.map((item) => getString(getObject(item).id)).filter(Boolean)
262
+ : []
263
+ await this.recordFromWrapper(bot, channelId, content, ids, options, 'create', localId)
264
+ return messages
265
+ } finally {
266
+ delete sendOptions.chatPatchId
267
+ }
268
+ }
269
+
270
+ bot.sendMessage = async (channelId, content, referrer, options) => {
271
+ const sendOptions = this.prepareOptions(options)
272
+ if (sendOptions.chatPatchId) {
273
+ return methods.sendMessage.call(bot, channelId, content, referrer, sendOptions)
274
+ }
275
+ const localId = randomUUID()
276
+ sendOptions.chatPatchId = localId
277
+ try {
278
+ const ids = await methods.sendMessage.call(bot, channelId, content, referrer, sendOptions)
279
+ await this.recordFromWrapper(bot, channelId, content, ids, options, 'send', localId)
280
+ return ids
281
+ } finally {
282
+ delete sendOptions.chatPatchId
283
+ }
284
+ }
285
+
286
+ bot.sendPrivateMessage = async (userId, content, guildId, options) => {
287
+ const sendOptions = this.prepareOptions(options)
288
+ if (sendOptions.chatPatchId) {
289
+ return methods.sendPrivateMessage.call(bot, userId, content, guildId, sendOptions)
290
+ }
291
+ const localId = randomUUID()
292
+ sendOptions.chatPatchId = localId
293
+ try {
294
+ const ids = await methods.sendPrivateMessage.call(bot, userId, content, guildId, sendOptions)
295
+ await this.recordFromWrapper(bot, userId, content, ids, options, 'private', localId)
296
+ return ids
297
+ } finally {
298
+ delete sendOptions.chatPatchId
299
+ }
300
+ }
301
+ }
302
+
303
+ private unwrapBot(bot: Bot) {
304
+ const methods = this.wrapped.get(bot)
305
+ if (!methods) return
306
+ bot.createMessage = methods.createMessage
307
+ bot.sendMessage = methods.sendMessage
308
+ bot.sendPrivateMessage = methods.sendPrivateMessage
309
+ this.wrapped.delete(bot)
310
+ }
311
+
312
+ private prepareOptions(options?: SendOptions): SendOptionsWithMeta {
313
+ return options ?? {} as SendOptionsWithMeta
314
+ }
315
+
316
+ private async recordFromWrapper(
317
+ bot: Bot,
318
+ channelId: string,
319
+ content: MessageContent,
320
+ ids: string[],
321
+ options: SendOptions | undefined,
322
+ mode: 'create' | 'send' | 'private',
323
+ localId: string,
324
+ ) {
325
+ if (!channelId || this.isBlocked(bot.platform ?? '')) return
326
+ const source: SelfMessageSource = options?.session ? 'plugin' : 'bot'
327
+ const record = this.buildRecord(
328
+ bot.platform ?? '',
329
+ bot.selfId,
330
+ channelId,
331
+ options?.session?.guildId,
332
+ content,
333
+ ids[0],
334
+ source,
335
+ mode,
336
+ localId,
337
+ )
338
+ await this.database.upsertSelfMessage(record)
339
+ this.logger.logInfo('机器人消息已记录:', record.kind, record.channelId, record.messageId || record.id)
340
+ }
341
+
342
+ // 收到 send 事件时只补消息 id,避免重复记录
343
+ private async recordSendEvent(session: Session) {
344
+ if (session.type !== 'send') return
345
+ const platform = session.platform || ''
346
+ const selfId = session.selfId || ''
347
+ const event = getObject(session.event)
348
+ const eventChannel = getObject(event.channel)
349
+ const eventMessage = getObject(event.message)
350
+ const channelId = session.channelId || getString(eventChannel.id)
351
+ if (this.isBlocked(platform) || !platform || !selfId || !channelId) return
352
+
353
+ const messageId = session.messageId || getString(eventMessage.id)
354
+ if (messageId) {
355
+ const updated = await this.database.updateSelfMessageByMessageId(
356
+ platform,
357
+ selfId,
358
+ channelId,
359
+ messageId,
360
+ { messageId },
361
+ )
362
+ if (updated) return
363
+ } else {
364
+ return
365
+ }
366
+
367
+ const record = this.buildRecord(
368
+ platform,
369
+ selfId,
370
+ channelId,
371
+ session.guildId,
372
+ session.content ?? '',
373
+ messageId,
374
+ 'bot',
375
+ 'send',
376
+ randomUUID(),
377
+ )
378
+ await this.database.upsertSelfMessage(record)
379
+ }
380
+
381
+ private resolveI18nElement(attrs: Record<string, unknown>): string {
382
+ const path = getString(attrs.path)
383
+ if (!path || !this.ctx.i18n) return `[${path || 'i18n'}]`
384
+ try {
385
+ const locales = this.ctx.i18n.fallback([])
386
+ const text = this.ctx.i18n.render(locales, [path], attrs || {})
387
+ if (text && typeof text === 'string') return text
388
+ } catch {
389
+ // 使用 path 原文兜底,避免 i18n 解析失败时整段丢失
390
+ }
391
+ const knownFallback = KOISHI_STATUS_I18N_FALLBACK[path]
392
+ if (knownFallback) return knownFallback
393
+ return `[${path}]`
394
+ }
395
+
396
+ private buildRecord(
397
+ platform: string,
398
+ selfId: string,
399
+ channelId: string,
400
+ guildId: string | undefined,
401
+ content: MessageContent,
402
+ messageId: string | undefined,
403
+ source: SelfMessageSource,
404
+ mode: 'create' | 'send' | 'private',
405
+ localId: string,
406
+ ): SelfMessageRecord {
407
+ const normalizedChannelId = mode === 'private' && !isPrivateChannel(channelId)
408
+ ? `private:${channelId}`
409
+ : channelId
410
+ const channelType: SelfMessageChannelType = mode === 'private' || isPrivateChannel(normalizedChannelId)
411
+ ? 'user'
412
+ : 'group'
413
+ const elements = this.normalizeElements(content)
414
+ const contentText = typeof content === 'string'
415
+ ? content
416
+ : h.toElementArray(content).join('')
417
+ const fingerprint = createHash('sha256')
418
+ .update([
419
+ platform,
420
+ selfId,
421
+ normalizedChannelId,
422
+ JSON.stringify(elements),
423
+ ].join('\u0000'))
424
+ .digest('hex')
425
+ return {
426
+ id: `self-${localId}`,
427
+ platform,
428
+ selfId,
429
+ channelId: normalizedChannelId,
430
+ guildId,
431
+ channelType,
432
+ messageId,
433
+ content: contentText,
434
+ elements: JSON.parse(JSON.stringify(elements)) as unknown[],
435
+ message: toSegments(elements, (attrs) => this.resolveI18nElement(attrs)),
436
+ forwardId: forwardId(elements),
437
+ forwardContent: toForwardNodes(elements, (attrs) => this.resolveI18nElement(attrs)),
438
+ sentAt: Date.now(),
439
+ sequence: ++this.sequence,
440
+ source,
441
+ kind: detectKind(elements),
442
+ fingerprint,
443
+ }
444
+ }
445
+
446
+ private normalizeElements(content: MessageContent): unknown[] {
447
+ try {
448
+ const elements = typeof content === 'string'
449
+ ? h.parse(content)
450
+ : h.toElementArray(content)
451
+ return JSON.parse(JSON.stringify(elements)) as unknown[]
452
+ } catch {
453
+ return []
454
+ }
455
+ }
456
+ }
@@ -0,0 +1,26 @@
1
+ import type { DefaultContext, DefaultState, ParameterizedContext } from 'koa'
2
+
3
+ type ServerContext = ParameterizedContext<DefaultState, DefaultContext>
4
+
5
+ declare module '@satorijs/core' {
6
+ interface Satori {
7
+ server: {
8
+ url: string
9
+ config: {
10
+ token?: string
11
+ }
12
+ }
13
+ }
14
+ }
15
+
16
+ declare module 'cordis' {
17
+ interface Context {
18
+ server: {
19
+ selfUrl?: string
20
+ config?: {
21
+ selfUrl?: string
22
+ }
23
+ get: (path: string, ...handlers: Array<(context: ServerContext) => unknown>) => unknown
24
+ }
25
+ }
26
+ }
package/src/types.ts CHANGED
@@ -1,62 +1,122 @@
1
- import { h } from 'koishi'
1
+ import {} from '@koishijs/console'
2
+ import type { Awaitable } from 'koishi'
2
3
 
3
- export interface BotInfo {
4
- selfId: string
5
- platform: string
6
- username: string
7
- avatar?: string
8
- status: 'online' | 'offline'
4
+ export interface SatoriBootstrap {
5
+ endpoint: string
6
+ token: string
7
+ basePath: string
9
8
  }
10
9
 
11
- export interface ChannelInfo {
12
- id: string
13
- name: string
14
- type: number | string
10
+ export interface MessageRecord {
11
+ id?: string
12
+ sequence?: number
13
+ type: string
14
+ platform: string
15
+ selfId: string
15
16
  channelId?: string
16
- guildName?: string
17
- isDirect?: boolean
17
+ guildId?: string
18
+ userId?: string
19
+ timestamp: number
20
+ timestampMs?: number
21
+ receivedAt?: number
22
+ content?: string
23
+ elements?: unknown[]
24
+ raw?: unknown
18
25
  }
19
26
 
20
- export interface QuoteInfo {
21
- messageId: string
27
+ export type SelfMessageChannelType = 'group' | 'user'
28
+ export type SelfMessageSource = 'webui' | 'bot' | 'plugin'
29
+
30
+ export interface SelfMessageRecord {
22
31
  id: string
23
- content: string
24
- elements?: h[]
25
- user: {
26
- id: string
27
- name: string
28
- userId: string
29
- avatar?: string
30
- username: string
31
- }
32
- timestamp: number
32
+ platform: string
33
+ selfId: string
34
+ channelId: string
35
+ guildId?: string
36
+ channelType?: SelfMessageChannelType
37
+ messageId?: string
38
+ content?: string
39
+ elements?: unknown[]
40
+ message?: unknown[]
41
+ forwardId?: string
42
+ forwardContent?: unknown[]
43
+ sentAt: number
44
+ sequence: number
45
+ source: SelfMessageSource
46
+ kind: string
47
+ fingerprint?: string
33
48
  }
34
49
 
35
- export interface MessageInfo {
36
- id: string
37
- content: string
38
- userId: string
39
- username: string
40
- avatar?: string
41
- timestamp: number
50
+ export interface SelfMessagePayload {
51
+ id?: string
52
+ platform: string
53
+ selfId: string
42
54
  channelId: string
55
+ guildId?: string
56
+ channelType?: SelfMessageChannelType
57
+ messageId?: string
58
+ content?: string
59
+ elements?: unknown[]
60
+ message?: unknown[]
61
+ forwardId?: string
62
+ forwardContent?: unknown[]
63
+ sentAt?: number
64
+ sequence?: number
65
+ source?: SelfMessageSource
66
+ kind?: string
67
+ }
68
+
69
+ export interface HistoryQuery {
70
+ platform: string
43
71
  selfId: string
44
- elements?: h[]
45
- type: 'user' | 'bot'
72
+ channelId: string
73
+ limit?: number
74
+ }
75
+
76
+ export interface HistoryResult {
77
+ messages: MessageRecord[]
78
+ }
79
+
80
+ export interface PinnedState {
81
+ bots: string[]
82
+ channels: string[]
83
+ }
84
+
85
+ export interface PluginConfigPayload {
86
+ basePath: string
87
+ maxMessagesPerChannel: number
88
+ historyPageSize: number
89
+ maxMediaFiles: number
90
+ blockedPlatforms: Array<{ platformName: string; exactMatch: boolean }>
91
+ loggerinfo: boolean
92
+ }
93
+
94
+ export interface ContactCacheItem {
95
+ id: string
96
+ name: string
97
+ avatar?: string
98
+ channelId?: string
46
99
  guildId?: string
47
- guildName?: string
100
+ raw?: unknown
101
+ }
102
+
103
+ export interface ContactCacheQuery {
48
104
  platform: string
49
- quote?: QuoteInfo
50
- isDirect?: boolean
51
- sending?: boolean
52
- realId?: string
53
- }
54
-
55
- export interface ChatData {
56
- bots: Record<string, BotInfo>
57
- channels: Record<string, Record<string, ChannelInfo>>
58
- messages: Record<string, MessageInfo[]>
59
- pinnedBots: string[]
60
- pinnedChannels: string[]
61
- lastSaveTime?: number
105
+ selfId: string
106
+ type: string
107
+ contacts?: ContactCacheItem[]
108
+ append?: boolean
109
+ }
110
+
111
+ export interface ContactCacheResult {
112
+ contacts?: ContactCacheItem[]
113
+ }
114
+
115
+ declare module '@koishijs/console' {
116
+ interface Events {
117
+ 'chat-patch/bootstrap'(): SatoriBootstrap
118
+ 'chat-patch/history'(query: HistoryQuery): Awaitable<HistoryResult>
119
+ 'chat-patch/config'(): PluginConfigPayload
120
+ 'chat-patch/contact-cache'(query: ContactCacheQuery): Awaitable<ContactCacheResult>
121
+ }
62
122
  }