From d3a156bef8ab87422faf6923d52d40c5eb7bbbe7 Mon Sep 17 00:00:00 2001 From: nap0o Date: Mon, 16 Mar 2026 16:51:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20Proton=20Pass,=20D?= =?UTF-8?q?ashlane=20Pass=20=E5=A4=87=E4=BB=BD=E6=95=B0=E6=8D=AE=E5=AF=BC?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 3 +- .../app/styles/modules/components/vault.css | 26 ++++----- .../migration/composables/useDataImport.js | 2 +- .../migration/service/dataMigrationService.js | 28 ++++++++-- .../features/migration/views/dataImport.vue | 11 +++- frontend/src/locales/en-US.json | 9 +-- frontend/src/locales/zh-CN.json | 9 +-- .../components/icons/iconDashlanePass.vue | 8 +++ .../components/icons/iconProtonPass.vue | 9 +++ frontend/src/shared/utils/asyncHelper.js | 39 ++++++++++--- .../shared/utils/serializers/csvStrategy.js | 26 ++++++++- ...rotonStrategy.js => protonAuthStrategy.js} | 17 +++--- .../utils/serializers/protonPassStrategy.js | 55 +++++++++++++++++++ frontend/vite.config.js | 8 ++- 14 files changed, 197 insertions(+), 53 deletions(-) create mode 100644 frontend/src/shared/components/icons/iconDashlanePass.vue create mode 100644 frontend/src/shared/components/icons/iconProtonPass.vue rename frontend/src/shared/utils/serializers/{protonStrategy.js => protonAuthStrategy.js} (89%) create mode 100644 frontend/src/shared/utils/serializers/protonPassStrategy.js diff --git a/frontend/package.json b/frontend/package.json index e74fe48..fb5a612 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "jspdf": "^4.2.0", "jsqr": "^1.4.0", "libsodium-wrappers-sumo": "^0.8.2", + "openpgp": "^6.3.0", "pinia": "^3.0.4", "qrcode": "^1.5.4", "scrypt-js": "^3.0.1", @@ -44,4 +45,4 @@ "overrides": { "serialize-javascript": "^7.0.3" } -} +} \ No newline at end of file diff --git a/frontend/src/app/styles/modules/components/vault.css b/frontend/src/app/styles/modules/components/vault.css index 03d0ce3..af12b80 100644 --- a/frontend/src/app/styles/modules/components/vault.css +++ b/frontend/src/app/styles/modules/components/vault.css @@ -377,41 +377,35 @@ background: var(--el-fill-color-light); color: var(--el-text-color-regular); padding: 3px 10px 4px 10px; - border-radius: 4px; + border-radius: 3px; font-size: 13px; white-space: nowrap; display: flex; align-items: center; justify-content: center; - transition: all 0.2s; - border: 1px solid transparent; + border: 1px solid var(--el-fill-color-light); } .category-tag:hover { background: var(--el-fill-color); } -/* Final Choice: Option 1 - Subtle Glassmorphism (毛玻璃高亮) */ +/* 分类 Chips 选中状态对齐 .vault-card.is-selected */ .category-tag.is-active { - background-color: var(--el-color-primary-light-9) !important; + background-color: var(--vault-selected-bg) !important; + border: 1px solid #409EFF !important; color: var(--el-color-primary) !important; - border-color: var(--el-color-primary-light-5) !important; -} - -/* 适配深色模式下的透明度感 */ -html.dark .category-tag.is-active { - background-color: rgba(64, 158, 255, 0.15) !important; - border-color: rgba(64, 158, 255, 0.4) !important; - color: var(--el-color-primary-light-3) !important; } .category-tag.is-active .tag-count { color: var(--el-color-primary) !important; - opacity: 0.7; + opacity: 0.9; } -html.dark .category-tag.is-active .tag-count { - color: var(--el-color-primary-light-3) !important; +/* 适配深色模式下的透明度感 */ +html.dark .category-tag.is-active { + border: none !important; + background-color: rgba(64, 158, 255, 0.2) !important; } .tag-count { diff --git a/frontend/src/features/migration/composables/useDataImport.js b/frontend/src/features/migration/composables/useDataImport.js index c18269e..ba41095 100644 --- a/frontend/src/features/migration/composables/useDataImport.js +++ b/frontend/src/features/migration/composables/useDataImport.js @@ -182,7 +182,7 @@ export function useDataImport(emitFn) { } } - if (detectedType === '2fauth_encrypted' || detectedType === 'aegis_encrypted' || detectedType === 'proton_encrypted' || detectedType === '2fas_encrypted' || detectedType === 'ente_encrypted' || detectedType === 'bitwarden_vault_encrypted') { + if (detectedType === '2fauth_encrypted' || detectedType === 'aegis_encrypted' || detectedType === 'proton_auth_encrypted' || detectedType === 'proton_pass_pgp' || detectedType === '2fas_encrypted' || detectedType === 'ente_encrypted' || detectedType === 'bitwarden_vault_encrypted') { importPassword.value = '' isDialogHandled.value = false showDecryptDialog.value = true diff --git a/frontend/src/features/migration/service/dataMigrationService.js b/frontend/src/features/migration/service/dataMigrationService.js index 3354003..a6b4e1f 100644 --- a/frontend/src/features/migration/service/dataMigrationService.js +++ b/frontend/src/features/migration/service/dataMigrationService.js @@ -5,7 +5,8 @@ import { tryParseJSON } from '@/shared/utils/encoding' import { gaMigrationStrategy } from '@/shared/utils/serializers/gauthStrategy' import { csvStrategy } from '@/shared/utils/serializers/csvStrategy' import { aegisStrategy } from '@/shared/utils/serializers/aegisStrategy' -import protonStrategy from '@/shared/utils/serializers/protonStrategy' +import { protonAuthStrategy } from '@/shared/utils/serializers/protonAuthStrategy' +import { protonPassStrategy } from '@/shared/utils/serializers/protonPassStrategy' import { enteStrategy } from '@/shared/utils/serializers/enteStrategy' import { migrationError } from '@/shared/utils/errors/migrationError' import { unzipSync } from 'fflate' @@ -75,7 +76,9 @@ export const dataMigrationService = { const firstLine = typeof textContent === 'string' ? textContent.split('\n')[0].toLowerCase() : '' if (firstLine.includes('login_totp')) return 'bitwarden_vault_csv' if (firstLine.includes('title') && firstLine.includes('otpauth')) return '1password_csv' - if (firstLine.includes('otpauth')) return 'bitwarden_auth_csv' // Bitwarden Auth standalone typically exports simpler CSV + if (firstLine.includes('otpauth')) return 'bitwarden_auth_csv' + if (firstLine.includes('totp') && firstLine.includes('vault') && firstLine.includes('createtime')) return 'proton_pass_csv' + if (firstLine.includes('otpurl') && firstLine.includes('title') && firstLine.includes('username')) return 'dashlane_csv' return 'generic_csv' } @@ -99,7 +102,7 @@ export const dataMigrationService = { if (json.schemaVersion && Array.isArray(json.services)) return '2fas' if (json.version === 1 && json.db && typeof json.db === 'object' && Array.isArray(json.db.entries)) return 'aegis' // Aegis unencrypted if (json.version === 1 && json.header && json.db && typeof json.db === 'string') return 'aegis_encrypted' - if (json.version === 1 && typeof json.salt === 'string' && typeof json.content === 'string') return 'proton_encrypted' + if (json.version === 1 && typeof json.salt === 'string' && typeof json.content === 'string') return 'proton_auth_encrypted' // Ente Auth 加密导出:同时包含 kdfParams 和 encryptedData 两个关键字段 if (json.kdfParams && typeof json.encryptedData === 'string') return 'ente_encrypted' // Bitwarden password-protected export (Vault) @@ -107,6 +110,10 @@ export const dataMigrationService = { } } + if (typeof textContent === 'string' && textContent.includes('-----BEGIN PGP MESSAGE-----')) { + return 'proton_pass_pgp' + } + if (filename) { const ext = filename.toLowerCase() if (ext.endsWith('.2fas')) return '2fas' @@ -1091,8 +1098,17 @@ export const dataMigrationService = { type = 'raw' } - if (type === 'proton_encrypted') { - rawVault = await protonStrategy.parse(content, password) + if (type === 'proton_auth_encrypted') { + if (!password) throw new migrationError('导入 Proton Authenticator 备份需要密码', 'MISSING_PASSWORD') + rawVault = await protonAuthStrategy.parse(content, password) + type = 'raw' + content = JSON.stringify(rawVault) + password = undefined + } + + if (type === 'proton_pass_pgp') { + if (!password) throw new migrationError('导入 Proton Pass 备份需要密码', 'MISSING_PASSWORD') + rawVault = await protonPassStrategy.parse(content, password) type = 'raw' content = JSON.stringify(rawVault) password = undefined @@ -1272,7 +1288,7 @@ export const dataMigrationService = { })) } } - else if (type === 'bitwarden_vault_csv' || type === 'bitwarden_auth_csv' || type === '1password_csv' || type === 'generic_csv') rawVault = csvStrategy.parseCsv(content) + else if (type === 'bitwarden_vault_csv' || type === 'bitwarden_auth_csv' || type === '1password_csv' || type === 'proton_pass_csv' || type === 'dashlane_csv' || type === 'generic_csv') rawVault = csvStrategy.parseCsv(content) return rawVault.map(acc => { if (typeof acc.account === 'string' && acc.account.includes(':')) { diff --git a/frontend/src/features/migration/views/dataImport.vue b/frontend/src/features/migration/views/dataImport.vue index 274c9d3..689ff95 100644 --- a/frontend/src/features/migration/views/dataImport.vue +++ b/frontend/src/features/migration/views/dataImport.vue @@ -61,7 +61,9 @@
Bitwarden Vault (.json/.csv) - {{ $t('migration.onepassword_format') }} + 1Password (.1pux/.csv) + Proton Pass (.pgp/.csv) + Dashlane (.csv)
@@ -99,7 +101,7 @@ - {{ $t('migration.ga_ms_import_guide') }} + {{ $t('migration.import_guide') }}
@@ -149,7 +151,8 @@ - + + @@ -187,7 +190,9 @@ import iconAegis from '@/shared/components/icons/iconAegis.vue' import iconGoogleAuth from '@/shared/components/icons/iconGoogleAuth.vue' import iconBitwarden from '@/shared/components/icons/iconBitwarden.vue' import iconMicrosoftAuth from '@/shared/components/icons/iconMicrosoftAuth.vue' +import iconDashlanePass from '@/shared/components/icons/iconDashlanePass.vue' import iconProtonAuth from '@/shared/components/icons/iconProtonAuth.vue' +import iconProtonPass from '@/shared/components/icons/iconProtonPass.vue' import iconEnte from '@/shared/components/icons/iconEnte.vue' import icon1Password from '@/shared/components/icons/icon1Password.vue' import iconLastPassAuth from '@/shared/components/icons/iconLastPassAuth.vue' diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json index c93539e..7671aa7 100644 --- a/frontend/src/locales/en-US.json +++ b/frontend/src/locales/en-US.json @@ -15,6 +15,7 @@ "update_available": "Update Available", "pwa_update_ready": "🎉 New Cloudflare Worker deployment is ready!", "refresh_now": "Refresh Now", + "loading_failed": "Component loading failed, please check your network connection", "copy_success": "Copy success", "copy_fail": "Copy failed", "offline_mode": "Network disconnected, the application is currently in offline cache mode", @@ -371,16 +372,17 @@ "center_desc": "Support restoring data from various 2FA apps or system backups.", "auto_identify_tip": "💡 Automatically recognize file types", "drag_drop_tip": "Drag backup files or migration QR screenshots here, or click to upload", - "support_desc": "Supported Formats", + "support_desc": "Supported List", "ga_auth_desc": "Export 'Transfer QR code' in the App and upload the screenshot (.png / .jpg)", "ms_auth_desc": "Microsoft Authenticator (Android Only)", "ms_auth_detail": "Export the following 3 files from Android and upload them together", - "ga_ms_import_guide": "Google / Microsoft Import Guide", + "import_guide": "Import Guide", "batch_import_processing": "📦 Batch Import Processing", "batch_progress": "{processed} / {total} tasks completed", "decrypt_backup_title": "🔓 Decrypt Backup File", "detect_aegis": "Aegis encrypted backup detected", - "detect_proton": "Proton Auth encrypted backup detected", + "detect_proton_auth": "Proton Authenticator encrypted backup detected", + "detect_proton_pass": "Proton Pass encrypted backup detected (.pgp)", "detect_2fas": "2FAS Auth encrypted backup detected", "detect_ente": "Ente Auth encrypted backup detected", "detect_bitwarden_vault": "Bitwarden Vault encrypted backup detected", @@ -390,7 +392,6 @@ "confirm_decrypt_import": "Confirm Decrypt & Import", "system_backup_format": "System Backup", "password_manager_format": "Password Manager (Vault)", - "onepassword_format": "1Password (.1pux/.csv)", "encrypted_backup_json": "Encrypted Backup (.json)", "plaintext_backup_json": "Plaintext Backup (.json)", "mobile_app_format": "Mobile 2FA Apps", diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json index 62efed9..d841c95 100644 --- a/frontend/src/locales/zh-CN.json +++ b/frontend/src/locales/zh-CN.json @@ -15,6 +15,7 @@ "update_available": "更新可用", "pwa_update_ready": "🎉 Cloudflare Worker 新部署已就绪!", "refresh_now": "立即刷新", + "loading_failed": "组件加载失败,请检查网络连接", "copy_success": "复制成功", "copy_fail": "复制失败", "offline_mode": "网络已断开,当前应用正处于离线缓存模式", @@ -371,16 +372,17 @@ "center_desc": "支持从各类 2FA 软件或本系统的备份文件中恢复数据。", "auto_identify_tip": "💡 自动识别文件类型导入", "drag_drop_tip": "将备份文件或迁移二维码截图拖到此处,或 点击上传", - "support_desc": "支持情况说明", + "support_desc": "支持列表", "ga_auth_desc": "请在 App 内导出\"迁移二维码\",将截图直接上传 (.png / .jpg)", "ms_auth_desc": "Microsoft Authenticator (仅支持 Android)", "ms_auth_detail": "Android 手机导出以下3个文件后,同时选中上传即可导入", - "ga_ms_import_guide": "Google / Microsoft Auth 导入指南", + "import_guide": "导入指南", "batch_import_processing": "📦 批量导入处理中", "batch_progress": "{processed} / {total} 任务已完成", "decrypt_backup_title": "🔓 解密备份文件", "detect_aegis": "检测到 Aegis 加密备份", - "detect_proton": "检测到 Proton Auth 加密备份", + "detect_proton_auth": "检测到 Proton Authenticator 加密备份", + "detect_proton_pass": "检测到 Proton Pass 加密备份 (.pgp)", "detect_2fas": "检测到 2FAS Auth 加密备份", "detect_ente": "检测到 Ente Auth 加密备份", "detect_bitwarden_vault": "检测到 Bitwarden Vault 加密备份", @@ -390,7 +392,6 @@ "confirm_decrypt_import": "确认解密并导入", "system_backup_format": "本系统备份", "password_manager_format": "密码管理器 (Vault)", - "onepassword_format": "1Password (.1pux/.csv)", "encrypted_backup_json": "加密备份 (.json)", "plaintext_backup_json": "明文备份 (.json)", "mobile_app_format": "移动端 2FA App", diff --git a/frontend/src/shared/components/icons/iconDashlanePass.vue b/frontend/src/shared/components/icons/iconDashlanePass.vue new file mode 100644 index 0000000..7434bbd --- /dev/null +++ b/frontend/src/shared/components/icons/iconDashlanePass.vue @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/frontend/src/shared/components/icons/iconProtonPass.vue b/frontend/src/shared/components/icons/iconProtonPass.vue new file mode 100644 index 0000000..39cabb9 --- /dev/null +++ b/frontend/src/shared/components/icons/iconProtonPass.vue @@ -0,0 +1,9 @@ + + diff --git a/frontend/src/shared/utils/asyncHelper.js b/frontend/src/shared/utils/asyncHelper.js index 125c718..5d0e2b6 100644 --- a/frontend/src/shared/utils/asyncHelper.js +++ b/frontend/src/shared/utils/asyncHelper.js @@ -1,5 +1,6 @@ -import { defineAsyncComponent, h } from 'vue' +import { defineAsyncComponent, h, ref, onMounted, onUnmounted } from 'vue' import { ElEmpty, ElButton } from 'element-plus' +import { i18n } from '@/locales' /** * 增强的异步组件工厂函数 @@ -10,14 +11,37 @@ export function createAsyncComponent(loader) { loader, // 加载中占位 loadingComponent: { - render() { - return h('div', { + setup() { + const dots = ref('...') + let timer = null + + onMounted(() => { + timer = setInterval(() => { + if (dots.value.length >= 10) { + dots.value = '...' + } else { + dots.value += '.' + } + }, 400) + }) + + onUnmounted(() => { + if (timer) clearInterval(timer) + }) + + return () => h('div', { style: 'display: flex; justify-content: center; align-items: center; min-height: 400px; width: 100%;' }, [ h('div', { class: 'is-loading', - style: 'font-size: 24px;' - }, 'Loading...') + style: ` + font-size: 24px; + font-family: monospace; + color: var(--el-color-primary); + font-weight: 500; + letter-spacing: 1px; + ` + }, `Loading${dots.value}`) ]) } }, @@ -25,14 +49,15 @@ export function createAsyncComponent(loader) { errorComponent: { props: ['error'], render() { + const { t } = i18n.global return h('div', { style: 'padding: 40px; text-align: center;' }, [ - h(ElEmpty, { description: '组件加载失败,请检查网络连接' }, { + h(ElEmpty, { description: t('common.loading_failed') }, { default: () => h(ElButton, { type: 'primary', onClick: () => window.location.reload() - }, '刷新页面') + }, t('common.refresh_now')) }) ]) } diff --git a/frontend/src/shared/utils/serializers/csvStrategy.js b/frontend/src/shared/utils/serializers/csvStrategy.js index 3852581..9973914 100644 --- a/frontend/src/shared/utils/serializers/csvStrategy.js +++ b/frontend/src/shared/utils/serializers/csvStrategy.js @@ -66,9 +66,11 @@ export const csvStrategy = { const isBitwardenVault = headers.includes('login_totp') const isBitwardenAuth = headers.includes('otpauth') && !headers.includes('title') const is1Password = headers.includes('otpauth') && headers.includes('title') + const isProtonPass = headers.includes('totp') && headers.includes('vault') && headers.includes('createtime') + const isDashlane = headers.includes('otpurl') && headers.includes('title') && headers.includes('username') const isGeneric = headers.includes('issuer') || headers.includes('secret') || headers.includes('name') - if (!isBitwardenVault && !isBitwardenAuth && !is1Password && !isGeneric) return [] + if (!isBitwardenVault && !isBitwardenAuth && !is1Password && !isProtonPass && !isDashlane && !isGeneric) return [] for (let i = 1; i < lines.length; i++) { const row = this._splitCsvLine(lines[i]) @@ -114,6 +116,28 @@ export const csvStrategy = { rawVault.push(accData) } } + } else if (isProtonPass) { + const totpStr = (rowData['totp'] || '').trim() + if (totpStr && totpStr.startsWith('otpauth://')) { + const accData = parseOtpUri(totpStr) + if (accData) { + accData.service = rowData['name'] || accData.service + accData.account = rowData['username'] || accData.account + accData.category = rowData['vault'] || '' + rawVault.push(accData) + } + } + } else if (isDashlane) { + const totpStr = (rowData['otpurl'] || '').trim() + if (totpStr && totpStr.startsWith('otpauth://')) { + const accData = parseOtpUri(totpStr) + if (accData) { + accData.service = rowData['title'] || accData.service + accData.account = rowData['username'] || accData.account + accData.category = rowData['category'] || '' + rawVault.push(accData) + } + } } else { const secret = rowData['secret'] || '' if (secret) { diff --git a/frontend/src/shared/utils/serializers/protonStrategy.js b/frontend/src/shared/utils/serializers/protonAuthStrategy.js similarity index 89% rename from frontend/src/shared/utils/serializers/protonStrategy.js rename to frontend/src/shared/utils/serializers/protonAuthStrategy.js index 97b6e8c..cc5886f 100644 --- a/frontend/src/shared/utils/serializers/protonStrategy.js +++ b/frontend/src/shared/utils/serializers/protonAuthStrategy.js @@ -1,8 +1,12 @@ import argon2 from 'argon2-browser/dist/argon2-bundled.min.js'; import { parseOtpUri } from '@/shared/utils/totp'; -export default { - name: 'Proton Pass (.json)', +/** + * Proton Authenticator (.json) Strategy + * Uses Argon2id + AES-GCM + */ +export const protonAuthStrategy = { + name: 'Proton Authenticator (.json)', fileType: 'application/json, text/plain', async parse(fileContent, password) { @@ -52,8 +56,6 @@ export default { distPath: '/' }); - // 2. Decrypt via Web Crypto API - // argon2-browser returns Uint8Array in argon2Result.hash const keyMaterial = argon2Result.hash; const cryptoKey = await window.crypto.subtle.importKey( @@ -64,7 +66,7 @@ export default { ['decrypt'] ); - // AAD defined in Rust + // AAD defined in Rust source of Proton Auth const aad = new TextEncoder().encode('proton.authenticator.export.v1'); const decryptedBuffer = await window.crypto.subtle.decrypt( @@ -81,7 +83,7 @@ export default { const decryptedString = new TextDecoder().decode(decryptedBuffer); const decryptedJson = JSON.parse(decryptedString); - // 3. Parse items + // Parse items const entries = decryptedJson.entries || []; const results = []; @@ -97,8 +99,7 @@ export default { return results; } catch (err) { - console.error('Proton decryption failed:', err); - // Must throw this precise message so UI prompts for password again if wrong + console.error('Proton Authenticator decryption failed:', err); throw new Error('INVALID_FORMAT_OR_PASSWORD'); } } diff --git a/frontend/src/shared/utils/serializers/protonPassStrategy.js b/frontend/src/shared/utils/serializers/protonPassStrategy.js new file mode 100644 index 0000000..70838d9 --- /dev/null +++ b/frontend/src/shared/utils/serializers/protonPassStrategy.js @@ -0,0 +1,55 @@ +import { parseOtpUri } from '@/shared/utils/totp'; +import * as openpgp from 'openpgp'; + +/** + * Proton Pass (.pgp) Strategy + * Uses OpenPGP (Armored Message) + */ +export const protonPassStrategy = { + name: 'Proton Pass (.pgp)', + fileType: 'text/plain', + + async parse(armoredContent, password) { + try { + const message = await openpgp.readMessage({ + armoredMessage: armoredContent + }); + + const { data: decrypted } = await openpgp.decrypt({ + message, + passwords: [password], + format: 'utf8' + }); + + const decryptedJson = JSON.parse(decrypted); + const results = []; + + const vaults = decryptedJson.vaults || {}; + + for (const vaultId in vaults) { + const vault = vaults[vaultId]; + const items = vault.items || []; + for (const item of items) { + const data = item.data || {}; + const content = data.content || {}; + const metadata = data.metadata || {}; + + if (content.totpUri) { + const parsed = parseOtpUri(content.totpUri); + if (parsed) { + // Use metadata name if available for service + if (metadata.name) parsed.service = metadata.name; + if (content.itemUsername) parsed.account = content.itemUsername; + results.push(parsed); + } + } + } + } + + return results; + } catch (err) { + console.error('Proton Pass PGP decryption failed:', err); + throw new Error('INVALID_FORMAT_OR_PASSWORD'); + } + } +}; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 86818a6..902cdbc 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -84,7 +84,8 @@ export default defineConfig({ '**/assets/pdf-utils*.js', '**/assets/compression-utils*.js', '**/assets/dataImport*.js', - '**/assets/dataMigrationService*.js' + '**/assets/dataMigrationService*.js', + '**/assets/openpgp*.js' ], cleanupOutdatedCaches: true, clientsClaim: true, @@ -117,7 +118,7 @@ export default defineConfig({ }, { // 为被 globIgnores 忽略的大体积工具 JS chunk 配置动态缓存 - urlPattern: /assets\/(wa-sqlite|argon2|hash-wasm|libsodium-wrappers|sql|jsQR|pdf-utils|compression-utils|dataImport|dataMigrationService).*\.js$/i, + urlPattern: /assets\/(wa-sqlite|argon2|hash-wasm|libsodium-wrappers|sql|jsQR|pdf-utils|compression-utils|dataImport|dataMigrationService|openpgp).*\.js$/i, handler: 'CacheFirst', options: { cacheName: 'lazy-tools-js-cache', @@ -190,6 +191,9 @@ export default defineConfig({ // 二维码处理相关 (体积较大且仅特定页面使用) if (id.includes('/node_modules/qrcode/') || id.includes('/node_modules/jsqr/')) return 'qr-utils' + // 加密相关:OpenPGP (体积大且仅特定导入场景使用) + if (id.includes('/node_modules/openpgp/')) return 'openpgp' + // 大型 UI 支撑库 if (id.includes('/node_modules/@tanstack/')) return 'tanstack-query'