koishi-plugin-chat-patch 3.0.2 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/client/vue/index.vue +60 -634
  2. package/client/web/dist/assets/AppIcon-DIku6-h0.png +0 -0
  3. package/client/web/dist/assets/AppIconDark-BYymN71t.png +0 -0
  4. package/client/web/dist/assets/CapacitorAppIcon-CSZwNhvi.png +0 -0
  5. package/client/web/dist/assets/CapacitorAppIconDark-BMvMOzsg.png +0 -0
  6. package/client/web/dist/assets/Chat-CTdyeaX2.css +1 -0
  7. package/client/web/dist/assets/Chat-DkZvD7d8.js +28 -0
  8. package/client/web/dist/assets/MealHungryPan-Beos3lRI.js +1 -0
  9. package/client/web/dist/assets/MsgBody-aZ4_aija.js +1 -0
  10. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CPZjRLp6.js +67 -0
  11. package/client/web/dist/assets/NapcatAppIcon-C7VOAkW9.png +0 -0
  12. package/client/web/dist/assets/Vue-lEhHouxB.png +0 -0
  13. package/client/web/dist/assets/append_darwin-CqBNU62J.css +1 -0
  14. package/client/web/dist/assets/append_full_vibrancy-j0D2LM9m.css +1 -0
  15. package/client/web/dist/assets/append_linux-uhxeBqeC.css +1 -0
  16. package/client/web/dist/assets/append_linux_vibrancy-COiZq8MS.css +1 -0
  17. package/client/web/dist/assets/append_napcat-DV8CAyAd.css +1 -0
  18. package/client/web/dist/assets/append_new-407Lo5_V.css +1 -0
  19. package/client/web/dist/assets/append_vibrancy-6FHctkOY.css +1 -0
  20. package/client/web/dist/assets/append_win32-srmD-78y.css +1 -0
  21. package/client/web/dist/assets/cakes-CxTVKdnK.svg +189 -0
  22. package/client/web/dist/assets/core-DhEqZVGG.js +1 -0
  23. package/client/web/dist/assets/event-Cadp6m6J.js +1 -0
  24. package/client/web/dist/assets/index-CPTXiemn.js +4045 -0
  25. package/client/web/dist/assets/index-Cg_esKB0.js +1 -0
  26. package/client/web/dist/assets/index-Cln012sP.css +1 -0
  27. package/client/web/dist/assets//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223-CJKIKBn3.ttf +0 -0
  28. package/client/web/dist/assets//346/234/237/345/276/205-C90_OY60.png +0 -0
  29. package/client/web/dist/bcui/css/color-dark.css +35 -0
  30. package/client/web/dist/bcui/css/color-light.css +35 -0
  31. package/client/web/dist/bcui/css/style.css +468 -0
  32. package/client/web/dist/bcui/js/main.js +193 -0
  33. package/client/web/dist/css/append-dark.css +13 -0
  34. package/client/web/dist/css/append-light.css +13 -0
  35. package/client/web/dist/css/append-napcat-dark.css +14 -0
  36. package/client/web/dist/css/append-napcat-light.css +14 -0
  37. package/client/web/dist/favicon.ico +0 -0
  38. package/client/web/dist/img/icons/icon-for-fuck-apple.png +0 -0
  39. package/client/web/dist/img/icons/icon-maskable.png +0 -0
  40. package/client/web/dist/img/icons/icon.png +0 -0
  41. package/client/web/dist/img/icons/icon.svg +10 -0
  42. package/client/web/dist/index.html +22 -0
  43. package/client/web/dist/robots.txt +2 -0
  44. package/client/web/dist/sw.js +58 -0
  45. package/client/web/env.d.ts +56 -0
  46. package/client/web/index.html +21 -0
  47. package/client/web/package-lock.json +3419 -0
  48. package/client/web/package.json +62 -0
  49. package/client/web/public/bcui/css/color-dark.css +35 -0
  50. package/client/web/public/bcui/css/color-light.css +35 -0
  51. package/client/web/public/bcui/css/style.css +468 -0
  52. package/client/web/public/bcui/js/main.js +193 -0
  53. package/client/web/public/css/append-dark.css +13 -0
  54. package/client/web/public/css/append-light.css +13 -0
  55. package/client/web/public/css/append-napcat-dark.css +14 -0
  56. package/client/web/public/css/append-napcat-light.css +14 -0
  57. package/client/web/public/favicon.ico +0 -0
  58. package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
  59. package/client/web/public/img/icons/icon-maskable.png +0 -0
  60. package/client/web/public/img/icons/icon.png +0 -0
  61. package/client/web/public/img/icons/icon.svg +10 -0
  62. package/client/web/public/robots.txt +2 -0
  63. package/client/web/public/sw.js +58 -0
  64. package/client/web/scripts/copy-bcui.mjs +17 -0
  65. package/client/web/src/App.vue +1127 -0
  66. package/client/web/src/assets/css/append/append_darwin.css +68 -0
  67. package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
  68. package/client/web/src/assets/css/append/append_linux.css +125 -0
  69. package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
  70. package/client/web/src/assets/css/append/append_napcat.css +35 -0
  71. package/client/web/src/assets/css/append/append_new.css +843 -0
  72. package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
  73. package/client/web/src/assets/css/append/append_win32.css +109 -0
  74. package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
  75. package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
  76. package/client/web/src/assets/css/chat.css +2104 -0
  77. package/client/web/src/assets/css/msg.css +941 -0
  78. package/client/web/src/assets/css/options.css +811 -0
  79. package/client/web/src/assets/css/sys_notice.css +62 -0
  80. package/client/web/src/assets/css/view.css +2105 -0
  81. package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
  82. package/client/web/src/assets/img/Vue.png +0 -0
  83. package/client/web/src/assets/img/cakes.svg +189 -0
  84. package/client/web/src/assets/img/icons/AppIcon.png +0 -0
  85. package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
  86. package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
  87. package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
  88. package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
  89. package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
  90. package/client/web/src/assets/img/icons/exit.png +0 -0
  91. package/client/web/src/assets/img/icons/pen.png +0 -0
  92. package/client/web/src/assets/img/stars.svg +89 -0
  93. package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
  94. package/client/web/src/assets/l10n/_About I10n.md +35 -0
  95. package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
  96. package/client/web/src/assets/l10n/zh-CN.po +1941 -0
  97. package/client/web/src/assets/logo.png +0 -0
  98. package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
  99. package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
  100. package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
  101. package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
  102. package/client/web/src/assets/qed.txt +1 -0
  103. package/client/web/src/components/AboutPan.vue +197 -0
  104. package/client/web/src/components/BulletinBody.vue +129 -0
  105. package/client/web/src/components/DepPan.vue +310 -0
  106. package/client/web/src/components/EmojiFace.vue +32 -0
  107. package/client/web/src/components/FacePan.vue +461 -0
  108. package/client/web/src/components/FileBody.vue +129 -0
  109. package/client/web/src/components/FileManager.vue +532 -0
  110. package/client/web/src/components/FriendBody.vue +54 -0
  111. package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
  112. package/client/web/src/components/History.vue +49 -0
  113. package/client/web/src/components/LazyLottie.vue +82 -0
  114. package/client/web/src/components/MergePan.vue +127 -0
  115. package/client/web/src/components/MsgBody.vue +1478 -0
  116. package/client/web/src/components/MusicPlayer.vue +549 -0
  117. package/client/web/src/components/NoticeBody.vue +168 -0
  118. package/client/web/src/components/QzonePermissionPan.vue +150 -0
  119. package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
  120. package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
  121. package/client/web/src/components/TinySessionBody.vue +51 -0
  122. package/client/web/src/components/UmamiInfoPan.vue +1549 -0
  123. package/client/web/src/components/UpdatePan.vue +263 -0
  124. package/client/web/src/components/ViewerCom.vue +1360 -0
  125. package/client/web/src/components/VoiceMsg.vue +626 -0
  126. package/client/web/src/components/WelPan.vue +894 -0
  127. package/client/web/src/components/msg-body/.gitkeep +0 -0
  128. package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
  129. package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
  130. package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
  131. package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
  132. package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
  133. package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
  134. package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
  135. package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
  136. package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
  137. package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
  138. package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
  139. package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
  140. package/client/web/src/components/notice-component/.gitkeep +0 -0
  141. package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
  142. package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
  143. package/client/web/src/components/tooltip/Tooltip.vue +50 -0
  144. package/client/web/src/components/tooltip/Tooltips.vue +14 -0
  145. package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
  146. package/client/web/src/function/base.ts +238 -0
  147. package/client/web/src/function/connect.ts +2042 -0
  148. package/client/web/src/function/debug.ts +10 -0
  149. package/client/web/src/function/elements/information.ts +141 -0
  150. package/client/web/src/function/elements/system.ts +101 -0
  151. package/client/web/src/function/elements/vueComp.ts +62 -0
  152. package/client/web/src/function/favicon.ts +76 -0
  153. package/client/web/src/function/input.ts +69 -0
  154. package/client/web/src/function/model/emoji.ts +127 -0
  155. package/client/web/src/function/model/img.ts +150 -0
  156. package/client/web/src/function/model/msg-body.ts +51 -0
  157. package/client/web/src/function/msg.ts +2674 -0
  158. package/client/web/src/function/notify.ts +225 -0
  159. package/client/web/src/function/option.ts +749 -0
  160. package/client/web/src/function/satori-model.ts +818 -0
  161. package/client/web/src/function/satori.ts +325 -0
  162. package/client/web/src/function/sender.ts +178 -0
  163. package/client/web/src/function/tooltip.ts +49 -0
  164. package/client/web/src/function/utils/appUtil.ts +2288 -0
  165. package/client/web/src/function/utils/avatarUtil.ts +31 -0
  166. package/client/web/src/function/utils/backgroundUtil.ts +110 -0
  167. package/client/web/src/function/utils/linkViewUtil.ts +101 -0
  168. package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
  169. package/client/web/src/function/utils/msgUtil.ts +1242 -0
  170. package/client/web/src/function/utils/pinyin.ts +141 -0
  171. package/client/web/src/function/utils/recordUtil.ts +187 -0
  172. package/client/web/src/function/utils/sessionUtil.ts +114 -0
  173. package/client/web/src/function/utils/systemUtil.ts +664 -0
  174. package/client/web/src/main.ts +91 -0
  175. package/client/web/src/pages/Chat.vue +3453 -0
  176. package/client/web/src/pages/Friends.vue +636 -0
  177. package/client/web/src/pages/Info.vue +717 -0
  178. package/client/web/src/pages/Messages.vue +875 -0
  179. package/client/web/src/pages/Options.vue +82 -0
  180. package/client/web/src/pages/Qzone.vue +1178 -0
  181. package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
  182. package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
  183. package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
  184. package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
  185. package/client/web/src/pages/options/OptAccount.vue +107 -0
  186. package/client/web/src/pages/options/OptAddon.vue +185 -0
  187. package/client/web/src/pages/options/OptDev.vue +704 -0
  188. package/client/web/src/pages/options/OptFunction.vue +403 -0
  189. package/client/web/src/pages/options/OptInfo.vue +165 -0
  190. package/client/web/src/pages/options/OptView.vue +846 -0
  191. package/client/web/src/runtime/backend.ts +310 -0
  192. package/client/web/src/state/auth.ts +14 -0
  193. package/client/web/src/state/chat.ts +36 -0
  194. package/client/web/src/state/connection.ts +20 -0
  195. package/client/web/src/state/contact.ts +37 -0
  196. package/client/web/src/state/qzone.ts +18 -0
  197. package/client/web/src/state/sessionHistory.ts +25 -0
  198. package/client/web/src/state/settings.ts +18 -0
  199. package/client/web/src/state/sticker.ts +10 -0
  200. package/client/web/src/state/ui.ts +56 -0
  201. package/client/web/tsconfig.json +27 -0
  202. package/client/web/tsconfig.tsbuildinfo +1 -0
  203. package/client/web/vite.config.ts +33 -0
  204. package/dist/index.js +1 -12
  205. package/dist/style.css +1 -1
  206. package/lib/index.js +2070 -2077
  207. package/package.json +15 -2
  208. package/readme.md +0 -22
  209. package/src/bootstrap.ts +71 -0
  210. package/src/cache.ts +319 -0
  211. package/src/config.ts +23 -35
  212. package/src/database.ts +650 -0
  213. package/src/index.ts +27 -59
  214. package/src/media.ts +302 -0
  215. package/src/recorder.ts +175 -0
  216. package/src/self-message.ts +456 -0
  217. package/src/server.d.ts +26 -0
  218. package/src/types.ts +108 -48
  219. package/src/web.ts +712 -0
  220. package/client/vue/chat-logic.ts +0 -683
  221. package/client/vue/composables/useChatActions.ts +0 -61
  222. package/client/vue/composables/useChatData.ts +0 -185
  223. package/client/vue/composables/useImageCache.ts +0 -281
  224. package/client/vue/composables/useVideoCache.ts +0 -125
  225. package/client/vue/types.ts +0 -72
  226. package/lib/api-handlers.d.ts +0 -25
  227. package/lib/config.d.ts +0 -14
  228. package/lib/file-manager.d.ts +0 -124
  229. package/lib/index.d.ts +0 -17
  230. package/lib/logger.d.ts +0 -9
  231. package/lib/message-handler.d.ts +0 -35
  232. package/lib/types.d.ts +0 -57
  233. package/lib/utils.d.ts +0 -18
  234. package/src/api-handlers.ts +0 -646
  235. package/src/file-manager.ts +0 -1436
  236. package/src/message-handler.ts +0 -507
  237. package/src/utils.ts +0 -189
