Skip to content

Commit 5d9fced

Browse files
committed
feat(decrypt): 解密支持 SSE 实时进度
- 新增 /api/decrypt_stream(GET + SSE):扫描 db_storage,逐库解密并推送 start/progress/complete/error - 前端解密页优先使用 SSE 展示实时进度,不支持时回退到原 POST(无进度) - 增加流式接口单测:验证事件序列与输出落盘
1 parent a14f8de commit 5d9fced

3 files changed

Lines changed: 570 additions & 29 deletions

File tree

frontend/pages/decrypt.vue

Lines changed: 197 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,40 @@
125125
</button>
126126
</div>
127127
</div>
128+
129+
<!-- 解密进度 -->
130+
<div v-if="loading || dbDecryptProgress.total > 0" class="mt-6">
131+
<div class="flex items-center justify-between mb-2">
132+
<div class="text-sm text-[#7F7F7F]">
133+
{{ dbDecryptProgress.message || (loading ? '解密中...' : '') }}
134+
</div>
135+
<div v-if="dbDecryptProgress.total > 0" class="text-sm font-mono text-[#000000e6]">
136+
{{ dbDecryptProgress.current }} / {{ dbDecryptProgress.total }}
137+
</div>
138+
</div>
139+
140+
<div class="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
141+
<div
142+
class="h-full bg-[#07C160] transition-all duration-300"
143+
:style="{ width: dbProgressPercent + '%' }"
144+
></div>
145+
</div>
146+
147+
<div v-if="dbDecryptProgress.current_file" class="mt-2 text-xs text-[#7F7F7F] truncate font-mono">
148+
{{ dbDecryptProgress.current_file }}
149+
</div>
150+
151+
<div v-if="dbDecryptProgress.total > 0" class="mt-3 grid grid-cols-2 gap-4 text-center">
152+
<div class="bg-gray-50 rounded-lg p-3">
153+
<div class="text-lg font-bold text-[#07C160]">{{ dbDecryptProgress.success_count }}</div>
154+
<div class="text-xs text-[#7F7F7F]">成功</div>
155+
</div>
156+
<div class="bg-gray-50 rounded-lg p-3">
157+
<div class="text-lg font-bold text-[#FA5151]">{{ dbDecryptProgress.fail_count }}</div>
158+
<div class="text-xs text-[#7F7F7F]">失败</div>
159+
</div>
160+
</div>
161+
</div>
128162
</form>
129163
</div>
130164
</div>
@@ -413,7 +447,7 @@
413447
</style>
414448

