mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-22 15:09:04 +00:00
@@ -372,6 +372,7 @@ if (process.platform === 'darwin') {
|
|||||||
let mainWindowReady = false
|
let mainWindowReady = false
|
||||||
let shouldShowMain = true
|
let shouldShowMain = true
|
||||||
let isAppQuitting = false
|
let isAppQuitting = false
|
||||||
|
let shutdownPromise: Promise<void> | null = null
|
||||||
let tray: Tray | null = null
|
let tray: Tray | null = null
|
||||||
let isClosePromptVisible = false
|
let isClosePromptVisible = false
|
||||||
const chatHistoryPayloadStore = new Map<string, { sessionId: string; title?: string; recordList: any[] }>()
|
const chatHistoryPayloadStore = new Map<string, { sessionId: string; title?: string; recordList: any[] }>()
|
||||||
@@ -3869,23 +3870,35 @@ app.whenReady().then(async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('before-quit', async () => {
|
const shutdownAppServices = async (): Promise<void> => {
|
||||||
isAppQuitting = true
|
if (shutdownPromise) return shutdownPromise
|
||||||
// 销毁 tray 图标
|
shutdownPromise = (async () => {
|
||||||
if (tray) { try { tray.destroy() } catch {} tray = null }
|
isAppQuitting = true
|
||||||
// 通知窗使用 hide 而非 close,退出时主动销毁,避免残留窗口阻塞进程退出。
|
// 销毁 tray 图标
|
||||||
destroyNotificationWindow()
|
if (tray) { try { tray.destroy() } catch {} tray = null }
|
||||||
insightService.stop()
|
// 通知窗使用 hide 而非 close,退出时主动销毁,避免残留窗口阻塞进程退出。
|
||||||
// 兜底:5秒后强制退出,防止某个异步任务卡住导致进程残留
|
destroyNotificationWindow()
|
||||||
const forceExitTimer = setTimeout(() => {
|
messagePushService.stop()
|
||||||
console.warn('[App] Force exit after timeout')
|
insightService.stop()
|
||||||
app.exit(0)
|
// 兜底:5秒后强制退出,防止某个异步任务卡住导致进程残留
|
||||||
}, 5000)
|
const forceExitTimer = setTimeout(() => {
|
||||||
forceExitTimer.unref()
|
console.warn('[App] Force exit after timeout')
|
||||||
// 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出
|
app.exit(0)
|
||||||
try { await httpService.stop() } catch {}
|
}, 5000)
|
||||||
// 终止 wcdb Worker 线程,避免线程阻止进程退出
|
forceExitTimer.unref()
|
||||||
try { await wcdbService.shutdown() } catch {}
|
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', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ class CloudControlService {
|
|||||||
this.pages.add(pageName)
|
this.pages.add(pageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
async stop(): Promise<void> {
|
||||||
if (this.timer) {
|
if (this.timer) {
|
||||||
clearTimeout(this.timer)
|
clearTimeout(this.timer)
|
||||||
this.timer = null
|
this.timer = null
|
||||||
@@ -230,7 +230,13 @@ class CloudControlService {
|
|||||||
this.circuitOpenedAt = 0
|
this.circuitOpenedAt = 0
|
||||||
this.nextDelayOverrideMs = null
|
this.nextDelayOverrideMs = null
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
wcdbService.cloudStop()
|
if (wcdbService.isReady()) {
|
||||||
|
try {
|
||||||
|
await wcdbService.cloudStop()
|
||||||
|
} catch {
|
||||||
|
// 忽略停止失败,避免阻塞主进程退出
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLogs() {
|
async getLogs() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { app, safeStorage } from 'electron'
|
import { app, safeStorage } from 'electron'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import Store from 'electron-store'
|
import Store from 'electron-store'
|
||||||
|
import { expandHomePath } from '../utils/pathUtils'
|
||||||
|
|
||||||
// 加密前缀标记
|
// 加密前缀标记
|
||||||
const SAFE_PREFIX = 'safe:' // safeStorage 加密(普通模式)
|
const SAFE_PREFIX = 'safe:' // safeStorage 加密(普通模式)
|
||||||
@@ -295,6 +296,10 @@ export class ConfigService {
|
|||||||
return this.decryptWxidConfigs(raw as any) as ConfigSchema[K]
|
return this.decryptWxidConfigs(raw as any) as ConfigSchema[K]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === 'dbPath' && typeof raw === 'string') {
|
||||||
|
return expandHomePath(raw) as ConfigSchema[K]
|
||||||
|
}
|
||||||
|
|
||||||
return raw
|
return raw
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +307,10 @@ export class ConfigService {
|
|||||||
let toStore = value
|
let toStore = value
|
||||||
const inLockMode = this.isLockMode() && this.unlockPassword
|
const inLockMode = this.isLockMode() && this.unlockPassword
|
||||||
|
|
||||||
|
if (key === 'dbPath' && typeof value === 'string') {
|
||||||
|
toStore = expandHomePath(value) as ConfigSchema[K]
|
||||||
|
}
|
||||||
|
|
||||||
if (ENCRYPTED_BOOL_KEYS.has(key)) {
|
if (ENCRYPTED_BOOL_KEYS.has(key)) {
|
||||||
const boolValue = value === true || value === 'true'
|
const boolValue = value === true || value === 'true'
|
||||||
// `false` 不需要写入 keychain,避免无意义触发 macOS 钥匙串弹窗
|
// `false` 不需要写入 keychain,避免无意义触发 macOS 钥匙串弹窗
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { join, basename } from 'path'
|
|||||||
import { existsSync, readdirSync, statSync, readFileSync } from 'fs'
|
import { existsSync, readdirSync, statSync, readFileSync } from 'fs'
|
||||||
import { homedir } from 'os'
|
import { homedir } from 'os'
|
||||||
import { createDecipheriv } from 'crypto'
|
import { createDecipheriv } from 'crypto'
|
||||||
|
import { expandHomePath } from '../utils/pathUtils'
|
||||||
|
|
||||||
export interface WxidInfo {
|
export interface WxidInfo {
|
||||||
wxid: string
|
wxid: string
|
||||||
@@ -139,13 +140,14 @@ export class DbPathService {
|
|||||||
* 查找账号目录(包含 db_storage 或图片目录)
|
* 查找账号目录(包含 db_storage 或图片目录)
|
||||||
*/
|
*/
|
||||||
findAccountDirs(rootPath: string): string[] {
|
findAccountDirs(rootPath: string): string[] {
|
||||||
|
const resolvedRootPath = expandHomePath(rootPath)
|
||||||
const accounts: string[] = []
|
const accounts: string[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = readdirSync(rootPath)
|
const entries = readdirSync(resolvedRootPath)
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const entryPath = join(rootPath, entry)
|
const entryPath = join(resolvedRootPath, entry)
|
||||||
let stat: ReturnType<typeof statSync>
|
let stat: ReturnType<typeof statSync>
|
||||||
try {
|
try {
|
||||||
stat = statSync(entryPath)
|
stat = statSync(entryPath)
|
||||||
@@ -216,13 +218,14 @@ export class DbPathService {
|
|||||||
* 扫描目录名候选(仅包含下划线的文件夹,排除 all_users)
|
* 扫描目录名候选(仅包含下划线的文件夹,排除 all_users)
|
||||||
*/
|
*/
|
||||||
scanWxidCandidates(rootPath: string): WxidInfo[] {
|
scanWxidCandidates(rootPath: string): WxidInfo[] {
|
||||||
|
const resolvedRootPath = expandHomePath(rootPath)
|
||||||
const wxids: WxidInfo[] = []
|
const wxids: WxidInfo[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (existsSync(rootPath)) {
|
if (existsSync(resolvedRootPath)) {
|
||||||
const entries = readdirSync(rootPath)
|
const entries = readdirSync(resolvedRootPath)
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const entryPath = join(rootPath, entry)
|
const entryPath = join(resolvedRootPath, entry)
|
||||||
let stat: ReturnType<typeof statSync>
|
let stat: ReturnType<typeof statSync>
|
||||||
try { stat = statSync(entryPath) } catch { continue }
|
try { stat = statSync(entryPath) } catch { continue }
|
||||||
if (!stat.isDirectory()) continue
|
if (!stat.isDirectory()) continue
|
||||||
@@ -235,9 +238,9 @@ export class DbPathService {
|
|||||||
|
|
||||||
|
|
||||||
if (wxids.length === 0) {
|
if (wxids.length === 0) {
|
||||||
const rootName = basename(rootPath)
|
const rootName = basename(resolvedRootPath)
|
||||||
if (rootName.includes('_') && rootName.toLowerCase() !== 'all_users') {
|
if (rootName.includes('_') && rootName.toLowerCase() !== 'all_users') {
|
||||||
const rootStat = statSync(rootPath)
|
const rootStat = statSync(resolvedRootPath)
|
||||||
wxids.push({ wxid: rootName, modifiedTime: rootStat.mtimeMs })
|
wxids.push({ wxid: rootName, modifiedTime: rootStat.mtimeMs })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,7 +251,7 @@ export class DbPathService {
|
|||||||
return a.wxid.localeCompare(b.wxid)
|
return a.wxid.localeCompare(b.wxid)
|
||||||
});
|
});
|
||||||
|
|
||||||
const globalInfo = this.parseGlobalConfig(rootPath);
|
const globalInfo = this.parseGlobalConfig(resolvedRootPath);
|
||||||
if (globalInfo) {
|
if (globalInfo) {
|
||||||
for (const w of sorted) {
|
for (const w of sorted) {
|
||||||
if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) {
|
if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) {
|
||||||
@@ -266,19 +269,20 @@ export class DbPathService {
|
|||||||
* 扫描 wxid 列表
|
* 扫描 wxid 列表
|
||||||
*/
|
*/
|
||||||
scanWxids(rootPath: string): WxidInfo[] {
|
scanWxids(rootPath: string): WxidInfo[] {
|
||||||
|
const resolvedRootPath = expandHomePath(rootPath)
|
||||||
const wxids: WxidInfo[] = []
|
const wxids: WxidInfo[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.isAccountDir(rootPath)) {
|
if (this.isAccountDir(resolvedRootPath)) {
|
||||||
const wxid = basename(rootPath)
|
const wxid = basename(resolvedRootPath)
|
||||||
const modifiedTime = this.getAccountModifiedTime(rootPath)
|
const modifiedTime = this.getAccountModifiedTime(resolvedRootPath)
|
||||||
return [{ wxid, modifiedTime }]
|
return [{ wxid, modifiedTime }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = this.findAccountDirs(rootPath)
|
const accounts = this.findAccountDirs(resolvedRootPath)
|
||||||
|
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
const fullPath = join(rootPath, account)
|
const fullPath = join(resolvedRootPath, account)
|
||||||
const modifiedTime = this.getAccountModifiedTime(fullPath)
|
const modifiedTime = this.getAccountModifiedTime(fullPath)
|
||||||
wxids.push({ wxid: account, modifiedTime })
|
wxids.push({ wxid: account, modifiedTime })
|
||||||
}
|
}
|
||||||
@@ -289,7 +293,7 @@ export class DbPathService {
|
|||||||
return a.wxid.localeCompare(b.wxid)
|
return a.wxid.localeCompare(b.wxid)
|
||||||
});
|
});
|
||||||
|
|
||||||
const globalInfo = this.parseGlobalConfig(rootPath);
|
const globalInfo = this.parseGlobalConfig(resolvedRootPath);
|
||||||
if (globalInfo) {
|
if (globalInfo) {
|
||||||
for (const w of sorted) {
|
for (const w of sorted) {
|
||||||
if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) {
|
if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) {
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ class MessagePushService {
|
|||||||
void this.refreshConfiguration('startup')
|
void this.refreshConfiguration('startup')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
this.started = false
|
||||||
|
this.processing = false
|
||||||
|
this.rerunRequested = false
|
||||||
|
this.resetRuntimeState()
|
||||||
|
}
|
||||||
|
|
||||||
handleDbMonitorChange(type: string, json: string): void {
|
handleDbMonitorChange(type: string, json: string): void {
|
||||||
if (!this.started) return
|
if (!this.started) return
|
||||||
if (!this.isPushEnabled()) return
|
if (!this.isPushEnabled()) return
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { join, dirname, basename } from 'path'
|
|||||||
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs'
|
import { appendFileSync, existsSync, mkdirSync, readdirSync, statSync, readFileSync } from 'fs'
|
||||||
import { tmpdir } from 'os'
|
import { tmpdir } from 'os'
|
||||||
import * as fzstd from 'fzstd'
|
import * as fzstd from 'fzstd'
|
||||||
|
import { expandHomePath } from '../utils/pathUtils'
|
||||||
|
|
||||||
//数据服务初始化错误信息,用于帮助用户诊断问题
|
//数据服务初始化错误信息,用于帮助用户诊断问题
|
||||||
let lastDllInitError: string | null = null
|
let lastDllInitError: string | null = null
|
||||||
@@ -481,7 +482,7 @@ export class WcdbCore {
|
|||||||
|
|
||||||
private resolveDbStoragePath(basePath: string, wxid: string): string | null {
|
private resolveDbStoragePath(basePath: string, wxid: string): string | null {
|
||||||
if (!basePath) return null
|
if (!basePath) return null
|
||||||
const normalized = basePath.replace(/[\\\\/]+$/, '')
|
const normalized = expandHomePath(basePath).replace(/[\\\\/]+$/, '')
|
||||||
if (normalized.toLowerCase().endsWith('db_storage') && existsSync(normalized)) {
|
if (normalized.toLowerCase().endsWith('db_storage') && existsSync(normalized)) {
|
||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
@@ -1600,6 +1601,9 @@ export class WcdbCore {
|
|||||||
*/
|
*/
|
||||||
close(): void {
|
close(): void {
|
||||||
if (this.handle !== null || this.initialized) {
|
if (this.handle !== null || this.initialized) {
|
||||||
|
// 先停止监控与云控回调,避免 shutdown 后仍有 native 回调访问已释放资源。
|
||||||
|
try { this.stopMonitor() } catch {}
|
||||||
|
try { this.cloudStop() } catch {}
|
||||||
try {
|
try {
|
||||||
// 不调用 closeAccount,直接 shutdown
|
// 不调用 closeAccount,直接 shutdown
|
||||||
this.wcdbShutdown()
|
this.wcdbShutdown()
|
||||||
|
|||||||
20
electron/utils/pathUtils.ts
Normal file
20
electron/utils/pathUtils.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user