feat(bridge): connect Gravity Web server to Extension Bridge — proxy routes + WS forwarding
This commit is contained in:
182
server/bridge-client.js
Normal file
182
server/bridge-client.js
Normal 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;
|
||||
@@ -11,6 +11,7 @@ const path = require('path');
|
||||
const { WebSocketServer, WebSocket } = require('ws');
|
||||
const SessionManager = require('./session-manager');
|
||||
const { AutoDiscovery } = require('./auto-discover');
|
||||
const BridgeClient = require('./bridge-client');
|
||||
|
||||
const PORT = process.env.PORT || 3300;
|
||||
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) => {
|
||||
res.json(sessionManager.listSessions());
|
||||
|
||||
Reference in New Issue
Block a user