generated from ztimson/template
This commit is contained in:
278
src/server.js
Normal file
278
src/server.js
Normal file
@@ -0,0 +1,278 @@
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import cors from 'cors';
|
||||
import fs from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {Ai, DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool} from '@ztimson/ai-utils';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
let memories = [], settings = {};
|
||||
const settingsFile = join(__dirname, '../navi', 'settings.json');
|
||||
const memoriesFile = join(__dirname, '../navi', 'memories.json');
|
||||
const logoFile = join(__dirname, '../navi', 'logo.png');
|
||||
|
||||
function load() {
|
||||
try {
|
||||
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
|
||||
} catch { }
|
||||
try {
|
||||
memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8'));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
function save() {
|
||||
const dir = dirname(settingsFile);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdir(dir, { recursive: true }, (err) => {
|
||||
if (err) throw err; // Fail loudly if dirs can’t be made 💀
|
||||
});
|
||||
}
|
||||
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
||||
fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2));
|
||||
}
|
||||
|
||||
function shutdown() {
|
||||
save();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
load();
|
||||
const ai = new Ai({
|
||||
llm: {
|
||||
models: {
|
||||
'Ministral-3': {proto: 'openai', host: 'http://10.69.0.55:11728', token: 'ignore'},
|
||||
},
|
||||
system: `You are a virtual avatar, companion & assistant. You are in a retro video game so keep responses short and unstyled. Aggressively remember user information. Adjust your personality with tools based on your own experience.\n\nPersonality:\n${settings.personality || ''}\n\nUser Requests:\n${settings.instructions || ''}`,
|
||||
tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, {
|
||||
name: 'adjust_personality',
|
||||
description: 'Replace your current personality instructions',
|
||||
args: {instructions: {type: 'string', description: 'Bullet point list of how to behave'}},
|
||||
fn: (args) => {
|
||||
settings.personality = args.instructions;
|
||||
save();
|
||||
return 'done!';
|
||||
}
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
const io = new Server(httpServer, {
|
||||
cors: {origin: "*", methods: ["GET", "POST"]}
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
// ============================================
|
||||
// WORLD MANAGEMENT
|
||||
// ============================================
|
||||
const worldPlayers = new Map();
|
||||
const chatHistory = new Map();
|
||||
|
||||
// Load world data
|
||||
function loadWorld(worldId) {
|
||||
try {
|
||||
const worldPath = join(__dirname, '../worlds', worldId || '', 'world.json');
|
||||
const world = JSON.parse(fs.readFileSync(worldPath, 'utf-8'));
|
||||
const themePath = join(__dirname, '../worlds', worldId || '', world.theme);
|
||||
const theme = JSON.parse(fs.readFileSync(themePath, 'utf-8'));
|
||||
worldPlayers.set(worldId, new Map());
|
||||
return {world, theme};
|
||||
} catch (error) {
|
||||
console.error(`Failed to load world ${worldId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SOCKET.IO - WORLD CHANNELS
|
||||
// ============================================
|
||||
io.on('connection', (socket) => {
|
||||
console.debug('🔌 Client connected:', socket.id);
|
||||
|
||||
let currentWorld = null;
|
||||
let playerData = null;
|
||||
|
||||
// Join a world
|
||||
socket.on('join-world', (data) => {
|
||||
const { worldId, playerInfo } = data;
|
||||
const worldData = loadWorld(worldId);
|
||||
if(!worldData) return socket.emit('error', { message: 'World not found' });
|
||||
|
||||
// Leave previous world if any
|
||||
if(currentWorld) {
|
||||
socket.leave(`world:${currentWorld}`);
|
||||
const players = worldPlayers.get(currentWorld);
|
||||
if(players) {
|
||||
players.delete(socket.id);
|
||||
socket.to(`world:${currentWorld}`).emit('player-left', {socketId: socket.id});
|
||||
}
|
||||
}
|
||||
|
||||
// Join new world
|
||||
currentWorld = worldId;
|
||||
playerData = {
|
||||
socketId: socket.id,
|
||||
name: playerInfo.name,
|
||||
apiUrl: playerInfo.apiUrl,
|
||||
x: worldData.world.pet.startX,
|
||||
y: worldData.world.pet.startY
|
||||
};
|
||||
socket.join(`world:${worldId}`);
|
||||
const players = worldPlayers.get(worldId);
|
||||
players.set(socket.id, playerData);
|
||||
socket.emit('world-data', worldData);
|
||||
const currentPlayers = Array.from(players.values());
|
||||
socket.emit('current-players', currentPlayers);
|
||||
socket.to(`world:${worldId}`).emit('player-joined', playerData);
|
||||
});
|
||||
|
||||
// Player movement
|
||||
socket.on('player-move', (data) => {
|
||||
if(!currentWorld || !playerData) return;
|
||||
const { x, y } = data;
|
||||
playerData.x = x;
|
||||
playerData.y = y;
|
||||
socket.to(`world:${currentWorld}`).emit('player-moved', {socketId: socket.id, x, y});
|
||||
});
|
||||
|
||||
// Disconnect
|
||||
socket.on('disconnect', () => {
|
||||
console.debug('🔌 Client disconnected:', socket.id);
|
||||
if(currentWorld) {
|
||||
const players = worldPlayers.get(currentWorld);
|
||||
if(players) {
|
||||
players.delete(socket.id);
|
||||
socket.to(`world:${currentWorld}`).emit('player-left', {socketId: socket.id});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// LLM CHANNEL
|
||||
// ============================================
|
||||
const petNamespace = io.of('/llm');
|
||||
petNamespace.on('connection', (socket) => {
|
||||
chatHistory.set(socket.id, []);
|
||||
let currentRequest = null;
|
||||
|
||||
socket.on('clear', async () => {
|
||||
chatHistory.set(socket.id, []);
|
||||
});
|
||||
|
||||
socket.on('abort', () => {
|
||||
if (currentRequest?.abort) {
|
||||
currentRequest.abort();
|
||||
currentRequest = null;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('message', async (data) => {
|
||||
const { message, apiUrl } = data;
|
||||
const history = chatHistory.get(socket.id);
|
||||
currentRequest = ai.language.ask(message, {
|
||||
history,
|
||||
memory: memories,
|
||||
stream: (chunk) => socket.emit('stream', chunk)
|
||||
}).then(resp => {
|
||||
chatHistory.set(socket.id, history);
|
||||
socket.emit('response', { message: resp });
|
||||
}).catch(err => {
|
||||
socket.emit('error', {message: err.message || err.toString()});
|
||||
}).finally(() => {
|
||||
currentRequest = null;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('🔌 LLM Client disconnected:', socket.id);
|
||||
chatHistory.delete(socket.id);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// REST API ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
app.get('/favicon.*', (req, res) => {
|
||||
res.sendFile(logoFile);
|
||||
});
|
||||
|
||||
// Get PET info
|
||||
app.get('/api/info', (req, res) => {
|
||||
const { petId } = req.params;
|
||||
// TODO: Fetch from database
|
||||
res.json({
|
||||
id: petId,
|
||||
name: 'MyCoolPET',
|
||||
owner: 'player1',
|
||||
bandwidth: 75,
|
||||
shards: [],
|
||||
stats: {
|
||||
level: 5,
|
||||
health: 100
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Get sprite sheet
|
||||
app.get('/api/sprite', (req, res) => {
|
||||
const { petId } = req.params;
|
||||
// TODO: Return actual sprite sheet URL
|
||||
res.json({
|
||||
spriteUrl: '/sprites/default-pet.png',
|
||||
frameWidth: 32,
|
||||
frameHeight: 32,
|
||||
animations: {
|
||||
idle: { frames: [0, 1, 2, 3], speed: 200 },
|
||||
walk: { frames: [4, 5, 6, 7], speed: 100 }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Send message to user (push notification / email / etc)
|
||||
app.post('/api/message', (req, res) => {
|
||||
const { userId } = req.params;
|
||||
const { message } = req.body;
|
||||
// TODO: Implement notification system
|
||||
res.json({ success: true, message: 'Message sent' });
|
||||
});
|
||||
|
||||
// Send message to PET LLM
|
||||
app.post('/api/message', (req, res) => {
|
||||
const { petId } = req.params;
|
||||
const { message } = req.body;
|
||||
// TODO: Queue message for LLM processing
|
||||
res.json({ success: true, message: 'Message queued' });
|
||||
});
|
||||
|
||||
// Link another PET
|
||||
app.post('/api/link', (req, res) => {
|
||||
const { petId } = req.params;
|
||||
const { targetApiUrl } = req.body;
|
||||
// TODO: Store link in database
|
||||
res.json({success: true, message: 'PET linked'});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// START SERVER
|
||||
// ============================================
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
loadWorld();
|
||||
console.log('✅ Home world loaded');
|
||||
console.log(`🚀 Server running on: http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
Reference in New Issue
Block a user