Files
agent/main.js
bangae1 e5aa5d1d5c asdf
2025-10-22 14:56:52 +09:00

203 lines
6.0 KiB
JavaScript

// main.js
const { app, BrowserWindow, Tray, Menu, nativeImage, dialog, ipcMain } = require('electron');
const path = require('path');
const io = require('socket.io-client');
const { desktopCapturer } = require('electron');
const { mouse, Point, straightTo, keyboard, Key, clipboard, Button} = require('@nut-tree-fork/nut-js')
// 고유 직원 ID
const EMPLOYEE_ID = "psn14020";
// 시그널링 서버 주소
const SIGNALING_SERVER = 'http://192.168.0.148:3001';
let tray = null;
let mainWindow = null;
let socket = null;
let displayId = null;
let offer = null;
let sources = null;
// 자동 시작 설정
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: true
});
// 백그라운드 창 (화면 공유용)
function createHiddenWindow() {
const win = new BrowserWindow({
show: true,
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
});
win.webContents.openDevTools();
win.loadFile('index.html');
return win;
}
// 트레이 아이콘
function createTray() {
const iconPath = path.join(__dirname, 'resources', 'icon.png');
const icon = nativeImage.createFromPath(iconPath);
tray = new Tray(icon || undefined);
const contextMenu = Menu.buildFromTemplate([
{ label: `직원 ID: ${EMPLOYEE_ID}`, enabled: false },
{ label: '종료', click: () => app.quit() }
]);
tray.setToolTip('원격 지원 에이전트 실행 중');
tray.setContextMenu(contextMenu);
}
// 🖥️ 사용 가능한 디스플레이 목록 전송
async function sendAvailableDisplays() {
try {
sources = await desktopCapturer.getSources({ types: ['screen'] });
const source = sources[0];
displayId = source.id
const displays = sources.map(source => ({
id: source.id,
name: source.name,
width: source.thumbnail.getSize().width,
height: source.thumbnail.getSize().height
}));
socket.emit('availableDisplays', { displays });
} catch (err) {
console.error('디스플레이 목록 가져오기 실패:', err);
}
}
const keyMap = {
Enter: Key.Enter,
Backspace: Key.Backspace,
Tab: Key.Tab,
Escape: Key.Escape,
ArrowDown: Key.Down,
ArrowUp: Key.Up,
ArrowLeft: Key.Left,
ArrowRight: Key.Right,
}
// 🖱️ 입력 이벤트 처리 (robotjs 필요)
async function setupInputHandler(data) {
// robotjs는 별도 설치 및 권한 필요
if (data.type === 'mouse') {
const { x, y, action } = data;
if(x === null || y === null) return
const targetPoint = new Point(x, y)
await mouse.move(straightTo(targetPoint));
console.log(x, y , action)
if (action === 'click') {
await mouse.click(Button.LEFT)
}
} else if (data.type === 'keyboard') {
const { key } = data;
// 간단한 키 매핑 (보안상 복잡한 키는 제한 권장)
console.log(key)
if(['Enter', 'Backspace', 'Tab', 'Escape'
, 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(key)) {
await keyboard.pressKey(keyMap[key]);
await keyboard.releaseKey(keyMap[key]);
} else if(key.length === 1) {
await keyboard.type(key.toLowerCase());
}
// if (key.length === 1 || ['Enter', 'Backspace', 'Tab', 'Escape'].includes(key)) {
// await keyboard.type(key.toLowerCase());
// }
}
}
// 시그널링 연결
function connectToSignaling() {
socket = io(SIGNALING_SERVER, {
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 2000
});
socket.on('connect', (data) => {
console.log('시그널링 서버 연결됨. 직원 등록 중...');
socket.emit('register', EMPLOYEE_ID);
sendAvailableDisplays(); // 연결 시 디스플레이 목록 전송
// rendererWindow.webContents.send('connection', data);
});
// WebRTC 신호 수신 (ICE candidate 등)
socket.on('icecandidate', async (data) => {
rendererWindow.webContents.send('icecandidate', data);
});
socket.on('disconnect', () => {
console.log('시그널링 서버와 연결 끊김');
rendererWindow.webContents.send('disconnect');
});
socket.on('responseOffer', async (data) => {
console.log('responseOffer', data);
const result = await dialog.showMessageBox({
type: 'question',
title: '원격 연결 요청',
message: `관리자로부터 원격 제어 요청이 왔습니다.\n허용하시겠습니까? ${data.displayId}`,
buttons: ['허용', '거부'],
defaultId: 1,
cancelId: 1
});
if (result.response === 0) {
// ✅ 렌더러 프로세스에 WebRTC 시작 명령 전달
rendererWindow.webContents.send('start-webrtc', {
displayId: data.displayId,
targetId: data.targetId,
offer: data.offer,
sources
});
} else {
socket.emit('webrtcSignal', { targetId: data.targetId, data: { type: 'reject' } });
}
})
socket.on('error', (err) => {
console.error('소켓 오류:', err);
});
// 입력 이벤트 리스너 설정
setupInputHandler();
}
// 앱 준비 시
let rendererWindow = null;
// 앱 준비 완료
app.whenReady().then(() => {
rendererWindow = createHiddenWindow();
createTray();
connectToSignaling();
app.on('activate', () => {
// macOS 대응
});
});
app.on('window-all-closed', () => {
// Windows/Linux에서 종료하지 않음
});
ipcMain.on('requestAnswer', (event, data) => {
socket.emit('requestAnswer', data);
})
ipcMain.on('icecandidate', (event, data) => {
socket.emit('icecandidate', data);
})
ipcMain.on('start', (event, data) => {
console.log('start', data);
socket.emit('start', data);
})
ipcMain.on('inputEvent', (event, data) => {
setupInputHandler(data)
})