This commit is contained in:
cc
2026-04-16 22:03:39 +08:00
parent f9122492db
commit fd4a214f9f
7 changed files with 97 additions and 34 deletions

View File

@@ -372,6 +372,7 @@ if (process.platform === 'darwin') {
let mainWindowReady = false
let shouldShowMain = true
let isAppQuitting = false
let shutdownPromise: Promise<void> | null = null
let tray: Tray | null = null
let isClosePromptVisible = false
const chatHistoryPayloadStore = new Map<string, { sessionId: string; title?: string; recordList: any[] }>()
@@ -3869,23 +3870,35 @@ app.whenReady().then(async () => {
})
})
app.on('before-quit', async () => {
isAppQuitting = true
// 销毁 tray 图标
if (tray) { try { tray.destroy() } catch {} tray = null }
// 通知窗使用 hide 而非 close退出时主动销毁避免残留窗口阻塞进程退出。
destroyNotificationWindow()
insightService.stop()
// 兜底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 { await wcdbService.shutdown() } catch {}
const shutdownAppServices = async (): Promise<void> => {
if (shutdownPromise) return shutdownPromise
shutdownPromise = (async () => {
isAppQuitting = true
// 销毁 tray 图标
if (tray) { try { tray.destroy() } catch {} tray = null }
// 通知窗使用 hide 而非 close退出时主动销毁避免残留窗口阻塞进程退出。
destroyNotificationWindow()
messagePushService.stop()
insightService.stop()
// 兜底5秒后强制退出防止某个异步任务卡住导致进程残留
const forceExitTimer = setTimeout(() => {
console.warn('[App] Force exit after timeout')
app.exit(0)
}, 5000)
forceExitTimer.unref()
try { await cloudControlService.stop() } catch {}
// 停止 chatService内部会关闭 cursor 与 DB避免退出阶段仍触发监控回调
try { chatService.close() } catch {}
// 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出
try { await httpService.stop() } catch {}
// 终止 wcdb Worker 线程,避免线程阻止进程退出
try { await wcdbService.shutdown() } catch {}
})()
return shutdownPromise
}
app.on('before-quit', () => {
void shutdownAppServices()
})
app.on('window-all-closed', () => {

View File

@@ -218,7 +218,7 @@ class CloudControlService {
this.pages.add(pageName)
}
stop() {
async stop(): Promise<void> {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
@@ -230,7 +230,13 @@ class CloudControlService {
this.circuitOpenedAt = 0
this.nextDelayOverrideMs = null
this.initialized = false
wcdbService.cloudStop()
if (wcdbService.isReady()) {
try {
await wcdbService.cloudStop()
} catch {
// 忽略停止失败,避免阻塞主进程退出
}
}
}
async getLogs() {

View File

@@ -2,6 +2,7 @@
import { app, safeStorage } from 'electron'
import crypto from 'crypto'
import Store from 'electron-store'
import { expandHomePath } from '../utils/pathUtils'
// 加密前缀标记
const SAFE_PREFIX = 'safe:' // safeStorage 加密(普通模式)
@@ -295,6 +296,10 @@ export class ConfigService {
return this.decryptWxidConfigs(raw as any) as ConfigSchema[K]
}
if (key === 'dbPath' && typeof raw === 'string') {
return expandHomePath(raw) as ConfigSchema[K]
}
return raw
}
@@ -302,6 +307,10 @@ export class ConfigService {
let toStore = value
const inLockMode = this.isLockMode() && this.unlockPassword
if (key === 'dbPath' && typeof value === 'string') {
toStore = expandHomePath(value) as ConfigSchema[K]
}
if (ENCRYPTED_BOOL_KEYS.has(key)) {
const boolValue = value === true || value === 'true'
// `false` 不需要写入 keychain避免无意义触发 macOS 钥匙串弹窗

View File

@@ -2,6 +2,7 @@ import { join, basename } from 'path'
import { existsSync, readdirSync, statSync, readFileSync } from 'fs'
import { homedir } from 'os'
import { createDecipheriv } from 'crypto'
import { expandHomePath } from '../utils/pathUtils'
export interface WxidInfo {
wxid: string
@@ -139,13 +140,14 @@ export class DbPathService {
* 查找账号目录(包含 db_storage 或图片目录)
*/
findAccountDirs(rootPath: string): string[] {
const resolvedRootPath = expandHomePath(rootPath)
const accounts: string[] = []
try {
const entries = readdirSync(rootPath)
const entries = readdirSync(resolvedRootPath)
for (const entry of entries) {
const entryPath = join(rootPath, entry)
const entryPath = join(resolvedRootPath, entry)
let stat: ReturnType<typeof statSync>
try {
stat = statSync(entryPath)
@@ -216,13 +218,14 @@ export class DbPathService {
* 扫描目录名候选(仅包含下划线的文件夹,排除 all_users
*/
scanWxidCandidates(rootPath: string): WxidInfo[] {
const resolvedRootPath = expandHomePath(rootPath)
const wxids: WxidInfo[] = []
try {
if (existsSync(rootPath)) {
const entries = readdirSync(rootPath)
if (existsSync(resolvedRootPath)) {
const entries = readdirSync(resolvedRootPath)
for (const entry of entries) {
const entryPath = join(rootPath, entry)
const entryPath = join(resolvedRootPath, entry)
let stat: ReturnType<typeof statSync>
try { stat = statSync(entryPath) } catch { continue }
if (!stat.isDirectory()) continue
@@ -235,9 +238,9 @@ export class DbPathService {
if (wxids.length === 0) {
const rootName = basename(rootPath)
const rootName = basename(resolvedRootPath)
if (rootName.includes('_') && rootName.toLowerCase() !== 'all_users') {
const rootStat = statSync(rootPath)
const rootStat = statSync(resolvedRootPath)
wxids.push({ wxid: rootName, modifiedTime: rootStat.mtimeMs })
}
}
@@ -248,7 +251,7 @@ export class DbPathService {
return a.wxid.localeCompare(b.wxid)
});
const globalInfo = this.parseGlobalConfig(rootPath);
const globalInfo = this.parseGlobalConfig(resolvedRootPath);
if (globalInfo) {
for (const w of sorted) {
if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) {
@@ -266,19 +269,20 @@ export class DbPathService {
* 扫描 wxid 列表
*/
scanWxids(rootPath: string): WxidInfo[] {
const resolvedRootPath = expandHomePath(rootPath)
const wxids: WxidInfo[] = []
try {
if (this.isAccountDir(rootPath)) {
const wxid = basename(rootPath)
const modifiedTime = this.getAccountModifiedTime(rootPath)
if (this.isAccountDir(resolvedRootPath)) {
const wxid = basename(resolvedRootPath)
const modifiedTime = this.getAccountModifiedTime(resolvedRootPath)
return [{ wxid, modifiedTime }]
}
const accounts = this.findAccountDirs(rootPath)
const accounts = this.findAccountDirs(resolvedRootPath)
for (const account of accounts) {
const fullPath = join(rootPath, account)
const fullPath = join(resolvedRootPath, account)
const modifiedTime = this.getAccountModifiedTime(fullPath)
wxids.push({ wxid: account, modifiedTime })
}
@@ -289,7 +293,7 @@ export class DbPathService {
return a.wxid.localeCompare(b.wxid)
});
const globalInfo = this.parseGlobalConfig(rootPath);
const globalInfo = this.parseGlobalConfig(resolvedRootPath);
if (globalInfo) {
for (const w of sorted) {
if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) {

View File

@@ -53,6 +53,13 @@ class MessagePushService {
void this.refreshConfiguration('startup')
}
stop(): void {
this.started = false
this.processing = false
this.rerunRequested = false
this.resetRuntimeState()
}
handleDbMonitorChange(type: string, json: string): void {
if (!this.started) return
if (!this.isPushEnabled()) return

View File

@@ -2,6 +2,7 @@ import { join, dirname, basename } from 'path'
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs'
import { tmpdir } from 'os'
import * as fzstd from 'fzstd'
import { expandHomePath } from '../utils/pathUtils'
//数据服务初始化错误信息,用于帮助用户诊断问题
let lastDllInitError: string | null = null
@@ -481,7 +482,7 @@ export class WcdbCore {
private resolveDbStoragePath(basePath: string, wxid: string): string | null {
if (!basePath) return null
const normalized = basePath.replace(/[\\\\/]+$/, '')
const normalized = expandHomePath(basePath).replace(/[\\\\/]+$/, '')
if (normalized.toLowerCase().endsWith('db_storage') && existsSync(normalized)) {
return normalized
}
@@ -1600,6 +1601,9 @@ export class WcdbCore {
*/
close(): void {
if (this.handle !== null || this.initialized) {
// 先停止监控与云控回调,避免 shutdown 后仍有 native 回调访问已释放资源。
try { this.stopMonitor() } catch {}
try { this.cloudStop() } catch {}
try {
// 不调用 closeAccount直接 shutdown
this.wcdbShutdown()

View File

@@ -0,0 +1,20 @@
import { homedir } from 'os'
/**
* Expand "~" prefix to current user's home directory.
* Examples:
* - "~" => "/Users/alex"
* - "~/Library/..." => "/Users/alex/Library/..."
*/
export function expandHomePath(inputPath: string): string {
const raw = String(inputPath || '').trim()
if (!raw) return raw
if (raw === '~') return homedir()
if (/^~[\\/]/.test(raw)) {
return `${homedir()}${raw.slice(1)}`
}
return raw
}