mirror of
https://fastgit.cc/github.com/hicccc77/WeFlow
synced 2026-04-30 13:50:29 +08:00
feat(image): 新增自动下载大图选项(win32 x64)
Co-authored-by: NineBird <CavanasD@users.noreply.github.com>
This commit is contained in:
@@ -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<void> => {
|
||||
}, 5000)
|
||||
forceExitTimer.unref()
|
||||
try { await cloudControlService.stop() } catch {}
|
||||
// 停止自动下载服务
|
||||
try { await imageDownloadService.stopAutoDownload() } catch {}
|
||||
// 停止 chatService(内部会关闭 cursor 与 DB),避免退出阶段仍触发监控回调
|
||||
try { chatService.close() } catch {}
|
||||
// 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -117,6 +117,7 @@ interface ConfigSchema {
|
||||
aiFootprintSystemPrompt: string
|
||||
/** 是否将 AI 见解调试日志输出到桌面 */
|
||||
aiInsightDebugLogEnabled: boolean
|
||||
autoDownloadHighRes: boolean
|
||||
}
|
||||
|
||||
interface ConfigStoreLike<T extends Record<string, any>> {
|
||||
@@ -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()
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<number | null> {
|
||||
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()
|
||||
|
||||
@@ -32,6 +32,7 @@ type SettingsTab =
|
||||
| 'aiCommon'
|
||||
| 'insight'
|
||||
| 'aiFootprint'
|
||||
| 'autoDownload'
|
||||
|
||||
const tabs: { id: Exclude<SettingsTab, 'insight' | 'aiFootprint'>; label: string; icon: React.ElementType }[] = [
|
||||
{ id: 'appearance', label: '外观', icon: Palette },
|
||||
@@ -39,6 +40,7 @@ const tabs: { id: Exclude<SettingsTab, 'insight' | 'aiFootprint'>; 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<SettingsTab, 'insight' | 'aiFootprint'>; 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<SettingsTab, 'aiCommon' | 'insight' | 'aiFootprint'>; label: string }> = [
|
||||
{ id: 'aiCommon', label: '基础配置' },
|
||||
{ id: 'insight', label: 'AI 见解' },
|
||||
@@ -149,6 +158,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
const [imageKeyPercent, setImageKeyPercent] = useState<number | null>(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 = {}) {
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderAutoDownloadTab = () => (
|
||||
<div className="tab-content">
|
||||
<div className="form-group">
|
||||
<label>自动下载大图</label>
|
||||
<span className="form-hint">
|
||||
开启后,WeFlow 会通过远程 Hook 技术强制微信在接收图片时下载高清原图(而非默认的缩略图)。
|
||||
<br />
|
||||
<strong>风险提示</strong>:Hook 涉及修改微信进程内存,虽不注入 DLL 但仍有被检测风险,请谨慎开启。
|
||||
</span>
|
||||
<div className="log-toggle-line">
|
||||
<span className="log-status">{autoDownloadHighRes ? '已开启' : '已关闭'}</span>
|
||||
<label className="switch" htmlFor="auto-download-high-res-toggle">
|
||||
<input
|
||||
id="auto-download-high-res-toggle"
|
||||
className="switch-input"
|
||||
type="checkbox"
|
||||
checked={autoDownloadHighRes}
|
||||
onChange={handleToggleAutoDownload}
|
||||
/>
|
||||
<span className="switch-slider" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
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 = {}) {
|
||||
|
||||
<div className="settings-layout">
|
||||
<div className="settings-tabs" role="tablist" aria-label="设置项">
|
||||
{tabs.flatMap((tab) => {
|
||||
{filteredTabs.flatMap((tab) => {
|
||||
const row: React.ReactNode[] = [
|
||||
<button
|
||||
key={tab.id}
|
||||
@@ -4850,6 +4900,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
{activeTab === 'aiCommon' && renderAiCommonTab()}
|
||||
{activeTab === 'insight' && renderInsightTab()}
|
||||
{activeTab === 'aiFootprint' && renderAiFootprintTab()}
|
||||
{activeTab === 'autoDownload' && renderAutoDownloadTab()}
|
||||
{activeTab === 'updates' && renderUpdatesTab()}
|
||||
{activeTab === 'analytics' && renderAnalyticsTab()}
|
||||
{activeTab === 'security' && renderSecurityTab()}
|
||||
|
||||
@@ -119,7 +119,8 @@ export const CONFIG_KEYS = {
|
||||
// AI 足迹
|
||||
AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled',
|
||||
AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt',
|
||||
AI_INSIGHT_DEBUG_LOG_ENABLED: 'aiInsightDebugLogEnabled'
|
||||
AI_INSIGHT_DEBUG_LOG_ENABLED: 'aiInsightDebugLogEnabled',
|
||||
AUTO_DOWNLOAD_HIGH_RES: 'autoDownloadHighRes'
|
||||
} as const
|
||||
|
||||
export interface WxidConfig {
|
||||
@@ -2147,3 +2148,12 @@ export async function setAiInsightDebugLogEnabled(enabled: boolean): Promise<voi
|
||||
await config.set(CONFIG_KEYS.AI_INSIGHT_DEBUG_LOG_ENABLED, enabled)
|
||||
}
|
||||
|
||||
export async function getAutoDownloadHighRes(): Promise<boolean> {
|
||||
const value = await config.get(CONFIG_KEYS.AUTO_DOWNLOAD_HIGH_RES)
|
||||
return value === true
|
||||
}
|
||||
|
||||
export async function setAutoDownloadHighRes(enabled: boolean): Promise<void> {
|
||||
await config.set(CONFIG_KEYS.AUTO_DOWNLOAD_HIGH_RES, enabled)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user