204 lines
6.4 KiB
TypeScript
204 lines
6.4 KiB
TypeScript
import express from 'express';
|
|
import { createServer as createViteServer } from 'vite';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import 'dotenv/config';
|
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
import { createServer } from 'http';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// Primary (Local) Gitea
|
|
const GITEA_URL = process.env.GITEA_URL || 'http://localhost:3000/api/v1';
|
|
const GITEA_TOKEN = process.env.GITEA_TOKEN || '';
|
|
|
|
// Backup (Remote) Gitea
|
|
const REMOTE_GITEA_URL = process.env.REMOTE_GITEA_URL || 'http://143.198.27.163:3000/api/v1';
|
|
const REMOTE_GITEA_TOKEN = process.env.REMOTE_GITEA_TOKEN || '';
|
|
|
|
async function startServer() {
|
|
const app = express();
|
|
const httpServer = createServer(app);
|
|
const PORT = 3000;
|
|
|
|
// WebSocket Server for Hermes/Evennia Bridge
|
|
const wss = new WebSocketServer({ noServer: true });
|
|
const clients = new Set<WebSocket>();
|
|
|
|
wss.on('connection', (ws) => {
|
|
clients.add(ws);
|
|
console.log(`Client connected to Nexus Bridge. Total: ${clients.size}`);
|
|
|
|
ws.on('close', () => {
|
|
clients.remove(ws);
|
|
console.log(`Client disconnected. Total: ${clients.size}`);
|
|
});
|
|
});
|
|
|
|
// Simulate Evennia Heartbeat (Source of Truth)
|
|
setInterval(() => {
|
|
const heartbeat = {
|
|
type: 'heartbeat',
|
|
frequency: 0.5 + Math.random() * 0.2, // 0.5Hz to 0.7Hz
|
|
intensity: 0.8 + Math.random() * 0.4,
|
|
timestamp: Date.now(),
|
|
source: 'evonia-layer'
|
|
};
|
|
const message = JSON.stringify(heartbeat);
|
|
clients.forEach(client => {
|
|
if (client.readyState === WebSocket.OPEN) {
|
|
client.send(message);
|
|
}
|
|
});
|
|
}, 2000);
|
|
|
|
app.use(express.json({ limit: '50mb' }));
|
|
|
|
// Diagnostic Endpoint for Agent Inspection
|
|
app.get('/api/diagnostic/inspect', async (req, res) => {
|
|
console.log('Diagnostic request received');
|
|
try {
|
|
const REPO_OWNER = 'google';
|
|
const REPO_NAME = 'timmy-tower';
|
|
|
|
const [stateRes, issuesRes] = await Promise.all([
|
|
fetch(`${GITEA_URL}/repos/${REPO_OWNER}/${REPO_NAME}/contents/world_state.json`, {
|
|
headers: { 'Authorization': `token ${GITEA_TOKEN}` }
|
|
}),
|
|
fetch(`${GITEA_URL}/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=all`, {
|
|
headers: { 'Authorization': `token ${GITEA_TOKEN}` }
|
|
})
|
|
]);
|
|
|
|
let worldState = null;
|
|
if (stateRes.ok) {
|
|
const content = await stateRes.json();
|
|
worldState = JSON.parse(Buffer.from(content.content, 'base64').toString());
|
|
} else if (stateRes.status !== 404) {
|
|
console.error(`Failed to fetch world state: ${stateRes.status} ${stateRes.statusText}`);
|
|
}
|
|
|
|
let issues = [];
|
|
if (issuesRes.ok) {
|
|
issues = await issuesRes.json();
|
|
} else {
|
|
console.error(`Failed to fetch issues: ${issuesRes.status} ${issuesRes.statusText}`);
|
|
}
|
|
|
|
res.json({
|
|
worldState,
|
|
issues,
|
|
repoExists: stateRes.status !== 404,
|
|
connected: GITEA_TOKEN !== ''
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Diagnostic error:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Helper for Gitea Proxy
|
|
const createGiteaProxy = (baseUrl: string, token: string) => async (req: express.Request, res: express.Response) => {
|
|
const path = req.params[0] + (req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '');
|
|
const url = `${baseUrl}/${path}`;
|
|
|
|
if (!token) {
|
|
console.warn(`Gitea Proxy Warning: No token provided for ${baseUrl}`);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: req.method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `token ${token}`,
|
|
},
|
|
body: ['GET', 'HEAD'].includes(req.method) ? undefined : JSON.stringify(req.body),
|
|
});
|
|
|
|
const data = await response.text();
|
|
res.status(response.status).send(data);
|
|
} catch (error: any) {
|
|
console.error(`Gitea Proxy Error (${baseUrl}):`, error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
};
|
|
|
|
// Gitea Proxy - Primary (Local)
|
|
app.get('/api/gitea/check', async (req, res) => {
|
|
try {
|
|
const response = await fetch(`${GITEA_URL}/user`, {
|
|
headers: { 'Authorization': `token ${GITEA_TOKEN}` }
|
|
});
|
|
if (response.ok) {
|
|
const user = await response.json();
|
|
res.json({ status: 'connected', user: user.username });
|
|
} else {
|
|
res.status(response.status).json({ status: 'error', message: `Gitea returned ${response.status}` });
|
|
}
|
|
} catch (error: any) {
|
|
res.status(500).json({ status: 'error', message: error.message });
|
|
}
|
|
});
|
|
|
|
app.all('/api/gitea/*', createGiteaProxy(GITEA_URL, GITEA_TOKEN));
|
|
|
|
// Gitea Proxy - Backup (Remote)
|
|
app.get('/api/gitea-remote/check', async (req, res) => {
|
|
try {
|
|
const response = await fetch(`${REMOTE_GITEA_URL}/user`, {
|
|
headers: { 'Authorization': `token ${REMOTE_GITEA_TOKEN}` }
|
|
});
|
|
if (response.ok) {
|
|
const user = await response.json();
|
|
res.json({ status: 'connected', user: user.username });
|
|
} else {
|
|
res.status(response.status).json({ status: 'error', message: `Gitea returned ${response.status}` });
|
|
}
|
|
} catch (error: any) {
|
|
res.status(500).json({ status: 'error', message: error.message });
|
|
}
|
|
});
|
|
|
|
app.all('/api/gitea-remote/*', createGiteaProxy(REMOTE_GITEA_URL, REMOTE_GITEA_TOKEN));
|
|
|
|
// WebSocket Upgrade Handler
|
|
httpServer.on('upgrade', (request, socket, head) => {
|
|
const pathname = new URL(request.url!, `http://${request.headers.host}`).pathname;
|
|
if (pathname === '/api/world/ws') {
|
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
wss.emit('connection', ws, request);
|
|
});
|
|
} else {
|
|
socket.destroy();
|
|
}
|
|
});
|
|
|
|
// Health Check
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({ status: 'ok' });
|
|
});
|
|
|
|
// Vite middleware for development
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
const vite = await createViteServer({
|
|
server: { middlewareMode: true },
|
|
appType: 'spa',
|
|
});
|
|
app.use(vite.middlewares);
|
|
} else {
|
|
const distPath = path.join(process.cwd(), 'dist');
|
|
app.use(express.static(distPath));
|
|
app.get('*', (req, res) => {
|
|
res.sendFile(path.join(distPath, 'index.html'));
|
|
});
|
|
}
|
|
|
|
httpServer.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`Server running on http://localhost:${PORT}`);
|
|
});
|
|
}
|
|
|
|
startServer();
|