创建新工程

This commit is contained in:
moyin
2025-12-05 19:00:14 +08:00
commit ff4fa5fffd
227 changed files with 32804 additions and 0 deletions

38
server/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# Dependencies
node_modules/
package-lock.json
yarn.lock
# Build output
dist/
*.js
*.js.map
# Data files
data/*.json
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Keep data directory but ignore contents
!data/.gitkeep

0
server/.gitkeep Normal file
View File

51
server/Dockerfile.prod Normal file
View File

@@ -0,0 +1,51 @@
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY yarn.lock ./
# Install dependencies
RUN yarn install --frozen-lockfile
# Copy source code
COPY . .
# Build the application
RUN yarn build
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
# Install production dependencies only
COPY package*.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile --production && yarn cache clean
# Copy built application
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/admin ./admin
# Create data directory
RUN mkdir -p data logs
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# Change ownership
RUN chown -R nodejs:nodejs /app
USER nodejs
# Expose ports
EXPOSE 8080 8081
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:8080/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
# Start the application
CMD ["node", "dist/server.js"]

418
server/README.md Normal file
View File

@@ -0,0 +1,418 @@
# AI Town WebSocket Server
WebSocket 服务器用于 AI Town 多人在线游戏。
## 功能特性
### 核心功能
- ✅ WebSocket 连接管理
- ✅ 客户端身份验证
- ✅ 角色创建和管理
- ✅ 实时位置同步
- ✅ 对话消息传递
- ✅ 心跳检测
- ✅ 数据持久化JSON 文件)
- ✅ 在线/离线状态管理
### 监控和维护
- ✅ 系统健康监控
- ✅ 自动数据备份
- ✅ 日志管理和分析
- ✅ 维护任务调度
- ✅ Web 管理界面
## 快速开始
### 1. 安装依赖
```bash
cd server
npm install
# 或
yarn install
```
### 2. 运行服务器
#### 开发模式TypeScript
```bash
npm run dev
# 或
yarn dev
```
#### 生产模式
```bash
# 编译 TypeScript
npm run build
# 或
yarn build
# 运行编译后的代码
npm start
# 或
yarn start
```
### 3. 配置
服务器默认运行在端口 **8080**
可以通过环境变量修改:
```bash
PORT=3000 npm run dev
```
## 消息协议
所有消息使用 JSON 格式,包含以下字段:
```json
{
"type": "message_type",
"data": {},
"timestamp": 1234567890
}
```
### 客户端 → 服务器
#### 1. 身份验证
```json
{
"type": "auth_request",
"data": {
"username": "player1"
},
"timestamp": 1234567890
}
```
#### 2. 创建角色
```json
{
"type": "character_create",
"data": {
"name": "Hero"
},
"timestamp": 1234567890
}
```
#### 3. 角色移动
```json
{
"type": "character_move",
"data": {
"position": {
"x": 100.0,
"y": 200.0
}
},
"timestamp": 1234567890
}
```
#### 4. 发送对话
```json
{
"type": "dialogue_send",
"data": {
"receiverId": "character_id",
"message": "Hello!"
},
"timestamp": 1234567890
}
```
#### 5. 心跳
```json
{
"type": "ping",
"data": {},
"timestamp": 1234567890
}
```
### 服务器 → 客户端
#### 1. 身份验证响应
```json
{
"type": "auth_response",
"data": {
"success": true,
"clientId": "uuid"
},
"timestamp": 1234567890
}
```
#### 2. 角色创建响应
```json
{
"type": "character_create",
"data": {
"success": true,
"character": {
"id": "uuid",
"name": "Hero",
"position": { "x": 1000, "y": 750 },
"isOnline": true
}
},
"timestamp": 1234567890
}
```
#### 3. 世界状态
```json
{
"type": "world_state",
"data": {
"characters": [
{
"id": "uuid",
"name": "Hero",
"position": { "x": 100, "y": 200 },
"isOnline": true
}
]
},
"timestamp": 1234567890
}
```
#### 4. 角色状态更新
```json
{
"type": "character_state",
"data": {
"characterId": "uuid",
"name": "Hero",
"position": { "x": 100, "y": 200 },
"isOnline": false
},
"timestamp": 1234567890
}
```
#### 5. 角色移动广播
```json
{
"type": "character_move",
"data": {
"characterId": "uuid",
"position": { "x": 100, "y": 200 }
},
"timestamp": 1234567890
}
```
#### 6. 心跳响应
```json
{
"type": "pong",
"data": {},
"timestamp": 1234567890
}
```
#### 7. 错误消息
```json
{
"type": "error",
"data": {
"message": "Error description"
},
"timestamp": 1234567890
}
```
## 数据持久化
角色数据保存在 `server/data/characters.json` 文件中。
### 自动保存
- 创建/更新角色时立即保存
- 每 5 分钟自动保存一次
- 服务器关闭时保存
### 数据格式
```json
[
{
"id": "uuid",
"name": "Hero",
"ownerId": "client_id",
"position": { "x": 1000, "y": 750 },
"isOnline": false,
"createdAt": 1234567890,
"lastSeen": 1234567890
}
]
```
## 心跳机制
- 客户端应每 30 秒发送一次 `ping` 消息
- 服务器每 30 秒检查一次客户端心跳
- 如果客户端 60 秒内没有心跳,将被断开连接
## 开发
### 项目结构
```
server/
├── src/
│ └── server.ts # 主服务器文件
├── data/
│ └── characters.json # 角色数据(自动生成)
├── dist/ # 编译输出(自动生成)
├── package.json
├── tsconfig.json
└── README.md
```
### TypeScript 配置
查看 `tsconfig.json` 了解 TypeScript 编译配置。
### 监听模式
开发时可以使用监听模式自动重新编译:
```bash
npm run watch
# 或
yarn watch
```
## 测试
### 使用 wscat 测试
安装 wscat
```bash
npm install -g wscat
```
连接到服务器:
```bash
wscat -c ws://localhost:8080
```
发送测试消息:
```json
{"type":"auth_request","data":{"username":"test"},"timestamp":1234567890}
{"type":"character_create","data":{"name":"TestHero"},"timestamp":1234567890}
{"type":"ping","data":{},"timestamp":1234567890}
```
### 使用 Godot 客户端测试
1. 启动服务器
2. 运行 Godot 项目中的 Main.tscn
3. 尝试登录和创建角色
## 日志
服务器会输出以下日志:
- `🚀` 服务器启动
- `✅` 客户端连接
- `❌` 客户端断开/错误
- `📨` 接收消息
- `🔐` 身份验证
- `👤` 角色创建
- `💾` 数据保存
- `📂` 数据加载
- `⏰` 心跳超时
- `🛑` 服务器关闭
## 环境要求
- Node.js 16+
- npm 或 yarn
## 依赖
- `ws` - WebSocket 库
- `uuid` - UUID 生成
- `typescript` - TypeScript 编译器
- `ts-node` - TypeScript 运行时
## 故障排除
### 端口已被占用
如果看到 `EADDRINUSE` 错误,说明端口已被占用。
解决方法:
1. 更改端口:`PORT=3000 npm run dev`
2. 或关闭占用端口的程序
### 无法连接
确保:
1. 服务器正在运行
2. 防火墙允许端口 8080
3. 客户端使用正确的 URL`ws://localhost:8080`
### 数据丢失
数据保存在 `server/data/characters.json`
如果数据丢失:
1. 检查文件是否存在
2. 检查文件权限
3. 查看服务器日志中的错误信息
## 监控和管理
### Web 管理界面
服务器启动后,管理界面可通过以下方式访问:
- URL: `http://localhost:8081/admin/`
- 认证令牌: `admin123`(可通过环境变量 `ADMIN_TOKEN` 修改)
### 管理功能
- **系统监控**: 查看内存、CPU、连接数等指标
- **备份管理**: 创建、恢复、删除数据备份
- **日志分析**: 查看和分析服务器日志
- **维护任务**: 管理自动维护任务
### 环境变量
```bash
PORT=8080 # WebSocket 服务器端口
ADMIN_PORT=8081 # 管理 API 端口
ADMIN_TOKEN=admin123 # 管理员访问令牌
```
## 生产部署
### 使用 PM2
```bash
npm install -g pm2
pm2 start dist/server.js --name ai-town-server
pm2 logs ai-town-server
```
### 使用 Docker
```dockerfile
FROM node:16
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 8080 8081
CMD ["npm", "start"]
```
## 许可证
MIT

741
server/admin/index.html Normal file
View File

@@ -0,0 +1,741 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Town 服务器管理面板</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 1.8rem;
font-weight: 600;
}
.container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
border: 1px solid #e1e5e9;
}
.card h2 {
font-size: 1.2rem;
margin-bottom: 1rem;
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 0.5rem;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-healthy { background-color: #27ae60; }
.status-warning { background-color: #f39c12; }
.status-critical { background-color: #e74c3c; }
.metric {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #ecf0f1;
}
.metric:last-child {
border-bottom: none;
}
.metric-label {
font-weight: 500;
color: #555;
}
.metric-value {
font-weight: 600;
color: #2c3e50;
}
.btn {
background: #3498db;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
margin: 0.25rem;
transition: background-color 0.2s;
}
.btn:hover {
background: #2980b9;
}
.btn-success { background: #27ae60; }
.btn-success:hover { background: #229954; }
.btn-warning { background: #f39c12; }
.btn-warning:hover { background: #e67e22; }
.btn-danger { background: #e74c3c; }
.btn-danger:hover { background: #c0392b; }
.log-entry {
font-family: 'Courier New', monospace;
font-size: 0.8rem;
padding: 0.5rem;
background: #f8f9fa;
border-left: 3px solid #3498db;
margin: 0.5rem 0;
white-space: pre-wrap;
}
.backup-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 4px;
margin: 0.5rem 0;
}
.backup-info {
flex: 1;
}
.backup-actions {
display: flex;
gap: 0.5rem;
}
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.error {
background: #fee;
color: #c33;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
}
.success {
background: #efe;
color: #363;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
}
.tabs {
display: flex;
border-bottom: 2px solid #e1e5e9;
margin-bottom: 1rem;
}
.tab {
padding: 0.75rem 1.5rem;
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
color: #666;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.tab.active {
color: #3498db;
border-bottom-color: #3498db;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
@media (max-width: 768px) {
.dashboard {
grid-template-columns: 1fr;
}
.container {
padding: 0 0.5rem;
}
.header {
padding: 1rem;
}
}
</style>
</head>
<body>
<div class="header">
<h1>🏢 AI Town 服务器管理面板</h1>
</div>
<div class="container">
<div class="dashboard">
<!-- 系统健康状态 -->
<div class="card">
<h2>🏥 系统健康状态</h2>
<div id="health-status" class="loading">加载中...</div>
</div>
<!-- 连接统计 -->
<div class="card">
<h2>🔗 连接统计</h2>
<div id="connection-stats" class="loading">加载中...</div>
</div>
<!-- 系统信息 -->
<div class="card">
<h2>💻 系统信息</h2>
<div id="system-info" class="loading">加载中...</div>
</div>
</div>
<!-- 管理功能标签页 -->
<div class="card">
<div class="tabs">
<button class="tab active" onclick="showTab('backups')">📦 备份管理</button>
<button class="tab" onclick="showTab('logs')">📋 日志分析</button>
<button class="tab" onclick="showTab('maintenance')">🔧 维护任务</button>
</div>
<!-- 备份管理 -->
<div id="backups" class="tab-content active">
<div style="margin-bottom: 1rem;">
<button class="btn btn-success" onclick="createBackup()">创建备份</button>
<button class="btn" onclick="loadBackups()">刷新列表</button>
</div>
<div id="backup-list" class="loading">加载中...</div>
</div>
<!-- 日志分析 -->
<div id="logs" class="tab-content">
<div style="margin-bottom: 1rem;">
<button class="btn" onclick="analyzeLogs()">分析日志</button>
<button class="btn" onclick="loadLogFiles()">日志文件</button>
</div>
<div id="log-analysis" class="loading">点击"分析日志"开始分析</div>
</div>
<!-- 维护任务 -->
<div id="maintenance" class="tab-content">
<div style="margin-bottom: 1rem;">
<button class="btn btn-warning" onclick="toggleMaintenanceMode()">维护模式</button>
<button class="btn" onclick="loadTasks()">刷新任务</button>
</div>
<div id="maintenance-tasks" class="loading">加载中...</div>
</div>
</div>
</div>
<script>
// 动态获取API基础URL
const API_BASE = window.location.protocol + '//' + window.location.hostname + ':8081';
const AUTH_TOKEN = 'admin123'; // 在生产环境中应该从安全的地方获取
// API请求封装
async function apiRequest(endpoint, options = {}) {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${AUTH_TOKEN}`,
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
return response.json();
}
// 显示错误信息
function showError(element, error) {
element.innerHTML = `<div class="error">错误: ${error.message}</div>`;
}
// 显示成功信息
function showSuccess(element, message) {
element.innerHTML = `<div class="success">${message}</div>`;
}
// 格式化字节数
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 格式化时间
function formatTime(timestamp) {
return new Date(timestamp).toLocaleString('zh-CN');
}
// 标签页切换
function showTab(tabName) {
// 隐藏所有标签页内容
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// 移除所有标签的active类
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
// 显示选中的标签页
document.getElementById(tabName).classList.add('active');
event.target.classList.add('active');
// 加载对应的数据
if (tabName === 'backups') {
loadBackups();
} else if (tabName === 'logs') {
// 日志分析需要手动触发
} else if (tabName === 'maintenance') {
loadTasks();
}
}
// 加载系统健康状态
async function loadHealthStatus() {
try {
const data = await apiRequest('/health');
const element = document.getElementById('health-status');
const statusClass = `status-${data.status}`;
const statusText = {
'healthy': '健康',
'warning': '警告',
'critical': '严重'
}[data.status] || data.status;
let html = `
<div class="metric">
<span class="metric-label">
<span class="status-indicator ${statusClass}"></span>
系统状态
</span>
<span class="metric-value">${statusText}</span>
</div>
<div class="metric">
<span class="metric-label">内存使用</span>
<span class="metric-value">${data.metrics.memory.percentage.toFixed(1)}%</span>
</div>
<div class="metric">
<span class="metric-label">CPU使用</span>
<span class="metric-value">${data.metrics.cpu.usage.toFixed(1)}%</span>
</div>
<div class="metric">
<span class="metric-label">存储使用</span>
<span class="metric-value">${data.metrics.storage.percentage.toFixed(1)}%</span>
</div>
<div class="metric">
<span class="metric-label">运行时间</span>
<span class="metric-value">${Math.floor(data.metrics.uptime / 1000 / 60)} 分钟</span>
</div>
`;
if (data.issues && data.issues.length > 0) {
html += '<div style="margin-top: 1rem;"><strong>问题:</strong><ul>';
data.issues.forEach(issue => {
html += `<li style="color: #e74c3c;">${issue}</li>`;
});
html += '</ul></div>';
}
element.innerHTML = html;
} catch (error) {
showError(document.getElementById('health-status'), error);
}
}
// 加载连接统计
async function loadConnectionStats() {
try {
const data = await apiRequest('/system/info');
const element = document.getElementById('connection-stats');
element.innerHTML = `
<div class="metric">
<span class="metric-label">活跃连接</span>
<span class="metric-value">${data.connections.active}</span>
</div>
<div class="metric">
<span class="metric-label">总连接数</span>
<span class="metric-value">${data.connections.total}</span>
</div>
<div class="metric">
<span class="metric-label">维护模式</span>
<span class="metric-value">${data.maintenanceMode ? '开启' : '关闭'}</span>
</div>
`;
} catch (error) {
showError(document.getElementById('connection-stats'), error);
}
}
// 加载系统信息
async function loadSystemInfo() {
try {
const data = await apiRequest('/system/info');
const element = document.getElementById('system-info');
const info = data.systemInfo;
element.innerHTML = `
<div class="metric">
<span class="metric-label">平台</span>
<span class="metric-value">${info.platform}</span>
</div>
<div class="metric">
<span class="metric-label">架构</span>
<span class="metric-value">${info.arch}</span>
</div>
<div class="metric">
<span class="metric-label">Node.js版本</span>
<span class="metric-value">${info.nodeVersion}</span>
</div>
<div class="metric">
<span class="metric-label">CPU核心数</span>
<span class="metric-value">${info.cpus}</span>
</div>
<div class="metric">
<span class="metric-label">总内存</span>
<span class="metric-value">${formatBytes(info.totalMemory)}</span>
</div>
`;
} catch (error) {
showError(document.getElementById('system-info'), error);
}
}
// 加载备份列表
async function loadBackups() {
try {
const data = await apiRequest('/backups');
const element = document.getElementById('backup-list');
if (data.backups.length === 0) {
element.innerHTML = '<p>暂无备份</p>';
return;
}
let html = '';
data.backups.forEach(backup => {
html += `
<div class="backup-item">
<div class="backup-info">
<strong>${backup.id}</strong><br>
<small>${backup.description || '无描述'}</small><br>
<small>时间: ${formatTime(backup.timestamp)} | 大小: ${formatBytes(backup.size)}</small>
</div>
<div class="backup-actions">
<button class="btn btn-success" onclick="restoreBackup('${backup.id}')">恢复</button>
<button class="btn btn-danger" onclick="deleteBackup('${backup.id}')">删除</button>
</div>
</div>
`;
});
element.innerHTML = html;
} catch (error) {
showError(document.getElementById('backup-list'), error);
}
}
// 创建备份
async function createBackup() {
try {
const description = prompt('请输入备份描述(可选):');
const data = await apiRequest('/backups', {
method: 'POST',
body: JSON.stringify({ description })
});
showSuccess(document.getElementById('backup-list'), '备份创建成功!');
setTimeout(loadBackups, 1000);
} catch (error) {
showError(document.getElementById('backup-list'), error);
}
}
// 恢复备份
async function restoreBackup(backupId) {
if (!confirm(`确定要恢复备份 ${backupId} 吗?这将覆盖当前数据!`)) {
return;
}
try {
const data = await apiRequest(`/backups/${backupId}`, {
method: 'POST'
});
if (data.success) {
showSuccess(document.getElementById('backup-list'), '备份恢复成功!');
} else {
showError(document.getElementById('backup-list'), new Error('备份恢复失败'));
}
} catch (error) {
showError(document.getElementById('backup-list'), error);
}
}
// 删除备份
async function deleteBackup(backupId) {
if (!confirm(`确定要删除备份 ${backupId} 吗?`)) {
return;
}
try {
const data = await apiRequest(`/backups/${backupId}`, {
method: 'DELETE'
});
if (data.success) {
showSuccess(document.getElementById('backup-list'), '备份删除成功!');
setTimeout(loadBackups, 1000);
} else {
showError(document.getElementById('backup-list'), new Error('备份删除失败'));
}
} catch (error) {
showError(document.getElementById('backup-list'), error);
}
}
// 分析日志
async function analyzeLogs() {
try {
const data = await apiRequest('/logs/analyze');
const element = document.getElementById('log-analysis');
const analytics = data.analytics;
let html = `
<h3>日志分析结果</h3>
<div class="metric">
<span class="metric-label">总条目数</span>
<span class="metric-value">${analytics.totalEntries}</span>
</div>
<div class="metric">
<span class="metric-label">错误数</span>
<span class="metric-value">${analytics.levelCounts[3] || 0}</span>
</div>
<div class="metric">
<span class="metric-label">警告数</span>
<span class="metric-value">${analytics.levelCounts[2] || 0}</span>
</div>
<div class="metric">
<span class="metric-label">连接数</span>
<span class="metric-value">${analytics.connectionStats.totalConnections}</span>
</div>
`;
if (analytics.topErrors.length > 0) {
html += '<h4>主要错误:</h4>';
analytics.topErrors.forEach(error => {
html += `<div class="log-entry">${error.message} (${error.count}次)</div>`;
});
}
element.innerHTML = html;
} catch (error) {
showError(document.getElementById('log-analysis'), error);
}
}
// 加载日志文件
async function loadLogFiles() {
try {
const data = await apiRequest('/logs/files');
const element = document.getElementById('log-analysis');
let html = '<h3>日志文件列表</h3>';
data.logFiles.forEach(file => {
html += `
<div class="backup-item">
<div class="backup-info">
<strong>${file.name}</strong><br>
<small>大小: ${formatBytes(file.size)} | 修改时间: ${formatTime(file.modified)}</small>
</div>
</div>
`;
});
element.innerHTML = html;
} catch (error) {
showError(document.getElementById('log-analysis'), error);
}
}
// 加载维护任务
async function loadTasks() {
try {
const data = await apiRequest('/maintenance/tasks');
const element = document.getElementById('maintenance-tasks');
let html = '';
data.tasks.forEach(task => {
const priorityColor = {
'low': '#95a5a6',
'medium': '#f39c12',
'high': '#e67e22',
'critical': '#e74c3c'
}[task.priority] || '#95a5a6';
html += `
<div class="backup-item">
<div class="backup-info">
<strong>${task.name}</strong>
<span style="color: ${priorityColor}; margin-left: 10px;">[${task.priority}]</span><br>
<small>${task.description}</small><br>
<small>调度: ${task.schedule} | 状态: ${task.enabled ? '启用' : '禁用'}</small>
${task.lastRun ? `<br><small>上次运行: ${formatTime(task.lastRun)}</small>` : ''}
</div>
<div class="backup-actions">
<button class="btn" onclick="runTask('${task.id}')">运行</button>
<button class="btn btn-warning" onclick="toggleTask('${task.id}', ${!task.enabled})">
${task.enabled ? '禁用' : '启用'}
</button>
</div>
</div>
`;
});
element.innerHTML = html;
} catch (error) {
showError(document.getElementById('maintenance-tasks'), error);
}
}
// 运行任务
async function runTask(taskId) {
try {
const data = await apiRequest(`/maintenance/tasks/${taskId}`, {
method: 'POST'
});
if (data.success) {
showSuccess(document.getElementById('maintenance-tasks'), '任务执行成功!');
setTimeout(loadTasks, 1000);
} else {
showError(document.getElementById('maintenance-tasks'), new Error(data.error || '任务执行失败'));
}
} catch (error) {
showError(document.getElementById('maintenance-tasks'), error);
}
}
// 切换任务状态
async function toggleTask(taskId, enabled) {
try {
const data = await apiRequest('/maintenance/tasks', {
method: 'PUT',
body: JSON.stringify({ taskId, updates: { enabled } })
});
if (data.success) {
showSuccess(document.getElementById('maintenance-tasks'), '任务状态更新成功!');
setTimeout(loadTasks, 1000);
} else {
showError(document.getElementById('maintenance-tasks'), new Error('任务状态更新失败'));
}
} catch (error) {
showError(document.getElementById('maintenance-tasks'), error);
}
}
// 切换维护模式
async function toggleMaintenanceMode() {
try {
const systemInfo = await apiRequest('/system/info');
const currentMode = systemInfo.maintenanceMode;
const newMode = !currentMode;
if (newMode && !confirm('确定要进入维护模式吗?这将影响用户访问。')) {
return;
}
const reason = newMode ? prompt('请输入维护原因:') || '手动维护' : '';
const data = await apiRequest('/maintenance/mode', {
method: 'POST',
body: JSON.stringify({ enabled: newMode, reason })
});
showSuccess(document.getElementById('maintenance-tasks'), data.message);
setTimeout(() => {
loadConnectionStats();
loadTasks();
}, 1000);
} catch (error) {
showError(document.getElementById('maintenance-tasks'), error);
}
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
loadHealthStatus();
loadConnectionStats();
loadSystemInfo();
loadBackups();
// 定期刷新健康状态
setInterval(() => {
loadHealthStatus();
loadConnectionStats();
}, 30000); // 30秒
});
</script>
</body>
</html>

0
server/data/.gitkeep Normal file
View File

View File

@@ -0,0 +1,10 @@
{
"id": "backup_1764929072516",
"timestamp": 1764929072520,
"size": 4442,
"compressed": true,
"description": "Scheduled maintenance backup",
"files": [
"characters.json.gz"
]
}

View File

@@ -0,0 +1,12 @@
{
"id": "backup_1764929388237",
"timestamp": 1764929388245,
"size": 6574,
"compressed": true,
"description": "Scheduled maintenance backup",
"files": [
"characters.json.gz",
"logs\\server_2025-12-05.log",
"maintenance_tasks.json.gz"
]
}

View File

@@ -0,0 +1,12 @@
{
"id": "backup_1764929391984",
"timestamp": 1764929391991,
"size": 6692,
"compressed": true,
"description": "Manual backup via API",
"files": [
"characters.json.gz",
"logs\\server_2025-12-05.log",
"maintenance_tasks.json.gz"
]
}

28
server/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "ai-town-server",
"version": "1.0.0",
"description": "WebSocket server for AI Town multiplayer game",
"main": "dist/server.js",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"start:monitor": "node start_with_monitoring.js",
"dev": "ts-node src/server.ts",
"watch": "tsc --watch",
"test:api": "node test_admin_api.js"
},
"keywords": ["websocket", "game", "multiplayer"],
"author": "",
"license": "MIT",
"dependencies": {
"ws": "^8.18.0",
"uuid": "^10.0.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/ws": "^8.5.13",
"@types/uuid": "^10.0.0",
"typescript": "^5.7.2",
"ts-node": "^10.9.2"
}
}

310
server/src/api/AdminAPI.ts Normal file
View File

@@ -0,0 +1,310 @@
import * as http from 'http';
import * as url from 'url';
import * as fs from 'fs';
import * as path from 'path';
import { HealthChecker } from '../monitoring/HealthChecker';
import { BackupManager } from '../backup/BackupManager';
import { LogManager } from '../logging/LogManager';
import { MaintenanceManager } from '../maintenance/MaintenanceManager';
export class AdminAPI {
private server: http.Server;
private healthChecker: HealthChecker;
private backupManager: BackupManager;
private logManager: LogManager;
private maintenanceManager: MaintenanceManager;
private getActiveConnections: () => number;
private getTotalConnections: () => number;
constructor(
port: number,
healthChecker: HealthChecker,
backupManager: BackupManager,
logManager: LogManager,
maintenanceManager: MaintenanceManager,
getActiveConnections: () => number,
getTotalConnections: () => number
) {
this.healthChecker = healthChecker;
this.backupManager = backupManager;
this.logManager = logManager;
this.maintenanceManager = maintenanceManager;
this.getActiveConnections = getActiveConnections;
this.getTotalConnections = getTotalConnections;
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
this.server.listen(port, () => {
this.logManager.info('ADMIN_API', `Admin API server started on port ${port}`);
console.log(`🔧 Admin API server started on port ${port}`);
});
}
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
// 设置CORS头
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
const parsedUrl = url.parse(req.url || '', true);
const pathname = parsedUrl.pathname || '';
const method = req.method || 'GET';
// 处理静态文件请求
if (pathname === '/' || pathname === '/admin' || pathname === '/admin/') {
await this.serveStaticFile(res, 'admin/index.html');
return;
}
if (pathname.startsWith('/admin/') && method === 'GET') {
const filePath = pathname.substring(1); // 移除开头的 /
await this.serveStaticFile(res, filePath);
return;
}
try {
// 简单的认证检查(在生产环境中应该使用更安全的方法)
const authHeader = req.headers.authorization;
if (!this.isAuthorized(authHeader)) {
this.sendResponse(res, 401, { error: 'Unauthorized' });
return;
}
// 路由处理
if (pathname === '/health' && method === 'GET') {
await this.handleHealthCheck(req, res);
} else if (pathname === '/backups' && method === 'GET') {
await this.handleListBackups(req, res);
} else if (pathname === '/backups' && method === 'POST') {
await this.handleCreateBackup(req, res);
} else if (pathname.startsWith('/backups/') && method === 'POST') {
await this.handleRestoreBackup(req, res, pathname);
} else if (pathname.startsWith('/backups/') && method === 'DELETE') {
await this.handleDeleteBackup(req, res, pathname);
} else if (pathname === '/logs/analyze' && method === 'GET') {
await this.handleAnalyzeLogs(req, res, parsedUrl.query);
} else if (pathname === '/logs/files' && method === 'GET') {
await this.handleListLogFiles(req, res);
} else if (pathname === '/maintenance/tasks' && method === 'GET') {
await this.handleListTasks(req, res);
} else if (pathname === '/maintenance/tasks' && method === 'PUT') {
await this.handleUpdateTask(req, res);
} else if (pathname.startsWith('/maintenance/tasks/') && method === 'POST') {
await this.handleRunTask(req, res, pathname);
} else if (pathname === '/maintenance/mode' && method === 'POST') {
await this.handleMaintenanceMode(req, res);
} else if (pathname === '/system/info' && method === 'GET') {
await this.handleSystemInfo(req, res);
} else {
this.sendResponse(res, 404, { error: 'Not found' });
}
} catch (error) {
this.logManager.error('ADMIN_API', 'API request error', {
error: error instanceof Error ? error.message : String(error),
pathname,
method
});
this.sendResponse(res, 500, { error: 'Internal server error' });
}
}
private isAuthorized(authHeader?: string): boolean {
// 简单的认证 - 在生产环境中应该使用更安全的方法
const expectedToken = process.env.ADMIN_TOKEN || 'admin123';
return authHeader === `Bearer ${expectedToken}`;
}
private async handleHealthCheck(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const health = await this.healthChecker.getHealthStatus(
this.getActiveConnections(),
this.getTotalConnections()
);
const serviceHealth = await this.healthChecker.checkServiceHealth();
this.sendResponse(res, 200, {
...health,
serviceHealth
});
}
private async handleListBackups(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const backups = await this.backupManager.listBackups();
this.sendResponse(res, 200, { backups });
}
private async handleCreateBackup(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const body = await this.readRequestBody(req);
const { description } = JSON.parse(body || '{}');
const backup = await this.backupManager.createBackup({
description: description || 'Manual backup via API'
});
this.sendResponse(res, 201, { backup });
}
private async handleRestoreBackup(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
const backupId = pathname.split('/')[2];
const success = await this.backupManager.restoreBackup(backupId);
this.sendResponse(res, success ? 200 : 400, {
success,
message: success ? 'Backup restored successfully' : 'Failed to restore backup'
});
}
private async handleDeleteBackup(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
const backupId = pathname.split('/')[2];
const success = await this.backupManager.deleteBackup(backupId);
this.sendResponse(res, success ? 200 : 400, {
success,
message: success ? 'Backup deleted successfully' : 'Failed to delete backup'
});
}
private async handleAnalyzeLogs(req: http.IncomingMessage, res: http.ServerResponse, query: any): Promise<void> {
const options: any = {};
if (query.startTime) options.startTime = parseInt(query.startTime);
if (query.endTime) options.endTime = parseInt(query.endTime);
if (query.level) options.level = parseInt(query.level);
if (query.category) options.category = query.category;
if (query.limit) options.limit = parseInt(query.limit);
const analytics = await this.logManager.analyzeLogs(options);
this.sendResponse(res, 200, { analytics });
}
private async handleListLogFiles(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const logFiles = this.logManager.getLogFiles();
this.sendResponse(res, 200, { logFiles });
}
private async handleListTasks(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const tasks = this.maintenanceManager.getTasks();
this.sendResponse(res, 200, { tasks });
}
private async handleUpdateTask(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const body = await this.readRequestBody(req);
const { taskId, updates } = JSON.parse(body || '{}');
const success = this.maintenanceManager.updateTask(taskId, updates);
this.sendResponse(res, success ? 200 : 400, {
success,
message: success ? 'Task updated successfully' : 'Failed to update task'
});
}
private async handleRunTask(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
const taskId = pathname.split('/')[3];
const result = await this.maintenanceManager.runTask(taskId);
this.sendResponse(res, result.success ? 200 : 400, result);
}
private async handleMaintenanceMode(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const body = await this.readRequestBody(req);
const { enabled, reason } = JSON.parse(body || '{}');
if (enabled) {
await this.maintenanceManager.enterMaintenanceMode(reason || 'Manual maintenance mode');
} else {
await this.maintenanceManager.exitMaintenanceMode();
}
this.sendResponse(res, 200, {
maintenanceMode: this.maintenanceManager.isInMaintenanceMode(),
message: enabled ? 'Entered maintenance mode' : 'Exited maintenance mode'
});
}
private async handleSystemInfo(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const systemInfo = this.healthChecker.getSystemInfo();
const health = await this.healthChecker.getHealthStatus(
this.getActiveConnections(),
this.getTotalConnections()
);
this.sendResponse(res, 200, {
systemInfo,
health,
connections: {
active: this.getActiveConnections(),
total: this.getTotalConnections()
},
maintenanceMode: this.maintenanceManager.isInMaintenanceMode()
});
}
private async readRequestBody(req: http.IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
resolve(body);
});
req.on('error', reject);
});
}
private sendResponse(res: http.ServerResponse, statusCode: number, data: any): void {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data, null, 2));
}
private async serveStaticFile(res: http.ServerResponse, filePath: string): Promise<void> {
try {
const fullPath = path.join(__dirname, '../../', filePath);
if (!fs.existsSync(fullPath)) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('File not found');
return;
}
const content = fs.readFileSync(fullPath);
const ext = path.extname(filePath).toLowerCase();
let contentType = 'text/plain';
switch (ext) {
case '.html':
contentType = 'text/html';
break;
case '.css':
contentType = 'text/css';
break;
case '.js':
contentType = 'application/javascript';
break;
case '.json':
contentType = 'application/json';
break;
}
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
} catch (error) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal server error');
}
}
public close(): void {
this.server.close();
}
}

View File

@@ -0,0 +1,384 @@
import * as fs from 'fs';
import * as path from 'path';
import * as zlib from 'zlib';
import { promisify } from 'util';
const gzip = promisify(zlib.gzip);
const gunzip = promisify(zlib.gunzip);
export interface BackupInfo {
id: string;
timestamp: number;
size: number;
compressed: boolean;
description?: string;
files: string[];
}
export interface BackupOptions {
compress?: boolean;
description?: string;
maxBackups?: number;
}
export class BackupManager {
private dataDir: string;
private backupDir: string;
constructor(dataDir: string) {
this.dataDir = dataDir;
this.backupDir = path.join(dataDir, 'backups');
this.ensureBackupDirectory();
}
/**
* 确保备份目录存在
*/
private ensureBackupDirectory(): void {
if (!fs.existsSync(this.backupDir)) {
fs.mkdirSync(this.backupDir, { recursive: true });
}
}
/**
* 创建备份
*/
async createBackup(options: BackupOptions = {}): Promise<BackupInfo> {
const {
compress = true,
description = 'Automatic backup',
maxBackups = 10
} = options;
const backupId = `backup_${Date.now()}`;
const backupPath = path.join(this.backupDir, backupId);
// 创建备份目录
fs.mkdirSync(backupPath, { recursive: true });
// 获取需要备份的文件
const filesToBackup = await this.getFilesToBackup();
const backedUpFiles: string[] = [];
console.log(`📦 Creating backup: ${backupId}`);
for (const file of filesToBackup) {
try {
const relativePath = path.relative(this.dataDir, file);
const backupFilePath = path.join(backupPath, relativePath);
// 确保目标目录存在
const backupFileDir = path.dirname(backupFilePath);
if (!fs.existsSync(backupFileDir)) {
fs.mkdirSync(backupFileDir, { recursive: true });
}
// 读取原文件
const fileData = await fs.promises.readFile(file);
if (compress && path.extname(file) === '.json') {
// 压缩JSON文件
const compressed = await gzip(fileData);
await fs.promises.writeFile(backupFilePath + '.gz', compressed);
backedUpFiles.push(relativePath + '.gz');
} else {
// 直接复制文件
await fs.promises.writeFile(backupFilePath, fileData);
backedUpFiles.push(relativePath);
}
console.log(` ✅ Backed up: ${relativePath}`);
} catch (error) {
console.error(` ❌ Failed to backup ${file}:`, error);
}
}
// 计算备份大小
const backupSize = await this.calculateDirectorySize(backupPath);
// 创建备份信息文件
const backupInfo: BackupInfo = {
id: backupId,
timestamp: Date.now(),
size: backupSize,
compressed: compress,
description,
files: backedUpFiles
};
await fs.promises.writeFile(
path.join(backupPath, 'backup_info.json'),
JSON.stringify(backupInfo, null, 2)
);
console.log(`📦 Backup created: ${backupId} (${this.formatBytes(backupSize)})`);
// 清理旧备份
await this.cleanupOldBackups(maxBackups);
return backupInfo;
}
/**
* 恢复备份
*/
async restoreBackup(backupId: string): Promise<boolean> {
const backupPath = path.join(this.backupDir, backupId);
if (!fs.existsSync(backupPath)) {
console.error(`❌ Backup not found: ${backupId}`);
return false;
}
// 读取备份信息
const backupInfoPath = path.join(backupPath, 'backup_info.json');
if (!fs.existsSync(backupInfoPath)) {
console.error(`❌ Backup info not found: ${backupId}`);
return false;
}
const backupInfo: BackupInfo = JSON.parse(
await fs.promises.readFile(backupInfoPath, 'utf-8')
);
console.log(`🔄 Restoring backup: ${backupId}`);
// 创建当前数据的备份(以防恢复失败)
const emergencyBackup = await this.createBackup({
description: `Emergency backup before restore ${backupId}`,
maxBackups: 50 // 保留更多紧急备份
});
try {
// 恢复文件
for (const file of backupInfo.files) {
const backupFilePath = path.join(backupPath, file);
const isCompressed = file.endsWith('.gz');
const originalFileName = isCompressed ? file.slice(0, -3) : file;
const targetPath = path.join(this.dataDir, originalFileName);
// 确保目标目录存在
const targetDir = path.dirname(targetPath);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
try {
if (isCompressed) {
// 解压缩文件
const compressedData = await fs.promises.readFile(backupFilePath);
const decompressed = await gunzip(compressedData);
await fs.promises.writeFile(targetPath, decompressed);
} else {
// 直接复制文件
const fileData = await fs.promises.readFile(backupFilePath);
await fs.promises.writeFile(targetPath, fileData);
}
console.log(` ✅ Restored: ${originalFileName}`);
} catch (error) {
console.error(` ❌ Failed to restore ${file}:`, error);
throw error;
}
}
console.log(`🔄 Backup restored successfully: ${backupId}`);
return true;
} catch (error) {
console.error(`❌ Restore failed, attempting to restore emergency backup:`, error);
// 尝试恢复紧急备份
const emergencyRestoreSuccess = await this.restoreBackup(emergencyBackup.id);
if (emergencyRestoreSuccess) {
console.log(`✅ Emergency backup restored successfully`);
} else {
console.error(`❌ Emergency backup restore also failed!`);
}
return false;
}
}
/**
* 获取所有备份列表
*/
async listBackups(): Promise<BackupInfo[]> {
const backups: BackupInfo[] = [];
try {
const backupDirs = await fs.promises.readdir(this.backupDir);
for (const dir of backupDirs) {
const backupPath = path.join(this.backupDir, dir);
const backupInfoPath = path.join(backupPath, 'backup_info.json');
if (fs.existsSync(backupInfoPath)) {
try {
const backupInfo: BackupInfo = JSON.parse(
await fs.promises.readFile(backupInfoPath, 'utf-8')
);
backups.push(backupInfo);
} catch (error) {
console.error(`Error reading backup info for ${dir}:`, error);
}
}
}
// 按时间戳排序(最新的在前)
backups.sort((a, b) => b.timestamp - a.timestamp);
} catch (error) {
console.error('Error listing backups:', error);
}
return backups;
}
/**
* 删除备份
*/
async deleteBackup(backupId: string): Promise<boolean> {
const backupPath = path.join(this.backupDir, backupId);
if (!fs.existsSync(backupPath)) {
console.error(`❌ Backup not found: ${backupId}`);
return false;
}
try {
await this.deleteDirectory(backupPath);
console.log(`🗑️ Backup deleted: ${backupId}`);
return true;
} catch (error) {
console.error(`❌ Failed to delete backup ${backupId}:`, error);
return false;
}
}
/**
* 获取需要备份的文件列表
*/
private async getFilesToBackup(): Promise<string[]> {
const files: string[] = [];
// 递归获取数据目录中的所有文件(除了备份目录)
const scanDirectory = async (dir: string) => {
const items = await fs.promises.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
// 跳过备份目录
if (itemPath === this.backupDir) {
continue;
}
const stat = await fs.promises.stat(itemPath);
if (stat.isDirectory()) {
await scanDirectory(itemPath);
} else if (stat.isFile()) {
// 只备份特定类型的文件
const ext = path.extname(item).toLowerCase();
if (['.json', '.txt', '.log', '.config'].includes(ext)) {
files.push(itemPath);
}
}
}
};
await scanDirectory(this.dataDir);
return files;
}
/**
* 计算目录大小
*/
private async calculateDirectorySize(dir: string): Promise<number> {
let totalSize = 0;
const items = await fs.promises.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const stat = await fs.promises.stat(itemPath);
if (stat.isDirectory()) {
totalSize += await this.calculateDirectorySize(itemPath);
} else {
totalSize += stat.size;
}
}
return totalSize;
}
/**
* 清理旧备份
*/
private async cleanupOldBackups(maxBackups: number): Promise<void> {
const backups = await this.listBackups();
if (backups.length > maxBackups) {
const backupsToDelete = backups.slice(maxBackups);
for (const backup of backupsToDelete) {
await this.deleteBackup(backup.id);
}
console.log(`🧹 Cleaned up ${backupsToDelete.length} old backups`);
}
}
/**
* 删除目录及其内容
*/
private async deleteDirectory(dir: string): Promise<void> {
const items = await fs.promises.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const stat = await fs.promises.stat(itemPath);
if (stat.isDirectory()) {
await this.deleteDirectory(itemPath);
} else {
await fs.promises.unlink(itemPath);
}
}
await fs.promises.rmdir(dir);
}
/**
* 格式化字节数
*/
private formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* 自动备份调度
*/
startAutoBackup(intervalHours: number = 6): NodeJS.Timeout {
console.log(`⏰ Auto backup scheduled every ${intervalHours} hours`);
return setInterval(async () => {
try {
await this.createBackup({
description: `Automatic backup - ${new Date().toISOString()}`,
maxBackups: 20
});
} catch (error) {
console.error('❌ Auto backup failed:', error);
}
}, intervalHours * 60 * 60 * 1000);
}
}