package/package.json CHANGED
@@ -1,14 +1,22 @@
1
1
  {
2
2
  "name": "koishi-plugin-chat-patch",
3
3
  "description": "[<ruby>chat-patch<rp>(</rp><rt>点我预览效果</rt><rp>)</rp></ruby>](https://i0.hdslb.com/bfs/openplatform/71074dfc9e5256fc3333d8bd8478bec1af874046.png) 视奸小插件((bushi( (低性能警告)。手机端适配。灵感来自 chat 插件。",
4
- "version": "3.0.2",
4
+ "version": "5.0.1",
5
+ "scripts": {
6
+ "build:web": "npm --prefix client/web run build",
7
+ "prepack": "npm run build:web",
8
+ "prepublishOnly": "npm run build:web",
9
+ "yakumo:before:build": "npm --prefix client/web run build"
10
+ },
5
11
  "main": "lib/index.js",
6
12
  "typings": "lib/index.d.ts",
7
13
  "files": [
8
14
  "client",
9
15
  "dist",
10
16
  "lib",
11
- "src"
17
+ "src",
18
+ "client/web/dist",
19
+ "client/web/src"
12
20
  ],
13
21
  "license": "MIT",
14
22
  "homepage": "https://github.com/koishi-shangxue-plugins/koishi-shangxue-apps/",
@@ -34,6 +42,8 @@
34
42
  },
