mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 14:20:51 +08:00
Merge pull request #24 from H3CoF6/feat/wx-key
feat(wx-key):support get db key and img key without other tools.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ wheels/
|
|||||||
# Local config templates
|
# Local config templates
|
||||||
/wechat_db_config_template.json
|
/wechat_db_config_template.json
|
||||||
.ace-tool/
|
.ace-tool/
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
# Local dev repos and data
|
# Local dev repos and data
|
||||||
/WxDatDecrypt/
|
/WxDatDecrypt/
|
||||||
|
|||||||
@@ -376,6 +376,21 @@ export const useApi = () => {
|
|||||||
return await request(url)
|
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 {
|
return {
|
||||||
detectWechat,
|
detectWechat,
|
||||||
detectCurrentAccount,
|
detectCurrentAccount,
|
||||||
@@ -408,6 +423,9 @@ export const useApi = () => {
|
|||||||
exportChatContacts,
|
exportChatContacts,
|
||||||
getWrappedAnnual,
|
getWrappedAnnual,
|
||||||
getWrappedAnnualMeta,
|
getWrappedAnnualMeta,
|
||||||
getWrappedAnnualCard
|
getWrappedAnnualCard,
|
||||||
|
getDbKey,
|
||||||
|
getImageKey,
|
||||||
|
getWxStatus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,8 @@
|
|||||||
"ogl": "^1.0.11",
|
"ogl": "^1.0.11",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tailwindcss": "3.4.17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,24 +26,40 @@
|
|||||||
<!-- 密钥输入 -->
|
<!-- 密钥输入 -->
|
||||||
<div>
|
<div>
|
||||||
<label for="key" class="block text-sm font-medium text-[#000000e6] mb-2">
|
<label for="key" class="block text-sm font-medium text-[#000000e6] mb-2">
|
||||||
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
|
||||||
</svg>
|
|
||||||
解密密钥 <span class="text-red-500">*</span>
|
解密密钥 <span class="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
|
||||||
<input
|
<div class="flex gap-3">
|
||||||
id="key"
|
<div class="relative flex-1">
|
||||||
v-model="formData.key"
|
<input
|
||||||
type="text"
|
id="key"
|
||||||
placeholder="请输入64位十六进制密钥"
|
v-model="formData.key"
|
||||||
class="w-full px-4 py-3 bg-white border border-[#EDEDED] rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200"
|
type="text"
|
||||||
:class="{ 'border-red-500': formErrors.key }"
|
placeholder="请输入64位十六进制密钥"
|
||||||
required
|
class="w-full px-4 py-3 bg-white border border-[#EDEDED] rounded-lg font-mono text-sm focus:outline-none focus:ring-2 focus:ring-[#07C160] focus:border-transparent transition-all duration-200"
|
||||||
/>
|
:class="{ 'border-red-500': formErrors.key }"
|
||||||
<div v-if="formData.key" class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
required
|
||||||
<span class="text-xs text-[#7F7F7F]">{{ formData.key.length }}/64</span>
|
/>
|
||||||
|
<div v-if="formData.key" class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||||
|
<span class="text-xs text-[#7F7F7F]">{{ formData.key.length }}/64</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleGetDbKey"
|
||||||
|
:disabled="isGettingDbKey"
|
||||||
|
class="flex-none inline-flex items-center px-4 py-3 bg-[#07C160] text-white rounded-lg text-sm font-medium hover:bg-[#06AD56] transition-all duration-200 disabled:opacity-50 disabled:cursor-wait whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<svg v-if="isGettingDbKey" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
{{ isGettingDbKey ? '获取中...' : '自动获取' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="formErrors.key" class="mt-1 text-sm text-red-600 flex items-center">
|
<p v-if="formErrors.key" class="mt-1 text-sm text-red-600 flex items-center">
|
||||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -55,7 +71,7 @@
|
|||||||
<svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 等工具获取的64位十六进制字符串
|
尝试自动获取,或者使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 等工具获取的64位十六进制字符串
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -131,6 +147,26 @@
|
|||||||
<!-- 填写密钥 -->
|
<!-- 填写密钥 -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="bg-gray-50 rounded-lg p-4">
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center mb-4 pb-3 border-b border-gray-200">
|
||||||
|
<span class="text-sm font-medium text-gray-500">手动输入或通过微信获取</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleGetImageKey"
|
||||||
|
:disabled="isGettingImageKey"
|
||||||
|
class="flex-none inline-flex items-center px-4 py-3 bg-[#07C160] text-white rounded-lg text-sm font-medium hover:bg-[#06AD56] transition-all duration-200 disabled:opacity-50 disabled:cursor-wait whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<svg v-if="isGettingImageKey" class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
{{ isGettingImageKey ? '正在获取...' : '自动获取' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[#000000e6] mb-2">XOR(必填)</label>
|
<label class="block text-sm font-medium text-[#000000e6] mb-2">XOR(必填)</label>
|
||||||
@@ -158,7 +194,7 @@
|
|||||||
<svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 mr-1 text-[#10AEEF]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 获取图片密钥;AES 可选(V4-V2 需要)
|
尝试自动获取,或使用 <a href="https://github.com/ycccccccy/wx_key" target="_blank" class="text-[#07C160] hover:text-[#06AD56]">wx_key</a> 获取图片密钥;AES 可选(V4-V2 需要)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -326,6 +362,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 警告渲染 -->
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="warning" class="bg-amber-50 border border-amber-200 rounded-lg p-4 mt-6 flex items-start">
|
||||||
|
<svg class="h-5 w-5 mr-2 flex-shrink-0 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-amber-800">温馨提示</p>
|
||||||
|
<p class="text-sm mt-1 text-amber-700">{{ warning }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
<!-- 错误提示 -->
|
<!-- 错误提示 -->
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="error" class="bg-red-50 border border-red-200 rounded-lg p-4 mt-6 animate-shake flex items-start">
|
<div v-if="error" class="bg-red-50 border border-red-200 rounded-lg p-4 mt-6 animate-shake flex items-start">
|
||||||
@@ -367,12 +416,15 @@
|
|||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useApi } from '~/composables/useApi'
|
import { useApi } from '~/composables/useApi'
|
||||||
|
|
||||||
const { decryptDatabase, saveMediaKeys, getSavedKeys } = useApi()
|
const { decryptDatabase, saveMediaKeys, getSavedKeys, getDbKey, getImageKey, getWxStatus } = useApi()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
const warning = ref('') // 警告,用于密钥提示
|
||||||
const currentStep = ref(0)
|
const currentStep = ref(0)
|
||||||
const mediaAccount = ref('')
|
const mediaAccount = ref('')
|
||||||
|
const isGettingDbKey = ref(false)
|
||||||
|
const isGettingImageKey = ref(false)
|
||||||
|
|
||||||
// 步骤定义
|
// 步骤定义
|
||||||
const steps = [
|
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 = () => {
|
const applyManualKeys = () => {
|
||||||
manualKeyErrors.xor_key = ''
|
manualKeyErrors.xor_key = ''
|
||||||
manualKeyErrors.aes_key = ''
|
manualKeyErrors.aes_key = ''
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
warning.value = ''
|
||||||
|
|
||||||
const aes = normalizeAesKey(manualKeys.aes_key)
|
const aes = normalizeAesKey(manualKeys.aes_key)
|
||||||
if (!aes.ok) {
|
if (!aes.ok) {
|
||||||
@@ -550,6 +681,7 @@ const handleDecrypt = async () => {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
warning.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await decryptDatabase({
|
const result = await decryptDatabase({
|
||||||
@@ -596,6 +728,7 @@ const decryptAllImages = async () => {
|
|||||||
mediaDecrypting.value = true
|
mediaDecrypting.value = true
|
||||||
mediaDecryptResult.value = null
|
mediaDecryptResult.value = null
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
warning.value = ''
|
||||||
|
|
||||||
// 重置进度
|
// 重置进度
|
||||||
decryptProgress.current = 0
|
decryptProgress.current = 0
|
||||||
@@ -671,6 +804,7 @@ const decryptAllImages = async () => {
|
|||||||
// 从密钥步骤进入图片解密步骤
|
// 从密钥步骤进入图片解密步骤
|
||||||
const goToMediaDecryptStep = async () => {
|
const goToMediaDecryptStep = async () => {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
warning.value = ''
|
||||||
// 校验并应用(未填写则允许直接进入,后端会使用已保存密钥或报错提示)
|
// 校验并应用(未填写则允许直接进入,后端会使用已保存密钥或报错提示)
|
||||||
const ok = applyManualKeys()
|
const ok = applyManualKeys()
|
||||||
if (!ok || manualKeyErrors.xor_key || manualKeyErrors.aes_key) return
|
if (!ok || manualKeyErrors.xor_key || manualKeyErrors.aes_key) return
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ dependencies = [
|
|||||||
"zstandard>=0.23.0",
|
"zstandard>=0.23.0",
|
||||||
"pilk>=0.2.4",
|
"pilk>=0.2.4",
|
||||||
"pypinyin>=0.53.0",
|
"pypinyin>=0.53.0",
|
||||||
|
"wx_key",
|
||||||
|
"packaging",
|
||||||
|
"httpx",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -40,3 +43,6 @@ include = [
|
|||||||
"src/wechat_decrypt_tool/native/wcdb_api.dll",
|
"src/wechat_decrypt_tool/native/wcdb_api.dll",
|
||||||
"src/wechat_decrypt_tool/native/WCDB.dll",
|
"src/wechat_decrypt_tool/native/WCDB.dll",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
find-links = ["./tools/key_wheels/"]
|
||||||
|
|||||||
357
src/wechat_decrypt_tool/key_service.py
Normal file
357
src/wechat_decrypt_tool/key_service.py
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@ from typing import Optional
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from ..key_store import get_account_keys_from_store
|
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 ..media_helpers import _load_media_keys, _resolve_account_dir
|
||||||
from ..path_fix import PathFixRoute
|
from ..path_fix import PathFixRoute
|
||||||
|
|
||||||
@@ -51,3 +52,76 @@ async def get_saved_keys(account: Optional[str] = None):
|
|||||||
"keys": result,
|
"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": {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import psutil
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from ..logging_config import get_logger
|
from ..logging_config import get_logger
|
||||||
@@ -71,3 +71,49 @@ async def detect_current_account(data_root_path: Optional[str] = None):
|
|||||||
'error': str(e),
|
'error': str(e),
|
||||||
'data': None,
|
'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
|
||||||
|
}
|
||||||
|
|||||||
2
tools/key_wheels/README.md
Normal file
2
tools/key_wheels/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
> 这里放wx_key模块的python预编译wheel:https://github.com/H3CoF6/py_wx_key/releases/
|
||||||
|
> 解压放入即可
|
||||||
BIN
tools/key_wheels/wx_key-1.0.0-cp310-cp310-win_amd64.whl
Normal file
BIN
tools/key_wheels/wx_key-1.0.0-cp310-cp310-win_amd64.whl
Normal file
Binary file not shown.
BIN
tools/key_wheels/wx_key-1.0.0-cp311-cp311-win_amd64.whl
Normal file
BIN
tools/key_wheels/wx_key-1.0.0-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
BIN
tools/key_wheels/wx_key-1.0.0-cp312-cp312-win_amd64.whl
Normal file
BIN
tools/key_wheels/wx_key-1.0.0-cp312-cp312-win_amd64.whl
Normal file
Binary file not shown.
BIN
tools/key_wheels/wx_key-1.0.0-cp313-cp313-win_amd64.whl
Normal file
BIN
tools/key_wheels/wx_key-1.0.0-cp313-cp313-win_amd64.whl
Normal file
Binary file not shown.
BIN
tools/key_wheels/wx_key-1.0.0-cp314-cp314-win_amd64.whl
Normal file
BIN
tools/key_wheels/wx_key-1.0.0-cp314-cp314-win_amd64.whl
Normal file
Binary file not shown.
47
uv.lock
generated
47
uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
[[package]]
|
[[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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "httptools"
|
name = "httptools"
|
||||||
version = "0.6.4"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.10"
|
version = "3.10"
|
||||||
@@ -844,7 +872,9 @@ dependencies = [
|
|||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
{ name = "httpx" },
|
||||||
{ name = "loguru" },
|
{ name = "loguru" },
|
||||||
|
{ name = "packaging" },
|
||||||
{ name = "pilk" },
|
{ name = "pilk" },
|
||||||
{ name = "psutil" },
|
{ name = "psutil" },
|
||||||
{ name = "pycryptodome" },
|
{ name = "pycryptodome" },
|
||||||
@@ -854,6 +884,7 @@ dependencies = [
|
|||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
|
{ name = "wx-key" },
|
||||||
{ name = "zstandard" },
|
{ name = "zstandard" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -867,7 +898,9 @@ requires-dist = [
|
|||||||
{ name = "aiofiles", specifier = ">=23.2.1" },
|
{ name = "aiofiles", specifier = ">=23.2.1" },
|
||||||
{ name = "cryptography", specifier = ">=41.0.0" },
|
{ name = "cryptography", specifier = ">=41.0.0" },
|
||||||
{ name = "fastapi", specifier = ">=0.104.0" },
|
{ name = "fastapi", specifier = ">=0.104.0" },
|
||||||
|
{ name = "httpx" },
|
||||||
{ name = "loguru", specifier = ">=0.7.0" },
|
{ name = "loguru", specifier = ">=0.7.0" },
|
||||||
|
{ name = "packaging" },
|
||||||
{ name = "pilk", specifier = ">=0.2.4" },
|
{ name = "pilk", specifier = ">=0.2.4" },
|
||||||
{ name = "psutil", specifier = ">=7.0.0" },
|
{ name = "psutil", specifier = ">=7.0.0" },
|
||||||
{ name = "pycryptodome", specifier = ">=3.23.0" },
|
{ name = "pycryptodome", specifier = ">=3.23.0" },
|
||||||
@@ -878,6 +911,7 @@ requires-dist = [
|
|||||||
{ name = "requests", specifier = ">=2.32.4" },
|
{ name = "requests", specifier = ">=2.32.4" },
|
||||||
{ name = "typing-extensions", specifier = ">=4.8.0" },
|
{ name = "typing-extensions", specifier = ">=4.8.0" },
|
||||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" },
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" },
|
||||||
|
{ name = "wx-key" },
|
||||||
{ name = "zstandard", specifier = ">=0.23.0" },
|
{ name = "zstandard", specifier = ">=0.23.0" },
|
||||||
]
|
]
|
||||||
provides-extras = ["build"]
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "zstandard"
|
name = "zstandard"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user