From 75884d7b4b101b81fe6cff8adc03b60cec5091de Mon Sep 17 00:00:00 2001 From: stary <834207172@qq.com> Date: Sat, 16 May 2026 22:22:34 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=9F=90=E4=B8=80=E6=88=BF?= =?UTF-8?q?=E9=97=B4=E7=9A=84=E7=94=A8=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/public/onebyone/connectview.js | 120 ++++++++++++++++++++++++-- client/public/onebyone/index.html | 10 +++ src/class/httphandler.ts | 65 +++++++++++++- src/class/websockethandler.ts | 71 ++++++++++++++- src/signaling.ts | 1 + 5 files changed, 258 insertions(+), 9 deletions(-) diff --git a/client/public/onebyone/connectview.js b/client/public/onebyone/connectview.js index 03753a7..567cfd9 100644 --- a/client/public/onebyone/connectview.js +++ b/client/public/onebyone/connectview.js @@ -10,6 +10,7 @@ const MAX_AVATAR_SIZE = 2 * 1024 * 1024; // 2MB // WebSocket连接状态更新回调 let onWsStatusChange = null; +let cachedOnlineUsers = []; /** * 设置WebSocket状态变化回调 @@ -63,15 +64,27 @@ export async function initWebSocket() { async function getAllConnectionIds() { showNotification('正在获取连接ID列表...'); try { - const response = await fetch('/signaling/connection-ids'); - if (!response.ok) { + const [connectionResponse, usersResponse] = await Promise.all([ + fetch('/signaling/connection-ids'), + fetch('/signaling/users') + ]); + + if (!connectionResponse.ok) { throw new Error('Failed to fetch connection IDs'); } - const data = await response.json(); - displayConnectionIds(data.connectionIds); + + if (!usersResponse.ok) { + throw new Error('Failed to fetch online users'); + } + + const connectionData = await connectionResponse.json(); + const usersData = await usersResponse.json(); + cachedOnlineUsers = Array.isArray(usersData.users) ? usersData.users : []; + displayConnectionIds(connectionData.connectionIds || []); + displayOnlineUsers(cachedOnlineUsers); } catch (error) { console.error('Error fetching connection IDs:', error); - showNotification('获取连接ID失败', 'error'); + showNotification('获取连接信息失败', 'error'); } } @@ -108,6 +121,100 @@ function displayConnectionIds(connectionIds) { } } +/** + * 转义HTML特殊字符 + * @param {string} value - 原始字符串 + * @returns {string} 安全字符串 + */ +function escapeHtml(value) { + return String(value || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * 按连接ID分组显示在线用户 + * @param {Array} users - 在线用户列表 + * @param {string} selectedConnectionId - 当前选中的连接ID + */ +function displayOnlineUsers(users, selectedConnectionId = '') { + const onlineUsersList = document.getElementById('onlineUsersList'); + const usersContainer = document.getElementById('usersContainer'); + const onlineUsersSummary = document.getElementById('onlineUsersSummary'); + + if (!onlineUsersList || !usersContainer || !onlineUsersSummary) { + return; + } + + const filteredUsers = selectedConnectionId + ? users.filter(user => user.connectionId === selectedConnectionId) + : users; + + onlineUsersSummary.textContent = selectedConnectionId + ? `${filteredUsers.length} 人在线 · 房间 ${selectedConnectionId}` + : `${filteredUsers.length} 人在线`; + + usersContainer.innerHTML = ''; + + if (filteredUsers.length === 0) { + usersContainer.innerHTML = '

暂无在线用户

