Files
connectai/src/bridge.ts
T
2026-05-03 20:40:40 +09:00

152 lines
6.2 KiB
TypeScript

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<void>) {
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 }));
}
}