35
43
  "peerDependencies": {
36
44
  "@koishijs/plugin-console": "^5.11.0",
45
+ "@koishijs/plugin-server": "^3.2.9",
46
+ "@koishijs/plugin-server-satori": "^2.9.1",
37
47
  "koishi": "^4.16.0"
38
48
  },
39
49
  "devDependencies": {
@@ -47,6 +57,9 @@
47
57
  "tailwindcss": "^3.4.17"
48
58
  },
49
59
  "dependencies": {
60
+ "file-type": "^16.5.4",
61
+ "koa-send": "^5.0.1",
62
+ "level": "^10.0.0",
50
63
  "mime-types": "^3.0.2"
51
64
  }
52
65
  }
package/readme.md CHANGED
@@ -1,25 +1,3 @@
1
1
  # koishi-plugin-chat-patch
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/koishi-plugin-chat-patch?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-chat-patch) [![npm downloads](https://img.shields.io/npm/dm/koishi-plugin-chat-patch)](https://www.npmjs.com/package/koishi-plugin-chat-patch)
4
-
5
- Koishi 控制台聊天插件,允许用户直接在控制台中管理机器人、频道和消息。
6
-
7
- 支持消息的收发、图片缓存、频道置顶等。
8
-
9
- ## ✨ 功能特性
10
-
11
- - **聊天界面**:在 Koishi 控制台内直接查看机器人的聊天记录。
12
- - **消息收发**:支持接收文本、图片等多种类型的消息。
13
- - **图片缓存**:通过 IndexedDB 缓存图片,优化加载速度并减少网络请求。
14
- - **手机端优化**:针对手机端进行适配,提供滑动返回等手势操作,并可配置聊天容器高度以防止输入框被遮挡。
15
- - **平台屏蔽**:可配置屏蔽特定平台的消息,避免不必要的干扰。
16
-
17
- ## 🛠️ 开发与贡献
18
-
19
- 欢迎 PR ~
20
-
21
- PR 方式请参考 -> <https://github.com/koishi-shangxue-plugins/koishi-shangxue-apps/tree/main?tab=readme-ov-file#%E5%A6%82%E4%BD%95%E5%9C%A8%E9%A1%B9%E7%9B%AE%E6%A8%A1%E6%9D%BF%E4%B8%AD%E5%BC%80%E5%8F%91%E6%AD%A4%E4%BB%93%E5%BA%93>
22
-
23
- ## 许可证
24
-
25
- 本项目采用 [MIT 许可证](LICENSE) 开源。
@@ -0,0 +1,71 @@
1
+ import { Context } from 'koishi'
2
+ import {} from '@koishijs/plugin-server'
3
+ import {} from '@koishijs/plugin-server-satori'
4
+ import {} from '@satorijs/plugin-server'
5
+
6
+ import { Config } from './config'
7
+ import { ChatDatabase } from './database'
8
+ import { PluginLogger } from './logger'
9
+ import { ContactCacheQuery, ContactCacheResult, HistoryQuery, HistoryResult, PluginConfigPayload, SatoriBootstrap } from './types'
10
+
11
+ function resolveEndpoint(ctx: Context, config: Config): string {
12
+ const url = ctx.satori?.server?.url ?? '/satori'
13
+ const clean = url.startsWith('undefined') ? url.slice(9) : url
14
+ if (/^https?:\/\//i.test(clean)) return clean
15
+ const base = ctx.server?.selfUrl ?? ctx.server?.config?.selfUrl ?? ''
16
+ return `${base}${clean.startsWith('/') ? clean : `/${clean}`}`
17
+ }
18
+
19
+ export function registerBootstrap(
20
+ ctx: Context,
21
+ config: Config,
22
+ database: ChatDatabase,
23
+ logger: PluginLogger,
24
+ ) {
25
+ ctx.console.addListener('chat-patch/bootstrap', (): SatoriBootstrap => {
26
+ logger.logInfo('返回 Satori bootstrap:', {
27
+ endpoint: resolveEndpoint(ctx, config),
28
+ basePath: config.basePath,
29
+ })
30
+ return {
31
+ endpoint: resolveEndpoint(ctx, config),
32
+ token: ctx.satori?.server?.config?.token ?? '',
33
+ basePath: config.basePath,
34
+ }
35
+ }, { authority: 4 })
36
+
37
+ ctx.console.addListener('chat-patch/history', async (query: HistoryQuery): Promise<HistoryResult> => {
38
+ const messages = await database.listMessages(
39
+ query.platform,
40
+ query.selfId,
41
+ query.channelId,
42
+ query.limit,
43
+ )
44
+ return { messages }
45
+ }, { authority: 4 })
46
+
47
+ ctx.console.addListener('chat-patch/config', (): PluginConfigPayload => {
48
+ return {
49
+ basePath: config.basePath,
50
+ maxMessagesPerChannel: config.maxMessagesPerChannel,
51
+ historyPageSize: config.historyPageSize,
52
+ maxMediaFiles: config.maxMediaFiles,
53
+ blockedPlatforms: config.blockedPlatforms ?? [],
54
+ loggerinfo: config.loggerinfo,
55
+ }
56
+ }, { authority: 4 })
57
+
58
+ ctx.console.addListener('chat-patch/contact-cache', async (query: ContactCacheQuery): Promise<ContactCacheResult> => {
59
+ if (query.contacts) {
60
+ if (query.append) {
61
+ for (const contact of query.contacts) {
62
+ await database.appendContact(query.platform, query.selfId, query.type, contact)
63
+ }
64
+ return { contacts: await database.getContacts(query.platform, query.selfId, query.type) }
65
+ }
66
+ await database.setContacts(query.platform, query.selfId, query.type, query.contacts)
67
+ return { contacts: query.contacts }
68
+ }
69
+ return { contacts: await database.getContacts(query.platform, query.selfId, query.type) }
70
+ }, { authority: 4 })
71
+ }
package/src/cache.ts ADDED
@@ -0,0 +1,319 @@
1
+ import { Bot, Context, Universal } from 'koishi'
2
+
3
+ import { ChatDatabase } from './database'
4
+ import { ContactCacheItem } from './types'
5
+ import { PluginLogger } from './logger'
6
+
7
+ function getString(value: unknown): string {
8
+ return typeof value === 'string' ? value : ''
9
+ }
10
+
11
+ function isUsableName(value: string, id: string): boolean {
12
+ return Boolean(value) && value !== id && !/unknown user|unknown guild|unknown channel/i.test(value)
13
+ }
14
+
15
+ function normalizeGroupId(value: string): string {
16
+ const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, '').trim()
17
+ const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/)
18
+ return wrapped ? wrapped[1] : raw || value
19
+ }
20
+
21
+ function isPrivateChannelType(value: unknown): boolean {
22
+ const num = Number(value)
23
+ if (Number.isFinite(num)) return num === 1
24
+ const text = String(value ?? '').toLowerCase()
25
+ return text === 'direct' || text === 'private'
26
+ }
27
+
28
+ function normalizeCacheType(type: string): 'group' | 'friend' {
29
+ if (type === 'user' || type === 'friend') return 'friend'
30
+ return 'group'
31
+ }
32
+
33
+ function encodeKeyPart(value: string): string {
34
+ return encodeURIComponent(value)
35
+ }
36
+
37
+ type FetchContact = (bot: Bot<Context>) => Promise<ContactCacheItem | null>
38
+
39
+ export class ContactCacheService {
40
+ private inflight = new Map<string, Promise<ContactCacheItem | null>>()
41
+
42
+ constructor(
43
+ private ctx: Context,
44
+ private database: ChatDatabase,
45
+ private logger: PluginLogger,
46
+ ) {}
47
+
48
+ // 查询单用户缓存;未命中时由后端调用 bot.getUser,并写入数据库
49
+ async getUser(
50
+ platform: string,
51
+ selfId: string,
52
+ userId: string,
53
+ guildId?: string,
54
+ channelId?: string,
55
+ fallbackName?: string,
56
+ fallbackAvatar?: string,
57
+ ): Promise<ContactCacheItem | null> {
58
+ if (!userId || userId === '0') return null
59
+ const guild = guildId && guildId !== '0' ? guildId : undefined
60
+ const channel = channelId && channelId !== '0' ? channelId : undefined
61
+ const item = await this.resolve(platform, selfId, 'user', userId, async (bot) => {
62
+ let user: Universal.User | null = null
63
+ let member: Universal.GuildMember | null = null
64
+ try {
65
+ user = await bot.getUser(userId, guild)
66
+ } catch (error) {
67
+ this.logger.logInfo('用户身份回源失败:', userId, error)
68
+ }
69
+
70
+ let name = getString(user?.name)
71
+ || getString(user?.nick)
72
+ || getString(user?.username)
73
+ || getString(user?.nickname)
74
+ let avatar = getString(user?.avatar)
75
+
76
+ if (guild && (!name || !avatar)) {
77
+ try {
78
+ member = await bot.getGuildMember(guild, userId)
79
+ } catch (error) {
80
+ this.logger.logInfo('群成员身份回源失败:', userId, error)
81
+ }
82
+ name = name
83
+ || getString(member?.name)
84
+ || getString(member?.nick)
85
+ || getString(member?.user?.name)
86
+ || getString(member?.user?.nick)
87
+ avatar = avatar
88
+ || getString(member?.avatar)
89
+ || getString(member?.user?.avatar)
90
+ }
91
+
92
+ const eventName = getString(fallbackName)
93
+ const eventAvatar = getString(fallbackAvatar)
94
+ if (!isUsableName(name, userId) && isUsableName(eventName, userId)) {
95
+ name = eventName
96
+ }
97
+ if (!avatar && eventAvatar) avatar = eventAvatar
98
+
99
+ if (!name && !avatar) return null
100
+ this.logger.logInfo('用户身份已回源并写入缓存:', userId, name)
101
+ return {
102
+ id: userId,
103
+ name: name || userId,
104
+ avatar: avatar || undefined,
105
+ raw: {
106
+ id: userId,
107
+ user_id: userId,
108
+ name,
109
+ nickname: name,
110
+ avatar: avatar || undefined,
111
+ channel_id: channel || undefined,
112
+ guild_id: guild || undefined,
113
+ },
114
+ }
115
+ }, {
116
+ name: fallbackName,
117
+ avatar: fallbackAvatar,
118
+ })
119
+ if (!item) return null
120
+ const needChannel = Boolean(channel && item.channelId !== channel)
121
+ const needGuild = Boolean(guild && item.guildId !== guild)
122
+ if (channel) item.channelId = channel
123
+ if (guild) item.guildId = guild
124
+ if (needChannel || needGuild) {
125
+ if (item.raw && typeof item.raw === 'object') {
126
+ const raw = item.raw as Record<string, unknown>
127
+ if (channel) raw.channel_id = channel
128
+ if (guild) raw.guild_id = guild
129
+ }
130
+ await this.database.appendContact(platform, selfId, 'friend', item)
131
+ }
132
+ return item
133
+ }
134
+
135
+ // 查询群/频道缓存;未命中时由后端调用 guild.get/channel.get,并写入数据库
136
+ async getGroup(
137
+ platform: string,
138
+ selfId: string,
139
+ id: string,
140
+ guildId?: string,
141
+ channelId?: string,
142
+ fallbackName?: string,
143
+ fallbackAvatar?: string,
144
+ channelType?: unknown,
145
+ ): Promise<ContactCacheItem | null> {
146
+ const guild = guildId && guildId !== '0' ? guildId : undefined
147
+ const channel = channelId && channelId !== '0' ? channelId : undefined
148
+ if (isPrivateChannelType(channelType)) return null
149
+ if (channelType === undefined
150
+ && (/^(?:private|direct):/i.test(id) || /^(?:private|direct):/i.test(channel || ''))) return null
151
+ const rawId = id && id !== '0' ? normalizeGroupId(id) : ''
152
+ const cacheId = rawId || guild || normalizeGroupId(channel || '') || ''
153
+ if (!cacheId) return null
154
+ const item = await this.resolve(platform, selfId, 'group', cacheId, async (bot) => {
155
+ const [guildData, channelData] = await Promise.all([
156
+ guild
157
+ ? this.safeGet(() => bot.getGuild(guild))
158
+ : cacheId
159
+ ? this.safeGet(() => bot.getGuild(cacheId))
160
+ : Promise.resolve(null),
161
+ channel
162
+ ? this.safeGet(() => bot.getChannel(channel, guild))
163
+ : Promise.resolve(null),
164
+ ])
165
+
166
+ let name = getString(guildData?.name) || getString(channelData?.name)
167
+ let avatar = getString(guildData?.avatar) || getString(channelData?.avatar)
168
+ const eventName = getString(fallbackName)
169
+ const eventAvatar = getString(fallbackAvatar)
170
+ if (!isUsableName(name, cacheId) && isUsableName(eventName, cacheId)) {
171
+ name = eventName
172
+ }
173
+ if (!avatar && eventAvatar) avatar = eventAvatar
174
+ if (!name && !avatar) return null
175
+ this.logger.logInfo('群组身份已回源并写入缓存:', cacheId, name)
176
+ return {
177
+ id: cacheId,
178
+ name: name || cacheId,
179
+ avatar: avatar || undefined,
180
+ raw: {
181
+ id: cacheId,
182
+ group_id: cacheId,
183
+ name,
184
+ group_name: name,
185
+ avatar: avatar || undefined,
186
+ channel_id: channel || undefined,
187
+ channel_type: channelType ?? undefined,
188
+ guild_id: guild || undefined,
189
+ },
190
+ }
191
+ }, {
192
+ name: fallbackName,
193
+ avatar: fallbackAvatar,
194
+ })
195
+ if (!item) return null
196
+ const needChannel = Boolean(channel && item.channelId !== channel)
197
+ const needGuild = Boolean(guild && item.guildId !== guild)
198
+ if (channel) item.channelId = channel
199
+ if (guild) item.guildId = guild
200
+ if (needChannel || needGuild) {
201
+ if (item.raw && typeof item.raw === 'object') {
202
+ const raw = item.raw as Record<string, unknown>
203
+ if (channel) raw.channel_id = channel
204
+ if (guild) raw.guild_id = guild
205
+ }
206
+ await this.database.appendContact(platform, selfId, 'group', item)
207
+ }
208
+ return item
209
+ }
210
+
211
+ // 收到群消息时更新该群对应的群友表
212
+ async recordGroupMember(
213
+ platform: string,
214
+ selfId: string,
215
+ groupId: string,
216
+ member: ContactCacheItem,
217
+ ) {
218
+ if (!platform || !selfId || !groupId || !member.id) return
219
+ await this.database.appendGroupMember(platform, selfId, groupId, member)
220
+ }
221
+
222
+ // 同一身份的并发请求合并,避免首条消息同时触发多个回源
223
+ private resolve(
224
+ platform: string,
225
+ selfId: string,
226
+ type: string,
227
+ id: string,
228
+ fetcher: FetchContact,
229
+ fallback?: { name?: string; avatar?: string },
230
+ ): Promise<ContactCacheItem | null> {
231
+ const canonical = normalizeCacheType(type)
232
+ const key = [platform, selfId, canonical, id].map(encodeKeyPart).join(':')
233
+ const existing = this.inflight.get(key)
234
+ if (existing) return existing
235
+
236
+ const task = this.resolveUncached(platform, selfId, canonical, id, fetcher, fallback)
237
+ .finally(() => this.inflight.delete(key))
238
+ this.inflight.set(key, task)
239
+ return task
240
+ }
241
+
242
+ private async resolveUncached(
243
+ platform: string,
244
+ selfId: string,
245
+ type: 'group' | 'friend',
246
+ id: string,
247
+ fetcher: FetchContact,
248
+ fallback?: { name?: string; avatar?: string },
249
+ ): Promise<ContactCacheItem | null> {
250
+ const cached = await this.database.getContact(platform, selfId, type, id)
251
+ if (cached && !this.shouldRefreshWithFallback(cached, id, fallback)) {
252
+ return this.publicContact(cached)
253
+ }
254
+
255
+ const bot = this.ctx.bots.find((item) => {
256
+ return item.platform === platform && item.selfId === selfId
257
+ })
258
+ if (!bot) {
259
+ const fallbackItem = this.toFallbackContact(id, fallback)
260
+ if (fallbackItem) {
261
+ await this.database.appendContact(platform, selfId, type, fallbackItem)
262
+ return this.publicContact(fallbackItem)
263
+ }
264
+ return null
265
+ }
266
+
267
+ const item = await fetcher(bot)
268
+ if (!item) return null
269
+ await this.database.appendContact(platform, selfId, type, item)
270
+ return this.publicContact(item)
271
+ }
272
+
273
+ private shouldRefreshWithFallback(
274
+ cached: ContactCacheItem,
275
+ id: string,
276
+ fallback?: { name?: string; avatar?: string },
277
+ ): boolean {
278
+ if (!fallback) return false
279
+ const cachedNameUsable = isUsableName(cached.name, id)
280
+ const fallbackNameUsable = isUsableName(getString(fallback.name), id)
281
+ const hasCachedAvatar = Boolean(cached.avatar)
282
+ const hasFallbackAvatar = Boolean(getString(fallback.avatar))
283
+ return (!cachedNameUsable && fallbackNameUsable) || (!hasCachedAvatar && hasFallbackAvatar)
284
+ }
285
+
286
+ private toFallbackContact(
287
+ id: string,
288
+ fallback?: { name?: string; avatar?: string },
289
+ ): ContactCacheItem | null {
290
+ const name = isUsableName(getString(fallback?.name), id) ? getString(fallback.name) : ''
291
+ const avatar = getString(fallback?.avatar)
292
+ if (!name && !avatar) return null
293
+ return {
294
+ id,
295
+ name: name || id,
296
+ avatar: avatar || undefined,
297
+ }
298
+ }
299
+
300
+ private async safeGet<T>(getter: () => Promise<T>): Promise<T | null> {
301
+ try {
302
+ return await getter()
303
+ } catch (error) {
304
+ this.logger.logInfo('群组身份回源失败:', error)
305
+ return null
306
+ }
307
+ }
308
+
309
+ private publicContact(item: ContactCacheItem): ContactCacheItem {
310
+ return {
311
+ id: item.id,
312
+ name: item.name,
313
+ avatar: item.avatar || undefined,
314
+ channelId: item.channelId || undefined,
315
+ guildId: item.guildId || undefined,
316
+ raw: item.raw ?? undefined,
317
+ }
318
+ }
319
+ }
package/src/config.ts CHANGED
@@ -1,47 +1,35 @@
1
1
  import { Schema } from 'koishi'
2
2
 
3
+ export interface BlockedPlatform {
4
+ platformName: string
5
+ exactMatch: boolean
6
+ }
7
+
3
8
  export interface Config {
4
9
  loggerinfo: boolean
5
- clearIndexedDBOnStart: boolean
10
+ basePath: string
6
11
  maxMessagesPerChannel: number
7
- messageChunkSize: number
8
- channelCacheLimit: number
9
- maxPersistImages: number
10
- blockedPlatforms: Array<{
11
- platformName: string
12
- exactMatch: boolean
13
- }>
12
+ historyPageSize: number
13
+ maxMediaFiles: number
14
+ blockedPlatforms: BlockedPlatform[]
14
15
  }
15
16
 
16
17
  export const Config: Schema<Config> = Schema.intersect([
17
18
  Schema.object({
18
- maxMessagesPerChannel: Schema.number().default(500).description('每个群组最大保存消息数量').min(50).max(1500).step(1),
19
- messageChunkSize: Schema.number().default(100).description('单个消息分块文件最大消息数量').min(20).max(500).step(1),
20
- channelCacheLimit: Schema.number().default(50).description('内存中最多缓存的频道消息数量').min(1).max(200).step(1),
21
- maxPersistImages: Schema.number().default(100).description('持久化存储的图片缓存数量').min(10).max(500).step(1),
22
- blockedPlatforms: Schema.array(Schema.object({
23
- platformName: Schema.string().description('平台名称或关键词'),
24
- exactMatch: Schema.boolean().default(false).description('完全匹配?如果关闭,包含关键词即屏蔽').default(true)
25
- })).role('table').description('屏蔽的平台列表').default(
26
- [
27
- {
28
- "platformName": "qq",
29
- "exactMatch": true
30
- },
31
- {
32
- "platformName": "qqguild",
33
- "exactMatch": true
34
- },
35
- {
36
- "platformName": "sandbox",
37
- "exactMatch": false
38
- }
39
- ]
40
- ),
19
+ basePath: Schema.string().default('/chat-patch').description('Web 聊天室挂载路径'),
20
+ maxMessagesPerChannel: Schema.number().default(500).min(50).max(5000).step(1).description('每个频道最多保存的历史消息数'),
21
+ historyPageSize: Schema.number().default(50).min(10).max(500).step(1).description('历史消息分页大小'),
22
+ maxMediaFiles: Schema.number().default(200).min(20).max(2000).step(1).description('媒体缓存文件上限'),
41
23
  }).description('基础设置'),
42
-
43
24
  Schema.object({
44
- clearIndexedDBOnStart: Schema.boolean().default(true).description('启动时强制清空IndexedDB缓存(适用于紧急情况,防止浏览器卡死)'),
45
- loggerinfo: Schema.boolean().default(false).description('日志调试模式').experimental(),
46
- }).description('开发者选项'),
25
+ blockedPlatforms: Schema.array(Schema.object({
26
+ platformName: Schema.string(),
27
+ exactMatch: Schema.boolean().default(true),
28
+ })).role('table').description('不记录历史的平台').default([
29
+ { platformName: 'qq', exactMatch: true },
30
+ { platformName: 'qqguild', exactMatch: true },
31
+ { platformName: 'sandbox', exactMatch: false },
32
+ ]),
33
+ loggerinfo: Schema.boolean().default(false).description('调试日志开关').hidden().experimental(),
34
+ }).description('高级设置'),
47
35
  ])