koishi-plugin-chat-patch 5.3.0 → 5.5.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 (77) hide show
  1. package/client/web/dist/assets/Chat-Clqxn7CB.js +28 -0
  2. package/client/web/dist/assets/{Chat-CTdyeaX2.css → Chat-DTEsebnY.css} +1 -1
  3. package/client/web/dist/assets/{MsgBody-DDacjWJ9.js → MsgBody-C0TKsXhC.js} +1 -1
  4. package/client/web/dist/assets/{MsgBody.vue_vue_type_script_setup_true_lang-B1dWyCB0.js → MsgBody.vue_vue_type_script_setup_true_lang-CCdBwlN_.js} +2 -2
  5. package/client/web/dist/assets/{append_darwin-CqBNU62J.css → append_darwin-Cql-dV6d.css} +1 -1
  6. package/client/web/dist/assets/{append_full_vibrancy-j0D2LM9m.css → append_full_vibrancy-ih3loy1O.css} +1 -1
  7. package/client/web/dist/assets/{append_linux-uhxeBqeC.css → append_linux-B3LvR8KB.css} +1 -1
  8. package/client/web/dist/assets/{append_new-407Lo5_V.css → append_new-DfhN_NX9.css} +1 -1
  9. package/client/web/dist/assets/{append_vibrancy-6FHctkOY.css → append_vibrancy-D3pVsmAa.css} +1 -1
  10. package/client/web/dist/assets/{append_win32-srmD-78y.css → append_win32-Dh9JlmlE.css} +1 -1
  11. package/client/web/dist/assets/index-B71pPwae.css +1 -0
  12. package/client/web/dist/assets/{index-BlLut5xn.js → index-BA37EQGa.js} +97 -76
  13. package/client/web/dist/index.html +2 -2
  14. package/client/web/src/App.vue +2 -2
  15. package/client/web/src/assets/css/append/append_darwin.css +2 -2
  16. package/client/web/src/assets/css/append/append_full_vibrancy.css +2 -2
  17. package/client/web/src/assets/css/append/append_linux.css +2 -2
  18. package/client/web/src/assets/css/append/append_new.css +2 -2
  19. package/client/web/src/assets/css/append/append_vibrancy.css +1 -1
  20. package/client/web/src/assets/css/append/append_win32.css +2 -2
  21. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +1 -1
  22. package/client/web/src/assets/css/chat.css +4 -3
  23. package/client/web/src/assets/css/msg.css +7 -3
  24. package/client/web/src/assets/css/options.css +2 -2
  25. package/client/web/src/assets/css/view.css +14 -3
  26. package/client/web/src/assets/l10n/zh-CN.po +21 -0
  27. package/client/web/src/components/AboutPan.vue +7 -4
  28. package/client/web/src/components/BulletinBody.vue +1 -1
  29. package/client/web/src/components/DepPan.vue +1 -1
  30. package/client/web/src/components/FriendBody.vue +1 -0
  31. package/client/web/src/components/MsgBody.vue +1 -3
  32. package/client/web/src/components/ThemeColorPickerPan.vue +1 -1
  33. package/client/web/src/components/TinySessionBody.vue +1 -0
  34. package/client/web/src/components/UmamiInfoPan.vue +1 -1
  35. package/client/web/src/components/UpdatePan.vue +2 -2
  36. package/client/web/src/components/WelPan.vue +1 -1
  37. package/client/web/src/components/tooltip/UserInfoTooltip.vue +1 -1
  38. package/client/web/src/function/connect.ts +279 -86
  39. package/client/web/src/function/elements/information.ts +1 -0
  40. package/client/web/src/function/msg.ts +42 -23
  41. package/client/web/src/function/satori-model.ts +35 -13
  42. package/client/web/src/function/satori.ts +33 -2
  43. package/client/web/src/function/utils/appUtil.ts +3 -11
  44. package/client/web/src/function/utils/avatarUtil.ts +18 -3
  45. package/client/web/src/function/utils/localHistoryUtil.ts +36 -12
  46. package/client/web/src/function/utils/msgUtil.ts +32 -21
  47. package/client/web/src/function/utils/sessionUtil.ts +20 -18
  48. package/client/web/src/main.ts +5 -0
  49. package/client/web/src/pages/Chat.vue +50 -47
  50. package/client/web/src/pages/Friends.vue +28 -24
  51. package/client/web/src/pages/Info.vue +5 -5
  52. package/client/web/src/pages/Messages.vue +57 -36
  53. package/client/web/src/pages/Qzone.vue +2 -2
  54. package/client/web/src/pages/chat-view/ChatTerminal.vue +4 -1
  55. package/client/web/src/pages/chat-view/SystemNotice.vue +3 -3
  56. package/client/web/src/pages/options/OptAccount.vue +6 -4
  57. package/client/web/src/pages/options/OptDev.vue +45 -2
  58. package/lib/database.d.ts +58 -0
  59. package/lib/index.js +234 -82
  60. package/lib/media.d.ts +24 -0
  61. package/lib/recorder.d.ts +21 -0
  62. package/lib/self-message.d.ts +27 -0
  63. package/lib/types.d.ts +121 -0
  64. package/lib/web.d.ts +7 -0
  65. package/package.json +1 -1
  66. package/src/bootstrap.ts +1 -0
  67. package/src/cache.ts +1 -5
  68. package/src/config.ts +1 -1
  69. package/src/database.ts +52 -8
  70. package/src/index.ts +4 -4
  71. package/src/media.ts +97 -0
  72. package/src/recorder.ts +35 -26
  73. package/src/self-message.ts +68 -19
  74. package/src/types.ts +10 -0
  75. package/src/web.ts +57 -48
  76. package/client/web/dist/assets/Chat-B448YOeN.js +0 -28
  77. package/client/web/dist/assets/index-fW_zcub3.css +0 -1
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto'
3
3
 
