Merge pull request #437 from hicccc77/dev

Dev
This commit is contained in:
cc
2026-03-15 11:06:58 +08:00
committed by GitHub
13 changed files with 365 additions and 89 deletions

View File

@@ -1,5 +1,5 @@
import './preload-env'
import { app, BrowserWindow, ipcMain, nativeTheme, session } from 'electron'
import { app, BrowserWindow, ipcMain, nativeTheme, session, Tray, Menu, nativeImage } from 'electron'
import { Worker } from 'worker_threads'
import { join, dirname } from 'path'
import { autoUpdater } from 'electron-updater'
@@ -96,6 +96,7 @@ const keyService = process.platform === 'darwin'
let mainWindowReady = false
let shouldShowMain = true
let isAppQuitting = false
let tray: Tray | null = null
// 更新下载状态管理Issue #294 修复)
let isDownloadInProgress = false
@@ -352,6 +353,13 @@ function createWindow(options: { autoShow?: boolean } = {}) {
callback(false)
})
win.on('close', (e) => {
if (isAppQuitting) return
// 关闭主窗口时隐藏到状态栏而不是退出
e.preventDefault()
win.hide()
})
win.on('closed', () => {
if (mainWindow !== win) return
@@ -359,7 +367,6 @@ function createWindow(options: { autoShow?: boolean } = {}) {
mainWindowReady = false
if (process.platform !== 'darwin' && !isAppQuitting) {
// 隐藏通知窗也是 BrowserWindow必须销毁否则会阻止应用退出。
destroyNotificationWindow()
if (BrowserWindow.getAllWindows().length === 0) {
app.quit()
@@ -2439,6 +2446,55 @@ app.whenReady().then(async () => {
updateSplashProgress(30, '正在加载界面...')
mainWindow = createWindow({ autoShow: false })
// 初始化系统托盘图标(与其他窗口 icon 路径逻辑保持一致)
const resolvedTrayIcon = process.platform === 'win32'
? join(__dirname, '../public/icon.ico')
: (process.platform === 'darwin'
? join(process.resourcesPath, 'icon.icns')
: join(process.resourcesPath, 'icon.ico'))
try {
tray = new Tray(resolvedTrayIcon)
tray.setToolTip('WeFlow')
const contextMenu = Menu.buildFromTemplate([
{
label: '显示主窗口',
click: () => {
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
}
}
},
{ type: 'separator' },
{
label: '退出',
click: () => {
isAppQuitting = true
app.quit()
}
}
])
tray.setContextMenu(contextMenu)
tray.on('click', () => {
if (mainWindow) {
if (mainWindow.isVisible()) {
mainWindow.focus()
} else {
mainWindow.show()
mainWindow.focus()
}
}
})
tray.on('double-click', () => {
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
}
})
} catch (e) {
console.warn('[Tray] Failed to create tray icon:', e)
}
// 配置网络服务
session.defaultSession.webRequest.onBeforeSendHeaders(
{
@@ -2486,12 +2542,20 @@ app.whenReady().then(async () => {
app.on('before-quit', async () => {
isAppQuitting = true
// 销毁 tray 图标
if (tray) { try { tray.destroy() } catch {} tray = null }
// 通知窗使用 hide 而非 close退出时主动销毁避免残留窗口阻塞进程退出。
destroyNotificationWindow()
// 兜底5秒后强制退出防止某个异步任务卡住导致进程残留
const forceExitTimer = setTimeout(() => {
console.warn('[App] Force exit after timeout')
app.exit(0)
}, 5000)
forceExitTimer.unref()
// 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出
try { await httpService.stop() } catch {}
// 终止 wcdb Worker 线程,避免线程阻止进程退出
try { wcdbService.shutdown() } catch {}
try { await wcdbService.shutdown() } catch {}
})
app.on('window-all-closed', () => {

View File

@@ -606,34 +606,14 @@ export class KeyService {
const logs: string[] = []
onStatus?.('正在定位微信安装路径...', 0)
let wechatPath = await this.findWeChatInstallPath()
if (!wechatPath) {
const err = '未找到微信安装路径请确认已安装PC微信'
onStatus?.('正在查找微信进程...', 0)
const pid = await this.findWeChatPid()
if (!pid) {
const err = '未找到微信进程,请先启动微信'
onStatus?.(err, 2)
return { success: false, error: err }
}
onStatus?.('正在关闭微信以进行获取...', 0)
const closed = await this.killWeChatProcesses()
if (!closed) {
const err = '无法自动关闭微信,请手动退出后重试'
onStatus?.(err, 2)
return { success: false, error: err }
}
onStatus?.('正在启动微信...', 0)
const sub = spawn(wechatPath, {
detached: true,
stdio: 'ignore',
cwd: dirname(wechatPath)
})
sub.unref()
onStatus?.('等待微信界面就绪...', 0)
const pid = await this.waitForWeChatWindow()
if (!pid) return { success: false, error: '启动微信失败或等待界面就绪超时' }
onStatus?.(`检测到微信窗口 (PID: ${pid}),正在获取...`, 0)
onStatus?.('正在检测微信界面组件...', 0)
await this.waitForWeChatWindowComponents(pid, 15000)

View File

@@ -363,7 +363,7 @@ export class KeyServiceMac {
// 用 AppleScript 的 quoted form 组装命令,避免复杂 shell 拼接导致整条失败
const scriptLines = [
`set helperPath to ${JSON.stringify(helperPath)}`,
`set cmd to quoted form of helperPath & " ${pid} ${waitMs}"`,
`set cmd to quoted form of helperPath & " ${pid} ${waitMs} 2>&1"`,
'do shell script cmd with administrator privileges'
]
onStatus?.('已准备就绪,现在登录微信或退出登录后重新登录微信', 0)
@@ -380,18 +380,27 @@ export class KeyServiceMac {
}
const lines = String(stdout).split(/\r?\n/).map(x => x.trim()).filter(Boolean)
const last = lines[lines.length - 1]
if (!last) throw new Error('elevated helper returned empty output')
if (!lines.length) throw new Error('elevated helper returned empty output')
let payload: any
try {
payload = JSON.parse(last)
} catch {
throw new Error('elevated helper returned invalid json: ' + last)
// 从所有行里提取所有 JSON 对象(同一行可能有多个拼接),找含 key/result 的那个
const extractJsonObjects = (s: string): any[] => {
const results: any[] = []
const re = /\{[^{}]*\}/g
let m: RegExpExecArray | null
while ((m = re.exec(s)) !== null) {
try { results.push(JSON.parse(m[0])) } catch { }
}
return results
}
if (payload?.success === true && typeof payload?.key === 'string') return payload.key
if (typeof payload?.result === 'string') return payload.result
throw new Error('elevated helper json missing key/result')
const fullOutput = lines.join('\n')
const allJson = extractJsonObjects(fullOutput)
// 优先找 success=true && key 字段
const successPayload = allJson.find(p => p?.success === true && typeof p?.key === 'string')
if (successPayload) return successPayload.key
// 其次找 result 字段
const resultPayload = allJson.find(p => typeof p?.result === 'string')
if (resultPayload) return resultPayload.result
throw new Error('elevated helper returned invalid json: ' + lines[lines.length - 1])
}
private mapDbKeyErrorMessage(code?: string, detail?: string): string {

View File

@@ -174,10 +174,10 @@ export class WcdbService {
/**
* 关闭服务
*/
shutdown(): void {
this.close()
async shutdown(): Promise<void> {
try { await this.close() } catch {}
if (this.worker) {
this.worker.terminate()
try { await this.worker.terminate() } catch {}
this.worker = null
}
}