415449
<script setup>
416-
import { ref, reactive, computed, onMounted } from 'vue'
450+
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue'
417451
import { useApi } from '~/composables/useApi'
418452
419453
const { decryptDatabase, saveMediaKeys, getSavedKeys, getDbKey, getImageKey, getWxStatus } = useApi()
@@ -625,6 +659,22 @@ const clearManualKeys = () => {
625659
const mediaDecryptResult = ref(null)
626660
const mediaDecrypting = ref(false)
627661
662+
// 数据库解密进度(SSE)
663+
const dbDecryptProgress = reactive({
664+
current: 0,
665+
total: 0,
666+
success_count: 0,
667+
fail_count: 0,
668+
current_file: '',
669+
status: '',
670+
message: ''
671+
})
672+
673+
const dbProgressPercent = computed(() => {
674+
if (dbDecryptProgress.total === 0) return 0
675+
return Math.round((dbDecryptProgress.current / dbDecryptProgress.total) * 100)
676+
})
677+
628678
// 实时解密进度
629679
const decryptProgress = reactive({
630680
current: 0,
@@ -673,6 +723,27 @@ const validateForm = () => {
673723
return isValid
674724
}
675725
726+
let dbDecryptEventSource = null
727+
onBeforeUnmount(() => {
728+
try {
729+
if (dbDecryptEventSource) dbDecryptEventSource.close()
730+
} catch (e) {
731+
// ignore
732+
} finally {
733+
dbDecryptEventSource = null
734+
}
735+
})
736+
737+
const resetDbDecryptProgress = () => {
738+
dbDecryptProgress.current = 0
739+
dbDecryptProgress.total = 0
740+
dbDecryptProgress.success_count = 0
741+
dbDecryptProgress.fail_count = 0
742+
dbDecryptProgress.current_file = ''
743+
dbDecryptProgress.status = ''
744+
dbDecryptProgress.message = ''
745+
}
746+
676747
// 处理解密
677748
const handleDecrypt = async () => {
678749
if (!validateForm()) {
@@ -682,43 +753,142 @@ const handleDecrypt = async () => {
682753
loading.value = true
683754
error.value = ''
684755
warning.value = ''
756+
757+
resetDbDecryptProgress()
685758
686759
try {
687-
const result = await decryptDatabase({
688-
key: formData.key,
689-
db_storage_path: formData.db_storage_path
690-
})
691-
692-
if (result.status === 'completed') {
693-
// 解密成功,保存结果并进入下一步
694-
decryptResult.value = result
695-
if (process.client && typeof window !== 'undefined') {
696-
sessionStorage.setItem('decryptResult', JSON.stringify(result))
760+
const canSse = process.client && typeof window !== 'undefined' && typeof EventSource !== 'undefined'
761+
762+
// Fallback: 如果环境不支持 SSE,则使用普通 POST(无进度)。
763+
if (!canSse) {
764+
const result = await decryptDatabase({
765+
key: formData.key,
766+
db_storage_path: formData.db_storage_path
767+
})
768+
769+
if (result.status === 'completed') {
770+
decryptResult.value = result
771+
if (process.client && typeof window !== 'undefined') {
772+
sessionStorage.setItem('decryptResult', JSON.stringify(result))
773+
}
774+
try {
775+
const accounts = Object.keys(result.account_results || {})
776+
if (accounts.length > 0) mediaAccount.value = accounts[0]
777+
} catch (e) {}
778+
779+
clearManualKeys()
780+
currentStep.value = 1
781+
await prefillKeysForAccount(mediaAccount.value)
782+
} else if (result.status === 'failed') {
783+
if (result.failure_count > 0 && result.success_count === 0) {
784+
error.value = result.message || '所有文件解密失败'
785+
} else {
786+
error.value = '部分文件解密失败,请检查密钥是否正确'
787+
}
788+
} else {
789+
error.value = result.message || '解密失败,请检查输入信息'
697790
}
698-
// 记录当前账号(用于图片解密/密钥保存)
791+
792+
loading.value = false
793+
return
794+
}
795+
796+
// SSE: 解密过程实时推送进度
797+
if (dbDecryptEventSource) {
798+
try {
799+
dbDecryptEventSource.close()
800+
} catch (e) {}
801+
dbDecryptEventSource = null
802+
}
803+
804+
const params = new URLSearchParams()
805+
params.set('key', formData.key)
806+
params.set('db_storage_path', formData.db_storage_path)
807+
const url = `http://localhost:8000/api/decrypt_stream?${params.toString()}`
808+
809+
dbDecryptProgress.message = '连接中...'
810+
const eventSource = new EventSource(url)
811+
dbDecryptEventSource = eventSource
812+
813+
eventSource.onmessage = async (event) => {
699814
try {
700-
const accounts = Object.keys(result.account_results || {})
701-
if (accounts.length > 0) mediaAccount.value = accounts[0]
815+
const data = JSON.parse(event.data)
816+
817+
if (data.type === 'scanning') {
818+
dbDecryptProgress.message = data.message || '正在扫描数据库文件...'
819+
} else if (data.type === 'start') {
820+
dbDecryptProgress.total = data.total || 0
821+
dbDecryptProgress.message = data.message || '开始解密...'
822+
} else if (data.type === 'progress') {
823+
dbDecryptProgress.current = data.current || 0
824+
dbDecryptProgress.total = data.total || 0
825+
dbDecryptProgress.success_count = data.success_count || 0
826+
dbDecryptProgress.fail_count = data.fail_count || 0
827+
dbDecryptProgress.current_file = data.current_file || ''
828+
dbDecryptProgress.status = data.status || ''
829+
dbDecryptProgress.message = data.message || ''
830+
} else if (data.type === 'phase') {
831+
// e.g. building cache
832+
dbDecryptProgress.message = data.message || ''
833+
} else if (data.type === 'complete') {
834+
dbDecryptProgress.status = 'complete'
835+
dbDecryptProgress.current = data.total_databases || dbDecryptProgress.total
836+
dbDecryptProgress.total = data.total_databases || dbDecryptProgress.total
837+
dbDecryptProgress.success_count = data.success_count || 0
838+
dbDecryptProgress.fail_count = data.failure_count || 0
839+
dbDecryptProgress.message = data.message || '解密完成'
840+
841+
decryptResult.value = data
842+
if (process.client && typeof window !== 'undefined') {
843+
sessionStorage.setItem('decryptResult', JSON.stringify(data))
844+
}
845+
846+
try {
847+
const accounts = Object.keys(data.account_results || {})
848+
if (accounts.length > 0) mediaAccount.value = accounts[0]
849+
} catch (e) {}
850+
851+
try {
852+
eventSource.close()
853+
} catch (e) {}
854+
dbDecryptEventSource = null
855+
loading.value = false
856+
857+
if (data.status === 'completed') {
858+
clearManualKeys()
859+
currentStep.value = 1
860+
await prefillKeysForAccount(mediaAccount.value)
861+
} else if (data.status === 'failed') {
862+
error.value = data.message || '所有文件解密失败'
863+
} else {
864+
error.value = data.message || '解密失败,请检查输入信息'
865+
}
866+
} else if (data.type === 'error') {
867+
error.value = data.message || '解密失败,请检查输入信息'
868+
try {
869+
eventSource.close()
870+
} catch (e) {}
871+
dbDecryptEventSource = null
872+
loading.value = false
873+
}
702874
} catch (e) {
703-
// ignore
875+
console.error('解析SSE消息失败:', e)
704876
}
877+
}
705878
706-
// 进入图片密钥填写步骤
707-
clearManualKeys()
708-
currentStep.value = 1
709-
await prefillKeysForAccount(mediaAccount.value)
710-
} else if (result.status === 'failed') {
711-
if (result.failure_count > 0 && result.success_count === 0) {
712-
error.value = result.message || '所有文件解密失败'
713-
} else {
714-
error.value = '部分文件解密失败,请检查密钥是否正确'
879+
eventSource.onerror = (e) => {
880+
console.error('SSE连接错误:', e)
881+
try {
882+
eventSource.close()
883+
} catch (err) {}
884+
dbDecryptEventSource = null
885+
if (loading.value) {
886+
error.value = 'SSE连接中断,请重试'
887+
loading.value = false
715888
}
716-
} else {
717-
error.value = result.message || '解密失败,请检查输入信息'
718889
}
719890
} catch (err) {
720891
error.value = err.message || '解密过程中发生错误'
721-
} finally {
722892
loading.value = false
723893
}
724894
}

0 commit comments

Comments
 (0)