View File

@@ -0,0 +1,484 @@
import * as fs from 'fs';
import * as path from 'path';
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
CRITICAL = 4
}
export interface LogEntry {
timestamp: number;
level: LogLevel;
category: string;
message: string;
data?: any;
clientId?: string;
characterId?: string;
}
export interface LogAnalytics {
totalEntries: number;
levelCounts: { [key in LogLevel]: number };
categoryCounts: { [category: string]: number };
timeRange: { start: number; end: number };
topErrors: Array<{ message: string; count: number }>;
connectionStats: {
totalConnections: number;
totalDisconnections: number;
averageSessionDuration: number;
};
}
export class LogManager {
private logDir: string;
private currentLogFile: string;
private logLevel: LogLevel;
private maxLogFileSize: number;
private maxLogFiles: number;
private logBuffer: LogEntry[] = [];
private flushInterval: NodeJS.Timeout;
constructor(dataDir: string, options: {
logLevel?: LogLevel;
maxLogFileSize?: number;
maxLogFiles?: number;
flushIntervalMs?: number;
} = {}) {
this.logDir = path.join(dataDir, 'logs');
this.logLevel = options.logLevel ?? LogLevel.INFO;
this.maxLogFileSize = options.maxLogFileSize ?? 10 * 1024 * 1024; // 10MB
this.maxLogFiles = options.maxLogFiles ?? 30; // 30 files
this.ensureLogDirectory();
this.currentLogFile = this.getCurrentLogFileName();
// 定期刷新日志缓冲区
this.flushInterval = setInterval(() => {
this.flushLogs();
}, options.flushIntervalMs ?? 5000); // 5秒
// 程序退出时刷新日志
process.on('exit', () => this.flushLogs());
process.on('SIGINT', () => this.flushLogs());
process.on('SIGTERM', () => this.flushLogs());
}
/**
* 确保日志目录存在
*/
private ensureLogDirectory(): void {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
}
/**
* 获取当前日志文件名
*/
private getCurrentLogFileName(): string {
const date = new Date();
const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD
return path.join(this.logDir, `server_${dateStr}.log`);
}
/**
* 记录日志
*/
log(level: LogLevel, category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
if (level < this.logLevel) {
return;
}
const entry: LogEntry = {
timestamp: Date.now(),
level,
category,
message,
data,
clientId,
characterId
};
this.logBuffer.push(entry);
// 如果是错误或关键级别,立即刷新
if (level >= LogLevel.ERROR) {
this.flushLogs();
}
// 控制台输出
this.outputToConsole(entry);
}
/**
* 便捷方法
*/
debug(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
this.log(LogLevel.DEBUG, category, message, data, clientId, characterId);
}
info(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
this.log(LogLevel.INFO, category, message, data, clientId, characterId);
}
warn(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
this.log(LogLevel.WARN, category, message, data, clientId, characterId);
}
error(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
this.log(LogLevel.ERROR, category, message, data, clientId, characterId);
}
critical(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
this.log(LogLevel.CRITICAL, category, message, data, clientId, characterId);
}
/**
* 刷新日志缓冲区到文件
*/
private flushLogs(): void {
if (this.logBuffer.length === 0) {
return;
}
try {
// 检查是否需要轮转日志文件
this.rotateLogFileIfNeeded();
// 将缓冲区中的日志写入文件
const logLines = this.logBuffer.map(entry => this.formatLogEntry(entry));
const logContent = logLines.join('\n') + '\n';
fs.appendFileSync(this.currentLogFile, logContent);
// 清空缓冲区
this.logBuffer = [];
} catch (error) {
console.error('Failed to flush logs:', error);
}
}
/**
* 格式化日志条目
*/
private formatLogEntry(entry: LogEntry): string {
const timestamp = new Date(entry.timestamp).toISOString();
const levelStr = LogLevel[entry.level].padEnd(8);
const category = entry.category.padEnd(15);
let line = `${timestamp} [${levelStr}] ${category} ${entry.message}`;
if (entry.clientId) {
line += ` [Client: ${entry.clientId}]`;
}
if (entry.characterId) {
line += ` [Character: ${entry.characterId}]`;
}
if (entry.data) {
line += ` [Data: ${JSON.stringify(entry.data)}]`;
}
return line;
}
/**
* 控制台输出
*/
private outputToConsole(entry: LogEntry): void {
const formatted = this.formatLogEntry(entry);
switch (entry.level) {
case LogLevel.DEBUG:
console.debug(formatted);
break;
case LogLevel.INFO:
console.info(formatted);
break;
case LogLevel.WARN:
console.warn(formatted);
break;
case LogLevel.ERROR:
case LogLevel.CRITICAL:
console.error(formatted);
break;
}
}
/**
* 轮转日志文件
*/
private rotateLogFileIfNeeded(): void {
try {
// 检查当前日志文件是否存在且大小是否超过限制
if (fs.existsSync(this.currentLogFile)) {
const stats = fs.statSync(this.currentLogFile);
if (stats.size >= this.maxLogFileSize) {
// 重命名当前文件
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const rotatedFileName = this.currentLogFile.replace('.log', `_${timestamp}.log`);
fs.renameSync(this.currentLogFile, rotatedFileName);
console.log(`📋 Log file rotated: ${path.basename(rotatedFileName)}`);
}
}
// 更新当前日志文件名(可能是新的日期)
this.currentLogFile = this.getCurrentLogFileName();
// 清理旧日志文件
this.cleanupOldLogFiles();
} catch (error) {
console.error('Failed to rotate log file:', error);
}
}
/**
* 清理旧日志文件
*/
private cleanupOldLogFiles(): void {
try {
const logFiles = fs.readdirSync(this.logDir)
.filter(file => file.endsWith('.log'))
.map(file => ({
name: file,
path: path.join(this.logDir, file),
mtime: fs.statSync(path.join(this.logDir, file)).mtime
}))
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
if (logFiles.length > this.maxLogFiles) {
const filesToDelete = logFiles.slice(this.maxLogFiles);
for (const file of filesToDelete) {
fs.unlinkSync(file.path);
console.log(`🗑️ Deleted old log file: ${file.name}`);
}
}
} catch (error) {
console.error('Failed to cleanup old log files:', error);
}
}
/**
* 分析日志
*/
async analyzeLogs(options: {
startTime?: number;
endTime?: number;
level?: LogLevel;
category?: string;
limit?: number;
} = {}): Promise<LogAnalytics> {
const {
startTime = Date.now() - 24 * 60 * 60 * 1000, // 默认24小时
endTime = Date.now(),
level,
category,
limit = 1000
} = options;
const entries = await this.readLogEntries(startTime, endTime, limit);
// 过滤条目
const filteredEntries = entries.filter(entry => {
if (level !== undefined && entry.level !== level) return false;
if (category && entry.category !== category) return false;
return true;
});
// 统计分析
const levelCounts = {
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 0,
[LogLevel.WARN]: 0,
[LogLevel.ERROR]: 0,
[LogLevel.CRITICAL]: 0
};
const categoryCounts: { [category: string]: number } = {};
const errorMessages: { [message: string]: number } = {};
let connectionCount = 0;
let disconnectionCount = 0;
const sessionDurations: number[] = [];
for (const entry of filteredEntries) {
levelCounts[entry.level]++;
categoryCounts[entry.category] = (categoryCounts[entry.category] || 0) + 1;
if (entry.level >= LogLevel.ERROR) {
errorMessages[entry.message] = (errorMessages[entry.message] || 0) + 1;
}
// 连接统计
if (entry.message.includes('connected')) {
connectionCount++;
} else if (entry.message.includes('disconnected')) {
disconnectionCount++;
}
}
// 排序错误消息
const topErrors = Object.entries(errorMessages)
.map(([message, count]) => ({ message, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
return {
totalEntries: filteredEntries.length,
levelCounts,
categoryCounts,
timeRange: { start: startTime, end: endTime },
topErrors,
connectionStats: {
totalConnections: connectionCount,
totalDisconnections: disconnectionCount,
averageSessionDuration: sessionDurations.length > 0
? sessionDurations.reduce((a, b) => a + b, 0) / sessionDurations.length
: 0
}
};
}
/**
* 读取日志条目
*/
private async readLogEntries(startTime: number, endTime: number, limit: number): Promise<LogEntry[]> {
const entries: LogEntry[] = [];
try {
const logFiles = fs.readdirSync(this.logDir)
.filter(file => file.endsWith('.log'))
.map(file => path.join(this.logDir, file))
.sort();
for (const logFile of logFiles) {
const content = fs.readFileSync(logFile, 'utf-8');
const lines = content.split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const entry = this.parseLogLine(line);
if (entry && entry.timestamp >= startTime && entry.timestamp <= endTime) {
entries.push(entry);
if (entries.length >= limit) {
return entries;
}
}
} catch (error) {
// 忽略解析错误的行
}
}
}
} catch (error) {
console.error('Failed to read log entries:', error);
}
return entries.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* 解析日志行
*/
private parseLogLine(line: string): LogEntry | null {
try {
// 格式: 2023-12-05T10:30:00.000Z [INFO ] CONNECTION Client connected [Client: abc123] [Data: {...}]
const timestampMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/);
if (!timestampMatch) return null;
const timestamp = new Date(timestampMatch[1]).getTime();
const levelMatch = line.match(/\[(\w+)\s*\]/);
if (!levelMatch) return null;
const levelStr = levelMatch[1];
const level = LogLevel[levelStr as keyof typeof LogLevel];
if (level === undefined) return null;
const parts = line.split('] ');
if (parts.length < 3) return null;
const category = parts[1].trim();
const messagePart = parts.slice(2).join('] ');
// 提取客户端ID和角色ID
const clientMatch = messagePart.match(/\[Client: ([^\]]+)\]/);
const characterMatch = messagePart.match(/\[Character: ([^\]]+)\]/);
const dataMatch = messagePart.match(/\[Data: (.+)\]$/);
let message = messagePart;
let data: any = undefined;
// 清理消息文本
message = message.replace(/\[Client: [^\]]+\]/, '').trim();
message = message.replace(/\[Character: [^\]]+\]/, '').trim();
if (dataMatch) {
message = message.replace(/\[Data: .+\]$/, '').trim();
try {
data = JSON.parse(dataMatch[1]);
} catch {
// 忽略JSON解析错误
}
}
return {
timestamp,
level,
category,
message,
data,
clientId: clientMatch ? clientMatch[1] : undefined,
characterId: characterMatch ? characterMatch[1] : undefined
};
} catch (error) {
return null;
}
}
/**
* 获取日志文件列表
*/
getLogFiles(): Array<{ name: string; size: number; modified: number }> {
try {
return fs.readdirSync(this.logDir)
.filter(file => file.endsWith('.log'))
.map(file => {
const filePath = path.join(this.logDir, file);
const stats = fs.statSync(filePath);
return {
name: file,
size: stats.size,
modified: stats.mtime.getTime()
};
})
.sort((a, b) => b.modified - a.modified);
} catch (error) {
console.error('Failed to get log files:', error);
return [];
}
}
/**
* 清理资源
*/
destroy(): void {
if (this.flushInterval) {
clearInterval(this.flushInterval);
}
this.flushLogs();
}
}

