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