chore: 完成v6.7.0版本迭代更新

本次更新涵盖多个功能模块的优化与新增:
1. 新增Wi-Fi直连配对方式与协作画布模块
2. 完成设备管理重命名API与前端适配
3. 优化日签卡片空数据保护与UI细节
4. 新增剪贴板工具与每日运势会话
5. 修复应用锁恢复、语音消息录制等已知问题
6. 完善文件传输统计与配对逻辑
7. 更新安卓权限配置与iOS隐私描述
8. 新增自动化测试脚本与文档
9. 清理旧版审计报告与测试文件
This commit is contained in:
Developer
2026-05-14 05:35:18 +08:00
parent 2c1a87e7c5
commit 228095f80a
208 changed files with 45933 additions and 12870 deletions

View File

@@ -26,8 +26,10 @@ class SnapdropServer {
this._rooms = {};
this._userIdIndex = {};
this._fingerprintIndex = {};
this._pairingRecords = {};
this._pendingPairRequests = {};
this._canvasRooms = {};
this._loadPairingRecords();
console.log('Snapdrop is running on port', port);
@@ -117,26 +119,42 @@ class SnapdropServer {
case 'heartbeat':
sender.lastBeat = Date.now();
break;
case 'textMessage':
this._handleTextMessage(sender, message);
break;
case 'fileMeta':
this._handleFileMeta(sender, message);
break;
case 'canvas-stroke':
this._handleCanvasStroke(sender, message);
break;
case 'canvas-cursor':
this._handleCanvasCursor(sender, message);
break;
case 'canvas-join':
this._handleCanvasJoin(sender, message);
break;
case 'canvas-leave':
this._handleCanvasLeave(sender, message);
break;
case 'canvas-snapshot':
this._handleCanvasSnapshot(sender, message);
break;
}
const handledTypes = ['disconnect', 'pong', 'register', 'discoverMyDevices', 'transportNegotiate', 'wsRelay', 'pair-request', 'pairRequest', 'pair-accept', 'pairAccept', 'pair-reject', 'pairReject', 'heartbeat'];
const handledTypes = ['disconnect', 'pong', 'register', 'discoverMyDevices', 'transportNegotiate', 'wsRelay', 'pair-request', 'pairRequest', 'pair-accept', 'pairAccept', 'pair-reject', 'pairReject', 'heartbeat', 'textMessage', 'fileMeta', 'canvas-stroke', 'canvas-cursor', 'canvas-join', 'canvas-leave', 'canvas-snapshot'];
if (message.to && !handledTypes.includes(message.type)) {
const recipientId = message.to;
let recipient = null;
if (this._rooms[sender.ip] && this._rooms[sender.ip][recipientId]) {
recipient = this._rooms[sender.ip][recipientId];
} else {
for (const roomId in this._rooms) {
if (this._rooms[roomId][recipientId]) {
recipient = this._rooms[roomId][recipientId];
break;
}
}
}
let recipientId = message.to;
let recipient = this._resolveTarget(sender, recipientId);
if (recipient) {
delete message.to;
message.sender = sender.id;
message.from = sender.fingerprint || sender.id;
message.fromPeerId = sender.id;
this._send(recipient, message);
} else {
console.log('Message routing failed: target not found for', recipientId, 'from', sender.id);
}
return;
}
@@ -158,6 +176,23 @@ class SnapdropServer {
}
this._userIdIndex[sender.userId].add(sender.id);
}
if (data.fingerprint) {
const oldFingerprint = sender.fingerprint;
sender.fingerprint = data.fingerprint;
if (oldFingerprint && this._fingerprintIndex[oldFingerprint] === sender.id) {
delete this._fingerprintIndex[oldFingerprint];
}
const existingPeerId = this._fingerprintIndex[sender.fingerprint];
if (existingPeerId && existingPeerId !== sender.id) {
const existingPeer = this._findPeer(existingPeerId);
if (existingPeer) {
console.log('Replacing old connection for fingerprint:', sender.fingerprint, 'old peer:', existingPeerId, 'new peer:', sender.id);
this._leaveRoom(existingPeer);
}
}
this._fingerprintIndex[sender.fingerprint] = sender.id;
console.log('Register fingerprint mapping:', sender.fingerprint, '->', sender.id);
}
if (data.ipCity) sender.ipCity = data.ipCity;
if (data.ipRange) sender.ipRange = data.ipRange;
if (data.deviceModel) sender.deviceModel = data.deviceModel;
@@ -168,6 +203,7 @@ class SnapdropServer {
this._send(sender, {
type: 'registered',
id: sender.id,
fingerprint: sender.fingerprint || '',
userId: sender.userId
});
}
@@ -184,30 +220,34 @@ class SnapdropServer {
return;
}
const myDevices = [];
const seenFingerprints = new Map();
for (const roomId in this._rooms) {
for (const peerId in this._rooms[roomId]) {
const peer = this._rooms[roomId][peerId];
if (peer.id === sender.id) continue;
if (peer.userId === userId) {
myDevices.push({
id: peer.id,
alias: peer.name.displayName,
deviceName: peer.name.deviceName,
deviceModel: peer.deviceModel || '',
platform: peer.platform || '',
ip: peer.ip,
ipCity: peer.ipCity || '',
isOnline: true,
rtcSupported: peer.rtcSupported
});
const fp = peer.fingerprint || peer.id;
if (!seenFingerprints.has(fp)) {
seenFingerprints.set(fp, {
id: peer.id,
fingerprint: peer.fingerprint || '',
alias: peer.name.displayName,
deviceName: peer.name.deviceName,
deviceModel: peer.deviceModel || '',
platform: peer.platform || '',
ip: peer.ip,
ipCity: peer.ipCity || '',
isOnline: true,
rtcSupported: peer.rtcSupported
});
}
}
}
}
this._send(sender, {
type: 'myDevicesResponse',
devices: myDevices
devices: Array.from(seenFingerprints.values())
});
}
@@ -215,12 +255,10 @@ class SnapdropServer {
const targetId = message.to;
if (!targetId) return;
let targetPeer = null;
for (const roomId in this._rooms) {
if (this._rooms[roomId][targetId]) {
targetPeer = this._rooms[roomId][targetId];
break;
}
let targetPeer = this._findPeer(targetId);
if (!targetPeer && this._fingerprintIndex[targetId]) {
targetPeer = this._findPeer(this._fingerprintIndex[targetId]);
}
if (!targetPeer) {
@@ -234,7 +272,7 @@ class SnapdropServer {
this._send(targetPeer, {
type: 'transportNegotiate',
from: sender.id,
from: sender.fingerprint || sender.id,
transport: message.transport,
payload: message.payload || {}
});
@@ -245,6 +283,11 @@ class SnapdropServer {
if (!targetId) return;
let targetPeer = this._findPeer(targetId);
if (!targetPeer && this._fingerprintIndex[targetId]) {
targetPeer = this._findPeer(this._fingerprintIndex[targetId]);
}
if (!targetPeer) {
this._send(sender, {
type: 'pair-response',
@@ -267,7 +310,7 @@ class SnapdropServer {
this._send(targetPeer, {
type: 'pair-request',
from: sender.id,
from: sender.fingerprint || sender.id,
requestId: requestId,
fingerprint: this._pendingPairRequests[requestId].fingerprint,
payload: message.payload || {}
@@ -310,7 +353,7 @@ class SnapdropServer {
if (fromPeer) {
this._send(fromPeer, {
type: 'pair-response',
from: sender.id,
from: sender.fingerprint || sender.id,
success: true,
requestId: requestId
});
@@ -318,7 +361,7 @@ class SnapdropServer {
this._send(sender, {
type: 'pair-response',
from: req.fromId,
from: fromPeer ? (fromPeer.fingerprint || fromPeer.id) : req.fromId,
success: true,
requestId: requestId
});
@@ -335,7 +378,7 @@ class SnapdropServer {
if (fromPeer) {
this._send(fromPeer, {
type: 'pair-response',
from: sender.id,
from: sender.fingerprint || sender.id,
success: false,
requestId: requestId,
error: 'Rejected'
@@ -352,6 +395,81 @@ class SnapdropServer {
return null;
}
_resolveTarget(sender, targetId) {
if (this._rooms[sender.ip] && this._rooms[sender.ip][targetId]) {
return this._rooms[sender.ip][targetId];
}
let recipient = this._findPeer(targetId);
if (recipient) return recipient;
if (this._fingerprintIndex[targetId]) {
const mappedPeerId = this._fingerprintIndex[targetId];
console.log('Fingerprint mapping:', targetId, '->', mappedPeerId);
recipient = this._findPeer(mappedPeerId);
if (recipient) return recipient;
}
if (sender.userId) {
const sameUserPeerIds = this._userIdIndex[sender.userId];
if (sameUserPeerIds) {
for (const peerId of sameUserPeerIds) {
if (peerId !== sender.id) {
recipient = this._findPeer(peerId);
if (recipient) return recipient;
}
}
}
}
if (this._userIdIndex[targetId]) {
for (const peerId of this._userIdIndex[targetId]) {
recipient = this._findPeer(peerId);
if (recipient) return recipient;
}
}
return null;
}
_handleTextMessage(sender, message) {
const targetId = message.to;
if (!targetId) return;
const recipient = this._resolveTarget(sender, targetId);
if (recipient) {
this._send(recipient, {
type: 'textMessage',
from: sender.fingerprint || sender.id,
fromPeerId: sender.id,
to: recipient.id,
message: message.message || message.payload || {},
timestamp: message.timestamp || Date.now()
});
} else {
console.log('textMessage routing failed: target not found for', targetId, 'from', sender.id);
}
}
_handleFileMeta(sender, message) {
const targetId = message.to;
if (!targetId) return;
const recipient = this._resolveTarget(sender, targetId);
if (recipient) {
this._send(recipient, {
type: 'fileMeta',
from: sender.fingerprint || sender.id,
fromPeerId: sender.id,
to: recipient.id,
file: message.file || message.payload || {},
timestamp: message.timestamp || Date.now()
});
} else {
console.log('fileMeta routing failed: target not found for', targetId, 'from', sender.id);
}
}
_handleWsRelay(sender, message) {
const targetId = message.to;
if (!targetId) return;
@@ -364,10 +482,37 @@ class SnapdropServer {
}
}
if (!targetPeer && this._fingerprintIndex[targetId]) {
const mappedPeerId = this._fingerprintIndex[targetId];
for (const roomId in this._rooms) {
if (this._rooms[roomId][mappedPeerId]) {
targetPeer = this._rooms[roomId][mappedPeerId];
break;
}
}
}
if (!targetPeer && sender.userId) {
const sameUserPeerIds = this._userIdIndex[sender.userId];
if (sameUserPeerIds) {
for (const peerId of sameUserPeerIds) {
if (peerId !== sender.id) {
for (const roomId in this._rooms) {
if (this._rooms[roomId][peerId]) {
targetPeer = this._rooms[roomId][peerId];
break;
}
}
if (targetPeer) break;
}
}
}
}
if (!targetPeer) {
this._send(sender, {
type: 'wsRelay',
from: sender.id,
from: sender.fingerprint || sender.id,
payload: {
relayType: 'error',
data: { error: 'Target peer not found', targetId: targetId }
@@ -384,11 +529,138 @@ class SnapdropServer {
this._send(targetPeer, {
type: 'wsRelay',
from: sender.id,
from: sender.fingerprint || sender.id,
payload: payload
});
}
_handleCanvasJoin(sender, message) {
const payload = message.payload || {};
const canvasId = payload.canvasId;
if (!canvasId) return;
if (!this._canvasRooms[canvasId]) {
this._canvasRooms[canvasId] = new Set();
}
this._canvasRooms[canvasId].add(sender.id);
sender.canvasRoomId = canvasId;
const members = Array.from(this._canvasRooms[canvasId]);
for (const peerId of this._canvasRooms[canvasId]) {
if (peerId === sender.id) continue;
const peer = this._findPeer(peerId);
if (peer) {
this._send(peer, {
type: 'canvas-join',
from: sender.fingerprint || sender.id,
payload: { canvasId: canvasId, members: members }
});
}
}
this._send(sender, {
type: 'canvas-join',
from: 'server',
payload: { canvasId: canvasId, members: members }
});
console.log('Canvas join:', sender.id, 'joined canvas', canvasId, 'members:', members.length);
}
_handleCanvasLeave(sender, message) {
const payload = message.payload || {};
const canvasId = payload.canvasId || sender.canvasRoomId;
if (!canvasId || !this._canvasRooms[canvasId]) return;
this._canvasRooms[canvasId].delete(sender.id);
sender.canvasRoomId = null;
for (const peerId of this._canvasRooms[canvasId]) {
const peer = this._findPeer(peerId);
if (peer) {
this._send(peer, {
type: 'canvas-leave',
from: sender.fingerprint || sender.id,
payload: { canvasId: canvasId }
});
}
}
if (this._canvasRooms[canvasId].size === 0) {
delete this._canvasRooms[canvasId];
}
console.log('Canvas leave:', sender.id, 'left canvas', canvasId);
}
_handleCanvasStroke(sender, message) {
const payload = message.payload || {};
const canvasId = payload.canvasId || sender.canvasRoomId;
if (!canvasId || !this._canvasRooms[canvasId]) return;
for (const peerId of this._canvasRooms[canvasId]) {
if (peerId === sender.id) continue;
const peer = this._findPeer(peerId);
if (peer) {
this._send(peer, {
type: 'canvas-stroke',
from: sender.fingerprint || sender.id,
payload: payload
});
}
}
}
_handleCanvasCursor(sender, message) {
const payload = message.payload || {};
const canvasId = payload.canvasId || sender.canvasRoomId;
if (!canvasId || !this._canvasRooms[canvasId]) return;
for (const peerId of this._canvasRooms[canvasId]) {
if (peerId === sender.id) continue;
const peer = this._findPeer(peerId);
if (peer) {
this._send(peer, {
type: 'canvas-cursor',
from: sender.fingerprint || sender.id,
payload: payload
});
}
}
}
_handleCanvasSnapshot(sender, message) {
const payload = message.payload || {};
const canvasId = payload.canvasId || sender.canvasRoomId;
const action = payload.action || 'request';
if (!canvasId || !this._canvasRooms[canvasId]) return;
if (action === 'request') {
const members = Array.from(this._canvasRooms[canvasId]);
const otherMember = members.find(id => id !== sender.id);
if (otherMember) {
const peer = this._findPeer(otherMember);
if (peer) {
this._send(peer, {
type: 'canvas-snapshot',
from: sender.fingerprint || sender.id,
payload: { canvasId: canvasId, action: 'request' }
});
}
}
} else if (action === 'response') {
const targetId = message.to;
if (targetId) {
const targetPeer = this._findPeer(targetId);
if (targetPeer) {
this._send(targetPeer, {
type: 'canvas-snapshot',
from: sender.fingerprint || sender.id,
payload: payload
});
}
}
}
}
_joinRoom(peer) {
if (!this._rooms[peer.ip]) {
this._rooms[peer.ip] = {};
@@ -426,6 +698,28 @@ class SnapdropServer {
if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return;
this._cancelKeepAlive(this._rooms[peer.ip][peer.id]);
if (peer.canvasRoomId && this._canvasRooms[peer.canvasRoomId]) {
this._canvasRooms[peer.canvasRoomId].delete(peer.id);
for (const peerId of this._canvasRooms[peer.canvasRoomId]) {
const otherPeer = this._findPeer(peerId);
if (otherPeer) {
this._send(otherPeer, {
type: 'canvas-leave',
from: peer.fingerprint || peer.id,
payload: { canvasId: peer.canvasRoomId }
});
}
}
if (this._canvasRooms[peer.canvasRoomId].size === 0) {
delete this._canvasRooms[peer.canvasRoomId];
}
peer.canvasRoomId = null;
}
if (peer.fingerprint && this._fingerprintIndex[peer.fingerprint] === peer.id) {
delete this._fingerprintIndex[peer.fingerprint];
}
delete this._rooms[peer.ip][peer.id];
if (peer.userId && this._userIdIndex[peer.userId]) {
@@ -489,10 +783,12 @@ class Peer {
this._setName(request);
this.userId = '';
this.fingerprint = '';
this.ipCity = '';
this.ipRange = '';
this.deviceModel = '';
this.platform = '';
this.canvasRoomId = null;
this.timerId = 0;
this.lastBeat = Date.now();

View File

@@ -617,6 +617,7 @@ class SignalingServer implements MessageComponentInterface
'from' => $fromId,
'data' => [
'text' => $data['data']['text'] ?? '',
'envelope' => $data['data']['envelope'] ?? null,
'timestamp' => time() * 1000,
],
]);