From b1807b21e7b927ace9cad0aca1572869c290c209 Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Wed, 29 Apr 2026 08:07:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=89=E6=8B=A9=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E7=9A=84=E5=89=8D=E7=AB=AF=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main.ts | 10 +- electron/preload.ts | 2 +- electron/services/config.ts | 4 +- electron/services/imageDownloadService.ts | 48 +++-- src/pages/SettingsPage.tsx | 228 +++++++++++++++------- src/services/config.ts | 13 +- 6 files changed, 209 insertions(+), 96 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 040899c..3c574df 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -3956,8 +3956,8 @@ function registerIpcHandlers() { }) // 自动下载原图 - ipcMain.handle('image:startAutoDownload', async () => { - return await imageDownloadService.startAutoDownload() + ipcMain.handle('image:startAutoDownload', async (_, whitelist?: string[]) => { + return await imageDownloadService.startAutoDownload(whitelist || []) }) ipcMain.handle('image:stopAutoDownload', async () => { @@ -4096,7 +4096,11 @@ app.whenReady().then(async () => { updateSplashProgress(28, '正在初始化...') registerIpcHandlers() if (configService.get('autoDownloadHighRes')) { - imageDownloadService.startAutoDownload() + const whitelistArr = configService.get('autoDownloadWhitelist') || [] + const whitelistStr = (Array.isArray(whitelistArr) && whitelistArr.length > 0) + ? (whitelistArr.join('\0') + '\0\0') + : '' + imageDownloadService.startAutoDownload(whitelistStr) } chatService.addDbMonitorListener((type, json) => { messagePushService.handleDbMonitorChange(type, json) diff --git a/electron/preload.ts b/electron/preload.ts index 84ed45f..562d968 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -366,7 +366,7 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('image:decryptProgress', listener) return () => ipcRenderer.removeListener('image:decryptProgress', listener) }, - startAutoDownload: () => ipcRenderer.invoke('image:startAutoDownload'), + startAutoDownload: (whitelist: string[] | string) => ipcRenderer.invoke('image:startAutoDownload', whitelist), stopAutoDownload: () => ipcRenderer.invoke('image:stopAutoDownload'), getAutoDownloadStatus: () => ipcRenderer.invoke('image:getAutoDownloadStatus') }, diff --git a/electron/services/config.ts b/electron/services/config.ts index 27c216c..1136b20 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -118,6 +118,7 @@ interface ConfigSchema { /** 是否将 AI 见解调试日志输出到桌面 */ aiInsightDebugLogEnabled: boolean autoDownloadHighRes: boolean + autoDownloadWhitelist: string[] } interface ConfigStoreLike> { @@ -296,7 +297,8 @@ export class ConfigService { aiFootprintEnabled: false, aiFootprintSystemPrompt: '', aiInsightDebugLogEnabled: false, - autoDownloadHighRes: false + autoDownloadHighRes: false, + autoDownloadWhitelist: [] } 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 199cb98..78eff18 100644 --- a/electron/services/imageDownloadService.ts +++ b/electron/services/imageDownloadService.ts @@ -12,7 +12,7 @@ export class ImageDownloadService { private koffi: any = null private lib: any = null private initialized = false - + private initImgHelper: any = null private uninstallImgHelper: any = null private getImgHelperError: any = null @@ -21,6 +21,8 @@ export class ImageDownloadService { private pollTimer: NodeJS.Timeout | null = null private isHooked = false + private lastWhitelist: string[] = [] + static getInstance(): ImageDownloadService { if (!ImageDownloadService.instance) { ImageDownloadService.instance = new ImageDownloadService() @@ -38,16 +40,14 @@ export class ImageDownloadService { try { this.koffi = require('koffi') const dllPath = this.getDllPath() - if (!existsSync(dllPath)) { - console.error(`[ImageDownloadService] dll not found: ${dllPath}`) - return false - } + if (!existsSync(dllPath)) return false this.lib = this.koffi.load(dllPath) - this.initImgHelper = this.lib.func('bool InitImgHelper(uint32)') + + this.initImgHelper = this.lib.func('bool InitImgHelper(uint32, const char*)') this.uninstallImgHelper = this.lib.func('void UninstallImgHelper()') this.getImgHelperError = this.lib.func('const char* GetImgHelperError()') - + this.initialized = true return true } catch (error) { @@ -96,16 +96,22 @@ export class ImageDownloadService { } } - async startAutoDownload(): Promise<{ success: boolean; error?: string }> { + async startAutoDownload(whitelist: string[] | string = []): Promise<{ success: boolean; error?: string }> { if (!await this.ensureInitialized()) { - return { success: false, error: '核心组件初始化失败,请检查环境' } + return { success: false, error: '核心组件初始化失败' } } - if (this.pollTimer) return { success: true } + if (this.isHooked) { + await this.unhook() + } - this.pollTimer = setInterval(() => this.checkAndHook(), 30000) - // 首次尝试 Hook,并返回结果 - return await this.checkAndHook(true) + this.lastWhitelist = whitelist + + if (!this.pollTimer) { + this.pollTimer = setInterval(() => this.checkAndHook(this.lastWhitelist, false), 30000) + } + + return await this.checkAndHook(whitelist, true) } async stopAutoDownload() { @@ -116,7 +122,7 @@ export class ImageDownloadService { await this.unhook() } - private async checkAndHook(isManualStart = false): Promise<{ success: boolean; error?: string }> { + private async checkAndHook(whitelist: string[] | string = [], isManualStart = false): Promise<{ success: boolean; error?: string }> { const pid = await this.findMainWeChatPid() if (!pid) { @@ -124,7 +130,6 @@ export class ImageDownloadService { console.log('[ImageDownloadService] WeChat exited, unhooking') await this.unhook() } - // 如果是手动开启时没找到进程,不认为是严重错误,只是挂起等待 return { success: true, error: '等待微信启动' } } @@ -139,7 +144,17 @@ export class ImageDownloadService { console.log(`[ImageDownloadService] attempting to hook PID: ${pid}`) try { - const success = this.initImgHelper(pid) + let whitelistBuffer: Buffer | null = null; + if (typeof whitelist === 'string') { + if (whitelist.length > 0) { + whitelistBuffer = Buffer.from(whitelist, 'utf8'); + } + } else if (Array.isArray(whitelist) && whitelist.length > 0) { + whitelistBuffer = Buffer.from(whitelist.join('\0') + '\0\0', 'utf8'); + } + + const success = this.initImgHelper(pid, whitelistBuffer) + if (success) { this.isHooked = true this.currentPid = pid @@ -148,7 +163,6 @@ export class ImageDownloadService { } else { const err = this.getImgHelperError() console.error(`[ImageDownloadService] hook failed: ${err}`) - // 如果是手动点击开启时失败,停止轮询并向上报错 if (isManualStart && this.pollTimer) { clearInterval(this.pollTimer) this.pollTimer = null diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 3521f77..09bca0a 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -327,7 +327,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [aiFootprintEnabled, setAiFootprintEnabled] = useState(false) const [aiFootprintSystemPrompt, setAiFootprintSystemPrompt] = useState('') const [aiInsightDebugLogEnabled, setAiInsightDebugLogEnabled] = useState(false) + + // 自动下载图片 const [autoDownloadStatus, setAutoDownloadStatus] = useState<{ isHooked: boolean; pid: number | null; supported: boolean } | null>(null) + const [autoDownloadSelectedIds, setAutoDownloadSelectedIds] = useState>(new Set()) + const [autoDownloadSearchKeyword, setAutoDownloadSearchKeyword] = useState('') // 检查 Hello 可用性 useEffect(() => { @@ -541,10 +545,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setExcludeWordsInput(savedExcludeWords.join('\n')) const savedAutoDownloadHighRes = await configService.getAutoDownloadHighRes() + const savedAutoDownloadWhitelist = await configService.getAutoDownloadWhitelist() const savedAnalyticsConsent = await configService.getAnalyticsConsent() setAnalyticsConsent(savedAnalyticsConsent ?? false) setAutoDownloadHighRes(savedAutoDownloadHighRes) - + setAutoDownloadSelectedIds(new Set(savedAutoDownloadWhitelist)) // 如果语言列表为空,保存默认值 @@ -4695,96 +4700,173 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { ) - const renderAutoDownloadTab = () => ( -
-
-
- 实验性功能 -

