feat(discovery): auto-detect Antigravity via CDP port scan + auto-create sessions
This commit is contained in:
@@ -130,7 +130,23 @@
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'action_clicked':
|
case 'action_clicked':
|
||||||
// 서버에서의 확인 응답 (토스트는 이미 프론트엔드에서 표시)
|
// 서버에서의 확인 응답
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'auto_discovered':
|
||||||
|
showToast(`Antigravity 자동 감지: ${msg.session.name}`, 'success');
|
||||||
|
// 활성 세션이 없으면 자동 전환
|
||||||
|
if (!sessionPanel.activeSessionId) {
|
||||||
|
sendWs({ type: 'switch_session', sessionId: msg.session.id });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'session_lost':
|
||||||
|
showToast('Antigravity 연결 소실', 'warning');
|
||||||
|
if (sessionPanel.activeSessionId === msg.sessionId) {
|
||||||
|
chatPanel.showEmpty();
|
||||||
|
sessionPanel.setActive(null);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
|
|||||||
129
server/auto-discover.js
Normal file
129
server/auto-discover.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Antigravity 자동 발견 모듈
|
||||||
|
*
|
||||||
|
* localhost의 CDP 포트 범위를 스캔하여 실행 중인 Antigravity 인스턴스를 감지.
|
||||||
|
* 발견된 인스턴스는 자동으로 세션 생성하여 웹 UI에 표시.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
// 스캔할 포트 범위
|
||||||
|
const SCAN_PORTS = [9000, 9222, 9229, 9333, 9001, 9002, 9003, 9004, 9005];
|
||||||
|
const SCAN_INTERVAL_MS = 15000; // 15초마다 스캔
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 포트의 CDP 엔드포인트에 요청하여 Antigravity인지 확인
|
||||||
|
* @returns {Promise<{port, title, url}|null>}
|
||||||
|
*/
|
||||||
|
function probeCDPPort(host, port, timeoutMs = 2000) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = http.get(`http://${host}:${port}/json`, { timeout: timeoutMs }, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', chunk => data += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const targets = JSON.parse(data);
|
||||||
|
// Antigravity 워크벤치 타겟 찾기
|
||||||
|
const mainTarget = targets.find(t =>
|
||||||
|
t.type === 'page' &&
|
||||||
|
t.url.includes('workbench.html') &&
|
||||||
|
!t.url.includes('jetski')
|
||||||
|
);
|
||||||
|
if (mainTarget) {
|
||||||
|
resolve({
|
||||||
|
port,
|
||||||
|
title: mainTarget.title || 'Antigravity',
|
||||||
|
url: mainTarget.url,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', () => resolve(null));
|
||||||
|
req.on('timeout', () => { req.destroy(); resolve(null); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모든 포트를 병렬 스캔하여 Antigravity 인스턴스 목록 반환
|
||||||
|
* @returns {Promise<Array<{port, title, url}>>}
|
||||||
|
*/
|
||||||
|
async function scanForAntigravity(host = 'localhost') {
|
||||||
|
const probes = SCAN_PORTS.map(port => probeCDPPort(host, port));
|
||||||
|
const results = await Promise.all(probes);
|
||||||
|
return results.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutoDiscovery {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.host = options.host || 'localhost';
|
||||||
|
this.intervalMs = options.intervalMs || SCAN_INTERVAL_MS;
|
||||||
|
this.onDiscovered = options.onDiscovered || null; // callback({port, title, url})
|
||||||
|
this.onLost = options.onLost || null; // callback(port)
|
||||||
|
this.knownPorts = new Set();
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 자동 발견 시작
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
|
console.log(`[AutoDiscovery] 시작 — ${this.host}에서 Antigravity 포트 스캔`);
|
||||||
|
|
||||||
|
// 즉시 한 번 스캔
|
||||||
|
await this._scan();
|
||||||
|
|
||||||
|
// 주기적 스캔
|
||||||
|
this.timer = setInterval(() => this._scan(), this.intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 자동 발견 중지
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 내부 스캔 로직
|
||||||
|
*/
|
||||||
|
async _scan() {
|
||||||
|
try {
|
||||||
|
const found = await scanForAntigravity(this.host);
|
||||||
|
const foundPorts = new Set(found.map(f => f.port));
|
||||||
|
|
||||||
|
// 새로 발견된 인스턴스
|
||||||
|
for (const instance of found) {
|
||||||
|
if (!this.knownPorts.has(instance.port)) {
|
||||||
|
this.knownPorts.add(instance.port);
|
||||||
|
console.log(`[AutoDiscovery] 발견: ${this.host}:${instance.port} — ${instance.title}`);
|
||||||
|
if (this.onDiscovered) {
|
||||||
|
this.onDiscovered(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사라진 인스턴스
|
||||||
|
for (const port of this.knownPorts) {
|
||||||
|
if (!foundPorts.has(port)) {
|
||||||
|
this.knownPorts.delete(port);
|
||||||
|
console.log(`[AutoDiscovery] 소실: ${this.host}:${port}`);
|
||||||
|
if (this.onLost) {
|
||||||
|
this.onLost(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[AutoDiscovery] 스캔 오류:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { AutoDiscovery, scanForAntigravity, probeCDPPort };
|
||||||
@@ -10,6 +10,7 @@ const http = require('http');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { WebSocketServer, WebSocket } = require('ws');
|
const { WebSocketServer, WebSocket } = require('ws');
|
||||||
const SessionManager = require('./session-manager');
|
const SessionManager = require('./session-manager');
|
||||||
|
const { AutoDiscovery } = require('./auto-discover');
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3300;
|
const PORT = process.env.PORT || 3300;
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -326,8 +327,78 @@ server.listen(PORT, () => {
|
|||||||
console.log(` Gravity Web 서버 시작`);
|
console.log(` Gravity Web 서버 시작`);
|
||||||
console.log(` http://localhost:${PORT}`);
|
console.log(` http://localhost:${PORT}`);
|
||||||
console.log('═'.repeat(50));
|
console.log('═'.repeat(50));
|
||||||
|
|
||||||
|
// Auto Discovery 시작
|
||||||
|
const discovery = new AutoDiscovery({
|
||||||
|
host: 'localhost',
|
||||||
|
intervalMs: 15000,
|
||||||
|
onDiscovered: async (instance) => {
|
||||||
|
// 이미 같은 포트의 세션이 있는지 확인
|
||||||
|
const existing = sessionManager.listSessions().find(
|
||||||
|
s => s.host === 'localhost' && s.cdpPort === instance.port
|
||||||
|
);
|
||||||
|
if (existing) return;
|
||||||
|
|
||||||
|
// 자동 세션 생성
|
||||||
|
try {
|
||||||
|
const session = await sessionManager.addSession(
|
||||||
|
`Antigravity :${instance.port}`,
|
||||||
|
'localhost',
|
||||||
|
instance.port
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[AutoDiscovery] 자동 세션 생성: ${session.name} (${session.id})`);
|
||||||
|
|
||||||
|
// 모든 WS 클라이언트에 세션 목록 업데이트
|
||||||
|
broadcastToAll({
|
||||||
|
type: 'sessions_list',
|
||||||
|
sessions: sessionManager.listSessions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 자동 발견 알림
|
||||||
|
broadcastToAll({
|
||||||
|
type: 'auto_discovered',
|
||||||
|
session: {
|
||||||
|
id: session.id,
|
||||||
|
name: session.name,
|
||||||
|
host: 'localhost',
|
||||||
|
port: instance.port,
|
||||||
|
status: session.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[AutoDiscovery] 세션 생성 실패:`, err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLost: (port) => {
|
||||||
|
// 해당 포트의 세션 상태 업데이트
|
||||||
|
const sessions = sessionManager.listSessions();
|
||||||
|
const lost = sessions.find(s => s.cdpPort === port && s.host === 'localhost');
|
||||||
|
if (lost) {
|
||||||
|
console.log(`[AutoDiscovery] 세션 소실: ${lost.name}`);
|
||||||
|
broadcastToAll({
|
||||||
|
type: 'session_lost',
|
||||||
|
sessionId: lost.id,
|
||||||
|
port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
discovery.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모든 WS 클라이언트에 메시지 브로드캐스트
|
||||||
|
*/
|
||||||
|
function broadcastToAll(data) {
|
||||||
|
const json = JSON.stringify(data);
|
||||||
|
for (const [ws] of wsClients) {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
console.log('\n[Server] 종료 중...');
|
console.log('\n[Server] 종료 중...');
|
||||||
|
|||||||
Reference in New Issue
Block a user