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,818 @@
1
+ // Satori 协议数据与 Stapxs UI 数据模型之间的转换层。
2
+
3
+ import { buildQqGroupAvatar } from './utils/avatarUtil'
4
+
5
+ interface SatoriElement {
6
+ type?: unknown
7
+ attrs?: Record<string, unknown>
8
+ children?: SatoriElement[]
9
+ }
10
+
11
+ interface SatoriObject {
12
+ [key: string]: unknown
13
+ }
14
+
15
+ function getObject(value: unknown): SatoriObject {
16
+ return typeof value === 'object' && value !== null ? value as SatoriObject : {}
17
+ }
18
+
19
+ function getString(value: unknown): string {
20
+ return typeof value === 'string' ? value : ''
21
+ }
22
+
23
+ function normalizeGroupId(value: string): string {
24
+ const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, '').trim()
25
+ const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/)
26
+ return wrapped ? wrapped[1] : raw || value
27
+ }
28
+
29
+ function isDirectChannelType(channel: SatoriObject): boolean {
30
+ const raw = channel.type
31
+ const num = Number(raw)
32
+ if (Number.isFinite(num)) return num === 1
33
+ const text = String(raw ?? '').toLowerCase()
34
+ return text === 'direct' || text === 'private'
35
+ }
36
+
37
+ function isGroupChannelType(channel: SatoriObject): boolean {
38
+ const raw = channel.type
39
+ const num = Number(raw)
40
+ if (Number.isFinite(num)) return num === 0
41
+ const text = String(raw ?? '').toLowerCase()
42
+ return ['text', 'group', 'room', 'chat', 'channel'].includes(text)
43
+ }
44
+
45
+ function isGroupChannel(channel: SatoriObject, guild: SatoriObject): boolean {
46
+ if (isDirectChannelType(channel)) return false
47
+ if (isGroupChannelType(channel)) return true
48
+ return Boolean(getString(guild.id))
49
+ }
50
+
51
+ function usableAvatar(value: unknown): string | undefined {
52
+ const avatar = getString(value)
53
+ if (!avatar || avatar === '/img/icons/icon.svg') return undefined
54
+ if (avatar.includes('/proxy/chatfile') || avatar.includes('proxy/chatfile')) {
55
+ const match = avatar.match(/[?&]url=([^&#]*)/)
56
+ if (!match?.[1]) return undefined
57
+ }
58
+ return avatar
59
+ }
60
+
61
+ function getNumber(value: unknown): number {
62
+ const num = Number(value)
63
+ return Number.isFinite(num) ? num : 0
64
+ }
65
+
66
+ function normalizeTimestampMs(value: unknown): number {
67
+ const num = getNumber(value)
68
+ if (!num) return Date.now()
69
+ return num < 1e12 ? num * 1000 : num
70
+ }
71
+
72
+ function normalizeResourceSrc(src: string, type: string): string {
73
+ if (!src.startsWith('base64://')) return src
74
+ const mime = type === 'image'
75
+ ? 'image/png'
76
+ : type === 'video'
77
+ ? 'video/mp4'
78
+ : type === 'audio'
79
+ ? 'audio/mp3'
80
+ : 'application/octet-stream'
81
+ return `data:${mime};base64,${src.slice(9)}`
82
+ }
83
+
84
+ function decodeEntities(value: string): string {
85
+ return value
86
+ .replace(/&lt;/g, '<')
87
+ .replace(/&gt;/g, '>')
88
+ .replace(/&quot;/g, '"')
89
+ .replace(/&#39;/g, "'")
90
+ .replace(/&amp;/g, '&')
91
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16)))
92
+ .replace(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(Number(code)))
93
+ }
94
+
95
+ function parseTagAttrs(source: string): Record<string, string> {
96
+ const attrs: Record<string, string> = {}
97
+ const pattern = /([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g
98
+ let match: RegExpExecArray | null
99
+ while ((match = pattern.exec(source)) !== null) {
100
+ const value = match[2] ?? match[3] ?? match[4] ?? ''
101
+ attrs[match[1]] = decodeEntities(value)
102
+ }
103
+ return attrs
104
+ }
105
+
106
+ function findClosingTag(source: string, start: number, tagName: string): number {
107
+ const pattern = new RegExp(`<(/?)${tagName}(?:\\s|/|>)`, 'g')
108
+ pattern.lastIndex = start
109
+ let depth = 1
110
+ while (true) {
111
+ const match = pattern.exec(source)
112
+ if (!match) return -1
113
+ const tagEnd = source.indexOf('>', match.index)
114
+ if (match[1]) {
115
+ depth -= 1
116
+ if (depth === 0) return match.index
117
+ } else if (tagEnd < 0 || source[tagEnd - 1] !== '/') {
118
+ depth += 1
119
+ }
120
+ }
121
+ }
122
+
123
+ function pushParsedTag(
124
+ tagName: string,
125
+ attrs: Record<string, string>,
126
+ children: Array<Record<string, unknown>>,
127
+ result: Array<Record<string, unknown>>,
128
+ ) {
129
+ const pushResource = (type: string, fallbackType?: string) => {
130
+ const src = normalizeResourceSrc(attrs.src || attrs.url || attrs.file || '', type)
131
+ result.push({ type, file: src, url: src, name: attrs.name, title: attrs.title })
132
+ }
133
+
134
+ switch (tagName) {
135
+ case 'p':
136
+ result.push(...children)
137
+ result.push({ type: 'text', text: '\n' })
138
+ break
139
+ case 'br':
140
+ result.push({ type: 'text', text: '\n' })
141
+ break
142
+ case 'i18n':
143
+ result.push({ type: 'i18n', path: attrs.path || '', params: attrs })
144
+ break
145
+ case 'at':
146
+ const atName = attrs.type === 'all' ? '所有人' : (attrs.name || attrs.id || '')
147
+ result.push({
148
+ type: 'at',
149
+ qq: attrs.id || '',
150
+ text: normalizeAtText(
151
+ atName,
152
+ attrs.id || '',
153
+ ),
154
+ })
155
+ break
156
+ case 'sharp':
157
+ result.push({ type: 'text', text: attrs.name || `#${attrs.id || ''}` })
158
+ break
159
+ case 'img':
160
+ case 'image':
161
+ pushResource('image')
162
+ break
163
+ case 'audio':
164
+ case 'record':
165
+ pushResource('record')
166
+ break
167
+ case 'video':
168
+ pushResource('video')
169
+ break
170
+ case 'file':
171
+ pushResource('file')
172
+ break
173
+ case 'mface':
174
+ case 'face': {
175
+ const childImage = children.find((child) => child.type === 'image')
176
+ const src = attrs.src || attrs.url || attrs.file || getString(childImage?.url)
177
+ if (src) {
178
+ result.push({ type: 'image', file: src, url: src, summary: attrs.name || attrs.title })
179
+ } else {
180
+ result.push({ type: 'face', id: attrs.id || '', text: attrs.name || '' })
181
+ }
182
+ break
183
+ }
184
+ case 'quote':
185
+ result.push({ type: 'reply', id: attrs.id || '' })
186
+ break
187
+ case 'message':
188
+ if ('forward' in attrs || attrs.forward) {
189
+ result.push({ type: 'forward', id: attrs.id || '', content: children })
190
+ } else {
191
+ result.push(...children)
192
+ }
193
+ break
194
+ case 'forward':
195
+ result.push({ type: 'forward', id: attrs.id || '', content: children })
196
+ break
197
+ case 'text':
198
+ result.push({ type: 'text', text: attrs.content || '' })
199
+ break
200
+ case 'a':
201
+ result.push({ type: 'text', text: attrs.href ? `${childrenText(children)} (${attrs.href})` : childrenText(children) })
202
+ break
203
+ case 'json':
204
+ result.push({ type: 'json', data: attrs.data || attrs.content || '' })
205
+ break
206
+ case 'xml':
207
+ result.push({ type: 'xml', data: attrs.data || attrs.content || '' })
208
+ break
209
+ case 'markdown':
210
+ result.push({ type: 'markdown', content: attrs.content || childrenText(children) })
211
+ break
212
+ case 'keyboard':
213
+ // 官方机器人按钮不渲染,只保留 Markdown 正文
214
+ break
215
+ default:
216
+ if (children.length > 0) {
217
+ result.push(...children)
218
+ } else if (tagName) {
219
+ result.push({ type: 'text', text: `[${tagName}]` })
220
+ }
221
+ }
222
+ }
223
+
224
+ function childrenText(children: Array<Record<string, unknown>>): string {
225
+ return children.map((item) => getString(item.text) || getString(item.path) || getString(item.summary) || getString(item.name)).join('')
226
+ }
227
+
228
+ function normalizeAtText(name: string, id: string): string {
229
+ if (!name) return id ? `@${id}` : '@'
230
+ return name.startsWith('@') ? name : `@${name}`
231
+ }
232
+
233
+ function containsElementMarkup(source: string): boolean {
234
+ return /<(img|image|audio|video|file|mface|face|quote|at|forward|json|xml|markdown|keyboard|p|br|i18n|a|text|sharp)(?:\s|\/|>)/i.test(source)
235
+ }
236
+
237
+ function splitMarkupTextSegments(
238
+ segments: Array<Record<string, unknown>>,
239
+ ): Array<Record<string, unknown>> {
240
+ const result: Array<Record<string, unknown>> = []
241
+ for (const segment of segments) {
242
+ const text = getString(segment.text)
243
+ if (segment.type === 'text' && containsElementMarkup(text)) {
244
+ result.push(...parseSatoriMarkup(text))
245
+ } else {
246
+ result.push(segment)
247
+ }
248
+ }
249
+ return result
250
+ }
251
+
252
+ export function parseSatoriMarkup(source: string): Array<Record<string, unknown>> {
253
+ const result: Array<Record<string, unknown>> = []
254
+ let index = 0
255
+ while (index < source.length) {
256
+ const open = source.indexOf('<', index)
257
+ if (open < 0) {
258
+ const text = source.slice(index)
259
+ if (text) result.push({ type: 'text', text: decodeEntities(text) })
260
+ break
261
+ }
262
+ if (open > index) {
263
+ const text = source.slice(index, open)
264
+ if (text) result.push({ type: 'text', text: decodeEntities(text) })
265
+ }
266
+ const close = source.indexOf('>', open + 1)
267
+ if (close < 0) {
268
+ const text = source.slice(index)
269
+ if (text) result.push({ type: 'text', text: decodeEntities(text) })
270
+ break
271
+ }
272
+ const raw = source.slice(open + 1, close).trim()
273
+ if (raw.startsWith('!--')) {
274
+ const commentEnd = source.indexOf('-->', close + 1)
275
+ index = commentEnd < 0 ? source.length : commentEnd + 3
276
+ continue
277
+ }
278
+ if (raw.startsWith('/')) {
279
+ index = close + 1
280
+ continue
281
+ }
282
+ const selfClosing = raw.endsWith('/')
283
+ const tagPart = selfClosing ? raw.slice(0, -1).trim() : raw
284
+ const spaceIndex = tagPart.search(/\s/)
285
+ const tagName = spaceIndex < 0 ? tagPart : tagPart.slice(0, spaceIndex)
286
+ const attrSource = spaceIndex < 0 ? '' : tagPart.slice(spaceIndex + 1)
287
+ const attrs = parseTagAttrs(attrSource)
288
+ if (selfClosing || tagName === 'img' || tagName === 'br') {
289
+ pushParsedTag(tagName, attrs, [], result)
290
+ index = close + 1
291
+ continue
292
+ }
293
+ const closingIndex = findClosingTag(source, close + 1, tagName)
294
+ if (closingIndex < 0) {
295
+ const children = parseSatoriMarkup(source.slice(close + 1))
296
+ pushParsedTag(tagName, attrs, children, result)
297
+ break
298
+ }
299
+ const inner = source.slice(close + 1, closingIndex)
300
+ const children = parseSatoriMarkup(inner)
301
+ pushParsedTag(tagName, attrs, children, result)
302
+ index = closingIndex + tagName.length + 3
303
+ }
304
+ return result
305
+ }
306
+
307
+ function messageSegments(message: SatoriObject): Array<Record<string, unknown>> {
308
+ const source = getString(message.content)
309
+ if (containsElementMarkup(source)) {
310
+ return parseSatoriMarkup(source)
311
+ }
312
+ const elements = message.elements
313
+ if (Array.isArray(elements) && elements.length > 0) {
314
+ return splitMarkupTextSegments(toSegments(elements))
315
+ }
316
+ return parseSatoriMarkup(source)
317
+ }
318
+
319
+ function toSegments(elements: unknown): Array<Record<string, unknown>> {
320
+ if (!Array.isArray(elements)) return []
321
+ const result: Array<Record<string, unknown>> = []
322
+ for (const item of elements) {
323
+ const element = getObject(item)
324
+ const type = getString(element.type)
325
+ const attrs = getObject(element.attrs)
326
+ const children = Array.isArray(element.children) ? element.children : []
327
+
328
+ if (type === 'text') {
329
+ result.push({ type: 'text', text: getString(attrs.content) })
330
+ } else if (type === 'at') {
331
+ const id = getString(attrs.id)
332
+ const name = getString(attrs.name) || (attrs.type === 'all' ? '所有人' : id)
333
+ result.push({
334
+ type: 'at',
335
+ qq: id,
336
+ text: normalizeAtText(name, id),
337
+ })
338
+ } else if (type === 'img' || type === 'image') {
339
+ const src = normalizeResourceSrc(getString(attrs.src) || getString(attrs.url) || getString(attrs.file), 'image')
340
+ result.push({ type: 'image', file: src, url: src, summary: getString(attrs.title) })
341
+ } else if (type === 'mface' || type === 'face') {
342
+ const nestedSegments = toSegments(children)
343
+ const nestedImage = nestedSegments.find((segment) => segment.type === 'image')
344
+ const src = normalizeResourceSrc(
345
+ getString(attrs.src) || getString(attrs.url) || getString(attrs.file) || getString(nestedImage?.url),
346
+ 'image',
347
+ )
348
+ if (src) {
349
+ result.push({ type: 'image', file: src, url: src, summary: getString(attrs.name) || getString(attrs.title) })
350
+ } else {
351
+ result.push({ type: 'face', id: getString(attrs.id), text: getString(attrs.name) })
352
+ }
353
+ } else if (type === 'audio') {
354
+ const src = normalizeResourceSrc(getString(attrs.src) || getString(attrs.url) || getString(attrs.file), 'audio')
355
+ result.push({ type: 'record', file: src, url: src })
356
+ } else if (type === 'video') {
357
+ const src = normalizeResourceSrc(getString(attrs.src) || getString(attrs.url) || getString(attrs.file), 'video')
358
+ result.push({ type: 'video', file: src, url: src })
359
+ } else if (type === 'file') {
360
+ const src = normalizeResourceSrc(getString(attrs.src) || getString(attrs.url) || getString(attrs.file), 'file')
361
+ result.push({ type: 'file', file: src, name: getString(attrs.name), url: src })
362
+ } else if (type === 'forward') {
363
+ result.push({
364
+ type: 'forward',
365
+ id: getString(attrs.id),
366
+ content: toSegments(children),
367
+ })
368
+ } else if (type === 'quote') {
369
+ result.push({ type: 'reply', id: getString(attrs.id) })
370
+ } else if (type === 'json') {
371
+ result.push({ type: 'json', data: getString(attrs.data) || getString(attrs.content) })
372
+ } else if (type === 'xml') {
373
+ result.push({ type: 'xml', data: getString(attrs.data) || getString(attrs.content) })
374
+ } else if (type === 'markdown') {
375
+ result.push({ type: 'markdown', content: getString(attrs.content) || childrenText(children) })
376
+ } else if (type === 'keyboard') {
377
+ // 按钮段不渲染
378
+ } else if (type === 'message' && ('forward' in attrs || attrs.forward)) {
379
+ result.push({ type: 'forward', id: getString(attrs.id), content: children })
380
+ } else if (type === 'p') {
381
+ result.push(...toSegments(children))
382
+ result.push({ type: 'text', text: '\n' })
383
+ } else if (type === 'br') {
384
+ result.push({ type: 'text', text: '\n' })
385
+ } else if (type === 'i18n') {
386
+ result.push({ type: 'i18n', path: getString(attrs.path), params: attrs })
387
+ } else if (children.length > 0) {
388
+ result.push(...toSegments(children))
389
+ } else {
390
+ result.push({ type: 'text', text: `[${type}]` })
391
+ }
392
+ }
393
+ return result
394
+ }
395
+
396
+ function messageFromEvent(event: SatoriObject): SatoriObject {
397
+ const message = event.message
398
+ if (typeof message === 'string') return { content: message }
399
+ if (Array.isArray(message)) return { elements: message }
400
+ return getObject(message)
401
+ }
402
+
403
+ function userFromEvent(event: SatoriObject): SatoriObject {
404
+ return getObject(event.user)
405
+ }
406
+
407
+ function memberFromEvent(event: SatoriObject): SatoriObject {
408
+ return getObject(event.member)
409
+ }
410
+
411
+ function channelFromEvent(event: SatoriObject): SatoriObject {
412
+ return getObject(event.channel)
413
+ }
414
+
415
+ function guildFromEvent(event: SatoriObject): SatoriObject {
416
+ return getObject(event.guild)
417
+ }
418
+
419
+ export function satoriEventToOneBot(
420
+ event: SatoriObject,
421
+ platformName = '',
422
+ ): Record<string, unknown> | null {
423
+ const type = getString(event.type)
424
+ const user = userFromEvent(event)
425
+ const member = memberFromEvent(event)
426
+ const channel = channelFromEvent(event)
427
+ const guild = guildFromEvent(event)
428
+ const message = messageFromEvent(event)
429
+ const selfId = getString(event.selfId) || getString(getObject(getObject(event.login).user).id)
430
+ const platform = getString(event.platform) || getString(getObject(event.login).platform) || platformName
431
+ const base = {
432
+ self_id: selfId,
433
+ platform,
434
+ sn: getNumber(event.sn),
435
+ timestamp_ms: getNumber(event.timestamp) ? normalizeTimestampMs(event.timestamp) : 0,
436
+ time: Math.floor(normalizeTimestampMs(event.timestamp) / 1000),
437
+ message_seq: getNumber(event.sn),
438
+ seq_id: getNumber(event.sn),
439
+ }
440
+
441
+ if (type === 'message' || type === 'message-created' || type === 'send') {
442
+ const isGroup = isGroupChannel(channel, guild)
443
+ const userId = getString(user.id)
444
+ || (isGroup ? '' : getString(channel.id).replace(/^private:/, ''))
445
+ const groupId = isGroup
446
+ ? getString(guild.id) || normalizeGroupId(getString(channel.id))
447
+ : ''
448
+ const directChannelId = isGroup ? groupId : userId
449
+ const nickname = getString(user.name) || getString(user.nick) || userId
450
+ const card = getString(member.nick) || getString(member.name) || ''
451
+ return {
452
+ ...base,
453
+ // 所有 Satori 平台统一保留原始报文
454
+ _rawSatori: event,
455
+ post_type: type === 'send' ? 'message_sent' : 'message',
456
+ message_type: isGroup ? 'group' : 'private',
457
+ channel_type: channel.type,
458
+ message_id: getString(message.id),
459
+ user_id: isGroup ? userId : directChannelId,
460
+ group_id: groupId,
461
+ group_name: isGroup ? (getString(guild.name) || getString(channel.name) || undefined) : undefined,
462
+ group_avatar: isGroup
463
+ ? (usableAvatar(guild.avatar) || usableAvatar(channel.avatar) || buildQqGroupAvatar(platform, groupId) || undefined)
464
+ : undefined,
465
+ channel_id: getString(channel.id) || (isGroup ? groupId : `private:${userId}`),
466
+ guild_id: getString(guild.id),
467
+ target_id: selfId,
468
+ message: messageSegments(message),
469
+ raw_message: getString(message.content),
470
+ sender: {
471
+ user_id: userId,
472
+ nickname,
473
+ card,
474
+ avatar: usableAvatar(user.avatar),
475
+ role: getString(member.title) || '',
476
+ },
477
+ infoList: {
478
+ message_id: getString(message.id),
479
+ private_id: isGroup ? '' : directChannelId,
480
+ group_id: groupId,
481
+ channel_id: getString(channel.id) || (isGroup ? groupId : `private:${userId}`),
482
+ guild_id: getString(guild.id),
483
+ target_id: selfId,
484
+ sender: userId,
485
+ },
486
+ }
487
+ }
488
+
489
+ if (type === 'guild-member-added' || type === 'guild-member-removed') {
490
+ return {
491
+ ...base,
492
+ post_type: 'notice',
493
+ notice_type: type === 'guild-member-added' ? 'group_increase' : 'group_decrease',
494
+ group_id: getString(guild.id),
495
+ user_id: getString(user.id),
496
+ operator_id: getString(getObject(event.operator).id) || selfId,
497
+ sub_type: 'approve',
498
+ }
499
+ }
500
+
501
+ if (type === 'friend-request' || type === 'guild-request' || type === 'guild-member-request') {
502
+ const isFriend = type === 'friend-request'
503
+ const isGuild = type === 'guild-request'
504
+ return {
505
+ ...base,
506
+ post_type: 'request',
507
+ request_type: isFriend ? 'friend' : 'group',
508
+ sub_type: isGuild ? 'invite' : 'add',
509
+ user_id: getString(user.id),
510
+ group_id: getString(guild.id),
511
+ comment: getString(getObject(event.operator).name) || getString(user.name),
512
+ flag: getString(message.id) || getString(user.id),
513
+ }
514
+ }
515
+
516
+ if (type === 'reaction-added' || type === 'reaction-removed') {
517
+ return {
518
+ ...base,
519
+ post_type: 'notice',
520
+ notice_type: 'reaction',
521
+ sub_type: type === 'reaction-added' ? 'added' : 'removed',
522
+ group_id: getString(guild.id),
523
+ user_id: getString(user.id),
524
+ message_id: getString(message.id),
525
+ }
526
+ }
527
+
528
+ return null
529
+ }
530
+
531
+ export function satoriElementsToText(elements: unknown): string {
532
+ return toSegments(elements)
533
+ .map((segment) => getString(segment.text) || getString(segment.path))
534
+ .join('')
535
+ }
536
+
537
+ function escapeText(value: unknown): string {
538
+ return getString(value)
539
+ .replace(/&/g, '&amp;')
540
+ .replace(/</g, '&lt;')
541
+ .replace(/>/g, '&gt;')
542
+ }
543
+
544
+ function escapeAttr(value: unknown): string {
545
+ return escapeText(value).replace(/"/g, '&quot;')
546
+ }
547
+
548
+ function serializeMessageSegment(segment: unknown): string {
549
+ if (typeof segment === 'string') return escapeText(segment)
550
+ const item = getObject(segment)
551
+ const attrs = getObject(item.data)
552
+ const type = getString(item.type)
553
+ const pick = (...keys: string[]) => {
554
+ for (const key of keys) {
555
+ const value = getString(item[key]) || getString(attrs[key])
556
+ if (value) return value
557
+ }
558
+ return ''
559
+ }
560
+
561
+ switch (type) {
562
+ case 'text':
563
+ return escapeText(pick('text', 'content'))
564
+ case 'image':
565
+ case 'img': {
566
+ const src = pick('url', 'file', 'src')
567
+ return src ? `<img src="${escapeAttr(src)}"/>` : ''
568
+ }
569
+ case 'record':
570
+ case 'audio': {
571
+ const src = pick('url', 'file', 'src')
572
+ return src ? `<audio src="${escapeAttr(src)}"/>` : ''
573
+ }
574
+ case 'video': {
575
+ const src = pick('url', 'file', 'src')
576
+ return src ? `<video src="${escapeAttr(src)}"/>` : ''
577
+ }
578
+ case 'file': {
579
+ const src = pick('url', 'file', 'src')
580
+ const name = pick('name', 'title')
581
+ return src ? `<file src="${escapeAttr(src)}"${name ? ` name="${escapeAttr(name)}"` : ''}/>` : ''
582
+ }
583
+ case 'at': {
584
+ const id = pick('qq', 'id')
585
+ const name = pick('name')
586
+ return id ? `<at id="${escapeAttr(id)}"${name ? ` name="${escapeAttr(name)}"` : ''}/>` : ''
587
+ }
588
+ case 'reply':
589
+ case 'quote': {
590
+ const id = pick('id')
591
+ return id ? `<quote id="${escapeAttr(id)}"/>` : ''
592
+ }
593
+ case 'face': {
594
+ const src = pick('url', 'file', 'src')
595
+ if (src) return `<img src="${escapeAttr(src)}"/>`
596
+ const id = pick('id', 'face_id', 'qq')
597
+ const name = pick('name', 'text')
598
+ return id ? `<face id="${escapeAttr(id)}"${name ? ` name="${escapeAttr(name)}"` : ''}/>` : ''
599
+ }
600
+ case 'json': {
601
+ const data = pick('data', 'content')
602
+ return data ? `<json data="${escapeAttr(data)}"/>` : ''
603
+ }
604
+ case 'xml': {
605
+ const data = pick('data', 'content')
606
+ return data ? `<xml data="${escapeAttr(data)}"/>` : ''
607
+ }
608
+ case 'markdown': {
609
+ const content = pick('content')
610
+ return content ? `<markdown content="${escapeAttr(content)}"/>` : ''
611
+ }
612
+ default:
613
+ return ''
614
+ }
615
+ }
616
+
617
+ function serializeSatoriContent(segments: unknown): string {
618
+ if (typeof segments === 'string') return segments
619
+ if (!Array.isArray(segments)) return ''
620
+ return segments.map(serializeMessageSegment).join('')
621
+ }
622
+
623
+ export function mapAction(action: string, params: Record<string, unknown>): {
624
+ method: string
625
+ params: Record<string, unknown>
626
+ } | null {
627
+ if (!action) return null
628
+ const normalized = action.replace(/^_/, '')
629
+ const guildId = getString(params.group_id) || getString(params.guild_id)
630
+ const userId = getString(params.user_id)
631
+ const messageId = getString(params.message_id)
632
+ const directChannel = userId && !/^(?:group|room|chat|channel|guild|private):/i.test(userId)
633
+ ? `private:${userId}`
634
+ : userId
635
+ const channelId = getString(params.channel_id) || guildId || directChannel || userId
636
+ const content = Array.isArray(params.message)
637
+ ? serializeSatoriContent(params.message)
638
+ : getString(params.message) || getString(params.content)
639
+
640
+ if (
641
+ ['send_msg', 'send_private_msg', 'send_group_msg'].includes(normalized) &&
642
+ (!channelId || channelId === '0')
643
+ ) {
644
+ return null
645
+ }
646
+
647
+ const table: Record<string, { method: string; params: Record<string, unknown> }> = {
648
+ get_login_info: { method: 'login.get', params: {} },
649
+ get_stranger_info: { method: 'user.get', params: { user_id: userId } },
650
+ delete_msg: { method: 'message.delete', params: { channel_id: channelId, message_id: messageId } },
651
+ get_msg: { method: 'message.get', params: { channel_id: channelId, message_id: messageId } },
652
+ send_msg: { method: 'message.create', params: { channel_id: channelId, content } },
653
+ send_private_msg: { method: 'message.create', params: { channel_id: channelId, content } },
654
+ send_group_msg: { method: 'message.create', params: { channel_id: channelId, content } },
655
+ set_group_kick: { method: 'guild.member.kick', params: { guild_id: guildId, user_id: userId } },
656
+ set_group_ban: {
657
+ method: 'guild.member.mute',
658
+ params: { guild_id: guildId, user_id: userId, duration: getNumber(params.duration) || 3600 },
659
+ },
660
+ send_respond: {
661
+ method: 'reaction.create',
662
+ params: { channel_id: channelId, message_id: messageId, emoji_id: getString(params.reaction_id) || getString(params.code) },
663
+ },
664
+ set_friend_add_request: {
665
+ method: 'friend.approve',
666
+ params: { message_id: getString(params.flag) || messageId, approve: params.approve !== false && params.approve !== 0 },
667
+ },
668
+ set_group_add_request: {
669
+ method: 'guild.member.approve',
670
+ params: { message_id: getString(params.flag) || messageId, approve: params.approve !== false && params.approve !== 0 },
671
+ },
672
+ }
673
+
674
+ return table[normalized] ?? null
675
+ }
676
+
677
+ function messageListFromResponse(data: unknown): unknown[] {
678
+ const root = getObject(data)
679
+ const list = Array.isArray(root.data) ? root.data : Array.isArray(data) ? data : []
680
+ return list.map((item) => {
681
+ const message = getObject(item)
682
+ const user = getObject(message.user)
683
+ const channel = getObject(message.channel)
684
+ const guild = getObject(message.guild)
685
+ const member = getObject(message.member)
686
+ const isGroup = isGroupChannel(channel, guild)
687
+ const groupId = isGroup
688
+ ? getString(guild.id) || normalizeGroupId(getString(channel.id))
689
+ : ''
690
+ const userId = getString(user.id)
691
+ || (isGroup ? '' : getString(channel.id).replace(/^private:/, ''))
692
+ const directChannelId = isGroup ? groupId : userId
693
+ return {
694
+ message_id: getString(message.id),
695
+ sn: getNumber(message.sn),
696
+ timestamp_ms: getNumber(message.timestamp) ? normalizeTimestampMs(message.timestamp) : 0,
697
+ time: Math.floor(normalizeTimestampMs(message.timestamp) / 1000),
698
+ message_seq: getNumber(message.sn) || getNumber(message.seq),
699
+ seq_id: getNumber(message.sn) || getNumber(message.seq),
700
+ message_type: isGroup ? 'group' : 'private',
701
+ channel_type: channel.type,
702
+ group_id: groupId,
703
+ user_id: groupId ? userId : directChannelId,
704
+ channel_id: getString(channel.id) || (isGroup ? groupId : `private:${userId}`),
705
+ guild_id: getString(guild.id),
706
+ sender: {
707
+ user_id: userId,
708
+ nickname: getString(user.name) || getString(user.nick) || userId,
709
+ card: getString(member.nick) || getString(member.name) || '',
710
+ avatar: usableAvatar(user.avatar) || usableAvatar(member.avatar),
711
+ },
712
+ message: messageSegments(message),
713
+ raw_message: getString(message.content),
714
+ }
715
+ })
716
+ }
717
+
718
+ export function satoriResponseToOneBot(
719
+ action: string,
720
+ data: unknown,
721
+ platform = '',
722
+ ): Record<string, unknown> {
723
+ const root = getObject(data)
724
+ const rawData = root.data ?? data
725
+
726
+ if (action === 'get_login_info') {
727
+ const user = getObject(getObject(rawData).user) || getObject(rawData)
728
+ return {
729
+ retcode: 0,
730
+ data: {
731
+ user_id: getString(user.id),
732
+ nickname: getString(user.name) || getString(user.nick),
733
+ platform: getString(getObject(rawData).platform),
734
+ },
735
+ }
736
+ }
737
+
738
+ if (action === 'get_friend_list' || action === 'get_group_list') {
739
+ const list = Array.isArray(rawData) ? rawData : Array.isArray(root.data) ? root.data : []
740
+ const mapped = list.map((item) => {
741
+ const value = getObject(item)
742
+ const user = getObject(value.user)
743
+ const id = getString(value.id)
744
+ const friendName = getString(user.name)
745
+ || getString(user.nick)
746
+ || getString(value.name)
747
+ || getString(value.nick)
748
+ || id
749
+ return action === 'get_friend_list'
750
+ ? {
751
+ user_id: getString(user.id) || id,
752
+ nickname: friendName,
753
+ remark: friendName === id ? '' : friendName,
754
+ avatar: usableAvatar(user.avatar) || usableAvatar(value.avatar),
755
+ group: {
756
+ group_id: 0,
757
+ group_name: '我的好友',
758
+ },
759
+ class_id: 0,
760
+ class_name: '我的好友',
761
+ }
762
+ : {
763
+ group_id: id,
764
+ group_name: getString(value.name) || id,
765
+ avatar: usableAvatar(value.avatar) || buildQqGroupAvatar(platform, id),
766
+ guild_id: id,
767
+ channel_id: id,
768
+ member_count: getNumber(value.member_count),
769
+ }
770
+ })
771
+ return { retcode: 0, data: mapped }
772
+ }
773
+
774
+ if (action === 'get_group_member_list') {
775
+ const list = Array.isArray(rawData) ? rawData : Array.isArray(root.data) ? root.data : []
776
+ const mapped = list.map((item) => {
777
+ const value = getObject(item)
778
+ const user = getObject(value.user)
779
+ const id = getString(user.id) || getString(value.id)
780
+ return {
781
+ user_id: id,
782
+ nickname: getString(user.name) || getString(user.nick) || id,
783
+ card: getString(value.nick) || getString(value.name) || '',
784
+ role: getString(value.title) || '',
785
+ }
786
+ })
787
+ return { retcode: 0, data: mapped }
788
+ }
789
+
790
+ if (action === 'get_group_member_info' || action === 'get_stranger_info') {
791
+ const value = getObject(rawData)
792
+ const user = getObject(value.user)
793
+ return {
794
+ retcode: 0,
795
+ data: {
796
+ user_id: getString(user.id) || getString(value.id),
797
+ nickname: getString(user.name) || getString(user.nick),
798
+ card: getString(value.nick) || getString(value.name) || '',
799
+ role: getString(value.title) || '',
800
+ sex: getString(user.sex),
801
+ },
802
+ }
803
+ }
804
+
805
+ if (action === 'get_group_msg_history' || action === 'get_friend_msg_history') {
806
+ return { retcode: 0, data: { messages: messageListFromResponse(data) } }
807
+ }
808
+
809
+ if (action === 'get_msg') {
810
+ return { retcode: 0, data: messageListFromResponse(data)[0] ?? rawData }
811
+ }
812
+
813
+ if (action === 'get_version_info') {
814
+ return { retcode: 0, data: { app_name: 'Satori', app_version: '1.0' } }
815
+ }
816
+
817
+ return { retcode: 0, data: rawData }
818
+ }