View File

@@ -0,0 +1,720 @@
import * as fs from 'fs';
import * as path from 'path';
import { HealthChecker, HealthStatus } from '../monitoring/HealthChecker';
import { BackupManager } from '../backup/BackupManager';
import { LogManager, LogLevel } from '../logging/LogManager';
export interface MaintenanceTask {
id: string;
name: string;
description: string;
schedule: string; // cron-like schedule
lastRun?: number;
nextRun?: number;
enabled: boolean;
priority: 'low' | 'medium' | 'high' | 'critical';
}
export interface MaintenanceReport {
timestamp: number;
tasks: Array<{
task: MaintenanceTask;
status: 'success' | 'failed' | 'skipped';
duration: number;
error?: string;
details?: any;
}>;
systemHealth: HealthStatus;
recommendations: string[];
}
export class MaintenanceManager {
private dataDir: string;
private healthChecker: HealthChecker;
private backupManager: BackupManager;
private logManager: LogManager;
private tasks: Map<string, MaintenanceTask> = new Map();
private maintenanceInterval: NodeJS.Timeout | null = null;
private isMaintenanceMode: boolean = false;
constructor(
dataDir: string,
healthChecker: HealthChecker,
backupManager: BackupManager,
logManager: LogManager
) {
this.dataDir = dataDir;
this.healthChecker = healthChecker;
this.backupManager = backupManager;
this.logManager = logManager;
this.initializeDefaultTasks();
this.loadTasks();
}
/**
* 初始化默认维护任务
*/
private initializeDefaultTasks(): void {
const defaultTasks: MaintenanceTask[] = [
{
id: 'health_check',
name: 'System Health Check',
description: 'Check system health metrics and alert on issues',
schedule: '*/5 * * * *', // Every 5 minutes
enabled: true,
priority: 'high'
},
{
id: 'backup_data',
name: 'Data Backup',
description: 'Create backup of all game data',
schedule: '0 */6 * * *', // Every 6 hours
enabled: true,
priority: 'high'
},
{
id: 'cleanup_logs',
name: 'Log Cleanup',
description: 'Clean up old log files and rotate current logs',
schedule: '0 2 * * *', // Daily at 2 AM
enabled: true,
priority: 'medium'
},
{
id: 'cleanup_temp_files',
name: 'Temporary Files Cleanup',
description: 'Remove temporary files and clean up cache',
schedule: '0 3 * * *', // Daily at 3 AM
enabled: true,
priority: 'low'
},
{
id: 'validate_data',
name: 'Data Validation',
description: 'Validate integrity of character and world data',
schedule: '0 4 * * 0', // Weekly on Sunday at 4 AM
enabled: true,
priority: 'medium'
},
{
id: 'performance_analysis',
name: 'Performance Analysis',
description: 'Analyze system performance and generate reports',
schedule: '0 1 * * *', // Daily at 1 AM
enabled: true,
priority: 'low'
},
{
id: 'security_scan',
name: 'Security Scan',
description: 'Scan for security issues and suspicious activities',
schedule: '0 0 * * *', // Daily at midnight
enabled: true,
priority: 'high'
}
];
for (const task of defaultTasks) {
this.tasks.set(task.id, task);
}
}
/**
* 启动维护调度器
*/
startScheduler(): void {
if (this.maintenanceInterval) {
return;
}
this.logManager.info('MAINTENANCE', 'Starting maintenance scheduler');
// 每分钟检查一次是否有任务需要执行
this.maintenanceInterval = setInterval(() => {
this.checkAndRunTasks();
}, 60000); // 1 minute
// 立即执行一次检查
this.checkAndRunTasks();
}
/**
* 停止维护调度器
*/
stopScheduler(): void {
if (this.maintenanceInterval) {
clearInterval(this.maintenanceInterval);
this.maintenanceInterval = null;
this.logManager.info('MAINTENANCE', 'Maintenance scheduler stopped');
}
}
/**
* 检查并运行到期的任务
*/
private async checkAndRunTasks(): Promise<void> {
const now = Date.now();
const tasksToRun: MaintenanceTask[] = [];
for (const task of this.tasks.values()) {
if (!task.enabled) continue;
const nextRun = this.calculateNextRun(task);
if (nextRun <= now) {
tasksToRun.push(task);
}
}
if (tasksToRun.length > 0) {
// 按优先级排序
tasksToRun.sort((a, b) => {
const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
});
await this.runMaintenanceTasks(tasksToRun);
}
}
/**
* 运行维护任务
*/
async runMaintenanceTasks(tasks: MaintenanceTask[]): Promise<MaintenanceReport> {
const startTime = Date.now();
this.logManager.info('MAINTENANCE', `Starting maintenance run with ${tasks.length} tasks`);
const report: MaintenanceReport = {
timestamp: startTime,
tasks: [],
systemHealth: await this.healthChecker.getHealthStatus(0, 0), // Will be updated
recommendations: []
};
for (const task of tasks) {
const taskStartTime = Date.now();
try {
this.logManager.info('MAINTENANCE', `Running task: ${task.name}`, { taskId: task.id });
const result = await this.executeTask(task);
const duration = Date.now() - taskStartTime;
task.lastRun = taskStartTime;
report.tasks.push({
task,
status: result.success ? 'success' : 'failed',
duration,
error: result.error,
details: result.details
});
if (result.success) {
this.logManager.info('MAINTENANCE', `Task completed: ${task.name}`, {
taskId: task.id,
duration
});
} else {
this.logManager.error('MAINTENANCE', `Task failed: ${task.name}`, {
taskId: task.id,
error: result.error
});
}
} catch (error) {
const duration = Date.now() - taskStartTime;
task.lastRun = taskStartTime;
report.tasks.push({
task,
status: 'failed',
duration,
error: error instanceof Error ? error.message : String(error)
});
this.logManager.error('MAINTENANCE', `Task error: ${task.name}`, {
taskId: task.id,
error: error instanceof Error ? error.message : String(error)
});
}
}
// 更新系统健康状态
report.systemHealth = await this.healthChecker.getHealthStatus(0, 0);
// 生成建议
report.recommendations = this.generateRecommendations(report);
// 保存任务状态
this.saveTasks();
const totalDuration = Date.now() - startTime;
this.logManager.info('MAINTENANCE', `Maintenance run completed`, {
totalTasks: tasks.length,
successfulTasks: report.tasks.filter(t => t.status === 'success').length,
failedTasks: report.tasks.filter(t => t.status === 'failed').length,
totalDuration
});
return report;
}
/**
* 执行单个维护任务
*/
private async executeTask(task: MaintenanceTask): Promise<{ success: boolean; error?: string; details?: any }> {
switch (task.id) {
case 'health_check':
return await this.executeHealthCheck();
case 'backup_data':
return await this.executeBackup();
case 'cleanup_logs':
return await this.executeLogCleanup();
case 'cleanup_temp_files':
return await this.executeTempCleanup();
case 'validate_data':
return await this.executeDataValidation();
case 'performance_analysis':
return await this.executePerformanceAnalysis();
case 'security_scan':
return await this.executeSecurityScan();
default:
return { success: false, error: `Unknown task: ${task.id}` };
}
}
/**
* 执行健康检查
*/
private async executeHealthCheck(): Promise<{ success: boolean; error?: string; details?: any }> {
try {
const health = await this.healthChecker.getHealthStatus(0, 0);
const serviceHealth = await this.healthChecker.checkServiceHealth();
if (health.status === 'critical' || !serviceHealth) {
this.logManager.critical('HEALTH', 'System health critical', { health, serviceHealth });
} else if (health.status === 'warning') {
this.logManager.warn('HEALTH', 'System health warning', { health });
}
return {
success: true,
details: {
health,
serviceHealth,
issues: health.issues
}
};
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
/**
* 执行备份
*/
private async executeBackup(): Promise<{ success: boolean; error?: string; details?: any }> {
try {
const backup = await this.backupManager.createBackup({
description: 'Scheduled maintenance backup',
maxBackups: 20
});
return {
success: true,
details: {
backupId: backup.id,
size: backup.size,
files: backup.files.length
}
};
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
/**
* 执行日志清理
*/
private async executeLogCleanup(): Promise<{ success: boolean; error?: string; details?: any }> {
try {
const logFiles = this.logManager.getLogFiles();
const oldFiles = logFiles.filter(file =>
Date.now() - file.modified > 30 * 24 * 60 * 60 * 1000 // 30 days
);
let deletedFiles = 0;
let deletedSize = 0;
for (const file of oldFiles) {
try {
const filePath = path.join(this.dataDir, 'logs', file.name);
fs.unlinkSync(filePath);
deletedFiles++;
deletedSize += file.size;
} catch (error) {
this.logManager.warn('MAINTENANCE', `Failed to delete log file: ${file.name}`, { error });
}
}
return {
success: true,
details: {
totalFiles: logFiles.length,
deletedFiles,
deletedSize
}
};
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
/**
* 执行临时文件清理
*/
private async executeTempCleanup(): Promise<{ success: boolean; error?: string; details?: any }> {
try {
let deletedFiles = 0;
let deletedSize = 0;
// 清理临时文件
const tempPatterns = ['.tmp', '.temp', '.cache', '.lock'];
const cleanDirectory = async (dir: string) => {
try {
const items = fs.readdirSync(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const stat = fs.statSync(itemPath);
if (stat.isDirectory()) {
await cleanDirectory(itemPath);
} else {
const shouldDelete = tempPatterns.some(pattern => item.endsWith(pattern)) ||
item.startsWith('.health_check_temp');
if (shouldDelete) {
fs.unlinkSync(itemPath);
deletedFiles++;
deletedSize += stat.size;
}
}
}
} catch (error) {
// 忽略访问错误
}
};
await cleanDirectory(this.dataDir);
return {
success: true,
details: {
deletedFiles,
deletedSize
}
};
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
/**
* 执行数据验证
*/
private async executeDataValidation(): Promise<{ success: boolean; error?: string; details?: any }> {
try {
const charactersFile = path.join(this.dataDir, 'characters.json');
let validationResults = {
charactersValid: false,
charactersCount: 0,
issues: [] as string[]
};
if (fs.existsSync(charactersFile)) {
try {
const data = JSON.parse(fs.readFileSync(charactersFile, 'utf-8'));
if (Array.isArray(data)) {
validationResults.charactersValid = true;
validationResults.charactersCount = data.length;
// 验证每个角色数据
for (let i = 0; i < data.length; i++) {
const char = data[i];
if (!char.id || !char.name || !char.ownerId) {
validationResults.issues.push(`Character ${i} missing required fields`);
}
}
} else {
validationResults.issues.push('Characters data is not an array');
}
} catch (error) {
validationResults.issues.push('Characters file is corrupted');
}
} else {
validationResults.issues.push('Characters file does not exist');
}
return {
success: validationResults.issues.length === 0,
details: validationResults
};
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
/**
* 执行性能分析
*/
private async executePerformanceAnalysis(): Promise<{ success: boolean; error?: string; details?: any }> {
try {
const analytics = await this.logManager.analyzeLogs({
startTime: Date.now() - 24 * 60 * 60 * 1000 // Last 24 hours
});
const systemInfo = this.healthChecker.getSystemInfo();
return {
success: true,
details: {
logAnalytics: analytics,
systemInfo
}
};
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
/**
* 执行安全扫描
*/
private async executeSecurityScan(): Promise<{ success: boolean; error?: string; details?: any }> {
try {
// 检查可疑活动
const analytics = await this.logManager.analyzeLogs({
startTime: Date.now() - 24 * 60 * 60 * 1000,
level: LogLevel.ERROR
});
const securityIssues: string[] = [];
// 检查错误率
if (analytics.totalEntries > 100 &&
analytics.levelCounts[LogLevel.ERROR] / analytics.totalEntries > 0.1) {
securityIssues.push('High error rate detected');
}
// 检查频繁的失败尝试
for (const error of analytics.topErrors) {
if (error.message.includes('failed') && error.count > 50) {
securityIssues.push(`Frequent failures: ${error.message}`);
}
}
return {
success: securityIssues.length === 0,
details: {
securityIssues,
errorAnalytics: analytics
}
};
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
}
/**
* 生成建议
*/
private generateRecommendations(report: MaintenanceReport): string[] {
const recommendations: string[] = [];
// 基于系统健康状态的建议
if (report.systemHealth.status === 'critical') {
recommendations.push('System is in critical state - immediate attention required');
} else if (report.systemHealth.status === 'warning') {
recommendations.push('System has warnings - monitor closely');
}
// 基于内存使用的建议
if (report.systemHealth.metrics.memory.percentage > 80) {
recommendations.push('High memory usage - consider restarting or optimizing');
}
// 基于存储空间的建议
if (report.systemHealth.metrics.storage.percentage > 90) {
recommendations.push('Low disk space - clean up old files or expand storage');
}
// 基于任务失败的建议
const failedTasks = report.tasks.filter(t => t.status === 'failed');
if (failedTasks.length > 0) {
recommendations.push(`${failedTasks.length} maintenance tasks failed - check logs for details`);
}
return recommendations;
}
/**
* 计算下次运行时间
*/
private calculateNextRun(task: MaintenanceTask): number {
// 简化的调度计算 - 在实际项目中应该使用完整的cron解析器
const now = Date.now();
const lastRun = task.lastRun || 0;
// 解析简单的调度格式
if (task.schedule.startsWith('*/')) {
const minutes = parseInt(task.schedule.split(' ')[0].substring(2));
return lastRun + (minutes * 60 * 1000);
} else if (task.schedule.startsWith('0 */')) {
const hours = parseInt(task.schedule.split(' ')[1].substring(2));
return lastRun + (hours * 60 * 60 * 1000);
} else if (task.schedule.startsWith('0 ')) {
// Daily tasks
const hour = parseInt(task.schedule.split(' ')[1]);
const nextRun = new Date();
nextRun.setHours(hour, 0, 0, 0);
if (nextRun.getTime() <= now) {
nextRun.setDate(nextRun.getDate() + 1);
}
return nextRun.getTime();
}
// 默认:如果没有运行过,立即运行
return lastRun === 0 ? now : now + 60000; // 1 minute
}
/**
* 加载任务配置
*/
private loadTasks(): void {
const tasksFile = path.join(this.dataDir, 'maintenance_tasks.json');
if (fs.existsSync(tasksFile)) {
try {
const data = JSON.parse(fs.readFileSync(tasksFile, 'utf-8'));
for (const taskData of data) {
if (this.tasks.has(taskData.id)) {
// 更新现有任务
const existingTask = this.tasks.get(taskData.id)!;
Object.assign(existingTask, taskData);
} else {
// 添加新任务
this.tasks.set(taskData.id, taskData);
}
}
this.logManager.info('MAINTENANCE', `Loaded ${data.length} maintenance tasks`);
} catch (error) {
this.logManager.error('MAINTENANCE', 'Failed to load maintenance tasks', { error });
}
}
}
/**
* 保存任务配置
*/
private saveTasks(): void {
const tasksFile = path.join(this.dataDir, 'maintenance_tasks.json');
try {
const tasksArray = Array.from(this.tasks.values());
fs.writeFileSync(tasksFile, JSON.stringify(tasksArray, null, 2));
} catch (error) {
this.logManager.error('MAINTENANCE', 'Failed to save maintenance tasks', { error });
}
}
/**
* 进入维护模式
*/
async enterMaintenanceMode(reason: string): Promise<void> {
this.isMaintenanceMode = true;
this.logManager.warn('MAINTENANCE', `Entering maintenance mode: ${reason}`);
// 这里可以添加通知所有客户端的逻辑
// 例如:广播维护模式消息,拒绝新连接等
}
/**
* 退出维护模式
*/
async exitMaintenanceMode(): Promise<void> {
this.isMaintenanceMode = false;
this.logManager.info('MAINTENANCE', 'Exiting maintenance mode');
// 这里可以添加恢复正常服务的逻辑
}
/**
* 检查是否在维护模式
*/
isInMaintenanceMode(): boolean {
return this.isMaintenanceMode;
}
/**
* 获取所有任务
*/
getTasks(): MaintenanceTask[] {
return Array.from(this.tasks.values());
}
/**
* 更新任务
*/
updateTask(taskId: string, updates: Partial<MaintenanceTask>): boolean {
const task = this.tasks.get(taskId);
if (!task) {
return false;
}
Object.assign(task, updates);
this.saveTasks();
this.logManager.info('MAINTENANCE', `Task updated: ${taskId}`, { updates });
return true;
}
/**
* 手动运行任务
*/
async runTask(taskId: string): Promise<{ success: boolean; error?: string; details?: any }> {
const task = this.tasks.get(taskId);
if (!task) {
return { success: false, error: `Task not found: ${taskId}` };
}
this.logManager.info('MAINTENANCE', `Manually running task: ${task.name}`, { taskId });
try {
const result = await this.executeTask(task);
task.lastRun = Date.now();
this.saveTasks();
return result;
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
}

View File

@@ -0,0 +1,221 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
export interface HealthStatus {
status: 'healthy' | 'warning' | 'critical';
timestamp: number;
metrics: {
memory: {
used: number;
total: number;
percentage: number;
};
cpu: {
usage: number;
};
connections: {
active: number;
total: number;
};
storage: {
used: number;
available: number;
percentage: number;
};
uptime: number;
};
issues: string[];
}
export class HealthChecker {
private startTime: number;
private lastCpuUsage: NodeJS.CpuUsage;
private dataDir: string;
constructor(dataDir: string) {
this.startTime = Date.now();
this.lastCpuUsage = process.cpuUsage();
this.dataDir = dataDir;
}
/**
* 获取系统健康状态
*/
async getHealthStatus(activeConnections: number, totalConnections: number): Promise<HealthStatus> {
const issues: string[] = [];
// 内存使用情况
const memoryUsage = process.memoryUsage();
const totalMemory = os.totalmem();
const memoryPercentage = (memoryUsage.rss / totalMemory) * 100;
if (memoryPercentage > 80) {
issues.push(`High memory usage: ${memoryPercentage.toFixed(1)}%`);
}
// CPU使用情况
const currentCpuUsage = process.cpuUsage(this.lastCpuUsage);
const cpuPercentage = (currentCpuUsage.user + currentCpuUsage.system) / 1000000 * 100;
this.lastCpuUsage = process.cpuUsage();
if (cpuPercentage > 80) {
issues.push(`High CPU usage: ${cpuPercentage.toFixed(1)}%`);
}
// 存储空间检查
const storageInfo = await this.getStorageInfo();
if (storageInfo.percentage > 90) {
issues.push(`Low disk space: ${storageInfo.percentage.toFixed(1)}% used`);
}
// 连接数检查
if (activeConnections > 1000) {
issues.push(`High connection count: ${activeConnections} active connections`);
}
// 运行时间
const uptime = Date.now() - this.startTime;
// 确定整体状态
let status: 'healthy' | 'warning' | 'critical' = 'healthy';
if (issues.length > 0) {
status = memoryPercentage > 90 || cpuPercentage > 90 || storageInfo.percentage > 95 ? 'critical' : 'warning';
}
return {
status,
timestamp: Date.now(),
metrics: {
memory: {
used: memoryUsage.rss,
total: totalMemory,
percentage: memoryPercentage
},
cpu: {
usage: cpuPercentage
},
connections: {
active: activeConnections,
total: totalConnections
},
storage: storageInfo,
uptime
},
issues
};
}
/**
* 获取存储空间信息
*/
private async getStorageInfo(): Promise<{ used: number; available: number; percentage: number }> {
try {
const stats = await fs.promises.stat(this.dataDir);
const diskUsage = await this.getDiskUsage(this.dataDir);
return {
used: diskUsage.used,
available: diskUsage.available,
percentage: (diskUsage.used / (diskUsage.used + diskUsage.available)) * 100
};
} catch (error) {
console.error('Error getting storage info:', error);
return { used: 0, available: 0, percentage: 0 };
}
}
/**
* 获取磁盘使用情况(跨平台)
*/
private async getDiskUsage(dirPath: string): Promise<{ used: number; available: number }> {
return new Promise((resolve, reject) => {
const { exec } = require('child_process');
// Windows
if (process.platform === 'win32') {
const drive = path.parse(dirPath).root;
exec(`wmic logicaldisk where caption="${drive}" get size,freespace /value`, (error: any, stdout: string) => {
if (error) {
reject(error);
return;
}
const lines = stdout.split('\n').filter(line => line.includes('='));
let freeSpace = 0;
let totalSize = 0;
lines.forEach(line => {
if (line.includes('FreeSpace=')) {
freeSpace = parseInt(line.split('=')[1]);
} else if (line.includes('Size=')) {
totalSize = parseInt(line.split('=')[1]);
}
});
resolve({
used: totalSize - freeSpace,
available: freeSpace
});
});
} else {
// Unix/Linux/macOS
exec(`df -k "${dirPath}"`, (error: any, stdout: string) => {
if (error) {
reject(error);
return;
}
const lines = stdout.split('\n');
if (lines.length < 2) {
reject(new Error('Invalid df output'));
return;
}
const parts = lines[1].split(/\s+/);
const used = parseInt(parts[2]) * 1024; // Convert from KB to bytes
const available = parseInt(parts[3]) * 1024;
resolve({ used, available });
});
}
});
}
/**
* 检查服务是否响应
*/
async checkServiceHealth(): Promise<boolean> {
try {
// 检查数据目录是否可访问
await fs.promises.access(this.dataDir, fs.constants.R_OK | fs.constants.W_OK);
// 检查是否可以创建临时文件
const tempFile = path.join(this.dataDir, '.health_check_temp');
await fs.promises.writeFile(tempFile, 'health_check');
await fs.promises.unlink(tempFile);
return true;
} catch (error) {
console.error('Service health check failed:', error);
return false;
}
}
/**
* 获取系统信息
*/
getSystemInfo() {
return {
platform: os.platform(),
arch: os.arch(),
nodeVersion: process.version,
hostname: os.hostname(),
cpus: os.cpus().length,
totalMemory: os.totalmem(),
freeMemory: os.freemem(),
loadAverage: os.loadavg(),
uptime: os.uptime()
};
}
}

1071
server/src/server.ts Normal file

File diff suppressed because it is too large Load Diff

17
server/tsconfig.json Normal file
View File

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