feat: release desktop 0.1.9 with device dedupe and new branding
This commit is contained in:
@@ -2750,6 +2750,62 @@ const DeviceSessionDB = {
|
||||
`).all(Math.floor(uid), safeLimit);
|
||||
},
|
||||
|
||||
listActiveByDevice(userId, options = {}) {
|
||||
const uid = Number(userId);
|
||||
if (!Number.isFinite(uid) || uid <= 0) return [];
|
||||
|
||||
const clientType = this._normalizeClientType(options.clientType);
|
||||
const deviceId = this._normalizeText(options.deviceId, 128);
|
||||
const deviceName = this._normalizeText(options.deviceName, 120);
|
||||
const platform = this._normalizeText(options.platform, 80);
|
||||
const safeLimit = Math.max(1, Math.min(50, Math.floor(Number(options.limit) || 20)));
|
||||
|
||||
if (deviceId) {
|
||||
return db.prepare(`
|
||||
SELECT *
|
||||
FROM user_device_sessions
|
||||
WHERE user_id = ?
|
||||
AND client_type = ?
|
||||
AND device_id = ?
|
||||
AND revoked_at IS NULL
|
||||
AND expires_at > datetime('now', 'localtime')
|
||||
ORDER BY datetime(COALESCE(last_active_at, created_at)) DESC, created_at DESC
|
||||
LIMIT ?
|
||||
`).all(Math.floor(uid), clientType, deviceId, safeLimit);
|
||||
}
|
||||
|
||||
if (clientType !== 'desktop') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const extraConditions = [];
|
||||
const params = [Math.floor(uid), clientType];
|
||||
if (deviceName) {
|
||||
extraConditions.push("COALESCE(device_name, '') = ?");
|
||||
params.push(deviceName);
|
||||
}
|
||||
if (platform) {
|
||||
extraConditions.push("COALESCE(platform, '') = ?");
|
||||
params.push(platform);
|
||||
}
|
||||
if (extraConditions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
params.push(safeLimit);
|
||||
return db.prepare(`
|
||||
SELECT *
|
||||
FROM user_device_sessions
|
||||
WHERE user_id = ?
|
||||
AND client_type = ?
|
||||
AND ${extraConditions.join(' AND ')}
|
||||
AND revoked_at IS NULL
|
||||
AND expires_at > datetime('now', 'localtime')
|
||||
ORDER BY datetime(COALESCE(last_active_at, created_at)) DESC, created_at DESC
|
||||
LIMIT ?
|
||||
`).all(...params);
|
||||
},
|
||||
|
||||
touch(sessionId, options = {}) {
|
||||
const sid = this._normalizeSessionId(sessionId);
|
||||
if (!sid) return { changes: 0 };
|
||||
|
||||
@@ -112,11 +112,11 @@ const DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS = Math.max(
|
||||
10,
|
||||
Math.min(3600, Number(process.env.DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS || 30))
|
||||
);
|
||||
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.8';
|
||||
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.9';
|
||||
const DEFAULT_DESKTOP_INSTALLER_URL = process.env.DESKTOP_INSTALLER_URL || '';
|
||||
const DEFAULT_DESKTOP_RELEASE_NOTES = process.env.DESKTOP_RELEASE_NOTES || '';
|
||||
const DESKTOP_INSTALLERS_DIR = path.resolve(__dirname, '../frontend/downloads');
|
||||
const DESKTOP_INSTALLER_FILE_PATTERN = /^wanwan-cloud-desktop_.*_x64-setup\.exe$/i;
|
||||
const DESKTOP_INSTALLER_FILE_PATTERN = /^(wanwan-cloud-desktop|玩玩云)_.*_x64-setup\.exe$/i;
|
||||
const RESUMABLE_UPLOAD_SESSION_TTL_MS = Number(process.env.RESUMABLE_UPLOAD_SESSION_TTL_MS || (24 * 60 * 60 * 1000)); // 24小时
|
||||
const RESUMABLE_UPLOAD_CHUNK_SIZE_BYTES = Number(process.env.RESUMABLE_UPLOAD_CHUNK_SIZE_BYTES || (4 * 1024 * 1024)); // 4MB
|
||||
const RESUMABLE_UPLOAD_MAX_CHUNK_SIZE_BYTES = Number(process.env.RESUMABLE_UPLOAD_MAX_CHUNK_SIZE_BYTES || (32 * 1024 * 1024)); // 32MB
|
||||
@@ -2931,7 +2931,7 @@ function resolveClientType(clientType, userAgent = '') {
|
||||
const normalized = normalizeClientType(clientType);
|
||||
if (normalized) return normalized;
|
||||
const ua = String(userAgent || '').toLowerCase();
|
||||
if (ua.includes('tauri') || ua.includes('electron') || ua.includes('wanwan-cloud-desktop')) {
|
||||
if (ua.includes('tauri') || ua.includes('electron') || ua.includes('wanwan-cloud-desktop') || ua.includes('玩玩云')) {
|
||||
return 'desktop';
|
||||
}
|
||||
return detectDeviceTypeFromUserAgent(ua) === 'mobile' ? 'mobile' : 'web';
|
||||
@@ -2987,6 +2987,46 @@ function formatOnlineDeviceRecord(row, currentSessionId = '') {
|
||||
};
|
||||
}
|
||||
|
||||
function buildDeviceDedupKey(row = {}) {
|
||||
const sessionId = String(row?.session_id || '').trim();
|
||||
const clientType = String(row?.client_type || 'web').trim().toLowerCase() || 'web';
|
||||
const deviceId = String(row?.device_id || '').trim();
|
||||
if (deviceId) {
|
||||
return `${clientType}:id:${deviceId}`;
|
||||
}
|
||||
|
||||
if (clientType === 'desktop') {
|
||||
const deviceName = String(row?.device_name || '').trim().toLowerCase();
|
||||
const platform = String(row?.platform || '').trim().toLowerCase();
|
||||
return `${clientType}:fallback:${deviceName}|${platform}`;
|
||||
}
|
||||
|
||||
return `session:${sessionId}`;
|
||||
}
|
||||
|
||||
function dedupeOnlineDeviceRows(rows = [], currentSessionId = '') {
|
||||
if (!Array.isArray(rows) || rows.length <= 1) return Array.isArray(rows) ? rows : [];
|
||||
const currentSid = String(currentSessionId || '').trim();
|
||||
const deduped = new Map();
|
||||
for (const row of rows) {
|
||||
const key = buildDeviceDedupKey(row);
|
||||
const sid = String(row?.session_id || '').trim();
|
||||
if (!deduped.has(key)) {
|
||||
deduped.set(key, row);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPicked = deduped.get(key);
|
||||
const pickedSid = String(currentPicked?.session_id || '').trim();
|
||||
const incomingIsCurrent = !!currentSid && sid === currentSid;
|
||||
const pickedIsCurrent = !!currentSid && pickedSid === currentSid;
|
||||
if (incomingIsCurrent && !pickedIsCurrent) {
|
||||
deduped.set(key, row);
|
||||
}
|
||||
}
|
||||
return Array.from(deduped.values());
|
||||
}
|
||||
|
||||
function normalizeTimeHHmm(value) {
|
||||
if (typeof value !== 'string') return null;
|
||||
const trimmed = value.trim();
|
||||
@@ -4228,8 +4268,35 @@ app.post('/api/login',
|
||||
});
|
||||
let sessionId = null;
|
||||
try {
|
||||
let reusedSessionId = '';
|
||||
const shouldReuseDeviceSession = Boolean(deviceContext.deviceId) || deviceContext.clientType === 'desktop';
|
||||
if (shouldReuseDeviceSession) {
|
||||
const matchedDeviceSessions = DeviceSessionDB.listActiveByDevice(user.id, {
|
||||
clientType: deviceContext.clientType,
|
||||
deviceId: deviceContext.deviceId,
|
||||
deviceName: deviceContext.deviceName,
|
||||
platform: deviceContext.platform,
|
||||
limit: 20
|
||||
});
|
||||
if (matchedDeviceSessions.length > 0) {
|
||||
reusedSessionId = String(matchedDeviceSessions[0]?.session_id || '').trim();
|
||||
let revokedCount = 0;
|
||||
for (const matched of matchedDeviceSessions) {
|
||||
const duplicateSid = String(matched?.session_id || '').trim();
|
||||
if (!duplicateSid || duplicateSid === reusedSessionId) continue;
|
||||
const revokeResult = DeviceSessionDB.revoke(duplicateSid, user.id, 'dedupe_login_same_device');
|
||||
if (Number(revokeResult?.changes || 0) > 0) {
|
||||
revokedCount += 1;
|
||||
}
|
||||
}
|
||||
if (revokedCount > 0) {
|
||||
console.log(`[在线设备] 登录会话去重 user=${user.id}, revoked=${revokedCount}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createdSession = DeviceSessionDB.create({
|
||||
sessionId: crypto.randomBytes(24).toString('hex'),
|
||||
sessionId: reusedSessionId || crypto.randomBytes(24).toString('hex'),
|
||||
userId: user.id,
|
||||
clientType: deviceContext.clientType,
|
||||
deviceId: deviceContext.deviceId,
|
||||
@@ -4426,9 +4493,9 @@ app.get('/api/user/profile', authMiddleware, (req, res) => {
|
||||
app.get('/api/user/online-devices', authMiddleware, (req, res) => {
|
||||
try {
|
||||
const currentSessionId = String(req.authSessionId || '').trim();
|
||||
const devices = DeviceSessionDB
|
||||
.listActiveByUser(req.user.id, 80)
|
||||
.map((row) => formatOnlineDeviceRecord(row, currentSessionId));
|
||||
const rawRows = DeviceSessionDB.listActiveByUser(req.user.id, 80);
|
||||
const dedupedRows = dedupeOnlineDeviceRows(rawRows, currentSessionId);
|
||||
const devices = dedupedRows.map((row) => formatOnlineDeviceRecord(row, currentSessionId));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user