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:
17
server-node/src/engine.ts
Normal file
17
server-node/src/engine.ts
Normal 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
82
server-node/src/index.ts
Normal 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
28
server-node/src/queue.ts
Normal 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
41
server-node/src/types.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user