diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 7965fd0..628ecf0 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -6,6 +6,7 @@ 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' @@ -17,7 +18,6 @@ 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 (!isElectronAppPackaged()) return + if (!app.isPackaged) return if (this.initFailureDialogShown) return const code = this.extractErrorCode(errorMessage) @@ -519,8 +519,6 @@ class ChatService { ].join('\n') try { - const dialog = getElectronDialog() - if (!dialog?.showMessageBox) return await dialog.showMessageBox({ type: 'error', title: 'WeFlow 启动失败', @@ -602,7 +600,7 @@ class ChatService { console.error('[ChatService] 数据库监听回调失败:', error) } } - const windows = getElectronBrowserWindow()?.getAllWindows?.() || [] + const windows = BrowserWindow.getAllWindows() // 广播给所有渲染进程窗口 windows.forEach((win) => { if (!win.isDestroyed()) { @@ -7182,7 +7180,7 @@ class ChatService { return join(cachePath, 'Voices') } // 回退到默认目录 - const documentsPath = getPathFallback('documents') + const documentsPath = app.getPath('documents') return join(documentsPath, 'WeFlow', 'Voices') } @@ -7192,7 +7190,7 @@ class ChatService { return join(cachePath, 'Emojis') } // 回退到默认目录 - const documentsPath = getPathFallback('documents') + const documentsPath = app.getPath('documents') return join(documentsPath, 'WeFlow', 'Emojis') } @@ -8437,13 +8435,13 @@ class ChatService { private async decodeSilkToPcm(silkData: Buffer, sampleRate: number): Promise { try { let wasmPath: string - if (isElectronAppPackaged()) { + if (app.isPackaged) { 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(getAppPathFallback(), 'node_modules', 'silk-wasm', 'lib', 'silk.wasm') + wasmPath = join(app.getAppPath(), 'node_modules', 'silk-wasm', 'lib', 'silk.wasm') } if (!existsSync(wasmPath)) { @@ -8631,7 +8629,7 @@ class ChatService { /** 获取持久化转写缓存文件路径 */ private getTranscriptCachePath(): string { const cachePath = this.configService.get('cachePath') - const base = cachePath || join(getPathFallback('documents'), 'WeFlow') + const base = cachePath || join(app.getPath('documents'), 'WeFlow') return join(base, 'Voices', 'transcripts.json') } diff --git a/electron/services/config.ts b/electron/services/config.ts index 16ece13..ff06ccd 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -1,14 +1,13 @@ -import { dirname, join } from 'path' +import { join } from 'path' +import { app, safeStorage } from 'electron' import crypto from 'crypto' -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' +import Store from 'electron-store' 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 @@ -113,68 +112,6 @@ 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', @@ -194,7 +131,7 @@ const LOCKABLE_NUMBER_KEYS: Set = new Set(['imageXorKey']) export class ConfigService { private static instance: ConfigService - private store!: ConfigStoreLike + private store!: Store // 锁定模式运行时状态 private unlockedKeys: Map = new Map() @@ -288,17 +225,36 @@ export class ConfigService { aiInsightDebugLogEnabled: false } - const cwd = String(process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || '').trim() - this.store = new JsonConfigStore({ + const storeOptions: any = { name: 'WeFlow-config', defaults, - cwd: cwd || undefined - }) - - if (!isWorkerRuntime()) { - this.migrateAuthFields() - this.migrateAiConfig() + 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 + } + } + + 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 + } + } + this.migrateAuthFields() + this.migrateAiConfig() } // === 状态查询 === @@ -400,8 +356,6 @@ 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') } @@ -410,8 +364,6 @@ 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) @@ -879,7 +831,7 @@ export class ConfigService { if (workerUserDataPath) { return workerUserDataPath } - return getPathFallback('userData') + return app?.getPath?.('userData') || process.cwd() } getCacheBasePath(): string { diff --git a/electron/services/contactCacheService.ts b/electron/services/contactCacheService.ts index 5cf1ecf..a481227 100644 --- a/electron/services/contactCacheService.ts +++ b/electron/services/contactCacheService.ts @@ -1,5 +1,6 @@ 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 deleted file mode 100644 index ea6aeea..0000000 --- a/electron/services/electronRuntime.ts +++ /dev/null @@ -1,96 +0,0 @@ -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 86e23f2..5ff1049 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 || getPathFallback('userData') + const userDataPath = workerUserDataPath || app?.getPath?.('userData') || process.cwd() 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 badca5f..66552b4 100644 --- a/electron/services/imageDecryptService.ts +++ b/electron/services/imageDecryptService.ts @@ -1,3 +1,4 @@ +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' @@ -7,7 +8,6 @@ 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 (isElectronAppPackaged()) { + if (app?.isPackaged) { 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 = (getElectronBrowserWindow() as { getAllWindows?: () => any[] } | undefined)?.getAllWindows + const getter = (BrowserWindow as unknown as { getAllWindows?: () => any[] } | undefined)?.getAllWindows if (typeof getter !== 'function') return [] const windows = getter() if (!Array.isArray(windows)) return [] @@ -2191,7 +2191,14 @@ export class ImageDecryptService { } private getElectronPath(name: 'userData' | 'documents' | 'temp'): string | null { - return getPathFallback(name) + 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 + } } private getUserDataPath(): string { diff --git a/electron/services/messageCacheService.ts b/electron/services/messageCacheService.ts index a340cbe..9d3079a 100644 --- a/electron/services/messageCacheService.ts +++ b/electron/services/messageCacheService.ts @@ -1,5 +1,6 @@ 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 87f7058..4785621 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(getPathFallback('userData'), 'logs') + const logDir = join(app.getPath('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 61fc8d6..5cc7804 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(getPathFallback('documents'), 'WeFlow', 'models', 'sensevoice') + return join(app.getPath('documents'), 'WeFlow', 'models', 'sensevoice') } private resolveModelPath(fileName: string): string { diff --git a/vite.config.ts b/vite.config.ts index cd13106..ffdf1e0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,6 +8,77 @@ const handleElectronOnStart = (options: { reload: () => void }) => { options.reload() } +const exportWorkerElectronShimPlugin = () => { + const virtualId = 'virtual:weflow-export-worker-electron' + const resolvedVirtualId = `\0${virtualId}` + + return { + name: 'weflow-export-worker-electron-shim', + enforce: 'pre' as const, + resolveId(id: string) { + if (id === virtualId) return resolvedVirtualId + return null + }, + load(id: string) { + if (id !== resolvedVirtualId) return null + return ` + import { homedir, tmpdir } from 'os' + import { join } from 'path' + + const workerUserDataPath = () => String(process.env.WEFLOW_USER_DATA_PATH || process.env.WEFLOW_CONFIG_CWD || '').trim() + const appDataPath = () => { + if (process.platform === 'win32' && process.env.APPDATA) return process.env.APPDATA + if (process.platform === 'darwin') return join(homedir(), 'Library', 'Application Support') + return process.env.XDG_CONFIG_HOME || join(homedir(), '.config') + } + const getPath = (name) => { + if (name === 'userData') return workerUserDataPath() || join(appDataPath(), 'WeFlow') + if (name === 'documents') return join(homedir(), 'Documents') + if (name === 'desktop') return join(homedir(), 'Desktop') + if (name === 'downloads') return join(homedir(), 'Downloads') + if (name === 'temp') return tmpdir() + if (name === 'appData') return appDataPath() + return process.cwd() + } + + export const app = { + isPackaged: Boolean(process.resourcesPath && process.env.NODE_ENV !== 'development'), + getPath, + getAppPath: () => process.cwd(), + getName: () => 'WeFlow', + getVersion: () => process.env.npm_package_version || '0.0.0' + } + export const BrowserWindow = { getAllWindows: () => [] } + export const dialog = { showMessageBox: async () => ({ response: 0, checkboxChecked: false }) } + export const shell = { openExternal: async () => false, showItemInFolder: () => {} } + export const ipcMain = { on: () => {}, handle: () => {}, removeHandler: () => {} } + export const ipcRenderer = { sendSync: () => ({}) } + export const safeStorage = { + isEncryptionAvailable: () => false, + encryptString: (value) => Buffer.from(String(value || ''), 'utf8'), + decryptString: (value) => Buffer.isBuffer(value) ? value.toString('utf8') : Buffer.from(value).toString('utf8') + } + export const Notification = class { + static isSupported() { return false } + on() { return this } + show() {} + close() {} + } + export default { app, BrowserWindow, dialog, shell, ipcMain, ipcRenderer, safeStorage, Notification } + ` + }, + transform(code: string, id: string) { + if (!/\.[cm]?[jt]s$/.test(id)) return null + if (!code.includes("'electron'") && !code.includes('"electron"')) return null + const next = code + .replace(/from\s+(['"])electron\1/g, `from '${virtualId}'`) + .replace(/import\s*\(\s*(['"])electron\1\s*\)/g, `import('${virtualId}')`) + .replace(/require\s*\(\s*(['"])electron\1\s*\)/g, `require('${virtualId}')`) + return next === code ? null : { code: next, map: null } + } + } +} + export default defineConfig({ base: './', server: { @@ -142,6 +213,7 @@ export default defineConfig({ entry: 'electron/exportWorker.ts', onstart: handleElectronOnStart, vite: { + plugins: [exportWorkerElectronShimPlugin()], build: { outDir: 'dist-electron', rollupOptions: {