fix: 移除冲突的 node-polyfills(改用 CDN 加载 WebTorrent)+ 加 P2P 进度翻译与顶栏活动指示灯

This commit is contained in:
ice-a
2026-10-16 19:48:04 +08:00
parent 7e65ae975f
commit 895977626b
7 changed files with 90 additions and 15 deletions
+4 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { TorrentFileEntry } from '~/types/api'
import { formatBytes } from '~/utils/format'
import { translateText } from '~/utils/i18n'
import { readFileBytesClient, type FileProgress } from '~/utils/clientTorrent'
const props = defineProps<{
@@ -62,7 +63,9 @@ async function load() {
timeoutMs: 25000,
onProgress: (p: FileProgress) => {
progress.value = p.progress
progressText.value = p.message
progressText.value = p.peers > 0
? `${translateText('下载中')} ${Math.round(p.progress * 100)}% · ${p.peers} ${translateText('个节点')}`
: translateText('正在连接 P2P 节点…')
}
})
const mime = MIME[extOf(current.value?.path || '')] || 'application/octet-stream'
+12 -1
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { TorrentFileEntry, TorrentView } from '~/types/api'
import { translateText } from '~/utils/i18n'
import { resolveMetadataClient, type ResolveProgress } from '~/utils/clientTorrent'
const props = defineProps<{
@@ -60,6 +61,16 @@ function applyResolved(res: { files: TorrentFileEntry[]; size: number; name: str
emit('resolved', res)
}
function resolveStatusText(p: ResolveProgress): string {
if (p.phase === 'connecting') {
return p.peers > 0
? `${translateText('已连接')} ${p.peers} ${translateText('个节点,正在获取元数据…')}`
: translateText('正在通过 WebRTC 寻找 P2P 节点…')
}
if (p.phase === 'metadata') return translateText('已拿到元数据,正在整理文件清单…')
return `${translateText('共')} ${p.peers} ${translateText('个节点')}`
}
async function doResolve() {
resolving.value = true
resolveStatus.value = ''
@@ -77,7 +88,7 @@ async function doResolve() {
const meta = await resolveMetadataClient(local.value.magnet, {
timeoutMs: 30000,
onProgress: (p: ResolveProgress) => {
resolveStatus.value = p.message
resolveStatus.value = resolveStatusText(p)
}
})
applyResolved(meta)
+6 -1
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { locale, setLocale, type Locale } from '~/utils/i18n'
import { locale, setLocale, translateText, type Locale } from '~/utils/i18n'
import { p2pActive } from '~/utils/clientTorrent'
const route = useRoute()
const health = useHealth()
@@ -50,6 +51,10 @@ async function logout() {
<i class="dot" />
{{ health?.webTorrent ? '磁力解析就绪' : '磁力解析(缓存模式)' }}
</span>
<span class="pill hide-sm" :class="p2pActive ? 'on' : 'dim'">
<i class="dot" />
{{ p2pActive ? translateText('P2P 活动中') : translateText('P2P 空闲') }}
</span>
<NuxtLink to="/settings" class="pill" :class="mongoPill.cls">
<i class="dot" />
{{ mongoPill.text }}
+52 -2
View File
@@ -28,15 +28,61 @@ function withWsTrackers(magnet: string): string {
return magnet + sep + add.map((t) => `tr=${encodeURIComponent(t)}`).join('&')
}
// 浏览器端 WebTorrent 以 CDN 的 UMD 包形式按需加载(避免把 webtorrent 打进 Vite 客户端,
// 同时绕开 Vite 8 与 node-polyfills 的 peer 依赖冲突)。优先 jsDelivr,失败再试 unpkg。
const WT_CDN_URLS = [
'https://cdn.jsdelivr.net/npm/webtorrent@3.0.21/dist/webtorrent.min.js',
'https://unpkg.com/webtorrent@3.0.21/dist/webtorrent.min.js'
]
let wtPromise: Promise<any> | null = null
function loadWebTorrent(): Promise<any> {
if (!wtPromise) {
// 仅在浏览器运行时按需加载,Vite 会将其单独拆包;服务端不会执行到此处
wtPromise = import('webtorrent').then((m) => (m as any).default || m)
wtPromise = new Promise((resolve, reject) => {
if (typeof window === 'undefined') {
reject(new Error('WebTorrent 只能在浏览器中运行'))
return
}
const existing = (window as any).WebTorrent
if (existing) {
resolve(existing)
return
}
const tryLoad = (i: number) => {
if (i >= WT_CDN_URLS.length) {
reject(new Error('无法从 CDN 加载 WebTorrent(可能被网络或 CSP 拦截)'))
return
}
const script = document.createElement('script')
script.src = WT_CDN_URLS[i]
script.async = true
script.onload = () => {
const WT = (window as any).WebTorrent
if (WT) resolve(WT)
else tryLoad(i + 1)
}
script.onerror = () => tryLoad(i + 1)
document.head.appendChild(script)
}
tryLoad(0)
})
}
return wtPromise
}
// 当前活跃的浏览器 P2P 会话数,用于顶栏指示
import { ref } from 'vue'
export const p2pActive = ref(false)
let activeSessions = 0
function enterSession() {
activeSessions++
p2pActive.value = activeSessions > 0
}
function leaveSession() {
activeSessions = Math.max(0, activeSessions - 1)
p2pActive.value = activeSessions > 0
}
function waitForMetadata(torrent: any, timeoutMs: number): Promise<void> {
return new Promise((resolve, reject) => {
if (torrent.metadata || torrent.ready) return resolve()
@@ -79,6 +125,7 @@ export async function resolveMetadataClient(
const timeoutMs = opts.timeoutMs ?? 30000
const WebTorrent = await loadWebTorrent()
const client = new WebTorrent()
enterSession()
try {
const torrent = client.add(withWsTrackers(magnet))
const report = (phase: ResolveProgress['phase'], message?: string) => {
@@ -103,6 +150,7 @@ export async function resolveMetadataClient(
onProgress?.({ phase: 'done', peers: torrent.numPeers ?? 0, message: `共 ${files.length} 个文件` })
return { name: String(torrent.name || ''), files, size: files.reduce((s, f) => s + f.length, 0) }
} finally {
leaveSession()
try {
client.destroy()
} catch {
@@ -120,6 +168,7 @@ export async function readFileBytesClient(
const timeoutMs = opts.timeoutMs ?? 25000
const WebTorrent = await loadWebTorrent()
const client = new WebTorrent()
enterSession()
try {
const torrent = client.add(withWsTrackers(magnet))
await waitForMetadata(torrent, timeoutMs)
@@ -153,6 +202,7 @@ export async function readFileBytesClient(
})
return ab
} finally {
leaveSession()
try {
client.destroy()
} catch {
+15
View File
@@ -11,6 +11,21 @@ export const zh2en: Record<string, string> = {
'MONGO 未连接': 'MONGO disconnected',
'磁力解析就绪': 'Magnet parsing ready',
'磁力解析(缓存模式)': 'Magnet parsing (cache mode)',
// ===== 浏览器端 P2P 进度文案 =====
'P2P 活动中': 'P2P active',
'P2P 空闲': 'P2P idle',
'已连接': 'Connected',
'个节点,正在获取元数据…': ' peers, fetching metadata…',
'正在通过 WebRTC 寻找 P2P 节点…': 'Looking for P2P nodes via WebRTC…',
'已拿到元数据,正在整理文件清单…': 'Metadata received, listing files…',
'共': 'Total',
'个节点': ' peers',
'个文件': ' files',
'下载中': 'Downloading',
'正在连接 P2P 节点…': 'Connecting to P2P nodes…',
'正在通过 WebRTC 从做种节点拉取图片…': 'Fetching image from seeders via WebRTC…',
'完成': 'Done',
'本地自托管 · 单进程 Nuxt 单体应用': 'Self-hosted · single-process Nuxt app',
'种子只做解析与预览,文件内容由你自己的下载器拉取': 'Torrents are only parsed and previewed; file contents are fetched by your own download client',
-8
View File
@@ -1,5 +1,3 @@
import nodePolyfills from 'vite-plugin-node-polyfills'
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: false },
@@ -29,12 +27,6 @@ export default defineNuxtConfig({
}
},
vite: {
plugins: [
nodePolyfills({
globals: { Buffer: true, global: true, process: true },
protocolImports: true
})
],
server: {
fs: { strict: false }
}
+1 -2
View File
@@ -20,7 +20,6 @@
},
"devDependencies": {
"@types/node": "^22.13.0",
"typescript": "^5.7.3",
"vite-plugin-node-polyfills": "^0.23.0"
"typescript": "^5.7.3"
}
}