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,141 @@
|
|
|
1
|
+
import { isDebugMode } from '../debug'
|
|
2
|
+
|
|
3
|
+
export type PinYinData = {
|
|
4
|
+
main: string[]
|
|
5
|
+
short: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* eslint-disable no-console */
|
|
9
|
+
|
|
10
|
+
const PINYIN_SCRIPT_SRC = 'https://lib.stapxs.cn/modules/pinyin.min.js'
|
|
11
|
+
|
|
12
|
+
let pinyinLoadPromise: Promise<boolean> | null = null
|
|
13
|
+
|
|
14
|
+
function createEmptyPinyinData(): PinYinData {
|
|
15
|
+
return {
|
|
16
|
+
main: [],
|
|
17
|
+
short: []
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasPinyinLib() {
|
|
22
|
+
return typeof window !== 'undefined' && typeof window.pinyin !== 'undefined'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function scheduleIdleTask(task: () => void) {
|
|
26
|
+
if (typeof window === 'undefined') return
|
|
27
|
+
|
|
28
|
+
const idleWindow = window as Window & {
|
|
29
|
+
requestIdleCallback?: (
|
|
30
|
+
callback: IdleRequestCallback,
|
|
31
|
+
options?: IdleRequestOptions
|
|
32
|
+
) => number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof idleWindow.requestIdleCallback === 'function') {
|
|
36
|
+
idleWindow.requestIdleCallback(() => task(), { timeout: 1500 })
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
window.setTimeout(task, 300)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isPinyinReady() {
|
|
44
|
+
return hasPinyinLib()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ensurePinyinLoaded(): Promise<boolean> {
|
|
48
|
+
if (hasPinyinLib()) return Promise.resolve(true)
|
|
49
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
50
|
+
return Promise.resolve(false)
|
|
51
|
+
}
|
|
52
|
+
if (pinyinLoadPromise !== null) return pinyinLoadPromise
|
|
53
|
+
|
|
54
|
+
pinyinLoadPromise = new Promise((resolve) => {
|
|
55
|
+
let script = document.querySelector(
|
|
56
|
+
'script[data-ssqq-pinyin-lib="true"]',
|
|
57
|
+
) as HTMLScriptElement | null
|
|
58
|
+
|
|
59
|
+
const finish = (success: boolean) => {
|
|
60
|
+
if (!success) {
|
|
61
|
+
pinyinLoadPromise = null
|
|
62
|
+
}
|
|
63
|
+
resolve(success)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const handleLoad = () => {
|
|
67
|
+
if (script) {
|
|
68
|
+
script.dataset.loaded = 'true'
|
|
69
|
+
}
|
|
70
|
+
finish(hasPinyinLib())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const handleError = () => {
|
|
74
|
+
if (isDebugMode()) console.warn('拼音库加载失败')
|
|
75
|
+
script?.remove()
|
|
76
|
+
finish(false)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (script?.dataset.loaded === 'true') {
|
|
80
|
+
finish(hasPinyinLib())
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!script) {
|
|
85
|
+
script = document.createElement('script')
|
|
86
|
+
script.src = PINYIN_SCRIPT_SRC
|
|
87
|
+
script.async = true
|
|
88
|
+
script.dataset.ssqqPinyinLib = 'true'
|
|
89
|
+
document.body.appendChild(script)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
script.addEventListener('load', handleLoad, { once: true })
|
|
93
|
+
script.addEventListener('error', handleError, { once: true })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return pinyinLoadPromise
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function preloadPinyin() {
|
|
100
|
+
scheduleIdleTask(() => {
|
|
101
|
+
void ensurePinyinLoaded()
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getPinyin(name: string): PinYinData {
|
|
106
|
+
if (!hasPinyinLib()) return createEmptyPinyinData()
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const pinyinLib = window.pinyin
|
|
110
|
+
if (!pinyinLib) return createEmptyPinyinData()
|
|
111
|
+
return {
|
|
112
|
+
main: pinyinLib.pinyin(name, {
|
|
113
|
+
heteronym: true,
|
|
114
|
+
compact: true,
|
|
115
|
+
style: 'normal',
|
|
116
|
+
}).map((item: string[]) => item.join('').toLowerCase()),
|
|
117
|
+
short: pinyinLib.pinyin(name, {
|
|
118
|
+
heteronym: true,
|
|
119
|
+
compact: true,
|
|
120
|
+
style: 'first_letter',
|
|
121
|
+
}).map((item: string[]) => item.join('').toLowerCase()),
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (isDebugMode()) console.warn('拼音转换失败:', error)
|
|
125
|
+
return createEmptyPinyinData()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function matchPinyin(
|
|
130
|
+
pinyinData: PinYinData,
|
|
131
|
+
matchStr: string
|
|
132
|
+
): boolean {
|
|
133
|
+
const str = matchStr.toLowerCase()
|
|
134
|
+
for (const py of pinyinData.main) {
|
|
135
|
+
if (py.includes(str)) return true
|
|
136
|
+
}
|
|
137
|
+
for (const pyShort of pinyinData.short) {
|
|
138
|
+
if (pyShort.includes(str)) return true
|
|
139
|
+
}
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @FileDescription: 语音消息处理工具
|
|
3
|
+
* @Author: FuQuan233
|
|
4
|
+
* @Date: 2026/06/04
|
|
5
|
+
* @Version: 1.0
|
|
6
|
+
* @Description: 通过 OneBot get_record API 加载语音消息。
|
|
7
|
+
* 以 out_format=mp3 请求格式转换,响应 base64 数据直接用于播放。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Logger } from '../../function/base'
|
|
11
|
+
import { Connector } from '../../function/connect'
|
|
12
|
+
import { getBootstrap } from '../../function/satori'
|
|
13
|
+
|
|
14
|
+
const logger = new Logger()
|
|
15
|
+
|
|
16
|
+
/** 语音消息原始数据(来自 OneBot record 消息段) */
|
|
17
|
+
export interface RecordMsgData {
|
|
18
|
+
file?: string
|
|
19
|
+
url?: string
|
|
20
|
+
path?: string
|
|
21
|
+
webUrl?: string
|
|
22
|
+
magic?: boolean
|
|
23
|
+
duration?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 语音加载结果 */
|
|
27
|
+
export interface RecordLoadResult {
|
|
28
|
+
src: string
|
|
29
|
+
duration: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 加载缓存 */
|
|
33
|
+
const loadCache = new Map<string, RecordLoadResult>()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* base64 字符串转 Blob URL
|
|
37
|
+
*/
|
|
38
|
+
function base64ToBlobUrl(base64: string): string {
|
|
39
|
+
if (base64.startsWith('base64://')) base64 = base64.substring(9)
|
|
40
|
+
const binary = atob(base64)
|
|
41
|
+
const bytes = new Uint8Array(binary.length)
|
|
42
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
|
43
|
+
return URL.createObjectURL(new Blob([bytes], { type: 'audio/mpeg' }))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 获取音频时长
|
|
48
|
+
*/
|
|
49
|
+
function getAudioDuration(url: string): Promise<number> {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const audio = new Audio()
|
|
52
|
+
let resolved = false
|
|
53
|
+
const done = (val: number) => {
|
|
54
|
+
if (!resolved) {
|
|
55
|
+
resolved = true
|
|
56
|
+
audio.removeEventListener('loadedmetadata', onMeta)
|
|
57
|
+
audio.removeEventListener('error', onErr)
|
|
58
|
+
audio.src = ''
|
|
59
|
+
resolve(val)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const onMeta = () => done(audio.duration || 0)
|
|
63
|
+
const onErr = () => done(0)
|
|
64
|
+
audio.addEventListener('loadedmetadata', onMeta)
|
|
65
|
+
audio.addEventListener('error', onErr)
|
|
66
|
+
setTimeout(() => done(0), 3000)
|
|
67
|
+
audio.src = url
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isDirectAudioSource(value: string): boolean {
|
|
72
|
+
return /^(https?:|data:|blob:)/i.test(value) || value.startsWith('/')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isLocalAudioPath(value: string): boolean {
|
|
76
|
+
return value.startsWith('file:') ||
|
|
77
|
+
/^[a-z]:[\\/]/i.test(value) ||
|
|
78
|
+
value.startsWith('\\\\')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function resolveLocalAudioUrl(value: string): Promise<string> {
|
|
82
|
+
if (isDirectAudioSource(value)) return value
|
|
83
|
+
if (!isLocalAudioPath(value)) return ''
|
|
84
|
+
const info = await getBootstrap()
|
|
85
|
+
const basePath = info.basePath || '/chat-patch'
|
|
86
|
+
const fileName = value
|
|
87
|
+
.replace(/^file:\/\//i, '')
|
|
88
|
+
.split(/[\\/]/)
|
|
89
|
+
.pop() || value
|
|
90
|
+
return `${location.origin}${basePath}/api/media?file=${encodeURIComponent(fileName)}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// 公开 API
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 加载语音消息为可播放的音频 URL
|
|
99
|
+
*
|
|
100
|
+
* 通过 OneBot get_record API (out_format=mp3) 获取语音,
|
|
101
|
+
* 响应中的 base64 字段直接包含 MP3 数据。
|
|
102
|
+
*
|
|
103
|
+
* @param data 语音消息数据
|
|
104
|
+
* @param msgId 消息 ID(用于缓存键)
|
|
105
|
+
* @returns 加载结果
|
|
106
|
+
* @throws 无法加载时抛出错误
|
|
107
|
+
*/
|
|
108
|
+
export async function loadRecord(data: RecordMsgData, msgId?: string): Promise<RecordLoadResult> {
|
|
109
|
+
const cacheKey = msgId || data.file!
|
|
110
|
+
|
|
111
|
+
// 缓存检查
|
|
112
|
+
const cached = loadCache.get(cacheKey)
|
|
113
|
+
if (cached) return cached
|
|
114
|
+
|
|
115
|
+
if (!data.file) {
|
|
116
|
+
throw new Error('语音消息缺少 file 字段')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const directSource = String(data.webUrl || data.url || data.file || data.path || '')
|
|
120
|
+
if (directSource) {
|
|
121
|
+
const directUrl = await resolveLocalAudioUrl(directSource)
|
|
122
|
+
if (directUrl) {
|
|
123
|
+
const duration = data.duration || (await getAudioDuration(directUrl))
|
|
124
|
+
const loadResult: RecordLoadResult = { src: directUrl, duration }
|
|
125
|
+
loadCache.set(cacheKey, loadResult)
|
|
126
|
+
return loadResult
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 调用 get_record API
|
|
131
|
+
const raw = await Connector.callApi('get_record', {
|
|
132
|
+
file: data.file,
|
|
133
|
+
out_format: 'mp3',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (!raw) throw new Error('get_record API 返回空')
|
|
137
|
+
|
|
138
|
+
// result 兼容数组和单个对象
|
|
139
|
+
const result: any = Array.isArray(raw) ? raw[0] : raw
|
|
140
|
+
|
|
141
|
+
if (!result?.base64 || typeof result.base64 !== 'string') {
|
|
142
|
+
logger.debug('get_record 响应中没有 base64: ' + JSON.stringify(result).substring(0, 200))
|
|
143
|
+
throw new Error('OneBot 端不支持语音格式转换')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const blobUrl = base64ToBlobUrl(result.base64)
|
|
147
|
+
const duration = data.duration || (await getAudioDuration(blobUrl))
|
|
148
|
+
|
|
149
|
+
logger.debug(`语音加载完成: ${duration.toFixed(1)}s`)
|
|
150
|
+
|
|
151
|
+
const loadResult: RecordLoadResult = { src: blobUrl, duration }
|
|
152
|
+
loadCache.set(cacheKey, loadResult)
|
|
153
|
+
return loadResult
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 释放语音资源
|
|
158
|
+
*/
|
|
159
|
+
export function revokeRecord(msgId?: string): void {
|
|
160
|
+
if (msgId) {
|
|
161
|
+
const cached = loadCache.get(msgId)
|
|
162
|
+
if (cached?.src.startsWith('blob:')) URL.revokeObjectURL(cached.src)
|
|
163
|
+
loadCache.delete(msgId)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 清除所有缓存
|
|
169
|
+
*/
|
|
170
|
+
export function clearRecordCache(): void {
|
|
171
|
+
for (const [, r] of loadCache) {
|
|
172
|
+
if (r.src.startsWith('blob:')) URL.revokeObjectURL(r.src)
|
|
173
|
+
}
|
|
174
|
+
loadCache.clear()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 格式化时长显示
|
|
179
|
+
* @param seconds 秒数
|
|
180
|
+
* @returns 格式化的时长字符串 (m:ss)
|
|
181
|
+
*/
|
|
182
|
+
export function formatRecordDuration(seconds: number): string {
|
|
183
|
+
if (!seconds || seconds <= 0) return '0:00'
|
|
184
|
+
const m = Math.floor(seconds / 60)
|
|
185
|
+
const s = Math.floor(seconds % 60)
|
|
186
|
+
return `${m}:${s.toString().padStart(2, '0')}`
|
|
187
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Session } from '../elements/information'
|
|
2
|
+
|
|
3
|
+
export function normalizeSessionId(value: number | string): string {
|
|
4
|
+
return String(value)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getSessionId(item: Session): number | string {
|
|
8
|
+
return item.user_id ?? item.group_id ?? 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function findSessionContact(
|
|
12
|
+
contacts: Session[],
|
|
13
|
+
sessionId: number | string,
|
|
14
|
+
) {
|
|
15
|
+
return contacts.find((item) => {
|
|
16
|
+
return normalizeSessionId(getSessionId(item)) === normalizeSessionId(sessionId)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getMissingGroupPreviewSessions(
|
|
21
|
+
contacts: Session[],
|
|
22
|
+
knownSessions: ReadonlyMap<number | string, Session>,
|
|
23
|
+
) {
|
|
24
|
+
return contacts.filter((item) => {
|
|
25
|
+
const sessionId = getSessionId(item)
|
|
26
|
+
return Boolean(item.group_id) &&
|
|
27
|
+
sessionId !== 0 &&
|
|
28
|
+
sessionId !== '' &&
|
|
29
|
+
!item.time &&
|
|
30
|
+
!item.raw_msg &&
|
|
31
|
+
!knownSessions.has(normalizeSessionId(sessionId))
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveIncomingSession(
|
|
36
|
+
contacts: Session[],
|
|
37
|
+
sessionId: number | string,
|
|
38
|
+
isGroup: boolean,
|
|
39
|
+
senderName?: string,
|
|
40
|
+
) {
|
|
41
|
+
const contact = findSessionContact(contacts, sessionId)
|
|
42
|
+
if (contact) return contact
|
|
43
|
+
|
|
44
|
+
const normalizedId = normalizeSessionId(sessionId)
|
|
45
|
+
|
|
46
|
+
// 消息事件可能早于好友/群列表返回。先保留会话动态状态,列表加载后再合并真实资料。
|
|
47
|
+
if (isGroup) {
|
|
48
|
+
return {
|
|
49
|
+
group_id: normalizedId,
|
|
50
|
+
group_name: normalizedId,
|
|
51
|
+
} as Session
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
user_id: normalizedId,
|
|
55
|
+
nickname: senderName || normalizedId,
|
|
56
|
+
remark: '',
|
|
57
|
+
} as Session
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SESSION_STATE_KEYS = [
|
|
61
|
+
'new_msg',
|
|
62
|
+
'raw_msg',
|
|
63
|
+
'raw_msg_base',
|
|
64
|
+
'time',
|
|
65
|
+
'always_top',
|
|
66
|
+
'message_id',
|
|
67
|
+
'highlight',
|
|
68
|
+
] as const
|
|
69
|
+
|
|
70
|
+
type SessionStateKey = (typeof SESSION_STATE_KEYS)[number]
|
|
71
|
+
|
|
72
|
+
function copyDefinedSessionState<K extends SessionStateKey>(
|
|
73
|
+
contact: Session,
|
|
74
|
+
currentSession: Session,
|
|
75
|
+
key: K,
|
|
76
|
+
) {
|
|
77
|
+
const value = currentSession[key]
|
|
78
|
+
if (value !== undefined) {
|
|
79
|
+
contact[key] = value
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function mergeSessionState(
|
|
84
|
+
contact: Session,
|
|
85
|
+
currentSession: Session,
|
|
86
|
+
) {
|
|
87
|
+
SESSION_STATE_KEYS.forEach((key) =>
|
|
88
|
+
copyDefinedSessionState(contact, currentSession, key),
|
|
89
|
+
)
|
|
90
|
+
return contact
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 让真实联系人接管消息早到时创建的占位会话。
|
|
95
|
+
* 返回 true 表示 Map 中的对象引用已替换,调用方需要重建派生会话列表。
|
|
96
|
+
*/
|
|
97
|
+
export function mergeEarlySessionContacts(
|
|
98
|
+
contacts: Session[],
|
|
99
|
+
sessions: Map<number | string, Session>,
|
|
100
|
+
) {
|
|
101
|
+
let didMerge = false
|
|
102
|
+
contacts.forEach((contact) => {
|
|
103
|
+
const sessionId = normalizeSessionId(getSessionId(contact))
|
|
104
|
+
const currentSession = sessions.get(sessionId)
|
|
105
|
+
if (currentSession && currentSession !== contact) {
|
|
106
|
+
sessions.set(
|
|
107
|
+
sessionId,
|
|
108
|
+
mergeSessionState(contact, currentSession),
|
|
109
|
+
)
|
|
110
|
+
didMerge = true
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
return didMerge
|
|
114
|
+
}
|