'; + onlineUsersList.classList.remove('hidden'); + return; + } + + const groupedUsers = filteredUsers.reduce((groups, user) => { + const connectionId = user.connectionId || 'unknown'; + if (!groups[connectionId]) { + groups[connectionId] = []; + } + groups[connectionId].push(user); + return groups; + }, {}); + + Object.entries(groupedUsers).forEach(([connectionId, roomUsers]) => { + const section = document.createElement('div'); + section.className = 'rounded-lg border border-white/10 bg-white/5 p-3'; + + const roomTitle = document.createElement('div'); + roomTitle.className = 'flex items-center justify-between mb-2'; + roomTitle.innerHTML = ` + ${escapeHtml(connectionId)} + ${roomUsers.length} 人 + `; + section.appendChild(roomTitle); + + const roomList = document.createElement('div'); + roomList.className = 'space-y-2'; + + roomUsers.forEach((user) => { + const userName = user.name || user.userId || '匿名用户'; + const avatar = user.avatar || '/images/p2.png'; + const roleLabel = user.role === 'host' ? '房主' : '成员'; + const userItem = document.createElement('div'); + userItem.className = 'flex items-center justify-between rounded-lg bg-black/20 px-3 py-2'; + userItem.innerHTML = ` +
+ ${escapeHtml(userName)} +
+
${escapeHtml(userName)}
+
${escapeHtml(user.userId || user.participantId || '未设置ID')}
+
+
+ ${roleLabel} + `; + roomList.appendChild(userItem); + }); + + section.appendChild(roomList); + usersContainer.appendChild(section); + }); + + onlineUsersList.classList.remove('hidden'); +} + /** * 选择连接ID * @param {string} id - 连接ID @@ -116,6 +223,7 @@ function selectConnectionId(id) { const connectionIdInput = document.getElementById('connectionIdInput'); if (connectionIdInput) { connectionIdInput.value = id; + displayOnlineUsers(cachedOnlineUsers, id); showNotification(`已选择连接ID: ${id}`); } } @@ -356,4 +464,4 @@ window.selectConnectionId = selectConnectionId; window.saveSettings = saveSettings; window.handleAvatarUpload = handleAvatarUpload; window.copyUserId = copyUserId; -window.toggleSettingsMenu = toggleSettingsMenu; \ No newline at end of file +window.toggleSettingsMenu = toggleSettingsMenu; diff --git a/client/public/onebyone/index.html b/client/public/onebyone/index.html index d919d9a..c2ed003 100644 --- a/client/public/onebyone/index.html +++ b/client/public/onebyone/index.html @@ -106,6 +106,16 @@ + +
diff --git a/src/class/httphandler.ts b/src/class/httphandler.ts index e374ed5..3af6034 100644 --- a/src/class/httphandler.ts +++ b/src/class/httphandler.ts @@ -7,7 +7,7 @@ import Offer from './offer'; import Answer from './answer'; import Candidate from './candidate'; import { v4 as uuid } from 'uuid'; -import { onGetAllConnectionIds } from './websockethandler'; +import { onGetAllConnectionIds, onGetOnlineUsers as onGetWsOnlineUsers } from './websockethandler'; import { log, LogLevel } from '../log'; /** * 断开连接记录类 @@ -1106,6 +1106,66 @@ function getAllConnectionIds(req: Request, res: Response): void { // 返回JSON响应,包含连接ID列表和总数量 res.json({ connectionIds: connectionIds, totalCount: connectionIds.length }); } + +/** + * 获取在线WebSocket用户列表 + * @param req HTTP请求对象 + * @param res HTTP响应对象 + */ +/** + * @swagger + * /signaling/users: + * get: + * summary: 获取在线WebSocket用户列表 + * description: 获取所有在线WebSocket用户,支持按 connectionId 过滤指定房间内的用户 + * parameters: + * - in: query + * name: connectionId + * schema: + * type: string + * required: false + * description: 连接ID,传入时仅返回该房间内的在线用户 + * responses: + * 200: + * description: 成功获取在线用户列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * users: + * type: array + * items: + * type: object + * properties: + * connectionId: + * type: string + * description: 所属连接ID + * participantId: + * type: string + * description: 参与者ID + * role: + * type: string + * enum: [host, participant] + * description: 角色 + * userId: + * type: string + * description: 用户ID + * name: + * type: string + * description: 用户名称 + * avatar: + * type: string + * description: 用户头像URL + * totalCount: + * type: number + * description: 在线用户总数 + */ +function getOnlineUsers(req: Request, res: Response): void { + const connectionId = typeof req.query.connectionId === 'string' ? req.query.connectionId : undefined; + const users = onGetWsOnlineUsers(connectionId); + res.json({ users: users, totalCount: users.length }); +} /** * 导出HTTP处理器函数 */ @@ -1125,5 +1185,6 @@ export { postAnswer, // 处理answer信令消息 postCandidate, // 处理candidate信令消息 onGetConnections, // 获取房间和用户信息 - getAllConnectionIds // 获取所有连接ID + getAllConnectionIds, // 获取所有连接ID + getOnlineUsers // 获取在线WebSocket用户列表 }; diff --git a/src/class/websockethandler.ts b/src/class/websockethandler.ts index 53eb5cb..4f53ce2 100644 --- a/src/class/websockethandler.ts +++ b/src/class/websockethandler.ts @@ -29,6 +29,21 @@ interface ConnectionGroup { participants: Set; } +interface UserInfo { + id: string; + name: string; + avatar?: string; +} + +interface OnlineUser { + connectionId: string; + participantId: string; + role: 'host' | 'participant'; + userId: string; + name: string; + avatar: string; +} + /** * 连接组映射 * 键: connectionId @@ -440,6 +455,53 @@ function onGetAllConnectionIds(): string[] { return connectionIds; } +/** + * 将WebSocket连接转换为在线用户信息 + * @param ws WebSocket连接实例 + * @param connectionId 连接ID + * @param role 用户角色 + * @returns 在线用户信息 + */ +function toOnlineUser(ws: WebSocket, connectionId: string, role: 'host' | 'participant'): OnlineUser { + const userInfo = ((ws as any).userInfo || {}) as UserInfo; + return { + connectionId: connectionId, + participantId: (ws as any).participantId || '', + role: role, + userId: userInfo.id || '', + name: userInfo.name || '', + avatar: userInfo.avatar || '' + }; +} + +/** + * 获取在线WebSocket用户列表 + * @param connectionId 可选的连接ID,传入时仅返回指定房间的在线用户 + * @returns 在线用户列表 + */ +function onGetOnlineUsers(connectionId?: string): OnlineUser[] { + if (connectionId) { + const group = connectionGroup.get(connectionId); + if (!group) { + return []; + } + + return [ + toOnlineUser(group.host, connectionId, 'host'), + ...Array.from(group.participants).map((participantWs) => toOnlineUser(participantWs, connectionId, 'participant')) + ]; + } + + const onlineUsers: OnlineUser[] = []; + connectionGroup.forEach((group, currentConnectionId) => { + onlineUsers.push(toOnlineUser(group.host, currentConnectionId, 'host')); + group.participants.forEach((participantWs) => { + onlineUsers.push(toOnlineUser(participantWs, currentConnectionId, 'participant')); + }); + }); + return onlineUsers; +} + /** * 处理chat-message信令(1对多模式) * host的消息转发给所有participants,participant的消息转发给host @@ -451,6 +513,13 @@ function onMessage(ws: WebSocket, message: any): void { const connectionId = message.connectionId; const chatMessage = message.message; const senderParticipantId = (ws as any).participantId; + if (chatMessage && chatMessage.type === 'user-info' && chatMessage.data) { + (ws as any).userInfo = { + id: chatMessage.data.id || '', + name: chatMessage.data.name || '匿名用户', + avatar: chatMessage.data.avatar || '' + }; + } chatMessage.participantId = senderParticipantId; chatMessage.connectionId = connectionId; if (connectionGroup.has(connectionId)) { @@ -476,4 +545,4 @@ function onMessage(ws: WebSocket, message: any): void { /** * 导出WebSocket处理器函数 */ -export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate, onCallConnectionId, onBroadcast, onGetAllConnectionIds, AddHeartbeat, RemoveHeartbeat, onMessage, isHost, broadcastToGroup, connectionGroup }; +export { reset, add, remove, onConnect, onDisconnect, onOffer, onAnswer, onCandidate, onCallConnectionId, onBroadcast, onGetAllConnectionIds, onGetOnlineUsers, AddHeartbeat, RemoveHeartbeat, onMessage, isHost, broadcastToGroup, connectionGroup }; diff --git a/src/signaling.ts b/src/signaling.ts index c523e82..6983daf 100644 --- a/src/signaling.ts +++ b/src/signaling.ts @@ -5,6 +5,7 @@ const router: express.Router = express.Router(); // 不需要会话ID的路由 router.get('/connection-ids', handler.getAllConnectionIds); +router.get('/users', handler.getOnlineUsers); // 需要会话ID的路由 router.use(handler.checkSessionId);