mirror of
https://fastgit.cc/github.com/hicccc77/WeFlow
synced 2026-04-30 13:50:29 +08:00
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -350,6 +350,8 @@ jobs:
|
||||
updpkgsums: true
|
||||
assets: |
|
||||
resources/installer/linux/weflow.desktop
|
||||
resources/installer/linux/icon.png
|
||||
resources/installer/linux/.gitignore
|
||||
|
||||
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
commit_username: H3CoF6
|
||||
|
||||
@@ -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,19 @@ function registerIpcHandlers() {
|
||||
}
|
||||
})
|
||||
|
||||
// 自动下载原图
|
||||
ipcMain.handle('image:startAutoDownload', async (_, whitelist?: string[]) => {
|
||||
return await imageDownloadService.startAutoDownload(whitelist || [])
|
||||
})
|
||||
|
||||
ipcMain.handle('image:stopAutoDownload', async () => {
|
||||
await imageDownloadService.stopAutoDownload()
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
ipcMain.handle('image:getAutoDownloadStatus', async () => {
|
||||
return await imageDownloadService.getStatus()
|
||||
})
|
||||
}
|
||||
|
||||
// 主窗口引用
|
||||
@@ -4081,6 +4095,13 @@ app.whenReady().then(async () => {
|
||||
// 注册 IPC 处理器
|
||||
updateSplashProgress(28, '正在初始化...')
|
||||
registerIpcHandlers()
|
||||
if (configService.get('autoDownloadHighRes')) {
|
||||
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)
|
||||
insightService.handleDbMonitorChange(type, json)
|
||||
@@ -4252,6 +4273,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: (whitelist: string[] | string) => ipcRenderer.invoke('image:startAutoDownload', whitelist),
|
||||
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,8 @@ interface ConfigSchema {
|
||||
aiFootprintSystemPrompt: string
|
||||
/** 是否将 AI 见解调试日志输出到桌面 */
|
||||
aiInsightDebugLogEnabled: boolean
|
||||
autoDownloadHighRes: boolean
|
||||
autoDownloadWhitelist: string[]
|
||||
}
|
||||
|
||||
interface ConfigStoreLike<T extends Record<string, any>> {
|
||||
@@ -294,7 +296,9 @@ export class ConfigService {
|
||||
aiInsightWeiboBindings: {},
|
||||
aiFootprintEnabled: false,
|
||||
aiFootprintSystemPrompt: '',
|
||||
aiInsightDebugLogEnabled: false
|
||||
aiInsightDebugLogEnabled: false,
|
||||
autoDownloadHighRes: false,
|
||||
autoDownloadWhitelist: []
|
||||
}
|
||||
|
||||
const cwd = String(process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || '').trim()
|
||||
|
||||
203
electron/services/imageDownloadService.ts
Normal file
203
electron/services/imageDownloadService.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
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
|
||||
|
||||
private lastWhitelist: string[] = []
|
||||
|
||||
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)) return false
|
||||
|
||||
this.lib = this.koffi.load(dllPath)
|
||||
|
||||
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) {
|
||||
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(whitelist: string[] | string = []): Promise<{ success: boolean; error?: string }> {
|
||||
if (!await this.ensureInitialized()) {
|
||||
return { success: false, error: '核心组件初始化失败' }
|
||||
}
|
||||
|
||||
if (this.isHooked) {
|
||||
await this.unhook()
|
||||
}
|
||||
|
||||
this.lastWhitelist = whitelist
|
||||
|
||||
if (!this.pollTimer) {
|
||||
this.pollTimer = setInterval(() => this.checkAndHook(this.lastWhitelist, false), 30000)
|
||||
}
|
||||
|
||||
return await this.checkAndHook(whitelist, true)
|
||||
}
|
||||
|
||||
async stopAutoDownload() {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer)
|
||||
this.pollTimer = null
|
||||
}
|
||||
await this.unhook()
|
||||
}
|
||||
|
||||
private async checkAndHook(whitelist: string[] | string = [], isManualStart = false): Promise<{ success: boolean; error?: string }> {
|
||||
const pid = await this.findMainWeChatPid()
|
||||
|
||||
if (!pid) {
|
||||
if (this.isHooked) {
|
||||
console.log('[ImageDownloadService] WeChat exited, unhooking')
|
||||
await this.unhook()
|
||||
}
|
||||
return { success: true, error: '等待微信启动' }
|
||||
}
|
||||
|
||||
if (this.isHooked && this.currentPid === pid) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
console.log('[ImageDownloadService] hook successful')
|
||||
return { success: true }
|
||||
} else {
|
||||
const err = this.getImgHelperError()
|
||||
console.error(`[ImageDownloadService] hook failed: ${err}`)
|
||||
if (isManualStart && this.pollTimer) {
|
||||
clearInterval(this.pollTimer)
|
||||
this.pollTimer = null
|
||||
}
|
||||
return { success: false, error: err || 'Hook 失败' }
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('[ImageDownloadService] InitImgHelper call crashed:', e)
|
||||
if (isManualStart && this.pollTimer) {
|
||||
clearInterval(this.pollTimer)
|
||||
this.pollTimer = null
|
||||
}
|
||||
return { success: false, error: `调用异常: ${e.message || String(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()
|
||||
1
resources/image/README.md
Normal file
1
resources/image/README.md
Normal file
@@ -0,0 +1 @@
|
||||
> 目前只适配了x64 win32平台,其它平台同样原理,但是代码还没写(
|
||||
BIN
resources/image/win32/x64/img_helper.dll
Normal file
BIN
resources/image/win32/x64/img_helper.dll
Normal file
Binary file not shown.
6
resources/installer/linux/.gitignore
vendored
Normal file
6
resources/installer/linux/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*.tar.gz
|
||||
*.tar.xz
|
||||
*.zip
|
||||
src/
|
||||
pkg/
|
||||
weflow-*/
|
||||
@@ -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)
|
||||
@@ -318,6 +328,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
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<Set<string>>(new Set())
|
||||
const [autoDownloadSearchKeyword, setAutoDownloadSearchKeyword] = useState('')
|
||||
|
||||
// 检查 Hello 可用性
|
||||
useEffect(() => {
|
||||
setHelloAvailable(isWindows)
|
||||
@@ -529,9 +544,12 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
setWordCloudExcludeWords(savedExcludeWords)
|
||||
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))
|
||||
|
||||
|
||||
// 如果语言列表为空,保存默认值
|
||||
@@ -694,6 +712,21 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
void refreshWhisperStatus(whisperModelDir)
|
||||
}, [whisperModelDir])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'autoDownload') {
|
||||
fetchAutoDownloadStatus()
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
if (autoDownloadHighRes) {
|
||||
interval = setInterval(fetchAutoDownloadStatus, 2000)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval)
|
||||
}
|
||||
}
|
||||
}, [activeTab, autoDownloadHighRes])
|
||||
|
||||
const getErrorMessage = (error: any): string => {
|
||||
const raw = typeof error?.message === 'string' ? error.message : String(error ?? '')
|
||||
const normalized = raw.replace(/^Error:\s*/i, '').trim()
|
||||
@@ -1022,11 +1055,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'antiRevoke' && activeTab !== 'insight') return
|
||||
if (activeTab !== 'antiRevoke' && activeTab !== 'insight' && activeTab !== 'autoDownload') return
|
||||
let canceled = false
|
||||
;(async () => {
|
||||
try {
|
||||
if (activeTab === 'antiRevoke') {
|
||||
if (activeTab === 'antiRevoke' || activeTab === 'autoDownload') {
|
||||
await ensureAntiRevokeSessionsLoaded()
|
||||
} else {
|
||||
await ensureChatSessionsLoaded()
|
||||
@@ -1588,6 +1621,15 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchAutoDownloadStatus = async () => {
|
||||
try {
|
||||
const status = await (window as any).electronAPI.image.getAutoDownloadStatus()
|
||||
setAutoDownloadStatus(status)
|
||||
} catch (error) {
|
||||
console.error('获取自动下载状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const renderAppearanceTab = () => (
|
||||
<div className="tab-content">
|
||||
<div className="theme-mode-toggle">
|
||||
@@ -4658,6 +4700,203 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderAutoDownloadTab = () => {
|
||||
const sortedSessions = [...antiRevokeSessions].sort((a, b) => (b.sortTimestamp || 0) - (a.sortTimestamp || 0))
|
||||
const keyword = autoDownloadSearchKeyword.trim().toLowerCase()
|
||||
const filteredSessions = sortedSessions.filter((session) => {
|
||||
if (!keyword) return true
|
||||
const displayName = String(session.displayName || '').toLowerCase()
|
||||
const username = String(session.username || '').toLowerCase()
|
||||
return displayName.includes(keyword) || username.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
|
||||
|
||||
const persistWhitelist = (ids: Set<string>) => {
|
||||
const whitelistArr = Array.from(ids)
|
||||
configService.setAutoDownloadWhitelist(whitelistArr)
|
||||
if (autoDownloadHighRes) {
|
||||
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<string>()
|
||||
setAutoDownloadSelectedIds(next)
|
||||
persistWhitelist(next)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tab-content anti-revoke-tab">
|
||||
{/* 顶部 Hero 区域保持不变 */}
|
||||
<div className="anti-revoke-hero" style={{ background: 'linear-gradient(110deg, var(--bg-primary) 0%, rgba(245, 158, 11, 0.1) 100%)', borderColor: 'rgba(245, 158, 11, 0.3)' }}>
|
||||
<div className="anti-revoke-hero-main">
|
||||
<span className="updates-chip" style={{ color: '#f59e0b', background: 'rgba(245, 158, 11, 0.15)', width: 'fit-content' }}>测试功能 (Test)</span>
|
||||
<h2 style={{ marginTop: '8px' }}>自动下载原图</h2>
|
||||
<p>强制微信在接收图片时下载高清原图。建议仅在必要会话中开启以节省流量和空间。</p>
|
||||
</div>
|
||||
<div className="anti-revoke-metrics">
|
||||
<div className={`anti-revoke-metric ${isHooked ? 'is-installed' : 'is-pending'}`}>
|
||||
<span className="label">服务状态</span>
|
||||
<span className="value" style={{ fontSize: '14px' }}>
|
||||
{isHooked ? '正在监控' : autoDownloadHighRes ? '等待连接' : '未启用'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="anti-revoke-metric">
|
||||
<span className="label">已选会话</span>
|
||||
<span className="value">{selectedCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="anti-revoke-control-card">
|
||||
<div className="anti-revoke-toolbar">
|
||||
<div className="filter-search-box anti-revoke-search">
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索联系人或群聊..."
|
||||
value={autoDownloadSearchKeyword}
|
||||
onChange={(e) => setAutoDownloadSearchKeyword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="anti-revoke-toolbar-actions">
|
||||
<div className="anti-revoke-btn-group">
|
||||
<button className="btn btn-secondary btn-sm" onClick={selectAllFiltered} disabled={filteredSessionIds.length === 0 || allFilteredSelected}>
|
||||
全选
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={clearSelection} disabled={selectedCount === 0}>
|
||||
清空选择
|
||||
</button>
|
||||
</div>
|
||||
<div className="anti-revoke-btn-group" style={{ marginLeft: '12px', paddingLeft: '12px', borderLeft: '1px solid var(--border-color)' }}>
|
||||
<label className="switch switch-md">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoDownloadHighRes}
|
||||
onChange={() => handleToggleAutoDownload(Array.from(autoDownloadSelectedIds))}
|
||||
/>
|
||||
<span className="switch-slider" />
|
||||
</label>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-secondary)', marginLeft: '8px' }}>
|
||||
{autoDownloadHighRes ? '服务已开启' : '服务已关闭'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="anti-revoke-batch-actions">
|
||||
<div className="anti-revoke-selected-count">
|
||||
<span>已选 <strong>{selectedCount}</strong> 个目标会话</span>
|
||||
<span style={{ opacity: 0.6 }}>(若不选则默认对所有聊天生效)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="anti-revoke-list">
|
||||
<div className="anti-revoke-list-header">
|
||||
<span>会话({filteredSessions.length})</span>
|
||||
<span>状态</span>
|
||||
</div>
|
||||
{filteredSessions.length === 0 ? (
|
||||
<div className="anti-revoke-empty">{autoDownloadSearchKeyword ? '没有匹配的会话' : '暂无会话'}</div>
|
||||
) : (
|
||||
filteredSessions.map((session) => {
|
||||
const isSelected = autoDownloadSelectedIds.has(session.username)
|
||||
return (
|
||||
<div key={session.username} className={`anti-revoke-row ${isSelected ? 'selected' : ''}`}>
|
||||
<label className="anti-revoke-row-main">
|
||||
<span className="anti-revoke-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleSelection(session.username)}
|
||||
/>
|
||||
<span className="check-indicator" aria-hidden="true">
|
||||
<Check size={12} />
|
||||
</span>
|
||||
</span>
|
||||
<Avatar src={session.avatarUrl} name={session.displayName} size={30} />
|
||||
<div className="anti-revoke-row-text">
|
||||
<span className="name">{session.displayName || session.username}</span>
|
||||
</div>
|
||||
</label>
|
||||
<div className="anti-revoke-row-status">
|
||||
<span className={`status-badge ${isSelected ? 'installed' : 'not-installed'}`}>
|
||||
<i className="status-dot" aria-hidden="true" />
|
||||
{isSelected ? '已监控' : '未开启'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 风险提示部分保持不变 */}
|
||||
<div className="api-warning-modal" style={{ width: '100%', border: '1px solid rgba(239, 68, 68, 0.2)', marginTop: '16px', background: 'rgba(239, 68, 68, 0.02)', animation: 'none', boxShadow: 'none', position: 'static' }}>
|
||||
<div className="modal-header" style={{ border: 'none', padding: '12px 20px 0' }}>
|
||||
<Lock size={16} color="#ef4444" />
|
||||
<h3 style={{ fontSize: '13px', color: '#ef4444' }}>风险警告</h3>
|
||||
</div>
|
||||
<div className="modal-body" style={{ fontSize: '12px', color: 'var(--text-secondary)', padding: '8px 20px 12px' }}>
|
||||
此功能通过内存 Hook 修改微信行为,具有一定的风险。请尽量仅在白名单模式下针对必要会话开启。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const handleToggleAutoDownload = async (whitelist?: string[] | string) => {
|
||||
const newVal = !autoDownloadHighRes
|
||||
setAutoDownloadHighRes(newVal)
|
||||
|
||||
try {
|
||||
if (newVal) {
|
||||
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 || '启动自动下载服务失败')
|
||||
}
|
||||
showMessage('自动下载已开启,正在尝试连接微信', true)
|
||||
await fetchAutoDownloadStatus()
|
||||
} else {
|
||||
await (window as any).electronAPI.image.stopAutoDownload()
|
||||
showMessage('自动下载已关闭', true)
|
||||
setAutoDownloadStatus(null)
|
||||
}
|
||||
await configService.setAutoDownloadHighRes(newVal)
|
||||
} catch (e: any) {
|
||||
// 发生错误时,将开关拨回去
|
||||
setAutoDownloadHighRes(!newVal)
|
||||
showMessage(`操作失败: ${e.message || String(e)}`, false)
|
||||
}
|
||||
}
|
||||
|
||||
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 +5031,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 +5089,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,9 @@ 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',
|
||||
AUTO_DOWNLOAD_WHITELIST: 'autoDownloadWhitelist'
|
||||
} as const
|
||||
|
||||
export interface WxidConfig {
|
||||
@@ -2147,3 +2149,22 @@ 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)
|
||||
}
|
||||
|
||||
export async function getAutoDownloadWhitelist(): Promise<string[]> {
|
||||
const value = await config.get(CONFIG_KEYS.AUTO_DOWNLOAD_WHITELIST)
|
||||
return Array.isArray(value) ? value : []
|
||||
}
|
||||
|
||||
export async function setAutoDownloadWhitelist(list: string[]): Promise<void> {
|
||||
const normalized = Array.from(new Set((list || []).map(item => String(item || '').trim()).filter(Boolean)))
|
||||
await config.set(CONFIG_KEYS.AUTO_DOWNLOAD_WHITELIST, normalized)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user