mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-06-18 15:54:08 +08:00
@@ -195,6 +195,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ImgHelper (Auto download large images) -->
|
||||
<div
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center group"
|
||||
:class="imgHelperBusy ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'"
|
||||
:title="imgHelperTitle"
|
||||
@click="toggleImgHelper"
|
||||
>
|
||||
<div class="sidebar-rail-plate w-[var(--sidebar-rail-btn)] h-[var(--sidebar-rail-btn)] rounded-md flex items-center justify-center transition-colors bg-transparent">
|
||||
<svg
|
||||
class="sidebar-rail-icon w-[var(--sidebar-rail-icon)] h-[var(--sidebar-rail-icon)]"
|
||||
:class="{ 'sidebar-rail-icon-active': imgHelperEnabled }"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<polyline points="21 15 16 10 5 21" />
|
||||
<path d="M12 9v5m-2-2l2 2 2-2" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Privacy -->
|
||||
<div
|
||||
class="sidebar-rail-action w-full h-[var(--sidebar-rail-step)] flex items-center justify-center cursor-pointer group"
|
||||
@@ -368,6 +395,7 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useChatAccountsStore } from '~/stores/chatAccounts'
|
||||
import { useChatRealtimeStore } from '~/stores/chatRealtime'
|
||||
import { useImgHelperStore } from '~/stores/imgHelper'
|
||||
import { usePrivacyStore } from '~/stores/privacy'
|
||||
import { useThemeStore } from '~/stores/theme'
|
||||
|
||||
@@ -384,6 +412,10 @@ themeStore.init()
|
||||
|
||||
const realtimeStore = useChatRealtimeStore()
|
||||
const { enabled: realtimeEnabled, available: realtimeAvailable, checking: realtimeChecking, statusError: realtimeStatusError, toggling: realtimeToggling } = storeToRefs(realtimeStore)
|
||||
|
||||
const imgHelperStore = useImgHelperStore()
|
||||
const { enabled: imgHelperEnabled, checking: imgHelperChecking, toggling: imgHelperToggling, error: imgHelperError } = storeToRefs(imgHelperStore)
|
||||
|
||||
const { open: settingsDialogOpen, openDialog: openSettingsDialog } = useSettingsDialog()
|
||||
const { getChatAccountInfo, deleteChatAccount } = useApi()
|
||||
|
||||
@@ -620,6 +652,18 @@ const toggleRealtime = async () => {
|
||||
if (realtimeBusy.value) return
|
||||
await realtimeStore.toggle({ silent: false })
|
||||
}
|
||||
|
||||
const imgHelperBusy = computed(() => !!imgHelperChecking.value || !!imgHelperToggling.value)
|
||||
|
||||
const imgHelperTitle = computed(() => {
|
||||
if (imgHelperEnabled.value) return '关闭自动下载大图'
|
||||
return imgHelperError.value || '开启自动下载大图'
|
||||
})
|
||||
|
||||
const toggleImgHelper = async () => {
|
||||
if (imgHelperBusy.value) return
|
||||
await imgHelperStore.toggle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -636,8 +636,21 @@ export const useApi = () => {
|
||||
return await request(url)
|
||||
}
|
||||
|
||||
const getImgHelperStatus = async () => {
|
||||
return await request('/system/img_helper/status')
|
||||
}
|
||||
|
||||
const toggleImgHelper = async (enabled) => {
|
||||
return await request('/system/img_helper/toggle', {
|
||||
method: 'POST',
|
||||
body: { enabled: !!enabled }
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
pickSystemDirectory,
|
||||
getImgHelperStatus,
|
||||
toggleImgHelper,
|
||||
detectWechat,
|
||||
detectCurrentAccount,
|
||||
decryptDatabase,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useImgHelperStore = defineStore('imgHelper', () => {
|
||||
const enabled = ref(false)
|
||||
const checking = ref(false)
|
||||
const toggling = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const fetchStatus = async () => {
|
||||
if (!process.client) return
|
||||
const api = useApi()
|
||||
checking.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const resp = await api.getImgHelperStatus()
|
||||
enabled.value = !!resp?.enabled
|
||||
} catch (e) {
|
||||
error.value = e?.message || '获取插件状态失败'
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = async () => {
|
||||
if (toggling.value) return
|
||||
|
||||
const targetState = !enabled.value
|
||||
|
||||
if (targetState) {
|
||||
// Show warning for first time or every time? User said "首次开启提示hook可能存在风控风险"
|
||||
// We can use localStorage to track if it's the first time.
|
||||
const hasWarned = localStorage.getItem('img_helper_warned')
|
||||
if (!hasWarned) {
|
||||
const confirmed = window.confirm('【安全提示】\n开启“自动下载大图”功能将使用 Hook 技术修改微信内存逻辑。这可能存在一定的风控风险,建议仅在需要时开启。\n\n确认开启吗?')
|
||||
if (!confirmed) return
|
||||
localStorage.setItem('img_helper_warned', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
toggling.value = true
|
||||
error.value = ''
|
||||
const api = useApi()
|
||||
try {
|
||||
const resp = await api.toggleImgHelper(targetState)
|
||||
enabled.value = !!resp?.enabled
|
||||
return true
|
||||
} catch (e) {
|
||||
error.value = e?.message || '操作失败'
|
||||
if (process.client) {
|
||||
window.alert(error.value)
|
||||
}
|
||||
return false
|
||||
} finally {
|
||||
toggling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize status
|
||||
if (process.client) {
|
||||
fetchStatus()
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
checking,
|
||||
toggling,
|
||||
error,
|
||||
fetchStatus,
|
||||
toggle
|
||||
}
|
||||
})
|
||||
@@ -37,6 +37,7 @@ from .routers.wechat_detection import router as _wechat_detection_router
|
||||
from .routers.wrapped import router as _wrapped_router
|
||||
from .request_logging import log_server_errors_middleware
|
||||
from .wcdb_realtime import WCDB_REALTIME, shutdown as _wcdb_shutdown
|
||||
from .img_helper import IMG_HELPER
|
||||
from .routers.biz import router as _biz_router
|
||||
from .routers.system import router as _system_router
|
||||
|
||||
@@ -188,6 +189,13 @@ async def _shutdown_wcdb_realtime() -> None:
|
||||
CHAT_REALTIME_AUTOSYNC.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Uninstall img_helper hook if enabled
|
||||
try:
|
||||
IMG_HELPER.disable()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
close_ok = False
|
||||
lock_timeout_s: float | None = 0.2
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import ctypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from .logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class ImgHelper:
|
||||
def __init__(self):
|
||||
self._lib: Optional[ctypes.CDLL] = None
|
||||
self._enabled = False
|
||||
self._lock = __import__("threading").Lock()
|
||||
|
||||
def _resolve_dll_path(self) -> Path:
|
||||
# 1. Default (source code layout)
|
||||
base = Path(__file__).resolve().parent
|
||||
path = base / "native" / "img_helper.dll"
|
||||
if path.exists():
|
||||
return path
|
||||
|
||||
# 2. Frozen (bundled exe)
|
||||
import sys
|
||||
if getattr(sys, "frozen", False):
|
||||
exe_dir = Path(sys.executable).resolve().parent
|
||||
# Try native subfolder or same folder as exe
|
||||
for p in [exe_dir / "native" / "img_helper.dll", exe_dir / "img_helper.dll"]:
|
||||
if p.exists():
|
||||
return p
|
||||
|
||||
# 3. Current working directory
|
||||
for p in [Path.cwd() / "native" / "img_helper.dll", Path.cwd() / "img_helper.dll"]:
|
||||
if p.exists():
|
||||
return p
|
||||
|
||||
return path # Fallback to default for error message
|
||||
|
||||
def _load_lib(self):
|
||||
if self._lib is not None:
|
||||
return self._lib
|
||||
|
||||
dll_path = self._resolve_dll_path()
|
||||
if not dll_path.exists():
|
||||
raise FileNotFoundError(f"Missing img_helper.dll at: {dll_path}")
|
||||
|
||||
try:
|
||||
# On Windows, ensure the DLL's directory is in the search path for dependencies
|
||||
if hasattr(os, 'add_dll_directory'):
|
||||
try:
|
||||
os.add_dll_directory(str(dll_path.parent))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
lib = ctypes.CDLL(str(dll_path))
|
||||
|
||||
lib.InitImgHelper.argtypes = [ctypes.c_uint32]
|
||||
lib.InitImgHelper.restype = ctypes.c_bool
|
||||
|
||||
lib.UninstallImgHelper.argtypes = []
|
||||
lib.UninstallImgHelper.restype = None
|
||||
|
||||
lib.GetImgHelperError.argtypes = []
|
||||
lib.GetImgHelperError.restype = ctypes.c_char_p
|
||||
|
||||
self._lib = lib
|
||||
return lib
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load img_helper.dll: {e}")
|
||||
raise
|
||||
|
||||
def enable(self, pid: int) -> tuple[bool, str]:
|
||||
with self._lock:
|
||||
try:
|
||||
lib = self._load_lib()
|
||||
if self._enabled:
|
||||
# If already enabled, we uninstall first to be safe as per DLL docs suggestion
|
||||
# about being designed to hook one process at a time.
|
||||
lib.UninstallImgHelper()
|
||||
|
||||
if lib.InitImgHelper(pid):
|
||||
self._enabled = True
|
||||
logger.info(f"ImgHelper hook applied to PID {pid}")
|
||||
return True, "Success"
|
||||
else:
|
||||
err_ptr = lib.GetImgHelperError()
|
||||
err_msg = err_ptr.decode('utf-8', errors='ignore') if err_ptr else "Unknown error"
|
||||
logger.error(f"ImgHelper hook failed: {err_msg}")
|
||||
return False, err_msg
|
||||
except Exception as e:
|
||||
logger.error(f"ImgHelper enable exception: {e}")
|
||||
return False, str(e)
|
||||
|
||||
def disable(self) -> bool:
|
||||
with self._lock:
|
||||
if not self._enabled:
|
||||
return True
|
||||
try:
|
||||
lib = self._load_lib()
|
||||
lib.UninstallImgHelper()
|
||||
self._enabled = False
|
||||
logger.info("ImgHelper hook uninstalled")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to uninstall img helper: {e}")
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
IMG_HELPER = ImgHelper()
|
||||
Binary file not shown.
@@ -1,7 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from ..img_helper import IMG_HELPER
|
||||
from .wechat_detection import check_wechat_status
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -32,4 +34,35 @@ async def pick_directory(title: str = "请选择目录", initial_dir: str = ""):
|
||||
# 在子线程中执行 GUI 操作
|
||||
folder_path = await loop.run_in_executor(pool, _open_folder_dialog, title, initial_dir)
|
||||
|
||||
return {"path": folder_path}
|
||||
return {"path": folder_path}
|
||||
|
||||
|
||||
@router.get("/api/system/img_helper/status", summary="获取大图下载辅助插件状态")
|
||||
async def get_img_helper_status():
|
||||
return {
|
||||
"enabled": IMG_HELPER.is_enabled
|
||||
}
|
||||
|
||||
|
||||
class ImgHelperToggleRequest(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
@router.post("/api/system/img_helper/toggle", summary="开启/关闭大图下载辅助插件")
|
||||
async def toggle_img_helper(req: ImgHelperToggleRequest):
|
||||
if not req.enabled:
|
||||
IMG_HELPER.disable()
|
||||
return {"status": "success", "enabled": False}
|
||||
|
||||
# Attempt to enable
|
||||
status_res = await check_wechat_status()
|
||||
wx_status = status_res.get("wx_status", {})
|
||||
if not wx_status.get("is_running") or not wx_status.get("pid"):
|
||||
raise HTTPException(status_code=400, detail="未检测到微信正在运行,请先打开微信再尝试!")
|
||||
|
||||
pid = wx_status["pid"]
|
||||
ok, err = IMG_HELPER.enable(pid)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=500, detail=f"开启失败: {err}")
|
||||
|
||||
return {"status": "success", "enabled": True}
|
||||
Reference in New Issue
Block a user