feat(bridge): connect Gravity Web server to Extension Bridge — proxy routes + WS forwarding

This commit is contained in:
2026-03-07 23:56:41 +09:00
parent 68298eba74
commit bee5e512a9
3 changed files with 257 additions and 1 deletions

Binary file not shown.

182
server/bridge-client.js Normal file
View File

@@ -0,0 +1,182 @@
/**
* Bridge Client — Antigravity Extension Bridge (localhost:9850)와 통신
*
* Extension이 설치된 Antigravity에서 제공하는 REST API + WebSocket을 통해
* 대화 목록, 메시지 전송, 승인/거절 등을 수행합니다.
*/
const http = require('http');
const WebSocket = require('ws');
const BRIDGE_HOST = 'localhost';
const BRIDGE_PORT = 9850;
const BRIDGE_BASE = `http://${BRIDGE_HOST}:${BRIDGE_PORT}`;
class BridgeClient {
constructor() {
this.ws = null;
this.connected = false;
this.listeners = new Map();
this.reconnectTimer = null;
}
/**
* Bridge 서버 상태 확인
*/
async checkHealth() {
try {
const data = await this._get('/api/health');
return { available: true, ...data };
} catch {
return { available: false };
}
}
/**
* 대화 목록 (제목, stepCount, 수정일시)
*/
async getSessions() {
return this._get('/api/sessions');
}
/**
* 전체 Cascade 데이터 (대화 내용, 스텝, notify_user 등)
*/
async getCascades() {
return this._get('/api/cascades');
}
/**
* 메시지 전송
*/
async sendMessage(message, sessionId) {
return this._post('/api/send', { message, sessionId });
}
/**
* 코드 편집 승인
*/
async acceptStep() {
return this._post('/api/accept', {});
}
/**
* 코드 편집 거절
*/
async rejectStep() {
return this._post('/api/reject', {});
}
/**
* 터미널 명령 승인
*/
async acceptTerminal() {
return this._post('/api/accept-terminal', {});
}
/**
* 에이전트 설정 조회
*/
async getPreferences() {
return this._get('/api/preferences');
}
/**
* WebSocket 실시간 이벤트 연결
*/
connectWs(onMessage) {
if (this.ws) {
this.ws.close();
}
try {
this.ws = new WebSocket(`ws://${BRIDGE_HOST}:${BRIDGE_PORT}/ws`);
} catch (err) {
console.log(`[Bridge WS] 연결 실패: ${err.message}`);
this._scheduleReconnect(onMessage);
return;
}
this.ws.on('open', () => {
this.connected = true;
console.log('[Bridge WS] 연결됨');
});
this.ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString());
onMessage(msg);
} catch { }
});
this.ws.on('close', () => {
this.connected = false;
console.log('[Bridge WS] 연결 해제');
this._scheduleReconnect(onMessage);
});
this.ws.on('error', () => {
this.connected = false;
});
}
_scheduleReconnect(onMessage) {
if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
console.log('[Bridge WS] 재연결 시도...');
this.connectWs(onMessage);
}, 5000);
}
disconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// --- HTTP 헬퍼 ---
_get(path) {
return new Promise((resolve, reject) => {
http.get(`${BRIDGE_BASE}${path}`, (res) => {
let body = '';
res.on('data', (c) => body += c);
res.on('end', () => {
try { resolve(JSON.parse(body)); }
catch { reject(new Error('Invalid JSON')); }
});
}).on('error', reject);
});
}
_post(path, data) {
return new Promise((resolve, reject) => {
const body = JSON.stringify(data);
const req = http.request(`${BRIDGE_BASE}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
},
}, (res) => {
let resBody = '';
res.on('data', (c) => resBody += c);
res.on('end', () => {
try { resolve(JSON.parse(resBody)); }
catch { reject(new Error('Invalid JSON')); }
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}
}
module.exports = BridgeClient;

View File

@@ -11,6 +11,7 @@ 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 { AutoDiscovery } = require('./auto-discover');
const BridgeClient = require('./bridge-client');
const PORT = process.env.PORT || 3300; const PORT = process.env.PORT || 3300;
const app = express(); const app = express();
@@ -252,7 +253,80 @@ function broadcastSessions() {
} }
} }
// ─── REST API ─────────────────────────────────────────── // ─── Bridge API 프록시 ──────────────────────────────────
const bridge = new BridgeClient();
app.get('/api/bridge/health', async (req, res) => {
try {
const health = await bridge.checkHealth();
res.json(health);
} catch (e) {
res.json({ available: false, error: e.message });
}
});
app.get('/api/bridge/sessions', async (req, res) => {
try {
const data = await bridge.getSessions();
res.json(data);
} catch (e) {
res.status(502).json({ error: 'Bridge 연결 실패: ' + e.message });
}
});
app.get('/api/bridge/cascades', async (req, res) => {
try {
const data = await bridge.getCascades();
res.json(data);
} catch (e) {
res.status(502).json({ error: 'Bridge 연결 실패: ' + e.message });
}
});
app.post('/api/bridge/send', async (req, res) => {
try {
const { message, sessionId } = req.body;
const result = await bridge.sendMessage(message, sessionId);
res.json(result);
} catch (e) {
res.status(502).json({ error: e.message });
}
});
app.post('/api/bridge/accept', async (req, res) => {
try {
const result = await bridge.acceptStep();
res.json(result);
} catch (e) {
res.status(502).json({ error: e.message });
}
});
app.post('/api/bridge/reject', async (req, res) => {
try {
const result = await bridge.rejectStep();
res.json(result);
} catch (e) {
res.status(502).json({ error: e.message });
}
});
app.post('/api/bridge/accept-terminal', async (req, res) => {
try {
const result = await bridge.acceptTerminal();
res.json(result);
} catch (e) {
res.status(502).json({ error: e.message });
}
});
// Bridge WS 이벤트 → 프론트엔드 포워딩
bridge.connectWs((msg) => {
broadcastToAll({ type: 'bridge_event', ...msg });
});
// ─── CDP REST API (레거시) ──────────────────────────────
app.get('/api/sessions', (req, res) => { app.get('/api/sessions', (req, res) => {
res.json(sessionManager.listSessions()); res.json(sessionManager.listSessions());