diff --git a/.gitignore b/.gitignore
index 0d3413b..6cf9ef4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ wheels/
# Local config templates
/wechat_db_config_template.json
.ace-tool/
+pnpm-lock.yaml
# Local dev repos and data
/WxDatDecrypt/
diff --git a/frontend/composables/useApi.js b/frontend/composables/useApi.js
index 63d1209..c1ce542 100644
--- a/frontend/composables/useApi.js
+++ b/frontend/composables/useApi.js
@@ -375,7 +375,22 @@ export const useApi = () => {
const url = `/wrapped/annual/cards/${safeId}` + (query.toString() ? `?${query.toString()}` : '')
return await request(url)
}
-
+
+ // 获取微信进程状态
+ const getWxStatus = async () => {
+ return await request('/wechat/status')
+ }
+
+ // 获取数据库密钥
+ const getDbKey = async () => {
+ return await request('/get_db_key')
+ }
+
+ // 获取图片密钥
+ const getImageKey = async () => {
+ return await request('/get_image_key')
+ }
+
return {
detectWechat,
detectCurrentAccount,
@@ -408,6 +423,9 @@ export const useApi = () => {
exportChatContacts,
getWrappedAnnual,
getWrappedAnnualMeta,
- getWrappedAnnualCard
+ getWrappedAnnualCard,
+ getDbKey,
+ getImageKey,
+ getWxStatus,
}
}
diff --git a/frontend/package.json b/frontend/package.json
index aff4c28..b527734 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,5 +19,8 @@
"ogl": "^1.0.11",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
+ },
+ "devDependencies": {
+ "tailwindcss": "3.4.17"
}
}
diff --git a/frontend/pages/decrypt.vue b/frontend/pages/decrypt.vue
index fb76aec..ab21f26 100644
--- a/frontend/pages/decrypt.vue
+++ b/frontend/pages/decrypt.vue
@@ -26,24 +26,40 @@
-
-
-
@@ -131,6 +147,26 @@
+
+
+
手动输入或通过微信获取
+
+
+
@@ -158,7 +194,7 @@
- 使用
wx_key 获取图片密钥;AES 可选(V4-V2 需要)
+ 尝试自动获取,或使用
wx_key 获取图片密钥;AES 可选(V4-V2 需要)
@@ -325,6 +361,19 @@
+
+
+
+
+
@@ -367,12 +416,15 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { useApi } from '~/composables/useApi'
-const { decryptDatabase, saveMediaKeys, getSavedKeys } = useApi()
+const { decryptDatabase, saveMediaKeys, getSavedKeys, getDbKey, getImageKey, getWxStatus } = useApi()
const loading = ref(false)
const error = ref('')
+const warning = ref('') // 警告,用于密钥提示
const currentStep = ref(0)
const mediaAccount = ref('')
+const isGettingDbKey = ref(false)
+const isGettingImageKey = ref(false)
// 步骤定义
const steps = [
@@ -453,10 +505,89 @@ const prefillKeysForAccount = async (account) => {
}
}
+const handleGetDbKey = async () => {
+ if (isGettingDbKey.value) return
+ isGettingDbKey.value = true
+
+ error.value = ''
+ warning.value = ''
+ formErrors.key = ''
+
+ try {
+ const statusRes = await getWxStatus() // pid不是主进程,但是没关系
+ const wxStatus = statusRes?.wx_status
+
+ if (wxStatus?.is_running) {
+ warning.value = '检测到微信正在运行,5秒后将终止进程并重启以获取密钥!!'
+ await new Promise(resolve => setTimeout(resolve, 5000))
+ } else {
+ // 没有逻辑
+ }
+
+ warning.value = '正在启动微信以获取密钥,请确保微信未开启“自动登录”,并在启动后 1 分钟内完成登录操作。'
+
+ const res = await getDbKey()
+
+ if (res && res.status === 0) {
+ if (res.data?.db_key) {
+ formData.key = res.data.db_key
+ warning.value = ''
+ }
+
+ if (res.errmsg && res.errmsg !== 'ok') {
+ warning.value = res.errmsg
+ }
+ } else {
+ error.value = '获取失败: ' + (res?.errmsg || '未知错误')
+ }
+ } catch (e) {
+ console.error(e)
+ error.value = '系统错误: ' + e.message
+ } finally {
+ isGettingDbKey.value = false
+ }
+}
+
+const handleGetImageKey = async () => {
+ if (isGettingImageKey.value) return
+ isGettingImageKey.value = true
+ manualKeyErrors.xor_key = ''
+ manualKeyErrors.aes_key = ''
+
+ error.value = ''
+ warning.value = ''
+
+ try {
+ const res = await getImageKey()
+
+ if (res && res.status === 0) {
+ if (res.data?.aes_key) {
+ manualKeys.aes_key = res.data.aes_key
+ }
+ if (res.data?.xor_key) {
+ // 后端记得处理为16进制再返回!!!
+ manualKeys.xor_key = res.data.xor_key
+ }
+
+ if (res.errmsg && res.errmsg !== 'ok') {
+ error.value = res.errmsg
+ }
+ } else {
+ error.value = '获取失败: ' + (res?.errmsg || '未知错误')
+ }
+ } catch (e) {
+ console.error(e)
+ error.value = '系统错误: ' + e.message
+ } finally {
+ isGettingImageKey.value = false
+ }
+}
+
const applyManualKeys = () => {
manualKeyErrors.xor_key = ''
manualKeyErrors.aes_key = ''
error.value = ''
+ warning.value = ''
const aes = normalizeAesKey(manualKeys.aes_key)
if (!aes.ok) {
@@ -550,6 +681,7 @@ const handleDecrypt = async () => {
loading.value = true
error.value = ''
+ warning.value = ''
try {
const result = await decryptDatabase({
@@ -596,6 +728,7 @@ const decryptAllImages = async () => {
mediaDecrypting.value = true
mediaDecryptResult.value = null
error.value = ''
+ warning.value = ''
// 重置进度
decryptProgress.current = 0
@@ -671,6 +804,7 @@ const decryptAllImages = async () => {
// 从密钥步骤进入图片解密步骤
const goToMediaDecryptStep = async () => {
error.value = ''
+ warning.value = ''
// 校验并应用(未填写则允许直接进入,后端会使用已保存密钥或报错提示)
const ok = applyManualKeys()
if (!ok || manualKeyErrors.xor_key || manualKeyErrors.aes_key) return
diff --git a/pyproject.toml b/pyproject.toml
index 96d2d4e..8b3e106 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,6 +19,9 @@ dependencies = [
"zstandard>=0.23.0",
"pilk>=0.2.4",
"pypinyin>=0.53.0",
+ "wx_key",
+ "packaging",
+ "httpx",
]
[project.optional-dependencies]
@@ -40,3 +43,6 @@ include = [
"src/wechat_decrypt_tool/native/wcdb_api.dll",
"src/wechat_decrypt_tool/native/WCDB.dll",
]
+
+[tool.uv]
+find-links = ["./tools/key_wheels/"]
diff --git a/src/wechat_decrypt_tool/key_service.py b/src/wechat_decrypt_tool/key_service.py
new file mode 100644
index 0000000..e2dec04
--- /dev/null
+++ b/src/wechat_decrypt_tool/key_service.py
@@ -0,0 +1,357 @@
+# import sys
+# import requests
+
+try:
+ import wx_key
+except ImportError:
+ print('[!] 环境中未安装wx_key依赖,可能无法自动获取数据库密钥')
+ wx_key = None
+ # sys.exit(1)
+
+import time
+import psutil
+import subprocess
+import hashlib
+import os
+import json
+import random
+import logging
+import httpx
+from pathlib import Path
+from typing import Optional, List, Dict, Any
+from dataclasses import dataclass
+from packaging import version as pkg_version # 建议使用 packaging 库处理版本比较
+from .wechat_detection import detect_wechat_installation
+from .key_store import upsert_account_keys_in_store
+from .media_helpers import _resolve_account_dir, _resolve_account_wxid_dir
+
+logger = logging.getLogger(__name__)
+
+
+# ====================== 以下是hook逻辑 ======================================
+
+@dataclass
+class HookConfig:
+ min_version: str
+ pattern: str # 用 00 不要用 ? !!!! 否则C++内存会炸
+ mask: str
+ offset: int
+
+
+class WeChatKeyFetcher:
+ def __init__(self):
+ self.process_name = "Weixin.exe"
+ self.timeout_seconds = 60
+
+ @staticmethod
+ def _hex_array_to_str(hex_array: List[int]) -> str:
+ return " ".join([f"{b:02X}" for b in hex_array])
+
+ def _get_hook_config(self, version_str: str) -> Optional[HookConfig]:
+ """搬运自wx_key代码,未来用ida脚本直接获取即可"""
+ try:
+ v_curr = pkg_version.parse(version_str)
+ except Exception as e:
+ logger.error(f"版本号解析失败: {version_str} || {e}")
+ return None
+
+ if v_curr > pkg_version.parse("4.1.6.14"):
+ return HookConfig(
+ min_version=">4.1.6.14",
+ pattern=self._hex_array_to_str([
+ 0x24, 0x50, 0x48, 0xC7, 0x45, 0x00, 0xFE, 0xFF, 0xFF, 0xFF,
+ 0x44, 0x89, 0xCF, 0x44, 0x89, 0xC3, 0x49, 0x89, 0xD6, 0x48,
+ 0x89, 0xCE, 0x48, 0x89
+ ]),
+ mask="xxxxxxxxxxxxxxxxxxxxxxxx",
+ offset=-3
+ )
+
+ if pkg_version.parse("4.1.4") <= v_curr <= pkg_version.parse("4.1.6.14"):
+ return HookConfig(
+ min_version="4.1.4-4.1.6.14",
+ pattern=self._hex_array_to_str([
+ 0x24, 0x08, 0x48, 0x89, 0x6c, 0x24, 0x10, 0x48, 0x89, 0x74,
+ 0x00, 0x18, 0x48, 0x89, 0x7c, 0x00, 0x20, 0x41, 0x56, 0x48,
+ 0x83, 0xec, 0x50, 0x41
+ ]),
+ mask="xxxxxxxxxx?xxxx?xxxxxxxx",
+ offset=-3
+ )
+
+ if v_curr < pkg_version.parse("4.1.4"):
+ return HookConfig(
+ min_version="<4.1.4",
+ pattern=self._hex_array_to_str([
+ 0x24, 0x50, 0x48, 0xc7, 0x45, 0x00, 0xfe, 0xff, 0xff, 0xff,
+ 0x44, 0x89, 0xcf, 0x44, 0x89, 0xc3, 0x49, 0x89, 0xd6, 0x48,
+ 0x89, 0xce, 0x48, 0x89
+ ]),
+ mask="xxxxxxxxxxxxxxxxxxxxxxxx",
+ offset=-15 # -0xf
+ )
+
+ return None
+
+ def kill_wechat(self):
+ """检测并查杀微信进程"""
+ killed = False
+ for proc in psutil.process_iter(['pid', 'name']):
+ try:
+ if proc.info['name'] == self.process_name:
+ logger.info(f"Killing WeChat process: {proc.info['pid']}")
+ proc.terminate()
+ killed = True
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
+ pass
+
+ if killed:
+ time.sleep(1) # 等待完全退出
+
+ def launch_wechat(self, exe_path: str) -> int:
+ """启动微信并返回 PID"""
+ try:
+
+ process = subprocess.Popen(exe_path)
+
+ time.sleep(2)
+ candidates = []
+ for proc in psutil.process_iter(['pid', 'name', 'create_time']):
+ if proc.info['name'] == self.process_name:
+ candidates.append(proc)
+
+ if candidates:
+
+ candidates.sort(key=lambda x: x.info['create_time'], reverse=True)
+ target_pid = candidates[0].info['pid']
+ return target_pid
+
+ return process.pid
+
+ except Exception as e:
+ logger.error(f"启动微信失败: {e}")
+ raise RuntimeError(f"无法启动微信: {e}")
+
+ def fetch_key(self) -> str:
+ """没有wx_key模块无法自动获取密钥"""
+ if wx_key is None:
+ raise RuntimeError("wx_key 模块未安装或加载失败")
+
+ install_info = detect_wechat_installation()
+
+ exe_path = install_info.get('wechat_exe_path')
+ version = install_info.get('wechat_version')
+
+ if not exe_path or not version:
+ raise RuntimeError("无法自动定位微信安装路径或版本")
+
+ logger.info(f"Detect WeChat: {version} at {exe_path}")
+
+ config = self._get_hook_config(version)
+ if not config:
+ raise RuntimeError(f"不支持的微信版本: {version}")
+
+ self.kill_wechat()
+
+ pid = self.launch_wechat(exe_path)
+ logger.info(f"WeChat launched, PID: {pid}")
+
+ logger.info(f"Initializing Hook with pattern: {config.pattern[:20]}... Offset: {config.offset}")
+
+ if not wx_key.initialize_hook(pid, "", config.pattern, config.mask, config.offset):
+ err = wx_key.get_last_error_msg()
+ raise RuntimeError(f"Hook初始化失败: {err}")
+
+ start_time = time.time()
+
+
+ try:
+ while True:
+ if time.time() - start_time > self.timeout_seconds:
+ raise TimeoutError("获取密钥超时 (60s)")
+
+ key = wx_key.poll_key_data()
+ if key:
+ found_key = key
+ break
+
+ while True:
+ msg, level = wx_key.get_status_message()
+ if msg is None:
+ break
+ if level == 2:
+ logger.error(f"[Hook Error] {msg}")
+
+ time.sleep(0.1)
+
+ finally:
+ logger.info("Cleaning up hook...")
+ wx_key.cleanup_hook()
+
+ if found_key:
+ return found_key
+ else:
+ raise RuntimeError("未知错误,未获取到密钥")
+
+def get_db_key_workflow():
+ fetcher = WeChatKeyFetcher()
+ return fetcher.fetch_key()
+
+
+# ============================== 以下是图片密钥逻辑 =====================================
+
+
+# 远程 API 配置
+REMOTE_URL = "https://view.free.c3o.re/dashboard"
+NEXT_ACTION_ID = "7c8f99280c70626ccf5960cc4a68f368197e15f8e9"
+
+
+def get_wechat_internal_global_config(wx_dir: Path, file_name1) -> bytes:
+ """
+ 读微信目录下的主配置文件
+ """
+ xwechat_files_root = wx_dir.parent
+
+ target_path = os.path.join(xwechat_files_root, "all_users", "config", file_name1)
+
+ if not os.path.exists(target_path):
+ logger.error(f"未找到微信内部 global_config: {target_path}")
+ raise FileNotFoundError(f"找不到文件: {target_path},请确认微信数据目录结构是否完整")
+
+ return Path(target_path).read_bytes()
+
+
+# def get_local_config_sha3_224() -> bytes:
+# """
+# 不要在意,抽象的实现 哈哈哈
+# """
+# content = json.dumps({
+# "wxfile_dir": "C:\\Users\\17078\\xwechat_files",
+# "weixin_id_folder": "wxid_lnyf4hdo9csb12_f1c4",
+# "cache_dir": "C:\\Users\\17078\\Desktop\\wxDBHook\\test\\wx-dat\\wx-dat\\.cache",
+# "db_key": "",
+# "port": 8001
+# }, indent=4).encode("utf-8")
+#
+# # 计算 SHA3-224
+# digest = hashlib.sha3_224(content).digest()
+# return digest
+
+# async def log_request(request):
+# print(f"--- Request Raw ---")
+# print(f"{request.method} {request.url} {request.extensions.get('http_version', b'HTTP/1.1').decode()}")
+# for name, value in request.headers.items():
+# print(f"{name}: {value}")
+#
+# print()
+#
+# body = request.read()
+# if body:
+# print(body.decode(errors='replace'))
+# print(f"-------------------\n")
+
+
+async def fetch_and_save_remote_keys(account: Optional[str] = None) -> Dict[str, Any]:
+ account_dir = _resolve_account_dir(account)
+ wx_id_dir = _resolve_account_wxid_dir(account_dir)
+ wxid = wx_id_dir.name
+
+ logger.info(f"正在为账号 {wxid} 获取密钥...")
+
+ try:
+ blob1_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1= "global_config") # 估计这是唯一有效的数据!!
+ logger.info(f"获取微信内部配置成功,大小: {len(blob1_bytes)} bytes")
+ except Exception as e:
+ raise RuntimeError(f"读取微信内部文件失败: {e}")
+
+ try:
+ blob2_bytes = get_wechat_internal_global_config(wx_id_dir, file_name1= "global_config.crc")
+ logger.info(f"获取微信内部配置成功,大小: {len(blob2_bytes)} bytes")
+ except Exception as e:
+ raise RuntimeError(f"读取微信内部文件失败: {e}")
+
+ blob3_bytes = b""
+
+ headers = {
+ "Accept": "text/x-component",
+ "Next-Action": NEXT_ACTION_ID,
+ "Next-Router-State-Tree": "%5B%22%22%2C%7B%22children%22%3A%5B%22dashboard%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D",
+ "Origin": "https://view.free.c3o.re",
+ "Referer": "https://view.free.c3o.re/dashboard",
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
+ }
+
+ files = {
+ '1': ('blob', blob1_bytes, 'application/octet-stream'),
+ '2': ('blob', blob2_bytes, 'application/octet-stream'),
+ '3': ('blob', blob3_bytes, 'application/octet-stream'),
+ '0': (None, json.dumps([wxid, "$A1", "$A2", "$A3", 0],separators=(",",":")).encode('utf-8')),
+ }
+
+
+ async with httpx.AsyncClient(timeout=30) as client:
+ logger.info("向远程服务器发送请求...")
+ response = await client.post(REMOTE_URL, headers=headers, files=files)
+
+ if response.status_code != 200:
+ raise RuntimeError(f"远程服务器错误: {response.status_code} - {response.text[:100]}")
+
+
+ result_data = {}
+ lines = response.text.split('\n')
+
+ found_config = False
+ for line in lines:
+ line = line.strip()
+ if not line:
+ continue
+
+ if line.startswith('1:'):
+ try:
+ json_part = line[2:] # 去掉 "1:"
+ data_obj = json.loads(json_part)
+
+ if "config" in data_obj:
+ config = data_obj["config"]
+ result_data = {
+ "xor_key": config.get("xor_key", ""),
+ "aes_key": config.get("aes_key", ""),
+ "nick_name": config.get("nick_name", ""),
+ "avatar_url": config.get("avatar_url", "")
+ }
+ found_config = True
+ break
+ except Exception as e:
+ logger.warning(f"解析响应行失败: {e}")
+ continue
+
+ if not found_config or not result_data.get("aes_key"):
+ logger.error(f"响应中未找到密钥信息。Full Response: {response.text[:500]}")
+ raise RuntimeError("解析失败: 服务器未返回 config 数据")
+
+ # 6. 处理并保存密钥
+ xor_raw = str(result_data["xor_key"])
+ aes_val = str(result_data["aes_key"])
+
+ try:
+ if xor_raw.startswith("0x"):
+ xor_int = int(xor_raw, 16)
+ else:
+ xor_int = int(xor_raw)
+ xor_hex_str = f"0x{xor_int:02X}"
+ except:
+ xor_hex_str = xor_raw
+
+ upsert_account_keys_in_store(
+ account=wxid,
+ image_xor_key=xor_hex_str,
+ image_aes_key=aes_val
+ )
+
+ return {
+ "wxid": wxid,
+ "xor_key": xor_hex_str,
+ "aes_key": aes_val,
+ "nick_name": result_data["nick_name"]
+ }
+
diff --git a/src/wechat_decrypt_tool/routers/keys.py b/src/wechat_decrypt_tool/routers/keys.py
index 20344e4..101f9e5 100644
--- a/src/wechat_decrypt_tool/routers/keys.py
+++ b/src/wechat_decrypt_tool/routers/keys.py
@@ -3,6 +3,7 @@ from typing import Optional
from fastapi import APIRouter
from ..key_store import get_account_keys_from_store
+from ..key_service import get_db_key_workflow, fetch_and_save_remote_keys
from ..media_helpers import _load_media_keys, _resolve_account_dir
from ..path_fix import PathFixRoute
@@ -51,3 +52,76 @@ async def get_saved_keys(account: Optional[str] = None):
"keys": result,
}
+
+@router.get("/api/get_db_key", summary="自动获取微信数据库密钥")
+async def get_wechat_db_key():
+ """
+ 自动流程:
+ 1. 结束微信进程
+ 2. 启动微信
+ 3. 根据版本注入 Hook
+ 4. 抓取密钥并返回
+ """
+ try:
+ # 不需要async吧,我相信fastapi的线程池
+ db_key = get_db_key_workflow()
+
+ return {
+ "status": 0,
+ "errmsg": "ok",
+ "data": {
+ "db_key": db_key
+ }
+ }
+
+ except TimeoutError:
+ return {
+ "status": -1,
+ "errmsg": "获取超时,请确保微信没有开启自动登录 或者 加快手速",
+ "data": {}
+ }
+ except Exception as e:
+ return {
+ "status": -1,
+ "errmsg": f"获取失败: {str(e)}",
+ "data": {}
+ }
+
+
+@router.get("/api/get_image_key", summary="获取并保存微信图片密钥")
+async def get_image_key(account: Optional[str] = None):
+ """
+ 通过模拟 Next.js Server Action 协议,利用本地微信配置文件换取 AES/XOR 密钥。
+
+ 1. 读取 [wx_dir]/all_users/config/global_config (Blob 1)
+ 2. 读 同上目录下的global_config.crc
+ 3. 构造 Multipart 包发送至远程服务器
+ 4. 解析返回流,自动存入本地数据库
+ """
+ try:
+ result = await fetch_and_save_remote_keys(account)
+
+ return {
+ "status": 0,
+ "errmsg": "ok",
+ "data": {
+ "xor_key": result["xor_key"],
+ "aes_key": result["aes_key"],
+ "nick_name": result.get("nick_name"),
+ "account": result["wxid"]
+ }
+ }
+ except FileNotFoundError as e:
+ return {
+ "status": -1,
+ "errmsg": f"文件缺失: {str(e)}",
+ "data": {}
+ }
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ return {
+ "status": -1,
+ "errmsg": f"获取失败: {str(e)}",
+ "data": {}
+ }
diff --git a/src/wechat_decrypt_tool/routers/wechat_detection.py b/src/wechat_decrypt_tool/routers/wechat_detection.py
index ae0077d..b2fe1c5 100644
--- a/src/wechat_decrypt_tool/routers/wechat_detection.py
+++ b/src/wechat_decrypt_tool/routers/wechat_detection.py
@@ -1,5 +1,5 @@
from typing import Optional
-
+import psutil
from fastapi import APIRouter
from ..logging_config import get_logger
@@ -71,3 +71,49 @@ async def detect_current_account(data_root_path: Optional[str] = None):
'error': str(e),
'data': None,
}
+
+
+@router.get("/api/wechat/status", summary="检查微信运行状态")
+async def check_wechat_status():
+ """
+ 检查系统中是否有 Weixin.exe 或 WeChat.exe 进程在运行
+ 返回: status=0 成功, wx_status={is_running: bool, pid: int, ...}
+ """
+ process_name_targets = ["Weixin.exe", "WeChat.exe"]
+
+ wx_status = {
+ "is_running": False,
+ "pid": None,
+ "exe_path": None,
+ "memory_usage_mb": 0.0
+ }
+
+ try:
+ for proc in psutil.process_iter(['pid', 'name', 'exe', 'memory_info']):
+ try:
+ if proc.info['name'] and proc.info['name'] in process_name_targets:
+ wx_status["is_running"] = True
+ wx_status["pid"] = proc.info['pid']
+ wx_status["exe_path"] = proc.info['exe']
+
+ mem = proc.info['memory_info']
+ if mem:
+ wx_status["memory_usage_mb"] = round(mem.rss / (1024 * 1024), 2)
+
+ break
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
+ continue
+
+ return {
+ "status": 0,
+ "errmsg": "ok",
+ "wx_status": wx_status
+ }
+
+ except Exception as e:
+ # 即使出错也返回 JSON,但 status 非 0
+ return {
+ "status": -1,
+ "errmsg": f"检查进程失败: {str(e)}",
+ "wx_status": wx_status
+ }
diff --git a/tools/key_wheels/README.md b/tools/key_wheels/README.md
new file mode 100644
index 0000000..fe0194f
--- /dev/null
+++ b/tools/key_wheels/README.md
@@ -0,0 +1,2 @@
+> 这里放wx_key模块的python预编译wheel:https://github.com/H3CoF6/py_wx_key/releases/
+> 解压放入即可
\ No newline at end of file
diff --git a/tools/key_wheels/wx_key-1.0.0-cp310-cp310-win_amd64.whl b/tools/key_wheels/wx_key-1.0.0-cp310-cp310-win_amd64.whl
new file mode 100644
index 0000000..0276318
Binary files /dev/null and b/tools/key_wheels/wx_key-1.0.0-cp310-cp310-win_amd64.whl differ
diff --git a/tools/key_wheels/wx_key-1.0.0-cp311-cp311-win_amd64.whl b/tools/key_wheels/wx_key-1.0.0-cp311-cp311-win_amd64.whl
new file mode 100644
index 0000000..b286145
Binary files /dev/null and b/tools/key_wheels/wx_key-1.0.0-cp311-cp311-win_amd64.whl differ
diff --git a/tools/key_wheels/wx_key-1.0.0-cp312-cp312-win_amd64.whl b/tools/key_wheels/wx_key-1.0.0-cp312-cp312-win_amd64.whl
new file mode 100644
index 0000000..c049ea0
Binary files /dev/null and b/tools/key_wheels/wx_key-1.0.0-cp312-cp312-win_amd64.whl differ
diff --git a/tools/key_wheels/wx_key-1.0.0-cp313-cp313-win_amd64.whl b/tools/key_wheels/wx_key-1.0.0-cp313-cp313-win_amd64.whl
new file mode 100644
index 0000000..42afeda
Binary files /dev/null and b/tools/key_wheels/wx_key-1.0.0-cp313-cp313-win_amd64.whl differ
diff --git a/tools/key_wheels/wx_key-1.0.0-cp314-cp314-win_amd64.whl b/tools/key_wheels/wx_key-1.0.0-cp314-cp314-win_amd64.whl
new file mode 100644
index 0000000..d19b6f4
Binary files /dev/null and b/tools/key_wheels/wx_key-1.0.0-cp314-cp314-win_amd64.whl differ
diff --git a/uv.lock b/uv.lock
index a3eb323..cc35501 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.11"
[[package]]
@@ -230,6 +230,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
[[package]]
name = "httptools"
version = "0.6.4"
@@ -259,6 +272,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" },
]
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
[[package]]
name = "idna"
version = "3.10"
@@ -844,7 +872,9 @@ dependencies = [
{ name = "aiofiles" },
{ name = "cryptography" },
{ name = "fastapi" },
+ { name = "httpx" },
{ name = "loguru" },
+ { name = "packaging" },
{ name = "pilk" },
{ name = "psutil" },
{ name = "pycryptodome" },
@@ -854,6 +884,7 @@ dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
{ name = "uvicorn", extra = ["standard"] },
+ { name = "wx-key" },
{ name = "zstandard" },
]
@@ -867,7 +898,9 @@ requires-dist = [
{ name = "aiofiles", specifier = ">=23.2.1" },
{ name = "cryptography", specifier = ">=41.0.0" },
{ name = "fastapi", specifier = ">=0.104.0" },
+ { name = "httpx" },
{ name = "loguru", specifier = ">=0.7.0" },
+ { name = "packaging" },
{ name = "pilk", specifier = ">=0.2.4" },
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "pycryptodome", specifier = ">=3.23.0" },
@@ -878,6 +911,7 @@ requires-dist = [
{ name = "requests", specifier = ">=2.32.4" },
{ name = "typing-extensions", specifier = ">=4.8.0" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" },
+ { name = "wx-key" },
{ name = "zstandard", specifier = ">=0.23.0" },
]
provides-extras = ["build"]
@@ -891,6 +925,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
]
+[[package]]
+name = "wx-key"
+version = "1.0.0"
+source = { registry = "tools/key_wheels" }
+wheels = [
+ { path = "wx_key-1.0.0-cp311-cp311-win_amd64.whl" },
+ { path = "wx_key-1.0.0-cp312-cp312-win_amd64.whl" },
+ { path = "wx_key-1.0.0-cp313-cp313-win_amd64.whl" },
+ { path = "wx_key-1.0.0-cp314-cp314-win_amd64.whl" },
+]
+
[[package]]
name = "zstandard"
version = "0.25.0"