修复wss连接、一些未知错误

This commit is contained in:
2026-03-28 18:58:52 +08:00
parent b66ba41431
commit 3d391415c6
8 changed files with 179 additions and 97 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

17
frontend/dist/assets/index-DPzeYqvr.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AirShare Pro</title>
<script type="module" crossorigin src="/assets/index-DE3lDjdM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CQ9sinZs.css">
<script type="module" crossorigin src="/assets/index-DPzeYqvr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C-7tVt-S.css">
</head>
<body>
<div id="app"></div>

View File

@@ -35,6 +35,7 @@ const connectedPeer = ref({
name: '--',
type: '等待连接',
deviceId: '',
networkGroupKey: '',
})
const transferItems = ref([])
const relayServer = ref('/ws')
@@ -82,6 +83,17 @@ const pendingTransferItems = computed(() =>
)
const hasPendingItems = computed(() => pendingTransferItems.value.length > 0)
const currentTransferNetworkHint = computed(() => {
if (!connectedPeer.value.deviceId) {
return ''
}
if (isSameLocalNetwork(connectedPeer.value.networkGroupKey) || hasTurnRelayConfigured()) {
return ''
}
return '当前是跨网络访问,未配置 TURN 时实时通道可能失败。文本和小文件可回退中转,大文件建议使用 MinIO。'
})
watch(
theme,
@@ -208,7 +220,7 @@ async function registerCurrentDevice() {
device_id: deviceId,
name: deviceName,
type: deviceType,
network_group_key: window.location.hostname || 'local',
network_group_key: deriveNetworkGroupKey(),
})
localStorage.setItem(DEVICE_ID_KEY, device.id)
@@ -269,7 +281,7 @@ async function refreshCandidates() {
description: `${formatDeviceType(device.type)} · 最近活跃 ${formatRelativeTime(device.last_seen_at)}`,
icon: mapDeviceIcon(device.type),
connectionType:
device.network_group_key && device.network_group_key === window.location.hostname
isSameLocalNetwork(device.network_group_key)
? '局域网直连优先'
: '点对点传输',
}))
@@ -412,6 +424,7 @@ function connectToPeer(device) {
name: device.name,
type: device.connectionType || device.type || '点对点传输',
deviceId: device.deviceId || device.id || '',
networkGroupKey: device.network_group_key || '',
}
isWaitingRoom.value = false
generatedRoomCode.value = '----'
@@ -439,6 +452,7 @@ function ensurePeerSession(device, preserveItems = false) {
name: device.name,
type: device.connectionType || device.type || '点对点传输',
deviceId: nextDeviceId,
networkGroupKey: device.network_group_key || '',
}
isWaitingRoom.value = false
generatedRoomCode.value = '----'
@@ -457,6 +471,7 @@ function closeTransfer() {
name: '--',
type: '等待连接',
deviceId: '',
networkGroupKey: '',
}
viewMode.value = 'main'
connectedPeer.value.baseType = '等待连接'
@@ -733,7 +748,7 @@ async function copyTextItem(id) {
}
try {
await navigator.clipboard.writeText(item.text)
await copyToClipboard(item.text)
item.copied = true
window.setTimeout(() => {
@@ -748,6 +763,33 @@ async function copyTextItem(id) {
}
}
async function copyToClipboard(value) {
if (navigator.clipboard?.writeText && window.isSecureContext) {
await navigator.clipboard.writeText(value)
return
}
const textarea = document.createElement('textarea')
textarea.value = value
textarea.setAttribute('readonly', 'readonly')
textarea.style.position = 'fixed'
textarea.style.top = '0'
textarea.style.left = '-9999px'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
try {
if (!document.execCommand('copy')) {
throw new Error('copy command failed')
}
} finally {
document.body.removeChild(textarea)
}
}
function simulateUpload(id) {
clearUploadTimer(id)
@@ -1169,6 +1211,48 @@ function buildRelayServerPath() {
return `${protocol}//${window.location.host}/ws`
}
function hasTurnRelayConfigured() {
return Array.isArray(runtimeConfig.value?.turn_urls) && runtimeConfig.value.turn_urls.some((item) => String(item || '').trim())
}
function deriveNetworkGroupKey() {
const hostname = String(window.location.hostname || '').trim().toLowerCase()
if (!hostname) {
return 'local'
}
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname.endsWith('.local')) {
return hostname
}
if (isPrivateIPv4Host(hostname)) {
return hostname
}
return ''
}
function isSameLocalNetwork(networkGroupKey) {
const localNetworkGroupKey = deriveNetworkGroupKey()
return !!localNetworkGroupKey && networkGroupKey === localNetworkGroupKey
}
function isPrivateIPv4Host(hostname) {
const parts = hostname.split('.')
if (parts.length !== 4 || parts.some((part) => !/^\d+$/.test(part))) {
return false
}
const [first, second] = parts.map((part) => Number(part))
if (first === 10 || first === 127) {
return true
}
if (first === 192 && second === 168) {
return true
}
return first === 172 && second >= 16 && second <= 31
}
function buildIceServers(configValue) {
const urls = Array.isArray(configValue?.turn_urls)
? configValue.turn_urls.map((item) => String(item || '').trim()).filter(Boolean)
@@ -1235,12 +1319,32 @@ function syncConnectionTypeLabel(status = '') {
}
}
function shouldRecreateRealtimeTransport(targetDeviceId) {
if (!peerConnection || currentRtcPeerId !== targetDeviceId) {
return true
}
if (peerConnection.signalingState === 'closed') {
return true
}
if (['failed', 'disconnected', 'closed'].includes(peerConnection.connectionState)) {
return true
}
if (['failed', 'disconnected', 'closed'].includes(peerConnection.iceConnectionState)) {
return true
}
return !controlChannel || controlChannel.readyState === 'closed'
}
async function ensureRealtimeTransport(targetDeviceId, options = {}) {
if (!targetDeviceId || !supportsWebRTC()) {
return null
}
if (!peerConnection || currentRtcPeerId !== targetDeviceId) {
if (shouldRecreateRealtimeTransport(targetDeviceId)) {
teardownRealtimeTransport()
createPeerConnection(targetDeviceId)
}
@@ -1582,6 +1686,7 @@ function buildPeerSessionFromDevice(deviceId, fallbackConnectionType = '等待
name: knownDevice?.name || `设备 ${shortId(deviceId)}`,
type: formatDeviceType(knownDevice?.type || 'desktop'),
connectionType: fallbackConnectionType,
network_group_key: knownDevice?.network_group_key || '',
}
}
@@ -1797,12 +1902,35 @@ function handleRelayMessage(raw) {
if (envelope.type === 'transfer.file') {
handleIncomingTransferFile(envelope)
return
}
if (envelope.type === 'peer.session.closed') {
handlePeerSessionClosed(envelope)
}
} catch (error) {
console.error(error)
}
}
function handlePeerSessionClosed(envelope) {
const senderDeviceId = envelope.device_id || ''
if (!senderDeviceId || connectedPeer.value.deviceId !== senderDeviceId) {
return
}
teardownRealtimeTransport()
resetTransferItems()
connectedPeer.value = {
name: '--',
type: '绛夊緟杩炴帴',
baseType: '绛夊緟杩炴帴',
deviceId: '',
networkGroupKey: '',
}
viewMode.value = 'main'
}
function handleIncomingTransferCreated(envelope) {
const payload = envelope.payload || {}
const senderDeviceId = envelope.device_id || payload.sender_device_id || ''
@@ -2178,7 +2306,7 @@ refreshCandidates = async function refreshCandidatesOverride() {
description: `${formatDeviceType(device.type)} · 最近活跃 ${formatRelativeTime(device.last_seen_at)}`,
icon: mapDeviceIcon(device.type),
connectionType:
device.network_group_key && device.network_group_key === window.location.hostname
isSameLocalNetwork(device.network_group_key)
? '局域网直连优先'
: '跨网络实时传输',
}))
@@ -2206,6 +2334,7 @@ connectToPeer = function connectToPeerOverride(device) {
type: nextBaseType,
baseType: nextBaseType,
deviceId: nextDeviceId,
networkGroupKey: device.network_group_key || '',
}
isWaitingRoom.value = false
generatedRoomCode.value = '----'
@@ -2231,6 +2360,7 @@ ensurePeerSession = function ensurePeerSessionOverride(device, preserveItems = f
type: nextBaseType,
baseType: nextBaseType,
deviceId: nextDeviceId,
networkGroupKey: device.network_group_key || '',
}
isWaitingRoom.value = false
generatedRoomCode.value = '----'
@@ -2243,6 +2373,10 @@ ensurePeerSession = function ensurePeerSessionOverride(device, preserveItems = f
}
closeTransfer = function closeTransferOverride() {
if (connectedPeer.value.deviceId) {
relayToPeer('peer.session.closed', connectedPeer.value.deviceId, {})
}
teardownRealtimeTransport()
resetTransferItems()
connectedPeer.value = {
@@ -2250,6 +2384,7 @@ closeTransfer = function closeTransferOverride() {
type: '等待连接',
baseType: '等待连接',
deviceId: '',
networkGroupKey: '',
}
viewMode.value = 'main'
}
@@ -2709,6 +2844,7 @@ handleIncomingTransferFile = function handleIncomingTransferFileOverride(envelop
:connection-type="connectedPeer.type"
:has-pending-items="hasPendingItems"
:items="transferItems"
:network-hint="currentTransferNetworkHint"
:peer-name="connectedPeer.name"
@close="closeTransfer"
@copy-item="copyTextItem"

View File

@@ -12,6 +12,10 @@ const props = defineProps({
type: String,
required: true,
},
networkHint: {
type: String,
default: '',
},
items: {
type: Array,
required: true,
@@ -84,6 +88,7 @@ function handleDrop(event) {
<div class="connected-to">
<h2>{{ peerName }}</h2>
<p>{{ connectionType }}</p>
<small v-if="networkHint" class="connection-hint">{{ networkHint }}</small>
</div>
<button class="close-btn" type="button" @click="$emit('close')">
<LocalIcon name="close" size="20" />

View File

@@ -708,6 +708,15 @@ input.room-code::placeholder {
color: var(--success-green);
}
.connection-hint {
display: block;
margin-top: 8px;
max-width: 42ch;
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
}
.close-btn {
width: 36px;
height: 36px;