文件
BizGaze_Remote/agent/renderer/agent.js
T
2026-06-05 17:29:09 +05:30

136 行
4.8 KiB
JavaScript

// Agent renderer: connects to the signaling server, handles consent,
// captures the screen, and streams it over WebRTC to the technician's viewer.
// Remote input events arrive on the data channel and are handed to the main
// process (via the preload bridge) for OS-level injection.
const statusEl = document.getElementById('status');
const consentBox = document.getElementById('consentBox');
const indicator = document.getElementById('indicator');
const logEl = document.getElementById('log');
let cfg = null;
let ws = null;
let pc = null;
let localStream = null;
let currentSessionId = null;
const log = (t) => { const d = document.createElement('div'); d.textContent = t; logEl.appendChild(d); logEl.scrollTop = logEl.scrollHeight; };
const setStatus = (t, cls = '') => { statusEl.textContent = t; statusEl.className = 'status ' + cls; };
window.agent.onConfig((c) => {
cfg = c;
if (!cfg.enrollToken) {
setStatus('No enroll token. Set AGENT_ENROLL_TOKEN and restart.', 'warn');
return;
}
connect();
});
function wsUrl() {
const u = new URL(cfg.serverUrl);
const proto = u.protocol === 'https:' ? 'wss:' : 'ws:';
return `${proto}//${u.host}/ws`;
}
function connect() {
setStatus('Connecting to server…');
ws = new WebSocket(wsUrl());
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'agent-hello', enrollToken: cfg.enrollToken }));
};
ws.onmessage = (e) => handle(JSON.parse(e.data));
ws.onclose = () => { setStatus('Disconnected. Reconnecting in 3s…', 'warn'); setTimeout(connect, 3000); };
}
async function handle(m) {
switch (m.type) {
case 'agent-registered':
setStatus(`Online as "${m.name}". Waiting for sessions.`, 'on');
log(`registered: ${m.name}`);
break;
case 'session-request':
if (m.unattended) { log(`unattended session ${m.sessionId} — auto-granting`); grant(m.sessionId); }
else showConsent(m);
break;
case 'start-stream':
currentSessionId = m.sessionId;
await startStreaming();
break;
case 'answer':
if (pc) await pc.setRemoteDescription(new RTCSessionDescription(m.sdp));
break;
case 'ice-candidate':
if (m.candidate && pc) await pc.addIceCandidate(new RTCIceCandidate(m.candidate));
break;
case 'session-ended':
teardown();
break;
case 'error':
setStatus('Server: ' + m.message, 'warn');
break;
}
}
function showConsent(m) {
consentBox.innerHTML = `
<div class="consent">
<h2>Allow remote support?</h2>
<p class="muted"><b>${escapeHtml(m.technician)}</b> is requesting to view and control this PC.</p>
<button class="grant" id="grantBtn">Allow</button>
<button class="deny" id="denyBtn">Deny</button>
</div>`;
document.getElementById('grantBtn').onclick = () => { consentBox.innerHTML = ''; grant(m.sessionId); };
document.getElementById('denyBtn').onclick = () => { consentBox.innerHTML = ''; deny(m.sessionId); };
}
const grant = (sessionId) => ws.send(JSON.stringify({ type: 'consent', sessionId, granted: true }));
const deny = (sessionId) => ws.send(JSON.stringify({ type: 'consent', sessionId, granted: false }));
async function startStreaming() {
setStatus('Sharing screen with technician…', 'on');
indicator.classList.add('show');
try {
localStream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: { ideal: 30 } }, audio: false });
} catch (err) {
log('getDisplayMedia failed: ' + err.message);
setStatus('Screen capture failed.', 'warn');
return;
}
pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
localStream.getTracks().forEach((t) => pc.addTrack(t, localStream));
// Viewer creates the input data channel; we receive it here.
pc.ondatachannel = (ev) => {
const ch = ev.channel;
ch.onmessage = (msg) => {
let evt; try { evt = JSON.parse(msg.data); } catch { return; }
window.agent.injectInput(evt); // -> main process -> OS injection
};
};
pc.onicecandidate = (ev) => {
if (ev.candidate) ws.send(JSON.stringify({ type: 'ice-candidate', sessionId: currentSessionId, candidate: ev.candidate }));
};
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: 'offer', sessionId: currentSessionId, sdp: pc.localDescription }));
log('sent offer for session ' + currentSessionId);
}
function teardown() {
indicator.classList.remove('show');
window.agent.sessionEnded();
if (localStream) { localStream.getTracks().forEach((t) => t.stop()); localStream = null; }
if (pc) { pc.close(); pc = null; }
currentSessionId = null;
setStatus('Session ended. Waiting for sessions.', 'on');
}
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }