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,325 @@
1
+ // Satori 协议客户端:负责 WebSocket 事件与 HTTP API 请求。
2
+
3
+ export interface SatoriBootstrap {
4
+ endpoint: string
5
+ token: string
6
+ basePath: string
7
+ }
8
+
9
+ export interface SatoriLogin {
10
+ platform: string
11
+ selfId: string
12
+ name: string
13
+ avatar?: string
14
+ status: number
15
+ features?: string[]
16
+ }
17
+
18
+ export interface SatoriEvent {
19
+ type: string
20
+ platform: string
21
+ selfId: string
22
+ timestamp: number
23
+ sn: number
24
+ body: Record<string, unknown>
25
+ }
26
+
27
+ interface PendingConsoleRequest {
28
+ resolve: (value: unknown) => void
29
+ reject: (reason: unknown) => void
30
+ }
31
+
32
+ let bootstrap: SatoriBootstrap | null = null
33
+ let bootstrapPromise: Promise<SatoriBootstrap> | null = null
34
+ let logins: SatoriLogin[] = []
35
+ let activeBot: { platform: string; selfId: string } | null = null
36
+ let sequence = Number(localStorage.getItem('chat-patch:sn') ?? 0)
37
+ const pendingConsole = new Map<string, PendingConsoleRequest>()
38
+ let consoleRequestId = 0
39
+ let resolveBootstrapCallback: ((value: SatoriBootstrap) => void) | null = null
40
+ let bootstrapTimeout = 0
41
+
42
+ function getString(value: unknown): string {
43
+ return typeof value === 'string' ? value : ''
44
+ }
45
+
46
+ function getObject(value: unknown): Record<string, unknown> {
47
+ return typeof value === 'object' && value !== null ? value as Record<string, unknown> : {}
48
+ }
49
+
50
+ async function parseJsonResponse(response: Response): Promise<unknown> {
51
+ const text = await response.text()
52
+ if (!response.ok) {
53
+ throw new Error(`Satori API 返回 ${response.status}`)
54
+ }
55
+ try {
56
+ return JSON.parse(text) as unknown
57
+ } catch {
58
+ throw new Error(`Satori API 返回了非 JSON 内容: ${text.slice(0, 120)}`)
59
+ }
60
+ }
61
+
62
+ function normalizeEndpoint(endpoint: string): string {
63
+ const raw = endpoint.startsWith('undefined') ? endpoint.slice(9) : endpoint
64
+ if (import.meta.env.DEV) {
65
+ const url = /^https?:\/\//i.test(raw)
66
+ ? new URL(raw)
67
+ : new URL(raw, location.origin)
68
+ return `${location.origin}${url.pathname}`.replace(/\/+$/, '')
69
+ }
70
+ if (/^https?:\/\//i.test(raw)) return raw.replace(/\/+$/, '')
71
+ return `${location.origin}${raw.startsWith('/') ? raw : `/${raw}`}`.replace(/\/+$/, '')
72
+ }
73
+
74
+ function resolveBootstrap(): Promise<SatoriBootstrap> {
75
+ if (bootstrap) return Promise.resolve(bootstrap)
76
+ if (bootstrapPromise) return bootstrapPromise
77
+
78
+ bootstrapPromise = new Promise<SatoriBootstrap>((resolve, reject) => {
79
+ resolveBootstrapCallback = resolve
80
+ bootstrapTimeout = window.setTimeout(() => {
81
+ resolveBootstrapCallback = null
82
+ bootstrapPromise = null
83
+ reject(new Error('等待 Satori bootstrap 超时'))
84
+ }, 10000)
85
+ })
86
+
87
+ window.parent.postMessage({ source: 'chat-patch-ready' }, '*')
88
+ return bootstrapPromise
89
+ }
90
+
91
+ function receiveBootstrap(data: Record<string, unknown>) {
92
+ const payload = getObject(data.payload)
93
+ bootstrap = {
94
+ endpoint: getString(payload.endpoint) || '/satori',
95
+ token: getString(payload.token),
96
+ basePath: getString(payload.basePath) || '/chat-patch',
97
+ }
98
+ window.clearTimeout(bootstrapTimeout)
99
+ resolveBootstrapCallback?.(bootstrap)
100
+ resolveBootstrapCallback = null
101
+ }
102
+
103
+ window.addEventListener('message', (event) => {
104
+ const data = getObject(event.data)
105
+ if (getString(data.source) === 'chat-patch-bootstrap') {
106
+ receiveBootstrap(data)
107
+ }
108
+ })
109
+
110
+ function wsUrl(): string {
111
+ const endpoint = normalizeEndpoint(bootstrap?.endpoint || '/satori')
112
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
113
+ const url = new URL(endpoint)
114
+ return `${protocol}//${url.host}${url.pathname}/v1/events`
115
+ }
116
+
117
+ export function getBootstrap(): Promise<SatoriBootstrap> {
118
+ return resolveBootstrap()
119
+ }
120
+
121
+ export function getLogins(): SatoriLogin[] {
122
+ return logins
123
+ }
124
+
125
+ export function getActiveBot(): { platform: string; selfId: string } | null {
126
+ return activeBot
127
+ }
128
+
129
+ export function setActiveBot(platform: string, selfId: string) {
130
+ activeBot = { platform, selfId }
131
+ }
132
+
133
+ export function getSequence(): number {
134
+ return sequence
135
+ }
136
+
137
+ export async function request(
138
+ method: string,
139
+ params: Record<string, unknown> = {},
140
+ bot: { platform?: string; selfId?: string } | null = activeBot,
141
+ ): Promise<unknown> {
142
+ const info = await resolveBootstrap()
143
+ const headers: Record<string, string> = {
144
+ 'Content-Type': 'application/json',
145
+ }
146
+ if (info.token) headers.Authorization = `Bearer ${info.token}`
147
+ if (bot?.platform) headers['Satori-Platform'] = bot.platform
148
+ if (bot?.selfId) headers['Satori-User-ID'] = bot.selfId
149
+
150
+ const response = await fetch(`${normalizeEndpoint(info.endpoint)}/v1/${method}`, {
151
+ method: 'POST',
152
+ headers,
153
+ body: JSON.stringify(params),
154
+ })
155
+ return parseJsonResponse(response)
156
+ }
157
+
158
+ export async function upload(files: File[], bot: { platform: string; selfId: string }): Promise<unknown> {
159
+ const info = await resolveBootstrap()
160
+ const form = new FormData()
161
+ for (const file of files) form.append('file', file, file.name)
162
+ const headers: Record<string, string> = {}
163
+ if (info.token) headers.Authorization = `Bearer ${info.token}`
164
+ headers['Satori-Platform'] = bot.platform
165
+ headers['Satori-User-ID'] = bot.selfId
166
+
167
+ const response = await fetch(`${normalizeEndpoint(info.endpoint)}/v1/upload.create`, {
168
+ method: 'POST',
169
+ headers,
170
+ body: form,
171
+ })
172
+ return parseJsonResponse(response)
173
+ }
174
+
175
+ export function requestConsole(method: string, params: Record<string, unknown> = {}): Promise<unknown> {
176
+ const id = ++consoleRequestId
177
+ return new Promise((resolve, reject) => {
178
+ pendingConsole.set(String(id), { resolve, reject })
179
+ window.parent.postMessage({
180
+ source: 'chat-patch-request',
181
+ id,
182
+ method,
183
+ params,
184
+ }, '*')
185
+ })
186
+ }
187
+
188
+ export function handleConsoleResponse(data: Record<string, unknown>) {
189
+ const id = getString(data.id)
190
+ const pending = pendingConsole.get(id)
191
+ if (!pending) return
192
+ pendingConsole.delete(id)
193
+ if (data.ok === false) {
194
+ pending.reject(new Error(getString(data.error) || 'console 请求失败'))
195
+ } else {
196
+ pending.resolve(data.payload)
197
+ }
198
+ }
199
+
200
+ export function connect(
201
+ onEvent: (event: SatoriEvent) => void,
202
+ onReady: (logins: SatoriLogin[]) => void,
203
+ onStatus: (online: boolean) => void,
204
+ ): () => void {
205
+ let socket: WebSocket | null = null
206
+ let pingTimer = 0
207
+ let reconnectTimer = 0
208
+ let retry = 0
209
+ let disposed = false
210
+
211
+ const handleMessage = (event: MessageEvent) => {
212
+ if (getString(event.data.source) === 'chat-patch-response') {
213
+ handleConsoleResponse(getObject(event.data))
214
+ return
215
+ }
216
+ if (getString(event.data.source) === 'chat-patch-bootstrap') {
217
+ const payload = getObject(event.data.payload)
218
+ bootstrap = {
219
+ endpoint: getString(payload.endpoint) || '/satori',
220
+ token: getString(payload.token),
221
+ basePath: getString(payload.basePath) || '/chat-patch',
222
+ }
223
+ }
224
+ }
225
+
226
+ const open = () => {
227
+ void resolveBootstrap().then((info) => {
228
+ if (disposed) return
229
+ bootstrap = info
230
+ socket = new WebSocket(wsUrl())
231
+ socket.addEventListener('open', () => {
232
+ retry = 0
233
+ socket?.send(JSON.stringify({
234
+ op: 3,
235
+ body: {
236
+ token: info.token || undefined,
237
+ sn: sequence || undefined,
238
+ },
239
+ }))
240
+ pingTimer = window.setInterval(() => {
241
+ socket?.send(JSON.stringify({ op: 1, body: {} }))
242
+ }, 10000)
243
+ onStatus(true)
244
+ })
245
+
246
+ socket.addEventListener('message', (event) => {
247
+ try {
248
+ const raw = JSON.parse(String(event.data)) as Record<string, unknown>
249
+ const op = Number(raw.op)
250
+ const body = getObject(raw.body)
251
+ if (op === 4) {
252
+ const loginList = Array.isArray(body.logins) ? body.logins : []
253
+ const seen = new Set<string>()
254
+ const nextLogins: SatoriLogin[] = []
255
+ for (const item of loginList) {
256
+ const login = getObject(item)
257
+ const user = getObject(login.user)
258
+ const platform = getString(login.platform) || getString(user.platform)
259
+ const selfId = getString(user.id) || getString(login.selfId)
260
+ const key = [platform, selfId].map((value) => encodeURIComponent(value)).join(':')
261
+ if (!platform || !selfId || seen.has(key)) continue
262
+ seen.add(key)
263
+ nextLogins.push({
264
+ platform: getString(login.platform) || getString(user.platform),
265
+ selfId,
266
+ name: getString(user.name) || getString(user.nick) || getString(login.selfId),
267
+ avatar: getString(user.avatar) || undefined,
268
+ status: Number(login.status ?? 1),
269
+ features: Array.isArray(login.features) ? login.features.map(String) : [],
270
+ })
271
+ }
272
+ logins = nextLogins
273
+ if (!activeBot && logins[0]) {
274
+ activeBot = { platform: logins[0].platform, selfId: logins[0].selfId }
275
+ }
276
+ onReady(logins)
277
+ return
278
+ }
279
+ if (op === 5) return
280
+ if (op === 0) {
281
+ sequence = Number(getObject(body).sn ?? sequence)
282
+ localStorage.setItem('chat-patch:sn', String(sequence))
283
+ onEvent({
284
+ type: getString(body.type),
285
+ platform: getString(body.platform),
286
+ selfId: getString(body.selfId) || getString(getObject(getObject(body.login).user).id),
287
+ timestamp: Number(body.timestamp ?? Date.now()),
288
+ sn: sequence,
289
+ body,
290
+ })
291
+ }
292
+ } catch {
293
+ // 忽略无法解析的信令
294
+ }
295
+ })
296
+
297
+ socket.addEventListener('close', () => {
298
+ window.clearInterval(pingTimer)
299
+ onStatus(false)
300
+ if (disposed) return
301
+ retry += 1
302
+ const delay = Math.min(30000, 1000 * 2 ** retry)
303
+ reconnectTimer = window.setTimeout(open, delay)
304
+ })
305
+
306
+ socket.addEventListener('error', () => {
307
+ socket?.close()
308
+ })
309
+ }).catch(() => {
310
+ onStatus(false)
311
+ })
312
+ }
313
+
314
+ window.addEventListener('message', handleMessage)
315
+ open()
316
+
317
+ return () => {
318
+ disposed = true
319
+ window.clearInterval(pingTimer)
320
+ window.clearTimeout(reconnectTimer)
321
+ window.removeEventListener('message', handleMessage)
322
+ socket?.close()
323
+ socket = null
324
+ }
325
+ }
@@ -0,0 +1,178 @@
1
+ // 将 UI 消息段转换为 Satori 元素字符串。
2
+
3
+ import type { MsgItemElem } from './elements/information'
4
+
5
+ function escapeText(value: string): string {
6
+ return value
7
+ .replaceAll('&', '&amp;')
8
+ .replaceAll('<', '&lt;')
9
+ .replaceAll('>', '&gt;')
10
+ }
11
+
12
+ function escapeAttr(value: string): string {
13
+ return escapeText(value).replaceAll('"', '&quot;')
14
+ }
15
+
16
+ const satoriMarkupPattern = /<(\/?)(quote|at|img|image|audio|record|video|file|mface|face|sharp|p|br|message|text|json|xml|markdown|keyboard)(?:\s[^>]*)?\/?>/gi
17
+ const sqTokenPattern = /(\[SQ:\d+\]|\[图片\]|\[@[^\]]+\])/g
18
+
19
+ function escapeTextPreservingMarkup(value: string): string {
20
+ const decoded = value
21
+ .replaceAll('&lt;', '<')
22
+ .replaceAll('&gt;', '>')
23
+ .replaceAll('&quot;', '"')
24
+ .replaceAll('&#39;', "'")
25
+ .replaceAll('&amp;', '&')
26
+ const saved: string[] = []
27
+ const marker = (index: number) => `\u0000sq${index}\u0000`
28
+ const masked = decoded.replace(satoriMarkupPattern, (tag) => {
29
+ saved.push(tag)
30
+ return marker(saved.length - 1)
31
+ })
32
+ return masked
33
+ .replaceAll('&', '&amp;')
34
+ .replaceAll('<', '&lt;')
35
+ .replaceAll('>', '&gt;')
36
+ .replace(/\u0000sq(\d+)\u0000/g, (_, index: string) => saved[Number(index)] ?? '')
37
+ }
38
+
39
+ function segmentToSatori(segment: MsgItemElem): string {
40
+ const type = String(segment.type ?? '')
41
+ if (type === 'text') return escapeText(String(segment.text ?? ''))
42
+ if (type === 'reply' || type === 'quote') {
43
+ const id = String(segment.id ?? segment.data?.id ?? '')
44
+ return id ? `<quote id="${escapeAttr(id)}"/>` : ''
45
+ }
46
+ if (type === 'at') {
47
+ const id = String(segment.qq ?? segment.id ?? '')
48
+ const name = String(segment.text ?? segment.name ?? '')
49
+ return `<at id="${escapeAttr(id)}"${name ? ` name="${escapeAttr(name)}"` : ''}/>`
50
+ }
51
+ if (type === 'image') {
52
+ const src = String(segment.file ?? segment.url ?? segment.src ?? '')
53
+ return src ? `<img src="${escapeAttr(src)}"/>` : ''
54
+ }
55
+ if (type === 'face' || type === 'bface') {
56
+ const id = String(segment.id ?? segment.data?.id ?? '')
57
+ if (/^\d+$/.test(id)) {
58
+ const codePoint = Number(id)
59
+ if (codePoint >= 128000) return String.fromCodePoint(codePoint)
60
+ }
61
+ return String(segment.text ?? '[表情]')
62
+ }
63
+ if (type === 'record' || type === 'audio') {
64
+ const src = String(segment.file ?? segment.url ?? segment.src ?? '')
65
+ const name = String(segment.fileName ?? segment.name ?? '')
66
+ return src ? `<audio src="${escapeAttr(src)}"${name ? ` name="${escapeAttr(name)}"` : ''}/>` : ''
67
+ }
68
+ if (type === 'video') {
69
+ const src = String(segment.file ?? segment.url ?? segment.src ?? '')
70
+ return src ? `<video src="${escapeAttr(src)}"/>` : ''
71
+ }
72
+ if (type === 'file') {
73
+ const src = String(segment.file ?? segment.url ?? segment.src ?? '')
74
+ const name = String(segment.name ?? '')
75
+ return src ? `<file src="${escapeAttr(src)}"${name ? ` name="${escapeAttr(name)}"` : ''}/>` : ''
76
+ }
77
+ if (type === 'json') {
78
+ const data = String(segment.data ?? segment.content ?? '')
79
+ return data ? `<json data="${escapeAttr(data)}"/>` : ''
80
+ }
81
+ if (type === 'xml') {
82
+ const data = String(segment.data ?? segment.content ?? '')
83
+ return data ? `<xml data="${escapeAttr(data)}"/>` : ''
84
+ }
85
+ if (type === 'markdown') {
86
+ const content = String(segment.content ?? '')
87
+ return content ? `<markdown content="${escapeAttr(content)}"/>` : ''
88
+ }
89
+ if (type === 'forward') {
90
+ const id = String(segment.id ?? '')
91
+ return id ? `<forward id="${escapeAttr(id)}"/>` : ''
92
+ }
93
+ return escapeText(String(segment.text ?? `[${type}]`))
94
+ }
95
+
96
+ /**
97
+ * 将一组消息节点构建为 Satori 合并转发元素
98
+ */
99
+ export function buildForwardMessage(nodes: unknown[]): string {
100
+ const messages = nodes.map((raw) => {
101
+ const item = typeof raw === 'object' && raw !== null
102
+ ? raw as Record<string, unknown>
103
+ : {}
104
+ const nodeData = typeof item.data === 'object' && item.data !== null
105
+ ? item.data as Record<string, unknown>
106
+ : item
107
+ const userId = String(item.user_id ?? nodeData.user_id ?? nodeData.uin ?? '')
108
+ const nickname = String(item.nickname ?? nodeData.nickname ?? nodeData.name ?? '')
109
+ const displayName = nickname || userId
110
+ const time = String(item.time ?? nodeData.time ?? '')
111
+ const content = Array.isArray(item.content)
112
+ ? item.content as unknown[]
113
+ : Array.isArray(nodeData.content)
114
+ ? nodeData.content as unknown[]
115
+ : []
116
+ const inner = content.map((segment) => segmentToSatori(segment as MsgItemElem)).join('')
117
+ const author = `<author id="${escapeAttr(userId)}" name="${escapeAttr(displayName)}"${time ? ` time="${escapeAttr(time)}"` : ''}/>`
118
+ return `<message>${author}${inner}</message>`
119
+ }).join('')
120
+ return `<figure>${messages}</figure>`
121
+ }
122
+
123
+ export function parseMsg(msg: string, cache: MsgItemElem[], _img: string[]): string {
124
+ let output = ''
125
+ const pattern = sqTokenPattern
126
+ const consumed = new Set<number>()
127
+ const replyPrefix = cache
128
+ .filter((segment) => segment?.type === 'reply' || segment?.type === 'quote')
129
+ .map((segment) => segmentToSatori(segment))
130
+ .join('')
131
+ let match: RegExpExecArray | null
132
+ let cursor = 0
133
+
134
+ while ((match = pattern.exec(msg)) !== null) {
135
+ output += escapeTextPreservingMarkup(msg.slice(cursor, match.index))
136
+ const token = match[0]
137
+ if (token.startsWith('[SQ:')) {
138
+ const index = Number(token.slice(4, -1))
139
+ const segment = cache[index]
140
+ if (segment) {
141
+ output += segmentToSatori(segment)
142
+ consumed.add(index)
143
+ }
144
+ } else if (token.startsWith('[@')) {
145
+ const atIndex = cache.findIndex((segment, index) => {
146
+ return !consumed.has(index) && segment?.type === 'at'
147
+ })
148
+ if (atIndex >= 0) {
149
+ output += segmentToSatori(cache[atIndex])
150
+ consumed.add(atIndex)
151
+ } else {
152
+ output += escapeTextPreservingMarkup(token)
153
+ }
154
+ } else {
155
+ const imageIndex = cache.findIndex((segment, index) => {
156
+ return !consumed.has(index) && segment?.type === 'image'
157
+ })
158
+ if (imageIndex >= 0) {
159
+ output += segmentToSatori(cache[imageIndex])
160
+ consumed.add(imageIndex)
161
+ } else {
162
+ output += '[图片]'
163
+ }
164
+ }
165
+ cursor = match.index + match[0].length
166
+ }
167
+ output += escapeTextPreservingMarkup(msg.slice(cursor))
168
+ return replyPrefix + output
169
+ }
170
+
171
+ export function getSQList(msg: string): RegExpMatchArray | null {
172
+ return msg.match(sqTokenPattern)
173
+ }
174
+
175
+ export default {
176
+ parseMsg,
177
+ getSQList,
178
+ }
@@ -0,0 +1,49 @@
1
+ import { Component, DirectiveBinding, markRaw, ObjectDirective, shallowReactive } from 'vue'
2
+ import { VueCompData } from './elements/vueComp'
3
+ import { v4 as uuid } from 'uuid'
4
+ import UserInfoTooltip from '../components/tooltip/UserInfoTooltip.vue'
5
+ import { vTooltip } from './utils/appUtil'
6
+
7
+ export const tooltipList: TooltipInfo<Component>[] = shallowReactive([])
8
+
9
+ export type TooltipInfo<T extends Component> = {
10
+ compData: VueCompData<T>
11
+ pos: { x: number, y: number }
12
+ id: string
13
+ }
14
+
15
+ export type TooltipController = {
16
+ id: string
17
+ close: () => void
18
+ }
19
+
20
+ export function addTooltip<T extends Component>(compData: VueCompData<T>, pos: { x: number, y: number }): TooltipController {
21
+ const id = uuid()
22
+ tooltipList.push({ compData, pos, id })
23
+ return {
24
+ id,
25
+ close: () => closeTooltip(id)
26
+ }
27
+ }
28
+
29
+ export function closeTooltip(id: string) {
30
+ const index = tooltipList.findIndex(t => t.id === id)
31
+ if (index !== -1) {
32
+ tooltipList.splice(index, 1)
33
+ }
34
+ }
35
+
36
+ type IUser = any
37
+
38
+ type VUserTooltipBinding = IUser | number | (() => IUser | number)
39
+
40
+ export const vUserTooltip: ObjectDirective<HTMLElement, VUserTooltipBinding> = {
41
+ mounted(el: HTMLElement, binding: DirectiveBinding<VUserTooltipBinding>) {
42
+ (vTooltip as any).mounted(el, {
43
+ value: { comp: markRaw(UserInfoTooltip), props: { user: binding.value } }
44
+ })
45
+ },
46
+ unmounted(el: HTMLElement) {
47
+ (vTooltip as any).unmounted(el)
48
+ }
49
+ }