diff --git a/README.md b/README.md index 3dad7d3..c6bfaf0 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ REDIS_PASSWORD=your-redis-password REDIS_DB=15 # 服务器配置 -PORT=3001 +PORT=19070 NODE_ENV=production ``` diff --git a/deploy/nginx/conf.d/bls_project_console.conf b/deploy/nginx/conf.d/bls_project_console.conf index 2c0b92a..51254d6 100644 --- a/deploy/nginx/conf.d/bls_project_console.conf +++ b/deploy/nginx/conf.d/bls_project_console.conf @@ -9,14 +9,14 @@ # /vol1/1000/Docker/nginx/sites/bls_project_console/ # and will be served from: # /var/www/bls_project_console/ -# - Backend runs on the HOST at 127.0.0.1:3001. +# - Backend runs on the HOST at 127.0.0.1:19070. # Nginx container reaches host via host.docker.internal. # On Linux you typically need in nginx docker-compose: # extra_hosts: # - "host.docker.internal:host-gateway" server { - listen 80; + listen 19199; server_name blv-rd.tech; root /var/www/bls_project_console; @@ -24,7 +24,7 @@ server { # API reverse proxy location /api/ { - proxy_pass http://host.docker.internal:3001; + proxy_pass http://host.docker.internal:19070; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/docs/ai-deployment-request-guide.md b/docs/ai-deployment-request-guide.md index 3488969..c7303f8 100644 --- a/docs/ai-deployment-request-guide.md +++ b/docs/ai-deployment-request-guide.md @@ -100,8 +100,8 @@ - 服务器类型:NAS 【访问信息】 -- 前端访问地址:blv-rd.tech:20001 -- 后端API地址:http://127.0.0.1:3001 +- 前端访问地址:blv-rd.tech:19199 +- 后端API地址:http://127.0.0.1:19070 【文件路径】 - 项目文件目录:/vol1/1000/Docker/nginx/project/bls/bls_project_console diff --git a/docs/bls-project-console.service b/docs/bls-project-console.service index 58a5619..9e2c9b0 100644 --- a/docs/bls-project-console.service +++ b/docs/bls-project-console.service @@ -12,7 +12,7 @@ RestartSec=10 StandardOutput=append:/vol1/1000/Docker/nginx/project/bls/bls_project_console/backend/logs/systemd-out.log StandardError=append:/vol1/1000/Docker/nginx/project/bls/bls_project_console/backend/logs/systemd-err.log Environment=NODE_ENV=production -Environment=PORT=19910 +Environment=PORT=19070 [Install] WantedBy=multi-user.target diff --git a/docs/configuration-check-report.md b/docs/configuration-check-report.md index 9cec046..0e49a0b 100644 --- a/docs/configuration-check-report.md +++ b/docs/configuration-check-report.md @@ -6,8 +6,8 @@ **检查日期**: 2026-01-16 **部署架构**: Nginx(Docker容器)+ Express后端(systemd管理) -**前端访问地址**: blv-rd.tech:20100 -**后端API地址**: http://127.0.0.1:19910 +**前端访问地址**: blv-rd.tech:19199 +**后端API地址**: http://127.0.0.1:19070 ## 二、配置文件清单 @@ -21,7 +21,7 @@ ```nginx server { - listen 20001; + listen 19199; server_name blv-rd.tech; root /var/www/bls_project_console; @@ -30,7 +30,7 @@ server { client_max_body_size 100M; location /api/ { - proxy_pass http://host.docker.internal:3001; + proxy_pass http://host.docker.internal:19070; proxy_http_version 1.1; proxy_set_header Host $host; @@ -54,10 +54,10 @@ server { **检查项**: -- ✅ 监听端口: 20100(正确) +- ✅ 监听端口: 19199(正确) - ✅ 服务器名称: blv-rd.tech(正确) - ✅ 静态文件根目录: /var/www/bls_project_console(正确) -- ✅ API代理地址: http://host.docker.internal:19910(正确,Nginx在Docker容器中) +- ✅ API代理地址: http://host.docker.internal:19070(正确,Nginx在Docker容器中) - ✅ 文件上传大小限制: 100M(正确) - ✅ Vue Router history模式支持: try_files $uri $uri/ /index.html(正确) - ✅ 超时设置: 连接60s、发送60s、读取300s(正确) @@ -93,7 +93,7 @@ RestartSec=10 StandardOutput=append:/vol1/1000/Docker/nginx/project/bls/bls_project_console/backend/logs/systemd-out.log StandardError=append:/vol1/1000/Docker/nginx/project/bls/bls_project_console/backend/logs/systemd-err.log Environment=NODE_ENV=production -Environment=PORT=3001 +Environment=PORT=19070 [Install] WantedBy=multi-user.target @@ -109,7 +109,7 @@ WantedBy=multi-user.target - ✅ 重启策略: on-failure(正确) - ✅ 重启延迟: 10秒(合理) - ✅ 日志输出: 标准输出和错误日志分离(正确) -- ✅ 环境变量: NODE_ENV=production, PORT=19910(正确) +- ✅ 环境变量: NODE_ENV=production, PORT=19070(正确) - ✅ 开机自启: WantedBy=multi-user.target(正确) **说明**: @@ -129,7 +129,7 @@ WantedBy=multi-user.target **关键配置**: ```javascript -const PORT = 19910; +const PORT = parsePort(process.env.PORT, 19070); app.use(cors()); app.use(express.json()); @@ -145,7 +145,7 @@ app.get('/api/health', (req, res) => { **检查项**: -- ✅ 端口配置: 19910(与systemd配置一致) +- ✅ 端口配置: 19070(与systemd配置一致) - ✅ CORS中间件: 已启用(正确) - ✅ JSON解析: 已启用(正确) - ✅ API路由: /api/logs, /api/commands, /api/projects(正确) @@ -155,7 +155,7 @@ app.get('/api/health', (req, res) => { **说明**: -- 端口19910与systemd服务配置中的PORT环境变量一致 +- 端口19070与systemd服务配置中的PORT环境变量一致 - 提供健康检查端点便于监控 - 支持优雅关闭,确保Redis连接正确关闭 @@ -181,7 +181,7 @@ export default defineConfig({ port: 3000, proxy: { '/api': { - target: 'http://localhost:3001', + target: 'http://localhost:19070', changeOrigin: true, }, }, @@ -200,7 +200,7 @@ export default defineConfig({ - ✅ 源码根目录: src/frontend(正确) - ✅ 输出目录: ../../dist(正确) - ✅ 开发服务器端口: 3000(正确) -- ✅ API代理: /api -> http://localhost:3001(正确,仅用于开发环境) +- ✅ API代理: /api -> http://localhost:19070(正确,仅用于开发环境) - ✅ 路径别名: @ -> src/frontend(正确) **说明**: @@ -270,10 +270,10 @@ app.mount('#app'); | 配置项 | 端口 | 状态 | | -------------------------- | ----- | --------------- | -| 后端服务器 (server.js) | 19910 | ✅ | -| Systemd服务 (PORT环境变量) | 19910 | ✅ | -| Nginx代理目标 | 19910 | ✅ | -| Nginx监听端口 | 20100 | ✅ | +| 后端服务器 (server.js) | 19070 | ✅ | +| Systemd服务 (PORT环境变量) | 19070 | ✅ | +| Nginx代理目标 | 19070 | ✅ | +| Nginx监听端口 | 19199 | ✅ | | Vite开发服务器 | 3000 | ✅ (仅开发环境) | ### 路径配置一致性 @@ -303,7 +303,7 @@ app.mount('#app'); 2. ✅ 上传dist文件夹内容到: /vol1/1000/Docker/nginx/project/bls/bls_project_console 3. ✅ 上传Nginx配置到: /vol1/1000/Docker/nginx/conf.d/bls_project_console.conf 4. ✅ 重启Nginx容器: `docker restart nginx` -5. ✅ 访问地址: http://blv-rd.tech:20100 +5. ✅ 访问地址: http://blv-rd.tech:19199 ### 后端部署流程 @@ -321,7 +321,7 @@ app.mount('#app'); ### 1. Nginx容器网络配置 -**问题**: Nginx容器需要能够访问宿主机的3001端口 +**问题**: Nginx容器需要能够访问宿主机的19070端口 **建议**: @@ -386,8 +386,8 @@ npm --version redis-cli ping # 4. 检查端口占用 -netstat -tlnp | grep 3001 -netstat -tlnp | grep 20001 +netstat -tlnp | grep 19070 +netstat -tlnp | grep 19199 ``` ### 部署后验证 @@ -397,16 +397,16 @@ netstat -tlnp | grep 20001 systemctl status bls-project-console.service # 2. 检查后端服务 -curl http://localhost:3001/api/health +curl http://localhost:19070/api/health # 3. 检查Nginx容器 docker ps | grep nginx # 4. 检查前端访问 -curl http://blv-rd.tech:20001 +curl http://blv-rd.tech:19199 # 5. 检查API代理 -curl http://blv-rd.tech:20001/api/health +curl http://blv-rd.tech:19199/api/health ``` --- diff --git a/docs/deployment-guide-systemd.md b/docs/deployment-guide-systemd.md index dff1c69..0cea584 100644 --- a/docs/deployment-guide-systemd.md +++ b/docs/deployment-guide-systemd.md @@ -9,8 +9,8 @@ ## 二、环境信息 -- **前端访问地址**: blv-rd.tech:20100 -- **后端API地址**: http://127.0.0.1:19910 +- **前端访问地址**: blv-rd.tech:19199 +- **后端API地址**: http://127.0.0.1:19070 - **NAS项目文件目录**: `/vol1/1000/Docker/nginx/project/bls/bls_project_console` - **NAS配置文件目录**: `/vol1/1000/Docker/nginx/conf.d` - **Systemd服务目录**: `/etc/systemd/system/` @@ -45,11 +45,11 @@ docker port nginx ### 3. 检查端口占用 ```bash -# 检查后端端口19910是否被占用 -netstat -tlnp | grep 19910 +# 检查后端端口19070是否被占用 +netstat -tlnp | grep 19070 -# 检查前端端口20100是否被占用 -netstat -tlnp | grep 20100 +# 检查前端端口19199是否被占用 +netstat -tlnp | grep 19199 ``` ### 4. 检查Redis服务 @@ -166,7 +166,7 @@ docker exec nginx nginx -s reload 在浏览器中访问: ``` -http://blv-rd.tech:20100 +http://blv-rd.tech:19199 ``` 应该能看到项目的前端页面。 @@ -265,7 +265,7 @@ nano .env ```env NODE_ENV=production -PORT=19910 +PORT=19070 REDIS_HOST=localhost REDIS_PORT=6379 ``` @@ -283,7 +283,7 @@ cd /vol1/1000/Docker/nginx/project/bls/bls_project_console/backend node server.js # 如果看到类似以下输出,说明启动成功: -# BLS Project Console backend server is running on port 19910 +# BLS Project Console backend server is running on port 19070 ``` 如果启动失败,查看错误信息并修复: @@ -293,7 +293,7 @@ node server.js redis-cli ping # 检查端口占用 -netstat -tlnp | grep 19910 +netstat -tlnp | grep 19070 # 查看详细错误日志 node server.js 2>&1 | tee startup.log @@ -332,7 +332,7 @@ RestartSec=10 StandardOutput=append:/vol1/1000/Docker/nginx/project/bls/bls_project_console/backend/logs/systemd-out.log StandardError=append:/vol1/1000/Docker/nginx/project/bls/bls_project_console/backend/logs/systemd-err.log Environment=NODE_ENV=production -Environment=PORT=19910 +Environment=PORT=19070 [Install] WantedBy=multi-user.target @@ -401,10 +401,10 @@ tail -f /vol1/1000/Docker/nginx/project/bls/bls_project_console/backend/logs/sys ```bash # 检查端口监听 -netstat -tlnp | grep 19910 +netstat -tlnp | grep 19070 # 测试API接口 -curl http://localhost:19910/api/projects +curl http://localhost:19070/api/projects # 查看服务进程 ps aux | grep "node server.js" @@ -413,7 +413,7 @@ ps aux | grep "node server.js" 在浏览器中访问: ``` -http://blv-rd.tech:20100/api/projects +http://blv-rd.tech:19199/api/projects ``` 应该能返回JSON数据。 @@ -573,7 +573,7 @@ docker logs nginx --tail 100 **可能原因**: - 后端服务未启动 -- 后端端口不是19910 +- 后端端口不是19070 - Redis连接失败 - 防火墙阻止了连接 @@ -584,7 +584,7 @@ docker logs nginx --tail 100 sudo systemctl status bls-project-console.service # 2. 检查后端端口 -netstat -tlnp | grep 19910 +netstat -tlnp | grep 19070 # 3. 查看服务日志 sudo journalctl -u bls-project-console.service -n 50 @@ -593,7 +593,7 @@ sudo journalctl -u bls-project-console.service -n 50 redis-cli ping # 5. 测试后端API -curl http://localhost:19910/api/projects +curl http://localhost:19070/api/projects # 6. 重启服务 sudo systemctl restart bls-project-console.service @@ -624,7 +624,7 @@ cat /etc/systemd/system/bls-project-console.service ls -la /vol1/1000/Docker/nginx/project/bls/bls_project_console/backend # 5. 检查端口占用 -netstat -tlnp | grep 3001 +netstat -tlnp | grep 19070 # 6. 检查Redis服务 sudo systemctl status redis @@ -640,7 +640,7 @@ node server.js **可能原因**: - 配置文件语法错误 -- 端口20100已被占用 +- 端口19199已被占用 - 配置文件路径错误 **解决方法**: @@ -650,7 +650,7 @@ node server.js docker exec nginx nginx -t # 2. 检查端口占用 -netstat -tlnp | grep 20100 +netstat -tlnp | grep 19199 # 3. 查看Nginx错误日志 docker logs nginx --tail 100 @@ -865,8 +865,8 @@ docker restart nginx ```bash # 只允许必要的端口 -sudo ufw allow 20100/tcp -sudo ufw allow 19910/tcp +sudo ufw allow 19199/tcp +sudo ufw allow 19070/tcp sudo ufw enable ``` diff --git a/docs/deployment-guide-windows.md b/docs/deployment-guide-windows.md index 3788a70..d8c4e07 100644 --- a/docs/deployment-guide-windows.md +++ b/docs/deployment-guide-windows.md @@ -105,7 +105,7 @@ module.exports = { max_memory_restart: '1G', env: { NODE_ENV: 'production', - PORT: 19910, + PORT: 19070, }, error_file: './logs/pm2-error.log', out_file: './logs/pm2-out.log', @@ -187,14 +187,14 @@ pm2 status pm2 logs bls-project-console # 测试健康检查接口 -curl http://localhost:19910/api/health +curl http://localhost:19070/api/health ``` #### 8.2 检查前端访问 在浏览器中访问: -- `http://localhost/` 或配置的域名 +- `http://localhost:19199/` 或配置的域名 #### 8.3 检查 API 代理 @@ -252,7 +252,7 @@ nginx -v 1. 检查端口是否被占用: ```bash - netstat -ano | findstr :19910 + netstat -ano | findstr :19070 ``` 2. 检查 Redis 连接: @@ -297,13 +297,13 @@ nginx -v 2. 检查 Nginx 代理配置: ```bash # 确保 proxy_pass 指向正确的后端地址 - curl http://localhost:19910/api/health + curl http://localhost:19070/api/health ``` ## 端口说明 -- **19910**: 后端 API 服务端口 -- **80**: Nginx HTTP 服务端口 +- **19070**: 后端 API 服务端口 +- **19199**: Nginx HTTP 服务端口 ## 注意事项 diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index c39d3e2..8046704 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -2,11 +2,11 @@ ## 一、环境信息 -- **前端访问地址**: blv-rd.tech:20100 +- **前端访问地址**: blv-rd.tech:19199 - **NAS项目文件目录**: `/vol1/1000/Docker/nginx/project/bls/bls_project_console` - **NAS配置文件目录**: `/vol1/1000/Docker/nginx/conf.d` - **项目类型**: Vue3前端 + Express后端 -- **后端端口**: 19910 +- **后端端口**: 19070 ## 二、本地编译步骤 @@ -137,7 +137,7 @@ pm2 delete bls-project-console **注意**: -- 后端服务会在宿主机上运行,端口为19910 +- 后端服务会在宿主机上运行,端口为19070 - 确保Redis服务已启动并可访问 - PM2会自动管理进程崩溃重启 @@ -170,7 +170,7 @@ docker exec nginx nginx -t 在浏览器中访问: ``` -http://blv-rd.tech:20100 +http://blv-rd.tech:19199 ``` 应该能看到项目的前端页面。 @@ -180,7 +180,7 @@ http://blv-rd.tech:20100 在浏览器中访问: ``` -http://blv-rd.tech:20100/api/projects +http://blv-rd.tech:19199/api/projects ``` 应该能返回JSON数据(如果后端正常运行)。 @@ -221,14 +221,14 @@ docker exec nginx tail -f /var/log/nginx-custom/error.log **可能原因**: - 后端服务未启动 -- 后端端口不是19910 +- 后端端口不是19070 - `host.docker.internal` 无法解析 - 防火墙阻止了连接 **解决方法**: 1. 检查PM2服务状态:`pm2 status` -2. 检查后端端口:`netstat -tlnp | grep 19910` +2. 检查后端端口:`netstat -tlnp | grep 19070` 3. 查看PM2日志:`pm2 logs bls-project-console` 4. 在Nginx容器内测试连接:`docker exec nginx ping host.docker.internal` 5. 检查防火墙规则 @@ -239,13 +239,13 @@ docker exec nginx tail -f /var/log/nginx-custom/error.log **可能原因**: - 配置文件语法错误 -- 端口20100已被占用 +- 端口19199已被占用 - 配置文件路径错误 **解决方法**: 1. 检查配置文件语法:`docker exec nginx nginx -t` -2. 检查端口占用:`netstat -tlnp | grep 20100` +2. 检查端口占用:`netstat -tlnp | grep 19199` 3. 查看Nginx错误日志:`docker logs nginx` ## 六、后续更新流程 @@ -342,7 +342,7 @@ Web_BLS_ProjectConsole/ - 应用名称:`bls-project-console` - 工作目录:`/vol1/1000/Docker/nginx/project/bls/bls_project_console/backend` - 启动脚本:`./server.js` -- 环境变量:`NODE_ENV=production`, `PORT=19910` +- 环境变量:`NODE_ENV=production`, `PORT=19070` - 内存限制:1GB(超过自动重启) - 日志文件:`./logs/pm2-error.log` 和 `./logs/pm2-out.log` @@ -374,7 +374,7 @@ pm2 show bls-project-console ## 九、注意事项 -1. **端口映射**: 确保Nginx容器的20100端口已映射到宿主机的20100端口 +1. **端口映射**: 确保Nginx容器的19199端口已映射到宿主机的19199端口 2. **host.docker.internal**: 在Linux上,需要在Docker Compose中添加 `extra_hosts` 配置 3. **文件权限**: 确保上传的文件有正确的读写权限 4. **Redis连接**: 确保后端能连接到Redis服务 diff --git a/docs/ecosystem.config.windows.js b/docs/ecosystem.config.windows.js index 54de1b2..230498d 100644 --- a/docs/ecosystem.config.windows.js +++ b/docs/ecosystem.config.windows.js @@ -10,7 +10,7 @@ module.exports = { max_memory_restart: '1G', env: { NODE_ENV: 'production', - PORT: 19910, + PORT: 19070, }, error_file: './logs/pm2-error.log', out_file: './logs/pm2-out.log', diff --git a/docs/nginx-deployment.conf b/docs/nginx-deployment.conf index 04537a1..2babd93 100644 --- a/docs/nginx-deployment.conf +++ b/docs/nginx-deployment.conf @@ -1,5 +1,5 @@ server { - listen 20100; + listen 19199; server_name blv-rd.tech; root /var/www/bls_project_console; @@ -8,7 +8,7 @@ server { client_max_body_size 100M; location /api/ { - proxy_pass http://host.docker.internal:19910; + proxy_pass http://host.docker.internal:19070; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/docs/nginx.conf.windows b/docs/nginx.conf.windows index c4afac2..7ad3ee8 100644 --- a/docs/nginx.conf.windows +++ b/docs/nginx.conf.windows @@ -1,12 +1,12 @@ server { - listen 80; + listen 19199; server_name localhost; root C:/nginx/sites/bls_project_console; index index.html; location /api/ { - proxy_pass http://127.0.0.1:19910; + proxy_pass http://127.0.0.1:19070; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/docs/openapi.yaml b/docs/openapi.yaml index d9ec6cf..39a8418 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -5,7 +5,7 @@ info: description: | BLS Project Console 后端 API(与当前实现保持一致)。 servers: - - url: http://localhost:3001 + - url: http://localhost:19070 paths: /api/health: get: diff --git a/openspec/changes/update-log-prune-schedule/proposal.md b/openspec/changes/update-log-prune-schedule/proposal.md new file mode 100644 index 0000000..47a0bda --- /dev/null +++ b/openspec/changes/update-log-prune-schedule/proposal.md @@ -0,0 +1,16 @@ +# Change: Update Log Prune Schedule + +## Why + +项目日志持续增长会造成Redis列表膨胀,影响读写性能,需要在服务端统一执行定时清理。 + +## What Changes + +- 每小时整点检查 `项目心跳` 内的全部项目日志列表 +- 删除 `${projectName}_项目控制台` 中超过24小时的日志记录 +- 若日志数量超过1000条,按时间倒序保留最新1000条 + +## Impact + +- Affected specs: specs/logging/spec.md +- Affected code: src/backend/routes/logs.js, src/backend/server.js diff --git a/openspec/changes/update-log-prune-schedule/specs/logging/spec.md b/openspec/changes/update-log-prune-schedule/specs/logging/spec.md new file mode 100644 index 0000000..7de5281 --- /dev/null +++ b/openspec/changes/update-log-prune-schedule/specs/logging/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Scheduled Log Pruning +The system SHALL prune each `${projectName}_项目控制台` log list once per hour. + +#### Scenario: Hourly pruning of console logs +- **WHEN** the server reaches the top of an hour +- **THEN** it SHALL remove log records older than 24 hours +- **AND** it SHALL keep only the latest 1000 log records by timestamp +- **AND** the pruning operation SHALL explicitly sort records by timestamp to ensure correctness regardless of insertion order + +### Requirement: Console Log Display +The console UI SHALL display logs in a compact format. + +#### Scenario: Log Message Truncation +- **WHEN** a log message is displayed in the list +- **THEN** it SHALL be restricted to a single line +- **AND** overflow content SHALL be indicated with an ellipsis +- **AND** clicking the message SHALL toggle expansion to show the full content +- **AND** only one message SHALL be expanded at a time diff --git a/openspec/changes/update-log-prune-schedule/tasks.md b/openspec/changes/update-log-prune-schedule/tasks.md new file mode 100644 index 0000000..223a7e8 --- /dev/null +++ b/openspec/changes/update-log-prune-schedule/tasks.md @@ -0,0 +1,13 @@ +## 1. OpenSpec + +- [x] 1.1 Add logging spec delta for scheduled log pruning + +## 2. Backend + +- [x] 2.1 Implement hourly log pruning with 24h TTL and 1000 limit +- [x] 2.2 Update scheduled pruning tests + +## 3. Verify + +- [x] 3.1 Run `npm run test` +- [x] 3.2 Run `npm run lint` diff --git a/src/backend/routes/logs.js b/src/backend/routes/logs.js index 2bd2d91..ef656a2 100644 --- a/src/backend/routes/logs.js +++ b/src/backend/routes/logs.js @@ -9,45 +9,6 @@ import { getProjectsList } from '../services/migrateHeartbeatData.js'; const LOGS_MAX_LEN = 1000; const LOG_TTL_MS = 24 * 60 * 60 * 1000; -async function listConsoleLogKeys(redis) { - const pattern = '*_项目控制台'; - if (typeof redis.scanIterator === 'function') { - const keys = []; - for await (const key of redis.scanIterator({ MATCH: pattern, COUNT: 500 })) { - keys.push(key); - } - return keys; - } - - if (typeof redis.keys === 'function') { - const list = await redis.keys(pattern); - return Array.isArray(list) ? list : []; - } - - return []; -} - -export async function trimProjectConsoleLogsByLength(redis, options = {}) { - const maxLen = - typeof options.maxLen === 'number' && Number.isFinite(options.maxLen) - ? Math.max(1, Math.trunc(options.maxLen)) - : LOGS_MAX_LEN; - - const keys = await listConsoleLogKeys(redis); - let trimmedKeys = 0; - - for (const key of keys) { - // eslint-disable-next-line no-await-in-loop - const len = await redis.lLen(key); - if (!Number.isFinite(len) || len <= maxLen) continue; - // eslint-disable-next-line no-await-in-loop - await redis.lTrim(key, -maxLen, -1); - trimmedKeys += 1; - } - - return { keysScanned: keys.length, keysTrimmed: trimmedKeys, maxLen }; -} - function parsePositiveInt(value, defaultValue) { const num = Number.parseInt(String(value), 10); if (!Number.isFinite(num) || num <= 0) return defaultValue; @@ -183,11 +144,9 @@ async function pruneAndReadLogsAtomically(redis, key, limit) { await redis.watch(key); const rawItems = await redis.lRange(key, 0, -1); - const ttlKept = rawItems.filter((raw) => shouldKeepRawLog(raw, cutoffMs)); - const kept = - ttlKept.length > 0 - ? ttlKept.slice(-maxLen) - : rawItems.slice(-Math.min(maxLen, rawItems.length)); + const kept = rawItems + .filter((raw) => shouldKeepRawLog(raw, cutoffMs)) + .slice(-maxLen); const needsRewrite = rawItems.length !== kept.length || rawItems.length > maxLen; if (!needsRewrite) return kept.slice(-effectiveLimit); @@ -211,6 +170,83 @@ async function pruneAndReadLogsAtomically(redis, key, limit) { return rawItems; } +export async function pruneConsoleLogsForProjects(injectedRedis, options = {}) { + const redis = injectedRedis || (await getRedisClient()); + if (!redis?.isReady) { + throw new Error('Redis 未就绪'); + } + + const maxLen = parsePositiveInt(options.maxLen, LOGS_MAX_LEN); + const configuredTtlMs = parseDurationMs(process.env.LOG_TTL_MS, LOG_TTL_MS); + const effectiveTtlMs = Math.max(configuredTtlMs, LOG_TTL_MS); + const cutoffMs = Date.now() - effectiveTtlMs; + const projects = await getProjectsList(redis); + if (!Array.isArray(projects) || projects.length === 0) { + return { trimmed: 0, scanned: 0 }; + } + + let trimmed = 0; + let scanned = 0; + + for (const project of projects) { + const projectName = + typeof project?.projectName === 'string' ? project.projectName.trim() : ''; + if (!projectName) continue; + scanned += 1; + + const key = projectConsoleKey(projectName); + const keyType = await redis.type(key); + if (keyType !== 'list') continue; + + let updated = false; + + for (let attempt = 0; attempt < 3; attempt += 1) { + try { + await redis.watch(key); + const rawItems = await redis.lRange(key, 0, -1); + if (rawItems.length === 0) break; + + const keptByTtl = rawItems.filter((raw) => shouldKeepRawLog(raw, cutoffMs)); + + // Explicitly sort by timestamp to ensure correctness regardless of insertion order + // Sort ascending (oldest -> newest), so we can slice from the end + keptByTtl.sort((a, b) => { + const tsA = parseLogTimestampMs(safeJsonParse(a)?.timestamp) || 0; + const tsB = parseLogTimestampMs(safeJsonParse(b)?.timestamp) || 0; + return tsA - tsB; + }); + + let kept = keptByTtl; + if (keptByTtl.length > maxLen) { + kept = keptByTtl.slice(-maxLen); + } + + const needsRewrite = + rawItems.length !== kept.length || keptByTtl.length !== rawItems.length; + if (!needsRewrite) break; + + const multi = redis.multi(); + multi.del(key); + if (kept.length > 0) { + multi.rPush(key, ...kept); + } + const execResult = await multi.exec(); + if (execResult === null) continue; + updated = true; + break; + } finally { + if (typeof redis.unwatch === 'function') { + await redis.unwatch(); + } + } + } + + if (updated) trimmed += 1; + } + + return { trimmed, scanned }; +} + // 获取日志列表 router.get('/', async (req, res) => { const projectName = diff --git a/src/backend/routes/projects.integration.test.js b/src/backend/routes/projects.integration.test.js index d90c28d..6947613 100644 --- a/src/backend/routes/projects.integration.test.js +++ b/src/backend/routes/projects.integration.test.js @@ -2,8 +2,8 @@ import { describe, expect, it } from 'vitest'; import request from 'supertest'; import { createApp } from '../app.js'; +import { pruneConsoleLogsForProjects } from './logs.js'; import { createFakeRedis } from '../test/fakeRedis.js'; -import { trimProjectConsoleLogsByLength } from './logs.js'; describe('projects API', () => { it('GET /api/projects returns projects from unified list', async () => { @@ -96,24 +96,6 @@ describe('projects API', () => { }); describe('logs API', () => { - it('trimProjectConsoleLogsByLength trims *_项目控制台 lists to 1000 items', async () => { - const projectName = 'Demo'; - const key = `${projectName}_项目控制台`; - const items = Array.from({ length: 1505 }, (_, i) => `l${i + 1}`); - const redis = createFakeRedis({ - [key]: items, - other: [ 'x', 'y' ], - }); - - const result = await trimProjectConsoleLogsByLength(redis, { maxLen: 1000 }); - expect(result).toMatchObject({ keysScanned: 1, keysTrimmed: 1, maxLen: 1000 }); - - const listItems = await redis.lRange(key, 0, -1); - expect(listItems.length).toBe(1000); - expect(listItems[0]).toBe('l506'); - expect(listItems[listItems.length - 1]).toBe('l1505'); - }); - it('GET /api/logs uses LOG_TTL_MS=24h without wiping recent logs', async () => { const prev = process.env.LOG_TTL_MS; process.env.LOG_TTL_MS = '24h'; @@ -221,38 +203,6 @@ describe('logs API', () => { expect(JSON.parse(listItems[0]).id).toBe('new'); }); - it('GET /api/logs keeps latest 1000 logs when all timestamps are expired', async () => { - const now = Date.now(); - const projectName = 'Demo'; - const key = `${projectName}_项目控制台`; - const items = Array.from({ length: 1505 }, (_, i) => - JSON.stringify({ - id: `log-${i + 1}`, - timestamp: new Date(now - 26 * 60 * 60 * 1000).toISOString(), - level: 'info', - message: `m${i + 1}`, - }), - ); - const redis = createFakeRedis({ - [key]: items, - }); - - const app = createApp({ redis }); - const resp = await request(app) - .get('/api/logs') - .query({ projectName, limit: 1000 }); - - expect(resp.status).toBe(200); - expect(resp.body.logs.length).toBe(1000); - expect(resp.body.logs[0].id).toBe('log-506'); - expect(resp.body.logs[resp.body.logs.length - 1].id).toBe('log-1505'); - - const listItems = await redis.lRange(key, 0, -1); - expect(listItems.length).toBe(1000); - expect(JSON.parse(listItems[0]).id).toBe('log-506'); - expect(JSON.parse(listItems[listItems.length - 1]).id).toBe('log-1505'); - }); - it('GET /api/logs keeps unix-second timestamps and prunes correctly', async () => { const now = Date.now(); const nowSec = Math.floor(now / 1000); @@ -382,6 +332,146 @@ describe('logs API', () => { expect(JSON.parse(listItems[0]).id).toBe('x'); }); + it('prunes project console logs to latest 1000 items (rpush order)', async () => { + const now = Date.now(); + const projectName = 'Demo'; + const key = `${projectName}_项目控制台`; + const logs = Array.from({ length: 1005 }, (_, i) => + JSON.stringify({ + id: `log-${i}`, + timestamp: new Date(now + i * 1000).toISOString(), + level: 'info', + message: `log-${i}`, + }), + ); + const redis = createFakeRedis({ + 项目心跳: [ + JSON.stringify({ + projectName, + apiBaseUrl: 'http://localhost:8080', + lastActiveAt: Date.now(), + }), + ], + [key]: logs, + }); + + const result = await pruneConsoleLogsForProjects(redis, { maxLen: 1000 }); + + expect(result.scanned).toBe(1); + expect(result.trimmed).toBe(1); + + const listItems = await redis.lRange(key, 0, -1); + expect(listItems.length).toBe(1000); + expect(JSON.parse(listItems[0]).id).toBe('log-5'); + expect(JSON.parse(listItems[999]).id).toBe('log-1004'); + }); + + it('prunes project console logs to latest 1000 items (lpush order)', async () => { + const now = Date.now(); + const projectName = 'Demo'; + const key = `${projectName}_项目控制台`; + const logs = Array.from({ length: 1005 }, (_, i) => + JSON.stringify({ + id: `log-${i}`, + timestamp: new Date(now + i * 1000).toISOString(), + level: 'info', + message: `log-${i}`, + }), + ).reverse(); + const redis = createFakeRedis({ + 项目心跳: [ + JSON.stringify({ + projectName, + apiBaseUrl: 'http://localhost:8080', + lastActiveAt: Date.now(), + }), + ], + [key]: logs, + }); + + const result = await pruneConsoleLogsForProjects(redis, { maxLen: 1000 }); + + expect(result.scanned).toBe(1); + expect(result.trimmed).toBe(1); + + const listItems = await redis.lRange(key, 0, -1); + expect(listItems.length).toBe(1000); + // Logic enforces chronological order (Oldest -> Newest) + expect(JSON.parse(listItems[0]).id).toBe('log-5'); + expect(JSON.parse(listItems[999]).id).toBe('log-1004'); + }); + + it('prunes project console logs older than 24h during scheduled cleanup', async () => { + const now = Date.now(); + const projectName = 'Demo'; + const key = `${projectName}_项目控制台`; + const redis = createFakeRedis({ + 项目心跳: [ + JSON.stringify({ + projectName, + apiBaseUrl: 'http://localhost:8080', + lastActiveAt: Date.now(), + }), + ], + [key]: [ + JSON.stringify({ + id: 'old', + timestamp: new Date(now - 25 * 60 * 60 * 1000).toISOString(), + level: 'info', + message: 'old', + }), + JSON.stringify({ + id: 'new', + timestamp: new Date(now - 60 * 60 * 1000).toISOString(), + level: 'info', + message: 'new', + }), + ], + }); + + const result = await pruneConsoleLogsForProjects(redis, { maxLen: 1000 }); + + expect(result.scanned).toBe(1); + expect(result.trimmed).toBe(1); + + const listItems = await redis.lRange(key, 0, -1); + expect(listItems.length).toBe(1); + expect(JSON.parse(listItems[0]).id).toBe('new'); + }); + + it('prunes correctly even if logs are inserted out of order', async () => { + const now = Date.now(); + const projectName = 'Demo'; + const key = `${projectName}_项目控制台`; + // Insert Oldest, Newest, Middle in that order + const logs = [ + JSON.stringify({ id: 'old', timestamp: new Date(now - 100000).toISOString(), message: 'old' }), + JSON.stringify({ id: 'new', timestamp: new Date(now).toISOString(), message: 'new' }), + JSON.stringify({ id: 'mid', timestamp: new Date(now - 50000).toISOString(), message: 'mid' }), + ]; + + const redis = createFakeRedis({ + 项目心跳: [ JSON.stringify({ projectName, apiBaseUrl: 'http://localhost:8080', lastActiveAt: Date.now() }) ], + [key]: logs, + }); + + // MaxLen 2: Should keep New and Mid. Old is dropped. + // Sorted result in Redis should be [Mid, New] (ascending time) + const result = await pruneConsoleLogsForProjects(redis, { maxLen: 2 }); + + expect(result.scanned).toBe(1); + expect(result.trimmed).toBe(1); + + const listItems = await redis.lRange(key, 0, -1); + expect(listItems.length).toBe(2); + const item0 = JSON.parse(listItems[0]); + const item1 = JSON.parse(listItems[1]); + + // Expect sorted by time: Mid, then New + expect(item0.id).toBe('mid'); + expect(item1.id).toBe('new'); + }); + it('POST /api/logs/clear deletes all logs for project', async () => { const projectName = 'Demo'; const key = `${projectName}_项目控制台`; diff --git a/src/backend/server.js b/src/backend/server.js index bd0ef40..cdedf67 100644 --- a/src/backend/server.js +++ b/src/backend/server.js @@ -1,6 +1,6 @@ import express from 'express'; import cors from 'cors'; -import logRoutes, { trimProjectConsoleLogsByLength } from './routes/logs.js'; +import logRoutes, { pruneConsoleLogsForProjects } from './routes/logs.js'; import commandRoutes from './routes/commands.js'; import projectRoutes from './routes/projects.js'; import { getRedisClient } from './services/redisClient.js'; @@ -13,7 +13,14 @@ function parsePort(value, defaultPort) { return Number.isFinite(parsed) ? parsed : defaultPort; } -const PORT = parsePort(process.env.PORT, 3001); +function msUntilNextHour(nowMs = Date.now()) { + const date = new Date(nowMs); + date.setMinutes(0, 0, 0); + date.setHours(date.getHours() + 1); + return date.getTime() - nowMs; +} + +const PORT = parsePort(process.env.PORT, 19070); app.use(cors()); app.use(express.json()); @@ -43,19 +50,26 @@ const server = app.listen(PORT, async () => { } }, intervalMs); - let trimRunning = false; - const logsTrimIntervalMs = 60 * 60 * 1000; - app.locals.logsTrimInterval = setInterval(async () => { - if (trimRunning) return; - trimRunning = true; - try { - await trimProjectConsoleLogsByLength(redis, { maxLen: 1000 }); - } catch (err) { - void err; - } finally { - trimRunning = false; - } - }, logsTrimIntervalMs); + const scheduleConsolePrune = () => { + const delay = msUntilNextHour(); + app.locals.consolePruneTimeout = setTimeout(async () => { + app.locals.consolePruneInterval = setInterval(async () => { + try { + await pruneConsoleLogsForProjects(redis); + } catch (err) { + void err; + } + }, 3_600_000); + + try { + await pruneConsoleLogsForProjects(redis); + } catch (err) { + void err; + } + }, delay); + }; + + scheduleConsolePrune(); } catch (err) { console.error('[redis] failed to connect on startup', err); } @@ -66,8 +80,11 @@ process.on('SIGINT', async () => { if (app.locals.heartbeatPruneInterval) { clearInterval(app.locals.heartbeatPruneInterval); } - if (app.locals.logsTrimInterval) { - clearInterval(app.locals.logsTrimInterval); + if (app.locals.consolePruneTimeout) { + clearTimeout(app.locals.consolePruneTimeout); + } + if (app.locals.consolePruneInterval) { + clearInterval(app.locals.consolePruneInterval); } if (app.locals.redis) { await app.locals.redis.quit(); diff --git a/src/frontend/App.vue b/src/frontend/App.vue index 6199028..38a46b9 100644 --- a/src/frontend/App.vue +++ b/src/frontend/App.vue @@ -22,7 +22,7 @@
无法连接到后端服务,请检查服务是否正常运行。
-