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.
- package/client/vue/index.vue +60 -634
- package/client/web/dist/assets/AppIcon-DIku6-h0.png +0 -0
- package/client/web/dist/assets/AppIconDark-BYymN71t.png +0 -0
- package/client/web/dist/assets/CapacitorAppIcon-CSZwNhvi.png +0 -0
- package/client/web/dist/assets/CapacitorAppIconDark-BMvMOzsg.png +0 -0
- package/client/web/dist/assets/Chat-CTdyeaX2.css +1 -0
- package/client/web/dist/assets/Chat-DkZvD7d8.js +28 -0
- package/client/web/dist/assets/MealHungryPan-Beos3lRI.js +1 -0
- package/client/web/dist/assets/MsgBody-aZ4_aija.js +1 -0
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CPZjRLp6.js +67 -0
- package/client/web/dist/assets/NapcatAppIcon-C7VOAkW9.png +0 -0
- package/client/web/dist/assets/Vue-lEhHouxB.png +0 -0
- package/client/web/dist/assets/append_darwin-CqBNU62J.css +1 -0
- package/client/web/dist/assets/append_full_vibrancy-j0D2LM9m.css +1 -0
- package/client/web/dist/assets/append_linux-uhxeBqeC.css +1 -0
- package/client/web/dist/assets/append_linux_vibrancy-COiZq8MS.css +1 -0
- package/client/web/dist/assets/append_napcat-DV8CAyAd.css +1 -0
- package/client/web/dist/assets/append_new-407Lo5_V.css +1 -0
- package/client/web/dist/assets/append_vibrancy-6FHctkOY.css +1 -0
- package/client/web/dist/assets/append_win32-srmD-78y.css +1 -0
- package/client/web/dist/assets/cakes-CxTVKdnK.svg +189 -0
- package/client/web/dist/assets/core-DhEqZVGG.js +1 -0
- package/client/web/dist/assets/event-Cadp6m6J.js +1 -0
- package/client/web/dist/assets/index-CPTXiemn.js +4045 -0
- package/client/web/dist/assets/index-Cg_esKB0.js +1 -0
- package/client/web/dist/assets/index-Cln012sP.css +1 -0
- package/client/web/dist/assets//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223-CJKIKBn3.ttf +0 -0
- package/client/web/dist/assets//346/234/237/345/276/205-C90_OY60.png +0 -0
- package/client/web/dist/bcui/css/color-dark.css +35 -0
- package/client/web/dist/bcui/css/color-light.css +35 -0
- package/client/web/dist/bcui/css/style.css +468 -0
- package/client/web/dist/bcui/js/main.js +193 -0
- package/client/web/dist/css/append-dark.css +13 -0
- package/client/web/dist/css/append-light.css +13 -0
- package/client/web/dist/css/append-napcat-dark.css +14 -0
- package/client/web/dist/css/append-napcat-light.css +14 -0
- package/client/web/dist/favicon.ico +0 -0
- package/client/web/dist/img/icons/icon-for-fuck-apple.png +0 -0
- package/client/web/dist/img/icons/icon-maskable.png +0 -0
- package/client/web/dist/img/icons/icon.png +0 -0
- package/client/web/dist/img/icons/icon.svg +10 -0
- package/client/web/dist/index.html +22 -0
- package/client/web/dist/robots.txt +2 -0
- package/client/web/dist/sw.js +58 -0
- package/client/web/env.d.ts +56 -0
- package/client/web/index.html +21 -0
- package/client/web/package-lock.json +3419 -0
- package/client/web/package.json +62 -0
- package/client/web/public/bcui/css/color-dark.css +35 -0
- package/client/web/public/bcui/css/color-light.css +35 -0
- package/client/web/public/bcui/css/style.css +468 -0
- package/client/web/public/bcui/js/main.js +193 -0
- package/client/web/public/css/append-dark.css +13 -0
- package/client/web/public/css/append-light.css +13 -0
- package/client/web/public/css/append-napcat-dark.css +14 -0
- package/client/web/public/css/append-napcat-light.css +14 -0
- package/client/web/public/favicon.ico +0 -0
- package/client/web/public/img/icons/icon-for-fuck-apple.png +0 -0
- package/client/web/public/img/icons/icon-maskable.png +0 -0
- package/client/web/public/img/icons/icon.png +0 -0
- package/client/web/public/img/icons/icon.svg +10 -0
- package/client/web/public/robots.txt +2 -0
- package/client/web/public/sw.js +58 -0
- package/client/web/scripts/copy-bcui.mjs +17 -0
- package/client/web/src/App.vue +1127 -0
- package/client/web/src/assets/css/append/append_darwin.css +68 -0
- package/client/web/src/assets/css/append/append_full_vibrancy.css +290 -0
- package/client/web/src/assets/css/append/append_linux.css +125 -0
- package/client/web/src/assets/css/append/append_linux_vibrancy.css +22 -0
- package/client/web/src/assets/css/append/append_napcat.css +35 -0
- package/client/web/src/assets/css/append/append_new.css +843 -0
- package/client/web/src/assets/css/append/append_vibrancy.css +113 -0
- package/client/web/src/assets/css/append/append_win32.css +109 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_horizontal.css +349 -0
- package/client/web/src/assets/css/append/mobile/append_mobile_vertical.css +96 -0
- package/client/web/src/assets/css/chat.css +2104 -0
- package/client/web/src/assets/css/msg.css +941 -0
- package/client/web/src/assets/css/options.css +811 -0
- package/client/web/src/assets/css/sys_notice.css +62 -0
- package/client/web/src/assets/css/view.css +2105 -0
- package/client/web/src/assets/font//346/226/271/346/255/243/345/226/265/345/221/234/344/275/223.ttf +0 -0
- package/client/web/src/assets/img/Vue.png +0 -0
- package/client/web/src/assets/img/cakes.svg +189 -0
- package/client/web/src/assets/img/icons/AppIcon.png +0 -0
- package/client/web/src/assets/img/icons/AppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/CapacitorAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIcon.png +0 -0
- package/client/web/src/assets/img/icons/NapcatAppIconDark.png +0 -0
- package/client/web/src/assets/img/icons/exit.png +0 -0
- package/client/web/src/assets/img/icons/pen.png +0 -0
- package/client/web/src/assets/img/stars.svg +89 -0
- package/client/web/src/assets/img//346/234/237/345/276/205.png +0 -0
- package/client/web/src/assets/l10n/_About I10n.md +35 -0
- package/client/web/src/assets/l10n/_l10nconfig.json +7 -0
- package/client/web/src/assets/l10n/zh-CN.po +1941 -0
- package/client/web/src/assets/logo.png +0 -0
- package/client/web/src/assets/pathMap/LLOneBot.yaml +2 -0
- package/client/web/src/assets/pathMap/Lagrange.OneBot.yaml +180 -0
- package/client/web/src/assets/pathMap/NapCat.Onebot.yaml +224 -0
- package/client/web/src/assets/pathMap/SnowLuma.yaml +80 -0
- package/client/web/src/assets/qed.txt +1 -0
- package/client/web/src/components/AboutPan.vue +197 -0
- package/client/web/src/components/BulletinBody.vue +129 -0
- package/client/web/src/components/DepPan.vue +310 -0
- package/client/web/src/components/EmojiFace.vue +32 -0
- package/client/web/src/components/FacePan.vue +461 -0
- package/client/web/src/components/FileBody.vue +129 -0
- package/client/web/src/components/FileManager.vue +532 -0
- package/client/web/src/components/FriendBody.vue +54 -0
- package/client/web/src/components/GlobalSessionSearchBar.vue +301 -0
- package/client/web/src/components/History.vue +49 -0
- package/client/web/src/components/LazyLottie.vue +82 -0
- package/client/web/src/components/MergePan.vue +127 -0
- package/client/web/src/components/MsgBody.vue +1478 -0
- package/client/web/src/components/MusicPlayer.vue +549 -0
- package/client/web/src/components/NoticeBody.vue +168 -0
- package/client/web/src/components/QzonePermissionPan.vue +150 -0
- package/client/web/src/components/RawMsgRenderPreviewPan.vue +190 -0
- package/client/web/src/components/ThemeColorPickerPan.vue +420 -0
- package/client/web/src/components/TinySessionBody.vue +51 -0
- package/client/web/src/components/UmamiInfoPan.vue +1549 -0
- package/client/web/src/components/UpdatePan.vue +263 -0
- package/client/web/src/components/ViewerCom.vue +1360 -0
- package/client/web/src/components/VoiceMsg.vue +626 -0
- package/client/web/src/components/WelPan.vue +894 -0
- package/client/web/src/components/msg-body/.gitkeep +0 -0
- package/client/web/src/components/msg-component/JsonSegComp.vue +138 -0
- package/client/web/src/components/msg-component/XmlSegComp.vue +132 -0
- package/client/web/src/components/msg-component/jsonComp/AutoReply.vue +97 -0
- package/client/web/src/components/msg-component/jsonComp/Contact.lua.vue +129 -0
- package/client/web/src/components/msg-component/jsonComp/Feed.lua.vue +94 -0
- package/client/web/src/components/msg-component/jsonComp/Forum.vue +84 -0
- package/client/web/src/components/msg-component/jsonComp/Mannounce.vue +111 -0
- package/client/web/src/components/msg-component/jsonComp/Map.vue +52 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.lua.vue +53 -0
- package/client/web/src/components/msg-component/jsonComp/Miniapp.vue +69 -0
- package/client/web/src/components/msg-component/jsonComp/Music.lua.vue +135 -0
- package/client/web/src/components/msg-component/jsonComp/Tuwen.lua.vue +56 -0
- package/client/web/src/components/notice-component/.gitkeep +0 -0
- package/client/web/src/components/notice-component/MealHungryPan.vue +119 -0
- package/client/web/src/components/tooltip/CustomFaceTooltip.vue +22 -0
- package/client/web/src/components/tooltip/Tooltip.vue +50 -0
- package/client/web/src/components/tooltip/Tooltips.vue +14 -0
- package/client/web/src/components/tooltip/UserInfoTooltip.vue +69 -0
- package/client/web/src/function/base.ts +238 -0
- package/client/web/src/function/connect.ts +2042 -0
- package/client/web/src/function/debug.ts +10 -0
- package/client/web/src/function/elements/information.ts +141 -0
- package/client/web/src/function/elements/system.ts +101 -0
- package/client/web/src/function/elements/vueComp.ts +62 -0
- package/client/web/src/function/favicon.ts +76 -0
- package/client/web/src/function/input.ts +69 -0
- package/client/web/src/function/model/emoji.ts +127 -0
- package/client/web/src/function/model/img.ts +150 -0
- package/client/web/src/function/model/msg-body.ts +51 -0
- package/client/web/src/function/msg.ts +2674 -0
- package/client/web/src/function/notify.ts +225 -0
- package/client/web/src/function/option.ts +749 -0
- package/client/web/src/function/satori-model.ts +818 -0
- package/client/web/src/function/satori.ts +325 -0
- package/client/web/src/function/sender.ts +178 -0
- package/client/web/src/function/tooltip.ts +49 -0
- package/client/web/src/function/utils/appUtil.ts +2288 -0
- package/client/web/src/function/utils/avatarUtil.ts +31 -0
- package/client/web/src/function/utils/backgroundUtil.ts +110 -0
- package/client/web/src/function/utils/linkViewUtil.ts +101 -0
- package/client/web/src/function/utils/localHistoryUtil.ts +517 -0
- package/client/web/src/function/utils/msgUtil.ts +1242 -0
- package/client/web/src/function/utils/pinyin.ts +141 -0
- package/client/web/src/function/utils/recordUtil.ts +187 -0
- package/client/web/src/function/utils/sessionUtil.ts +114 -0
- package/client/web/src/function/utils/systemUtil.ts +664 -0
- package/client/web/src/main.ts +91 -0
- package/client/web/src/pages/Chat.vue +3453 -0
- package/client/web/src/pages/Friends.vue +636 -0
- package/client/web/src/pages/Info.vue +717 -0
- package/client/web/src/pages/Messages.vue +875 -0
- package/client/web/src/pages/Options.vue +82 -0
- package/client/web/src/pages/Qzone.vue +1178 -0
- package/client/web/src/pages/chat-view/ChatDanmaku.vue +759 -0
- package/client/web/src/pages/chat-view/ChatGlagame.vue +642 -0
- package/client/web/src/pages/chat-view/ChatTerminal.vue +1044 -0
- package/client/web/src/pages/chat-view/SystemNotice.vue +171 -0
- package/client/web/src/pages/options/OptAccount.vue +107 -0
- package/client/web/src/pages/options/OptAddon.vue +185 -0
- package/client/web/src/pages/options/OptDev.vue +704 -0
- package/client/web/src/pages/options/OptFunction.vue +403 -0
- package/client/web/src/pages/options/OptInfo.vue +165 -0
- package/client/web/src/pages/options/OptView.vue +846 -0
- package/client/web/src/runtime/backend.ts +310 -0
- package/client/web/src/state/auth.ts +14 -0
- package/client/web/src/state/chat.ts +36 -0
- package/client/web/src/state/connection.ts +20 -0
- package/client/web/src/state/contact.ts +37 -0
- package/client/web/src/state/qzone.ts +18 -0
- package/client/web/src/state/sessionHistory.ts +25 -0
- package/client/web/src/state/settings.ts +18 -0
- package/client/web/src/state/sticker.ts +10 -0
- package/client/web/src/state/ui.ts +56 -0
- package/client/web/tsconfig.json +27 -0
- package/client/web/tsconfig.tsbuildinfo +1 -0
- package/client/web/vite.config.ts +33 -0
- package/dist/index.js +1 -12
- package/dist/style.css +1 -1
- package/lib/index.js +2070 -2077
- package/package.json +15 -2
- package/readme.md +0 -22
- package/src/bootstrap.ts +71 -0
- package/src/cache.ts +319 -0
- package/src/config.ts +23 -35
- package/src/database.ts +650 -0
- package/src/index.ts +27 -59
- package/src/media.ts +302 -0
- package/src/recorder.ts +175 -0
- package/src/self-message.ts +456 -0
- package/src/server.d.ts +26 -0
- package/src/types.ts +108 -48
- package/src/web.ts +712 -0
- package/client/vue/chat-logic.ts +0 -683
- package/client/vue/composables/useChatActions.ts +0 -61
- package/client/vue/composables/useChatData.ts +0 -185
- package/client/vue/composables/useImageCache.ts +0 -281
- package/client/vue/composables/useVideoCache.ts +0 -125
- package/client/vue/types.ts +0 -72
- package/lib/api-handlers.d.ts +0 -25
- package/lib/config.d.ts +0 -14
- package/lib/file-manager.d.ts +0 -124
- package/lib/index.d.ts +0 -17
- package/lib/logger.d.ts +0 -9
- package/lib/message-handler.d.ts +0 -35
- package/lib/types.d.ts +0 -57
- package/lib/utils.d.ts +0 -18
- package/src/api-handlers.ts +0 -646
- package/src/file-manager.ts +0 -1436
- package/src/message-handler.ts +0 -507
- 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
|
+
}
|