diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 628ecf0..7965fd0 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -6,7 +6,6 @@ import * as https from 'https' import * as http from 'http' import * as fzstd from 'fzstd' import * as crypto from 'crypto' -import { app, BrowserWindow, dialog } from 'electron' import { ConfigService } from './config' import { wcdbService } from './wcdbService' import { MessageCacheService } from './messageCacheService' @@ -18,6 +17,7 @@ import { voiceTranscribeService } from './voiceTranscribeService' import { ImageDecryptService } from './imageDecryptService' import { CONTACT_REGION_LOOKUP_DATA } from './contactRegionLookupData' import { LRUCache } from '../utils/LRUCache.js' +import { getAppPathFallback, getElectronBrowserWindow, getElectronDialog, getPathFallback, isElectronAppPackaged } from './electronRuntime' export interface ChatSession { username: string @@ -498,7 +498,7 @@ class ChatService { } private async maybeShowInitFailureDialog(errorMessage: string): Promise { - if (!app.isPackaged) return + if (!isElectronAppPackaged()) return if (this.initFailureDialogShown) return const code = this.extractErrorCode(errorMessage) @@ -519,6 +519,8 @@ class ChatService { ].join('\n') try { + const dialog = getElectronDialog() + if (!dialog?.showMessageBox) return await dialog.showMessageBox({ type: 'error', title: 'WeFlow 启动失败', @@ -600,7 +602,7 @@ class ChatService { console.error('[ChatService] 数据库监听回调失败:', error) } } - const windows = BrowserWindow.getAllWindows() + const windows = getElectronBrowserWindow()?.getAllWindows?.() || [] // 广播给所有渲染进程窗口 windows.forEach((win) => { if (!win.isDestroyed()) { @@ -7180,7 +7182,7 @@ class ChatService { return join(cachePath, 'Voices') } // 回退到默认目录 - const documentsPath = app.getPath('documents') + const documentsPath = getPathFallback('documents') return join(documentsPath, 'WeFlow', 'Voices') } @@ -7190,7 +7192,7 @@ class ChatService { return join(cachePath, 'Emojis') } // 回退到默认目录 - const documentsPath = app.getPath('documents') + const documentsPath = getPathFallback('documents') return join(documentsPath, 'WeFlow', 'Emojis') } @@ -8435,13 +8437,13 @@ class ChatService { private async decodeSilkToPcm(silkData: Buffer, sampleRate: number): Promise { try { let wasmPath: string - if (app.isPackaged) { + if (isElectronAppPackaged()) { wasmPath = join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', 'silk-wasm', 'lib', 'silk.wasm') if (!existsSync(wasmPath)) { wasmPath = join(process.resourcesPath, 'node_modules', 'silk-wasm', 'lib', 'silk.wasm') } } else { - wasmPath = join(app.getAppPath(), 'node_modules', 'silk-wasm', 'lib', 'silk.wasm') + wasmPath = join(getAppPathFallback(), 'node_modules', 'silk-wasm', 'lib', 'silk.wasm') } if (!existsSync(wasmPath)) { @@ -8629,7 +8631,7 @@ class ChatService { /** 获取持久化转写缓存文件路径 */ private getTranscriptCachePath(): string { const cachePath = this.configService.get('cachePath') - const base = cachePath || join(app.getPath('documents'), 'WeFlow') + const base = cachePath || join(getPathFallback('documents'), 'WeFlow') return join(base, 'Voices', 'transcripts.json') } diff --git a/electron/services/config.ts b/electron/services/config.ts index ff06ccd..16ece13 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -1,13 +1,14 @@ -import { join } from 'path' -import { app, safeStorage } from 'electron' +import { dirname, join } from 'path' import crypto from 'crypto' -import Store from 'electron-store' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' import { expandHomePath } from '../utils/pathUtils' +import { getElectronSafeStorage, getPathFallback, isWorkerRuntime } from './electronRuntime' // 加密前缀标记 const SAFE_PREFIX = 'safe:' // safeStorage 加密(普通模式) const isSafeStorageAvailable = (): boolean => { try { + const safeStorage = getElectronSafeStorage() return typeof safeStorage?.isEncryptionAvailable === 'function' && safeStorage.isEncryptionAvailable() } catch { return false @@ -112,6 +113,68 @@ interface ConfigSchema { aiInsightDebugLogEnabled: boolean } +interface ConfigStoreLike> { + get(key: K): T[K] + set(key: K, value: T[K]): void + clear(): void + store: T +} + +function cloneJson(value: T): T { + return JSON.parse(JSON.stringify(value)) +} + +class JsonConfigStore> implements ConfigStoreLike { + private readonly filePath: string + private readonly defaults: T + private data: T + + constructor(options: { name: string; defaults: T; cwd?: string }) { + const baseDir = options.cwd || getPathFallback('userData') + mkdirSync(baseDir, { recursive: true }) + this.filePath = join(baseDir, `${options.name}.json`) + this.defaults = cloneJson(options.defaults) + this.data = cloneJson(options.defaults) + this.load() + } + + get store(): T { + return this.data + } + + private load(): void { + try { + if (!existsSync(this.filePath)) return + const raw = readFileSync(this.filePath, 'utf8') + const parsed = JSON.parse(raw) + if (parsed && typeof parsed === 'object') { + this.data = { ...cloneJson(this.defaults), ...parsed } + } + } catch { + this.data = cloneJson(this.defaults) + } + } + + private persist(): void { + mkdirSync(dirname(this.filePath), { recursive: true }) + writeFileSync(this.filePath, JSON.stringify(this.data), 'utf8') + } + + get(key: K): T[K] { + return this.data[key] + } + + set(key: K, value: T[K]): void { + this.data[key] = value + this.persist() + } + + clear(): void { + this.data = cloneJson(this.defaults) + this.persist() + } +} + // 需要 safeStorage 加密的字段(普通模式) const ENCRYPTED_STRING_KEYS: Set = new Set([ 'decryptKey', @@ -131,7 +194,7 @@ const LOCKABLE_NUMBER_KEYS: Set = new Set(['imageXorKey']) export class ConfigService { private static instance: ConfigService - private store!: Store + private store!: ConfigStoreLike // 锁定模式运行时状态 private unlockedKeys: Map = new Map() @@ -225,36 +288,17 @@ export class ConfigService { aiInsightDebugLogEnabled: false } - const storeOptions: any = { + const cwd = String(process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || '').trim() + this.store = new JsonConfigStore({ name: 'WeFlow-config', defaults, - projectName: String(process.env.WEFLOW_PROJECT_NAME || 'WeFlow').trim() || 'WeFlow' - } - const runningInWorker = process.env.WEFLOW_WORKER === '1' - if (runningInWorker) { - const cwd = String(process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || '').trim() - if (cwd) { - storeOptions.cwd = cwd - } - } + cwd: cwd || undefined + }) - try { - this.store = new Store(storeOptions) - } catch (error) { - const message = String((error as Error)?.message || error || '') - if (message.includes('projectName')) { - const fallbackOptions = { - ...storeOptions, - projectName: 'WeFlow', - cwd: storeOptions.cwd || process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || process.cwd() - } - this.store = new Store(fallbackOptions) - } else { - throw error - } + if (!isWorkerRuntime()) { + this.migrateAuthFields() + this.migrateAiConfig() } - this.migrateAuthFields() - this.migrateAiConfig() } // === 状态查询 === @@ -356,6 +400,8 @@ export class ConfigService { if (!plaintext) return '' if (plaintext.startsWith(SAFE_PREFIX)) return plaintext if (!isSafeStorageAvailable()) return plaintext + const safeStorage = getElectronSafeStorage() + if (!safeStorage) return plaintext const encrypted = safeStorage.encryptString(plaintext) return SAFE_PREFIX + encrypted.toString('base64') } @@ -364,6 +410,8 @@ export class ConfigService { if (!stored) return '' if (!stored.startsWith(SAFE_PREFIX)) return stored if (!isSafeStorageAvailable()) return '' + const safeStorage = getElectronSafeStorage() + if (!safeStorage) return '' try { const buf = Buffer.from(stored.slice(SAFE_PREFIX.length), 'base64') return safeStorage.decryptString(buf) @@ -831,7 +879,7 @@ export class ConfigService { if (workerUserDataPath) { return workerUserDataPath } - return app?.getPath?.('userData') || process.cwd() + return getPathFallback('userData') } getCacheBasePath(): string { diff --git a/electron/services/contactCacheService.ts b/electron/services/contactCacheService.ts index a481227..5cf1ecf 100644 --- a/electron/services/contactCacheService.ts +++ b/electron/services/contactCacheService.ts @@ -1,6 +1,5 @@ import { join, dirname } from 'path' import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs' -import { app } from 'electron' import { ConfigService } from './config' export interface ContactCacheEntry { diff --git a/electron/services/electronRuntime.ts b/electron/services/electronRuntime.ts new file mode 100644 index 0000000..ea6aeea --- /dev/null +++ b/electron/services/electronRuntime.ts @@ -0,0 +1,96 @@ +import { homedir, tmpdir } from 'os' +import { join } from 'path' + +type RuntimeRequire = (id: string) => any + +let cachedElectron: any | null | false = null + +export function isWorkerRuntime(): boolean { + return process.env.WEFLOW_WORKER === '1' +} + +export function getElectronModule(): any | null { + if (isWorkerRuntime()) return null + if (cachedElectron !== null) return cachedElectron || null + try { + const runtimeRequire = (0, eval)('require') as RuntimeRequire + cachedElectron = runtimeRequire('electron') + } catch { + cachedElectron = false + } + return cachedElectron || null +} + +export function getElectronApp(): any | null { + return getElectronModule()?.app || null +} + +export function getElectronBrowserWindow(): any | null { + return getElectronModule()?.BrowserWindow || null +} + +export function getElectronDialog(): any | null { + return getElectronModule()?.dialog || null +} + +export function getElectronSafeStorage(): any | null { + return getElectronModule()?.safeStorage || null +} + +export function getElectronPath(name: string): string | null { + try { + const getter = getElectronApp()?.getPath + if (typeof getter === 'function') { + return getter(name) + } + } catch { + // fall through to caller fallback + } + return null +} + +export function getAppPathFallback(): string { + try { + const getter = getElectronApp()?.getAppPath + if (typeof getter === 'function') { + return getter() + } + } catch { + // fall through + } + return process.cwd() +} + +export function getPathFallback(name: string): string { + const fromElectron = getElectronPath(name) + if (fromElectron) return fromElectron + + const home = homedir() + switch (name) { + case 'userData': { + const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim() + if (workerUserDataPath) return workerUserDataPath + if (process.platform === 'win32' && process.env.APPDATA) return join(process.env.APPDATA, 'WeFlow') + if (process.platform === 'darwin') return join(home, 'Library', 'Application Support', 'WeFlow') + return join(process.env.XDG_CONFIG_HOME || join(home, '.config'), 'WeFlow') + } + case 'documents': + return join(home, 'Documents') + case 'desktop': + return join(home, 'Desktop') + case 'downloads': + return join(home, 'Downloads') + case 'temp': + return tmpdir() + case 'appData': + return process.platform === 'win32' && process.env.APPDATA ? process.env.APPDATA : join(home, '.config') + default: + return process.cwd() + } +} + +export function isElectronAppPackaged(): boolean { + const app = getElectronApp() + if (typeof app?.isPackaged === 'boolean') return app.isPackaged + return Boolean((process as any).resourcesPath && process.env.NODE_ENV !== 'development') +} diff --git a/electron/services/exportRecordService.ts b/electron/services/exportRecordService.ts index 5ff1049..86e23f2 100644 --- a/electron/services/exportRecordService.ts +++ b/electron/services/exportRecordService.ts @@ -1,6 +1,6 @@ -import { app } from 'electron' import fs from 'fs' import path from 'path' +import { getPathFallback } from './electronRuntime' export interface ExportRecord { exportTime: number @@ -20,7 +20,7 @@ class ExportRecordService { private resolveFilePath(): string { if (this.filePath) return this.filePath const workerUserDataPath = String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim() - const userDataPath = workerUserDataPath || app?.getPath?.('userData') || process.cwd() + const userDataPath = workerUserDataPath || getPathFallback('userData') fs.mkdirSync(userDataPath, { recursive: true }) this.filePath = path.join(userDataPath, 'weflow-export-records.json') return this.filePath diff --git a/electron/services/imageDecryptService.ts b/electron/services/imageDecryptService.ts index 66552b4..badca5f 100644 --- a/electron/services/imageDecryptService.ts +++ b/electron/services/imageDecryptService.ts @@ -1,4 +1,3 @@ -import { app, BrowserWindow } from 'electron' import { basename, dirname, extname, join } from 'path' import { pathToFileURL } from 'url' import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, appendFileSync } from 'fs' @@ -8,6 +7,7 @@ import crypto from 'crypto' import { ConfigService } from './config' import { wcdbService } from './wcdbService' import { decryptDatViaNative, nativeAddonLocation } from './nativeImageDecrypt' +import { getElectronBrowserWindow, getPathFallback, isElectronAppPackaged } from './electronRuntime' // 获取 ffmpeg-static 的路径 function getStaticFfmpegPath(): string | null { @@ -35,7 +35,7 @@ function getStaticFfmpegPath(): string | null { } // 方法3: 打包后的路径 - if (app?.isPackaged) { + if (isElectronAppPackaged()) { const resourcesPath = process.resourcesPath const packedPath = join(resourcesPath, 'app.asar.unpacked', 'node_modules', 'ffmpeg-static', 'ffmpeg.exe') if (existsSync(packedPath)) { @@ -1475,7 +1475,7 @@ export class ImageDecryptService { private getActiveWindowsSafely(): Array<{ isDestroyed: () => boolean; webContents: { send: (channel: string, payload: unknown) => void } }> { try { - const getter = (BrowserWindow as unknown as { getAllWindows?: () => any[] } | undefined)?.getAllWindows + const getter = (getElectronBrowserWindow() as { getAllWindows?: () => any[] } | undefined)?.getAllWindows if (typeof getter !== 'function') return [] const windows = getter() if (!Array.isArray(windows)) return [] @@ -2191,14 +2191,7 @@ export class ImageDecryptService { } private getElectronPath(name: 'userData' | 'documents' | 'temp'): string | null { - try { - const getter = (app as unknown as { getPath?: (n: string) => string } | undefined)?.getPath - if (typeof getter !== 'function') return null - const value = getter(name) - return typeof value === 'string' && value.trim() ? value : null - } catch { - return null - } + return getPathFallback(name) } private getUserDataPath(): string { diff --git a/electron/services/messageCacheService.ts b/electron/services/messageCacheService.ts index 9d3079a..a340cbe 100644 --- a/electron/services/messageCacheService.ts +++ b/electron/services/messageCacheService.ts @@ -1,6 +1,5 @@ import { join, dirname } from 'path' import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs' -import { app } from 'electron' import { ConfigService } from './config' export interface SessionMessageCacheEntry { diff --git a/electron/services/videoService.ts b/electron/services/videoService.ts index 4785621..87f7058 100644 --- a/electron/services/videoService.ts +++ b/electron/services/videoService.ts @@ -1,9 +1,9 @@ import { join } from 'path' import { existsSync, readdirSync, statSync, readFileSync, appendFileSync, mkdirSync } from 'fs' import { pathToFileURL } from 'url' -import { app } from 'electron' import { ConfigService } from './config' import { wcdbService } from './wcdbService' +import { getPathFallback } from './electronRuntime' export interface VideoInfo { videoUrl?: string // 视频文件路径(用于 readFile) @@ -45,7 +45,7 @@ class VideoService { try { const timestamp = new Date().toISOString() const metaStr = meta ? ` ${JSON.stringify(meta)}` : '' - const logDir = join(app.getPath('userData'), 'logs') + const logDir = join(getPathFallback('userData'), 'logs') if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true }) appendFileSync(join(logDir, 'wcdb.log'), `[${timestamp}] [VideoService] ${message}${metaStr}\n`, 'utf8') } catch { } diff --git a/electron/services/voiceTranscribeService.ts b/electron/services/voiceTranscribeService.ts index 5cc7804..61fc8d6 100644 --- a/electron/services/voiceTranscribeService.ts +++ b/electron/services/voiceTranscribeService.ts @@ -1,9 +1,9 @@ -import { app } from 'electron' import { existsSync, mkdirSync, statSync, unlinkSync, createWriteStream, openSync, writeSync, closeSync } from 'fs' import { join } from 'path' import * as https from 'https' import * as http from 'http' import { ConfigService } from './config' +import { getPathFallback } from './electronRuntime' // Sherpa-onnx 类型定义 type OfflineRecognizer = any @@ -91,7 +91,7 @@ export class VoiceTranscribeService { private resolveModelDir(): string { const configured = this.configService.get('whisperModelDir') as string | undefined if (configured) return configured - return join(app.getPath('documents'), 'WeFlow', 'models', 'sensevoice') + return join(getPathFallback('documents'), 'WeFlow', 'models', 'sensevoice') } private resolveModelPath(fileName: string): string {