4
4
  import { Config } from './config'
5
5
  import { ChatDatabase } from './database'
6
+ import { MediaManager } from './media'
6
7
  import { PluginLogger } from './logger'
7
8
  import { SelfMessageChannelType, SelfMessageRecord, SelfMessageSource } from './types'
8
9
 
@@ -14,11 +15,13 @@ type SendOptionsWithMeta = SendOptions & {
14
15
  type BotSendMethod = Bot['sendMessage']
15
16
  type BotCreateMethod = Bot['createMessage']
16
17
  type BotPrivateMethod = Bot['sendPrivateMessage']
18
+ type BotDeleteMethod = Bot['deleteMessage']
17
19
 
18
20
  interface BotMethods {
19
21
  createMessage: BotCreateMethod
20
22
  sendMessage: BotSendMethod
21
23
  sendPrivateMessage: BotPrivateMethod
24
+ deleteMessage: BotDeleteMethod
22
25
  }
23
26
 
24
27
  interface SatoriElement {
@@ -38,6 +41,11 @@ function getString(value: unknown): string {
38
41
  return typeof value === 'string' ? value : ''
39
42
  }
40
43
 
44
+ function getMessageId(value: unknown): string {
45
+ const raw = getObject(value).id ?? getObject(value).message_id
46
+ return raw == null ? '' : String(raw)
47
+ }
48
+
41
49
  function getNumber(value: unknown): number {
42
50
  const num = Number(value)
43
51
  return Number.isFinite(num) ? num : 0
@@ -51,8 +59,9 @@ function getObject(value: unknown): Record<string, unknown> {
51
59
 
52
60
  function toSatoriElement(value: unknown): SatoriElement {
53
61
  const raw = getObject(value)
54
- const attrs = typeof raw.attrs === 'object' && raw.attrs !== null
55
- ? raw.attrs as Record<string, unknown>
62
+ const rawAttrs = raw.attrs ?? raw.data
63
+ const attrs = typeof rawAttrs === 'object' && rawAttrs !== null
64
+ ? rawAttrs as Record<string, unknown>
56
65
  : {}
57
66
  const children = Array.isArray(raw.children)
58
67
  ? raw.children.map(toSatoriElement)
@@ -71,10 +80,6 @@ function isForwardContainer(element: SatoriElement): boolean {
71
80
  && ('forward' in (element.attrs ?? {}) || element.attrs?.forward === true))
72
81
  }
73
82
 
74
- function isPrivateChannel(value: string): boolean {
75
- return /^(?:private|direct):/i.test(value)
76
- }
77
-
78
83
  function toSegments(
79
84
  elements: unknown[],
80
85
  resolveI18n?: (attrs: Record<string, unknown>) => string,
@@ -85,9 +90,9 @@ function toSegments(
85
90
  const attrs = element.attrs ?? {}
86
91
  const type = getString(element.type)
87
92
  if (type === 'text') {
88
- result.push({ type: 'text', text: getString(attrs.content) })
93
+ result.push({ type: 'text', text: getString(attrs.content) || getString(attrs.text) })
89
94
  } else if (type === 'at') {
90
- const id = getString(attrs.id)
95
+ const id = getString(attrs.id) || getString(attrs.qq)
91
96
  const name = getString(attrs.name) || (attrs.type === 'all' ? '所有人' : id)
92
97
  result.push({ type: 'at', qq: id, text: name.startsWith('@') ? name : `@${name}` })
93
98
  } else if (type === 'img' || type === 'image') {
@@ -210,6 +215,7 @@ export class SelfMessageRecorder {
210
215
  private ctx: Context,
211
216
  private config: Config,
212
217
  private database: ChatDatabase,
218
+ private media: MediaManager,
213
219
  private logger: PluginLogger,
214
220
  ) {}
215
221
 
@@ -245,6 +251,7 @@ export class SelfMessageRecorder {
245
251
  createMessage: bot.createMessage,
246
252
  sendMessage: bot.sendMessage,
247
253
  sendPrivateMessage: bot.sendPrivateMessage,
254
+ deleteMessage: bot.deleteMessage,
248
255
  }
249
256
  this.wrapped.set(bot, methods)
250
257
 
@@ -258,7 +265,7 @@ export class SelfMessageRecorder {
258
265
  try {
259
266
  const messages = await methods.createMessage.call(bot, channelId, content, referrer, sendOptions)
260
267
  const ids = Array.isArray(messages)
261
- ? messages.map((item) => getString(getObject(item).id)).filter(Boolean)
268
+ ? messages.map((item) => getMessageId(item)).filter((id) => id !== '')
262
269
  : []
263
270
  await this.recordFromWrapper(bot, channelId, content, ids, options, 'create', localId)
264
271
  return messages
@@ -267,6 +274,11 @@ export class SelfMessageRecorder {
267
274
  }
268
275
  }
269
276
 
277
+ bot.deleteMessage = async (channelId, messageId) => {
278
+ await methods.deleteMessage.call(bot, channelId, messageId)
279
+ await this.markSelfMessageRevoked(bot, channelId, messageId)
280
+ }
281
+
270
282
  bot.sendMessage = async (channelId, content, referrer, options) => {
271
283
  const sendOptions = this.prepareOptions(options)
272
284
  if (sendOptions.chatPatchId) {
@@ -275,7 +287,8 @@ export class SelfMessageRecorder {
275
287
  const localId = randomUUID()
276
288
  sendOptions.chatPatchId = localId
277
289
  try {
278
- const ids = await methods.sendMessage.call(bot, channelId, content, referrer, sendOptions)
290
+ const ids = (await methods.sendMessage.call(bot, channelId, content, referrer, sendOptions))
291
+ .filter((id) => id !== '')
279
292
  await this.recordFromWrapper(bot, channelId, content, ids, options, 'send', localId)
280
293
  return ids
281
294
  } finally {
@@ -291,7 +304,8 @@ export class SelfMessageRecorder {
291
304
  const localId = randomUUID()
292
305
  sendOptions.chatPatchId = localId
293
306
  try {
294
- const ids = await methods.sendPrivateMessage.call(bot, userId, content, guildId, sendOptions)
307
+ const ids = (await methods.sendPrivateMessage.call(bot, userId, content, guildId, sendOptions))
308
+ .filter((id) => id !== '')
295
309
  await this.recordFromWrapper(bot, userId, content, ids, options, 'private', localId)
296
310
  return ids
297
311
  } finally {
@@ -306,6 +320,7 @@ export class SelfMessageRecorder {
306
320
  bot.createMessage = methods.createMessage
307
321
  bot.sendMessage = methods.sendMessage
308
322
  bot.sendPrivateMessage = methods.sendPrivateMessage
323
+ bot.deleteMessage = methods.deleteMessage
309
324
  this.wrapped.delete(bot)
310
325
  }
311
326
 
@@ -323,6 +338,10 @@ export class SelfMessageRecorder {
323
338
  localId: string,
324
339
  ) {
325
340
  if (!channelId || this.isBlocked(bot.platform ?? '')) return
341
+ if (!ids.length) {
342
+ this.logger.warn('发送未返回消息 ID,判定为发送失败:', bot.platform, channelId)
343
+ return
344
+ }
326
345
  const source: SelfMessageSource = options?.session ? 'plugin' : 'bot'
327
346
  const record = this.buildRecord(
328
347
  bot.platform ?? '',
@@ -336,9 +355,26 @@ export class SelfMessageRecorder {
336
355
  localId,
337
356
  )
338
357
  await this.database.upsertSelfMessage(record)
358
+ void this.media.cacheMessageMedia(content, channelId).catch((error) => {
359
+ this.logger.warn('缓存机器人消息媒体失败:', error)
360
+ })
339
361
  this.logger.logInfo('机器人消息已记录:', record.kind, record.channelId, record.messageId || record.id)
340
362
  }
341
363
 
364
+ private async markSelfMessageRevoked(bot: Bot, channelId: string, messageId: string) {
365
+ if (!messageId || !channelId || this.isBlocked(bot.platform ?? '')) return
366
+ const updated = await this.database.updateSelfMessageByMessageId(
367
+ bot.platform ?? '',
368
+ bot.selfId,
369
+ channelId,
370
+ messageId,
371
+ { revoked: true, revokedAt: Date.now() },
372
+ )
373
+ if (!updated) {
374
+ this.logger.warn('撤回消息未找到本地自消息记录:', bot.platform, channelId, messageId)
375
+ }
376
+ }
377
+
342
378
  // 收到 send 事件时只补消息 id,避免重复记录
343
379
  private async recordSendEvent(session: Session) {
344
380
  if (session.type !== 'send') return
@@ -350,7 +386,7 @@ export class SelfMessageRecorder {
350
386
  const channelId = session.channelId || getString(eventChannel.id)
351
387
  if (this.isBlocked(platform) || !platform || !selfId || !channelId) return
352
388
 
353
- const messageId = session.messageId || getString(eventMessage.id)
389
+ const messageId = session.messageId || getMessageId(eventMessage)
354
390
  if (messageId) {
355
391
  const updated = await this.database.updateSelfMessageByMessageId(
356
392
  platform,
@@ -376,6 +412,9 @@ export class SelfMessageRecorder {
376
412
  randomUUID(),
377
413
  )
378
414
  await this.database.upsertSelfMessage(record)
415
+ void this.media.cacheMessageMedia(session.content ?? '', channelId).catch((error) => {
416
+ this.logger.warn('缓存 send 事件媒体失败:', error)
417
+ })
379
418
  }
380
419
 
381
420
  private resolveI18nElement(attrs: Record<string, unknown>): string {
@@ -404,10 +443,9 @@ export class SelfMessageRecorder {
404
443
  mode: 'create' | 'send' | 'private',
405
444
  localId: string,
406
445
  ): SelfMessageRecord {
407
- const normalizedChannelId = mode === 'private' && !isPrivateChannel(channelId)
408
- ? `private:${channelId}`
409
- : channelId
410
- const channelType: SelfMessageChannelType = mode === 'private' || isPrivateChannel(normalizedChannelId)
446
+ const normalizedChannelId = channelId
447
+ // 只按调用入口判断私聊,不根据频道 ID 前缀推断
448
+ const channelType: SelfMessageChannelType = mode === 'private'
411
449
  ? 'user'
412
450
  : 'group'
413
451
  const elements = this.normalizeElements(content)
@@ -445,9 +483,20 @@ export class SelfMessageRecorder {
445
483
 
446
484
  private normalizeElements(content: MessageContent): unknown[] {
447
485
  try {
448
- const elements = typeof content === 'string'
449
- ? h.parse(content)
450
- : h.toElementArray(content)
486
+ const normalized = typeof content === 'string'
487
+ ? content
488
+ : Array.isArray(content)
489
+ ? content.map((raw) => {
490
+ const item = getObject(raw)
491
+ if (typeof item.data === 'object' && item.data !== null && item.attrs === undefined) {
492
+ return h(getString(item.type), item.data as Record<string, unknown>)
493
+ }
494
+ return raw
495
+ })
496
+ : content
497
+ const elements = typeof normalized === 'string'
498
+ ? h.parse(normalized)
499
+ : h.toElementArray(normalized)
451
500
  return JSON.parse(JSON.stringify(elements)) as unknown[]
452
501
  } catch {
453
502
  return []
package/src/types.ts CHANGED
@@ -5,6 +5,10 @@ export interface SatoriBootstrap {
5
5
  endpoint: string
6
6
  token: string
7
7
  basePath: string
8
+ blockedPlatforms: Array<{
9
+ platformName: string
10
+ exactMatch: boolean
11
+ }>
8
12
  }
9
13
 
10
14
  export interface MessageRecord {
@@ -22,6 +26,8 @@ export interface MessageRecord {
22
26
  content?: string
23
27
  elements?: unknown[]
24
28
  raw?: unknown
29
+ revoked?: boolean
30
+ revokedAt?: number
25
31
  }
26
32
 
27
33
  export type SelfMessageChannelType = 'group' | 'user'
@@ -45,6 +51,8 @@ export interface SelfMessageRecord {
45
51
  source: SelfMessageSource
46
52
  kind: string
47
53
  fingerprint?: string
54
+ revoked?: boolean
55
+ revokedAt?: number
48
56
  }
49
57
 
50
58
  export interface SelfMessagePayload {
@@ -64,6 +72,8 @@ export interface SelfMessagePayload {
64
72
  sequence?: number
65
73
  source?: SelfMessageSource
66
74
  kind?: string
75
+ revoked?: boolean
76
+ revokedAt?: number
67
77
  }
68
78
 
69
79
  export interface HistoryQuery {
package/src/web.ts CHANGED
@@ -9,9 +9,10 @@ import type { DefaultContext, DefaultState, ParameterizedContext } from 'koa'
9
9
  import FileType from 'file-type'
10
10
 
11
11
  import { Config } from './config'
12
- import { ContactCacheService } from './cache'
13
- import { ChatDatabase } from './database'
14
- import { PluginLogger } from './logger'
12
+ import { ContactCacheService } from './cache'
13
+ import { ChatDatabase } from './database'
14
+ import { MediaManager } from './media'
15
+ import { PluginLogger } from './logger'
15
16
  import { ContactCacheItem, SelfMessagePayload, SelfMessageRecord } from './types'
16
17
 
17
18
  interface ViteConsoleServer {
@@ -29,11 +30,12 @@ interface ViteConsoleLike {
29
30
 
30
31
  export function registerWeb(
31
32
  ctx: Context,
32
- config: Config,
33
- database: ChatDatabase,
34
- contactCache: ContactCacheService,
35
- logger: PluginLogger,
36
- ) {
33
+ config: Config,
34
+ database: ChatDatabase,
35
+ contactCache: ContactCacheService,
36
+ media: MediaManager,
37
+ logger: PluginLogger,
38
+ ) {
37
39
  const webRoot = path.resolve(__dirname, '..', 'client', 'web', 'dist')
38
40
  const webSource = path.resolve(__dirname, '..', 'client', 'web', 'src')
39
41
  const webPublic = path.resolve(__dirname, '..', 'client', 'web', 'public')
@@ -82,40 +84,27 @@ export function registerWeb(
82
84
  koa.body = ''
83
85
  }
84
86
 
85
- // web 应用里仍有 /img 这类运行时绝对路径,开发模式直接由 Koishi 服务托管
86
- const registerDevPublic = (pattern: string, prefix: string) => {
87
- ctx.server.get(pattern, async (koa: WebContext, next: () => Promise<void>) => {
88
- if (!getVite()) return next()
89
- await servePublicFile(koa, prefix)
90
- })
91
- }
92
-
93
- registerDevPublic('/img(.*)', 'img')
87
+ // web 应用里仍有 /img 这类运行时绝对路径,生产/开发模式都要由 Koishi 服务托管
88
+ const registerPublic = (pattern: string, prefix: string) => {
89
+ ctx.server.get(pattern, async (koa: WebContext) => {
90
+ await servePublicFile(koa, prefix)
91
+ })
92
+ }
93
+
94
+ registerPublic('/img(.*)', 'img')
94
95
 
95
96
  const toExtension = (value: string): string => {
96
97
  const ext = value.startsWith('.') ? value : `.${value}`
97
98
  return ext.toLowerCase()
98
99
  }
99
100
 
100
- const normalizeGroupId = (value: string): string => {
101
- const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, '').trim()
102
- const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/)
103
- return wrapped ? wrapped[1] : raw || value
104
- }
101
+ const normalizeGroupId = (value: string): string => {
102
+ return String(value)
103
+ }
105
104
 
106
- const historyChannelCandidates = (channelId: string): string[] => {
107
- const raw = normalizeGroupId(channelId)
108
- const candidates = [channelId]
109
- if (raw && raw !== channelId) candidates.push(raw)
110
- const id = raw || channelId
111
- if (!/^(?:group|room|chat|channel|guild|private):/i.test(channelId)) {
112
- candidates.push(`group:${id}`, `private:${id}`)
113
- }
114
- if (id) {
115
- candidates.push(` [_${id}_] `, `_${id}_`, `[${id}]`, `group:${id}`)
116
- }
117
- return [...new Set(candidates)]
118
- }
105
+ const historyChannelCandidates = (channelId: string): string[] => {
106
+ return channelId ? [channelId] : []
107
+ }
119
108
 
120
109
  const mimeToExt: Record<string, string> = {
121
110
  'audio/mpeg': '.mp3',
@@ -380,7 +369,9 @@ export function registerWeb(
380
369
  channelId: String(body.channelId ?? ''),
381
370
  guildId: typeof body.guildId === 'string' ? body.guildId : undefined,
382
371
  channelType: body.channelType === 'user' ? 'user' : 'group',
383
- messageId: typeof body.messageId === 'string' ? body.messageId : undefined,
372
+ messageId: typeof body.messageId === 'string' || typeof body.messageId === 'number'
373
+ ? String(body.messageId)
374
+ : undefined,
384
375
  content: typeof body.content === 'string' ? body.content : undefined,
385
376
  elements: Array.isArray(body.elements) ? body.elements as unknown[] : undefined,
386
377
  message: Array.isArray(body.message) ? body.message as unknown[] : undefined,
@@ -392,8 +383,12 @@ export function registerWeb(
392
383
  sequence: typeof body.sequence === 'number' && Number.isFinite(body.sequence)
393
384
  ? body.sequence
394
385
  : 0,
395
- source: body.source === 'plugin' ? 'plugin' : 'webui',
396
- kind: typeof body.kind === 'string' ? body.kind : 'text',
386
+ source: body.source === 'plugin' ? 'plugin' : 'webui',
387
+ kind: typeof body.kind === 'string' ? body.kind : 'text',
388
+ revoked: body.revoked === true,
389
+ revokedAt: typeof body.revokedAt === 'number' && Number.isFinite(body.revokedAt)
390
+ ? body.revokedAt
391
+ : undefined,
397
392
  }
398
393
  if (!payload.platform || !payload.selfId || !payload.channelId) {
399
394
  koa.status = 400
@@ -415,9 +410,11 @@ export function registerWeb(
415
410
  forwardContent: payload.forwardContent,
416
411
  sentAt: payload.sentAt ?? Date.now(),
417
412
  sequence: payload.sequence ?? 0,
418
- source: payload.source ?? 'webui',
419
- kind: payload.kind ?? 'text',
420
- }
413
+ source: payload.source ?? 'webui',
414
+ kind: payload.kind ?? 'text',
415
+ revoked: payload.revoked,
416
+ revokedAt: payload.revokedAt,
417
+ }
421
418
  await database.upsertSelfMessage(record)
422
419
  koa.body = { ok: true, message: record }
423
420
  })
@@ -630,7 +627,7 @@ export function registerWeb(
630
627
  }
631
628
  })
632
629
 
633
- ctx.server.post(`${config.basePath}/api/cache`, async (koa) => {
630
+ ctx.server.post(`${config.basePath}/api/cache`, async (koa) => {
634
631
  const requestBody = (koa.request as unknown as { body?: unknown }).body
635
632
  const body = (requestBody ?? {}) as Record<string, unknown>
636
633
  const platform = String(body.platform ?? '')
@@ -670,12 +667,24 @@ export function registerWeb(
670
667
  await database.setContacts(platform, selfId, normalizedType, contacts)
671
668
  }
672
669
  }
673
- koa.body = {
674
- contacts: await database.getContacts(platform, selfId, normalizedType),
675
- }
676
- })
677
-
678
- const serveFile = async (koa: WebContext, fileName: string) => {
670
+ koa.body = {
671
+ contacts: await database.getContacts(platform, selfId, normalizedType),
672
+ }
673
+ })
674
+
675
+ ctx.server.post(`${config.basePath}/api/clear-all`, async (koa) => {
676
+ try {
677
+ await database.clearAll()
678
+ await media.clearAll()
679
+ koa.body = { ok: true }
680
+ } catch (error) {
681
+ logger.warn('清空全部缓存失败:', error)
682
+ koa.status = 500
683
+ koa.body = { ok: false, error: String(error) }
684
+ }
685
+ })
686
+
687
+ const serveFile = async (koa: WebContext, fileName: string) => {
679
688
  const fullPath = path.join(webRoot, fileName)
680
689
  if (existsSync(fullPath) && statSync(fullPath).isFile()) {
681
690
  await send(koa, path.relative(webRoot, fullPath), { root: webRoot })