自动下载原图

-

强制微信在接收图片时下载高清原图,而非默认的模糊缩略图。

-
-
+ const renderAutoDownloadTab = () => { + const sortedSessions = [...chatSessions].sort((a, b) => (b.sortTimestamp || 0) - (a.sortTimestamp || 0)) + const keyword = autoDownloadSearchKeyword.trim().toLowerCase() + const filteredSessions = sortedSessions.filter((session) => { + if (!keyword) return true + return (session.displayName || '').toLowerCase().includes(keyword) || + session.username.toLowerCase().includes(keyword) + }) + const filteredSessionIds = filteredSessions.map((session) => session.username) + const selectedCount = autoDownloadSelectedIds.size + const selectedInFilteredCount = filteredSessionIds.filter((id) => autoDownloadSelectedIds.has(id)).length + const allFilteredSelected = filteredSessionIds.length > 0 && selectedInFilteredCount === filteredSessionIds.length + const isHooked = autoDownloadStatus?.isHooked -
-
-
-
- 启用自动下载 -
- 开启后,WeFlow 将通过远程 Hook 技术干预微信进程。 -
+ const persistWhitelist = (ids: Set) => { + const whitelistArr = Array.from(ids) + configService.setAutoDownloadWhitelist(whitelistArr) + if (autoDownloadHighRes) { + // 转换为 wxid\0wxid\0wxid\0\0 格式 + const whitelistStr = whitelistArr.length > 0 ? (whitelistArr.join('\0') + '\0\0') : ''; + (window as any).electronAPI.image.startAutoDownload(whitelistStr) + } + } + + const toggleSelection = (id: string) => { + const next = new Set(autoDownloadSelectedIds) + if (next.has(id)) next.delete(id) + else next.add(id) + setAutoDownloadSelectedIds(next) + persistWhitelist(next) + } + + const selectAllFiltered = () => { + const next = new Set(autoDownloadSelectedIds) + filteredSessionIds.forEach(id => next.add(id)) + setAutoDownloadSelectedIds(next) + persistWhitelist(next) + } + + const clearSelection = () => { + const next = new Set() + setAutoDownloadSelectedIds(next) + persistWhitelist(next) + } + + return ( +
+ {/* 顶部 Hero 区域 */} +
+
+ 测试功能 (Beta) +

自动下载原图

+

强制微信在接收图片时下载高清原图。建议仅在必要会话中开启以节省流量和空间。

+
+
+
+ 服务状态 + + {isHooked ? '正在监控' : autoDownloadHighRes ? '等待连接' : '未启用'} + +
+
+ 已选会话 + {selectedCount}
-
-
-
-
- -

运行状态

+
+
+
+ + setAutoDownloadSearchKeyword(e.target.value)} + /> +
+
+
+ + +
+
+ + + {autoDownloadHighRes ? '已开启' : '已关闭'} + +
+
+
+ +
+
+ 已选 {selectedCount} 个目标会话 + (若不选则默认对所有聊天生效) +
+
-
- {!autoDownloadHighRes ? ( -
服务未启动
- ) : !autoDownloadStatus ? ( -
正在检测状态...
- ) : !autoDownloadStatus.supported ? ( -
⚠️ 当前系统架构不支持此功能(仅支持 Win32 x64)
- ) : autoDownloadStatus.isHooked ? ( -
- ✓ 运行中 - 已成功挂载到微信进程 (PID: {autoDownloadStatus.pid}) -
+ +
+
+ 会话({filteredSessions.length}) + 选择 +
+ {filteredSessions.length === 0 ? ( +
{autoDownloadSearchKeyword ? '没有匹配的会话' : '暂无会话'}
) : ( -
- ⏳ 等待中 - 未检测到微信主进程 (Weixin.exe) 运行,请启动微信 -
+ filteredSessions.map((session) => ( +
toggleSelection(session.username)} + > +
+ +
+ {session.displayName || session.username} + {session.username} +
+
+
+ + + +
+
+ )) )}
-
-
-
- -

风险提示

-
-
-
-
- - 此功能涉及hook修改微信进程内存 -
-
- - 虽然当前方案不直接注入 DLL,但仍存在被微信安全机制检测的风险 -
-
- - 建议先少量测试使用,确认有无被检测的风险 -
+ {/* 风险提示 */} +
+
+ +

风险警告

+
+
+ 此功能通过内存 Hook 修改微信行为,具有一定的风险。请尽量仅在白名单模式下针对必要会话开启。
-
- ) - const handleToggleAutoDownload = async () => { + ) + } + const handleToggleAutoDownload = async (whitelist?: string[] | string) => { const newVal = !autoDownloadHighRes setAutoDownloadHighRes(newVal) try { if (newVal) { - const result = await (window as any).electronAPI.image.startAutoDownload() + let currentWhitelist: string[] | string = whitelist || Array.from(autoDownloadSelectedIds) + if (Array.isArray(currentWhitelist)) { + currentWhitelist = currentWhitelist.length > 0 ? (currentWhitelist.join('\0') + '\0\0') : '' + } + const result = await (window as any).electronAPI.image.startAutoDownload(currentWhitelist) if (result && !result.success) { // 如果底层明确返回了失败 throw new Error(result.error || '启动自动下载服务失败') diff --git a/src/services/config.ts b/src/services/config.ts index c06c6f4..0d6588e 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -120,7 +120,8 @@ export const CONFIG_KEYS = { AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled', AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt', AI_INSIGHT_DEBUG_LOG_ENABLED: 'aiInsightDebugLogEnabled', - AUTO_DOWNLOAD_HIGH_RES: 'autoDownloadHighRes' + AUTO_DOWNLOAD_HIGH_RES: 'autoDownloadHighRes', + AUTO_DOWNLOAD_WHITELIST: 'autoDownloadWhitelist' } as const export interface WxidConfig { @@ -2157,3 +2158,13 @@ export async function setAutoDownloadHighRes(enabled: boolean): Promise { await config.set(CONFIG_KEYS.AUTO_DOWNLOAD_HIGH_RES, enabled) } +export async function getAutoDownloadWhitelist(): Promise { + const value = await config.get(CONFIG_KEYS.AUTO_DOWNLOAD_WHITELIST) + return Array.isArray(value) ? value : [] +} + +export async function setAutoDownloadWhitelist(list: string[]): Promise { + const normalized = Array.from(new Set((list || []).map(item => String(item || '').trim()).filter(Boolean))) + await config.set(CONFIG_KEYS.AUTO_DOWNLOAD_WHITELIST, normalized) +} +