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
@@ -0,0 +1,2042 @@
1
+ // Satori 连接适配层:向现有 UI 暴露兼容的 Connector 接口。
2
+
3
+ import { reactive } from 'vue'
4
+ import { dispatch, msgPreprocess } from './msg'
5
+ import {
6
+ connect as connectSatori,
7
+ getActiveBot,
8
+ getBootstrap,
9
+ getLogins,
10
+ request,
11
+ requestConsole,
12
+ setActiveBot,
13
+ type SatoriEvent,
14
+ } from './satori'
15
+ import {
16
+ mapAction,
17
+ satoriEventToOneBot,
18
+ satoriResponseToOneBot,
19
+ } from './satori-model'
20
+ import { buildForwardMessage } from './sender'
21
+ import { Logger, LogType } from './base'
22
+ import { isDebugMode } from './debug'
23
+ import { useAuthStore } from '../state/auth'
24
+ import { useChatStore } from '../state/chat'
25
+ import { useContactStore } from '../state/contact'
26
+ import { useSettingsStore } from '../state/settings'
27
+ import { getMsgRawTxt, hasAtMe, updateBaseOnMsgList } from './utils/msgUtil'
28
+ import { normalizeSessionId } from './utils/sessionUtil'
29
+ import { buildQqGroupAvatar } from './utils/avatarUtil'
30
+ import type { ConnectionHistoryItem, LoginCacheElem } from './elements/system'
31
+ import type { UserFriendElem, UserGroupElem } from './elements/information'
32
+
33
+ const HISTORY_KEY = 'chat-patch:connection-history'
34
+ const logger = new Logger()
35
+ const unsupportedMethods = new Set<string>()
36
+
37
+ function encodeKeyPart(value: string): string {
38
+ return encodeURIComponent(value)
39
+ }
40
+
41
+ function getChannelKind(value: unknown): 'group' | 'direct' | 'unknown' {
42
+ if (typeof value === 'number') {
43
+ if (value === 0) return 'group'
44
+ if (value === 1) return 'direct'
45
+ return 'unknown'
46
+ }
47
+ if (typeof value === 'string' && value !== '') {
48
+ const num = Number(value)
49
+ if (Number.isFinite(num)) {
50
+ if (num === 0) return 'group'
51
+ if (num === 1) return 'direct'
52
+ return 'unknown'
53
+ }
54
+ const lower = value.toLowerCase()
55
+ if (['text', 'group', 'room', 'chat', 'channel'].includes(lower)) return 'group'
56
+ if (lower === 'direct' || lower === 'private') return 'direct'
57
+ }
58
+ return 'unknown'
59
+ }
60
+
61
+ function isGroupMessage(msg: Record<string, unknown>): boolean {
62
+ const kind = getChannelKind(msg.channel_type)
63
+ if (kind !== 'unknown') return kind === 'group'
64
+ const messageType = getString(msg.message_type)
65
+ if (messageType) return messageType === 'group'
66
+ return Boolean(msg.group_id)
67
+ }
68
+
69
+ function isPrivateMessage(msg: Record<string, unknown>): boolean {
70
+ const kind = getChannelKind(msg.channel_type)
71
+ if (kind !== 'unknown') return kind === 'direct'
72
+ const messageType = getString(msg.message_type)
73
+ if (messageType) return messageType === 'private'
74
+ return !Boolean(msg.group_id)
75
+ }
76
+
77
+ function identityDebug(...args: unknown[]) {
78
+ if (isDebugMode()) {
79
+ console.log('[chat-patch-identity]', ...args)
80
+ }
81
+ }
82
+
83
+ interface CachedIdentityInfo {
84
+ name: string
85
+ avatar: string
86
+ raw: Record<string, unknown>
87
+ }
88
+
89
+ const identityInfoCache = new Map<string, CachedIdentityInfo>()
90
+ const identityInflight = new Set<string>()
91
+
92
+ function identityCacheKey(prefix: string, platform: string, selfId: string, id: string) {
93
+ return [prefix, platform, selfId, id].map(encodeKeyPart).join(':')
94
+ }
95
+
96
+ function markIdentityLookup(key: string, _hasAvatar: boolean, info?: CachedIdentityInfo) {
97
+ // 缺失也记录一次,避免同一会话内反复回源
98
+ identityInfoCache.set(key, info ?? { name: '', avatar: '', raw: {} })
99
+ }
100
+
101
+ function hasIdentityData(info: CachedIdentityInfo | undefined): boolean {
102
+ // 未查询过时允许回源;只有明确空结果才跳过
103
+ return !info || Boolean(info.name || info.avatar)
104
+ }
105
+
106
+ export function getCachedUserAvatar(platform: string, selfId: string, userId: string): string {
107
+ if (userId === selfId) {
108
+ const login = getLogins().find((item) => {
109
+ return item.platform === platform && item.selfId === selfId
110
+ })
111
+ if (hasUsableAvatar(login?.avatar)) return getString(login.avatar)
112
+ }
113
+ const info = identityInfoCache.get(identityCacheKey('user', platform, selfId, userId))
114
+ return hasUsableAvatar(info?.avatar) ? getString(info.avatar) : ''
115
+ }
116
+
117
+ export function requestUserAvatar(
118
+ platform: string,
119
+ selfId: string,
120
+ userId: string,
121
+ msg: Record<string, unknown>,
122
+ ) {
123
+ if (!userId || userId === '0') return
124
+ void fetchUserIdentity(platform, selfId, userId, null, msg)
125
+ }
126
+
127
+ export let websocket: WebSocket | undefined = undefined
128
+ let disposeConnection: (() => void) | null = null
129
+ const pendingBotEvents = new Map<string, SatoriEvent[]>()
130
+
131
+ function botEventKey(platform: string, selfId: string) {
132
+ return [platform, selfId].map(encodeKeyPart).join(':')
133
+ }
134
+
135
+ function isActiveBot(bot: { platform: string; selfId: string }) {
136
+ const active = getActiveBot()
137
+ return Boolean(active && active.platform === bot.platform && active.selfId === bot.selfId)
138
+ }
139
+
140
+ function isCurrentBotEvent(event: SatoriEvent) {
141
+ const active = getActiveBot()
142
+ return Boolean(
143
+ active &&
144
+ event.platform === active.platform &&
145
+ event.selfId === active.selfId,
146
+ )
147
+ }
148
+
149
+ function readHistory(): ConnectionHistoryItem[] {
150
+ try {
151
+ const raw = localStorage.getItem(HISTORY_KEY)
152
+ const parsed = raw ? JSON.parse(raw) as unknown : []
153
+ return Array.isArray(parsed) ? parsed as ConnectionHistoryItem[] : []
154
+ } catch {
155
+ return []
156
+ }
157
+ }
158
+
159
+ function writeHistory(history: ConnectionHistoryItem[]) {
160
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(history))
161
+ }
162
+
163
+ export const login: LoginCacheElem = reactive({
164
+ quickLogin: null,
165
+ address: '',
166
+ token: '',
167
+ status: false,
168
+ creating: false,
169
+ connectionHistory: readHistory(),
170
+ })
171
+
172
+ export function appendAccessToken(url: string, token?: string): string {
173
+ if (!token) return url
174
+ const separator = url.includes('?') ? '&' : '?'
175
+ return `${url}${separator}access_token=${encodeURIComponent(token)}`
176
+ }
177
+
178
+ export function decodeStoredToken(token: string): string {
179
+ return token
180
+ }
181
+
182
+ function setJsonMap() {
183
+ const authStore = useAuthStore()
184
+ authStore.jsonMap = {
185
+ version_info: '$.data',
186
+ login_info: {
187
+ uin: '$.data.user_id',
188
+ nickname: '$.data.nickname',
189
+ },
190
+ message_list: {
191
+ name: 'get_group_msg_history',
192
+ private_name: 'get_friend_msg_history',
193
+ source: '$.data.messages[*]',
194
+ type: '$.data',
195
+ pagerType: 'full',
196
+ message_value: {
197
+ image: { url: '$.file' },
198
+ },
199
+ list: {
200
+ message_id: '/message_id',
201
+ message_type: '/message_type',
202
+ time: '/time',
203
+ group_id: '/group_id',
204
+ sender: '/sender',
205
+ message: '/message',
206
+ raw_message: '/raw_message',
207
+ },
208
+ },
209
+ message_info: {
210
+ message_id: '$.message_id',
211
+ private_id: '$.user_id',
212
+ group_id: '$.group_id',
213
+ target_id: '$.target_id',
214
+ sender: '$.sender.user_id',
215
+ },
216
+ }
217
+ }
218
+
219
+ function pickAvatar(entity: Record<string, unknown>, root: Record<string, unknown>): string {
220
+ return getString(entity.avatar)
221
+ || getString(root.avatar)
222
+ || getString(entity.avatar_url)
223
+ || getString(root.avatar_url)
224
+ || getString(entity.avatarUrl)
225
+ || getString(root.avatarUrl)
226
+ || getString(entity.icon)
227
+ || getString(root.icon)
228
+ }
229
+
230
+ function hasUsableAvatar(value: unknown): boolean {
231
+ const avatar = getString(value)
232
+ if (!avatar || avatar === '/img/icons/icon.svg') return false
233
+ if (avatar.includes('/proxy/chatfile') || avatar.includes('proxy/chatfile')) {
234
+ try {
235
+ const parsed = new URL(avatar, location.origin)
236
+ return Boolean(parsed.searchParams.get('url'))
237
+ } catch {
238
+ return false
239
+ }
240
+ }
241
+ return true
242
+ }
243
+
244
+ function applyUserIdentity(
245
+ userId: string,
246
+ session: Record<string, unknown> | null,
247
+ msg: Record<string, unknown>,
248
+ name: string,
249
+ avatar: string,
250
+ channelId = '',
251
+ ) {
252
+ if (name && session) {
253
+ session.nickname = name
254
+ session.remark = name
255
+ }
256
+ if (avatar && session) session.avatar = avatar
257
+ if (channelId && session && !getString(session.channel_id)) {
258
+ session.channel_id = channelId
259
+ }
260
+ if (msg.sender && typeof msg.sender === 'object') {
261
+ const sender = getObject(msg.sender)
262
+ if (avatar) sender.avatar = avatar
263
+ if (name) sender.nickname = name
264
+ }
265
+ syncUserIdentity(userId, name, avatar, channelId)
266
+ const chatStore = useChatStore()
267
+ const applySenderAvatar = (item: Record<string, unknown>) => {
268
+ const itemSender = getObject(item.sender)
269
+ if (String(itemSender.user_id) !== userId) return
270
+ if (avatar) itemSender.avatar = avatar
271
+ if (name) itemSender.nickname = name
272
+ }
273
+ chatStore.messageList.forEach(applySenderAvatar)
274
+ for (const messages of chatStore.sessionMessageCache.values()) {
275
+ messages.forEach(applySenderAvatar)
276
+ }
277
+ }
278
+
279
+ function syncUserIdentity(userId: string, name: string, avatar: string, channelId = '') {
280
+ const contactStore = useContactStore()
281
+ const applyUser = (target: Record<string, unknown>) => {
282
+ if (name) {
283
+ target.nickname = name
284
+ target.remark = name
285
+ }
286
+ if (avatar) target.avatar = avatar
287
+ else if (target.avatar && !hasUsableAvatar(target.avatar)) delete target.avatar
288
+ if (channelId) {
289
+ if (!getString(target.channel_id)) target.channel_id = channelId
290
+ if (!getString(target.channelId)) target.channelId = channelId
291
+ }
292
+ }
293
+ const globalSession = contactStore.baseOnMsgList.get(String(userId))
294
+ if (globalSession) {
295
+ applyUser(globalSession as unknown as Record<string, unknown>)
296
+ contactStore.baseOnMsgList.set(String(userId), globalSession)
297
+ }
298
+ const globalItem = contactStore.onMsgList.find((item) => {
299
+ return String(item.user_id) === userId
300
+ })
301
+ if (globalItem) {
302
+ applyUser(globalItem as unknown as Record<string, unknown>)
303
+ contactStore.onMsgList = [...contactStore.onMsgList]
304
+ }
305
+ const friendItem = contactStore.userList.find((item) => {
306
+ return String(item.user_id) === userId
307
+ })
308
+ if (friendItem) {
309
+ applyUser(friendItem as unknown as Record<string, unknown>)
310
+ contactStore.userList = [...contactStore.userList]
311
+ }
312
+ updateBaseOnMsgList()
313
+ }
314
+
315
+ function syncGroupIdentity(
316
+ sessionId: string,
317
+ apply: (target: Record<string, unknown>) => void,
318
+ ) {
319
+ const contactStore = useContactStore()
320
+ const globalSession = contactStore.baseOnMsgList.get(String(sessionId))
321
+ if (globalSession) {
322
+ apply(globalSession as unknown as Record<string, unknown>)
323
+ contactStore.baseOnMsgList.set(String(sessionId), globalSession)
324
+ }
325
+ const globalItem = contactStore.onMsgList.find((item) => {
326
+ return String(item.group_id) === sessionId
327
+ })
328
+ if (globalItem) {
329
+ apply(globalItem as unknown as Record<string, unknown>)
330
+ contactStore.onMsgList = [...contactStore.onMsgList]
331
+ }
332
+ const groupItem = contactStore.userList.find((item) => {
333
+ return String(item.group_id) === sessionId
334
+ })
335
+ if (groupItem) {
336
+ apply(groupItem as unknown as Record<string, unknown>)
337
+ contactStore.userList = [...contactStore.userList]
338
+ }
339
+ updateBaseOnMsgList()
340
+ }
341
+
342
+ function updateBotStateGroup(
343
+ platform: string,
344
+ selfId: string,
345
+ sessionId: string,
346
+ apply: (target: Record<string, unknown>) => void,
347
+ ) {
348
+ const contactStore = useContactStore()
349
+ const state = contactStore.botStates.get(selfId)
350
+ const session = state?.onMsgList.find((item) => String(item.group_id) === sessionId)
351
+ if (!state || !session) return
352
+ apply(session as unknown as Record<string, unknown>)
353
+ const key = normalizeSessionId(String(sessionId))
354
+ const baseList = [...state.baseList]
355
+ const baseIndex = baseList.findIndex(([baseKey]) => normalizeSessionId(String(baseKey)) === key)
356
+ const typedSession = session as unknown as UserFriendElem & UserGroupElem
357
+ if (baseIndex >= 0) {
358
+ baseList[baseIndex] = [key, typedSession]
359
+ } else {
360
+ baseList.push([key, typedSession])
361
+ }
362
+ contactStore.botStates.set(selfId, {
363
+ userList: [...state.userList],
364
+ baseList,
365
+ onMsgList: [...state.onMsgList],
366
+ })
367
+ }
368
+
369
+ async function resolveGroupIdentity(
370
+ platform: string,
371
+ selfId: string,
372
+ sessionId: string,
373
+ guildId: string,
374
+ channelId: string,
375
+ session: Record<string, unknown>,
376
+ msg: Record<string, unknown>,
377
+ ) {
378
+ if (!sessionId || sessionId === '0') return
379
+ const key = identityCacheKey('group', platform, selfId, sessionId)
380
+ const cachedInfo = identityInfoCache.get(key)
381
+ if (!hasIdentityData(cachedInfo)) {
382
+ identityDebug('group identity: skipped', { platform, selfId, sessionId, key })
383
+ return
384
+ }
385
+
386
+ const eventName = typeof msg.group_name === 'string' ? msg.group_name : ''
387
+ const eventAvatar = hasUsableAvatar(
388
+ typeof msg.group_avatar === 'string' ? msg.group_avatar : '',
389
+ ) ? (typeof msg.group_avatar === 'string' ? msg.group_avatar : '') : ''
390
+ const generatedAvatar = buildQqGroupAvatar(platform, sessionId)
391
+ identityDebug('group identity: request cache', { platform, selfId, sessionId, guildId, channelId })
392
+ const cacheResult = await requestIdentityCache({
393
+ platform,
394
+ selfId,
395
+ type: 'group',
396
+ id: sessionId,
397
+ guildId,
398
+ channelId,
399
+ name: eventName,
400
+ avatar: eventAvatar || generatedAvatar,
401
+ channelType: msg.channel_type,
402
+ }).catch(() => null)
403
+ const cached = getObject(cacheResult)
404
+ const cachedName = getString(cached.name)
405
+ const cachedAvatar = hasUsableAvatar(cached.avatar)
406
+ ? getString(cached.avatar)
407
+ : generatedAvatar
408
+ const cachedChannelId = getString(cached.channel_id) || getString(cached.channelId) || channelId
409
+ const cachedGuildId = getString(cached.guild_id) || getString(cached.guildId) || guildId
410
+ if (cachedName || cachedAvatar) {
411
+ const cachedApply = (target: Record<string, unknown>) => {
412
+ if (cachedName) target.group_name = cachedName
413
+ if (cachedAvatar) target.avatar = cachedAvatar
414
+ if (cachedChannelId) target.channel_id = cachedChannelId
415
+ if (cachedGuildId) target.guild_id = cachedGuildId
416
+ }
417
+ cachedApply(session)
418
+ syncGroupIdentity(sessionId, cachedApply)
419
+ updateBotStateGroup(platform, selfId, sessionId, cachedApply)
420
+ markIdentityLookup(key, true, { name: cachedName || sessionId, avatar: cachedAvatar, raw: cached })
421
+ return
422
+ }
423
+
424
+ // 后端回源失败时只使用事件自带的字段,不再每次消息都请求 guild/channel
425
+ const name = eventName
426
+ const avatar = eventAvatar || generatedAvatar
427
+ if (!name && !avatar) {
428
+ markIdentityLookup(key, false)
429
+ return
430
+ }
431
+
432
+ const applyGroup = (target: Record<string, unknown>) => {
433
+ if (name) target.group_name = name
434
+ if (avatar) target.avatar = avatar
435
+ else if (target.avatar && !hasUsableAvatar(target.avatar)) delete target.avatar
436
+ if (channelId) target.channel_id = channelId
437
+ if (guildId) target.guild_id = guildId
438
+ }
439
+ applyGroup(session)
440
+ syncGroupIdentity(sessionId, applyGroup)
441
+ updateBotStateGroup(platform, selfId, sessionId, applyGroup)
442
+ markIdentityLookup(key, true, { name, avatar, raw: { group_name: name, avatar } })
443
+ }
444
+
445
+ async function fetchUserIdentity(
446
+ platform: string,
447
+ selfId: string,
448
+ userId: string,
449
+ session: Record<string, unknown> | null,
450
+ msg: Record<string, unknown>,
451
+ ) {
452
+ if (!userId || userId === '0') return
453
+ const key = identityCacheKey('user', platform, selfId, userId)
454
+ const inflightKey = `user:${key}`
455
+ if (identityInflight.has(inflightKey)) return
456
+ identityInflight.add(inflightKey)
457
+ try {
458
+ if (userId === selfId) {
459
+ const login = getLogins().find((item) => {
460
+ return item.platform === platform && item.selfId === selfId
461
+ })
462
+ if (login) {
463
+ const loginName = login.name || userId
464
+ const loginAvatar = login.avatar || ''
465
+ applyUserIdentity(userId, session, msg, loginName, loginAvatar)
466
+ markIdentityLookup(key, true, {
467
+ name: loginName,
468
+ avatar: loginAvatar,
469
+ raw: {
470
+ id: userId,
471
+ user_id: userId,
472
+ nickname: loginName,
473
+ name: loginName,
474
+ avatar: loginAvatar,
475
+ },
476
+ })
477
+ return
478
+ }
479
+ }
480
+ const cachedInfo = identityInfoCache.get(key)
481
+ if (!hasIdentityData(cachedInfo)) {
482
+ identityDebug('user identity: skipped', {
483
+ platform,
484
+ selfId,
485
+ userId,
486
+ })
487
+ return
488
+ }
489
+
490
+ const guildId = getString(msg.guild_id) || getString(msg.group_id)
491
+ const isPrivate = isPrivateMessage(msg)
492
+ const channelId = isPrivate
493
+ ? (getString(msg.channel_id) || `private:${userId}`)
494
+ : ''
495
+ const sender = getObject(msg.sender)
496
+ const senderName = getString(sender.nickname) || getString(sender.card) || getString(sender.name)
497
+ const senderAvatar = hasUsableAvatar(sender.avatar) ? getString(sender.avatar) : ''
498
+ identityDebug('user identity: request cache', { platform, selfId, userId, guildId, channelId, senderName })
499
+ const data = await requestIdentityCache({
500
+ platform,
501
+ selfId,
502
+ type: 'user',
503
+ id: userId,
504
+ guildId,
505
+ channelId: channelId || undefined,
506
+ name: senderName,
507
+ avatar: senderAvatar,
508
+ }).catch(() => null)
509
+ const root = getObject(data)
510
+ const name = getString(root.name)
511
+ || getString(root.nick)
512
+ || getString(root.nickname)
513
+ || getString(root.username)
514
+ const avatar = hasUsableAvatar(root.avatar) ? getString(root.avatar) : ''
515
+ identityDebug('user identity: cache result', { platform, selfId, userId, name, avatar })
516
+ if (!name && !avatar) {
517
+ markIdentityLookup(key, false)
518
+ return
519
+ }
520
+
521
+ const cachedChannelId = getString(root.channel_id) || getString(root.channelId) || channelId
522
+ applyUserIdentity(userId, session, msg, name, avatar, cachedChannelId)
523
+ markIdentityLookup(key, true, { name, avatar, raw: root })
524
+ } finally {
525
+ identityInflight.delete(inflightKey)
526
+ }
527
+ }
528
+
529
+ function recordBotMessage(platform: string, selfId: string, msg: Record<string, unknown>) {
530
+ if (!msg.local_time) msg.local_time = Date.now()
531
+ if (!msg.timestamp_ms) msg.timestamp_ms = Number(msg.time) * 1000 || Date.now()
532
+ identityDebug('recordBotMessage: enter', {
533
+ platform,
534
+ selfId,
535
+ post_type: msg.post_type,
536
+ message_type: msg.message_type,
537
+ group_id: msg.group_id,
538
+ user_id: msg.user_id,
539
+ channel_id: msg.channel_id,
540
+ sender: msg.sender,
541
+ })
542
+ const isGroup = isGroupMessage(msg)
543
+ const channelId = typeof msg.channel_id === 'string' ? msg.channel_id : ''
544
+ const sessionId = String(
545
+ msg.group_id
546
+ || msg.user_id
547
+ || (isGroup ? '' : channelId.replace(/^private:/, '')),
548
+ )
549
+ if (!sessionId || sessionId === '0') {
550
+ identityDebug('recordBotMessage: skipped invalid session', { sessionId, msg })
551
+ return
552
+ }
553
+ identityDebug('recordBotMessage: session', { sessionId })
554
+ const sender = getObject(msg.sender)
555
+ const hasSenderAvatar = hasUsableAvatar(sender.avatar)
556
+ const senderId = String(
557
+ sender.user_id
558
+ || msg.user_id
559
+ || (isGroup ? '' : channelId.replace(/^private:/, '')),
560
+ )
561
+ const raw = getMsgRawTxt(msg) || (typeof msg.raw_message === 'string' ? msg.raw_message : '')
562
+ const senderName = String(sender.card || sender.nickname || (isGroup ? senderId : ''))
563
+ const rawPreview = isGroup && senderName ? `${senderName}: ${raw}` : raw
564
+ const cachedGroupInfo = isGroup
565
+ ? identityInfoCache.get(identityCacheKey('group', platform, selfId, sessionId))
566
+ : undefined
567
+ const eventGroupName = typeof msg.group_name === 'string' ? msg.group_name : ''
568
+ const eventGroupAvatar = hasUsableAvatar(msg.group_avatar) ? getString(msg.group_avatar) : ''
569
+ const groupName = isGroup
570
+ ? (eventGroupName || cachedGroupInfo?.name || sessionId)
571
+ : undefined
572
+ const groupAvatar = isGroup
573
+ ? (eventGroupAvatar || cachedGroupInfo?.avatar || buildQqGroupAvatar(platform, sessionId) || undefined)
574
+ : undefined
575
+ const contactStore = useContactStore()
576
+ const state = contactStore.botStates.get(selfId) ?? {
577
+ userList: [],
578
+ baseList: [],
579
+ onMsgList: [],
580
+ }
581
+ const rawTime = Number(msg.time)
582
+ const sessionTime = Number.isFinite(rawTime) && rawTime > 0 ? rawTime : Date.now() / 1000
583
+ let session: Record<string, unknown> = {
584
+ user_id: isGroup ? undefined : sessionId,
585
+ group_id: isGroup ? sessionId : undefined,
586
+ group_name: groupName,
587
+ nickname: isGroup ? '' : String(sender.nickname ?? senderId ?? sessionId),
588
+ remark: '',
589
+ avatar: isGroup ? groupAvatar : (hasUsableAvatar(sender.avatar) ? getString(sender.avatar) : undefined),
590
+ raw_msg: rawPreview,
591
+ raw_msg_base: raw,
592
+ highlight: hasAtMe(msg) ? '[有人@你]' : undefined,
593
+ time: sessionTime,
594
+ message_id: String(msg.message_id ?? ''),
595
+ new_msg: true,
596
+ channel_id: typeof msg.channel_id === 'string' ? msg.channel_id : undefined,
597
+ guild_id: typeof msg.guild_id === 'string' ? msg.guild_id : undefined,
598
+ }
599
+ const existing = state.onMsgList.find((item) => {
600
+ return isGroup
601
+ ? String(item.group_id) === sessionId
602
+ : String(item.user_id) === sessionId
603
+ })
604
+ if (existing) {
605
+ const previousAvatar = existing.avatar
606
+ const previousGroupName = existing.group_name
607
+ const previousNickname = existing.nickname
608
+ const previousRemark = existing.remark
609
+ Object.assign(existing, session)
610
+ if (!hasUsableAvatar(session.avatar) && hasUsableAvatar(previousAvatar)) {
611
+ existing.avatar = previousAvatar
612
+ }
613
+ if (
614
+ isGroup &&
615
+ !msg.group_name &&
616
+ previousGroupName &&
617
+ String(previousGroupName) !== String(sessionId)
618
+ ) {
619
+ existing.group_name = previousGroupName
620
+ }
621
+ if (!isGroup && !sender.nickname && previousNickname) {
622
+ existing.nickname = previousNickname
623
+ }
624
+ if (!isGroup && !existing.remark && previousRemark) {
625
+ existing.remark = previousRemark
626
+ }
627
+ session = existing
628
+ } else {
629
+ state.onMsgList.unshift(session)
630
+ }
631
+ const baseKey = normalizeSessionId(String(sessionId))
632
+ const baseList = [...state.baseList]
633
+ const baseIndex = baseList.findIndex(([key]) => normalizeSessionId(String(key)) === baseKey)
634
+ const typedSession = session as unknown as UserFriendElem & UserGroupElem
635
+ if (baseIndex >= 0) {
636
+ baseList[baseIndex] = [baseKey, typedSession]
637
+ } else {
638
+ baseList.push([baseKey, typedSession])
639
+ }
640
+ contactStore.botStates.set(selfId, {
641
+ userList: [...state.userList],
642
+ baseList,
643
+ onMsgList: [...state.onMsgList],
644
+ })
645
+ if (
646
+ isActiveBot({ platform, selfId }) &&
647
+ !contactStore.baseOnMsgList.has(String(sessionId))
648
+ ) {
649
+ contactStore.baseOnMsgList.set(
650
+ String(sessionId),
651
+ session as unknown as UserFriendElem & UserGroupElem,
652
+ )
653
+ updateBaseOnMsgList()
654
+ }
655
+
656
+ const chatStore = useChatStore()
657
+ const cacheKey = [platform, selfId, sessionId].map(encodeKeyPart).join(':')
658
+ const messages = chatStore.sessionMessageCache.get(cacheKey) ?? []
659
+ const messageId = String(msg.message_id ?? '')
660
+ if (messageId && !messages.some((item) => String(item.message_id) === messageId)) {
661
+ messages.push(msg)
662
+ chatStore.sessionMessageCache.set(cacheKey, messages)
663
+ }
664
+ const currentShowId = String(chatStore.chatInfo.show.id ?? '')
665
+ const currentShowType = String(chatStore.chatInfo.show.type ?? '')
666
+ const msgChannelId = String(msg.channel_id ?? '')
667
+ const showChannelId = String(chatStore.chatInfo.show.channel_id ?? '')
668
+ const isCurrentChat = isActiveBot({ platform, selfId }) && (
669
+ (isGroup && currentShowType === 'group' && String(sessionId) === currentShowId) ||
670
+ (!isGroup && currentShowType === 'user' && String(sessionId) === currentShowId) ||
671
+ (msgChannelId && showChannelId && msgChannelId === showChannelId)
672
+ )
673
+ if (
674
+ isCurrentChat &&
675
+ messageId &&
676
+ !chatStore.messageList.some((item) => String(item.message_id) === messageId)
677
+ ) {
678
+ // 实时消息直接写入当前聊天窗口,避免依赖旧的消息解析链路
679
+ chatStore.messageList.push(msg)
680
+ }
681
+ const knownSessionAvatar = contactStore.baseOnMsgList.get(String(sessionId))
682
+ const knownWithAvatar = hasUsableAvatar(session.avatar)
683
+ || Boolean(
684
+ knownSessionAvatar &&
685
+ hasUsableAvatar(knownSessionAvatar.avatar) &&
686
+ (isGroup
687
+ ? knownSessionAvatar.group_id !== undefined
688
+ : knownSessionAvatar.user_id !== undefined),
689
+ )
690
+ identityDebug('recordBotMessage', {
691
+ platform,
692
+ selfId,
693
+ isGroup,
694
+ sessionId,
695
+ senderId,
696
+ knownWithAvatar,
697
+ sessionAvatar: session.avatar,
698
+ baseAvatar: knownSessionAvatar?.avatar,
699
+ })
700
+ if (isGroup) {
701
+ identityDebug('recordBotMessage: group identity branch', {
702
+ sessionId,
703
+ senderId,
704
+ knownWithAvatar,
705
+ hasSenderAvatar,
706
+ })
707
+ void resolveGroupIdentity(
708
+ platform,
709
+ selfId,
710
+ sessionId,
711
+ typeof msg.guild_id === 'string' ? msg.guild_id : '',
712
+ channelId,
713
+ session,
714
+ msg,
715
+ )
716
+ if (senderId && senderId !== '0') {
717
+ void fetchUserIdentity(platform, selfId, senderId, null, msg)
718
+ }
719
+ } else if (senderId && senderId !== '0') {
720
+ identityDebug('recordBotMessage: private identity branch', {
721
+ sessionId,
722
+ senderId,
723
+ knownWithAvatar,
724
+ hasSenderAvatar,
725
+ })
726
+ void fetchUserIdentity(platform, selfId, senderId, session, msg)
727
+ }
728
+ }
729
+
730
+ export function restoreBotStateFromMessageCache(platform: string, selfId: string) {
731
+ const chatStore = useChatStore()
732
+ const contactStore = useContactStore()
733
+ const prefix = `${encodeKeyPart(platform)}:${encodeKeyPart(selfId)}:`
734
+ const sessions: Array<UserFriendElem & UserGroupElem> = []
735
+ for (const [key, messages] of chatStore.sessionMessageCache) {
736
+ if (!key.startsWith(prefix)) continue
737
+ const msg = messages[messages.length - 1]
738
+ if (!msg) continue
739
+ const isGroup = isGroupMessage(msg)
740
+ const id = String(isGroup ? msg.group_id : msg.user_id)
741
+ if (!id || id === '0') continue
742
+ const sender = getObject(msg.sender)
743
+ const senderId = getString(sender.user_id) || getString(msg.user_id) || ''
744
+ const senderName = getString(sender.card) || getString(sender.nickname) || (isGroup ? senderId : '')
745
+ const raw = getMsgRawTxt(msg) || (typeof msg.raw_message === 'string' ? msg.raw_message : '')
746
+ const cachedGroupInfo = isGroup
747
+ ? identityInfoCache.get(identityCacheKey('group', platform, selfId, id))
748
+ : undefined
749
+ const eventGroupAvatar = hasUsableAvatar(msg.group_avatar) ? getString(msg.group_avatar) : ''
750
+ sessions.push({
751
+ user_id: isGroup ? undefined : id,
752
+ group_id: isGroup ? id : undefined,
753
+ group_name: isGroup
754
+ ? (getString(msg.group_name) || cachedGroupInfo?.name || id)
755
+ : '',
756
+ nickname: isGroup ? '' : getString(sender.nickname) || getString(sender.card) || id,
757
+ remark: isGroup ? '' : getString(sender.nickname) || '',
758
+ avatar: isGroup
759
+ ? (eventGroupAvatar || cachedGroupInfo?.avatar || buildQqGroupAvatar(platform, id) || '')
760
+ : hasUsableAvatar(sender.avatar) ? getString(sender.avatar) : '',
761
+ raw_msg: isGroup && senderName ? `${senderName}: ${raw}` : raw,
762
+ raw_msg_base: raw,
763
+ highlight: hasAtMe(msg) ? '[有人@你]' : undefined,
764
+ time: Number(msg.local_time ?? msg.timestamp_ms ?? (Number(msg.time) ? Number(msg.time) * 1000 : 0)),
765
+ message_id: getString(msg.message_id),
766
+ channel_id: getString(msg.channel_id),
767
+ guild_id: getString(msg.guild_id),
768
+ } as UserFriendElem & UserGroupElem)
769
+ }
770
+ if (!sessions.length) return
771
+
772
+ const state = contactStore.botStates.get(selfId) ?? {
773
+ userList: [],
774
+ baseList: [],
775
+ onMsgList: [],
776
+ }
777
+ const baseList = [...state.baseList]
778
+ const baseKeys = new Set(baseList.map(([key]) => normalizeSessionId(key)))
779
+ const existing = new Set(state.onMsgList.map((item) => {
780
+ return String(item.user_id ?? item.group_id ?? '')
781
+ }))
782
+ for (const session of sessions) {
783
+ const id = String(session.user_id ?? session.group_id ?? '')
784
+ if (id && !existing.has(id)) {
785
+ state.onMsgList.unshift(session)
786
+ existing.add(id)
787
+ }
788
+ if (id && !baseKeys.has(normalizeSessionId(id))) {
789
+ baseList.push([normalizeSessionId(id), session])
790
+ baseKeys.add(normalizeSessionId(id))
791
+ }
792
+ }
793
+ contactStore.botStates.set(selfId, {
794
+ userList: [...state.userList],
795
+ baseList,
796
+ onMsgList: [...state.onMsgList],
797
+ })
798
+ }
799
+
800
+ function onSatoriEvent(event: SatoriEvent) {
801
+ const oneBot = satoriEventToOneBot(event.body, String(event.platform || ''))
802
+ if (oneBot) {
803
+ oneBot._rawSatori = {
804
+ type: event.type,
805
+ platform: event.platform,
806
+ selfId: event.selfId,
807
+ timestamp: event.timestamp,
808
+ sn: event.sn,
809
+ body: event.body,
810
+ }
811
+ }
812
+ identityDebug('satori event', { event, oneBot })
813
+ if (!oneBot) return
814
+ if (!isCurrentBotEvent(event)) {
815
+ const key = botEventKey(event.platform, event.selfId)
816
+ const events = pendingBotEvents.get(key) ?? []
817
+ events.push(event)
818
+ if (events.length > 500) events.splice(0, events.length - 500)
819
+ pendingBotEvents.set(key, events)
820
+ }
821
+ if (oneBot.post_type === 'message' || oneBot.post_type === 'message_sent') {
822
+ void msgPreprocess(oneBot, {
823
+ platform: String(event.platform || oneBot.platform || ''),
824
+ selfId: String(event.selfId || oneBot.self_id || ''),
825
+ }).then((processed) => {
826
+ if (!processed) return
827
+ recordBotMessage(event.platform, event.selfId, processed)
828
+ // 只有当前机器人写入全局 UI;其他机器人仍在上方实时记录。
829
+ if (isCurrentBotEvent(event)) {
830
+ dispatch(processed)
831
+ }
832
+ }).catch(() => {
833
+ recordBotMessage(event.platform, event.selfId, oneBot)
834
+ if (isCurrentBotEvent(event)) {
835
+ dispatch(oneBot)
836
+ }
837
+ })
838
+ } else if (isCurrentBotEvent(event)) {
839
+ dispatch(oneBot)
840
+ }
841
+ }
842
+
843
+ export function flushPendingBotEvents(platform: string, selfId: string) {
844
+ const key = botEventKey(platform, selfId)
845
+ const events = pendingBotEvents.get(key)
846
+ if (!events?.length) return
847
+ pendingBotEvents.delete(key)
848
+ for (const event of events) {
849
+ const oneBot = satoriEventToOneBot(event.body, String(event.platform || ''))
850
+ if (oneBot) {
851
+ oneBot._rawSatori = {
852
+ type: event.type,
853
+ platform: event.platform,
854
+ selfId: event.selfId,
855
+ timestamp: event.timestamp,
856
+ sn: event.sn,
857
+ body: event.body,
858
+ }
859
+ }
860
+ if (oneBot) {
861
+ void msgPreprocess(oneBot, {
862
+ platform: String(event.platform || oneBot.platform || ''),
863
+ selfId: String(event.selfId || oneBot.self_id || ''),
864
+ }).then((processed) => {
865
+ if (processed) dispatch(processed)
866
+ }).catch(() => {
867
+ dispatch(oneBot)
868
+ })
869
+ }
870
+ }
871
+ }
872
+
873
+ function onSatoriReady(logins: Array<{ platform: string; selfId: string; name: string; avatar?: string; features?: string[] }>) {
874
+ login.creating = false
875
+ login.status = true
876
+ const authStore = useAuthStore()
877
+ for (const item of logins) {
878
+ if (!item.platform || !item.selfId) continue
879
+ identityInfoCache.set(identityCacheKey('user', item.platform, item.selfId, item.selfId), {
880
+ name: item.name || item.selfId,
881
+ avatar: item.avatar || '',
882
+ raw: {
883
+ id: item.selfId,
884
+ user_id: item.selfId,
885
+ nickname: item.name || item.selfId,
886
+ name: item.name || item.selfId,
887
+ avatar: item.avatar || '',
888
+ },
889
+ })
890
+ }
891
+ const first = logins[0]
892
+ if (!first) return
893
+
894
+ setActiveBot(first.platform, first.selfId)
895
+ setJsonMap()
896
+ login.uin = first.selfId
897
+ login.nickname = first.name
898
+ login.platform = first.platform
899
+ login.satoriLogins = logins
900
+ login.selectedSatoriBot = first.selfId
901
+ authStore.loginInfo = {
902
+ uin: first.selfId,
903
+ user_id: first.selfId,
904
+ nickname: first.name,
905
+ platform: first.platform,
906
+ avatar: first.avatar,
907
+ satoriLogins: logins,
908
+ selectedSatoriBot: first.selfId,
909
+ }
910
+ authStore.botInfo = {
911
+ app_name: 'Satori',
912
+ app_version: '1.0',
913
+ platform: first.platform,
914
+ }
915
+ saveConnectionToHistory(login.address, login.token, first.selfId, first.name)
916
+ flushPendingBotEvents(first.platform, first.selfId)
917
+ void loadContactsFromCache()
918
+ }
919
+
920
+ function contactId(item: unknown): string {
921
+ const obj = getObject(item)
922
+ const raw = String(obj.user_id ?? obj.group_id ?? obj.id ?? '')
923
+ if (obj.user_id) return raw
924
+ return normalizeGroupId(raw)
925
+ }
926
+
927
+ function normalizeGroupId(value: string): string {
928
+ const raw = value.replace(/^(?:group|room|chat|channel|guild):/i, '').trim()
929
+ const wrapped = raw.match(/^\[_?([\s\S]+?)_?\]$/)
930
+ return wrapped ? wrapped[1] : raw || value
931
+ }
932
+
933
+ function contactName(item: unknown): string {
934
+ const obj = getObject(item)
935
+ return String(obj.nickname ?? obj.remark ?? obj.group_name ?? obj.name ?? '')
936
+ }
937
+
938
+ function contactCachePayload(item: unknown): Record<string, unknown> {
939
+ const obj = getObject(item)
940
+ return {
941
+ id: contactId(obj),
942
+ name: contactName(obj),
943
+ channelId: getString(obj.channel_id) || getString(obj.channelId) || undefined,
944
+ guildId: getString(obj.guild_id) || getString(obj.guildId) || undefined,
945
+ raw: item,
946
+ }
947
+ }
948
+
949
+ async function requestContactCache(params: {
950
+ platform: string
951
+ selfId: string
952
+ type: string
953
+ groupId?: string
954
+ userId?: string
955
+ contacts?: Record<string, unknown>[]
956
+ append?: boolean
957
+ }) {
958
+ if (!params.platform || !params.selfId || !params.type) {
959
+ logger.add(
960
+ LogType.ERR,
961
+ `缓存请求缺少参数: ${params.platform}/${params.selfId}/${params.type}`,
962
+ )
963
+ return { contacts: [] }
964
+ }
965
+ const info = await getBootstrap()
966
+ const basePath = info.basePath || '/chat-patch'
967
+ const method = params.contacts ? 'POST' : 'GET'
968
+ const query = new URLSearchParams()
969
+ if (method === 'GET') {
970
+ query.set('platform', params.platform)
971
+ query.set('selfId', params.selfId)
972
+ query.set('type', params.type)
973
+ if (params.groupId) query.set('groupId', params.groupId)
974
+ if (params.userId) query.set('userId', params.userId)
975
+ }
976
+ const url = `${location.origin}${basePath}/api/cache${method === 'GET' ? `?${query.toString()}` : ''}`
977
+ const response = await fetch(url, {
978
+ method,
979
+ headers: params.contacts ? { 'Content-Type': 'application/json' } : undefined,
980
+ body: params.contacts ? JSON.stringify(params) : undefined,
981
+ })
982
+ if (!response.ok) {
983
+ throw new Error(`缓存 API 返回 ${response.status}`)
984
+ }
985
+ return response.json() as Promise<Record<string, unknown>>
986
+ }
987
+
988
+ // 收消息时查询单条身份缓存;后端未命中会自动回源并写库
989
+ async function requestIdentityCache(params: {
990
+ platform: string
991
+ selfId: string
992
+ type: 'user' | 'group'
993
+ id: string
994
+ guildId?: string
995
+ channelId?: string
996
+ channelType?: unknown
997
+ name?: string
998
+ avatar?: string
999
+ }): Promise<Record<string, unknown> | null> {
1000
+ if (!params.platform || !params.selfId || !params.id) return null
1001
+ const info = await getBootstrap()
1002
+ const basePath = info.basePath || '/chat-patch'
1003
+ const query = new URLSearchParams({
1004
+ platform: params.platform,
1005
+ selfId: params.selfId,
1006
+ type: params.type,
1007
+ [params.type === 'user' ? 'userId' : 'groupId']: params.id,
1008
+ })
1009
+ if (params.guildId) query.set('guildId', params.guildId)
1010
+ if (params.channelId) query.set('channelId', params.channelId)
1011
+ if (params.channelType !== undefined && params.channelType !== null) {
1012
+ query.set('channelType', String(params.channelType))
1013
+ }
1014
+ if (params.name) query.set('name', params.name)
1015
+ if (params.avatar) query.set('avatar', params.avatar)
1016
+
1017
+ const response = await fetch(`${location.origin}${basePath}/api/cache?${query.toString()}`)
1018
+ if (!response.ok) {
1019
+ throw new Error(`身份缓存 API 返回 ${response.status}`)
1020
+ }
1021
+ const text = await response.text()
1022
+ if (!text || text === 'null') return null
1023
+ try {
1024
+ const parsed = JSON.parse(text) as unknown
1025
+ return typeof parsed === 'object' && parsed !== null
1026
+ ? parsed as Record<string, unknown>
1027
+ : null
1028
+ } catch {
1029
+ return null
1030
+ }
1031
+ }
1032
+
1033
+ async function requestAllBotsCache() {
1034
+ const info = await getBootstrap()
1035
+ const basePath = info.basePath || '/chat-patch'
1036
+ const response = await fetch(`${location.origin}${basePath}/api/cache/all`)
1037
+ if (!response.ok) {
1038
+ throw new Error(`全部缓存 API 返回 ${response.status}`)
1039
+ }
1040
+ return response.json() as Promise<{ bots: unknown[] }>
1041
+ }
1042
+
1043
+ export async function fetchForwardMessage(
1044
+ platform: string,
1045
+ selfId: string,
1046
+ id: string,
1047
+ channelId?: string,
1048
+ ): Promise<unknown[] | null> {
1049
+ if (!platform || !selfId || !id) return null
1050
+ const info = await getBootstrap()
1051
+ const basePath = info.basePath || '/chat-patch'
1052
+ const query = new URLSearchParams({ platform, selfId, id })
1053
+ if (channelId) query.set('channelId', channelId)
1054
+ try {
1055
+ const response = await fetch(`${location.origin}${basePath}/api/forward?${query.toString()}`)
1056
+ if (!response.ok) return null
1057
+ const data = await response.json() as unknown
1058
+ if (Array.isArray(data)) return data as unknown[]
1059
+ const obj = data as Record<string, unknown>
1060
+ if (Array.isArray(obj.messages)) return obj.messages as unknown[]
1061
+ const nested = obj.data
1062
+ if (typeof nested === 'object' && nested !== null) {
1063
+ const nestedObj = nested as Record<string, unknown>
1064
+ if (Array.isArray(nestedObj.messages)) return nestedObj.messages as unknown[]
1065
+ }
1066
+ return null
1067
+ } catch {
1068
+ return null
1069
+ }
1070
+ }
1071
+
1072
+ export interface SendForwardResult {
1073
+ ok: boolean
1074
+ native: boolean
1075
+ messageId?: string
1076
+ }
1077
+
1078
+ function extractFirstMessageId(data: unknown): string | undefined {
1079
+ const root = getObject(data)
1080
+ const list = Array.isArray(data)
1081
+ ? data as unknown[]
1082
+ : Array.isArray(root.data)
1083
+ ? root.data as unknown[]
1084
+ : []
1085
+ const first = list.length > 0 ? getObject(list[0]) : root
1086
+ return getString(first.id) || getString(first.message_id) || undefined
1087
+ }
1088
+
1089
+ export async function sendForwardMessage(
1090
+ platform: string,
1091
+ selfId: string,
1092
+ type: 'group' | 'user',
1093
+ id: string,
1094
+ messages: unknown[],
1095
+ ): Promise<SendForwardResult> {
1096
+ if (!platform || !selfId || !id || messages.length === 0) {
1097
+ return { ok: false, native: false }
1098
+ }
1099
+ // 优先走 Satori 原生合并转发,避免依赖 onebot 专属的 send-forward API
1100
+ if (platform) {
1101
+ const channelId = type === 'group'
1102
+ ? /^(?:group|room|chat|channel|guild):/i.test(id)
1103
+ ? id
1104
+ : platform === 'yunhu'
1105
+ ? `group:${id}`
1106
+ : id
1107
+ : /^(?:private|direct):/i.test(id)
1108
+ ? id
1109
+ : `private:${id}`
1110
+ const content = buildForwardMessage(messages)
1111
+ if (content) {
1112
+ try {
1113
+ const data = await request('message.create', { channel_id: channelId, content }, { platform, selfId })
1114
+ return {
1115
+ ok: true,
1116
+ native: true,
1117
+ messageId: extractFirstMessageId(data),
1118
+ }
1119
+ } catch {
1120
+ // 原生路径失败时回退到旧 API,方便兼容尚未热重载的 Koishi 后端
1121
+ }
1122
+ }
1123
+ }
1124
+ const info = await getBootstrap()
1125
+ const basePath = info.basePath || '/chat-patch'
1126
+ try {
1127
+ const response = await fetch(`${location.origin}${basePath}/api/send-forward`, {
1128
+ method: 'POST',
1129
+ headers: { 'Content-Type': 'application/json' },
1130
+ body: JSON.stringify({ platform, selfId, type, id, messages }),
1131
+ })
1132
+ return { ok: response.ok, native: false }
1133
+ } catch {
1134
+ return { ok: false, native: false }
1135
+ }
1136
+ }
1137
+
1138
+ export interface SaveSelfMessagePayload {
1139
+ id?: string
1140
+ platform: string
1141
+ selfId: string
1142
+ channelId: string
1143
+ guildId?: string
1144
+ channelType: 'group' | 'user'
1145
+ messageId?: string
1146
+ content?: string
1147
+ elements?: unknown[]
1148
+ message?: unknown[]
1149
+ forwardId?: string
1150
+ forwardContent?: unknown[]
1151
+ sentAt?: number
1152
+ source?: 'webui' | 'bot' | 'plugin'
1153
+ kind?: string
1154
+ }
1155
+
1156
+ export async function saveSentSelfMessage(payload: SaveSelfMessagePayload): Promise<boolean> {
1157
+ if (!payload.platform || !payload.selfId || !payload.channelId) return false
1158
+ try {
1159
+ const info = await getBootstrap()
1160
+ const basePath = info.basePath || '/chat-patch'
1161
+ const response = await fetch(`${location.origin}${basePath}/api/self-messages`, {
1162
+ method: 'POST',
1163
+ headers: { 'Content-Type': 'application/json' },
1164
+ body: JSON.stringify(payload),
1165
+ })
1166
+ return response.ok
1167
+ } catch {
1168
+ return false
1169
+ }
1170
+ }
1171
+
1172
+ // 把后端自消息缓存转换为前端 message_sent 结构
1173
+ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1174
+ const obj = getObject(record)
1175
+ const platform = getString(obj.platform)
1176
+ const selfId = getString(obj.selfId)
1177
+ const channelId = getString(obj.channelId)
1178
+ const sentAt = Number(obj.sentAt ?? Date.now())
1179
+ const messageId = getString(obj.messageId)
1180
+ const localId = getString(obj.id) || `self-${sentAt}`
1181
+ const channelType = getString(obj.channelType) === 'user'
1182
+ || /^(?:private|direct):/i.test(channelId)
1183
+ ? 'user'
1184
+ : 'group'
1185
+ if (!platform || !selfId || !channelId) return null
1186
+
1187
+ const login = getLogins().find((item) => {
1188
+ return item.platform === platform && item.selfId === selfId
1189
+ })
1190
+ const forwardContent = Array.isArray(obj.forwardContent)
1191
+ ? obj.forwardContent as unknown[]
1192
+ : []
1193
+ let message = Array.isArray(obj.message)
1194
+ ? obj.message as unknown[]
1195
+ : []
1196
+ if (forwardContent.length > 0) {
1197
+ const forwardIndex = message.findIndex((segment) => getObject(segment).type === 'forward')
1198
+ if (forwardIndex >= 0) {
1199
+ const existing = getObject(message[forwardIndex])
1200
+ const existingContent = Array.isArray(existing.content) ? existing.content as unknown[] : []
1201
+ if (existingContent.length === 0) {
1202
+ message[forwardIndex] = { ...existing, content: forwardContent }
1203
+ }
1204
+ } else if (message.length === 0) {
1205
+ message = [{
1206
+ type: 'forward',
1207
+ id: messageId || localId,
1208
+ content: forwardContent,
1209
+ }]
1210
+ }
1211
+ }
1212
+ if (message.length === 0) {
1213
+ const synthetic = satoriEventToOneBot({
1214
+ type: 'send',
1215
+ platform,
1216
+ selfId,
1217
+ timestamp: sentAt,
1218
+ message: {
1219
+ content: getString(obj.content),
1220
+ elements: Array.isArray(obj.elements) ? obj.elements : undefined,
1221
+ },
1222
+ channel: {
1223
+ id: channelId,
1224
+ type: channelType === 'user' ? 1 : 0,
1225
+ },
1226
+ guild: getString(obj.guildId) ? { id: getString(obj.guildId) } : undefined,
1227
+ }, platform)
1228
+ if (Array.isArray(synthetic?.message)) {
1229
+ message = synthetic.message as unknown[]
1230
+ }
1231
+ }
1232
+ if (message.length === 0) return null
1233
+
1234
+ const groupId = channelType === 'group' ? normalizeGroupId(channelId) : ''
1235
+ const userId = channelType === 'user' ? channelId.replace(/^(?:private|direct):/i, '') : ''
1236
+ const guildId = getString(obj.guildId)
1237
+ const fallbackId = messageId || `self-${localId}`
1238
+ const rawMessage = getString(obj.content) || getMsgRawTxt({ message })
1239
+ const msg: Record<string, unknown> = {
1240
+ post_type: 'message_sent',
1241
+ message_type: channelType === 'group' ? 'group' : 'private',
1242
+ channel_type: channelType === 'group' ? 0 : 1,
1243
+ message_id: fallbackId,
1244
+ fake_message_id: localId,
1245
+ user_id: groupId ? '' : userId,
1246
+ group_id: groupId || undefined,
1247
+ channel_id: channelId,
1248
+ guild_id: guildId || undefined,
1249
+ target_id: selfId,
1250
+ time: Math.floor(sentAt / 1000) || Math.floor(Date.now() / 1000),
1251
+ local_time: sentAt,
1252
+ timestamp_ms: sentAt,
1253
+ time_ms: sentAt,
1254
+ sender: {
1255
+ user_id: selfId,
1256
+ nickname: getString(login?.name) || selfId,
1257
+ avatar: getString(login?.avatar) || undefined,
1258
+ },
1259
+ message,
1260
+ raw_message: rawMessage,
1261
+ infoList: {
1262
+ message_id: fallbackId,
1263
+ private_id: groupId ? '' : userId,
1264
+ group_id: groupId || undefined,
1265
+ channel_id: channelId,
1266
+ guild_id: guildId || undefined,
1267
+ target_id: selfId,
1268
+ sender: selfId,
1269
+ },
1270
+ _from_self_cache: true,
1271
+ }
1272
+ return msg
1273
+ }
1274
+
1275
+ export async function loadChatHistoryFromCache(params: {
1276
+ platform: string
1277
+ selfId: string
1278
+ channelId: string
1279
+ limit?: number
1280
+ beforeTimeMs?: number
1281
+ }): Promise<Record<string, unknown>[]> {
1282
+ const info = await getBootstrap()
1283
+ const basePath = info.basePath || '/chat-patch'
1284
+ const query = new URLSearchParams({
1285
+ platform: params.platform,
1286
+ selfId: params.selfId,
1287
+ channelId: params.channelId,
1288
+ limit: String(params.limit ?? 20),
1289
+ })
1290
+ if (params.beforeTimeMs) query.set('beforeTime', String(params.beforeTimeMs))
1291
+ const response = await fetch(`${location.origin}${basePath}/api/history?${query.toString()}`)
1292
+ if (!response.ok) {
1293
+ throw new Error(`历史缓存 API 返回 ${response.status}`)
1294
+ }
1295
+ const result = getObject(await response.json() as unknown)
1296
+ const records = Array.isArray(result.messages) ? result.messages as unknown[] : []
1297
+ const messages: Record<string, unknown>[] = []
1298
+ for (const record of records) {
1299
+ const recordObj = getObject(record)
1300
+ const raw = getObject(recordObj.raw)
1301
+ const msg = satoriEventToOneBot(raw, params.platform)
1302
+ if (!msg || !msg.message_id) continue
1303
+ const localTime = Number(recordObj.receivedAt ?? recordObj.timestampMs ?? recordObj.timestamp ?? 0)
1304
+ if (localTime) {
1305
+ msg.local_time = localTime
1306
+ msg.timestamp_ms = localTime
1307
+ msg.time_ms = localTime
1308
+ }
1309
+ const userId = getString(msg.user_id)
1310
+ const groupId = getString(msg.group_id)
1311
+ msg.infoList = {
1312
+ message_id: getString(msg.message_id),
1313
+ private_id: groupId ? '' : userId,
1314
+ group_id: groupId || undefined,
1315
+ channel_id: getString(msg.channel_id),
1316
+ guild_id: getString(msg.guild_id),
1317
+ target_id: getString(msg.target_id),
1318
+ sender: userId,
1319
+ }
1320
+ messages.push(msg)
1321
+ }
1322
+ const existingIds = new Set(messages.map((item) => getString(item.message_id)))
1323
+ const selfRecords = Array.isArray(result.selfMessages) ? result.selfMessages as unknown[] : []
1324
+ for (const record of selfRecords) {
1325
+ const msg = selfMessageToOneBot(record)
1326
+ if (!msg) continue
1327
+ const messageId = getString(msg.message_id)
1328
+ const fakeId = getString(msg.fake_message_id)
1329
+ if (existingIds.has(messageId) || (fakeId && existingIds.has(fakeId))) continue
1330
+ existingIds.add(messageId)
1331
+ messages.push(msg)
1332
+ }
1333
+ messages.sort((a, b) => {
1334
+ const timeA = Number(a.local_time ?? a.timestamp_ms ?? a.time_ms ?? 0)
1335
+ const timeB = Number(b.local_time ?? b.timestamp_ms ?? b.time_ms ?? 0)
1336
+ return timeA - timeB
1337
+ })
1338
+ return messages
1339
+ }
1340
+
1341
+ async function loadAllBotsCache() {
1342
+ const contactStore = useContactStore()
1343
+ const result = await requestAllBotsCache()
1344
+ const bots = Array.isArray(getObject(result).bots) ? getObject(result).bots as unknown[] : []
1345
+ for (const rawBot of bots) {
1346
+ const bot = getObject(rawBot)
1347
+ const platform = getString(bot.platform)
1348
+ const selfId = getString(bot.selfId)
1349
+ if (!platform || !selfId) continue
1350
+
1351
+ const groups = Array.isArray(bot.groups) ? bot.groups as unknown[] : []
1352
+ const friends = Array.isArray(bot.friends) ? bot.friends as unknown[] : []
1353
+ const groupRaws = groups.map((contact) => {
1354
+ const cached = getObject(contact)
1355
+ const raw = getObject(cached.raw) || cached
1356
+ if (raw.avatar && !hasUsableAvatar(raw.avatar)) delete raw.avatar
1357
+ return raw
1358
+ })
1359
+ const friendRaws = friends.map((contact) => cachedFriendRaw(getObject(contact), contact))
1360
+ for (const raw of groupRaws) {
1361
+ const id = normalizeGroupId(getString(raw.group_id) || getString(raw.id))
1362
+ if (!id) continue
1363
+ const name = getString(raw.group_name) || getString(raw.name)
1364
+ const avatar = hasUsableAvatar(raw.avatar)
1365
+ ? getString(raw.avatar)
1366
+ : buildQqGroupAvatar(platform, id)
1367
+ if (avatar) raw.avatar = avatar
1368
+ if (!name && !avatar) continue
1369
+ identityInfoCache.set(identityCacheKey('group', platform, selfId, id), {
1370
+ name,
1371
+ avatar,
1372
+ raw,
1373
+ })
1374
+ }
1375
+ for (const raw of friendRaws) {
1376
+ const id = getString(raw.user_id) || getString(raw.id)
1377
+ if (!id) continue
1378
+ const name = getString(raw.nickname) || getString(raw.remark) || getString(raw.name)
1379
+ const avatar = hasUsableAvatar(raw.avatar) ? getString(raw.avatar) : ''
1380
+ if (!name && !avatar) continue
1381
+ identityInfoCache.set(identityCacheKey('user', platform, selfId, id), {
1382
+ name,
1383
+ avatar,
1384
+ raw,
1385
+ })
1386
+ }
1387
+ const userList = [...groupRaws, ...friendRaws] as unknown as (UserFriendElem & UserGroupElem)[]
1388
+
1389
+ const state = contactStore.botStates.get(selfId)
1390
+ if (!state || state.userList.length === 0) {
1391
+ contactStore.botStates.set(selfId, {
1392
+ userList,
1393
+ baseList: state?.baseList ?? [],
1394
+ onMsgList: state?.onMsgList ?? [],
1395
+ })
1396
+ }
1397
+
1398
+ if (isActiveBot({ platform, selfId })) {
1399
+ if (groupRaws.length > 0) {
1400
+ dispatch({ retcode: 0, data: groupRaws }, 'getGroupList')
1401
+ }
1402
+ if (friendRaws.length > 0) {
1403
+ dispatch({ retcode: 0, data: friendRaws }, 'getFriendList')
1404
+ }
1405
+ }
1406
+ }
1407
+ }
1408
+
1409
+ async function saveContactCache(
1410
+ action: string,
1411
+ data: unknown,
1412
+ bot?: { platform: string; selfId: string } | null,
1413
+ ) {
1414
+ const active = bot ?? getActiveBot()
1415
+ if (!active || !Array.isArray(data)) return
1416
+ const type = action === 'get_friend_list' ? 'friend' : 'group'
1417
+ const contacts = data.map(contactCachePayload)
1418
+ await requestContactCache({
1419
+ platform: active.platform,
1420
+ selfId: active.selfId,
1421
+ type,
1422
+ contacts,
1423
+ })
1424
+ }
1425
+
1426
+ async function appendContactCache(
1427
+ type: string,
1428
+ item: unknown,
1429
+ bot?: { platform: string; selfId: string } | null,
1430
+ ) {
1431
+ const active = bot ?? getActiveBot()
1432
+ if (!active) return
1433
+ const contact = contactCachePayload(item)
1434
+ if (!contact.id) return
1435
+ await requestContactCache({
1436
+ platform: active.platform,
1437
+ selfId: active.selfId,
1438
+ type,
1439
+ contacts: [contact],
1440
+ append: true,
1441
+ })
1442
+ }
1443
+
1444
+ function buildFriendItem(raw: unknown, data: unknown): Record<string, unknown> {
1445
+ const root = getObject(data)
1446
+ const user = getObject(root.user) || getObject(root)
1447
+ const name = String(user.name ?? user.nick ?? user.username ?? '')
1448
+ const avatar = hasUsableAvatar(user.avatar) ? getString(user.avatar) : ''
1449
+ const item: Record<string, unknown> = { ...getObject(raw) }
1450
+ if (!item.channel_id && item.channelId) item.channel_id = item.channelId
1451
+ if (!item.guild_id && item.guildId) item.guild_id = item.guildId
1452
+ if (name) {
1453
+ item.nickname = name
1454
+ item.remark = name
1455
+ }
1456
+ if (avatar) item.avatar = avatar
1457
+ else if (item.avatar && !hasUsableAvatar(item.avatar)) delete item.avatar
1458
+ if (item.class_id === undefined) item.class_id = 0
1459
+ if (!item.class_name) item.class_name = '我的好友'
1460
+ return item
1461
+ }
1462
+
1463
+ function cachedFriendRaw(
1464
+ cachedItem: Record<string, unknown>,
1465
+ fallback: unknown,
1466
+ ): Record<string, unknown> {
1467
+ return buildFriendItem(getObject(cachedItem).raw ?? fallback, {})
1468
+ }
1469
+
1470
+ function getString(value: unknown): string {
1471
+ return typeof value === 'string' ? value : ''
1472
+ }
1473
+
1474
+ function asArray(value: unknown): unknown[] | null {
1475
+ return Array.isArray(value) ? value : null
1476
+ }
1477
+
1478
+ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | null> {
1479
+ return Promise.race([
1480
+ promise,
1481
+ new Promise<null>((resolve) => setTimeout(() => resolve(null), ms)),
1482
+ ])
1483
+ }
1484
+
1485
+ function buildGroupItem(
1486
+ raw: unknown,
1487
+ data: unknown,
1488
+ platform = '',
1489
+ ): Record<string, unknown> {
1490
+ const root = getObject(data)
1491
+ const guild = getObject(root.guild) || getObject(root)
1492
+ const channel = getObject(root.channel)
1493
+ const item = { ...getObject(raw) }
1494
+ const id = normalizeGroupId(getString(item.group_id) || getString(item.id) || getString(item.guild_id) || '')
1495
+ const name = getString(guild.name)
1496
+ || getString(channel.name)
1497
+ || getString(item.group_name)
1498
+ || getString(item.name)
1499
+ const avatar = hasUsableAvatar(guild.avatar)
1500
+ ? getString(guild.avatar)
1501
+ : hasUsableAvatar(channel.avatar)
1502
+ ? getString(channel.avatar)
1503
+ : hasUsableAvatar(item.avatar)
1504
+ ? getString(item.avatar)
1505
+ : buildQqGroupAvatar(platform, id)
1506
+ if (name) item.group_name = name
1507
+ if (avatar) item.avatar = avatar
1508
+ else if (item.avatar && !hasUsableAvatar(item.avatar)) delete item.avatar
1509
+ const guildId = getString(guild.id)
1510
+ || getString(item.guild_id)
1511
+ || getString(item.guildId)
1512
+ || id
1513
+ const channelId = getString(channel.id)
1514
+ || getString(item.channel_id)
1515
+ || getString(item.channelId)
1516
+ if (guildId) item.guild_id = guildId
1517
+ if (channelId) item.channel_id = channelId
1518
+ return item
1519
+ }
1520
+
1521
+ // 刷新群组时逐个请求 guild.get / channel.get,并把结果写回缓存和界面
1522
+ async function fetchGroupProfiles(
1523
+ active: { platform: string; selfId: string },
1524
+ groups: unknown[],
1525
+ updateUi = true,
1526
+ ): Promise<Record<string, unknown>[]> {
1527
+ const contactStore = updateUi ? useContactStore() : null
1528
+ const processedItems: Record<string, unknown>[] = []
1529
+
1530
+ const processGroup = async (raw: unknown) => {
1531
+ const id = contactId(raw)
1532
+ if (!id) return
1533
+ const rawObj = getObject(raw)
1534
+ const guildId = getString(rawObj.guild_id) || getString(rawObj.guildId) || id
1535
+ const channelId = getString(rawObj.channel_id) || getString(rawObj.channelId) || id
1536
+ const [guildData, channelData] = await Promise.all([
1537
+ withTimeout(
1538
+ request('guild.get', { guild_id: guildId }, active).catch(() => null),
1539
+ 5000,
1540
+ ),
1541
+ withTimeout(
1542
+ request('channel.get', { channel_id: channelId, guild_id: guildId }, active).catch(() => null),
1543
+ 5000,
1544
+ ),
1545
+ ])
1546
+ const item = buildGroupItem(raw, { guild: guildData, channel: channelData }, active.platform)
1547
+ processedItems.push(item)
1548
+
1549
+ if (updateUi && contactStore) {
1550
+ const existing = contactStore.userList.find((contact) => {
1551
+ return String(contact.group_id) === id
1552
+ })
1553
+ if (existing) {
1554
+ if (item.group_name) existing.group_name = String(item.group_name)
1555
+ if (item.avatar) existing.avatar = String(item.avatar)
1556
+ else if (existing.avatar && !hasUsableAvatar(existing.avatar)) delete existing.avatar
1557
+ contactStore.userList = [...contactStore.userList]
1558
+ } else {
1559
+ dispatch({ retcode: 0, data: [item] }, 'getGroupList')
1560
+ }
1561
+ }
1562
+
1563
+ if (updateUi) {
1564
+ void withTimeout(appendContactCache('group', item, active), 3000).catch(() => {
1565
+ // 单个群组缓存写入失败不阻塞后续请求
1566
+ })
1567
+ } else {
1568
+ await withTimeout(appendContactCache('group', item, active), 3000).catch(() => {
1569
+ // 后台缓存写入失败不中断整个机器人
1570
+ })
1571
+ }
1572
+ }
1573
+
1574
+ const batchSize = 30
1575
+ for (let start = 0; start < groups.length; start += batchSize) {
1576
+ const batchStart = Date.now()
1577
+ const batch = groups.slice(start, start + batchSize)
1578
+ await Promise.all(batch.map((raw) => processGroup(raw).catch((error: unknown) => {
1579
+ logger.add(LogType.ERR, `处理群组身份信息失败: ${String(error)}`)
1580
+ })))
1581
+ const elapsed = Date.now() - batchStart
1582
+ const wait = Math.max(0, 1000 - elapsed)
1583
+ if (wait > 0) {
1584
+ await new Promise((resolve) => setTimeout(resolve, wait))
1585
+ }
1586
+ }
1587
+ return processedItems
1588
+ }
1589
+
1590
+ async function cacheBotContacts(_bot: { platform: string; selfId: string }) {
1591
+ // 联系人缓存已改为收到消息时写入,不再请求 guild.list / friend.list
1592
+ }
1593
+
1594
+ export async function refreshAllBots() {
1595
+ useContactStore().botStates.clear()
1596
+ await loadContactsFromCache()
1597
+ }
1598
+
1599
+ async function fetchFriendProfiles(
1600
+ active: { platform: string; selfId: string },
1601
+ friends: unknown[],
1602
+ updateUi = true,
1603
+ ): Promise<Record<string, unknown>[]> {
1604
+ const contactStore = updateUi ? useContactStore() : null
1605
+ const botKey = [active.platform, active.selfId].map(encodeKeyPart).join(':')
1606
+ const userGetBlocked = unsupportedMethods.has(`user.get:${botKey}`)
1607
+ const processedItems: Record<string, unknown>[] = []
1608
+
1609
+ const processFriend = async (raw: unknown) => {
1610
+ const id = contactId(raw)
1611
+ if (!id) return
1612
+ let data: unknown = {}
1613
+ if (!userGetBlocked) {
1614
+ try {
1615
+ data = await Promise.race([
1616
+ request('user.get', { user_id: id }, active),
1617
+ new Promise<null>((resolve) => setTimeout(() => resolve(null), 5000)),
1618
+ ]) ?? {}
1619
+ } catch (error: unknown) {
1620
+ const message = error instanceof Error ? error.message : String(error)
1621
+ if (/is not a function|Satori API 返回 500/i.test(message)) {
1622
+ if (!unsupportedMethods.has(`user.get:${botKey}`)) {
1623
+ unsupportedMethods.add(`user.get:${botKey}`)
1624
+ logger.error(
1625
+ new Error(message),
1626
+ '当前机器人不支持 user.get,已跳过好友身份补充',
1627
+ )
1628
+ }
1629
+ } else {
1630
+ logger.add(
1631
+ LogType.ERR,
1632
+ `获取好友身份信息失败 user.get ${id}: ${message}`,
1633
+ )
1634
+ }
1635
+ }
1636
+ }
1637
+ const item = buildFriendItem(raw, data)
1638
+ processedItems.push(item)
1639
+ if (updateUi && contactStore) {
1640
+ const existing = contactStore.userList.find((contact) => {
1641
+ return String(contact.user_id) === id
1642
+ })
1643
+ if (existing) {
1644
+ if (item.nickname) existing.nickname = String(item.nickname)
1645
+ if (item.remark) existing.remark = String(item.remark)
1646
+ if (item.avatar) existing.avatar = String(item.avatar)
1647
+ if (item.class_id !== undefined) existing.class_id = Number(item.class_id)
1648
+ if (item.class_name) existing.class_name = String(item.class_name)
1649
+ contactStore.userList = [...contactStore.userList]
1650
+ } else {
1651
+ dispatch({ retcode: 0, data: [item] }, 'getFriendList')
1652
+ contactStore.friendLoadedCount += 1
1653
+ }
1654
+ }
1655
+ // 缓存写入不能阻塞身份请求循环,否则第一个好友后就会停住。
1656
+ if (updateUi) {
1657
+ void withTimeout(appendContactCache('friend', item, active), 3000).catch(() => {
1658
+ // 单个缓存写入失败不阻塞后续好友
1659
+ })
1660
+ } else {
1661
+ await withTimeout(appendContactCache('friend', item, active), 3000).catch(() => {
1662
+ // 后台缓存写入失败不中断整个机器人
1663
+ })
1664
+ }
1665
+ }
1666
+
1667
+ const batchSize = 30
1668
+ for (let start = 0; start < friends.length; start += batchSize) {
1669
+ const batchStart = Date.now()
1670
+ const batch = friends.slice(start, start + batchSize)
1671
+ await Promise.all(batch.map((raw) => processFriend(raw).catch((error: unknown) => {
1672
+ logger.add(LogType.ERR, `处理好友身份信息失败: ${String(error)}`)
1673
+ })))
1674
+ const elapsed = Date.now() - batchStart
1675
+ const wait = Math.max(0, 1000 - elapsed)
1676
+ if (wait > 0) {
1677
+ await new Promise((resolve) => setTimeout(resolve, wait))
1678
+ }
1679
+ }
1680
+ return processedItems
1681
+ }
1682
+
1683
+ async function handleFriendListResponse(
1684
+ active: { platform: string; selfId: string },
1685
+ friends: unknown[],
1686
+ ) {
1687
+ const contactStore = useContactStore()
1688
+ const cachePromise = requestContactCache({
1689
+ platform: active.platform,
1690
+ selfId: active.selfId,
1691
+ type: 'friend',
1692
+ }).catch(() => ({}))
1693
+ const cacheResult = await Promise.race([
1694
+ cachePromise,
1695
+ new Promise<null>((resolve) => setTimeout(() => resolve(null), 3000)),
1696
+ ]) ?? {}
1697
+ const cached = Array.isArray(getObject(cacheResult).contacts)
1698
+ ? getObject(cacheResult).contacts as unknown[]
1699
+ : []
1700
+ const cachedMap = new Map<string, Record<string, unknown>>()
1701
+ for (const contact of cached) {
1702
+ const item = getObject(contact)
1703
+ const id = getString(item.id)
1704
+ if (id) cachedMap.set(id, item)
1705
+ }
1706
+ if (!Array.isArray(friends) || friends.length === 0) {
1707
+ contactStore.friendLoading = false
1708
+ if (cachedMap.size > 0) {
1709
+ dispatch({
1710
+ retcode: 0,
1711
+ data: [...cachedMap.values()].map((contact) => cachedFriendRaw(contact, contact)),
1712
+ }, 'getFriendList')
1713
+ }
1714
+ return
1715
+ }
1716
+ const remoteCached = friends.filter((item) => cachedMap.has(contactId(item)))
1717
+ const missing = friends.filter((item) => !cachedMap.has(contactId(item)))
1718
+ const cachedNameIsId = remoteCached.some((item) => {
1719
+ const cachedItem = cachedMap.get(contactId(item))
1720
+ const raw = cachedFriendRaw(getObject(cachedItem), item)
1721
+ const id = String(raw.user_id ?? raw.id ?? '')
1722
+ return !raw.nickname || String(raw.nickname) === id
1723
+ })
1724
+
1725
+ // 从空列表开始重建,先把已缓存好友放回来。
1726
+ contactStore.friendLoading = true
1727
+ contactStore.friendLoadedCount = 0
1728
+ contactStore.friendTotalCount = friends.length
1729
+ contactStore.userList = contactStore.userList.filter((item) => item.group_id)
1730
+ if (remoteCached.length > 0) {
1731
+ const cachedRaws = remoteCached.map((item) => {
1732
+ const cachedItem = cachedMap.get(contactId(item))
1733
+ return cachedFriendRaw(getObject(cachedItem), item)
1734
+ })
1735
+ for (const raw of cachedRaws) {
1736
+ dispatch({ retcode: 0, data: [raw] }, 'getFriendList')
1737
+ contactStore.friendLoadedCount += 1
1738
+ await new Promise((resolve) => setTimeout(resolve, 60))
1739
+ }
1740
+ }
1741
+ try {
1742
+ if (missing.length > 0 && !cachedNameIsId) {
1743
+ // 本地缓存不完整,只补缺失的好友。
1744
+ await fetchFriendProfiles(active, missing)
1745
+ } else {
1746
+ // 本地缓存完整,或旧缓存里还是 QQ 号时,全量刷新身份信息。
1747
+ await fetchFriendProfiles(active, friends)
1748
+ }
1749
+ } finally {
1750
+ contactStore.friendLoading = false
1751
+ }
1752
+ }
1753
+
1754
+ async function loadContactType(
1755
+ active: { platform: string; selfId: string },
1756
+ item: { type: string; action: string; echo: string },
1757
+ ) {
1758
+ // 页面加载只读 LevelDB 缓存;网络请求统一由刷新按钮触发。
1759
+ const result = await requestContactCache({
1760
+ platform: active.platform,
1761
+ selfId: active.selfId,
1762
+ type: item.type,
1763
+ })
1764
+ const contacts = Array.isArray(getObject(result).contacts) ? getObject(result).contacts as unknown[] : []
1765
+ if (contacts.length) {
1766
+ const isFriend = item.type === 'friend'
1767
+ dispatch({
1768
+ retcode: 0,
1769
+ data: contacts.map((contact) => {
1770
+ const cached = getObject(contact)
1771
+ return isFriend ? cachedFriendRaw(cached, contact) : cached.raw ?? contact
1772
+ }),
1773
+ }, item.echo)
1774
+ }
1775
+ }
1776
+
1777
+ export async function loadGroupMembersFromCache(groupId: string) {
1778
+ const active = getActiveBot()
1779
+ if (!active || !groupId || groupId === '0') return
1780
+ const authStore = useAuthStore()
1781
+ try {
1782
+ await loadAllBotsCache()
1783
+ const result = await requestContactCache({
1784
+ platform: active.platform,
1785
+ selfId: active.selfId,
1786
+ type: 'member',
1787
+ groupId,
1788
+ })
1789
+ const members = Array.isArray(getObject(result).members)
1790
+ ? getObject(result).members as unknown[]
1791
+ : []
1792
+ const mapped = members.map((contact) => {
1793
+ const cached = getObject(contact)
1794
+ const raw = getObject(cached.raw) || cached
1795
+ const id = getString(raw.user_id) || getString(raw.id)
1796
+ const cachedName = getString(cached.name) || getString(cached.nickname)
1797
+ const cachedUserAvatar = id
1798
+ ? getCachedUserAvatar(active.platform, active.selfId, id)
1799
+ : ''
1800
+ const avatar = hasUsableAvatar(raw.avatar)
1801
+ ? getString(raw.avatar)
1802
+ : hasUsableAvatar(cached.avatar)
1803
+ ? getString(cached.avatar)
1804
+ : cachedUserAvatar
1805
+ return {
1806
+ user_id: id,
1807
+ nickname: getString(raw.nickname) || getString(raw.name) || cachedName || id,
1808
+ card: getString(raw.card) || '',
1809
+ role: getString(raw.role) || getString(raw.title) || '',
1810
+ avatar,
1811
+ }
1812
+ })
1813
+ const selfId = String(active.selfId)
1814
+ const hasSelfMember = mapped.some((member) => String(member.user_id) === selfId)
1815
+ if (!hasSelfMember) {
1816
+ const selfName = String(authStore.loginInfo.nickname ?? authStore.loginInfo.name ?? '')
1817
+ || selfId
1818
+ const selfAvatar = String(authStore.loginInfo.avatar ?? '')
1819
+ mapped.unshift({
1820
+ user_id: selfId,
1821
+ nickname: selfName,
1822
+ card: '',
1823
+ role: '',
1824
+ avatar: selfAvatar,
1825
+ })
1826
+ await requestContactCache({
1827
+ platform: active.platform,
1828
+ selfId,
1829
+ type: 'member',
1830
+ groupId,
1831
+ contacts: [{
1832
+ id: selfId,
1833
+ name: selfName,
1834
+ avatar: selfAvatar || undefined,
1835
+ raw: {
1836
+ user_id: selfId,
1837
+ nickname: selfName,
1838
+ card: '',
1839
+ avatar: selfAvatar,
1840
+ },
1841
+ }],
1842
+ append: true,
1843
+ }).catch(() => undefined)
1844
+ }
1845
+ const needsIdentity = (member: typeof mapped[number]) => {
1846
+ const usableName = (value: string) => Boolean(value)
1847
+ && value !== member.user_id
1848
+ && !/unknown user|unknown guild|unknown channel/i.test(value)
1849
+ const hasName = usableName(member.nickname) || usableName(member.card)
1850
+ return !hasName || !hasUsableAvatar(member.avatar)
1851
+ }
1852
+ await Promise.all(mapped
1853
+ .filter((member) => member.user_id && member.user_id !== '0' && needsIdentity(member))
1854
+ .map(async (member) => {
1855
+ const info = await requestIdentityCache({
1856
+ platform: active.platform,
1857
+ selfId: active.selfId,
1858
+ type: 'user',
1859
+ id: member.user_id,
1860
+ guildId: groupId,
1861
+ }).catch(() => null)
1862
+ const root = getObject(info)
1863
+ const name = getString(root.name) || getString(root.nickname) || getString(root.nick) || getString(root.username)
1864
+ const avatar = hasUsableAvatar(root.avatar) ? getString(root.avatar) : ''
1865
+ if (name && name !== member.user_id) {
1866
+ const unusableName = (value: string) => !value
1867
+ || value === member.user_id
1868
+ || /unknown user|unknown guild|unknown channel/i.test(value)
1869
+ if (unusableName(member.nickname)) member.nickname = name
1870
+ if (unusableName(member.card)) member.card = name
1871
+ }
1872
+ if (avatar) member.avatar = avatar
1873
+ if (name || avatar) {
1874
+ await requestContactCache({
1875
+ platform: active.platform,
1876
+ selfId: active.selfId,
1877
+ type: 'member',
1878
+ groupId,
1879
+ contacts: [{
1880
+ id: member.user_id,
1881
+ name: name || member.nickname || member.user_id,
1882
+ avatar: avatar || undefined,
1883
+ raw: root,
1884
+ }],
1885
+ append: true,
1886
+ }).catch(() => undefined)
1887
+ }
1888
+ }))
1889
+ dispatch({ retcode: 0, data: mapped }, 'getGroupMemberList')
1890
+ } catch (error: unknown) {
1891
+ const message = error instanceof Error ? error.message : String(error)
1892
+ logger.add(LogType.ERR, `加载群成员缓存失败: ${message}`)
1893
+ }
1894
+ }
1895
+
1896
+ export async function loadContactsFromCache() {
1897
+ const current = getActiveBot()
1898
+ const active = current ?? (login.platform && login.uin
1899
+ ? { platform: login.platform, selfId: String(login.uin) }
1900
+ : null)
1901
+ if (!active) return
1902
+ try {
1903
+ await loadAllBotsCache()
1904
+ } catch (error: unknown) {
1905
+ const message = error instanceof Error ? error.message : String(error)
1906
+ logger.add(LogType.ERR, `加载全部机器人缓存失败: ${message}`)
1907
+ }
1908
+ }
1909
+
1910
+ function startSatori() {
1911
+ disposeConnection?.()
1912
+ login.creating = true
1913
+ disposeConnection = connectSatori(onSatoriEvent, onSatoriReady, (online) => {
1914
+ login.status = online
1915
+ login.creating = !online
1916
+ })
1917
+ }
1918
+
1919
+ export class Connector {
1920
+ static create(address: string, token: string, _wss?: boolean) {
1921
+ login.address = address || 'Satori'
1922
+ login.token = token || ''
1923
+ startSatori()
1924
+ }
1925
+
1926
+ static onopen(_address: string, _token?: string) {
1927
+ login.creating = false
1928
+ login.status = true
1929
+ }
1930
+
1931
+ static onmessage(message: string) {
1932
+ try {
1933
+ dispatch(JSON.parse(message) as Record<string, unknown>)
1934
+ } catch {
1935
+ // 忽略无效消息
1936
+ }
1937
+ }
1938
+
1939
+ static onclose(_code: number, _message?: string, _address?: string, _token?: string) {
1940
+ login.status = false
1941
+ }
1942
+
1943
+ static close() {
1944
+ disposeConnection?.()
1945
+ disposeConnection = null
1946
+ login.status = false
1947
+ }
1948
+
1949
+ static forceDisconnect(_reason?: string) {
1950
+ Connector.close()
1951
+ setTimeout(() => {
1952
+ if (!login.creating) startSatori()
1953
+ }, 1000)
1954
+ }
1955
+
1956
+ static async callApi(api: string, args: Record<string, unknown>): Promise<unknown | undefined> {
1957
+ const mapped = mapAction(api, args)
1958
+ if (!mapped) return undefined
1959
+ const active = getActiveBot()
1960
+ const data = await request(mapped.method, mapped.params, active)
1961
+ return satoriResponseToOneBot(api, data, active.platform).data
1962
+ }
1963
+
1964
+ static send(action: string, value: Record<string, unknown> = {}, echo?: string) {
1965
+ const mapped = mapAction(action, value)
1966
+ if (!mapped) {
1967
+ dispatch({ retcode: 404, data: null }, echo)
1968
+ return
1969
+ }
1970
+ const active = getActiveBot()
1971
+ if (active && (action === 'get_friend_list' || action === 'get_group_list')) {
1972
+ const type = action === 'get_friend_list' ? 'friend' : 'group'
1973
+ void loadContactType(active, { type, action, echo })
1974
+ return
1975
+ }
1976
+ void request(mapped.method, mapped.params, active)
1977
+ .then((data) => {
1978
+ const response = satoriResponseToOneBot(action, data, active.platform)
1979
+ dispatch(response, echo)
1980
+ })
1981
+ .catch((error: unknown) => {
1982
+ dispatch({ retcode: -1, data: null, error: String(error) }, echo)
1983
+ })
1984
+ }
1985
+
1986
+ static sendSeeMod(_name: string, _value: Record<string, unknown>, echo?: string) {
1987
+ dispatch({ retcode: 404, data: null }, echo)
1988
+ }
1989
+
1990
+ static sendRaw(_name: string, _value: Record<string, unknown>, echo?: string) {
1991
+ dispatch({ retcode: 404, data: null }, echo)
1992
+ }
1993
+
1994
+ static sendRawJson(str: string) {
1995
+ try {
1996
+ const parsed = JSON.parse(str) as Record<string, unknown>
1997
+ Connector.send(String(parsed.action ?? ''), getObject(parsed.params))
1998
+ } catch {
1999
+ // 忽略无效的原始消息
2000
+ }
2001
+ }
2002
+ }
2003
+
2004
+ function getObject(value: unknown): Record<string, unknown> {
2005
+ return typeof value === 'object' && value !== null ? value as Record<string, unknown> : {}
2006
+ }
2007
+
2008
+ export function loadConnectionHistory(): ConnectionHistoryItem[] {
2009
+ return login.connectionHistory
2010
+ }
2011
+
2012
+ export function saveConnectionToHistory(address: string, token: string, uin?: string, nickname?: string) {
2013
+ const settingsStore = useSettingsStore()
2014
+ const history = login.connectionHistory
2015
+ const existing = history.findIndex((item) => item.address === address)
2016
+ const item: ConnectionHistoryItem = {
2017
+ address,
2018
+ token: settingsStore.sysConfig.save_password ? token : '',
2019
+ uin,
2020
+ nickname,
2021
+ lastConnected: Date.now(),
2022
+ }
2023
+ if (existing >= 0) {
2024
+ history[existing] = item
2025
+ } else {
2026
+ history.unshift(item)
2027
+ if (history.length > 10) history.pop()
2028
+ }
2029
+ writeHistory(history)
2030
+ }
2031
+
2032
+ export function loadConnectionFromHistory(item: ConnectionHistoryItem) {
2033
+ login.address = item.address
2034
+ login.token = item.token
2035
+ }
2036
+
2037
+ export function deleteConnectionHistory(index: number) {
2038
+ if (index >= 0 && index < login.connectionHistory.length) {
2039
+ login.connectionHistory.splice(index, 1)
2040
+ writeHistory(login.connectionHistory)
2041
+ }
2042
+ }