diff --git a/electron/main.ts b/electron/main.ts index b57b76b..5f54f94 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -34,6 +34,7 @@ import { insightService } from './services/insightService' import { normalizeWeiboCookieInput, weiboService } from './services/social/weiboService' import { bizService } from './services/bizService' import { backupService } from './services/backupService' +import { imageDownloadService } from './services/imageDownloadService' // 配置自动更新 autoUpdater.autoDownload = false @@ -3954,6 +3955,20 @@ function registerIpcHandlers() { } }) + // 自动下载原图 + ipcMain.handle('image:startAutoDownload', async () => { + await imageDownloadService.startAutoDownload() + return { success: true } + }) + + ipcMain.handle('image:stopAutoDownload', async () => { + await imageDownloadService.stopAutoDownload() + return { success: true } + }) + + ipcMain.handle('image:getAutoDownloadStatus', async () => { + return await imageDownloadService.getStatus() + }) } // 主窗口引用 @@ -4081,6 +4096,9 @@ app.whenReady().then(async () => { // 注册 IPC 处理器 updateSplashProgress(28, '正在初始化...') registerIpcHandlers() + if (configService.get('autoDownloadHighRes')) { + imageDownloadService.startAutoDownload() + } chatService.addDbMonitorListener((type, json) => { messagePushService.handleDbMonitorChange(type, json) insightService.handleDbMonitorChange(type, json) @@ -4252,6 +4270,8 @@ const shutdownAppServices = async (): Promise => { }, 5000) forceExitTimer.unref() try { await cloudControlService.stop() } catch {} + // 停止自动下载服务 + try { await imageDownloadService.stopAutoDownload() } catch {} // 停止 chatService(内部会关闭 cursor 与 DB),避免退出阶段仍触发监控回调 try { chatService.close() } catch {} // 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出 diff --git a/electron/preload.ts b/electron/preload.ts index c7ba7c2..84ed45f 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -365,7 +365,10 @@ contextBridge.exposeInMainWorld('electronAPI', { }) => callback(payload) ipcRenderer.on('image:decryptProgress', listener) return () => ipcRenderer.removeListener('image:decryptProgress', listener) - } + }, + startAutoDownload: () => ipcRenderer.invoke('image:startAutoDownload'), + stopAutoDownload: () => ipcRenderer.invoke('image:stopAutoDownload'), + getAutoDownloadStatus: () => ipcRenderer.invoke('image:getAutoDownloadStatus') }, // 视频 @@ -374,6 +377,11 @@ contextBridge.exposeInMainWorld('electronAPI', { parseVideoMd5: (content: string) => ipcRenderer.invoke('video:parseVideoMd5', content) }, + process: { + platform: process.platform, + arch: process.arch + }, + // 数据分析 analytics: { getOverallStatistics: (force?: boolean) => ipcRenderer.invoke('analytics:getOverallStatistics', force), diff --git a/electron/services/config.ts b/electron/services/config.ts index 9e38931..27c216c 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -117,6 +117,7 @@ interface ConfigSchema { aiFootprintSystemPrompt: string /** 是否将 AI 见解调试日志输出到桌面 */ aiInsightDebugLogEnabled: boolean + autoDownloadHighRes: boolean } interface ConfigStoreLike> { @@ -294,7 +295,8 @@ export class ConfigService { aiInsightWeiboBindings: {}, aiFootprintEnabled: false, aiFootprintSystemPrompt: '', - aiInsightDebugLogEnabled: false + aiInsightDebugLogEnabled: false, + autoDownloadHighRes: false } const cwd = String(process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || '').trim() diff --git a/electron/services/imageDownloadService.ts b/electron/services/imageDownloadService.ts index e69de29..3d978f6 100644 --- a/electron/services/imageDownloadService.ts +++ b/electron/services/imageDownloadService.ts @@ -0,0 +1,174 @@ +import { app } from 'electron' +import { join } from 'path' +import { existsSync } from 'fs' +import { execFile } from 'child_process' +import { promisify } from 'util' +// import { ConfigService } from './config' + +const execFileAsync = promisify(execFile) + +export class ImageDownloadService { + private static instance: ImageDownloadService + private koffi: any = null + private lib: any = null + private initialized = false + + private initImgHelper: any = null + private uninstallImgHelper: any = null + private getImgHelperError: any = null + + private currentPid: number | null = null + private pollTimer: NodeJS.Timeout | null = null + private isHooked = false + + static getInstance(): ImageDownloadService { + if (!ImageDownloadService.instance) { + ImageDownloadService.instance = new ImageDownloadService() + } + return ImageDownloadService.instance + } + + private constructor() { + } + + private async ensureInitialized(): Promise { + if (this.initialized) return true + if (process.platform !== 'win32' || process.arch !== 'x64') return false + + try { + this.koffi = require('koffi') + const dllPath = this.getDllPath() + if (!existsSync(dllPath)) { + console.error(`[ImageDownloadService] dll not found: ${dllPath}`) + return false + } + + this.lib = this.koffi.load(dllPath) + this.initImgHelper = this.lib.func('bool InitImgHelper(uint32)') + this.uninstallImgHelper = this.lib.func('void UninstallImgHelper()') + this.getImgHelperError = this.lib.func('const char* GetImgHelperError()') + + this.initialized = true + return true + } catch (error) { + console.error('[ImageDownloadService] failed to initialize:', error) + return false + } + } + + private getDllPath(): string { + const isPackaged = app.isPackaged + const candidates: string[] = [] + + if (isPackaged) { + candidates.push(join(process.resourcesPath, 'resources', 'image', 'win32', 'x64', 'img_helper.dll')) + } else { + candidates.push(join(process.cwd(), 'resources', 'image', 'win32', 'x64', 'img_helper.dll')) + } + + for (const path of candidates) { + if (existsSync(path)) return path + } + return candidates[0] + } + + private async findMainWeChatPid(): Promise { + try { + const script = ` + Get-CimInstance Win32_Process -Filter "Name = 'Weixin.exe'" | + Select-Object ProcessId, CommandLine | + ConvertTo-Json -Compress + `; + + const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', script]) + if (!stdout || !stdout.trim()) return null + + let processes = JSON.parse(stdout.trim()) + if (!Array.isArray(processes)) processes = [processes] + + const target = processes + .filter((p: any) => p.CommandLine && p.CommandLine.toLowerCase().includes('weixin.exe')) + .sort((a: any, b: any) => a.CommandLine.length - b.CommandLine.length)[0] + + return target ? target.ProcessId : null; + } catch (e) { + return null + } + } + + async startAutoDownload() { + if (!await this.ensureInitialized()) return + + if (this.pollTimer) return + + this.pollTimer = setInterval(() => this.checkAndHook(), 30000) + // Initial check + await this.checkAndHook() + } + + async stopAutoDownload() { + if (this.pollTimer) { + clearInterval(this.pollTimer) + this.pollTimer = null + } + await this.unhook() + } + + private async checkAndHook() { + const pid = await this.findMainWeChatPid() + + if (!pid) { + if (this.isHooked) { + console.log('[ImageDownloadService] WeChat exited, unhooking') + await this.unhook() + } + return + } + + if (this.isHooked && this.currentPid === pid) { + return + } + + if (this.isHooked && this.currentPid !== pid) { + console.log('[ImageDownloadService] WeChat PID changed, re-hooking') + await this.unhook() + } + + console.log(`[ImageDownloadService] attempting to hook PID: ${pid}`) + try { + const success = this.initImgHelper(pid) + if (success) { + this.isHooked = true + this.currentPid = pid + console.log('[ImageDownloadService] hook successful') + } else { + const err = this.getImgHelperError() + console.error(`[ImageDownloadService] hook failed: ${err}`) + } + } catch (e) { + console.error('[ImageDownloadService] InitImgHelper call crashed:', e) + } + } + + private async unhook() { + if (this.isHooked && this.uninstallImgHelper) { + try { + this.uninstallImgHelper() + } catch (e) { + console.error('[ImageDownloadService] uninstall failed:', e) + } + } + this.isHooked = false + this.currentPid = null + } + + async getStatus() { + return { + isHooked: this.isHooked, + pid: this.currentPid, + supported: process.platform === 'win32' && process.arch === 'x64' + } + } +} + +export const imageDownloadService = ImageDownloadService.getInstance() diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 0d508ed..a5bf75b 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -32,6 +32,7 @@ type SettingsTab = | 'aiCommon' | 'insight' | 'aiFootprint' + | 'autoDownload' const tabs: { id: Exclude; label: string; icon: React.ElementType }[] = [ { id: 'appearance', label: '外观', icon: Palette }, @@ -39,6 +40,7 @@ const tabs: { id: Exclude; label: string { id: 'antiRevoke', label: '防撤回', icon: RotateCcw }, { id: 'database', label: '数据库连接', icon: Database }, { id: 'models', label: '模型管理', icon: Mic }, + { id: 'autoDownload', label: '自动下载', icon: Download }, { id: 'cache', label: '缓存', icon: HardDrive }, { id: 'api', label: 'API 服务', icon: Globe }, { id: 'analytics', label: '分析', icon: BarChart2 }, @@ -47,6 +49,13 @@ const tabs: { id: Exclude; label: string { id: 'about', label: '关于', icon: Info } ] +const filteredTabs = tabs.filter(tab => { + if (tab.id === 'autoDownload') { + return (window as any).electronAPI.process.platform === 'win32' && (window as any).electronAPI.process.arch === 'x64' + } + return true +}) + const aiTabs: Array<{ id: Extract; label: string }> = [ { id: 'aiCommon', label: '基础配置' }, { id: 'insight', label: 'AI 见解' }, @@ -149,6 +158,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [imageKeyPercent, setImageKeyPercent] = useState(null) const [logEnabled, setLogEnabled] = useState(false) + const [autoDownloadHighRes, setAutoDownloadHighRes] = useState(false) const [whisperModelName, setWhisperModelName] = useState('base') const [whisperModelDir, setWhisperModelDir] = useState('') const [isWhisperDownloading, setIsWhisperDownloading] = useState(false) @@ -529,8 +539,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setWordCloudExcludeWords(savedExcludeWords) setExcludeWordsInput(savedExcludeWords.join('\n')) + const savedAutoDownloadHighRes = await configService.getAutoDownloadHighRes() const savedAnalyticsConsent = await configService.getAnalyticsConsent() setAnalyticsConsent(savedAnalyticsConsent ?? false) + setAutoDownloadHighRes(savedAutoDownloadHighRes) @@ -4658,6 +4670,44 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { ) + const renderAutoDownloadTab = () => ( +
+
+ + + 开启后,WeFlow 会通过远程 Hook 技术强制微信在接收图片时下载高清原图(而非默认的缩略图)。 +
+ 风险提示:Hook 涉及修改微信进程内存,虽不注入 DLL 但仍有被检测风险,请谨慎开启。 +
+
+ {autoDownloadHighRes ? '已开启' : '已关闭'} + +
+
+
+ ) + + const handleToggleAutoDownload = async () => { + const newVal = !autoDownloadHighRes + setAutoDownloadHighRes(newVal) + await configService.setAutoDownloadHighRes(newVal) + if (newVal) { + await (window as any).electronAPI.image.startAutoDownload() + } else { + await (window as any).electronAPI.image.stopAutoDownload() + } + showMessage(newVal ? '自动下载已开启' : '自动下载已关闭', true) + } + const renderUpdatesTab = () => { const downloadPercent = Math.max(0, Math.min(100, Number(downloadProgress?.percent || 0))) const channelCards: { id: configService.UpdateChannel; title: string; desc: string }[] = [ @@ -4792,7 +4842,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
- {tabs.flatMap((tab) => { + {filteredTabs.flatMap((tab) => { const row: React.ReactNode[] = [