feat: AI Town MVP - 完整三层架构实现

- Python FastAPI 引擎:世界状态模拟、全局能量条、世界事件系统
- Node.js WebSocket 服务器:实时通信、事件队列批处理
- 前端仪表盘:世界状态可视化、行动日志、事件展示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2025-12-30 10:29:35 +08:00
commit 8b90d55f02
20 changed files with 2729 additions and 0 deletions

17
server-node/src/engine.ts Normal file
View File

@@ -0,0 +1,17 @@
import { StepRequest, StepResponse } from './types';
const ENGINE_URL = 'http://localhost:8000/step';
export async function callEngine(request: StepRequest): Promise<StepResponse> {
const response = await fetch(ENGINE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`Engine returned ${response.status}`);
}
return response.json() as Promise<StepResponse>;
}

82
server-node/src/index.ts Normal file
View File

@@ -0,0 +1,82 @@
import express from 'express';
import path from 'path';
import http from 'http';
import { WebSocketServer, WebSocket } from 'ws';
import { eventQueue } from './queue';
import { callEngine } from './engine';
import { ClientMessage, GameEvent } from './types';
const PORT = 3000;
const TICK_INTERVAL = 2000;
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
// 静态托管 ../web 目录
const webDir = path.join(__dirname, '../../web');
app.use(express.static(webDir));
// 健康检查
app.get('/health', (_req, res) => {
res.send('ok');
});
// WebSocket 连接处理
wss.on('connection', (ws) => {
console.log('[WS] Client connected');
ws.on('message', (data) => {
try {
const msg: ClientMessage = JSON.parse(data.toString());
if (msg.type === 'comment' && msg.text && msg.user) {
const event: GameEvent = {
type: msg.type,
text: msg.text,
user: msg.user,
ts: Date.now(),
};
eventQueue.push(event);
console.log(`[Queue] Added event, size: ${eventQueue.size()}`);
}
} catch (err) {
console.error('[WS] Invalid message:', err);
}
});
ws.on('close', () => {
console.log('[WS] Client disconnected');
});
});
// 广播消息给所有客户端
function broadcast(data: object): void {
const message = JSON.stringify(data);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// 定时调用 Python 引擎
async function tick(): Promise<void> {
const events = eventQueue.drain();
console.log(`[Tick] Processing ${events.length} events`);
try {
const response = await callEngine({ events });
console.log(`[Tick] Step success, tick=${response.world_state.tick}`);
broadcast(response);
} catch (err) {
console.error('[Tick] Engine call failed:', err);
}
}
setInterval(tick, TICK_INTERVAL);
// 启动服务器
server.listen(PORT, () => {
console.log(`[Server] HTTP + WebSocket running on http://localhost:${PORT}`);
console.log(`[Server] Serving static files from ${webDir}`);
});

28
server-node/src/queue.ts Normal file
View File

@@ -0,0 +1,28 @@
import { GameEvent } from './types';
const MAX_QUEUE_SIZE = 200;
class EventQueue {
private queue: GameEvent[] = [];
push(event: GameEvent): void {
this.queue.push(event);
if (this.queue.length > MAX_QUEUE_SIZE) {
const dropped = this.queue.length - MAX_QUEUE_SIZE;
this.queue = this.queue.slice(dropped);
console.log(`[Queue] Dropped ${dropped} oldest events`);
}
}
drain(): GameEvent[] {
const events = this.queue;
this.queue = [];
return events;
}
size(): number {
return this.queue.length;
}
}
export const eventQueue = new EventQueue();

41
server-node/src/types.ts Normal file
View File

@@ -0,0 +1,41 @@
export interface GameEvent {
type: string;
text: string;
user: string;
ts: number;
}
export interface StepRequest {
events: GameEvent[];
}
export interface AgentState {
emotion: string;
goal: string;
memory: string[];
}
export interface WorldState {
tick: number;
weather: string;
town_mood: number;
agents: Record<string, AgentState>;
events: string[];
}
export interface Action {
agent_id: string;
say: string;
do: string;
}
export interface StepResponse {
world_state: WorldState;
actions: Action[];
}
export interface ClientMessage {
type: string;
text: string;
user: string;
}