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

52
server-node/README.md Normal file
View File

@@ -0,0 +1,52 @@
# AI Town Server (Node.js)
WebSocket 服务器,连接前端和 Python 引擎。
## 功能
- HTTP 静态托管 `../web` 目录
- WebSocket 接收客户端消息并入队
- 每 2 秒批量调用 Python 引擎并广播结果
## 启动
```bash
# 安装依赖
npm install
# 开发模式(热重载)
npm run dev
# 生产构建
npm run build
npm start
```
## 依赖服务
启动前需先运行 Python 引擎:
```bash
cd ../engine-python
uvicorn app.main:app --port 8000
```
## API
- `GET /health` - 健康检查,返回 `ok`
- `WebSocket /` - 游戏消息通道
## WebSocket 协议
**客户端发送:**
```json
{ "type": "comment", "text": "你好", "user": "玩家A" }
```
**服务端广播:**
```json
{
"world_state": { "tick": 1, "weather": "sunny", ... },
"actions": [{ "agent_id": "alice", "say": "...", "do": "..." }]
}
```

1547
server-node/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
server-node/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "ai-town-server",
"version": "1.0.0",
"description": "AI Town WebSocket server",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"express": "^4.18.2",
"ws": "^8.14.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.10.0",
"@types/ws": "^8.5.10",
"tsx": "^4.6.2",
"typescript": "^5.3.2"
}
}

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;
}

16
server-node/tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}