import * as http from 'http'; import * as fs from 'fs'; import { _getBrainDir, findBrainFiles, logError, logInfo, summarizeText } from './utils'; import { getConfig } from './config'; import { IAIService, IBrainService, AIService, BrainService } from './core/services'; export interface BridgeInterface { injectSystemMessage(msg: string): void; getHistoryText(): string; sendPromptFromExtension(prompt: string): void; brainEnabled: boolean; findBrainFiles(dir: string): string[]; } /** * BridgeServer: * 외부 툴(EZER, A.U 등)과 Astra 확장을 연결하는 통신 브릿지. * 서비스 레이어(AIService, BrainService)를 통해 비즈니스 로직을 분리하여 유지보수성을 극대화했습니다. */ export class BridgeServer { private server: http.Server | null = null; private aiService: IAIService; private brainService: IBrainService; constructor( private provider: BridgeInterface, aiService?: IAIService, brainService?: IBrainService ) { // 의존성 주입 (DIP): 기본값 제공 및 외부 주입 허용 this.aiService = aiService || new AIService(); this.brainService = brainService || new BrainService(); } public start(port: number = 4825) { this.server = http.createServer((req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } const url = req.url || ''; const method = req.method; // 라우팅 로직 (SRP에 따라 비즈니스 로직은 서비스로 위임) if (method === 'GET' && url === '/ping') { this.handlePing(res); } else if (method === 'POST' && url === '/api/exam') { this.handlePost(req, res, this.processExam.bind(this)); } else if (method === 'POST' && url === '/api/evaluate') { this.handlePost(req, res, this.processEvaluate.bind(this)); } else if (method === 'GET' && url === '/api/evaluate-history') { this.processEvaluateHistory(res); } else if (method === 'POST' && url === '/api/brain-inject') { this.handlePost(req, res, this.processBrainInject.bind(this)); } else { res.writeHead(404); res.end(); } }); this.server.on('error', (err: any) => { if (err.code === 'EADDRINUSE') { logError(`🚫 Bridge Port ${port} in use. Connection with EZER/A.U might fail.`); } else { logError(`Bridge server error:`, err); } }); this.server.listen(port, '127.0.0.1', () => { logInfo(`Bridge server active on 127.0.0.1:${port}.`); }); } private handlePing(res: http.ServerResponse) { const brainDir = _getBrainDir(); const brainCount = fs.existsSync(brainDir) ? findBrainFiles(brainDir).length : 0; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', config: getConfig(), brain: { fileCount: brainCount, enabled: this.provider.brainEnabled } })); } private handlePost(req: http.IncomingMessage, res: http.ServerResponse, processor: (data: any, res: http.ServerResponse) => Promise) { let body = ''; req.on('data', chunk => body += chunk.toString()); req.on('end', async () => { try { const parsed = JSON.parse(body); await processor(parsed, res); } catch (e: any) { logError('Bridge request processor failed.', { url: req.url, error: e.message }); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message })); } }); } private async processExam(data: any, res: http.ServerResponse) { const prompt = data.prompt || 'Automatic Prompt'; this.provider.sendPromptFromExtension(`[Bridge] ${prompt}`); const result = await this.aiService.call(prompt); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, rawOutput: result })); } private async processEvaluate(data: any, res: http.ServerResponse) { const prompt = data.prompt || ''; this.provider.injectSystemMessage(`**[A.U Evaluation]** Analyzing input...`); const result = await this.aiService.call(`[EVALUATE] ${prompt}`); this.provider.injectSystemMessage(`**[Result]** ${summarizeText(result, 200)}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ rawOutput: result })); } private async processEvaluateHistory(res: http.ServerResponse) { const historyText = this.provider.getHistoryText(); if (!historyText || historyText.length < 50) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: "Insufficient history" })); return; } const result = await this.aiService.call(`Analyze chat history for metrics (JSON):\n${historyText.slice(-6000)}`); const jsonMatch = result.match(/\{[\s\S]*?\}/); res.writeHead(jsonMatch ? 200 : 500, { 'Content-Type': 'application/json' }); res.end(jsonMatch ? jsonMatch[0] : JSON.stringify({ error: "Parse failed", raw: result })); } private async processBrainInject(data: any, res: http.ServerResponse) { const { title, markdown, prompt } = data; await this.brainService.inject(title, markdown); this.provider.injectSystemMessage(`**[Brain]** Knowledge captured: ${title}`); const result = await this.aiService.call(prompt || `Analyze: ${title}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, rawOutput: result })); } }