新增启动页面;修复转发表情包无法索引的问题;修复群回复中消息溢出错误;修复群消息中消息类型判定错误

This commit is contained in:
cc
2026-02-26 19:40:26 +08:00
parent 1c6e14acb4
commit 4a09b682b2
13 changed files with 779 additions and 30 deletions

View File

@@ -82,6 +82,8 @@ let configService: ConfigService | null = null
// 协议窗口实例
let agreementWindow: BrowserWindow | null = null
let onboardingWindow: BrowserWindow | null = null
// Splash 启动窗口
let splashWindow: BrowserWindow | null = null
const keyService = new KeyService()
let mainWindowReady = false
@@ -122,9 +124,10 @@ function createWindow(options: { autoShow?: boolean } = {}) {
})
// 窗口准备好后显示
// Splash 模式下不在这里 show由启动流程统一控制
win.once('ready-to-show', () => {
mainWindowReady = true
if (autoShow || shouldShowMain) {
if (autoShow && !splashWindow) {
win.show()
}
})
@@ -250,6 +253,73 @@ function createAgreementWindow() {
return agreementWindow
}
/**
* 创建 Splash 启动窗口
* 使用纯 HTML 页面,不依赖 React确保极速显示
*/
function createSplashWindow(): BrowserWindow {
const isDev = !!process.env.VITE_DEV_SERVER_URL
const iconPath = isDev
? join(__dirname, '../public/icon.ico')
: join(process.resourcesPath, 'icon.ico')
splashWindow = new BrowserWindow({
width: 760,
height: 460,
resizable: false,
frame: false,
transparent: true,
backgroundColor: '#00000000',
hasShadow: false,
center: true,
skipTaskbar: false,
icon: iconPath,
webPreferences: {
contextIsolation: true,
nodeIntegration: false
// 不需要 preload —— 通过 executeJavaScript 单向推送进度
},
show: false
})
if (isDev) {
splashWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}splash.html`)
} else {
splashWindow.loadFile(join(__dirname, '../dist/splash.html'))
}
splashWindow.once('ready-to-show', () => {
splashWindow?.show()
})
splashWindow.on('closed', () => {
splashWindow = null
})
return splashWindow
}
/**
* 向 Splash 窗口发送进度更新
*/
function updateSplashProgress(percent: number, text: string, indeterminate = false) {
if (splashWindow && !splashWindow.isDestroyed()) {
splashWindow.webContents
.executeJavaScript(`updateProgress(${percent}, ${JSON.stringify(text)}, ${indeterminate})`)
.catch(() => {})
}
}
/**
* 关闭 Splash 窗口
*/
function closeSplash() {
if (splashWindow && !splashWindow.isDestroyed()) {
splashWindow.close()
splashWindow = null
}
}
/**
* 创建首次引导窗口
*/
@@ -1508,26 +1578,70 @@ function checkForUpdatesOnStartup() {
}, 3000)
}
app.whenReady().then(() => {
app.whenReady().then(async () => {
// 立即创建 Splash 窗口,确保用户尽快看到反馈
createSplashWindow()
// 等待 Splash 页面加载完成后再推送进度
if (splashWindow) {
await new Promise<void>((resolve) => {
if (splashWindow!.webContents.isLoading()) {
splashWindow!.webContents.once('did-finish-load', () => resolve())
} else {
resolve()
}
})
splashWindow.webContents
.executeJavaScript(`setVersion(${JSON.stringify(app.getVersion())})`)
.catch(() => {})
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
// 初始化配置服务
updateSplashProgress(5, '正在加载配置...')
configService = new ConfigService()
// 将用户主题配置推送给 Splash 窗口
if (splashWindow && !splashWindow.isDestroyed()) {
const themeId = configService.get('themeId') || 'cloud-dancer'
const themeMode = configService.get('theme') || 'system'
splashWindow.webContents
.executeJavaScript(`applyTheme(${JSON.stringify(themeId)}, ${JSON.stringify(themeMode)})`)
.catch(() => {})
}
await delay(200)
// 设置资源路径
updateSplashProgress(10, '正在初始化...')
const candidateResources = app.isPackaged
? join(process.resourcesPath, 'resources')
: join(app.getAppPath(), 'resources')
const fallbackResources = join(process.cwd(), 'resources')
const resourcesPath = existsSync(candidateResources) ? candidateResources : fallbackResources
const userDataPath = app.getPath('userData')
await delay(200)
// 初始化数据库服务
updateSplashProgress(18, '正在初始化...')
wcdbService.setPaths(resourcesPath, userDataPath)
wcdbService.setLogEnabled(configService.get('logEnabled') === true)
await delay(200)
// 注册 IPC 处理器
updateSplashProgress(25, '正在初始化...')
registerIpcHandlers()
await delay(200)
// 检查配置状态
const onboardingDone = configService.get('onboardingDone')
shouldShowMain = onboardingDone === true
mainWindow = createWindow({ autoShow: shouldShowMain })
if (!onboardingDone) {
createOnboardingWindow()
}
// 创建主窗口(不显示,由启动流程统一控制)
updateSplashProgress(30, '正在加载界面...')
mainWindow = createWindow({ autoShow: false })
// 解决朋友圈图片无法加载问题(添加 Referer
// 配置网络服务
session.defaultSession.webRequest.onBeforeSendHeaders(
{
urls: ['*://*.qpic.cn/*', '*://*.wx.qq.com/*']
@@ -1538,7 +1652,31 @@ app.whenReady().then(() => {
}
)
// 启动时检测更新
// 等待主窗口加载完成(真正耗时阶段,进度条末端呼吸光点)
updateSplashProgress(30, '正在加载界面...', true)
await new Promise<void>((resolve) => {
if (mainWindowReady) {
resolve()
} else {
mainWindow!.once('ready-to-show', () => {
mainWindowReady = true
resolve()
})
}
})
// 加载完成,收尾
updateSplashProgress(100, '启动完成')
await new Promise((resolve) => setTimeout(resolve, 250))
closeSplash()
if (!onboardingDone) {
createOnboardingWindow()
} else {
mainWindow?.show()
}
// 启动时检测更新(不阻塞启动)
checkForUpdatesOnStartup()
app.on('activate', () => {

View File

@@ -991,12 +991,34 @@ class ChatService {
}
console.warn(`[ChatService] 表情包数据库未命中: md5=${msg.emojiMd5}, db=${dbPath}`)
// 数据库未命中时,尝试从本地 emoji 缓存目录查找(转发的表情包只有 md5无 CDN URL
this.findEmojiInLocalCache(msg)
} catch (e) {
console.error(`[ChatService] 恢复表情包失败: md5=${msg.emojiMd5}`, e)
}
}
/**
* 从本地 WeFlow emoji 缓存目录按 md5 查找文件
*/
private findEmojiInLocalCache(msg: Message): void {
if (!msg.emojiMd5) return
const cacheDir = this.getEmojiCacheDir()
if (!existsSync(cacheDir)) return
const extensions = ['.gif', '.png', '.webp', '.jpg', '.jpeg']
for (const ext of extensions) {
const filePath = join(cacheDir, `${msg.emojiMd5}${ext}`)
if (existsSync(filePath)) {
msg.emojiLocalPath = filePath
// 同步写入内存缓存,避免重复查找
emojiCache.set(msg.emojiMd5, filePath)
return
}
}
}
/**
* 查找 emoticon.db 路径
*/
@@ -1338,6 +1360,9 @@ class ChatService {
chatRecordList = type49Info.chatRecordList
transferPayerUsername = type49Info.transferPayerUsername
transferReceiverUsername = type49Info.transferReceiverUsername
// 引用消息appmsg type=57的 quotedContent/quotedSender
if (type49Info.quotedContent !== undefined) quotedContent = type49Info.quotedContent
if (type49Info.quotedSender !== undefined) quotedSender = type49Info.quotedSender
} else if (localType === 244813135921 || (content && content.includes('<type>57</type>'))) {
const quoteInfo = this.parseQuoteMessage(content)
quotedContent = quoteInfo.content
@@ -1381,6 +1406,8 @@ class ChatService {
chatRecordList = chatRecordList || type49Info.chatRecordList
transferPayerUsername = transferPayerUsername || type49Info.transferPayerUsername
transferReceiverUsername = transferReceiverUsername || type49Info.transferReceiverUsername
if (!quotedContent && type49Info.quotedContent !== undefined) quotedContent = type49Info.quotedContent
if (!quotedSender && type49Info.quotedSender !== undefined) quotedSender = type49Info.quotedSender
}
messages.push({
@@ -1549,7 +1576,17 @@ class ChatService {
private parseType49(content: string): string {
const title = this.extractXmlValue(content, 'title')
const type = this.extractXmlValue(content, 'type')
// 从 appmsg 直接子节点提取 type避免匹配到 refermsg 内部的 <type>
let type = ''
const appmsgMatch = /<appmsg[\s\S]*?>([\s\S]*?)<\/appmsg>/i.exec(content)
if (appmsgMatch) {
const inner = appmsgMatch[1]
.replace(/<refermsg[\s\S]*?<\/refermsg>/gi, '')
.replace(/<patMsg[\s\S]*?<\/patMsg>/gi, '')
const typeMatch = /<type>([\s\S]*?)<\/type>/i.exec(inner)
if (typeMatch) type = typeMatch[1].trim()
}
if (!type) type = this.extractXmlValue(content, 'type')
const normalized = content.toLowerCase()
const locationLabel =
this.extractXmlAttribute(content, 'location', 'label') ||
@@ -1964,6 +2001,8 @@ class ChatService {
*/
private parseType49Message(content: string): {
xmlType?: string
quotedContent?: string
quotedSender?: string
linkTitle?: string
linkUrl?: string
linkThumb?: string
@@ -2008,8 +2047,20 @@ class ChatService {
try {
if (!content) return {}
// 提取 appmsg 的 type
const xmlType = this.extractXmlValue(content, 'type')
// 提取 appmsg 直接子节点的 type,避免匹配到 refermsg 内部的 <type>
// 先尝试从 <appmsg>...</appmsg> 块内提取,再用正则跳过嵌套标签
let xmlType = ''
const appmsgMatch = /<appmsg[\s\S]*?>([\s\S]*?)<\/appmsg>/i.exec(content)
if (appmsgMatch) {
// 在 appmsg 内容中,找第一个 <type> 但跳过在子元素内部的(如 refermsg > type
// 策略去掉所有嵌套块refermsg、patMsg 等),再提取 type
const appmsgInner = appmsgMatch[1]
.replace(/<refermsg[\s\S]*?<\/refermsg>/gi, '')
.replace(/<patMsg[\s\S]*?<\/patMsg>/gi, '')
const typeMatch = /<type>([\s\S]*?)<\/type>/i.exec(appmsgInner)
if (typeMatch) xmlType = typeMatch[1].trim()
}
if (!xmlType) xmlType = this.extractXmlValue(content, 'type')
if (!xmlType) return {}
const result: any = { xmlType }
@@ -2126,6 +2177,12 @@ class ChatService {
result.appMsgKind = 'transfer'
} else if (xmlType === '87') {
result.appMsgKind = 'announcement'
} else if (xmlType === '57') {
// 引用回复消息,解析 refermsg
result.appMsgKind = 'quote'
const quoteInfo = this.parseQuoteMessage(content)
result.quotedContent = quoteInfo.content
result.quotedSender = quoteInfo.sender
} else if ((xmlType === '5' || xmlType === '49') && (sourceUsername?.startsWith('gh_') || appName?.includes('公众号') || sourceName)) {
result.appMsgKind = 'official-link'
} else if (url) {