创建新工程
This commit is contained in:
38
server/.gitignore
vendored
Normal file
38
server/.gitignore
vendored
Normal 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
0
server/.gitkeep
Normal file
51
server/Dockerfile.prod
Normal file
51
server/Dockerfile.prod
Normal 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
418
server/README.md
Normal 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
741
server/admin/index.html
Normal 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
0
server/data/.gitkeep
Normal file
10
server/data/backups/backup_1764929072516/backup_info.json
Normal file
10
server/data/backups/backup_1764929072516/backup_info.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "backup_1764929072516",
|
||||
"timestamp": 1764929072520,
|
||||
"size": 4442,
|
||||
"compressed": true,
|
||||
"description": "Scheduled maintenance backup",
|
||||
"files": [
|
||||
"characters.json.gz"
|
||||
]
|
||||
}
|
||||
BIN
server/data/backups/backup_1764929072516/characters.json.gz
Normal file
BIN
server/data/backups/backup_1764929072516/characters.json.gz
Normal file
Binary file not shown.
12
server/data/backups/backup_1764929388237/backup_info.json
Normal file
12
server/data/backups/backup_1764929388237/backup_info.json
Normal 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"
|
||||
]
|
||||
}
|
||||
BIN
server/data/backups/backup_1764929388237/characters.json.gz
Normal file
BIN
server/data/backups/backup_1764929388237/characters.json.gz
Normal file
Binary file not shown.
Binary file not shown.
12
server/data/backups/backup_1764929391984/backup_info.json
Normal file
12
server/data/backups/backup_1764929391984/backup_info.json
Normal 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"
|
||||
]
|
||||
}
|
||||
BIN
server/data/backups/backup_1764929391984/characters.json.gz
Normal file
BIN
server/data/backups/backup_1764929391984/characters.json.gz
Normal file
Binary file not shown.
Binary file not shown.
28
server/package.json
Normal file
28
server/package.json
Normal 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
310
server/src/api/AdminAPI.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
384
server/src/backup/BackupManager.ts
Normal file
384
server/src/backup/BackupManager.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
484
server/src/logging/LogManager.ts
Normal file
484
server/src/logging/LogManager.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
720
server/src/maintenance/MaintenanceManager.ts
Normal file
720
server/src/maintenance/MaintenanceManager.ts
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
221
server/src/monitoring/HealthChecker.ts
Normal file
221
server/src/monitoring/HealthChecker.ts
Normal 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
1071
server/src/server.ts
Normal file
File diff suppressed because it is too large
Load Diff
17
server/tsconfig.json
Normal file
17
server/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user