Skip to content

Commit a6a8d63

Browse files
committed
feat: 添加媒体解密进度取消功能,优化检测逻辑
1 parent 27d6381 commit a6a8d63

6 files changed

Lines changed: 351 additions & 67 deletions

File tree

frontend/pages/decrypt.vue

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@
272272
<div class="w-full bg-gray-200 rounded-full h-2.5 overflow-hidden">
273273
<div
274274
class="h-2.5 rounded-full transition-all duration-300 ease-out"
275-
:class="decryptProgress.status === 'complete' ? 'bg-[#07C160]' : 'bg-[#91D300]'"
275+
:class="decryptProgress.status === 'complete' ? 'bg-[#07C160]' : decryptProgress.status === 'cancelled' ? 'bg-[#FAAD14]' : 'bg-[#91D300]'"
276276
:style="{ width: progressPercent + '%' }"
277277
></div>
278278
</div>
@@ -366,6 +366,13 @@
366366
</svg>
367367
{{ mediaDecrypting ? '解密中...' : (mediaDecryptResult ? '重新解密' : '开始解密图片') }}
368368
</button>
369+
<button
370+
v-if="mediaDecrypting"
371+
@click="cancelMediaDecrypt"
372+
class="inline-flex items-center px-6 py-3 bg-[#FA5151] text-white rounded-lg font-medium hover:bg-[#E54D4D] transition-all duration-200"
373+
>
374+
停止解密
375+
</button>
369376
<button
370377
@click="skipToChat"
371378
:disabled="mediaDecrypting"
@@ -671,6 +678,18 @@ const validateForm = () => {
671678
}
672679
673680
let dbDecryptEventSource = null
681+
let mediaDecryptEventSource = null
682+
683+
const closeMediaDecryptEventSource = () => {
684+
try {
685+
if (mediaDecryptEventSource) mediaDecryptEventSource.close()
686+
} catch (e) {
687+
// ignore
688+
} finally {
689+
mediaDecryptEventSource = null
690+
}
691+
}
692+
674693
onBeforeUnmount(() => {
675694
try {
676695
if (dbDecryptEventSource) dbDecryptEventSource.close()
@@ -679,6 +698,8 @@ onBeforeUnmount(() => {
679698
} finally {
680699
dbDecryptEventSource = null
681700
}
701+
702+
closeMediaDecryptEventSource()
682703
})
683704
684705
const resetDbDecryptProgress = () => {
@@ -691,6 +712,17 @@ const resetDbDecryptProgress = () => {
691712
dbDecryptProgress.message = ''
692713
}
693714
715+
const resetMediaDecryptProgress = () => {
716+
decryptProgress.current = 0
717+
decryptProgress.total = 0
718+
decryptProgress.success_count = 0
719+
decryptProgress.skip_count = 0
720+
decryptProgress.fail_count = 0
721+
decryptProgress.current_file = ''
722+
decryptProgress.fileStatus = ''
723+
decryptProgress.status = ''
724+
}
725+
694726
// 处理解密
695727
const handleDecrypt = async () => {
696728
if (!validateForm()) {
@@ -877,20 +909,14 @@ const handleDecrypt = async () => {
877909
878910
// 批量解密所有图片(使用SSE实时进度)
879911
const decryptAllImages = async () => {
912+
closeMediaDecryptEventSource()
880913
mediaDecrypting.value = true
881914
mediaDecryptResult.value = null
882915
error.value = ''
883916
warning.value = ''
884917
885918
// 重置进度
886-
decryptProgress.current = 0
887-
decryptProgress.total = 0
888-
decryptProgress.success_count = 0
889-
decryptProgress.skip_count = 0
890-
decryptProgress.fail_count = 0
891-
decryptProgress.current_file = ''
892-
decryptProgress.fileStatus = ''
893-
decryptProgress.status = ''
919+
resetMediaDecryptProgress()
894920
895921
try {
896922
// 构建SSE URL
@@ -903,8 +929,11 @@ const decryptAllImages = async () => {
903929
904930
// 使用EventSource接收SSE
905931
const eventSource = new EventSource(url)
932+
mediaDecryptEventSource = eventSource
906933
907934
eventSource.onmessage = (event) => {
935+
if (mediaDecryptEventSource !== eventSource) return
936+
908937
try {
909938
const data = JSON.parse(event.data)
910939
@@ -928,21 +957,23 @@ const decryptAllImages = async () => {
928957
decryptProgress.skip_count = data.skip_count
929958
decryptProgress.fail_count = data.fail_count
930959
mediaDecryptResult.value = data
931-
eventSource.close()
932960
mediaDecrypting.value = false
961+
closeMediaDecryptEventSource()
933962
} else if (data.type === 'error') {
934963
error.value = data.message
935-
eventSource.close()
936964
mediaDecrypting.value = false
965+
closeMediaDecryptEventSource()
937966
}
938967
} catch (e) {
939968
console.error('解析SSE消息失败:', e)
940969
}
941970
}
942971
943972
eventSource.onerror = (e) => {
973+
if (mediaDecryptEventSource !== eventSource) return
974+
944975
console.error('SSE连接错误:', e)
945-
eventSource.close()
976+
closeMediaDecryptEventSource()
946977
if (mediaDecrypting.value) {
947978
error.value = 'SSE连接中断,请重试'
948979
mediaDecrypting.value = false
@@ -951,9 +982,19 @@ const decryptAllImages = async () => {
951982
} catch (err) {
952983
error.value = err.message || '图片解密过程中发生错误'
953984
mediaDecrypting.value = false
985+
closeMediaDecryptEventSource()
954986
}
955987
}
956988
989+
const cancelMediaDecrypt = () => {
990+
if (!mediaDecrypting.value) return
991+
992+
decryptProgress.status = 'cancelled'
993+
mediaDecrypting.value = false
994+
warning.value = '已停止图片解密,已完成的图片会保留。'
995+
closeMediaDecryptEventSource()
996+
}
997+
957998
// 从密钥步骤进入图片解密步骤
958999
const goToMediaDecryptStep = async () => {
9591000
error.value = ''

frontend/pages/detection-result.vue

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,19 @@
4141
<svg v-if="!loading" class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4242
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
4343
</svg>
44-
<svg v-else class="animate-spin w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24">
45-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
46-
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
44+
<svg v-else class="w-4 h-4 mr-2 animate-spin" fill="none" viewBox="0 0 48 48" aria-hidden="true">
45+
<circle class="opacity-20" cx="24" cy="24" r="18" stroke="currentColor" stroke-width="6"></circle>
46+
<circle
47+
cx="24"
48+
cy="24"
49+
r="18"
50+
stroke="currentColor"
51+
stroke-width="6"
52+
stroke-linecap="round"
53+
stroke-dasharray="28 72"
54+
pathLength="100"
55+
transform="rotate(-90 24 24)"
56+
></circle>
4757
</svg>
4858
{{ loading ? '检测中...' : '手动选择目录检测' }}
4959
</button>
@@ -53,9 +63,19 @@
5363
<div>
5464
<!-- 检测中状态 -->
5565
<div v-if="loading" class="absolute inset-0 bg-white/80 backdrop-blur-sm z-20 rounded-2xl flex flex-col items-center justify-center border border-[#EDEDED]">
56-
<svg class="w-16 h-16 animate-spin text-[#07C160]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
57-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
58-
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
66+
<svg class="w-16 h-16 animate-spin text-[#07C160]" fill="none" viewBox="0 0 48 48" aria-hidden="true">
67+
<circle class="opacity-20" cx="24" cy="24" r="18" stroke="currentColor" stroke-width="6"></circle>
68+
<circle
69+
cx="24"
70+
cy="24"
71+
r="18"
72+
stroke="currentColor"
73+
stroke-width="6"
74+
stroke-linecap="round"
75+
stroke-dasharray="28 72"
76+
pathLength="100"
77+
transform="rotate(-90 24 24)"
78+
></circle>
5979
</svg>
6080
<p class="mt-4 text-lg text-[#7F7F7F]">正在检测微信数据...</p>
6181
</div>
@@ -395,4 +415,4 @@ onMounted(() => {
395415
}
396416
startDetection()
397417
})
398-
</script>
418+
</script>

src/wechat_decrypt_tool/routers/media.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import json
33
from typing import Optional
44

5-
from fastapi import APIRouter, HTTPException
5+
from fastapi import APIRouter, HTTPException, Request
66
from fastapi.responses import Response, StreamingResponse
77
from pydantic import BaseModel, Field
88

@@ -226,6 +226,7 @@ async def get_decrypted_resource(md5: str, account: Optional[str] = None):
226226

227227
@router.get("/api/media/decrypt_all_stream", summary="批量解密所有图片资源(SSE实时进度)")
228228
async def decrypt_all_media_stream(
229+
request: Request,
229230
account: Optional[str] = None,
230231
xor_key: Optional[str] = None,
231232
aes_key: Optional[str] = None,
@@ -252,8 +253,18 @@ async def decrypt_all_media_stream(
252253
- 解密后非有效图片格式
253254
"""
254255

256+
async def is_client_disconnected() -> bool:
257+
try:
258+
return await request.is_disconnected()
259+
except Exception:
260+
return False
261+
255262
async def generate_progress():
256263
try:
264+
if await is_client_disconnected():
265+
logger.info("[SSE] 客户端已断开,取消图片解密任务")
266+
return
267+
257268
account_dir = _resolve_account_dir(account)
258269
wxid_dir = _resolve_account_wxid_dir(account_dir)
259270

@@ -301,6 +312,10 @@ async def generate_progress():
301312
total_files = len(dat_files)
302313
logger.info(f"[SSE] 共发现 {total_files} 个.dat文件(仅图片)")
303314

315+
if await is_client_disconnected():
316+
logger.info("[SSE] 扫描完成后客户端已断开,停止图片解密任务")
317+
return
318+
304319
if total_files == 0:
305320
yield f"data: {json.dumps({'type': 'complete', 'message': '未发现需要解密的图片文件', 'total': 0, 'success_count': 0, 'skip_count': 0, 'fail_count': 0})}\n\n"
306321
return
@@ -319,6 +334,10 @@ async def generate_progress():
319334
resource_dir.mkdir(parents=True, exist_ok=True)
320335

321336
for i, (dat_path, md5) in enumerate(dat_files):
337+
if await is_client_disconnected():
338+
logger.info("[SSE] 客户端已断开,停止图片解密任务")
339+
return
340+
322341
current = i + 1
323342
file_name = dat_path.name
324343

0 commit comments

Comments
 (0)