feat: 5.4.0版本大更新,新增多端桌面小组件与多项功能优化
- 重构「灵感」模块为「发现」模块,统一页面命名与文案 - 新增flutter_tts语音朗读依赖与鸿蒙Nearby配对方式 - 添加Android/iOS/鸿蒙全平台桌面小组件支持(7种类型) - 完善文件传输模块,新增画布邀请消息与删除会话功能 - 优化协作画布光标广播节流逻辑,修复已知bug - 更新应用英文名与隐私政策入口,新增翻译API抽象层 - 移除用户中心多余的加号按钮,完善空状态组件类型
This commit is contained in:
316
server/index.js
316
server/index.js
@@ -30,8 +30,48 @@ class SnapdropServer {
|
||||
this._pairingRecords = {};
|
||||
this._pendingPairRequests = {};
|
||||
this._canvasRooms = {};
|
||||
this._pairingCodes = {};
|
||||
this._radarPool = {};
|
||||
this._activeScreenShares = {};
|
||||
|
||||
this._pairingCodeCleanup = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const code in this._pairingCodes) {
|
||||
if (now > this._pairingCodes[code].expiresAt) {
|
||||
delete this._pairingCodes[code];
|
||||
}
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
this._radarPoolCleanup = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const deviceId in this._radarPool) {
|
||||
if (now - this._radarPool[deviceId].lastBroadcast > 30000) {
|
||||
delete this._radarPool[deviceId];
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
this._loadPairingRecords();
|
||||
|
||||
if (this._wss._server) {
|
||||
this._wss._server.on('request', (req, res) => {
|
||||
if (req.url === '/health') {
|
||||
const totalPeers = Object.values(this._rooms).reduce((sum, room) => sum + Object.keys(room).length, 0);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status: 'healthy',
|
||||
peers: totalPeers,
|
||||
pairingCodes: Object.keys(this._pairingCodes).length,
|
||||
radarDevices: Object.keys(this._radarPool).length,
|
||||
activeScreenShares: Object.keys(this._activeScreenShares).length,
|
||||
canvasRooms: Object.keys(this._canvasRooms).length,
|
||||
uptime: process.uptime()
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Snapdrop is running on port', port);
|
||||
}
|
||||
|
||||
@@ -158,9 +198,33 @@ class SnapdropServer {
|
||||
case 'canvas-snapshot':
|
||||
this._handleCanvasSnapshot(sender, message);
|
||||
break;
|
||||
case 'pairing-code-create':
|
||||
this._handlePairingCodeCreate(sender, message);
|
||||
break;
|
||||
case 'pairing-code-join':
|
||||
this._handlePairingCodeJoin(sender, message);
|
||||
break;
|
||||
case 'radar-broadcast':
|
||||
this._handleRadarBroadcast(sender, message);
|
||||
break;
|
||||
case 'radar-scan':
|
||||
this._handleRadarScan(sender, message);
|
||||
break;
|
||||
case 'screen-share-request':
|
||||
this._handleScreenShareRequest(sender, message);
|
||||
break;
|
||||
case 'screen-share-accept':
|
||||
this._handleScreenShareAccept(sender, message);
|
||||
break;
|
||||
case 'screen-share-reject':
|
||||
this._handleScreenShareReject(sender, message);
|
||||
break;
|
||||
case 'screen-share-stop':
|
||||
this._handleScreenShareStop(sender, message);
|
||||
break;
|
||||
}
|
||||
|
||||
const handledTypes = ['disconnect', 'pong', 'register', 'discoverMyDevices', 'discover', 'transportNegotiate', 'wsRelay', 'pair-request', 'pairRequest', 'pair-accept', 'pairAccept', 'pair-reject', 'pairReject', 'heartbeat', 'ping', 'textMessage', 'fileMeta', 'canvas-stroke', 'canvas-cursor', 'canvas-join', 'canvas-leave', 'canvas-snapshot'];
|
||||
const handledTypes = ['disconnect', 'pong', 'register', 'discoverMyDevices', 'discover', 'transportNegotiate', 'wsRelay', 'pair-request', 'pairRequest', 'pair-accept', 'pairAccept', 'pair-reject', 'pairReject', 'heartbeat', 'ping', 'textMessage', 'fileMeta', 'canvas-stroke', 'canvas-cursor', 'canvas-join', 'canvas-leave', 'canvas-snapshot', 'pairing-code-create', 'pairing-code-join', 'radar-broadcast', 'radar-scan', 'screen-share-request', 'screen-share-accept', 'screen-share-reject', 'screen-share-stop'];
|
||||
if (message.to && !handledTypes.includes(message.type)) {
|
||||
let recipientId = message.to;
|
||||
let recipient = this._resolveTarget(sender, recipientId);
|
||||
@@ -695,6 +759,228 @@ class SnapdropServer {
|
||||
}
|
||||
}
|
||||
|
||||
_handlePairingCodeCreate(sender, message) {
|
||||
const payload = message.payload || message.data || {};
|
||||
const CHARS = '23456789ABCDEFGHJKMNPQRSTUVWXYZ';
|
||||
let code = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
code += CHARS.charAt(Math.floor(Math.random() * CHARS.length));
|
||||
}
|
||||
for (const existingCode in this._pairingCodes) {
|
||||
if (this._pairingCodes[existingCode].creatorId === sender.id) {
|
||||
delete this._pairingCodes[existingCode];
|
||||
}
|
||||
}
|
||||
this._pairingCodes[code] = {
|
||||
creatorId: sender.id,
|
||||
creatorFingerprint: sender.fingerprint || '',
|
||||
creatorAlias: payload.alias || sender.name.displayName,
|
||||
creatorDeviceModel: payload.deviceModel || sender.deviceModel || '',
|
||||
creatorDeviceType: payload.deviceType || sender.name.type || '',
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + 300000,
|
||||
used: false
|
||||
};
|
||||
this._send(sender, {
|
||||
type: 'pairing-code-created',
|
||||
pairingCode: code,
|
||||
expiresAt: this._pairingCodes[code].expiresAt
|
||||
});
|
||||
console.log('Pairing code created:', code, 'by', sender.id, '(' + (sender.fingerprint || '') + ')');
|
||||
}
|
||||
|
||||
_handlePairingCodeJoin(sender, message) {
|
||||
const payload = message.payload || message.data || {};
|
||||
const code = (payload.pairingCode || '').toUpperCase().trim();
|
||||
if (!code) {
|
||||
this._send(sender, { type: 'pairing-matched', success: false, error: '配对码不能为空' });
|
||||
return;
|
||||
}
|
||||
const entry = this._pairingCodes[code];
|
||||
if (!entry) {
|
||||
this._send(sender, { type: 'pairing-matched', success: false, error: '配对码不存在或已过期' });
|
||||
return;
|
||||
}
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
delete this._pairingCodes[code];
|
||||
this._send(sender, { type: 'pairing-matched', success: false, error: '配对码已过期' });
|
||||
return;
|
||||
}
|
||||
if (entry.used) {
|
||||
this._send(sender, { type: 'pairing-matched', success: false, error: '配对码已被使用' });
|
||||
return;
|
||||
}
|
||||
if (entry.creatorId === sender.id) {
|
||||
this._send(sender, { type: 'pairing-matched', success: false, error: '不能使用自己创建的配对码' });
|
||||
return;
|
||||
}
|
||||
entry.used = true;
|
||||
const creatorPeer = this._findPeer(entry.creatorId);
|
||||
this._send(sender, {
|
||||
type: 'pairing-matched',
|
||||
success: true,
|
||||
pairingCode: code,
|
||||
peer: {
|
||||
id: entry.creatorId,
|
||||
fingerprint: entry.creatorFingerprint,
|
||||
alias: entry.creatorAlias,
|
||||
deviceModel: entry.creatorDeviceModel,
|
||||
deviceType: entry.creatorDeviceType
|
||||
}
|
||||
});
|
||||
if (creatorPeer) {
|
||||
this._send(creatorPeer, {
|
||||
type: 'pairing-matched',
|
||||
success: true,
|
||||
pairingCode: code,
|
||||
peer: {
|
||||
id: sender.id,
|
||||
fingerprint: sender.fingerprint || '',
|
||||
alias: sender.name.displayName,
|
||||
deviceModel: sender.deviceModel || '',
|
||||
deviceType: sender.name.type || ''
|
||||
}
|
||||
});
|
||||
}
|
||||
delete this._pairingCodes[code];
|
||||
console.log('Pairing matched via code:', code, 'creator:', entry.creatorId, 'joiner:', sender.id);
|
||||
}
|
||||
|
||||
_handleRadarBroadcast(sender, message) {
|
||||
const payload = message.payload || message.data || {};
|
||||
this._radarPool[sender.id] = {
|
||||
deviceId: sender.id,
|
||||
fingerprint: sender.fingerprint || '',
|
||||
alias: payload.alias || sender.name.displayName,
|
||||
deviceModel: payload.deviceModel || sender.deviceModel || '',
|
||||
deviceType: payload.deviceType || sender.name.type || '',
|
||||
ipRange: payload.ipRange || sender.ipRange || '',
|
||||
ipCity: payload.ipCity || sender.ipCity || '',
|
||||
ip: sender.ip,
|
||||
userId: sender.userId || '',
|
||||
lastBroadcast: Date.now()
|
||||
};
|
||||
this._send(sender, { type: 'radar-broadcast', success: true });
|
||||
}
|
||||
|
||||
_handleRadarScan(sender, message) {
|
||||
const payload = message.payload || message.data || {};
|
||||
const myIpRange = payload.ipRange || sender.ipRange || '';
|
||||
const myIpCity = payload.ipCity || sender.ipCity || '';
|
||||
const devices = [];
|
||||
for (const deviceId in this._radarPool) {
|
||||
const entry = this._radarPool[deviceId];
|
||||
if (deviceId === sender.id) continue;
|
||||
if (Date.now() - entry.lastBroadcast > 30000) continue;
|
||||
let matchType = 'remote';
|
||||
if (sender.userId && entry.userId && sender.userId === entry.userId) {
|
||||
matchType = 'my-device';
|
||||
} else if (myIpRange && entry.ipRange && myIpRange === entry.ipRange) {
|
||||
matchType = 'same-network';
|
||||
} else if (myIpCity && entry.ipCity && myIpCity === entry.ipCity) {
|
||||
matchType = 'same-city';
|
||||
}
|
||||
devices.push({
|
||||
id: entry.deviceId,
|
||||
fingerprint: entry.fingerprint,
|
||||
alias: entry.alias,
|
||||
deviceModel: entry.deviceModel,
|
||||
deviceType: entry.deviceType,
|
||||
ipCity: entry.ipCity,
|
||||
matchType: matchType
|
||||
});
|
||||
}
|
||||
devices.sort((a, b) => {
|
||||
const order = { 'my-device': 0, 'same-network': 1, 'same-city': 2, 'remote': 3 };
|
||||
return (order[a.matchType] || 4) - (order[b.matchType] || 4);
|
||||
});
|
||||
this._send(sender, { type: 'radar-devices', devices: devices });
|
||||
}
|
||||
|
||||
_handleScreenShareRequest(sender, message) {
|
||||
const targetId = message.to;
|
||||
if (!targetId) return;
|
||||
const targetPeer = this._resolveTarget(sender, targetId);
|
||||
if (!targetPeer) {
|
||||
this._send(sender, {
|
||||
type: 'screen-share-response',
|
||||
from: targetId,
|
||||
success: false,
|
||||
error: 'Target device not found or offline'
|
||||
});
|
||||
return;
|
||||
}
|
||||
const payload = message.payload || message.data || {};
|
||||
const direction = payload.direction || 'view';
|
||||
this._send(targetPeer, {
|
||||
type: 'screen-share-request',
|
||||
from: sender.fingerprint || sender.id,
|
||||
fromPeerId: sender.id,
|
||||
direction: direction,
|
||||
requestId: Date.now().toString(36) + Math.random().toString(36).substr(2),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
console.log('Screen share request:', sender.id, '->', targetId, 'direction:', direction);
|
||||
}
|
||||
|
||||
_handleScreenShareAccept(sender, message) {
|
||||
const targetId = message.to;
|
||||
if (!targetId) return;
|
||||
const targetPeer = this._resolveTarget(sender, targetId);
|
||||
if (!targetPeer) {
|
||||
this._send(sender, { type: 'screen-share-response', success: false, error: 'Target device not found' });
|
||||
return;
|
||||
}
|
||||
const payload = message.payload || message.data || {};
|
||||
const shareKey = [sender.id, targetId].sort().join(':');
|
||||
this._activeScreenShares[shareKey] = {
|
||||
sharer: payload.direction === 'share' ? sender.id : targetId,
|
||||
viewer: payload.direction === 'share' ? targetId : sender.id,
|
||||
direction: payload.direction || 'view',
|
||||
startedAt: Date.now()
|
||||
};
|
||||
this._send(targetPeer, {
|
||||
type: 'screen-share-accept',
|
||||
from: sender.fingerprint || sender.id,
|
||||
fromPeerId: sender.id,
|
||||
direction: payload.direction || 'view',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
console.log('Screen share accepted:', sender.id, '<->', targetId);
|
||||
}
|
||||
|
||||
_handleScreenShareReject(sender, message) {
|
||||
const targetId = message.to;
|
||||
if (!targetId) return;
|
||||
const targetPeer = this._resolveTarget(sender, targetId);
|
||||
if (targetPeer) {
|
||||
this._send(targetPeer, {
|
||||
type: 'screen-share-reject',
|
||||
from: sender.fingerprint || sender.id,
|
||||
fromPeerId: sender.id,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
console.log('Screen share rejected:', sender.id, 'rejected request from', targetId);
|
||||
}
|
||||
|
||||
_handleScreenShareStop(sender, message) {
|
||||
const targetId = message.to;
|
||||
if (!targetId) return;
|
||||
const shareKey = [sender.id, targetId].sort().join(':');
|
||||
delete this._activeScreenShares[shareKey];
|
||||
const targetPeer = this._resolveTarget(sender, targetId);
|
||||
if (targetPeer) {
|
||||
this._send(targetPeer, {
|
||||
type: 'screen-share-stop',
|
||||
from: sender.fingerprint || sender.id,
|
||||
fromPeerId: sender.id,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
console.log('Screen share stopped:', sender.id, '<->', targetId);
|
||||
}
|
||||
|
||||
_joinRoom(peer) {
|
||||
if (!this._rooms[peer.ip]) {
|
||||
this._rooms[peer.ip] = {};
|
||||
@@ -763,6 +1049,34 @@ class SnapdropServer {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._radarPool[peer.id]) {
|
||||
delete this._radarPool[peer.id];
|
||||
}
|
||||
|
||||
for (const shareKey in this._activeScreenShares) {
|
||||
const share = this._activeScreenShares[shareKey];
|
||||
if (share.sharer === peer.id || share.viewer === peer.id) {
|
||||
const otherPeerId = share.sharer === peer.id ? share.viewer : share.sharer;
|
||||
const otherPeer = this._findPeer(otherPeerId);
|
||||
if (otherPeer) {
|
||||
this._send(otherPeer, {
|
||||
type: 'screen-share-stop',
|
||||
from: peer.fingerprint || peer.id,
|
||||
fromPeerId: peer.id,
|
||||
reason: 'peer-disconnected',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
delete this._activeScreenShares[shareKey];
|
||||
}
|
||||
}
|
||||
|
||||
for (const code in this._pairingCodes) {
|
||||
if (this._pairingCodes[code].creatorId === peer.id) {
|
||||
delete this._pairingCodes[code];
|
||||
}
|
||||
}
|
||||
|
||||
peer.socket.terminate();
|
||||
if (!Object.keys(this._rooms[peer.ip]).length) {
|
||||
delete this._rooms[peer.ip];
|
||||
|
||||
Reference in New Issue
Block a user