feat(image): 新增自动下载大图选项(win32 x64)

Co-authored-by: NineBird <CavanasD@users.noreply.github.com>
This commit is contained in:
H3CoF6
2026-04-29 04:05:48 +08:00
parent 9c7ed1729a
commit 1f0b2613bf
6 changed files with 269 additions and 4 deletions

View File

@@ -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 端口占用,避免进程无法退出

View File

@@ -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),

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()}

View File

@@ -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)
}