Compare commits
380 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca0de7d04d | |||
| 764703d401 | |||
| b8785d1d51 | |||
| 5b372fb8a2 | |||
| f9934bd4f1 | |||
| 66a4cba39b | |||
| 540a7fce05 | |||
| 2e911dcca7 | |||
| a5d5518054 | |||
| e3fe161ae5 | |||
| d0be2fdcf1 | |||
| 72d82d737b | |||
| eab35a35be | |||
| e5e2582743 | |||
| 722917bfc9 | |||
| ac17f91106 | |||
| 9ee5c8a708 | |||
| fb9dbcb11a | |||
| 72e46cef32 | |||
| e5d482264d | |||
| c9e08799f9 | |||
| 89865ec690 | |||
| ba69bc3355 | |||
| 49f159a8ce | |||
| 4e1347ba45 | |||
| 61b8d7d419 | |||
| 954bdbd972 | |||
| 314bece418 | |||
| 687e059a86 | |||
| baacdb0252 | |||
| 58bdf820dc | |||
| dd66d99f2d | |||
| 107b089b16 | |||
| 1f5daebeed | |||
| 4ba45abf38 | |||
| 5a56e5a8cb | |||
| 850038a3ec | |||
| beb802802a | |||
| 476d87c678 | |||
| 49f88a2cd8 | |||
| b11360015d | |||
| 3b2856f388 | |||
| 9d9fbc62f5 | |||
| 807f4c2448 | |||
| da061d35db | |||
| d0544c9bd2 | |||
| 3f5eacae60 | |||
| b25d0798f6 | |||
| 6b1d23c7d7 | |||
| 6951b8049f | |||
| da0424a268 | |||
| 569263dddb | |||
| 646929ddff | |||
| 0a7c5ee617 | |||
| 93400b7793 | |||
| a5aa5f81ad | |||
| 86c865e710 | |||
| 1bd69077a2 | |||
| f37ebf8621 | |||
| e42dc77127 | |||
| 536d520744 | |||
| 6276ea08b7 | |||
| c81fe3183f | |||
| 5e7c5beb43 | |||
| 9b7dba99ff | |||
| 222703d733 | |||
| d884ea00ca | |||
| 422dcae0ff | |||
| 343def7e77 | |||
| 83ce11f55c | |||
| e613aecc8e | |||
| f71da47ccf | |||
| 1acac6439a | |||
| feda614c52 | |||
| 6cef3d2942 | |||
| 00f44032ff | |||
| 8ddcf2ad0d | |||
| 65e4478b4b | |||
| 2f830424a4 | |||
| 7b33b745a0 | |||
| bfda70bbe7 | |||
| 2f67e9e2b9 | |||
| 811eb97fbf | |||
| 0100ab2582 | |||
| b866e944ab | |||
| a2ae74d582 | |||
| 9ac6a2d27f | |||
| 3cec226b89 | |||
| 51948469c2 | |||
| 4b5602a719 | |||
| 9c9c763e9c | |||
| 0cc1cc14a4 | |||
| f078d0e03b | |||
| 524dabcf7f | |||
| 0ccd1ba934 | |||
| a6673157a5 | |||
| 78798e3630 | |||
| 68fdcbae0a | |||
| 181560eca2 | |||
| c6f1d73ff5 | |||
| 6cb591212d | |||
| 7cbddf5d48 | |||
| cb738a8e9a | |||
| 5393252036 | |||
| 3bc2843642 | |||
| 1d6d26d612 | |||
| c65e3ea2a7 | |||
| 67dac0168f | |||
| 57d67c494b | |||
| 928e3c5fdb | |||
| 832e34fd98 | |||
| 19cfb7caaa | |||
| ea3046f635 | |||
| 39a8cf16cf | |||
| b62d4f6f9f | |||
| 44c19f329a | |||
| 9e924562d2 | |||
| e3eb0e65a5 | |||
| fc056059f9 | |||
| bd6467dff9 | |||
| e704b01093 | |||
| 246070e599 | |||
| d93c518a14 | |||
| 1cfac23c01 | |||
| 799140f6c4 | |||
| 8935081ac5 | |||
| ab431028c1 | |||
| cc2dad7f31 | |||
| 24b4439b2b | |||
| b8af1fc418 | |||
| b77a314b36 | |||
| d519113734 | |||
| 65c77155d8 | |||
| 2deb89b49c | |||
| b76295aaec | |||
| 8311821590 | |||
| 73fdcafb81 | |||
| 91d0d4104e | |||
| 7fa4bd6dff | |||
| a20cefcfbd | |||
| 6183047fd6 | |||
| df2c188f5d | |||
| 2d00d6a8eb | |||
| 080c1158d4 | |||
| facc1e8ae4 | |||
| 1ecd8e4542 | |||
| c6485168b6 | |||
| e9af02dd22 | |||
| c9baecfdfa | |||
| 3d5870b404 | |||
| afd32118b2 | |||
| d0308aaef6 | |||
| 04dda2c419 | |||
| e084d90c61 | |||
| 24aa8494e5 | |||
| 5c7775c10c | |||
| 7a6479dbfa | |||
| 0fdae981cf | |||
| 3419ab8a09 | |||
| d512460dc9 | |||
| 0f02e047cc | |||
| 35583115ff | |||
| f745714530 | |||
| 0d056a45e9 | |||
| 0ded4a28ec | |||
| 5ac4a5c7cf | |||
| 9c5ebdfdbd | |||
| c457cb4058 | |||
| 8b2263916c | |||
| 293a0d64fe | |||
| 94fbdf7b2d | |||
| e65c9993a2 | |||
| ae47eb42de | |||
| c570801119 | |||
| cba8b3d8f6 | |||
| caef4a216b | |||
| 507b957b4e | |||
| c70424a224 | |||
| e8c1acba9f | |||
| 34786737b4 | |||
| be633f3e58 | |||
| 00a3309005 | |||
| 3d15200cfa | |||
| d8aca5bf18 | |||
| cb60af4483 | |||
| 5a5adafb39 | |||
| c0296b9d8e | |||
| ca4aceba3e | |||
| b5dfa8a870 | |||
| 17cdc96bb0 | |||
| 6012af0b4d | |||
| 9bdf7e420f | |||
| 40d672fdbb | |||
| 557c8d28c1 | |||
| 9f7be66356 | |||
| ac38442383 | |||
| 14131f98f0 | |||
| 64503c0260 | |||
| d19f5af807 | |||
| 463841a740 | |||
| 36fde6e6ed | |||
| 1a61fae590 | |||
| a50f17941d | |||
| 362b28199f | |||
| 5213def150 | |||
| 5166660f3a | |||
| 48ef06babb | |||
| ede7fbaa64 | |||
| 4c4d41b8f5 | |||
| 32feb1b6db | |||
| ebe276ea29 | |||
| 84282e87ae | |||
| de8d860244 | |||
| 3f50a40d25 | |||
| dc48f71b25 | |||
| 913fd16bb9 | |||
| deffa8a8f4 | |||
| 64a8a18287 | |||
| cc0ca47d61 | |||
| 449373f24f | |||
| 730b665306 | |||
| f5bea6f0d2 | |||
| c808c28edf | |||
| d53309e37e | |||
| a8fe7fed0a | |||
| f91cd847a9 | |||
| 993a234152 | |||
| e709c72f1f | |||
| f7c4b95dd6 | |||
| 75864fe04f | |||
| ae22db4b3e | |||
| 0b82a3fee7 | |||
| d785c78d6d | |||
| 56906e8fe2 | |||
| da234c4e73 | |||
| 7193b88156 | |||
| ac66f5b4e7 | |||
| 6dd0b487b7 | |||
| fe211619aa | |||
| 0cbda8c840 | |||
| 5958c21048 | |||
| 02423cb803 | |||
| 152852e2c3 | |||
| 3c7036babf | |||
| 74de9d303e | |||
| ad2e7667d3 | |||
| 82c297776a | |||
| 77b332c037 | |||
| 97be1fd214 | |||
| 366bdc18fa | |||
| 280357d7d7 | |||
| fd9b7d85b5 | |||
| 8b82d6bab0 | |||
| 869f64ac18 | |||
| 69056491a7 | |||
| 02f1fa4ef3 | |||
| eadf7254a1 | |||
| 1c70e597a3 | |||
| 867e2aa552 | |||
| a2d392dae7 | |||
| 8b10f9d64a | |||
| b1aa235726 | |||
| 5afb18b745 | |||
| a1b0b2c514 | |||
| f591033309 | |||
| 89fa517693 | |||
| 973e23e038 | |||
| ad7f4d8d62 | |||
| fb4e51ec5a | |||
| 8cd091add8 | |||
| 6be989c299 | |||
| ccb78ed980 | |||
| 95aa8a610f | |||
| 9de6ac9904 | |||
| cf0725b037 | |||
| 3943ff4f3a | |||
| deb3a94ee6 | |||
| 99201f48fd | |||
| c72631a293 | |||
| a1276f38ad | |||
| 400dcae781 | |||
| cdd0be328a | |||
| fbf5eadcf2 | |||
| 9bba58a529 | |||
| 488838c989 | |||
| da43960f38 | |||
| 5fcc1e22a6 | |||
| 37ceaef6d9 | |||
| d414b945e1 | |||
| 4ca07fa252 | |||
| d2832567c7 | |||
| 14dc29f241 | |||
| b9e8db7a3f | |||
| 08d0baf762 | |||
| 9c063a788f | |||
| 5a42345bc0 | |||
| 6c5e6a2daa | |||
| ac0eb79707 | |||
| e37bc6cea9 | |||
| 2c6f3abbd0 | |||
| d5c498549b | |||
| 07eab6020c | |||
| 938e700fb3 | |||
| 424c4efd64 | |||
| c08e2a2242 | |||
| 6108d69b24 | |||
| 680577fc97 | |||
| 86bb32f87e | |||
| c3c3bae370 | |||
| 33b7ed93bf | |||
| 61b891db73 | |||
| aa65f10c52 | |||
| 80d73d1b74 | |||
| 752a24497c | |||
| 341d16fd6d | |||
| d744d7df8e | |||
| 4af9acf564 | |||
| 6354d507df | |||
| 7e29397235 | |||
| 4969876ce8 | |||
| b676e320de | |||
| 938d26927f | |||
| 8530f0e0d5 | |||
| cea5730804 | |||
| dfc37f6e90 | |||
| 0bcfeaa6d6 | |||
| cb5b4c42de | |||
| b36e1d8010 | |||
| 8a5d4159a4 | |||
| c03fe97d18 | |||
| e9feaa6cdc | |||
| 83547c0b2e | |||
| 2ec24371c5 | |||
| 8ae4bc48cd | |||
| 9d34655cc1 | |||
| 249a1aaed9 | |||
| 120ae186ef | |||
| cca410da33 | |||
| 26833f00d3 | |||
| d4ea03d735 | |||
| f262876b25 | |||
| bfa50337e5 | |||
| 305f8d5689 | |||
| 785ffe2a7e | |||
| 55b7cbe904 | |||
| b9139093f1 | |||
| f796ddf0a5 | |||
| c9a99f4fb3 | |||
| 8c8d2a74d5 | |||
| 302fdcc1a1 | |||
| 5590391779 | |||
| 56993e4f98 | |||
| cd3c35b5e6 | |||
| f0c586a345 | |||
| 00a6491e59 | |||
| 7caa880ebd | |||
| a584792603 | |||
| 1a27327036 | |||
| 814e11588e | |||
| 73c1776e1e | |||
| ead4b2580f | |||
| 3ba85986ed | |||
| 3ecd3d75d4 | |||
| 539c33ef42 | |||
| e5b0c2d4ef | |||
| 4cfed27bf7 | |||
| 003ca96f07 | |||
| 9e20898fd0 | |||
| cad49e6de1 | |||
| 06f1e9ee07 | |||
| 55e14805de | |||
| ae94101eb8 | |||
| f9045a8404 | |||
| 05600232b7 | |||
| 98cb33c3d6 | |||
| 2cf7602573 | |||
| 669a62b17a | |||
| 99e23d7e78 | |||
| 5ae8855894 | |||
| b2617b4e31 |
@@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
description: 减少盲目搜文件、控制 Read 次数与 token 消耗
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Agent 探索约束(省 token)
|
|
||||||
|
|
||||||
## 开始前
|
|
||||||
|
|
||||||
1. 读根目录 `AGENTS.md` 的「代码地图」与「问题 → 文件」表
|
|
||||||
2. 架构/API 全览只读 `docs/project/nexus-functional-development-guide.md`(或用户指定的 §),禁止为找结构全库 Grep
|
|
||||||
3. 用户已给出路径/文件名时,只读该路径,禁止扩大范围
|
|
||||||
|
|
||||||
## 搜索顺序
|
|
||||||
|
|
||||||
```
|
|
||||||
AGENTS.md / 功能指南 § → Grep(带 path)→ Read(≤3 个文件)→ 仍不够再扩一圈
|
|
||||||
```
|
|
||||||
|
|
||||||
- **禁止**无 `path` 的全仓库 `Grep` / `Glob`(除非用户明确要求全库)
|
|
||||||
- **Grep 优先于 Read**;Glob 只用于定位文件名,不逐个 Read 全部匹配
|
|
||||||
- 同一任务 **Read 不超过 5 个文件**;找到答案后立即停止
|
|
||||||
- 不要重复 Read 已在本对话出现过的文件
|
|
||||||
|
|
||||||
## 分层定位(禁止跨层乱搜)
|
|
||||||
|
|
||||||
| 层 | 路径 |
|
|
||||||
|----|------|
|
|
||||||
| HTTP 路由 | `server/api/<module>.py` |
|
|
||||||
| 业务 | `server/application/services/` |
|
|
||||||
| 数据访问 | `server/infrastructure/` |
|
|
||||||
| 模型 | `server/domain/models/` |
|
|
||||||
| 前端页 | `frontend/src/pages/` |
|
|
||||||
| 前端路由 | `frontend/src/router/index.ts` |
|
|
||||||
| 前端 API 客户端 | `frontend/src/api/` |
|
|
||||||
|
|
||||||
真实前端源码在 `frontend/src/`,不是 `web/app/*.html`(那是构建产物)。
|
|
||||||
|
|
||||||
## 文档索引
|
|
||||||
|
|
||||||
- 功能 SSOT:`docs/project/nexus-functional-development-guide.md`
|
|
||||||
- 文档导航:`docs/README.md`
|
|
||||||
- 本地路径/MCP:`docs/project/linux-dev-paths.md`
|
|
||||||
- 运维(连机/exec):用户 Skill `Nexus平台` 或 `docs/project/nexus-1panel-operations-knowledge.md`
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": {
|
|
||||||
"compound-engineering": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-14
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
## 栈
|
## 栈
|
||||||
|
|
||||||
FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA(15 页)· Vite → `web/app/`
|
FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA(14 页)· Vite → `web/app/`
|
||||||
|
|
||||||
## 环境(勿在仓库写密码)
|
## 环境(勿在仓库写密码)
|
||||||
|
|
||||||
@@ -29,25 +29,13 @@ FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA(15 页)· Vite
|
|||||||
|
|
||||||
见 `.cursor/rules/perfect-implementation.mdc` · `nexus-security.mdc` — 完美实现、无静默吞错、无明文密钥、CUD 审计。
|
见 `.cursor/rules/perfect-implementation.mdc` · `nexus-security.mdc` — 完美实现、无静默吞错、无明文密钥、CUD 审计。
|
||||||
|
|
||||||
## Agent Git / diff(省 token、防卡顿)
|
|
||||||
|
|
||||||
- **禁止**无路径过滤的全仓 `git diff` / `git diff HEAD`(`web/app/assets` 构建产物可导致数十万行输出)。
|
|
||||||
- **优先**:`git diff --stat` 或 `git diff -- frontend/src server/api tests docs`。
|
|
||||||
- **提交**:只 `git add` 源码与文档;`web/app/assets/*` 由 Docker 内 `vite build` 生成,勿为 diff 把海量 hash 文件纳入上下文。
|
|
||||||
- **部署/门控**:定向 pytest(如 `tests/integration/test_*.py`),避免并行全仓 diff + 本地 build + 远程镜像重建。
|
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
默认 **本机 rsync 直传**(不 push Gitea):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash deploy/pre_deploy_check.sh && bash deploy/deploy-production.sh
|
bash deploy/pre_deploy_check.sh && bash deploy/deploy-production.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
仅同步源码(不重建镜像):`bash deploy/rsync-local-to-server.sh`
|
前端: `cd frontend && npx vite build` → tar/scp(见 `deploy/deploy-frontend.sh`)
|
||||||
仍走 Gitea 拉取:`NEXUS_DEPLOY_VIA_GIT=1 bash deploy/deploy-production.sh`
|
|
||||||
|
|
||||||
前端: `cd frontend && npx vite build` → 部署脚本内 tar/scp 到 `web/app/`
|
|
||||||
|
|
||||||
## 进度条(改代码时输出)
|
## 进度条(改代码时输出)
|
||||||
|
|
||||||
|
|||||||
+29
-40
@@ -1,40 +1,29 @@
|
|||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
.claude
|
.claude
|
||||||
.cursor
|
.cursor
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
**/*.pyc
|
**/*.pyc
|
||||||
**/.pytest_cache
|
**/.pytest_cache
|
||||||
**/.mypy_cache
|
**/.mypy_cache
|
||||||
**/.ruff_cache
|
**/.ruff_cache
|
||||||
.coverage
|
.coverage
|
||||||
htmlcov/
|
htmlcov/
|
||||||
|
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
frontend-v2/node_modules
|
frontend/dist
|
||||||
frontend/dist
|
node_modules
|
||||||
node_modules
|
|
||||||
.venv
|
.env
|
||||||
.venv-*
|
docker/.env
|
||||||
venv
|
docker/runtime
|
||||||
.pytest_cache
|
|
||||||
.ruff_cache
|
tests/
|
||||||
.playwright-mcp
|
docs/
|
||||||
2025.2
|
standards/
|
||||||
=2025.2
|
*.md
|
||||||
*.bak-
|
!docker/README.md
|
||||||
|
|
||||||
.env
|
web/uploads
|
||||||
docker/.env
|
backups/
|
||||||
docker/runtime
|
deploy/gate_log.jsonl
|
||||||
tmp/
|
|
||||||
|
|
||||||
tests/
|
|
||||||
docs/
|
|
||||||
standards/
|
|
||||||
*.md
|
|
||||||
!docker/README.md
|
|
||||||
|
|
||||||
web/uploads
|
|
||||||
backups/
|
|
||||||
deploy/gate_log.jsonl
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Nexus Environment Configuration
|
||||||
|
# Copy this file to .env and modify values for your deployment.
|
||||||
|
# Or run the web installer at /app/install.html to auto-generate this file.
|
||||||
|
#
|
||||||
|
# Generate secret keys: openssl rand -hex 32
|
||||||
|
|
||||||
|
# ── Brand ──
|
||||||
|
NEXUS_SYSTEM_NAME=Nexus
|
||||||
|
NEXUS_SYSTEM_TITLE=Nexus — 服务器运维管理平台
|
||||||
|
|
||||||
|
# ── Server ──
|
||||||
|
NEXUS_HOST=0.0.0.0
|
||||||
|
NEXUS_PORT=8600
|
||||||
|
|
||||||
|
# ── Deployment (auto-set by installer) ──
|
||||||
|
NEXUS_DEPLOY_PATH=/opt/nexus
|
||||||
|
# Comma-separated frontend origins for CORS
|
||||||
|
NEXUS_CORS_ORIGINS=http://localhost:8600
|
||||||
|
# Base URL for Agent heartbeat reporting
|
||||||
|
NEXUS_API_BASE_URL=http://localhost:8600
|
||||||
|
|
||||||
|
# ── Database (MySQL, REQUIRED) ──
|
||||||
|
NEXUS_DATABASE_URL=mysql+aiomysql://root:password@127.0.0.1:3306/nexus
|
||||||
|
# Pool params: auto-calculated by installer from MySQL max_connections
|
||||||
|
# Formula: pool_size = max(20, maxConn*0.4), overflow = max(20, maxConn*0.3)
|
||||||
|
# Default below matches MySQL max_connections=400 (160+120=280 max)
|
||||||
|
# install wizard will overwrite these with values tuned to your MySQL instance
|
||||||
|
NEXUS_DB_POOL_SIZE=160
|
||||||
|
NEXUS_DB_MAX_OVERFLOW=120
|
||||||
|
|
||||||
|
# ── Security (REQUIRED — generate with: openssl rand -hex 32) ──
|
||||||
|
NEXUS_SECRET_KEY=change-me-use-openssl-rand-hex-32
|
||||||
|
NEXUS_API_KEY=change-me-use-openssl-rand-hex-32
|
||||||
|
NEXUS_ENCRYPTION_KEY=
|
||||||
|
|
||||||
|
# ── Redis (REQUIRED) ──
|
||||||
|
NEXUS_REDIS_URL=redis://127.0.0.1:6379/0
|
||||||
|
|
||||||
|
# ── SSH ──
|
||||||
|
NEXUS_SSH_STRICT_HOST_CHECKING=false
|
||||||
|
|
||||||
|
# ── Alert Thresholds (%) ──
|
||||||
|
NEXUS_CPU_ALERT_THRESHOLD=80
|
||||||
|
NEXUS_MEM_ALERT_THRESHOLD=80
|
||||||
|
NEXUS_DISK_ALERT_THRESHOLD=80
|
||||||
|
|
||||||
|
# ── Health Check ──
|
||||||
|
NEXUS_HEALTH_CHECK_INTERVAL=60
|
||||||
|
|
||||||
|
# ── Telegram Alerts (optional — configure in Settings UI) ──
|
||||||
|
NEXUS_TELEGRAM_BOT_TOKEN=
|
||||||
|
NEXUS_TELEGRAM_CHAT_ID=
|
||||||
|
|
||||||
|
# ── MySQL MCP (@yclenove/mysql-mcp-server — run: python scripts/sync_mysql_mcp_env.py) ──
|
||||||
|
# Cursor 从项目根 .env 读取;与 NEXUS_DATABASE_URL 保持一致;默认只读
|
||||||
|
# MYSQL_HOST=127.0.0.1
|
||||||
|
# MYSQL_PORT=3306
|
||||||
|
# MYSQL_USER=root
|
||||||
|
# MYSQL_PASSWORD=password
|
||||||
|
# MYSQL_DATABASE=nexus
|
||||||
|
# MYSQL_READONLY=true # production / MCP read-only
|
||||||
|
# MYSQL_READONLY=false # local dev: full DML/DDL via MCP
|
||||||
|
# MYSQL_DATABASE_ALLOWLIST=nexus
|
||||||
@@ -35,58 +35,18 @@ jobs:
|
|||||||
run: mypy server/ --ignore-missing-imports
|
run: mypy server/ --ignore-missing-imports
|
||||||
continue-on-error: true # Type errors don't block deployment yet
|
continue-on-error: true # Type errors don't block deployment yet
|
||||||
|
|
||||||
- name: Run fast tests with coverage
|
- name: Run tests with coverage
|
||||||
run: |
|
run: |
|
||||||
coverage run -m pytest tests/ -m "not chain and not slow" -q --tb=short
|
coverage run -m pytest tests/ -v --tb=short
|
||||||
coverage report --fail-under=50
|
coverage report --fail-under=50
|
||||||
coverage xml
|
coverage xml
|
||||||
|
|
||||||
- name: Run chain tests
|
|
||||||
run: pytest tests/chain -m chain -q --tb=short
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Upload coverage report
|
- name: Upload coverage report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: coverage-report
|
name: coverage-report
|
||||||
path: coverage.xml
|
path: coverage.xml
|
||||||
|
|
||||||
# ── CI: E2E (main only, optional secrets) ──
|
|
||||||
e2e:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: test
|
|
||||||
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
|
||||||
working-directory: frontend
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Install Playwright Chromium
|
|
||||||
working-directory: frontend
|
|
||||||
run: npx playwright install chromium
|
|
||||||
|
|
||||||
- name: Run Playwright E2E
|
|
||||||
working-directory: frontend
|
|
||||||
env:
|
|
||||||
NEXUS_E2E_BASE: ${{ secrets.NEXUS_E2E_BASE || 'http://127.0.0.1:8600' }}
|
|
||||||
NEXUS_E2E_USER: ${{ secrets.NEXUS_E2E_USER || 'admin' }}
|
|
||||||
NEXUS_E2E_PASSWORD: ${{ secrets.NEXUS_E2E_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
if [ -z "$NEXUS_E2E_PASSWORD" ]; then
|
|
||||||
echo "NEXUS_E2E_PASSWORD not set — skip E2E"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
npm run test:e2e
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
# ── CD: Deploy ──
|
# ── CD: Deploy ──
|
||||||
deploy:
|
deploy:
|
||||||
needs: test # Only deploy if tests pass
|
needs: test # Only deploy if tests pass
|
||||||
|
|||||||
+88
-90
@@ -1,90 +1,88 @@
|
|||||||
# Nexus — Python
|
# Nexus — Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
build/
|
build/
|
||||||
.eggs/
|
.eggs/
|
||||||
|
|
||||||
# Environment (NEVER commit — contains production credentials)
|
# Environment (NEVER commit — contains production credentials)
|
||||||
.env
|
.env
|
||||||
.install_locked
|
.install_locked
|
||||||
.install_state.json
|
.install_state.json
|
||||||
SECRETS.md
|
SECRETS.md
|
||||||
.git-askpass-nexus.sh
|
.git-askpass-nexus.sh
|
||||||
deploy/nexus-1panel.secrets.sh
|
deploy/nexus-1panel.secrets.sh
|
||||||
*.pem
|
*.pem
|
||||||
.env.*
|
.env.*
|
||||||
.venv/
|
.venv/
|
||||||
.venv-*/
|
venv/
|
||||||
venv/
|
ENV/
|
||||||
ENV/
|
|
||||||
|
# Agent memory (local MCP persistence, do not commit)
|
||||||
# Agent memory (local MCP persistence, do not commit)
|
.claude/mcp-memory.jsonl
|
||||||
.claude/mcp-memory.jsonl
|
|
||||||
|
# MCP tool data (local development artifacts)
|
||||||
# MCP tool data (local development artifacts)
|
.megamemory/
|
||||||
.megamemory/
|
.playwright-mcp/
|
||||||
.playwright-mcp/
|
frontend/test-results/
|
||||||
frontend/test-results/
|
tmp/
|
||||||
frontend/e2e/.auth/
|
|
||||||
tmp/
|
# IDE
|
||||||
|
.vscode/
|
||||||
# IDE
|
.idea/
|
||||||
.vscode/
|
*.swp
|
||||||
.idea/
|
*.swo
|
||||||
*.swp
|
*~
|
||||||
*.swo
|
|
||||||
*~
|
# OS
|
||||||
|
.DS_Store
|
||||||
# OS
|
Thumbs.db
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
# Logs
|
||||||
|
*.log
|
||||||
# Logs
|
nexus.log
|
||||||
*.log
|
|
||||||
nexus.log
|
# Node offline cache for WSL install (large tarball)
|
||||||
|
.cache/
|
||||||
# Node offline cache for WSL install (large tarball)
|
|
||||||
.cache/
|
# Test
|
||||||
|
.pytest_cache/
|
||||||
# Test
|
htmlcov/
|
||||||
.pytest_cache/
|
.coverage
|
||||||
htmlcov/
|
coverage.xml
|
||||||
.coverage
|
|
||||||
coverage.xml
|
# Node (for Tailwind/Alpine.js build)
|
||||||
|
node_modules/
|
||||||
# Node (for Tailwind/Alpine.js build)
|
web/css/tailwind-output.css
|
||||||
node_modules/
|
|
||||||
web/css/tailwind-output.css
|
# Build output (Vite/Vuetify frontend)
|
||||||
|
# SPA 构建产物纳入 Git,便于服务器 git pull 部署(见 deploy/deploy-frontend.sh)
|
||||||
# Build output (Vite/Vuetify frontend)
|
# 仍忽略本地临时文件
|
||||||
# SPA 构建产物纳入 Git,便于服务器 git pull 部署(见 deploy/deploy-frontend.sh)
|
web/app/assets/.tmp/
|
||||||
# 仍忽略本地临时文件
|
# Legacy dist dir (no longer used)
|
||||||
web/app/assets/.tmp/
|
**/dist/
|
||||||
# Legacy dist dir (no longer used)
|
|
||||||
**/dist/
|
# Uploads
|
||||||
|
web/uploads/
|
||||||
# Uploads
|
|
||||||
web/uploads/
|
# Data (sensitive)
|
||||||
|
web/data/config.php
|
||||||
# Data (sensitive)
|
web/data/*.db
|
||||||
web/data/config.php
|
web/data/1panel-hosts.json
|
||||||
web/data/*.db
|
|
||||||
web/data/1panel-hosts.json
|
# Backups
|
||||||
|
backups/
|
||||||
# Backups
|
|
||||||
backups/
|
# Docker local secrets
|
||||||
|
docker/.env
|
||||||
# Docker local secrets
|
docker/.env.prod
|
||||||
docker/.env
|
!docker/.env.example
|
||||||
docker/.env.prod
|
!docker/.env.prod.example
|
||||||
!docker/.env.example
|
scripts/npm-proxy.env
|
||||||
!docker/.env.prod.example
|
|
||||||
scripts/npm-proxy.env
|
# Deploy
|
||||||
|
deploy/*.key
|
||||||
# Deploy
|
|
||||||
deploy/*.key
|
# Backups
|
||||||
|
backups/
|
||||||
# Backups
|
|
||||||
backups/
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
name: nexus-btpanel-review
|
|
||||||
description: Use when reviewing or changing Nexus BT/宝塔 panel integration, one-click login, API bootstrap, session keepalive fixes, or BT credential handling.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Nexus BT Panel Review
|
|
||||||
|
|
||||||
Use for 宝塔/BT panel integration changes.
|
|
||||||
|
|
||||||
## Call chain to map
|
|
||||||
|
|
||||||
API one-click login -> btpanel_service -> BT API credential lookup/bootstrap -> SSH bootstrap if needed -> login URL/tmp_token -> panel session behavior.
|
|
||||||
|
|
||||||
## Required checks
|
|
||||||
|
|
||||||
- Do not log or return BT API keys, session cookies, temporary login tokens, SSH private keys, or passwords.
|
|
||||||
- One-click login should fail closed for real authentication errors, but best-effort environment repair must not block login unless required for correctness.
|
|
||||||
- Session keepalive/cleanup repair must be idempotent and throttled; do not restart or rewrite BT panel aggressively without explicit approval.
|
|
||||||
- Respect existing BT panel services and ports. Do not occupy panel, nginx, redis, or mysql ports.
|
|
||||||
- Old servers with existing BT API credentials must still receive the session-cleanup TTL check before login.
|
|
||||||
- Record bootstrap state without storing secrets.
|
|
||||||
|
|
||||||
## Regression tests
|
|
||||||
|
|
||||||
Cover:
|
|
||||||
|
|
||||||
- Existing credentials still trigger throttled TTL check.
|
|
||||||
- Missing SSH auth does not block login URL generation.
|
|
||||||
- Failed repair is throttled and retried later.
|
|
||||||
- Temporary login token TTL remains bounded.
|
|
||||||
- No secret values appear in returned bootstrap/status payloads.
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
name: nexus-security-review
|
|
||||||
description: Use when reviewing or changing Nexus backend/API/security-sensitive code. Focuses on FastAPI admin APIs, SQLAlchemy data access, SSH operations, file manager boundaries, logging redaction, and regression tests.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Nexus Security Review
|
|
||||||
|
|
||||||
Use this skill before changing Nexus backend/API code or doing a security pass.
|
|
||||||
|
|
||||||
## Required workflow
|
|
||||||
|
|
||||||
1. Identify entrypoints: FastAPI router, service method, DB model/session, Redis/lock usage, SSH/BT panel call.
|
|
||||||
2. Draw the request path in notes: API -> schema -> service -> DB/Redis/SSH/external.
|
|
||||||
3. Check auth first: admin-only endpoints must depend on the current admin dependency; background/admin tools must still preserve audit logs.
|
|
||||||
4. Validate every user-controlled value at the schema/API boundary and again before shell/path-sensitive sinks.
|
|
||||||
5. For every fix, add pytest coverage for the dangerous case and the allowed case.
|
|
||||||
6. Run focused tests, then full pytest when practical, then git diff --check.
|
|
||||||
7. Update the Nexus Markdown report with changed files, tests, and residual risks.
|
|
||||||
|
|
||||||
## Nexus-specific rules
|
|
||||||
|
|
||||||
- Do not print secrets: passwords, SSH keys, tokens, cookies, API keys, .env values, BT panel credentials.
|
|
||||||
- script_service is an intentional administrator remote shell. Do not report arbitrary shell execution there as a vulnerability. Review only auth, audit trail, timeout, output truncation, and redaction.
|
|
||||||
- File manager APIs must not unexpectedly operate outside the target path/server chosen by the admin UI.
|
|
||||||
- Any remote shell command must use shlex.quote; add -- where command options can be confused with filenames.
|
|
||||||
- Archive extraction must list and validate members before extraction; reject absolute paths, .., backslashes, and special file types.
|
|
||||||
- Error responses must not expose stack traces or secrets.
|
|
||||||
- DB access should use SQLAlchemy parameters/query builder, not raw string interpolation.
|
|
||||||
|
|
||||||
## Review output
|
|
||||||
|
|
||||||
For each finding record:
|
|
||||||
|
|
||||||
- Severity: Critical/High/Medium/Low/Hardening
|
|
||||||
- Call chain
|
|
||||||
- Evidence file/function
|
|
||||||
- Exploit or failure boundary
|
|
||||||
- Fix summary
|
|
||||||
- Test command/result
|
|
||||||
- Commit hash if committed
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
name: nexus-ssh-safety
|
|
||||||
description: Use when reviewing or editing Nexus code that executes commands on managed servers over SSH, including file manager, transfer, archive, agent install, and BT panel bootstrap flows.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Nexus SSH Safety
|
|
||||||
|
|
||||||
Use for any Nexus feature that builds shell commands or transfers files over SSH.
|
|
||||||
|
|
||||||
## Command construction rules
|
|
||||||
|
|
||||||
- Prefer argument arrays/local library APIs when available; otherwise quote every dynamic shell token with shlex.quote.
|
|
||||||
- Insert -- before filename/path operands for commands that parse options:
|
|
||||||
m, mv, cp, ar, zip, chmod, chown, etc.
|
|
||||||
- Never concatenate unquoted user input into shell command strings.
|
|
||||||
- Keep timeouts explicit for network/SSH work; avoid unbounded commands.
|
|
||||||
- Truncate or sanitize command output before returning it to API clients.
|
|
||||||
- Do not log secrets or full credential-bearing URLs.
|
|
||||||
|
|
||||||
## Path and archive rules
|
|
||||||
|
|
||||||
- Normalize paths with POSIX semantics for remote Linux paths.
|
|
||||||
- Reject empty paths where dangerous, root/system paths for recursive destructive operations, and traversal where the feature expects a bounded directory.
|
|
||||||
- Archive creation must protect member names beginning with -.
|
|
||||||
- Archive extraction must validate member names and types before extraction.
|
|
||||||
- Temporary files/directories must be unique and cleaned best-effort.
|
|
||||||
|
|
||||||
## script_service exception
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Collecting tzdata
|
|
||||||
Downloading tzdata-2026.2-py2.py3-none-any.whl.metadata (1.4 kB)
|
|
||||||
Downloading tzdata-2026.2-py2.py3-none-any.whl (349 kB)
|
|
||||||
Installing collected packages: tzdata
|
|
||||||
Successfully installed tzdata-2026.2
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 项目概要
|
## 项目概要
|
||||||
|
|
||||||
Nexus:2000+ 子机运维平台。后端 FastAPI + MySQL + Redis;前端 **Vue 3 + Vuetify 4 + TypeScript SPA**(15 页,`frontend/src/pages/`);构建输出 `web/app/`;安装向导 `web/app/install.html`。
|
Nexus:2000+ 子机运维平台。后端 FastAPI + MySQL + Redis;前端 **Vue 3 + Vuetify 4 + TypeScript SPA**(14 页,`frontend/src/pages/`);构建输出 `web/app/`;安装向导 `web/app/install.html`。
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
|
|
||||||
@@ -19,14 +19,6 @@ cd frontend && npm run dev # :3000/app/
|
|||||||
|
|
||||||
路径见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)。
|
路径见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)。
|
||||||
|
|
||||||
## Git(仅本机快照)
|
|
||||||
|
|
||||||
- 本地 `git commit` 用于回滚;**不 `git push`**(除非你明确要求)。
|
|
||||||
- 已移除 `origin` 跟踪,避免误推;原远程:`http://66.154.115.8:3000/admin/Nexus.git`(需恢复时说一声)。
|
|
||||||
- `.env`、`SECRETS.md`、`*.pem` 已在 `.gitignore`,**勿提交**。
|
|
||||||
- 改代码仍写 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)。
|
|
||||||
- 用户说「提交快照 / commit」时执行。
|
|
||||||
|
|
||||||
## 强制约束
|
## 强制约束
|
||||||
|
|
||||||
- 完美实现:`.cursor/rules/perfect-implementation.mdc`
|
- 完美实现:`.cursor/rules/perfect-implementation.mdc`
|
||||||
@@ -40,57 +32,13 @@ cd frontend && npm run dev # :3000/app/
|
|||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
默认 **本机 rsync → SSH 生产机**(不 push Gitea):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash deploy/pre_deploy_check.sh
|
bash deploy/pre_deploy_check.sh
|
||||||
bash deploy/deploy-production.sh # rsync + Docker upgrade --skip-git + 前端
|
bash deploy/deploy-production.sh # 需 SSH nexus + 用户批准
|
||||||
```
|
```
|
||||||
|
|
||||||
可选:`NEXUS_DEPLOY_VIA_GIT=1` 恢复远程 `git pull`。仅 `git push` 当你明确要求时。
|
|
||||||
|
|
||||||
详情见功能指南 §17–18。
|
详情见功能指南 §17–18。
|
||||||
|
|
||||||
## 归档文档
|
## 归档文档
|
||||||
|
|
||||||
历史审计合并卷:`docs/archive/README.md`
|
历史审计合并卷:`docs/archive/README.md`
|
||||||
|
|
||||||
## 代码地图(Agent 先读这里,勿全库搜)
|
|
||||||
|
|
||||||
```
|
|
||||||
server/main.py # 路由挂载入口
|
|
||||||
server/config.py # 配置(.env + settings 表)
|
|
||||||
server/api/*.py # HTTP/WebSocket 路由(按模块拆分)
|
|
||||||
server/application/ # 业务 services
|
|
||||||
server/infrastructure/ # DB / Redis / SSH / Telegram
|
|
||||||
server/domain/models/ # SQLAlchemy ORM
|
|
||||||
frontend/src/pages/ # Vue 页面(15 页 SPA)
|
|
||||||
frontend/src/router/ # Hash 路由 #/
|
|
||||||
frontend/src/api/ # 前端 API 客户端
|
|
||||||
frontend/e2e/ # Playwright 验收
|
|
||||||
tests/test_*.py # pytest
|
|
||||||
docs/project/ # 功能/部署 SSOT
|
|
||||||
deploy/ # 生产部署脚本
|
|
||||||
scripts/ # 本地验证、MCP、门控
|
|
||||||
.cursor/mcp.json # mysql-mcp
|
|
||||||
```
|
|
||||||
|
|
||||||
## 问题 → 文件(常见锚点)
|
|
||||||
|
|
||||||
| 问什么 | 先看 |
|
|
||||||
|--------|------|
|
|
||||||
| 某 API 怎么实现 | `server/main.py` 找 router → `server/api/<模块>.py` |
|
|
||||||
| 服务器列表/搜索/exec | `server/api/servers.py`, `server/api/scripts.py` |
|
|
||||||
| 认证/JWT | `server/api/auth.py`, `server/api/dependencies.py` |
|
|
||||||
| Agent 心跳/安装 | `server/api/agent.py` |
|
|
||||||
| WebSSH/终端 | `server/api/webssh.py`, `server/api/terminal.py` |
|
|
||||||
| 同步/文件/搜索 | `server/api/sync_v2.py`, `files.py`, `search.py` |
|
|
||||||
| 设置/审计/告警 | `server/api/settings.py` |
|
|
||||||
| 前端某页 UI | `frontend/src/router/index.ts` → `frontend/src/pages/` |
|
|
||||||
| MySQL MCP | `.cursor/mcp.json`, `scripts/linux_mcp_mysql.sh` |
|
|
||||||
| 本地开发路径 | `docs/project/linux-dev-paths.md` |
|
|
||||||
| 14 页/API 全览 | `docs/project/nexus-functional-development-guide.md` §9–10 |
|
|
||||||
|
|
||||||
## 探索规则(省 token)
|
|
||||||
|
|
||||||
详见 `.cursor/rules/agent-exploration.mdc`。要点:**先地图再 Grep(带 path)→ Read ≤5 个文件 → 停止**。
|
|
||||||
|
|||||||
+29
-46
@@ -1,46 +1,29 @@
|
|||||||
# Nexus 6.0 - API + static SPA (web/app)
|
# Nexus 6.0 — API + 静态 SPA (web/app)
|
||||||
FROM python:3.12-slim-bookworm
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
ARG DEBIAN_MIRROR=https://mirrors.aliyun.com/debian
|
RUN apt-get update \
|
||||||
ARG DEBIAN_SECURITY_MIRROR=https://mirrors.aliyun.com/debian-security
|
&& apt-get install -y --no-install-recommends curl \
|
||||||
ARG PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
ARG PIP_TRUSTED_HOST=mirrors.aliyun.com
|
|
||||||
|
WORKDIR /app
|
||||||
RUN set -eux; \
|
|
||||||
sed -i \
|
COPY requirements.txt .
|
||||||
-e "s|http://deb.debian.org/debian-security|${DEBIAN_SECURITY_MIRROR}|g" \
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
-e "s|http://deb.debian.org/debian|${DEBIAN_MIRROR}|g" \
|
|
||||||
/etc/apt/sources.list.d/debian.sources; \
|
COPY server/ ./server/
|
||||||
apt-get update; \
|
COPY web/app/ ./web/app/
|
||||||
apt-get install -y --no-install-recommends \
|
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||||
curl \
|
RUN chmod +x ./docker/entrypoint.sh
|
||||||
openssh-client \
|
|
||||||
rsync \
|
ENV PYTHONPATH=/app \
|
||||||
sshpass; \
|
PYTHONUNBUFFERED=1 \
|
||||||
rm -rf /var/lib/apt/lists/*
|
NEXUS_HOST=0.0.0.0 \
|
||||||
|
NEXUS_PORT=8600
|
||||||
WORKDIR /app
|
|
||||||
|
EXPOSE 8600
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir --index-url "${PIP_INDEX_URL}" --trusted-host "${PIP_TRUSTED_HOST}" -r requirements.txt
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||||||
|
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
||||||
COPY server/ ./server/
|
|
||||||
COPY web/app/ ./web/app/
|
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||||
COPY web/app-v2/ ./web/app-v2/
|
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
||||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
|
||||||
RUN chmod +x ./docker/entrypoint.sh
|
|
||||||
|
|
||||||
ENV PYTHONPATH=/app \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
NEXUS_HOST=0.0.0.0 \
|
|
||||||
NEXUS_PORT=8600
|
|
||||||
|
|
||||||
EXPOSE 8600
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
|
||||||
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
|
||||||
|
|
||||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
|
||||||
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+57
-77
@@ -1,77 +1,57 @@
|
|||||||
# Nexus 6.0 鈥?production image (frontend build + API)
|
# Nexus 6.0 — production image (frontend build + API)
|
||||||
# Build: docker compose -f docker/docker-compose.prod.yml build
|
# Build: docker compose -f docker/docker-compose.prod.yml build
|
||||||
# See: docs/design/plans/2026-06-04-1panel-docker-production.md
|
# See: docs/design/plans/2026-06-04-1panel-docker-production.md
|
||||||
|
|
||||||
# 鈹€鈹€ Stage 1: Vue/Vite SPA 鈹€鈹€
|
# ── Stage 1: Vue/Vite SPA ──
|
||||||
FROM node:20-bookworm-slim AS frontend
|
FROM node:20-bookworm-slim AS frontend
|
||||||
WORKDIR /build/frontend
|
WORKDIR /build/frontend
|
||||||
|
|
||||||
COPY frontend/package.json frontend/package-lock.json ./
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
RUN npm ci --ignore-scripts
|
RUN npm ci --ignore-scripts
|
||||||
|
|
||||||
COPY frontend/ ./
|
COPY frontend/ ./
|
||||||
RUN npx vite build
|
RUN npx vite build
|
||||||
|
|
||||||
|
# ── Stage 2: Python API + built static assets ──
|
||||||
# Stage 1b: React/Vite App V2
|
FROM python:3.12-slim-bookworm
|
||||||
FROM node:20-bookworm-slim AS frontend_v2
|
|
||||||
WORKDIR /build/frontend-v2
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends curl \
|
||||||
COPY frontend-v2/package.json frontend-v2/package-lock.json* ./
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
RUN if [ -f package-lock.json ]; then npm ci --ignore-scripts; else npm install --ignore-scripts; fi
|
|
||||||
|
WORKDIR /app
|
||||||
COPY frontend-v2/ ./
|
|
||||||
RUN npx vite build
|
COPY requirements.txt .
|
||||||
# 鈹€鈹€ Stage 2: Python API + built static assets 鈹€鈹€
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
FROM python:3.12-slim-bookworm
|
|
||||||
|
COPY server/ ./server/
|
||||||
RUN apt-get update \
|
COPY web/agent/ ./web/agent/
|
||||||
&& apt-get install -y --no-install-recommends \
|
COPY --from=frontend /build/web/app ./web/app/
|
||||||
curl \
|
# Install wizard (Alpine + Tailwind static page; not part of Vite build)
|
||||||
openssh-client \
|
COPY web/app/install.html ./web/app/install.html
|
||||||
rsync \
|
RUN mkdir -p ./web/app/vendor
|
||||||
sshpass \
|
COPY web/app/vendor/tailwindcss-browser.js \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
web/app/vendor/theme-init.js \
|
||||||
|
web/app/vendor/theme.css \
|
||||||
WORKDIR /app
|
web/app/vendor/alpinejs.min.js \
|
||||||
|
./web/app/vendor/
|
||||||
COPY requirements.txt .
|
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN chmod +x ./docker/entrypoint.sh \
|
||||||
|
&& test -f /app/web/app/install.html \
|
||||||
COPY server/ ./server/
|
&& test -f /app/web/app/vendor/alpinejs.min.js \
|
||||||
COPY web/agent/ ./web/agent/
|
&& test -f /app/web/app/vendor/tailwindcss-browser.js \
|
||||||
COPY --from=frontend /build/web/app ./web/app/
|
&& test -f /app/web/app/vendor/theme.css \
|
||||||
COPY --from=frontend_v2 /build/web/app-v2 ./web/app-v2/
|
&& test -f /app/web/app/vendor/theme-init.js
|
||||||
# Login wallpaper cache (Vite does not emit this dir; sync also writes here at runtime)
|
|
||||||
COPY web/app/wallpapers/ ./web/app/wallpapers/
|
ENV PYTHONPATH=/app \
|
||||||
# Install wizard + static SPA assets not emitted by Vite
|
PYTHONUNBUFFERED=1 \
|
||||||
COPY web/app/install.html ./web/app/install.html
|
NEXUS_HOST=0.0.0.0 \
|
||||||
COPY web/app/servers_import_template.csv ./web/app/servers_import_template.csv
|
NEXUS_PORT=8600
|
||||||
RUN mkdir -p ./web/app/vendor
|
|
||||||
COPY web/app/vendor/tailwindcss-browser.js \
|
EXPOSE 8600
|
||||||
web/app/vendor/theme-init.js \
|
|
||||||
web/app/vendor/theme.css \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||||||
web/app/vendor/alpinejs.min.js \
|
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
||||||
./web/app/vendor/
|
|
||||||
COPY docker/entrypoint.sh ./docker/entrypoint.sh
|
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||||
RUN chmod +x ./docker/entrypoint.sh \
|
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
||||||
&& test -f /app/web/app/install.html \
|
|
||||||
&& test -f /app/web/app/vendor/alpinejs.min.js \
|
|
||||||
&& test -f /app/web/app/vendor/tailwindcss-browser.js \
|
|
||||||
&& test -f /app/web/app/vendor/theme.css \
|
|
||||||
&& test -f /app/web/app/vendor/theme-init.js
|
|
||||||
|
|
||||||
ENV PYTHONPATH=/app \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
NEXUS_HOST=0.0.0.0 \
|
|
||||||
NEXUS_PORT=8600
|
|
||||||
|
|
||||||
EXPOSE 8600
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
|
||||||
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
|
|
||||||
|
|
||||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
|
||||||
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +1,70 @@
|
|||||||
# 1Panel OpenResty — Nexus reverse proxy (paste into site custom config)
|
# 1Panel OpenResty — Nexus reverse proxy (paste into site custom config)
|
||||||
# Domain: api.synaglobal.vip
|
# Domain: api.synaglobal.vip
|
||||||
# Upstream: Nexus Docker on 127.0.0.1:8600 (docker-compose.prod.yml)
|
# Upstream: Nexus Docker on 127.0.0.1:8600 (docker-compose.prod.yml)
|
||||||
#
|
#
|
||||||
# SSL: configure in 1Panel UI (Let's Encrypt). This snippet assumes HTTPS server block.
|
# SSL: configure in 1Panel UI (Let's Encrypt). This snippet assumes HTTPS server block.
|
||||||
|
|
||||||
upstream nexus_api {
|
upstream nexus_api {
|
||||||
server 127.0.0.1:8600;
|
server 127.0.0.1:8600;
|
||||||
keepalive 32;
|
keepalive 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
# WebSocket log without query string (JWT must not hit access_log)
|
# WebSocket log without query string (JWT must not hit access_log)
|
||||||
log_format nexus_ws '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent';
|
log_format nexus_ws '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent';
|
||||||
|
|
||||||
client_max_body_size 500m;
|
client_max_body_size 100m;
|
||||||
client_body_timeout 600s;
|
|
||||||
|
location /api/ {
|
||||||
location /api/ {
|
proxy_pass http://nexus_api;
|
||||||
proxy_pass http://nexus_api;
|
proxy_http_version 1.1;
|
||||||
proxy_http_version 1.1;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_read_timeout 300s;
|
||||||
proxy_read_timeout 600s;
|
proxy_send_timeout 300s;
|
||||||
proxy_send_timeout 600s;
|
proxy_connect_timeout 10s;
|
||||||
proxy_connect_timeout 10s;
|
}
|
||||||
}
|
|
||||||
|
location /ws/ {
|
||||||
location /ws/ {
|
access_log /www/sites/api.synaglobal.vip/log/ws.log nexus_ws;
|
||||||
access_log /www/sites/api.synaglobal.vip/log/ws.log nexus_ws;
|
proxy_pass http://nexus_api;
|
||||||
proxy_pass http://nexus_api;
|
proxy_http_version 1.1;
|
||||||
proxy_http_version 1.1;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Host $host;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_read_timeout 3600s;
|
||||||
proxy_read_timeout 3600s;
|
proxy_send_timeout 3600s;
|
||||||
proxy_send_timeout 3600s;
|
}
|
||||||
}
|
|
||||||
|
location /health {
|
||||||
location /health {
|
proxy_pass http://nexus_api;
|
||||||
proxy_pass http://nexus_api;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header Host $host;
|
}
|
||||||
}
|
|
||||||
|
location /app/ {
|
||||||
location /app/ {
|
proxy_pass http://nexus_api;
|
||||||
proxy_pass http://nexus_api;
|
proxy_http_version 1.1;
|
||||||
proxy_http_version 1.1;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
}
|
||||||
}
|
|
||||||
|
location /agent/ {
|
||||||
location /agent/ {
|
proxy_pass http://nexus_api;
|
||||||
proxy_pass http://nexus_api;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header Host $host;
|
}
|
||||||
}
|
|
||||||
|
# Fallback: install mode or misc routes served by FastAPI
|
||||||
# Fallback: install mode or misc routes served by FastAPI
|
location / {
|
||||||
location / {
|
proxy_pass http://nexus_api;
|
||||||
proxy_pass http://nexus_api;
|
proxy_http_version 1.1;
|
||||||
proxy_http_version 1.1;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,272 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Apply a Nexus release tarball on the production host.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# sudo bash deploy/apply-release-bundle.sh \
|
|
||||||
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
|
||||||
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1 \
|
|
||||||
# --deploy-path /opt/nexus
|
|
||||||
#
|
|
||||||
# Validate only (no backup, no rsync, no rebuild):
|
|
||||||
# bash deploy/apply-release-bundle.sh --validate-only \
|
|
||||||
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
|
||||||
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1
|
|
||||||
#
|
|
||||||
# This script intentionally preserves runtime secrets and data:
|
|
||||||
# - .env
|
|
||||||
# - docker/.env.prod
|
|
||||||
# - web/data
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
TARBALL=""
|
|
||||||
EXPECTED_SHA256=""
|
|
||||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
|
||||||
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/opt/nexus-backups}"
|
|
||||||
SKIP_REBUILD=0
|
|
||||||
DRY_RUN=0
|
|
||||||
VALIDATE_ONLY=0
|
|
||||||
APPLY_TMP=""
|
|
||||||
|
|
||||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
|
||||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
|
||||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
sed -n '1,26p' "$0"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--tarball)
|
|
||||||
TARBALL="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--sha256)
|
|
||||||
EXPECTED_SHA256="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--deploy-path)
|
|
||||||
DEPLOY_PATH="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--backup-dir)
|
|
||||||
BACKUP_DIR="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--skip-rebuild)
|
|
||||||
SKIP_REBUILD=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--dry-run)
|
|
||||||
DRY_RUN=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--validate-only)
|
|
||||||
VALIDATE_ONLY=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
error "Unknown argument: $1"
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
require_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || {
|
|
||||||
error "Missing required command: $1"
|
|
||||||
exit 127
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require_runtime_cmds() {
|
|
||||||
require_cmd tar
|
|
||||||
require_cmd sha256sum
|
|
||||||
if [[ "$VALIDATE_ONLY" == 1 ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
require_cmd rsync
|
|
||||||
require_cmd curl
|
|
||||||
}
|
|
||||||
|
|
||||||
run() {
|
|
||||||
echo "+ $*"
|
|
||||||
if [[ "$DRY_RUN" == 1 ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
"$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [[ -n "${APPLY_TMP:-}" && -d "$APPLY_TMP" ]]; then
|
|
||||||
rm -rf "$APPLY_TMP"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_runtime() {
|
|
||||||
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
|
|
||||||
echo docker
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
|
||||||
echo supervisor
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_sha256() {
|
|
||||||
if [[ -z "$EXPECTED_SHA256" ]]; then
|
|
||||||
warn "No --sha256 provided; skipping checksum verification"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local actual
|
|
||||||
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
|
|
||||||
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
|
|
||||||
error "SHA256 mismatch"
|
|
||||||
error "expected: $EXPECTED_SHA256"
|
|
||||||
error "actual: $actual"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
info "SHA256 OK: $actual"
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_extracted_tree() {
|
|
||||||
local src="$1"
|
|
||||||
for required in server deploy Dockerfile.prod frontend-v2 web/app-v2; do
|
|
||||||
if [[ ! -e "${src}/${required}" ]]; then
|
|
||||||
error "Release bundle missing required path: ${required}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
for forbidden in .env docker/.env.prod web/data .venv-py312-codex frontend-v2/node_modules 2025.2 =2025.2; do
|
|
||||||
if [[ -e "${src}/${forbidden}" ]]; then
|
|
||||||
error "Release bundle contains forbidden path: ${forbidden}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
info "Extracted release tree validation OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
backup_current() {
|
|
||||||
if [[ ! -d "$DEPLOY_PATH" ]]; then
|
|
||||||
warn "Deploy path does not exist yet: $DEPLOY_PATH"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local stamp backup
|
|
||||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
|
||||||
backup="${BACKUP_DIR}/nexus-before-${stamp}.tar.gz"
|
|
||||||
run mkdir -p "$BACKUP_DIR"
|
|
||||||
info "Backup current deploy path -> $backup"
|
|
||||||
run tar czf "$backup" \
|
|
||||||
--exclude='./web/data' \
|
|
||||||
--exclude='./.venv*' \
|
|
||||||
--exclude='./venv' \
|
|
||||||
--exclude='./frontend/node_modules' \
|
|
||||||
--exclude='./frontend-v2/node_modules' \
|
|
||||||
-C "$(dirname "$DEPLOY_PATH")" "$(basename "$DEPLOY_PATH")"
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_release() {
|
|
||||||
local src="$1"
|
|
||||||
run mkdir -p "$DEPLOY_PATH"
|
|
||||||
info "Sync release tree -> $DEPLOY_PATH"
|
|
||||||
run rsync -a --delete \
|
|
||||||
--exclude='.env' \
|
|
||||||
--exclude='docker/.env.prod' \
|
|
||||||
--exclude='web/data' \
|
|
||||||
"${src}/" "${DEPLOY_PATH}/"
|
|
||||||
}
|
|
||||||
|
|
||||||
rebuild_or_restart() {
|
|
||||||
if [[ "$SKIP_REBUILD" == 1 ]]; then
|
|
||||||
warn "--skip-rebuild set; not rebuilding/restarting Nexus"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local runtime
|
|
||||||
runtime="$(detect_runtime)"
|
|
||||||
info "Runtime: $runtime"
|
|
||||||
case "$runtime" in
|
|
||||||
docker)
|
|
||||||
if [[ -x "${DEPLOY_PATH}/deploy/nexus-1panel.sh" || -f "${DEPLOY_PATH}/deploy/nexus-1panel.sh" ]]; then
|
|
||||||
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/nexus-1panel.sh" upgrade --skip-git
|
|
||||||
else
|
|
||||||
error "Docker runtime detected but deploy/nexus-1panel.sh missing"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -f "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" ]]; then
|
|
||||||
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
supervisor)
|
|
||||||
run supervisorctl restart nexus
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
error "Unknown runtime. Rebuild/restart manually."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_local_endpoints() {
|
|
||||||
local port health app app_v2
|
|
||||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
|
||||||
port="${port:-8600}"
|
|
||||||
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
|
||||||
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
|
|
||||||
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
|
|
||||||
echo " /health -> ${health}"
|
|
||||||
echo " /app/ -> ${app}"
|
|
||||||
echo " /app-v2/ -> ${app_v2}"
|
|
||||||
if [[ "$health" != "ok" || "$app" != "200" || "$app_v2" != "200" ]]; then
|
|
||||||
error "Local endpoint verification failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
info "Local endpoint verification OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
if [[ -z "$TARBALL" ]]; then
|
|
||||||
error "--tarball is required"
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
[[ -f "$TARBALL" ]] || {
|
|
||||||
error "Tarball not found: $TARBALL"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
require_runtime_cmds
|
|
||||||
|
|
||||||
info "Tarball: $TARBALL"
|
|
||||||
info "Deploy path: $DEPLOY_PATH"
|
|
||||||
info "Backup dir: $BACKUP_DIR"
|
|
||||||
verify_sha256
|
|
||||||
|
|
||||||
local extracted
|
|
||||||
APPLY_TMP="$(mktemp -d /tmp/nexus-release-apply.XXXXXX)"
|
|
||||||
trap cleanup EXIT
|
|
||||||
run tar xzf "$TARBALL" -C "$APPLY_TMP"
|
|
||||||
extracted="${APPLY_TMP}/nexus-release"
|
|
||||||
validate_extracted_tree "$extracted"
|
|
||||||
if [[ "$VALIDATE_ONLY" == 1 ]]; then
|
|
||||||
info "Validate-only mode: tarball checksum and content validation passed"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
backup_current
|
|
||||||
sync_release "$extracted"
|
|
||||||
rebuild_or_restart
|
|
||||||
if [[ "$DRY_RUN" != 1 && "$SKIP_REBUILD" != 1 ]]; then
|
|
||||||
verify_local_endpoints
|
|
||||||
fi
|
|
||||||
info "Release applied successfully"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -19,33 +19,21 @@ echo "═══ Nexus Frontend Deploy ═══"
|
|||||||
|
|
||||||
REMOTE_RUNTIME=$(ssh_cmd 'bash -s' <<'REMOTE'
|
REMOTE_RUNTIME=$(ssh_cmd 'bash -s' <<'REMOTE'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
docker_cmd() {
|
|
||||||
if docker "$@" 2>/dev/null; then return 0; fi
|
|
||||||
sudo docker "$@" 2>/dev/null
|
|
||||||
}
|
|
||||||
compose_cmd() {
|
|
||||||
if docker compose "$@" 2>/dev/null; then return 0; fi
|
|
||||||
sudo docker compose "$@" 2>/dev/null
|
|
||||||
}
|
|
||||||
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
|
for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do
|
||||||
[[ -d "$d/server" ]] || continue
|
[[ -d "$d/server" ]] || continue
|
||||||
if [[ -f "$d/docker/.env.prod" ]] && compose_cmd version >/dev/null 2>&1; then
|
if [[ -f "$d/docker/.env.prod" ]] && docker compose version >/dev/null 2>&1; then
|
||||||
prof="$d/docker/.host-profile"
|
prof="$d/docker/.host-profile"
|
||||||
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
|
[[ -f "$prof" ]] || prof="$d/docker/profiles/2c8g.env"
|
||||||
if [[ -f "$prof" ]]; then
|
if [[ -f "$prof" ]]; then
|
||||||
args=(-f "$d/docker/docker-compose.prod.yml")
|
args=(-f "$d/docker/docker-compose.prod.yml")
|
||||||
docker_cmd network inspect 1panel-network >/dev/null 2>&1 && \
|
docker network inspect 1panel-network >/dev/null 2>&1 && \
|
||||||
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
|
[[ -f "$d/docker/docker-compose.1panel.yml" ]] && \
|
||||||
args+=(-f "$d/docker/docker-compose.1panel.yml")
|
args+=(-f "$d/docker/docker-compose.1panel.yml")
|
||||||
q=$(cd "$d" && compose_cmd "${args[@]}" \
|
q=$(cd "$d" && docker compose "${args[@]}" \
|
||||||
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
|
--env-file "$d/docker/.env.prod" --env-file "$prof" ps -q nexus 2>/dev/null || true)
|
||||||
[[ -n "$q" ]] && echo "docker $d" && exit 0
|
[[ -n "$q" ]] && echo "docker $d" && exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ -f "$d/deploy/nexus-1panel.sh" ]]; then
|
|
||||||
q=$(docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E '^nexus-prod-nexus-' || true)
|
|
||||||
[[ -n "$q" ]] && echo "docker $d" && exit 0
|
|
||||||
fi
|
|
||||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
||||||
echo "supervisor $d"
|
echo "supervisor $d"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+19
-76
@@ -1,19 +1,16 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Full production deploy: local rsync + backend upgrade + frontend + health
|
# Full production deploy: push (optional) + backend upgrade + frontend + health
|
||||||
#
|
#
|
||||||
# 默认从本机工作区 rsync 到生产机,不 push Gitea、不远程 git pull。
|
# 自动识别远程 Docker(/opt/nexus)或 Supervisor(/www/wwwroot/...)运行时。
|
||||||
#
|
#
|
||||||
# Prerequisites on YOUR machine:
|
# Prerequisites on YOUR machine:
|
||||||
# - rsync, SSH Host nexus OR export NEXUS_SSH="azureuser@20.24.218.235"
|
# - SSH: ~/.ssh/config Host nexus OR export NEXUS_SSH="azureuser@20.24.218.235"
|
||||||
# - Key: export NEXUS_SSH_KEY=~/.ssh/id_rsa_nexus
|
# - Key: export NEXUS_SSH_KEY=~/.ssh/id_rsa_nexus
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# cd "$NEXUS_ROOT"
|
# cd "$NEXUS_ROOT"
|
||||||
# bash deploy/pre_deploy_check.sh
|
# bash deploy/pre_deploy_check.sh
|
||||||
# bash deploy/deploy-production.sh
|
# bash deploy/deploy-production.sh
|
||||||
#
|
|
||||||
# 仍走 Gitea(仅当显式设置):
|
|
||||||
# NEXUS_DEPLOY_VIA_GIT=1 bash deploy/deploy-production.sh
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -34,15 +31,8 @@ fi
|
|||||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
||||||
scp_cmd() { scp "${SSH_OPTS[@]}" "$@"; }
|
scp_cmd() { scp "${SSH_OPTS[@]}" "$@"; }
|
||||||
|
|
||||||
DEPLOY_VIA_GIT="${NEXUS_DEPLOY_VIA_GIT:-0}"
|
|
||||||
case "${DEPLOY_VIA_GIT}" in
|
|
||||||
1|true|yes|on) DEPLOY_VIA_GIT=1 ;;
|
|
||||||
*) DEPLOY_VIA_GIT=0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "═══ Nexus production deploy ═══"
|
echo "═══ Nexus production deploy ═══"
|
||||||
echo "SSH target: ${NEXUS_SSH_HOST}"
|
echo "SSH target: ${NEXUS_SSH_HOST}"
|
||||||
echo "Mode: $([[ "$DEPLOY_VIA_GIT" == 1 ]] && echo 'Gitea git pull' || echo 'local rsync')"
|
|
||||||
|
|
||||||
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
||||||
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
||||||
@@ -104,72 +94,27 @@ fi
|
|||||||
echo "Deploy path: ${DEPLOY_PATH}"
|
echo "Deploy path: ${DEPLOY_PATH}"
|
||||||
echo "Runtime: ${RUNTIME}"
|
echo "Runtime: ${RUNTIME}"
|
||||||
|
|
||||||
resolve_deploy_chown() {
|
if [[ "$RUNTIME" == "docker" ]]; then
|
||||||
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
|
echo "▶ Docker: git pull + 镜像重建(前端在镜像内 vite build)..."
|
||||||
echo "${NEXUS_DEPLOY_CHOWN}"
|
ssh_cmd "cd ${DEPLOY_PATH} && sudo git fetch --all && sudo git reset --hard origin/main && \
|
||||||
return
|
sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
|
||||||
fi
|
elif [[ "$RUNTIME" == "supervisor" ]]; then
|
||||||
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
|
echo "▶ Supervisor: git pull + restart..."
|
||||||
echo "www:www"
|
ssh_cmd "cd ${DEPLOY_PATH} && git fetch --all && git reset --hard origin/main && supervisorctl restart nexus"
|
||||||
return
|
|
||||||
fi
|
|
||||||
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
|
|
||||||
}
|
|
||||||
DEPLOY_CHOWN="$(resolve_deploy_chown)"
|
|
||||||
echo "File owner: ${DEPLOY_CHOWN}"
|
|
||||||
|
|
||||||
if [[ "$DEPLOY_VIA_GIT" == 1 ]]; then
|
echo "▶ Frontend: build + upload..."
|
||||||
echo "▶ Git mode: remote git pull..."
|
|
||||||
if [[ "$RUNTIME" == "docker" ]]; then
|
|
||||||
ssh_cmd "cd ${DEPLOY_PATH} && sudo git fetch --all && sudo git reset --hard origin/main && \
|
|
||||||
sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
|
|
||||||
elif [[ "$RUNTIME" == "supervisor" ]]; then
|
|
||||||
ssh_cmd "cd ${DEPLOY_PATH} && git fetch --all && git reset --hard origin/main && supervisorctl restart nexus"
|
|
||||||
else
|
|
||||||
echo "ERROR: 无法识别远程运行时" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "▶ Local sync → ${DEPLOY_PATH}..."
|
|
||||||
NEXUS_DEPLOY_PATH="${DEPLOY_PATH}" bash "${ROOT}/deploy/rsync-local-to-server.sh"
|
|
||||||
|
|
||||||
if [[ "$RUNTIME" == "docker" ]]; then
|
|
||||||
echo "▶ Docker: 重建镜像(--skip-git)..."
|
|
||||||
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade --skip-git"
|
|
||||||
elif [[ "$RUNTIME" == "supervisor" ]]; then
|
|
||||||
echo "▶ Supervisor: restart..."
|
|
||||||
ssh_cmd "supervisorctl restart nexus"
|
|
||||||
else
|
|
||||||
echo "ERROR: 无法识别远程运行时(非 Docker 也非 Supervisor)" >&2
|
|
||||||
echo " 请手动: ssh ${NEXUS_SSH_HOST} 'sudo nx update'" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$RUNTIME" == "docker" || "$RUNTIME" == "supervisor" ]]; then
|
|
||||||
echo "▶ Frontend: local vite build + upload to host web/app..."
|
|
||||||
cd frontend && npx vite build && cd ..
|
cd frontend && npx vite build && cd ..
|
||||||
tar czf /tmp/nexus-frontend.tar.gz -C web/app index.html assets/
|
tar czf /tmp/nexus-frontend.tar.gz -C web/app index.html assets/
|
||||||
scp_cmd /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
|
scp_cmd /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
|
||||||
ssh_cmd "cd ${DEPLOY_PATH}/web/app && sudo tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz && sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app"
|
ssh_cmd "cd ${DEPLOY_PATH}/web/app && tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz"
|
||||||
|
|
||||||
echo "▶ Frontend V2: local vite build:safe + upload to host web/app-v2..."
|
|
||||||
cd frontend-v2 && npm run build:safe && cd ..
|
|
||||||
tar czf /tmp/nexus-frontend-v2.tar.gz -C web/app-v2 index.html assets/
|
|
||||||
scp_cmd /tmp/nexus-frontend-v2.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend-v2.tar.gz"
|
|
||||||
ssh_cmd "sudo mkdir -p ${DEPLOY_PATH}/web/app-v2 && cd ${DEPLOY_PATH}/web/app-v2 && sudo tar xzf /tmp/nexus-frontend-v2.tar.gz && rm /tmp/nexus-frontend-v2.tar.gz && sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app-v2"
|
|
||||||
|
|
||||||
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
|
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
|
||||||
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app --dry-run"
|
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py --dry-run"
|
||||||
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
|
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"
|
||||||
fi
|
fi
|
||||||
fi
|
else
|
||||||
|
echo "ERROR: 无法识别远程运行时(非 Docker 也非 Supervisor)" >&2
|
||||||
if [[ "$RUNTIME" == "docker" ]]; then
|
echo " 请手动: ssh ${NEXUS_SSH_HOST} 'sudo nx update'" >&2
|
||||||
echo "▶ Sync host web/app and web/app-v2 into container..."
|
exit 1
|
||||||
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} NEXUS_PUBLISH_PORT=\$(grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod 2>/dev/null | cut -d= -f2 | tr -d '\"' || echo 8600) bash ${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" || {
|
|
||||||
echo "WARN: web/app sync failed — run sync_webapp_to_container.sh on server manually" >&2
|
|
||||||
}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 3
|
sleep 3
|
||||||
@@ -177,11 +122,9 @@ PORT="$(ssh_cmd "grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod
|
|||||||
PORT="${PORT:-8600}"
|
PORT="${PORT:-8600}"
|
||||||
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:${PORT}/health" 2>/dev/null || true)
|
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:${PORT}/health" 2>/dev/null || true)
|
||||||
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app/" 2>/dev/null || echo "000")
|
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app/" 2>/dev/null || echo "000")
|
||||||
SPA_V2=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app-v2/" 2>/dev/null || echo "000")
|
|
||||||
echo " /health → ${HEALTH}"
|
echo " /health → ${HEALTH}"
|
||||||
echo " /app/ → ${SPA}"
|
echo " /app/ → ${SPA}"
|
||||||
echo " /app-v2/ → ${SPA_V2}"
|
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" ]]; then
|
||||||
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" && "${SPA_V2}" == "200" ]]; then
|
|
||||||
echo "✓ Deploy successful"
|
echo "✓ Deploy successful"
|
||||||
if ssh_cmd "command -v crontab >/dev/null && crontab -l 2>/dev/null | grep -q nexus-health-monitor" 2>/dev/null; then
|
if ssh_cmd "command -v crontab >/dev/null && crontab -l 2>/dev/null | grep -q nexus-health-monitor" 2>/dev/null; then
|
||||||
echo "✓ Ops cron (health_monitor) already installed"
|
echo "✓ Ops cron (health_monitor) already installed"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+20
-173
@@ -9,64 +9,12 @@ set -euo pipefail
|
|||||||
|
|
||||||
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-/opt/nexus}"
|
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-/opt/nexus}"
|
||||||
NEXUS_SERVICE="${NEXUS_SERVICE:-nexus}"
|
NEXUS_SERVICE="${NEXUS_SERVICE:-nexus}"
|
||||||
STATE_DIR=""
|
FAIL_FILE="/tmp/nexus_health_fail_count"
|
||||||
FAIL_FILE=""
|
LAST_RESTART_FILE="/tmp/nexus_health_last_restart"
|
||||||
LAST_RESTART_FILE=""
|
|
||||||
# Legacy paths (cron user may have created root-owned files under /tmp)
|
|
||||||
LEGACY_FAIL_FILE="/tmp/nexus_health_fail_count"
|
|
||||||
LEGACY_LAST_RESTART_FILE="/tmp/nexus_health_last_restart"
|
|
||||||
MAX_FAIL=3
|
MAX_FAIL=3
|
||||||
RESTART_COOLDOWN_SEC="${NEXUS_HEALTH_RESTART_COOLDOWN:-300}"
|
RESTART_COOLDOWN_SEC="${NEXUS_HEALTH_RESTART_COOLDOWN:-300}"
|
||||||
ENV_PROD="${DEPLOY_DIR}/docker/.env.prod"
|
ENV_PROD="${DEPLOY_DIR}/docker/.env.prod"
|
||||||
|
|
||||||
ensure_state_paths() {
|
|
||||||
if [[ -n "$STATE_DIR" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local preferred="${DEPLOY_DIR}/var/layer3-health"
|
|
||||||
local fallback="${HOME:-/tmp}/.nexus/layer3-health"
|
|
||||||
if mkdir -p "$preferred" 2>/dev/null && [[ -w "$preferred" ]]; then
|
|
||||||
STATE_DIR="$preferred"
|
|
||||||
elif command -v sudo >/dev/null 2>&1 && sudo mkdir -p "$preferred" 2>/dev/null \
|
|
||||||
&& sudo chown -R "$(id -un):$(id -gn)" "$preferred" 2>/dev/null \
|
|
||||||
&& [[ -w "$preferred" ]]; then
|
|
||||||
STATE_DIR="$preferred"
|
|
||||||
else
|
|
||||||
mkdir -p "$fallback"
|
|
||||||
STATE_DIR="$fallback"
|
|
||||||
fi
|
|
||||||
FAIL_FILE="${STATE_DIR}/fail_count"
|
|
||||||
LAST_RESTART_FILE="${STATE_DIR}/last_restart"
|
|
||||||
}
|
|
||||||
|
|
||||||
docker_cmd() {
|
|
||||||
if docker "$@" 2>/dev/null; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if command -v sudo >/dev/null 2>&1; then
|
|
||||||
sudo docker "$@" 2>/dev/null
|
|
||||||
else
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
init_layer3_state() {
|
|
||||||
ensure_state_paths
|
|
||||||
chmod 755 "$STATE_DIR" 2>/dev/null || true
|
|
||||||
if [[ ! -f "$FAIL_FILE" && -f "$LEGACY_FAIL_FILE" ]]; then
|
|
||||||
cp "$LEGACY_FAIL_FILE" "$FAIL_FILE" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$LAST_RESTART_FILE" && -f "$LEGACY_LAST_RESTART_FILE" ]]; then
|
|
||||||
cp "$LEGACY_LAST_RESTART_FILE" "$LAST_RESTART_FILE" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
write_state_file() {
|
|
||||||
local path="$1" value="$2"
|
|
||||||
init_layer3_state
|
|
||||||
printf '%s\n' "$value" >"$path" 2>/dev/null || return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_publish_port() {
|
resolve_publish_port() {
|
||||||
local port
|
local port
|
||||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||||
@@ -77,7 +25,7 @@ PUBLISH_PORT="$(resolve_publish_port)"
|
|||||||
HEALTH_URL="http://127.0.0.1:${PUBLISH_PORT}/health"
|
HEALTH_URL="http://127.0.0.1:${PUBLISH_PORT}/health"
|
||||||
|
|
||||||
resolve_nexus_container() {
|
resolve_nexus_container() {
|
||||||
docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
read_env_var() {
|
read_env_var() {
|
||||||
@@ -85,24 +33,12 @@ read_env_var() {
|
|||||||
grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"' || true
|
grep -E "^${key}=" "$file" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r"' || true
|
||||||
}
|
}
|
||||||
|
|
||||||
normalize_env_value() {
|
|
||||||
local v="${1:-}"
|
|
||||||
v="${v//$'\r'/}"
|
|
||||||
if [[ ${#v} -ge 2 && ${v:0:1} == '"' && ${v: -1} == '"' ]]; then
|
|
||||||
v="${v:1:-1}"
|
|
||||||
v="${v//\\n/$'\n'}"
|
|
||||||
v="${v//\\\"/\"}"
|
|
||||||
v="${v//\\\\/\\}"
|
|
||||||
fi
|
|
||||||
echo "$v"
|
|
||||||
}
|
|
||||||
|
|
||||||
load_telegram_from_db() {
|
load_telegram_from_db() {
|
||||||
local cname="$1"
|
local cname="$1"
|
||||||
[[ -n "$cname" ]] || return 1
|
[[ -n "$cname" ]] || return 1
|
||||||
[[ -n "${BOT_TOKEN:-}" && -n "${CHAT_ID:-}" ]] && return 0
|
[[ -n "${BOT_TOKEN:-}" && -n "${CHAT_ID:-}" ]] && return 0
|
||||||
local out token chat
|
local out token chat
|
||||||
out="$(docker_cmd exec "$cname" python3 -c "
|
out="$(docker exec "$cname" python3 -c "
|
||||||
import asyncio
|
import asyncio
|
||||||
from server.config import settings as s
|
from server.config import settings as s
|
||||||
from server.infrastructure.database.session import AsyncSessionLocal
|
from server.infrastructure.database.session import AsyncSessionLocal
|
||||||
@@ -146,52 +82,41 @@ load_telegram_config() {
|
|||||||
NEXUS_TELEGRAM_BOT_TOKEN=*) BOT_TOKEN="${line#NEXUS_TELEGRAM_BOT_TOKEN=}" ;;
|
NEXUS_TELEGRAM_BOT_TOKEN=*) BOT_TOKEN="${line#NEXUS_TELEGRAM_BOT_TOKEN=}" ;;
|
||||||
NEXUS_TELEGRAM_CHAT_ID=*) CHAT_ID="${line#NEXUS_TELEGRAM_CHAT_ID=}" ;;
|
NEXUS_TELEGRAM_CHAT_ID=*) CHAT_ID="${line#NEXUS_TELEGRAM_CHAT_ID=}" ;;
|
||||||
esac
|
esac
|
||||||
done < <(docker_cmd exec "$cname" grep -E '^NEXUS_TELEGRAM_' /app/.env 2>/dev/null || true)
|
done < <(docker exec "$cname" grep -E '^NEXUS_TELEGRAM_' /app/.env 2>/dev/null || true)
|
||||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
|
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
|
||||||
load_telegram_from_db "$cname" || true
|
load_telegram_from_db "$cname" || true
|
||||||
fi
|
fi
|
||||||
BOT_TOKEN="$(normalize_env_value "$BOT_TOKEN")"
|
|
||||||
CHAT_ID="$(normalize_env_value "$CHAT_ID")"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send_telegram() {
|
send_telegram() {
|
||||||
local msg="${1}" resp http log="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
[[ -n "${BOT_TOKEN:-}" && -n "${CHAT_ID:-}" ]] || return 1
|
||||||
BOT_TOKEN="$(normalize_env_value "${BOT_TOKEN:-}")"
|
curl -sf --max-time 10 "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
||||||
CHAT_ID="$(normalize_env_value "${CHAT_ID:-}")"
|
|
||||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] || return 1
|
|
||||||
resp="$(curl -s --max-time 15 -w '\n%{http_code}' \
|
|
||||||
"https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
|
|
||||||
-d "chat_id=${CHAT_ID}" \
|
-d "chat_id=${CHAT_ID}" \
|
||||||
--data-urlencode "text=${msg}" \
|
-d "text=$1" \
|
||||||
-d "parse_mode=HTML" 2>/dev/null)" || return 1
|
-d "parse_mode=HTML" \
|
||||||
http="${resp##*$'\n'}"
|
> /dev/null 2>&1
|
||||||
if [[ "$http" != "200" ]]; then
|
|
||||||
echo "$(date -Is 2>/dev/null || date) telegram send failed http=${http}" >> "$log" 2>/dev/null || true
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bump_fail_count() {
|
bump_fail_count() {
|
||||||
local fail=0
|
local fail=0
|
||||||
init_layer3_state
|
|
||||||
if command -v flock >/dev/null 2>&1; then
|
if command -v flock >/dev/null 2>&1; then
|
||||||
(
|
(
|
||||||
flock -x 9
|
flock -x 9
|
||||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||||
fail=$((fail + 1))
|
fail=$((fail + 1))
|
||||||
write_state_file "$FAIL_FILE" "$fail" || true
|
echo "$fail" > "$FAIL_FILE"
|
||||||
) 9>"${FAIL_FILE}.lock"
|
) 9>"${FAIL_FILE}.lock"
|
||||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||||
else
|
else
|
||||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||||
fail=$((fail + 1))
|
fail=$((fail + 1))
|
||||||
write_state_file "$FAIL_FILE" "$fail" || true
|
echo "$fail" > "$FAIL_FILE"
|
||||||
fi
|
fi
|
||||||
echo "$fail"
|
echo "$fail"
|
||||||
}
|
}
|
||||||
|
|
||||||
reset_fail_count() {
|
reset_fail_count() {
|
||||||
write_state_file "$FAIL_FILE" "0" || true
|
echo "0" > "$FAIL_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
restart_cooldown_active() {
|
restart_cooldown_active() {
|
||||||
@@ -204,13 +129,13 @@ restart_cooldown_active() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mark_restart_attempt() {
|
mark_restart_attempt() {
|
||||||
write_state_file "$LAST_RESTART_FILE" "$(date +%s)" || true
|
date +%s > "$LAST_RESTART_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
restart_nexus() {
|
restart_nexus() {
|
||||||
local cname="$1" via=""
|
local cname="$1" via=""
|
||||||
if [[ -n "$cname" ]]; then
|
if [[ -n "$cname" ]]; then
|
||||||
docker_cmd restart "$cname" >/dev/null 2>&1 && via="Docker 容器 ${cname}"
|
docker restart "$cname" >/dev/null 2>&1 && via="Docker 容器 ${cname}"
|
||||||
elif command -v supervisorctl >/dev/null 2>&1 && supervisorctl status "$NEXUS_SERVICE" >/dev/null 2>&1; then
|
elif command -v supervisorctl >/dev/null 2>&1 && supervisorctl status "$NEXUS_SERVICE" >/dev/null 2>&1; then
|
||||||
supervisorctl restart "$NEXUS_SERVICE" >/dev/null 2>&1 && via="Supervisor ${NEXUS_SERVICE}"
|
supervisorctl restart "$NEXUS_SERVICE" >/dev/null 2>&1 && via="Supervisor ${NEXUS_SERVICE}"
|
||||||
else
|
else
|
||||||
@@ -219,86 +144,8 @@ restart_nexus() {
|
|||||||
echo "$via"
|
echo "$via"
|
||||||
}
|
}
|
||||||
|
|
||||||
telegram_health_fail_cooldown() {
|
|
||||||
local body
|
|
||||||
body="$(cat <<EOF
|
|
||||||
🔴 <b>【Layer 3 宿主机巡检】健康检查连续失败(冷却期内)</b>
|
|
||||||
宿主机:${HOST_LABEL}
|
|
||||||
检测地址:${HEALTH_URL}
|
|
||||||
连续失败:已达 ${MAX_FAIL} 次
|
|
||||||
Docker 容器:${NEXUS_CONTAINER:-未识别}
|
|
||||||
具体情况:/health 仍不可达,但距上次自动重启不足 ${RESTART_COOLDOWN_SEC} 秒,本次跳过重启以免频繁抖动
|
|
||||||
建议操作:查看 docker logs ${NEXUS_CONTAINER:-nexus} 与 ${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}
|
|
||||||
时间:${TS_LABEL}
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
send_telegram "$body" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
telegram_health_fail_restart() {
|
|
||||||
local body
|
|
||||||
body="$(cat <<EOF
|
|
||||||
🔴 <b>【Layer 3 宿主机巡检】Nexus 连续 ${MAX_FAIL} 次无响应</b>
|
|
||||||
宿主机:${HOST_LABEL}
|
|
||||||
检测地址:${HEALTH_URL}
|
|
||||||
连续失败:${FAIL} 次(阈值 ${MAX_FAIL} 次)
|
|
||||||
Docker 容器:${NEXUS_CONTAINER:-未识别}
|
|
||||||
具体情况:宿主机 cron 无法访问 API 健康端点,平台可能已宕机或端口异常
|
|
||||||
正在执行:自动重启 Nexus 服务…
|
|
||||||
时间:${TS_LABEL}
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
send_telegram "$body" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
telegram_health_recovered() {
|
|
||||||
local via="$1" body
|
|
||||||
body="$(cat <<EOF
|
|
||||||
🟢 <b>【Layer 3 宿主机巡检】Nexus 已自动恢复</b>
|
|
||||||
宿主机:${HOST_LABEL}
|
|
||||||
检测地址:${HEALTH_URL}
|
|
||||||
恢复方式:${via}
|
|
||||||
具体情况:自动重启后 /health 检测通过,服务已恢复在线
|
|
||||||
时间:${TS_LABEL}
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
send_telegram "$body" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
telegram_health_still_down() {
|
|
||||||
local via="$1" body
|
|
||||||
body="$(cat <<EOF
|
|
||||||
🔴 <b>【Layer 3 宿主机巡检】自动重启后仍未恢复</b>
|
|
||||||
宿主机:${HOST_LABEL}
|
|
||||||
检测地址:${HEALTH_URL}
|
|
||||||
已尝试:${via}
|
|
||||||
具体情况:重启完成后 /health 仍失败,可能存在数据库/Redis 故障或配置错误
|
|
||||||
建议操作:立即 SSH 登录排查 docker logs、MySQL、Redis,需人工介入
|
|
||||||
时间:${TS_LABEL}
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
send_telegram "$body" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
telegram_health_restart_failed() {
|
|
||||||
local body
|
|
||||||
body="$(cat <<EOF
|
|
||||||
🔴 <b>【Layer 3 宿主机巡检】无法执行自动重启</b>
|
|
||||||
宿主机:${HOST_LABEL}
|
|
||||||
检测地址:${HEALTH_URL}
|
|
||||||
具体情况:未找到可重启的 Docker 容器或 Supervisor 服务(${NEXUS_SERVICE})
|
|
||||||
建议操作:确认 nexus 容器名称与 docker ps 输出,手动执行 docker restart 或 nx update
|
|
||||||
时间:${TS_LABEL}
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
send_telegram "$body" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
NEXUS_CONTAINER="$(resolve_nexus_container)"
|
NEXUS_CONTAINER="$(resolve_nexus_container)"
|
||||||
load_telegram_config "$NEXUS_CONTAINER"
|
load_telegram_config "$NEXUS_CONTAINER"
|
||||||
init_layer3_state
|
|
||||||
HOST_LABEL="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo unknown)"
|
|
||||||
TS_LABEL="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || date)"
|
|
||||||
|
|
||||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||||
reset_fail_count
|
reset_fail_count
|
||||||
@@ -311,21 +158,21 @@ if [[ "$FAIL" -lt "$MAX_FAIL" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if restart_cooldown_active; then
|
if restart_cooldown_active; then
|
||||||
telegram_health_fail_cooldown
|
send_telegram "🔴 <b>Nexus /health 连续失败</b>\n冷却期内跳过自动重启(${RESTART_COOLDOWN_SEC}s)\n请检查: docker logs ${NEXUS_CONTAINER:-nexus}" || true
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
telegram_health_fail_restart
|
send_telegram "🔴 <b>Nexus 连续 ${MAX_FAIL} 次无响应</b>\n正在自动重启…" || true
|
||||||
mark_restart_attempt
|
mark_restart_attempt
|
||||||
|
|
||||||
if VIA="$(restart_nexus "$NEXUS_CONTAINER")"; then
|
if VIA="$(restart_nexus "$NEXUS_CONTAINER")"; then
|
||||||
sleep 5
|
sleep 5
|
||||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||||
telegram_health_recovered "$VIA"
|
send_telegram "🟢 <b>Nexus 已恢复</b>\n${VIA} 重启成功" || true
|
||||||
reset_fail_count
|
reset_fail_count
|
||||||
else
|
else
|
||||||
telegram_health_still_down "$VIA"
|
send_telegram "🔴 <b>Nexus 重启后仍未恢复</b>\n${VIA} — 需人工介入" || true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
telegram_health_restart_failed
|
send_telegram "🔴 <b>Nexus 无法自动重启</b>\n未找到 Docker 容器或 Supervisor 服务" || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
+1
-1
@@ -416,7 +416,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Upload limit
|
# Upload limit
|
||||||
client_max_body_size 500m;
|
client_max_body_size 100m;
|
||||||
|
|
||||||
# Well-known for SSL cert validation
|
# Well-known for SSL cert validation
|
||||||
location ~ \\.well-known {
|
location ~ \\.well-known {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh
|
||||||
|
# 用于 git push(scripts/git-push.sh);clone/pull 仍可匿名
|
||||||
|
|
||||||
|
# NEXUS_GITEA_HOST='66.154.115.8:3000'
|
||||||
|
# NEXUS_GITEA_REPO='admin/Nexus.git'
|
||||||
|
# admin 账号密码或 Access Token:
|
||||||
|
# NEXUS_GITEA_TOKEN='你的密码或令牌'
|
||||||
|
|
||||||
|
# 新机安装勿填下面三项(install-nexus-fresh / --fresh 会自动按机生成)
|
||||||
|
# NEXUS_SECRET_KEY=''
|
||||||
|
# NEXUS_API_KEY=''
|
||||||
|
# NEXUS_ENCRYPTION_KEY=''
|
||||||
|
|
||||||
|
NEXUS_DOMAIN='api.synaglobal.vip'
|
||||||
|
NEXUS_ROOT='/opt/nexus'
|
||||||
|
NEXUS_PROFILE='2c8g'
|
||||||
|
NEXUS_FROM_ENV='/www/wwwroot/api.synaglobal.vip/.env'
|
||||||
|
|
||||||
|
# 本机生产 SSH(deploy-production.sh 读取;私钥路径勿提交真实 .pem)
|
||||||
|
# NEXUS_SSH='azureuser@20.24.218.235'
|
||||||
|
# NEXUS_SSH_KEY='/path/to/nz-admin.pem'
|
||||||
Regular → Executable
+6
-34
@@ -50,7 +50,6 @@ error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|||||||
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
|
||||||
|
|
||||||
SKIP_CLONE=false
|
SKIP_CLONE=false
|
||||||
SKIP_GIT=false
|
|
||||||
FRESH_INSTALL=false
|
FRESH_INSTALL=false
|
||||||
REUSE_SECRETS=false
|
REUSE_SECRETS=false
|
||||||
FROM_ENV="${NEXUS_FROM_ENV}"
|
FROM_ENV="${NEXUS_FROM_ENV}"
|
||||||
@@ -182,20 +181,15 @@ apply_pool_to_env_prod() {
|
|||||||
local root="$1" prof="$2"
|
local root="$1" prof="$2"
|
||||||
local env_file="$root/$ENV_PROD"
|
local env_file="$root/$ENV_PROD"
|
||||||
[[ -f "$env_file" ]] || return 0
|
[[ -f "$env_file" ]] || return 0
|
||||||
if [[ ! -f "$prof" ]]; then
|
|
||||||
warn "未找到资源档位文件: $prof,保留 $ENV_PROD 中现有数据库连接池配置"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
local pool overflow
|
local pool overflow
|
||||||
pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2- || true)"
|
pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2-)"
|
||||||
overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2- || true)"
|
overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2-)"
|
||||||
[[ -n "$pool" ]] && sed -i "s|^NEXUS_DB_POOL_SIZE=.*|NEXUS_DB_POOL_SIZE=${pool}|" "$env_file"
|
[[ -n "$pool" ]] && sed -i "s|^NEXUS_DB_POOL_SIZE=.*|NEXUS_DB_POOL_SIZE=${pool}|" "$env_file"
|
||||||
[[ -n "$overflow" ]] && sed -i "s|^NEXUS_DB_MAX_OVERFLOW=.*|NEXUS_DB_MAX_OVERFLOW=${overflow}|" "$env_file"
|
[[ -n "$overflow" ]] && sed -i "s|^NEXUS_DB_MAX_OVERFLOW=.*|NEXUS_DB_MAX_OVERFLOW=${overflow}|" "$env_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
has_1panel_network() {
|
has_1panel_network() {
|
||||||
docker network inspect 1panel-network >/dev/null 2>&1 \
|
docker network inspect 1panel-network >/dev/null 2>&1
|
||||||
|| sudo docker network inspect 1panel-network >/dev/null 2>&1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
upsert_env_var() {
|
upsert_env_var() {
|
||||||
@@ -1023,14 +1017,10 @@ verify_health() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ensure_repo() {
|
ensure_repo() {
|
||||||
if [[ ! -d "$NEXUS_ROOT" ]]; then
|
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
|
||||||
error "未找到 $NEXUS_ROOT,请先: bash $0 install"
|
error "未找到 $NEXUS_ROOT,请先: bash $0 install"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ "$SKIP_GIT" != true && ! -d "$NEXUS_ROOT/.git" ]]; then
|
|
||||||
error "未找到 $NEXUS_ROOT/.git,请先: bash $0 install;如为发布包/rsync 部署请使用 --skip-git"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
if [[ ! -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
|
||||||
error "缺少 $NEXUS_ROOT/$ENV_PROD"
|
error "缺少 $NEXUS_ROOT/$ENV_PROD"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -1147,12 +1137,8 @@ cmd_upgrade() {
|
|||||||
load_saved_profile "$NEXUS_ROOT"
|
load_saved_profile "$NEXUS_ROOT"
|
||||||
resolve_profile
|
resolve_profile
|
||||||
check_ports_preflight "$NEXUS_ROOT"
|
check_ports_preflight "$NEXUS_ROOT"
|
||||||
if [[ "$SKIP_GIT" == true ]]; then
|
git_fetch
|
||||||
info "跳过 Git 同步(本机 rsync 已推送代码)"
|
show_pending || true
|
||||||
else
|
|
||||||
git_fetch
|
|
||||||
show_pending || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$CHECK_ONLY" == true ]]; then
|
if [[ "$CHECK_ONLY" == true ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
@@ -1162,12 +1148,6 @@ cmd_upgrade() {
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$SKIP_GIT" == true ]]; then
|
|
||||||
info "本机直传模式:跳过 git reset,仍备份并重建镜像"
|
|
||||||
if [[ "$NO_BACKUP" != true ]]; then
|
|
||||||
backup_mysql "$NEXUS_ROOT"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
local behind
|
local behind
|
||||||
behind="$(commits_behind)"
|
behind="$(commits_behind)"
|
||||||
if [[ "$behind" != "0" ]]; then
|
if [[ "$behind" != "0" ]]; then
|
||||||
@@ -1179,7 +1159,6 @@ cmd_upgrade() {
|
|||||||
backup_mysql "$NEXUS_ROOT"
|
backup_mysql "$NEXUS_ROOT"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")"
|
apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")"
|
||||||
upsert_env_var "$NEXUS_ROOT/$ENV_PROD" "NEXUS_HOST_ROOT" "$NEXUS_ROOT"
|
upsert_env_var "$NEXUS_ROOT/$ENV_PROD" "NEXUS_HOST_ROOT" "$NEXUS_ROOT"
|
||||||
@@ -1226,12 +1205,6 @@ cmd_upgrade() {
|
|||||||
docker image prune -f >/dev/null 2>&1 || true
|
docker image prune -f >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/install-nx-cli.sh" 2>/dev/null || true
|
||||||
if [[ -x "$NEXUS_ROOT/deploy/sync_webapp_to_container.sh" ]]; then
|
|
||||||
step "同步 Git web/app 进容器(消除镜像 vite 缓存 hash 漂移)..."
|
|
||||||
NEXUS_PUBLISH_PORT="$port" NEXUS_ROOT="$NEXUS_ROOT" bash "$NEXUS_ROOT/deploy/sync_webapp_to_container.sh" || {
|
|
||||||
warn "web/app 同步失败,继续健康检查(请手动执行 sync_webapp_to_container.sh)"
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
if ! verify_health "$NEXUS_ROOT" "升级"; then
|
if ! verify_health "$NEXUS_ROOT" "升级"; then
|
||||||
rollback_compose_upgrade "$NEXUS_ROOT" || true
|
rollback_compose_upgrade "$NEXUS_ROOT" || true
|
||||||
exit 1
|
exit 1
|
||||||
@@ -1292,7 +1265,6 @@ parse_common_upgrade() {
|
|||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help) usage ;;
|
-h|--help) usage ;;
|
||||||
--skip-git) SKIP_GIT=true; shift ;;
|
|
||||||
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
|
||||||
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
|
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
|
||||||
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
|
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
|
||||||
|
|||||||
@@ -75,5 +75,5 @@ server {
|
|||||||
error_log {{LOG_DIR}}/{{SERVER_NAME}}_nexus.error.log;
|
error_log {{LOG_DIR}}/{{SERVER_NAME}}_nexus.error.log;
|
||||||
|
|
||||||
# ── Upload limit ──
|
# ── Upload limit ──
|
||||||
client_max_body_size 500m;
|
client_max_body_size 100m;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,5 +109,5 @@ server {
|
|||||||
error_log {{LOG_DIR}}/{{SERVER_NAME}}.error.log;
|
error_log {{LOG_DIR}}/{{SERVER_NAME}}.error.log;
|
||||||
|
|
||||||
# ── Upload limit ──
|
# ── Upload limit ──
|
||||||
client_max_body_size 500m;
|
client_max_body_size 100m;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-69
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Nexus Pre-deploy Gate Check (v3 — 8 gates)
|
# Nexus Pre-deploy Gate Check (v2 — 7 gates)
|
||||||
# 在部署前强制检查 8 道门控,任何一道不过则阻止部署
|
# 在部署前强制检查 7 道门控,任何一道不过则阻止部署
|
||||||
#
|
#
|
||||||
# 门控1: CHANGELOG — docs/changelog/ 下必须有今天的 .md 文件(且行数≥10)
|
# 门控1: CHANGELOG — docs/changelog/ 下必须有今天的 .md 文件(且行数≥10)
|
||||||
# 门控2: AUDIT — docs/audit/ 下必须有今天的审计记录(且包含关键段落)
|
# 门控2: AUDIT — docs/audit/ 下必须有今天的审计记录(且包含关键段落)
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
# 门控5: IMPORT — python -c "import server.main" 成功
|
# 门控5: IMPORT — python -c "import server.main" 成功
|
||||||
# 门控6: SECURITY — bandit -r server/ 无 HIGH/MEDIUM
|
# 门控6: SECURITY — bandit -r server/ 无 HIGH/MEDIUM
|
||||||
# 门控7: REVIEW — 审计文件包含 Closure表+文件清单+DoD,且文件清单与 git diff 交叉验证
|
# 门控7: REVIEW — 审计文件包含 Closure表+文件清单+DoD,且文件清单与 git diff 交叉验证
|
||||||
# 门控8: AI_REVIEW — cursor-agent 审查 HEAD~1..HEAD;缓存 docs/reviews/<head_sha>.json
|
|
||||||
#
|
#
|
||||||
# 用法: bash deploy/pre_deploy_check.sh
|
# 用法: bash deploy/pre_deploy_check.sh
|
||||||
# 退出码: 0=全部通过, 1=至少一道门未过
|
# 退出码: 0=全部通过, 1=至少一道门未过
|
||||||
@@ -23,7 +22,7 @@ DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
|
|||||||
TODAY=$(date +%Y-%m-%d)
|
TODAY=$(date +%Y-%m-%d)
|
||||||
TIMESTAMP=$(date -Iseconds)
|
TIMESTAMP=$(date -Iseconds)
|
||||||
GATES_PASSED=0
|
GATES_PASSED=0
|
||||||
GATES_TOTAL=8
|
GATES_TOTAL=7
|
||||||
BLOCKED=0
|
BLOCKED=0
|
||||||
GATE_RESULTS=""
|
GATE_RESULTS=""
|
||||||
|
|
||||||
@@ -54,14 +53,14 @@ gate_log() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
echo " Nexus Pre-deploy Gate Check (v3)"
|
echo " Nexus Pre-deploy Gate Check (v2)"
|
||||||
echo " Date: ${TODAY}"
|
echo " Date: ${TODAY}"
|
||||||
echo " Gates: ${GATES_TOTAL}"
|
echo " Gates: ${GATES_TOTAL}"
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Gate 1: Changelog (存在 + 行数≥10) ──
|
# ── Gate 1: Changelog (存在 + 行数≥10) ──
|
||||||
echo -n "Gate 1/8: Changelog ... "
|
echo -n "Gate 1/7: Changelog ... "
|
||||||
CHANGELOG_DIR="${DEPLOY_DIR}/docs/changelog"
|
CHANGELOG_DIR="${DEPLOY_DIR}/docs/changelog"
|
||||||
if ls "${CHANGELOG_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
if ls "${CHANGELOG_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||||
FILE=$(ls -t "${CHANGELOG_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
FILE=$(ls -t "${CHANGELOG_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||||
@@ -87,7 +86,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Gate 2: Audit (存在 + 关键段落) ──
|
# ── Gate 2: Audit (存在 + 关键段落) ──
|
||||||
echo -n "Gate 2/8: Audit ... "
|
echo -n "Gate 2/7: Audit ... "
|
||||||
AUDIT_DIR="${DEPLOY_DIR}/docs/audit"
|
AUDIT_DIR="${DEPLOY_DIR}/docs/audit"
|
||||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||||
@@ -118,7 +117,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Gate 3: Test (必须存在 + 必须通过) ──
|
# ── Gate 3: Test (必须存在 + 必须通过) ──
|
||||||
echo -n "Gate 3/8: Test ... "
|
echo -n "Gate 3/7: Test ... "
|
||||||
TEST_SCRIPT="${DEPLOY_DIR}/tests/test_api.py"
|
TEST_SCRIPT="${DEPLOY_DIR}/tests/test_api.py"
|
||||||
if [ ! -f "${TEST_SCRIPT}" ]; then
|
if [ ! -f "${TEST_SCRIPT}" ]; then
|
||||||
echo -e "${RED}BLOCK${NC}"
|
echo -e "${RED}BLOCK${NC}"
|
||||||
@@ -127,43 +126,15 @@ if [ ! -f "${TEST_SCRIPT}" ]; then
|
|||||||
BLOCKED=1
|
BLOCKED=1
|
||||||
gate_log "test" "BLOCK" "script not found"
|
gate_log "test" "BLOCK" "script not found"
|
||||||
else
|
else
|
||||||
# Always hit local API for gate 3 (ignore shell NEXUS_TEST_BASE used by import/e2e scripts)
|
TEST_OUTPUT=$(cd "${DEPLOY_DIR}" && python3 "${TEST_SCRIPT}" 2>&1) || true
|
||||||
TEST_OUTPUT=$(cd "${DEPLOY_DIR}" && env -u NEXUS_TEST_BASE NEXUS_TEST_BASE=http://127.0.0.1:8600 python3 "${TEST_SCRIPT}" 2>&1) || true
|
|
||||||
FAIL_COUNT=$(echo "${TEST_OUTPUT}" | grep -cE "^\s+\[FAIL\]" 2>/dev/null || true)
|
FAIL_COUNT=$(echo "${TEST_OUTPUT}" | grep -cE "^\s+\[FAIL\]" 2>/dev/null || true)
|
||||||
FAIL_COUNT=${FAIL_COUNT:-0}
|
FAIL_COUNT=${FAIL_COUNT:-0}
|
||||||
GATE3_PASSED=0
|
|
||||||
if [ "${FAIL_COUNT}" -gt 0 ] || echo "${TEST_OUTPUT}" | grep -qE "[0-9]+ test(s) failed"; then
|
if [ "${FAIL_COUNT}" -gt 0 ] || echo "${TEST_OUTPUT}" | grep -qE "[0-9]+ test(s) failed"; then
|
||||||
# 本机未起 :8600 时,若失败均为 Connection refused,回退 pytest 专项
|
echo -e "${RED}BLOCK${NC}"
|
||||||
NON_CONN_FAILS=$(echo "${TEST_OUTPUT}" | grep -E "^\s+\[FAIL\]" | grep -vcE "Connection refused|Errno 111" 2>/dev/null || true)
|
echo " └─ Tests failed. Output:"
|
||||||
NON_CONN_FAILS=${NON_CONN_FAILS:-0}
|
echo "${TEST_OUTPUT}" | head -20 | sed 's/^/ │ /'
|
||||||
if [ "${FAIL_COUNT}" -gt 0 ] && [ "${NON_CONN_FAILS}" -eq 0 ]; then
|
BLOCKED=1
|
||||||
PYTEST_BIN=$(_pick_bin pytest || true)
|
gate_log "test" "BLOCK" "tests failed"
|
||||||
FALLBACK_TESTS="tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py tests/test_gate_ai_review.py"
|
|
||||||
if [ -n "${PYTEST_BIN}" ]; then
|
|
||||||
echo -e "${YELLOW}fallback${NC}"
|
|
||||||
echo " └─ Local :8600 unreachable — running pytest gate batch"
|
|
||||||
FALLBACK_OUTPUT=$(cd "${DEPLOY_DIR}" && "${PYTEST_BIN}" ${FALLBACK_TESTS} -q 2>&1) || FALLBACK_RC=$?
|
|
||||||
FALLBACK_RC=${FALLBACK_RC:-0}
|
|
||||||
if [ "${FALLBACK_RC}" -eq 0 ]; then
|
|
||||||
echo -e " └─ ${GREEN}PASS${NC} (pytest fallback)"
|
|
||||||
GATES_PASSED=$((GATES_PASSED + 1))
|
|
||||||
gate_log "test" "PASS" "pytest fallback :8600 down"
|
|
||||||
GATE3_PASSED=1
|
|
||||||
else
|
|
||||||
echo -e " └─ ${RED}BLOCK${NC} pytest fallback failed:"
|
|
||||||
echo "${FALLBACK_OUTPUT}" | tail -15 | sed 's/^/ │ /'
|
|
||||||
BLOCKED=1
|
|
||||||
gate_log "test" "BLOCK" "pytest fallback failed"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ "${GATE3_PASSED}" -eq 0 ]; then
|
|
||||||
echo -e "${RED}BLOCK${NC}"
|
|
||||||
echo " └─ Tests failed. Output:"
|
|
||||||
echo "${TEST_OUTPUT}" | head -20 | sed 's/^/ │ /'
|
|
||||||
BLOCKED=1
|
|
||||||
gate_log "test" "BLOCK" "tests failed"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo -e "${GREEN}PASS${NC}"
|
echo -e "${GREEN}PASS${NC}"
|
||||||
echo " └─ All tests passed"
|
echo " └─ All tests passed"
|
||||||
@@ -173,7 +144,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Gate 4: Lint (ruff — F only: undefined names, unused imports) ──
|
# ── Gate 4: Lint (ruff — F only: undefined names, unused imports) ──
|
||||||
echo -n "Gate 4/8: Lint ... "
|
echo -n "Gate 4/7: Lint ... "
|
||||||
RUFF_BIN=$(_pick_bin ruff || true)
|
RUFF_BIN=$(_pick_bin ruff || true)
|
||||||
if [ -n "${RUFF_BIN}" ]; then
|
if [ -n "${RUFF_BIN}" ]; then
|
||||||
LINT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${RUFF_BIN}" check server/ --select F 2>&1) || true
|
LINT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${RUFF_BIN}" check server/ --select F 2>&1) || true
|
||||||
@@ -200,7 +171,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Gate 5: Import (python -c "import server.main") ──
|
# ── Gate 5: Import (python -c "import server.main") ──
|
||||||
echo -n "Gate 5/8: Import ... "
|
echo -n "Gate 5/7: Import ... "
|
||||||
IMPORT_PYTHON=""
|
IMPORT_PYTHON=""
|
||||||
for p in "${DEPLOY_DIR}/.venv/bin/python3" "${DEPLOY_DIR}/venv/bin/python3"; do
|
for p in "${DEPLOY_DIR}/.venv/bin/python3" "${DEPLOY_DIR}/venv/bin/python3"; do
|
||||||
if [ -x "${p}" ]; then
|
if [ -x "${p}" ]; then
|
||||||
@@ -235,7 +206,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Gate 6: Security (bandit — HIGH only) ──
|
# ── Gate 6: Security (bandit — HIGH only) ──
|
||||||
echo -n "Gate 6/8: Security ... "
|
echo -n "Gate 6/7: Security ... "
|
||||||
BANDIT_BIN=$(_pick_bin bandit || true)
|
BANDIT_BIN=$(_pick_bin bandit || true)
|
||||||
if [ -n "${BANDIT_BIN}" ]; then
|
if [ -n "${BANDIT_BIN}" ]; then
|
||||||
SEC_OUTPUT=$(cd "${DEPLOY_DIR}" && "${BANDIT_BIN}" -r server/ -f txt -ll 2>&1) || true
|
SEC_OUTPUT=$(cd "${DEPLOY_DIR}" && "${BANDIT_BIN}" -r server/ -f txt -ll 2>&1) || true
|
||||||
@@ -262,18 +233,18 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Gate 7: Review (审计文件交叉验证) ──
|
# ── Gate 7: Review (审计文件交叉验证) ──
|
||||||
echo -n "Gate 7/8: Review ... "
|
echo -n "Gate 7/7: Review ... "
|
||||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||||
# 检查审计文件中是否列出了实际改动的文件
|
# 检查审计文件中是否列出了实际改动的文件
|
||||||
REVIEW_OK=1
|
REVIEW_OK=1
|
||||||
# 如果是git仓库,交叉验证改动文件
|
# 如果是git仓库,交叉验证改动文件
|
||||||
if command -v git &>/dev/null && [ -d "${DEPLOY_DIR}/.git" ]; then
|
if command -v git &>/dev/null && [ -d "${DEPLOY_DIR}/.git" ]; then
|
||||||
CHANGED_FILES=$(cd "${DEPLOY_DIR}" && git diff --name-only HEAD~1..HEAD 2>/dev/null || true)
|
CHANGED_FILES=$(cd "${DEPLOY_DIR}" && git diff --name-only HEAD~1 2>/dev/null || true)
|
||||||
for CF in ${CHANGED_FILES}; do
|
for CF in ${CHANGED_FILES}; do
|
||||||
# 跳过非代码文件与门控运行日志
|
# 跳过非代码文件
|
||||||
case "${CF}" in
|
case "${CF}" in
|
||||||
docs/*|CLAUDE.md|*.md|deploy/gate_log.jsonl|web/app/assets/*) continue ;;
|
docs/*|CLAUDE.md|*.md) continue ;;
|
||||||
esac
|
esac
|
||||||
# 检查审计文件是否提到了这个文件
|
# 检查审计文件是否提到了这个文件
|
||||||
if ! grep -q "$(basename "${CF}")" "${FILE}" 2>/dev/null; then
|
if ! grep -q "$(basename "${CF}")" "${FILE}" 2>/dev/null; then
|
||||||
@@ -301,26 +272,6 @@ else
|
|||||||
gate_log "review" "BLOCK" "no audit file"
|
gate_log "review" "BLOCK" "no audit file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Gate 8: AI Review (cursor-agent + docs/reviews cache) ──
|
|
||||||
echo -n "Gate 8/8: AI Review ... "
|
|
||||||
AI_REVIEW_OUT=$(cd "${DEPLOY_DIR}" && python3 "${DEPLOY_DIR}/scripts/gate_ai_review.py" 2>&1) || AI_REVIEW_RC=$?
|
|
||||||
AI_REVIEW_RC=${AI_REVIEW_RC:-0}
|
|
||||||
if [ "${AI_REVIEW_RC}" -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}PASS${NC}"
|
|
||||||
echo "${AI_REVIEW_OUT}" | sed 's/^/ └─ /' | head -5
|
|
||||||
GATES_PASSED=$((GATES_PASSED + 1))
|
|
||||||
if echo "${AI_REVIEW_OUT}" | grep -qi "skip"; then
|
|
||||||
gate_log "ai_review" "PASS" "skipped no code"
|
|
||||||
else
|
|
||||||
gate_log "ai_review" "PASS" "ok"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "${RED}BLOCK${NC}"
|
|
||||||
echo "${AI_REVIEW_OUT}" | sed 's/^/ │ /' | head -12
|
|
||||||
BLOCKED=1
|
|
||||||
gate_log "ai_review" "BLOCK" "see gate_ai_review.py output"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Pre-flight: shell scripts LF ──
|
# ── Pre-flight: shell scripts LF ──
|
||||||
echo -n "Pre-flight: Shell EOL (LF) ... "
|
echo -n "Pre-flight: Shell EOL (LF) ... "
|
||||||
if bash "${DEPLOY_DIR}/deploy/check_shell_eol.sh" >/dev/null 2>&1; then
|
if bash "${DEPLOY_DIR}/deploy/check_shell_eol.sh" >/dev/null 2>&1; then
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Nexus production host preflight before applying a release tarball.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# bash deploy/preflight-release-host.sh \
|
|
||||||
# --tarball /tmp/nexus-release-20260708.tar.gz \
|
|
||||||
# --sha256 a4eb8f35f0b98d40c1ad83117bfc652e7686ffc7d55f6bf90fcfd433a2ad6701 \
|
|
||||||
# --deploy-path /opt/nexus
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
TARBALL=""
|
|
||||||
EXPECTED_SHA256=""
|
|
||||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
|
|
||||||
MIN_FREE_MB="${NEXUS_RELEASE_MIN_FREE_MB:-1024}"
|
|
||||||
|
|
||||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
|
||||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
|
||||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
sed -n '1,12p' "$0"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--tarball)
|
|
||||||
TARBALL="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--sha256)
|
|
||||||
EXPECTED_SHA256="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--deploy-path)
|
|
||||||
DEPLOY_PATH="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--min-free-mb)
|
|
||||||
MIN_FREE_MB="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
error "Unknown argument: $1"
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
check_cmd() {
|
|
||||||
local cmd="$1"
|
|
||||||
if command -v "$cmd" >/dev/null 2>&1; then
|
|
||||||
info "command OK: $cmd"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
error "missing command: $cmd"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
docker_cmd() {
|
|
||||||
if docker "$@" 2>/dev/null; then return 0; fi
|
|
||||||
sudo docker "$@" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_runtime() {
|
|
||||||
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
|
|
||||||
echo docker
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
|
|
||||||
echo supervisor
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_tarball() {
|
|
||||||
if [[ -z "$TARBALL" ]]; then
|
|
||||||
warn "--tarball not provided; skipping tarball checksum/content checks"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
[[ -f "$TARBALL" ]] || {
|
|
||||||
error "tarball not found: $TARBALL"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
info "tarball exists: $TARBALL"
|
|
||||||
if [[ -n "$EXPECTED_SHA256" ]]; then
|
|
||||||
local actual
|
|
||||||
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
|
|
||||||
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
|
|
||||||
error "SHA256 mismatch: expected=$EXPECTED_SHA256 actual=$actual"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
info "SHA256 OK: $actual"
|
|
||||||
else
|
|
||||||
warn "--sha256 not provided; checksum not verified"
|
|
||||||
fi
|
|
||||||
tar tzf "$TARBALL" nexus-release/server nexus-release/deploy nexus-release/web/app-v2 >/dev/null
|
|
||||||
info "tarball required paths OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_deploy_path() {
|
|
||||||
if [[ -d "$DEPLOY_PATH" ]]; then
|
|
||||||
info "deploy path exists: $DEPLOY_PATH"
|
|
||||||
else
|
|
||||||
warn "deploy path does not exist yet: $DEPLOY_PATH"
|
|
||||||
fi
|
|
||||||
if [[ -f "${DEPLOY_PATH}/.env" ]]; then
|
|
||||||
info "runtime .env preserved path exists"
|
|
||||||
else
|
|
||||||
warn "${DEPLOY_PATH}/.env not found"
|
|
||||||
fi
|
|
||||||
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]]; then
|
|
||||||
info "docker/.env.prod exists"
|
|
||||||
else
|
|
||||||
warn "${DEPLOY_PATH}/docker/.env.prod not found"
|
|
||||||
fi
|
|
||||||
if [[ -d "${DEPLOY_PATH}/web/data" ]]; then
|
|
||||||
info "web/data exists"
|
|
||||||
else
|
|
||||||
warn "${DEPLOY_PATH}/web/data not found"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_disk_space() {
|
|
||||||
local target free_mb
|
|
||||||
target="$DEPLOY_PATH"
|
|
||||||
[[ -e "$target" ]] || target="$(dirname "$DEPLOY_PATH")"
|
|
||||||
free_mb="$(df -Pm "$target" | awk 'NR==2 {print $4}')"
|
|
||||||
if [[ -z "$free_mb" ]]; then
|
|
||||||
warn "unable to determine free disk space for $target"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if (( free_mb < MIN_FREE_MB )); then
|
|
||||||
error "low disk space on $target: ${free_mb}MB < ${MIN_FREE_MB}MB"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
info "disk free OK: ${free_mb}MB >= ${MIN_FREE_MB}MB"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_runtime() {
|
|
||||||
local runtime
|
|
||||||
runtime="$(detect_runtime)"
|
|
||||||
info "runtime detected: $runtime"
|
|
||||||
case "$runtime" in
|
|
||||||
docker)
|
|
||||||
docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' >/dev/null \
|
|
||||||
&& info "Nexus docker container found" \
|
|
||||||
|| warn "Nexus docker container not found by known name pattern"
|
|
||||||
;;
|
|
||||||
supervisor)
|
|
||||||
supervisorctl status nexus || true
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
warn "runtime unknown; apply script may require manual restart"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
check_local_endpoint() {
|
|
||||||
local port health app app_v2
|
|
||||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
|
||||||
port="${port:-8600}"
|
|
||||||
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
|
|
||||||
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
|
|
||||||
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
|
|
||||||
echo " current /health -> ${health:-<empty>}"
|
|
||||||
echo " current /app/ -> ${app}"
|
|
||||||
echo " current /app-v2/ -> ${app_v2}"
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local failures=0
|
|
||||||
for cmd in tar sha256sum df awk grep sed curl; do
|
|
||||||
check_cmd "$cmd" || failures=$((failures + 1))
|
|
||||||
done
|
|
||||||
check_cmd rsync || failures=$((failures + 1))
|
|
||||||
|
|
||||||
verify_tarball || failures=$((failures + 1))
|
|
||||||
check_deploy_path
|
|
||||||
check_disk_space || failures=$((failures + 1))
|
|
||||||
check_runtime
|
|
||||||
check_local_endpoint
|
|
||||||
|
|
||||||
if (( failures > 0 )); then
|
|
||||||
error "preflight failed: ${failures} blocking issue(s)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
info "preflight OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
Regular → Executable
@@ -1,112 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Sync local Nexus working tree to production host (no Gitea push/pull).
|
|
||||||
# Transfer via tar|ssh; remote ownership auto-detected (www:www on BT, else deploy path owner).
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# bash deploy/rsync-local-to-server.sh
|
|
||||||
# NEXUS_DEPLOY_PATH=/opt/nexus bash deploy/rsync-local-to-server.sh
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
cd "${ROOT}"
|
|
||||||
|
|
||||||
SECRETS="${ROOT}/deploy/nexus-1panel.secrets.sh"
|
|
||||||
if [[ -f "$SECRETS" ]]; then
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source "$SECRETS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
|
|
||||||
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
|
|
||||||
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
|
|
||||||
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
|
|
||||||
fi
|
|
||||||
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
|
|
||||||
|
|
||||||
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
|
|
||||||
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-}"
|
|
||||||
if [[ -z "$DEPLOY_PATH" ]]; then
|
|
||||||
DEPLOY_PATH="$(ssh_cmd 'for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do [[ -d "$d/server" ]] && echo "$d" && exit 0; done; echo /opt/nexus')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
resolve_deploy_chown() {
|
|
||||||
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
|
|
||||||
echo "${NEXUS_DEPLOY_CHOWN}"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
|
|
||||||
echo "www:www"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
|
|
||||||
}
|
|
||||||
DEPLOY_CHOWN="$(resolve_deploy_chown)"
|
|
||||||
|
|
||||||
echo "═══ Nexus local → server sync ═══"
|
|
||||||
echo "SSH target: ${NEXUS_SSH_HOST}"
|
|
||||||
echo "Remote path: ${DEPLOY_PATH}"
|
|
||||||
echo "Owner: ${DEPLOY_CHOWN}"
|
|
||||||
|
|
||||||
TAR_EXCLUDES=(
|
|
||||||
--exclude='.git'
|
|
||||||
--exclude='.env'
|
|
||||||
--exclude='.env.*'
|
|
||||||
--exclude='SECRETS.md'
|
|
||||||
--exclude='deploy/nexus-1panel.secrets.sh'
|
|
||||||
--exclude='docker/.env.prod'
|
|
||||||
--exclude='web/data'
|
|
||||||
--exclude='frontend/node_modules'
|
|
||||||
--exclude='frontend-v2/node_modules'
|
|
||||||
--exclude='node_modules'
|
|
||||||
--exclude='.venv'
|
|
||||||
--exclude='.venv-*'
|
|
||||||
--exclude='venv'
|
|
||||||
--exclude='__pycache__'
|
|
||||||
--exclude='.pytest_cache'
|
|
||||||
--exclude='.ruff_cache'
|
|
||||||
--exclude='.cursor'
|
|
||||||
--exclude='.playwright-mcp'
|
|
||||||
--exclude='.install_locked'
|
|
||||||
--exclude='.install_state.json'
|
|
||||||
--exclude='web/app/assets'
|
|
||||||
--exclude='tmp'
|
|
||||||
--exclude='2025.2'
|
|
||||||
--exclude='=2025.2'
|
|
||||||
--exclude='*.bak-'
|
|
||||||
--exclude='frontend/test-results'
|
|
||||||
--exclude='frontend/e2e/.auth'
|
|
||||||
)
|
|
||||||
|
|
||||||
echo "▶ tar stream → ${DEPLOY_PATH} ..."
|
|
||||||
tar -C "${ROOT}" -czf - "${TAR_EXCLUDES[@]}" . | \
|
|
||||||
ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "sudo tar -xzf - -C '${DEPLOY_PATH}'"
|
|
||||||
|
|
||||||
echo "▶ chown ${DEPLOY_CHOWN} (skip docker/.env.prod, web/data, .env) ..."
|
|
||||||
ssh_cmd "sudo bash -s" <<REMOTE
|
|
||||||
set -euo pipefail
|
|
||||||
root='${DEPLOY_PATH}'
|
|
||||||
own='${DEPLOY_CHOWN}'
|
|
||||||
chown_tree() {
|
|
||||||
local p="\$1"
|
|
||||||
[[ -e "\$p" ]] || return 0
|
|
||||||
sudo chown -R "\$own" "\$p"
|
|
||||||
}
|
|
||||||
for top in server deploy scripts tests standards docs frontend web; do
|
|
||||||
chown_tree "\$root/\$top"
|
|
||||||
done
|
|
||||||
if [[ -d "\$root/docker" ]]; then
|
|
||||||
while IFS= read -r -d '' p; do
|
|
||||||
sudo chown -R "\$own" "\$p"
|
|
||||||
done < <(find "\$root/docker" \( -path "\$root/docker/.env.prod" \) -prune -o -print0)
|
|
||||||
fi
|
|
||||||
for f in pyproject.toml requirements.txt requirements-dev.txt AGENTS.md CLAUDE.md; do
|
|
||||||
[[ -f "\$root/\$f" ]] && sudo chown "\$own" "\$root/\$f"
|
|
||||||
done
|
|
||||||
REMOTE
|
|
||||||
|
|
||||||
echo "✓ sync complete (owner ${DEPLOY_CHOWN})"
|
|
||||||
Regular → Executable
@@ -1,119 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Sync Git-tracked web/app and web/app-v2 into the running Nexus Docker container.
|
|
||||||
#
|
|
||||||
# Root cause: Dockerfile.prod runs vite build inside the image; Docker layer cache
|
|
||||||
# can produce asset hashes that differ from committed web/app on the host.
|
|
||||||
#
|
|
||||||
# Usage (on production host after git pull / upgrade):
|
|
||||||
# NEXUS_ROOT=/opt/nexus bash deploy/sync_webapp_to_container.sh
|
|
||||||
#
|
|
||||||
# Called automatically from deploy/nexus-1panel.sh cmd_upgrade and deploy-production.sh.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
|
|
||||||
CONTAINER="${NEXUS_CONTAINER:-}"
|
|
||||||
NEXUS_PUBLISH_PORT="${NEXUS_PUBLISH_PORT:-8600}"
|
|
||||||
|
|
||||||
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
|
|
||||||
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
|
||||||
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
|
||||||
|
|
||||||
docker_cmd() {
|
|
||||||
if docker "$@" 2>/dev/null; then return 0; fi
|
|
||||||
sudo docker "$@" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_container() {
|
|
||||||
if [[ -n "$CONTAINER" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
CONTAINER="$(docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true)"
|
|
||||||
if [[ -z "$CONTAINER" ]]; then
|
|
||||||
error "未找到运行中的 Nexus 容器(可设 NEXUS_CONTAINER=名称)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
info "容器: $CONTAINER"
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_entry_bundle() {
|
|
||||||
local html bundle_path code
|
|
||||||
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app/" 2>/dev/null || true)"
|
|
||||||
bundle_path="$(printf '%s' "$html" | sed -n 's/.*src="\(\/app\/assets\/index-[^"]*\.js\)".*/\1/p' | head -1)"
|
|
||||||
if [[ -z "$bundle_path" ]]; then
|
|
||||||
error "/app/ index.html 未解析到主 bundle"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${bundle_path}" 2>/dev/null || echo 000)"
|
|
||||||
if [[ "$code" != "200" ]]; then
|
|
||||||
error "主 bundle ${bundle_path} → HTTP ${code}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
info "入口校验 OK: ${bundle_path} → HTTP 200"
|
|
||||||
}
|
|
||||||
|
|
||||||
verify_app_v2_entry_bundle() {
|
|
||||||
local html bundle_path code
|
|
||||||
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app-v2/" 2>/dev/null || true)"
|
|
||||||
bundle_path="$(printf '%s' "$html" | sed -n 's/.*src="\(\/app-v2\/assets\/[^"]*\.js\)".*/\1/p' | head -1)"
|
|
||||||
if [[ -z "$bundle_path" ]]; then
|
|
||||||
error "/app-v2/ index.html 未解析到主 bundle"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${bundle_path}" 2>/dev/null || echo 000)"
|
|
||||||
if [[ "$code" != "200" ]]; then
|
|
||||||
error "V2 主 bundle ${bundle_path} → HTTP ${code}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
info "V2 入口校验 OK: ${bundle_path} → HTTP 200"
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local app="${NEXUS_ROOT}/web/app"
|
|
||||||
local app_v2="${NEXUS_ROOT}/web/app-v2"
|
|
||||||
if [[ ! -f "${app}/index.html" ]]; then
|
|
||||||
error "缺少 ${app}/index.html,请先 git pull"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -d "${app}/assets" ]]; then
|
|
||||||
error "缺少 ${app}/assets"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "${app_v2}/index.html" ]]; then
|
|
||||||
error "缺少 ${app_v2}/index.html,请先构建 frontend-v2"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -d "${app_v2}/assets" ]]; then
|
|
||||||
error "缺少 ${app_v2}/assets"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
resolve_container
|
|
||||||
|
|
||||||
info "同步 web/app → 容器 /app/web/app ..."
|
|
||||||
docker_cmd exec "$CONTAINER" rm -rf /app/web/app/assets
|
|
||||||
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app
|
|
||||||
docker_cmd cp "${app}/index.html" "${CONTAINER}:/app/web/app/index.html"
|
|
||||||
docker_cmd cp "${app}/assets" "${CONTAINER}:/app/web/app/assets"
|
|
||||||
|
|
||||||
if [[ -f "${app}/favicon.ico" ]]; then
|
|
||||||
docker_cmd cp "${app}/favicon.ico" "${CONTAINER}:/app/web/app/favicon.ico" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "同步 web/app-v2 → 容器 /app/web/app-v2 ..."
|
|
||||||
docker_cmd exec "$CONTAINER" rm -rf /app/web/app-v2/assets
|
|
||||||
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app-v2
|
|
||||||
docker_cmd cp "${app_v2}/index.html" "${CONTAINER}:/app/web/app-v2/index.html"
|
|
||||||
docker_cmd cp "${app_v2}/assets" "${CONTAINER}:/app/web/app-v2/assets"
|
|
||||||
|
|
||||||
if [[ -x "${NEXUS_ROOT}/deploy/prune_frontend_assets.py" ]]; then
|
|
||||||
info "容器内 prune 未引用 assets(可选)..."
|
|
||||||
docker_cmd exec "$CONTAINER" python3 /app/deploy/prune_frontend_assets.py --dry-run 2>/dev/null || \
|
|
||||||
warn "容器内 prune 跳过(脚本或路径不可用)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
verify_entry_bundle
|
|
||||||
verify_app_v2_entry_bundle
|
|
||||||
info "web/app 与 web/app-v2 同步完成"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+8
-15
@@ -1,8 +1,8 @@
|
|||||||
# Nexus 6.0 鈥?Docker Compose (dev / staging)
|
# Nexus 6.0 — Docker Compose (dev / staging)
|
||||||
# Usage:
|
# Usage:
|
||||||
# cp docker/.env.example docker/.env # or: python docker/generate_env.py
|
# cp docker/.env.example docker/.env # or: python docker/generate_env.py
|
||||||
# docker compose --env-file docker/.env up -d --build
|
# docker compose up -d --build
|
||||||
# open http://localhost:18600/app/
|
# open http://localhost:8600/app/
|
||||||
|
|
||||||
name: nexus
|
name: nexus
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
retries: 8
|
retries: 8
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
ports:
|
ports:
|
||||||
- "${MYSQL_PUBLISH_PORT:-13306}:3306"
|
- "${MYSQL_PUBLISH_PORT:-3306}:3306"
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
@@ -38,18 +38,12 @@ services:
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 8
|
retries: 8
|
||||||
ports:
|
ports:
|
||||||
- "${REDIS_PUBLISH_PORT:-16379}:6379"
|
- "${REDIS_PUBLISH_PORT:-6379}:6379"
|
||||||
|
|
||||||
nexus:
|
nexus:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
network: host
|
|
||||||
args:
|
|
||||||
DEBIAN_MIRROR: https://mirrors.aliyun.com/debian
|
|
||||||
DEBIAN_SECURITY_MIRROR: https://mirrors.aliyun.com/debian-security
|
|
||||||
PIP_INDEX_URL: https://mirrors.aliyun.com/pypi/simple/
|
|
||||||
PIP_TRUSTED_HOST: mirrors.aliyun.com
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
@@ -57,7 +51,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "${NEXUS_PUBLISH_PORT:-18600}:8600"
|
- "${NEXUS_PUBLISH_PORT:-8600}:8600"
|
||||||
environment:
|
environment:
|
||||||
MYSQL_HOST: mysql
|
MYSQL_HOST: mysql
|
||||||
MYSQL_PORT: "3306"
|
MYSQL_PORT: "3306"
|
||||||
@@ -70,11 +64,11 @@ services:
|
|||||||
NEXUS_DEPLOY_PATH: /app
|
NEXUS_DEPLOY_PATH: /app
|
||||||
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD:-nexus_dev_pass}@mysql:3306/nexus
|
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD:-nexus_dev_pass}@mysql:3306/nexus
|
||||||
NEXUS_REDIS_URL: redis://redis:6379/0
|
NEXUS_REDIS_URL: redis://redis:6379/0
|
||||||
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:-http://localhost:18600,http://127.0.0.1:18600,http://localhost:8600,http://127.0.0.1:8600}
|
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:-http://localhost:8600,http://127.0.0.1:8600}
|
||||||
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:-}
|
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:-}
|
||||||
NEXUS_API_KEY: ${NEXUS_API_KEY:-}
|
NEXUS_API_KEY: ${NEXUS_API_KEY:-}
|
||||||
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:-}
|
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:-}
|
||||||
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:-http://localhost:18600}
|
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:-http://localhost:8600}
|
||||||
env_file:
|
env_file:
|
||||||
- path: docker/.env
|
- path: docker/.env
|
||||||
required: false
|
required: false
|
||||||
@@ -93,4 +87,3 @@ volumes:
|
|||||||
redis-data:
|
redis-data:
|
||||||
nexus-state:
|
nexus-state:
|
||||||
nexus-web-data:
|
nexus-web-data:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Copy to docker/.env (or run: python docker/generate_env.py)
|
||||||
|
# Used by docker compose for nexus service — never commit docker/.env
|
||||||
|
|
||||||
|
MYSQL_ROOT_PASSWORD=change_me_root
|
||||||
|
MYSQL_PASSWORD=change_me_nexus
|
||||||
|
|
||||||
|
# Leave empty to use install wizard at http://localhost:8600/app/install.html
|
||||||
|
# Or run generate_env.py to fill these three:
|
||||||
|
NEXUS_SECRET_KEY=
|
||||||
|
NEXUS_API_KEY=
|
||||||
|
NEXUS_ENCRYPTION_KEY=
|
||||||
|
|
||||||
|
NEXUS_CORS_ORIGINS=http://localhost:8600,http://127.0.0.1:8600
|
||||||
|
NEXUS_API_BASE_URL=http://localhost:8600
|
||||||
|
|
||||||
|
# Optional host port overrides
|
||||||
|
# NEXUS_PUBLISH_PORT=8600
|
||||||
|
# MYSQL_PUBLISH_PORT=3306
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Production Compose env — copy to docker/.env.prod (never commit)
|
||||||
|
# Fresh install: bash deploy/install-nexus-fresh.sh (auto-generates NEXUS keys per host)
|
||||||
|
# MySQL / Redis 不在 Docker 栈 — 请在宿主机或 1Panel 自建,安装向导步骤 3 填写
|
||||||
|
|
||||||
|
# Auto-generated on fresh install (unique per server). Do NOT copy between hosts.
|
||||||
|
NEXUS_SECRET_KEY=
|
||||||
|
NEXUS_API_KEY=
|
||||||
|
NEXUS_ENCRYPTION_KEY=
|
||||||
|
|
||||||
|
# 1 = entrypoint must not write /app/.env until install wizard step 3
|
||||||
|
NEXUS_INSTALL_WIZARD_PENDING=0
|
||||||
|
|
||||||
|
# Public URL
|
||||||
|
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
|
||||||
|
NEXUS_API_BASE_URL=https://api.synaglobal.vip
|
||||||
|
|
||||||
|
# Nexus container resource profile (docker/profiles/*.env)
|
||||||
|
NEXUS_DB_POOL_SIZE=30
|
||||||
|
NEXUS_DB_MAX_OVERFLOW=20
|
||||||
|
|
||||||
|
# 1Panel:nx update 自动探测并写入(安装向导步骤 3 预填)
|
||||||
|
# NEXUS_1PANEL_DB_HOST=1Panel-mysql-xxxx
|
||||||
|
# NEXUS_1PANEL_REDIS_HOST=1Panel-redis-xxxx
|
||||||
|
|
||||||
|
# 宿主机 Compose 插值/ nx update 同步用;安装向导完成后以卷内 /app/.env 为准(不经 Compose 注入)
|
||||||
|
# 1Panel 有密码时: redis://:你的密码@1Panel-redis-xxxx:6379/0
|
||||||
|
NEXUS_REDIS_URL=redis://host.docker.internal:6379/0
|
||||||
|
|
||||||
|
# 宿主机 git 仓库路径(容器内安装向导用于提示 nx cron / nx update)
|
||||||
|
NEXUS_HOST_ROOT=/opt/nexus
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
# NEXUS_PUBLISH_PORT=8600
|
||||||
+20
-20
@@ -1,20 +1,20 @@
|
|||||||
# MySQL 8.4 — Nexus production tuning for 1 vCPU / 4 GiB host (minimal)
|
# MySQL 8.4 — Nexus production tuning for 1 vCPU / 4 GiB host (minimal)
|
||||||
|
|
||||||
[mysqld]
|
[mysqld]
|
||||||
character-set-server = utf8mb4
|
character-set-server = utf8mb4
|
||||||
collation-server = utf8mb4_unicode_ci
|
collation-server = utf8mb4_unicode_ci
|
||||||
|
|
||||||
innodb_buffer_pool_size = 512M
|
innodb_buffer_pool_size = 512M
|
||||||
innodb_log_file_size = 128M
|
innodb_log_file_size = 128M
|
||||||
|
|
||||||
max_connections = 80
|
max_connections = 80
|
||||||
|
|
||||||
innodb_read_io_threads = 2
|
innodb_read_io_threads = 2
|
||||||
innodb_write_io_threads = 2
|
innodb_write_io_threads = 2
|
||||||
|
|
||||||
table_open_cache = 1000
|
table_open_cache = 1000
|
||||||
tmp_table_size = 32M
|
tmp_table_size = 32M
|
||||||
max_heap_table_size = 32M
|
max_heap_table_size = 32M
|
||||||
|
|
||||||
[client]
|
[client]
|
||||||
default-character-set = utf8mb4
|
default-character-set = utf8mb4
|
||||||
|
|||||||
+25
-25
@@ -1,25 +1,25 @@
|
|||||||
# MySQL 8.4 — Nexus production tuning for 2 vCPU / 8 GiB host
|
# MySQL 8.4 — Nexus production tuning for 2 vCPU / 8 GiB host
|
||||||
# Mounted by docker/docker-compose.prod.yml (see docs/design/plans/2026-06-04-1panel-docker-production.md)
|
# Mounted by docker/docker-compose.prod.yml (see docs/design/plans/2026-06-04-1panel-docker-production.md)
|
||||||
|
|
||||||
[mysqld]
|
[mysqld]
|
||||||
character-set-server = utf8mb4
|
character-set-server = utf8mb4
|
||||||
collation-server = utf8mb4_unicode_ci
|
collation-server = utf8mb4_unicode_ci
|
||||||
|
|
||||||
# ~1 GiB for InnoDB (leave RAM for Nexus, Redis, 1Panel, OS)
|
# ~1 GiB for InnoDB (leave RAM for Nexus, Redis, 1Panel, OS)
|
||||||
innodb_buffer_pool_size = 1024M
|
innodb_buffer_pool_size = 1024M
|
||||||
innodb_log_file_size = 256M
|
innodb_log_file_size = 256M
|
||||||
|
|
||||||
# Nexus async pool default 30+20; leave headroom for admin/migrations
|
# Nexus async pool default 30+20; leave headroom for admin/migrations
|
||||||
max_connections = 120
|
max_connections = 120
|
||||||
|
|
||||||
# 2 vCPU: avoid aggressive IO threads
|
# 2 vCPU: avoid aggressive IO threads
|
||||||
innodb_read_io_threads = 4
|
innodb_read_io_threads = 4
|
||||||
innodb_write_io_threads = 4
|
innodb_write_io_threads = 4
|
||||||
|
|
||||||
# Sensible defaults on small VMs
|
# Sensible defaults on small VMs
|
||||||
table_open_cache = 2000
|
table_open_cache = 2000
|
||||||
tmp_table_size = 64M
|
tmp_table_size = 64M
|
||||||
max_heap_table_size = 64M
|
max_heap_table_size = 64M
|
||||||
|
|
||||||
[client]
|
[client]
|
||||||
default-character-set = utf8mb4
|
default-character-set = utf8mb4
|
||||||
|
|||||||
+20
-20
@@ -1,20 +1,20 @@
|
|||||||
# MySQL 8.4 — Nexus production tuning for 4 vCPU / 16 GiB host
|
# MySQL 8.4 — Nexus production tuning for 4 vCPU / 16 GiB host
|
||||||
|
|
||||||
[mysqld]
|
[mysqld]
|
||||||
character-set-server = utf8mb4
|
character-set-server = utf8mb4
|
||||||
collation-server = utf8mb4_unicode_ci
|
collation-server = utf8mb4_unicode_ci
|
||||||
|
|
||||||
innodb_buffer_pool_size = 4096M
|
innodb_buffer_pool_size = 4096M
|
||||||
innodb_log_file_size = 512M
|
innodb_log_file_size = 512M
|
||||||
|
|
||||||
max_connections = 200
|
max_connections = 200
|
||||||
|
|
||||||
innodb_read_io_threads = 8
|
innodb_read_io_threads = 8
|
||||||
innodb_write_io_threads = 8
|
innodb_write_io_threads = 8
|
||||||
|
|
||||||
table_open_cache = 4000
|
table_open_cache = 4000
|
||||||
tmp_table_size = 128M
|
tmp_table_size = 128M
|
||||||
max_heap_table_size = 128M
|
max_heap_table_size = 128M
|
||||||
|
|
||||||
[client]
|
[client]
|
||||||
default-character-set = utf8mb4
|
default-character-set = utf8mb4
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Host profile: 1 vCPU / 4 GiB
|
# Host profile: 1 vCPU / 4 GiB
|
||||||
NEXUS_MEM_LIMIT=1g
|
NEXUS_MEM_LIMIT=1g
|
||||||
NEXUS_CPUS=1
|
NEXUS_CPUS=1
|
||||||
NEXUS_DB_POOL_SIZE=15
|
NEXUS_DB_POOL_SIZE=15
|
||||||
NEXUS_DB_MAX_OVERFLOW=10
|
NEXUS_DB_MAX_OVERFLOW=10
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Host profile: 2 vCPU / 8 GiB — default production
|
# Host profile: 2 vCPU / 8 GiB — default production
|
||||||
NEXUS_MEM_LIMIT=2g
|
NEXUS_MEM_LIMIT=2g
|
||||||
NEXUS_CPUS=1.5
|
NEXUS_CPUS=1.5
|
||||||
NEXUS_DB_POOL_SIZE=30
|
NEXUS_DB_POOL_SIZE=30
|
||||||
NEXUS_DB_MAX_OVERFLOW=20
|
NEXUS_DB_MAX_OVERFLOW=20
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Host profile: 4 vCPU / 16 GiB
|
# Host profile: 4 vCPU / 16 GiB
|
||||||
NEXUS_MEM_LIMIT=4g
|
NEXUS_MEM_LIMIT=4g
|
||||||
NEXUS_CPUS=3
|
NEXUS_CPUS=3
|
||||||
NEXUS_DB_POOL_SIZE=50
|
NEXUS_DB_POOL_SIZE=50
|
||||||
NEXUS_DB_MAX_OVERFLOW=30
|
NEXUS_DB_MAX_OVERFLOW=30
|
||||||
|
|||||||
@@ -0,0 +1,733 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<title>Nexus 6.0 — 功能说明书</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0f172a;color:#e2e8f0;line-height:1.6}
|
||||||
|
a{color:#60a5fa;text-decoration:none}
|
||||||
|
a:hover{text-decoration:underline}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.wrap{max-width:960px;margin:0 auto;padding:32px 24px 80px}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.hero{text-align:center;padding:48px 0 40px;border-bottom:1px solid #1e293b;margin-bottom:40px}
|
||||||
|
.hero h1{font-size:2.2rem;font-weight:700;color:#f8fafc;letter-spacing:-0.02em}
|
||||||
|
.hero .sub{font-size:1rem;color:#64748b;margin-top:8px}
|
||||||
|
.badge{display:inline-block;margin:4px;padding:3px 10px;border-radius:999px;font-size:0.72rem;font-weight:600}
|
||||||
|
.badge-blue{background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb55}
|
||||||
|
.badge-green{background:#14371f;color:#4ade80;border:1px solid #16a34a55}
|
||||||
|
.badge-amber{background:#3d2a00;color:#fbbf24;border:1px solid #d9770655}
|
||||||
|
.badge-red{background:#3b1212;color:#f87171;border:1px solid #dc262655}
|
||||||
|
|
||||||
|
/* TOC */
|
||||||
|
.toc{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:20px 24px;margin-bottom:36px}
|
||||||
|
.toc h2{font-size:0.85rem;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px}
|
||||||
|
.toc ol{padding-left:18px;column-count:2;column-gap:24px}
|
||||||
|
.toc li{margin:5px 0;font-size:0.88rem}
|
||||||
|
|
||||||
|
/* Section */
|
||||||
|
.section{margin-bottom:48px}
|
||||||
|
.section-title{font-size:1.4rem;font-weight:700;color:#f1f5f9;padding-bottom:10px;border-bottom:2px solid #334155;margin-bottom:24px;display:flex;align-items:center;gap:10px}
|
||||||
|
.section-title .icon{font-size:1.2rem}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;margin-bottom:16px}
|
||||||
|
.card-title{font-size:1.05rem;font-weight:600;color:#f1f5f9;margin-bottom:6px;display:flex;align-items:center}
|
||||||
|
.card-title .num{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:#334155;font-size:0.7rem;font-weight:700;color:#94a3b8;margin-right:8px;shrink:0}
|
||||||
|
.card-desc{color:#94a3b8;font-size:0.88rem;margin-bottom:14px}
|
||||||
|
|
||||||
|
/* Info rows */
|
||||||
|
.info-grid{display:grid;grid-template-columns:1fr;gap:10px}
|
||||||
|
.info-block{border-radius:8px;padding:12px 14px}
|
||||||
|
.info-block.usage{background:#0f2d1f;border:1px solid #166534}
|
||||||
|
.info-block.warn{background:#2a1a00;border:1px solid #92400e}
|
||||||
|
.info-block.note{background:#1a1f2e;border:1px solid #4338ca}
|
||||||
|
.info-title{font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
|
||||||
|
.info-title.u{color:#4ade80}
|
||||||
|
.info-title.w{color:#fbbf24}
|
||||||
|
.info-title.n{color:#818cf8}
|
||||||
|
.info-block ul{padding-left:16px;font-size:0.86rem;color:#cbd5e1}
|
||||||
|
.info-block li{margin:4px 0}
|
||||||
|
.info-block p{font-size:0.86rem;color:#cbd5e1}
|
||||||
|
|
||||||
|
/* Sub table */
|
||||||
|
.feat-table{width:100%;border-collapse:collapse;font-size:0.86rem;margin-top:10px}
|
||||||
|
.feat-table th{text-align:left;color:#64748b;font-weight:600;font-size:0.75rem;text-transform:uppercase;padding:6px 10px;border-bottom:1px solid #334155}
|
||||||
|
.feat-table td{padding:7px 10px;border-bottom:1px solid #1e293b;color:#cbd5e1;vertical-align:top}
|
||||||
|
.feat-table tr:last-child td{border-bottom:none}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer{text-align:center;color:#334155;font-size:0.8rem;margin-top:60px;padding-top:24px;border-top:1px solid #1e293b}
|
||||||
|
code{background:#334155;color:#7dd3fc;padding:2px 6px;border-radius:4px;font-size:0.83em;font-family:monospace}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<!-- HERO -->
|
||||||
|
<div class="hero">
|
||||||
|
<h1>⚡ Nexus 6.0</h1>
|
||||||
|
<p class="sub">服务器运维管理平台 — 全站功能说明书 · 2026-05-23</p>
|
||||||
|
<p class="sub" style="margin-top:10px;font-size:0.85rem">
|
||||||
|
Markdown 维护版:<code>docs/project/nexus-full-site-features.md</code> ·
|
||||||
|
AI 接续:<code>docs/project/AI-HANDOFF-2026-05-23.md</code>
|
||||||
|
</p>
|
||||||
|
<div style="margin-top:14px">
|
||||||
|
<span class="badge badge-blue">FastAPI</span>
|
||||||
|
<span class="badge badge-blue">SQLAlchemy 2.0</span>
|
||||||
|
<span class="badge badge-blue">Redis 7</span>
|
||||||
|
<span class="badge badge-blue">asyncssh</span>
|
||||||
|
<span class="badge badge-blue">Tailwind v4</span>
|
||||||
|
<span class="badge badge-green">2000+ 服务器</span>
|
||||||
|
<span class="badge badge-green">JWT + TOTP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TOC -->
|
||||||
|
<div class="toc">
|
||||||
|
<h2>目录</h2>
|
||||||
|
<ol>
|
||||||
|
<li><a href="#sitemap">全站页面地图</a></li>
|
||||||
|
<li><a href="#network">网络与通道</a></li>
|
||||||
|
<li><a href="#auth">认证与访问控制</a></li>
|
||||||
|
<li><a href="#dashboard">仪表盘</a></li>
|
||||||
|
<li><a href="#alerts">告警中心</a></li>
|
||||||
|
<li><a href="#servers">服务器管理</a></li>
|
||||||
|
<li><a href="#assets">资产管理</a></li>
|
||||||
|
<li><a href="#webssh">Web SSH</a></li>
|
||||||
|
<li><a href="#commands">命令与推送日志</a></li>
|
||||||
|
<li><a href="#push">批量文件推送</a></li>
|
||||||
|
<li><a href="#scripts">脚本与执行历史</a></li>
|
||||||
|
<li><a href="#files">远程文件管理</a></li>
|
||||||
|
<li><a href="#schedules">定时调度</a></li>
|
||||||
|
<li><a href="#credentials">凭据管理</a></li>
|
||||||
|
<li><a href="#retries">重试队列</a></li>
|
||||||
|
<li><a href="#search">全局搜索</a></li>
|
||||||
|
<li><a href="#settings">系统设置</a></li>
|
||||||
|
<li><a href="#audit">审计日志</a></li>
|
||||||
|
<li><a href="#install">安装向导</a></li>
|
||||||
|
<li><a href="#guardian">3 层守护</a></li>
|
||||||
|
<li><a href="#agent">子机 Agent</a></li>
|
||||||
|
<li><a href="#background">后台任务</a></li>
|
||||||
|
<li><a href="#acceptance">开发验收标准</a></li>
|
||||||
|
<li><a href="#notimpl">未实现 / 已否决</a></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 0. SITEMAP -->
|
||||||
|
<div class="section" id="sitemap">
|
||||||
|
<div class="section-title"><span class="icon">🗺</span> 全站页面地图(18 页)</div>
|
||||||
|
<div class="card">
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>页面</th><th>路径</th><th>功能</th></tr>
|
||||||
|
<tr><td>安装向导</td><td><code>/app/install.html</code></td><td>无 .env 时安装中心机</td></tr>
|
||||||
|
<tr><td>登录</td><td><code>/app/login.html</code></td><td>JWT + TOTP + 防暴破</td></tr>
|
||||||
|
<tr><td>仪表盘</td><td><code>/app/index.html</code></td><td>统计、WS 刷新统计</td></tr>
|
||||||
|
<tr><td>服务器</td><td><code>/app/servers.html</code></td><td>CRUD、Agent 安装/升级、漂移徽章</td></tr>
|
||||||
|
<tr><td>资产管理</td><td><code>/app/assets.html</code></td><td>Platform / Node</td></tr>
|
||||||
|
<tr><td>文件管理</td><td><code>/app/files.html</code></td><td>SSH 浏览与文件操作</td></tr>
|
||||||
|
<tr><td>推送</td><td><code>/app/push.html</code></td><td>rsync 批量 + dry-run</td></tr>
|
||||||
|
<tr><td>脚本库</td><td><code>/app/scripts.html</code></td><td>脚本 CRUD、直接执行</td></tr>
|
||||||
|
<tr><td>执行历史</td><td><code>/app/script-executions.html</code></td><td>停止/重试/日志</td></tr>
|
||||||
|
<tr><td>凭据</td><td><code>/app/credentials.html</code></td><td>DB 凭据 + 密码预设</td></tr>
|
||||||
|
<tr><td>定时调度</td><td><code>/app/schedules.html</code></td><td>Cron 推送或脚本</td></tr>
|
||||||
|
<tr><td>重试队列</td><td><code>/app/retries.html</code></td><td>推送失败重试</td></tr>
|
||||||
|
<tr><td>命令日志</td><td><code>/app/commands.html</code></td><td>WebSSH 命令 + 推送日志</td></tr>
|
||||||
|
<tr><td>告警中心</td><td><code>/app/alerts.html</code></td><td>告警历史与统计</td></tr>
|
||||||
|
<tr><td>审计</td><td><code>/app/audit.html</code></td><td>操作审计、多维过滤</td></tr>
|
||||||
|
<tr><td>设置</td><td><code>/app/settings.html</code></td><td>阈值、Telegram、白名单、TOTP</td></tr>
|
||||||
|
<tr><td>WebSSH</td><td><code>/app/terminal.html</code></td><td>xterm 终端</td></tr>
|
||||||
|
</table>
|
||||||
|
<p class="card-desc" style="margin-top:12px">共享:<code>layout.js</code>(侧栏/移动适配)、<code>api.js</code>(JWT、toast、全局搜索)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- NETWORK -->
|
||||||
|
<div class="section" id="network">
|
||||||
|
<div class="section-title"><span class="icon">🔌</span> 网络与通道模型</div>
|
||||||
|
<div class="card">
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>方向</th><th>端口</th><th>用途</th><th>必须</th></tr>
|
||||||
|
<tr><td>子机 → 中心</td><td><strong>443 出站</strong></td><td>Agent 心跳、长任务 curl 回调</td><td>✅</td></tr>
|
||||||
|
<tr><td>中心 → 子机</td><td><strong>22 入站</strong></td><td>推送、文件、脚本、健康检查、Agent 安装</td><td>✅</td></tr>
|
||||||
|
<tr><td>子机本机</td><td><code>127.0.0.1:8601</code></td><td>Agent HTTP(可不暴露公网)</td><td>可选</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="info-block note" style="margin-top:12px"><div class="info-title n">说明</div><p>脚本执行与手动健康检查已改走 <strong>SSH</strong>,不依赖公网 8601。宝塔环境请用系统 Python 3.12 + <code>install.sh</code> venv,勿用面板 Python 项目管理器托管 Agent。</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 1. AUTH -->
|
||||||
|
<div class="section" id="auth">
|
||||||
|
<div class="section-title"><span class="icon">🔒</span> 1. 认证与访问控制</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> JWT 登录 + 自动刷新</div>
|
||||||
|
<p class="card-desc">管理员通过用户名+密码登录,获取 Access Token(30 分钟)和 Refresh Token(7 天)。前端到期前 2 分钟自动换新 Token,无感续期。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>访问 <code>/app/login.html</code>,输入管理员账号密码</li>
|
||||||
|
<li>Token 存于 <code>localStorage</code>,关闭浏览器后 7 天内免登录</li>
|
||||||
|
<li>8 小时无操作自动退出</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li>JWT 存于 localStorage,XSS 漏洞可能导致 Token 泄露(已做 CSP 缓解)</li>
|
||||||
|
<li>无 RBAC:只有一个管理员角色,鉴权即信任</li>
|
||||||
|
<li>不支持多管理员角色区分</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> TOTP 双因素认证</div>
|
||||||
|
<p class="card-desc">可选启用 Google Authenticator 兼容的 TOTP。登录时先验密码,再验 6 位动态码(30 秒窗口)。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>进入「设置」→「安全设置」→ 启用 / <strong>重新绑定</strong> TOTP,扫描二维码</li>
|
||||||
|
<li>禁用 TOTP 需同时提供当前密码 + TOTP 码</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li>TOTP 秘钥丢失无法恢复,需提前备份</li>
|
||||||
|
<li>系统时间偏差 >30s 会导致验证失败(接受 ±1 窗口共 90s)</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">3</span> 登录防暴破</div>
|
||||||
|
<p class="card-desc">同一 IP + 用户名 15 分钟内失败 5 次,账号自动锁定并返回 HTTP 429。</p>
|
||||||
|
<div class="info-block note"><div class="info-title n">注意</div><p>锁定基于 IP + 用户名组合,更换 IP 可绕过。适合防自动化爆破,不防分布式攻击。</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
|
||||||
|
<p class="card-desc">可在设置中开启「仅允许白名单 IP 登录」。支持手动 IP/CIDR,以及从<strong>代理订阅 URL</strong>解析节点 IP(每 2 小时自动刷新,与手动列表合并)。</p>
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>「设置」→「登录 IP 白名单」→ 总开关 + 订阅地址 + 手动 IP</li>
|
||||||
|
<li>非白名单 IP 登录被拒绝并记审计</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. DASHBOARD -->
|
||||||
|
<div class="section" id="dashboard">
|
||||||
|
<div class="section-title"><span class="icon">📊</span> 2. 仪表盘与实时告警</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 实时统计面板</div>
|
||||||
|
<p class="card-desc">首页展示服务器总数、在线数、离线数、告警中台数、最近推送记录。数据来源 Redis(实时)+ MySQL(历史),通过 WebSocket 触发刷新。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>在线状态取自 Redis <code>heartbeat:{id}</code>,子机 60s 上报一次</li>
|
||||||
|
<li>告警计数 = Redis <code>alerts:*</code> key 数量</li>
|
||||||
|
<li>WS 断线会自动重连(3 次指数退避)</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li>在线状态最多滞后 60s(子机心跳间隔)</li>
|
||||||
|
<li>Redis 不可用时统计退化为 MySQL 数据,可能不准</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> 告警通知(Telegram)</div>
|
||||||
|
<p class="card-desc">CPU / 内存 / 磁盘超过阈值(默认 80%)时,通过 Telegram Bot 推送告警;恢复后发送恢复通知。同服务器同指标 5 分钟内不重复推送。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>「设置」配置 <code>TELEGRAM_BOT_TOKEN</code> + <code>TELEGRAM_CHAT_ID</code></li>
|
||||||
|
<li>阈值可在设置页调整(CPU/内存/磁盘各独立)</li>
|
||||||
|
<li>浏览器仪表盘<strong>只显示计数,不弹窗、不响铃</strong>(设计决策)</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li>浏览器仪表盘不弹告警列表;历史见「告警中心」页</li>
|
||||||
|
<li>Telegram Bot 被封或网络不通时告警静默失败(有日志)</li>
|
||||||
|
<li>告警防抖 key 存于进程内存,重启后防抖状态重置</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ALERTS -->
|
||||||
|
<div class="section" id="alerts">
|
||||||
|
<div class="section-title"><span class="icon">🔔</span> 告警中心</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 告警历史(alerts.html)</div>
|
||||||
|
<p class="card-desc">将 CPU/内存/磁盘告警与恢复事件持久化到 <code>alert_logs</code>,支持分页、按服务器/类型/日期筛选,以及近 7 日统计图表数据。</p>
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>侧栏「告警中心」查看历史列表</li>
|
||||||
|
<li>API:<code>GET /api/alert-history/</code>、<code>GET /api/alert-history/stats</code></li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> 分项 Telegram 开关</div>
|
||||||
|
<p class="card-desc">设置页可单独关闭某类告警的 Telegram 推送(告警/恢复/系统/时钟漂移等),便于后续扩展通道。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. SERVERS -->
|
||||||
|
<div class="section" id="servers">
|
||||||
|
<div class="section-title"><span class="icon">🖥</span> 3. 服务器管理</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 服务器 CRUD</div>
|
||||||
|
<p class="card-desc">增删改查服务器配置,支持 SSH 密钥 / 密码双认证,可按平台类型(Platform)和节点(Node 树)分组。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>新增服务器:填写名称、域名/IP、SSH 端口、认证方式</li>
|
||||||
|
<li>认证方式选「密钥」时粘贴私钥内容,自动 Fernet 加密存储</li>
|
||||||
|
<li>支持 <code>platform_id</code> / <code>node_id</code> 分组归类</li>
|
||||||
|
<li>服务器列表分页显示(默认 50 条/页)</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li>批量导入(CSV)未实现,2000 台需逐一或通过 API 批量添加</li>
|
||||||
|
<li>删除服务器同时删除关联推送日志(CASCADE)</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> 健康检查</div>
|
||||||
|
<p class="card-desc">手动触发 <code>POST /api/servers/check</code>:经 <strong>SSH</strong> 执行 Python 采集 CPU/内存/磁盘(<code>remote_probe</code>),<strong>不需要</strong>公网 8601。</p>
|
||||||
|
<div class="info-block note"><div class="info-title n">与在线状态区别</div><p>列表「在线」主要来自 Agent <strong>443 心跳</strong>;健康检查用于即时 SSH 探活与指标快照。</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">4</span> Agent 安装与升级</div>
|
||||||
|
<p class="card-desc">服务器详情「Agent 安装」:复制 <code>install.sh</code> 命令或一键 SSH 安装(带日志);安装后轮询在线。升级通过 SSH 拉取最新 <code>agent.py</code> 并 restart。</p>
|
||||||
|
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||||
|
<li>安装脚本:Python 3.12 venv、监听 <code>127.0.0.1:8601</code>,无需公网放行 8601</li>
|
||||||
|
<li>时钟漂移:心跳带 <code>agent_time</code>,列表显示 warn/crit 徽章</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">3</span> 每服务器 API Key</div>
|
||||||
|
<p class="card-desc">为每台服务器单独生成 Agent API Key(格式 <code>nxs-xxx</code>),用于该服务器子机与中心通信的专属认证。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>服务器详情 → 「生成 Agent Key」,完整 Key 仅本次显示,立即复制到子机 <code>config.json</code></li>
|
||||||
|
<li>列表页只显示前 8 位用于识别</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||||
|
<li>Key 生成后无法再次查看(一次性展示设计)</li>
|
||||||
|
<li>丢失需重新生成并更新子机配置</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ASSETS -->
|
||||||
|
<div class="section" id="assets">
|
||||||
|
<div class="section-title"><span class="icon">🗂</span> 资产管理</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> Platform 与 Node</div>
|
||||||
|
<p class="card-desc"><code>assets.html</code> 管理资产类型模板(Platform)与树形节点(Node),服务器表单可关联分组。</p>
|
||||||
|
<p class="card-desc">API:<code>/api/assets/platforms</code>、<code>/api/assets/nodes</code>(含 tree)</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> SSH 会话与命令(只读查询)</div>
|
||||||
|
<p class="card-desc">同页或「命令日志」可查询 <code>ssh_sessions</code>、<code>command_logs</code>(WebSSH 产生)。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. WEB SSH -->
|
||||||
|
<div class="section" id="webssh">
|
||||||
|
<div class="section-title"><span class="icon">💻</span> 4. Web SSH 终端</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 浏览器终端</div>
|
||||||
|
<p class="card-desc">基于 xterm.js + asyncssh,在浏览器内打开远程服务器 SSH 会话,支持全屏、自动 resize、彩色输出。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>服务器列表点击「终端」→ 弹出 <code>/app/terminal.html?server_id=X</code></li>
|
||||||
|
<li>认证:先获取短效 webssh-token(15 分钟),绑定到该服务器,再建 WebSocket</li>
|
||||||
|
<li>支持窗口 resize 自动通知远程 shell</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li>Token 通过 URL 参数传递(?token=...),HTTPS 下安全;HTTP 下有风险</li>
|
||||||
|
<li>不支持文件上传 / 拖拽(需用文件管理页)</li>
|
||||||
|
<li>会话不录像(已明确否决)</li>
|
||||||
|
<li>网络中断 WebSocket 不会自动重连,需刷新页面</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COMMANDS -->
|
||||||
|
<div class="section" id="commands">
|
||||||
|
<div class="section-title"><span class="icon">💻</span> 命令与推送日志</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> WebSSH 会话与命令</div>
|
||||||
|
<p class="card-desc"><code>commands.html</code>:会话列表、命令列表(危险命令高亮)、按服务器过滤。数据来自 WebSSH,直接 SSH 不记录。</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> 推送日志</div>
|
||||||
|
<p class="card-desc">同页「推送日志」Tab:rsync 批量推送历史,支持服务器/状态/同步模式/触发方式过滤与分页。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5. PUSH -->
|
||||||
|
<div class="section" id="push">
|
||||||
|
<div class="section-title"><span class="icon">📤</span> 5. 批量文件推送</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> rsync 批量推送</div>
|
||||||
|
<p class="card-desc">将源目录通过 rsync-over-SSH 推送到选定的服务器列表,支持 4 种同步模式,内置分批(50 台/批)并发(10 路)控制。</p>
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>同步模式</th><th>rsync 参数</th><th>适用场景</th></tr>
|
||||||
|
<tr><td>增量同步</td><td><code>-az</code></td><td>日常更新,只同步有变动的文件</td></tr>
|
||||||
|
<tr><td>全量同步</td><td><code>-az --delete</code></td><td>强制目标与源完全一致,会删除目标多余文件</td></tr>
|
||||||
|
<tr><td>校验和</td><td><code>-az --checksum</code></td><td>按文件内容而非时间戳判断差异</td></tr>
|
||||||
|
<tr><td>直接覆盖</td><td><code>-az --inplace</code></td><td>不用临时文件,直接覆盖(大文件节省空间)</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="info-grid" style="margin-top:12px">
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li><strong>全量同步(--delete)会不可逆删除</strong>目标目录多余文件,务必先「预览」</li>
|
||||||
|
<li>rsync 在中心机运行,中心机需能 SSH 到所有子机</li>
|
||||||
|
<li>失败的服务器自动加入重试队列(指数退避,最多 100 次)</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> 推送前预览(dry-run)</div>
|
||||||
|
<p class="card-desc">选中全量/覆盖模式时,页面显示橙色警告,引导先对首台服务器运行 <code>rsync --dry-run --stats</code>,查看待传输/删除文件数量后再决定是否推送。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>选择「全量同步」→ 出现橙色预览按钮 → 点击 → 展示首台服务器变动摘要</li>
|
||||||
|
<li>可展开「详细文件列表」(最多 200 行)</li>
|
||||||
|
<li>预览不阻塞推送,看完数据自行决定是否点「开始推送」</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||||
|
<li>仅预览第一台选中的服务器,其余服务器可能略有差异</li>
|
||||||
|
<li>预览需 SSH 连通,与真实推送消耗相同时间(通常 <5s)</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">3</span> 失败自动重试队列</div>
|
||||||
|
<p class="card-desc">推送失败的服务器自动进入 <code>push_retry_jobs</code> 队列,后台每 5 分钟按指数退避重试(60s → 120s → 240s...,最大 64 分钟)。</p>
|
||||||
|
<div class="info-block note"><div class="info-title n">注意</div><p>最多重试 100 次。可在「重试」页手动强制立即重试或删除任务。</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 6. SCRIPTS -->
|
||||||
|
<div class="section" id="scripts">
|
||||||
|
<div class="section-title"><span class="icon">📜</span> 脚本与执行历史</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 脚本库管理</div>
|
||||||
|
<p class="card-desc">维护可复用 Shell 脚本库(按 ops/deploy/check/cleanup 分类),一键下发到选定的服务器。</p>
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>「脚本」页 → 新建脚本 → 填写名称/类别/内容</li>
|
||||||
|
<li>点击脚本名 → 编辑;点击「执行」→ 选择服务器 → 运行</li>
|
||||||
|
<li>支持直接输入临时命令(无需保存到库)</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> 批量执行(短任务 / 长任务)</div>
|
||||||
|
<p class="card-desc">短任务:同步等待结果(默认 30s 超时)。长任务:nohup 后台运行,完成后 curl 回调中心记录结果。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>选择服务器 → 填写命令 → 「执行」</li>
|
||||||
|
<li>执行走 <strong>SSH 22</strong>(结果含 <code>channel: ssh</code>),无需子机公网 8601</li>
|
||||||
|
<li>长任务:nohup + 子机 curl 回调中心(需 HTTPS <code>NEXUS_API_BASE_URL</code>)</li>
|
||||||
|
<li>「执行历史」页:停止 / 重试 / 标记卡住 / 拉取 nexus-job 日志</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li><code>rm -rf /</code> 等危险命令被中心<strong>拦截(400)</strong>,但仅匹配预设规则</li>
|
||||||
|
<li>长任务依赖子机能访问中心 <code>NEXUS_API_BASE_URL</code>(必须配置且使用 HTTPS)</li>
|
||||||
|
<li>执行历史默认保留 2 小时 Redis 热数据,60s 刷入 MySQL</li>
|
||||||
|
<li>执行命令时密码等敏感 $DB_* 变量存储时已脱敏(替换为 ***)</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">3</span> $DB_* 数据库凭据替换</div>
|
||||||
|
<p class="card-desc">脚本中使用 <code>$DB_USER</code> / <code>$DB_PASS</code> / <code>$DB_HOST</code> / <code>$DB_PORT</code> / <code>$DB_NAME</code> 占位符,执行时自动替换为选定的数据库凭据,密码不出现在脚本文本或日志中。</p>
|
||||||
|
<div class="info-block note"><div class="info-title n">注意</div><p>替换在中心机完成后发往子机,子机接收到的已是明文命令;传输通过 HTTPS,子机侧无加密保护。</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 7. FILES -->
|
||||||
|
<div class="section" id="files">
|
||||||
|
<div class="section-title"><span class="icon">📁</span> 7. 远程文件管理</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 目录浏览</div>
|
||||||
|
<p class="card-desc">通过 SSH exec(<code>ls -la</code>)浏览指定服务器的远程目录,支持面包屑导航和目录点击进入。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>「文件管理」→ 选服务器 → 输入路径 → 「浏览」(或按 Enter)</li>
|
||||||
|
<li>点击目录名进入子目录,点击「..」返回上级</li>
|
||||||
|
<li>文件按 目录优先 / 字母顺序 排列</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li>无左侧树状面板(每次需手动导航)</li>
|
||||||
|
<li>显示的文件大小为 <code>ls</code> 输出的字节数,大文件不友好</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> 文件操作(删除/重命名/新建目录)</div>
|
||||||
|
<p class="card-desc">鼠标悬停文件/目录行显示操作按钮,通过 SSH exec 在远程执行操作。</p>
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>操作</th><th>命令</th><th>触发方式</th></tr>
|
||||||
|
<tr><td>删除文件</td><td><code>rm -f path</code></td><td>悬停 → 🗑 → confirm 弹窗</td></tr>
|
||||||
|
<tr><td>删除目录</td><td><code>rm -rf path</code></td><td>悬停 → 🗑 → 额外警告弹窗</td></tr>
|
||||||
|
<tr><td>重命名</td><td><code>mv src dst</code></td><td>悬停 → ✏ → prompt 输入新名</td></tr>
|
||||||
|
<tr><td>新建目录</td><td><code>mkdir -p path</code></td><td>右上角 📁+ → prompt 输入名</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li>删除目录(rm -rf)<strong>不可恢复</strong>,浏览器弹窗确认无法替代真正的安全保障</li>
|
||||||
|
<li>无文件编辑器(已明确否决,用 WebSSH vim/nano 代替)</li>
|
||||||
|
<li>无文件上传功能(可用 SFTP API 或 WebSSH scp)</li>
|
||||||
|
<li>无文件内容预览</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 8. SCHEDULES -->
|
||||||
|
<div class="section" id="schedules">
|
||||||
|
<div class="section-title"><span class="icon">⏰</span> 8. 定时调度</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> Cron 调度(推送 + 脚本)</div>
|
||||||
|
<p class="card-desc">支持两种 <code>schedule_type</code>:<strong>文件推送</strong> 或 <strong>脚本执行</strong>。5 字段 Cron,后台每 60s 检查(仅 Primary Worker)。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>「定时」→ 选类型 → 填 Cron、目标服务器</li>
|
||||||
|
<li>脚本型:选脚本库 ID 或内联内容,可设超时与长任务</li>
|
||||||
|
<li>示例:<code>0 2 * * *</code> = 每天凌晨 2 点</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||||
|
<li>Cron 精度受 60s 检查间隔限制</li>
|
||||||
|
<li>脚本调度同样走 SSH 执行通道</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RETRIES -->
|
||||||
|
<div class="section" id="retries">
|
||||||
|
<div class="section-title"><span class="icon">🔄</span> 重试队列</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="card-desc"><code>retries.html</code> 管理推送失败任务:<code>retry_runner</code> 每 5 分钟指数退避;可手动立即重试或删除。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SEARCH -->
|
||||||
|
<div class="section" id="search">
|
||||||
|
<div class="section-title"><span class="icon">🔍</span> 全局搜索</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="card-desc">侧栏搜索框调用 <code>GET /api/search/</code>,跨服务器、脚本、凭据、定时任务关键字匹配。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 9. CREDENTIALS -->
|
||||||
|
<div class="section" id="credentials">
|
||||||
|
<div class="section-title"><span class="icon">🔑</span> 9. 凭据管理</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 数据库凭据</div>
|
||||||
|
<p class="card-desc">存储 MySQL/PostgreSQL 连接信息,密码 Fernet 加密,API 响应不含密码(仅元数据)。供脚本执行时通过 $DB_* 变量引用。</p>
|
||||||
|
<div class="info-block warn"><div class="info-title w">注意</div><p>凭据只加密存储,子机接收到的命令已含明文连接信息(仅在中心→子机的 HTTPS 传输中受保护)。</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> 密码预设</div>
|
||||||
|
<p class="card-desc">存储 SSH 密码模板(Fernet 加密),可在推送配置中引用,避免重复输入常用密码。</p>
|
||||||
|
<div class="info-block note"><div class="info-title n">注意</div><p>密码预设不能用于脚本 $DB_PASS 替换,只用于推送密码模板。</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 10. SETTINGS -->
|
||||||
|
<div class="section" id="settings">
|
||||||
|
<div class="section-title"><span class="icon">⚙️</span> 10. 系统设置</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 可配置项</div>
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>设置项</th><th>说明</th><th>是否可改</th></tr>
|
||||||
|
<tr><td>系统名称 / 标题</td><td>品牌展示名称</td><td>✅ 可改</td></tr>
|
||||||
|
<tr><td>告警阈值(CPU/内存/磁盘)</td><td>超过阈值触发 Telegram 告警,默认 80%</td><td>✅ 可改</td></tr>
|
||||||
|
<tr><td>Redis URL</td><td>Redis 连接地址</td><td>✅ 可改</td></tr>
|
||||||
|
<tr><td>数据库连接池大小</td><td>pool_size / max_overflow</td><td>✅ 可改</td></tr>
|
||||||
|
<tr><td>Telegram Bot Token / Chat ID</td><td>告警推送配置</td><td>✅ 可改</td></tr>
|
||||||
|
<tr><td>SECRET_KEY / API_KEY / ENCRYPTION_KEY</td><td>核心安全密钥</td><td>❌ 不可改(需改 .env 重启)</td></tr>
|
||||||
|
<tr><td>DATABASE_URL</td><td>数据库连接</td><td>❌ 不可改</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">2</span> Telegram</div>
|
||||||
|
<p class="card-desc">配置 Bot Token / Chat ID;<strong>测试发送</strong>;通过 getUpdates <strong>检测 Chat ID</strong>;各告警类型独立开关。</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">3</span> TOTP 与 API Key</div>
|
||||||
|
<p class="card-desc">TOTP 启用/禁用/<strong>重新绑定</strong>;全局 API Key 需密码二次验证后查看(记审计)。</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
|
||||||
|
<p class="card-desc">总开关、订阅 URL(2h 刷新)、手动 IP、解析预览(见「认证」章节)。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 11. AUDIT -->
|
||||||
|
<div class="section" id="audit">
|
||||||
|
<div class="section-title"><span class="icon">📋</span> 11. 审计日志</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 操作审计</div>
|
||||||
|
<p class="card-desc">所有 CUD 操作(创建/更新/删除服务器、推送、执行脚本、登录、改密码等)自动写入 <code>audit_logs</code>,记录操作人、IP、目标、详情。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>「审计」页:日期起止、管理员用户名、操作类型(分组下拉)</li>
|
||||||
|
<li>每页 50 条分页</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
|
||||||
|
<li>无日志导出(已否决 CSV/JSON)</li>
|
||||||
|
<li>审计日志不会自动清理,长期需归档</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 12. INSTALL -->
|
||||||
|
<div class="section" id="install">
|
||||||
|
<div class="section-title"><span class="icon">🚀</span> 12. 安装向导</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 5 步安装向导</div>
|
||||||
|
<p class="card-desc">无 <code>.env</code> 文件时系统进入安装模式,访问 <code>/app/install.html</code> 完成 5 步配置:欢迎 → 数据库 → Redis → 管理员账号 → 完成。自动生成 API Key / Secret Key / Encryption Key。</p>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
|
||||||
|
<li>安装完成后 <code>.env</code> 自动生成,安装 API 除 GET /status 外全部锁定(返回 403)</li>
|
||||||
|
<li>重装需手动删除 <code>.env</code> 并重启</li>
|
||||||
|
</ul></div>
|
||||||
|
<div class="info-block warn"><div class="info-title w">注意</div><ul>
|
||||||
|
<li>安装过程未验证数据库/Redis 连接成功才允许进入下一步(应在步骤中手动验证)</li>
|
||||||
|
<li>安装模式下除 install API 和静态文件外,其余 API 返回 503</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 13. GUARDIAN -->
|
||||||
|
<div class="section" id="guardian">
|
||||||
|
<div class="section-title"><span class="icon">🛡</span> 13. 三层守护机制</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> 三层守护</div>
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>层级</th><th>机制</th><th>触发条件</th><th>动作</th></tr>
|
||||||
|
<tr><td>Layer 1</td><td>Supervisor</td><td>Python 进程崩溃</td><td>自动重启</td></tr>
|
||||||
|
<tr><td>Layer 2</td><td>Python self_monitor(30s)</td><td>Redis/MySQL 不可达</td><td>发 Telegram 告警/恢复</td></tr>
|
||||||
|
<tr><td>Layer 3</td><td>Shell cron(1 分钟)</td><td>连续 3 次 /health 失败</td><td>supervisorctl restart + Telegram</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">注意</div><p>Shell cron 依赖宝塔/系统 crontab 配置,部署时需手动添加定时任务。</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 14. AGENT -->
|
||||||
|
<div class="section" id="agent">
|
||||||
|
<div class="section-title"><span class="icon">🤖</span> 14. 子机 Agent</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="num">1</span> Agent 功能</div>
|
||||||
|
<p class="card-desc">部署在每台被管服务器上的轻量服务:<strong>心跳走 443 出站</strong>;HTTP 仅本机 <code>127.0.0.1:8601</code>。中心侧脚本已改 <strong>SSH</strong>,/exec 端点保留供兼容。</p>
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>能力</th><th>说明</th></tr>
|
||||||
|
<tr><td>心跳</td><td>POST 中心 <code>/api/agent/heartbeat</code>,写 Redis,智能加急</td></tr>
|
||||||
|
<tr><td>时钟漂移</td><td><code>agent_time</code> 与中心比对,warn/crit + Telegram</td></tr>
|
||||||
|
<tr><td>IP 白名单</td><td><code>allowed_ips</code> 限制访问本机 Agent HTTP</td></tr>
|
||||||
|
<tr><td>长任务回调</td><td>POST <code>/api/agent/script-callback</code></td></tr>
|
||||||
|
<tr><td>安装</td><td><code>web/agent/install.sh</code> — python3.12 venv + systemd</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="info-block warn" style="margin-top:12px"><div class="info-title w">注意</div><ul>
|
||||||
|
<li>升级 API 存在,建议与 venv 安装脚本对齐后使用</li>
|
||||||
|
<li>长任务回调需子机出站访问中心 HTTPS</li>
|
||||||
|
</ul></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BACKGROUND -->
|
||||||
|
<div class="section" id="background">
|
||||||
|
<div class="section-title"><span class="icon">⚙</span> 后台任务(Primary Worker)</div>
|
||||||
|
<div class="card">
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>任务</th><th>间隔</th><th>说明</th></tr>
|
||||||
|
<tr><td>heartbeat_flush</td><td>10min</td><td>Redis 心跳 → MySQL</td></tr>
|
||||||
|
<tr><td>self_monitor</td><td>30s</td><td>Redis/MySQL/WS 自检 + Telegram</td></tr>
|
||||||
|
<tr><td>schedule_runner</td><td>60s</td><td>Cron 调度</td></tr>
|
||||||
|
<tr><td>retry_runner</td><td>5min</td><td>推送重试</td></tr>
|
||||||
|
<tr><td>ip_allowlist_refresh</td><td>2h</td><td>订阅 IP 刷新</td></tr>
|
||||||
|
<tr><td>script_execution_flush</td><td>60s</td><td>执行记录 Redis→MySQL</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ACCEPTANCE -->
|
||||||
|
<div class="section" id="acceptance">
|
||||||
|
<div class="section-title"><span class="icon">✅</span> 标准与验收(与团队同等严谨)</div>
|
||||||
|
<div class="card">
|
||||||
|
<p class="card-desc"><strong>全量标准转接</strong>:<code>docs/project/standards-transfer-package.md</code> — 含 Cursor 规则、<code>standards/</code> 三件套、逐行八步 DoD、设计/上线清单。下一任 AI <strong>不得降低</strong>执行力度。</p>
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>类别</th><th>权威路径</th></tr>
|
||||||
|
<tr><td>标准转接包</td><td><code>docs/project/standards-transfer-package.md</code></td></tr>
|
||||||
|
<tr><td>验收 L0~L5</td><td><code>development-acceptance-standard.md</code></td></tr>
|
||||||
|
<tr><td>系统开发</td><td><code>standards/system-development-standard.md</code></td></tr>
|
||||||
|
<tr><td>逐行审计 v2</td><td><code>standards/line-walk-audit-standard-v2.md</code></td></tr>
|
||||||
|
<tr><td>审计原则</td><td><code>standards/audit-core-principles.md</code></td></tr>
|
||||||
|
<tr><td>Cursor</td><td><code>.cursor/rules/*.mdc</code>(含 audit-line-review)</td></tr>
|
||||||
|
</table>
|
||||||
|
<p class="card-desc" style="margin-top:12px">逐行审查:8 步/文件、Closure 全表含 SAFE、FINDING 必须 <code>文件:行号</code>、单回复 ≤5 文件。AI 接续:HANDOFF + 全站功能 + 标准转接包。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 15. NOT IMPL -->
|
||||||
|
<div class="section" id="notimpl">
|
||||||
|
<div class="section-title"><span class="icon">🚫</span> 15. 未实现 / 已否决功能</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<table class="feat-table">
|
||||||
|
<tr><th>功能</th><th>状态</th><th>原因 / 替代方案</th></tr>
|
||||||
|
<tr><td>文件编辑器(Monaco/ACE)</td><td><span class="badge badge-red">已否决</span></td><td>运维场景用 WebSSH + vim/nano;嵌入编辑器增加复杂度无实质收益</td></tr>
|
||||||
|
<tr><td>会话录像(asciicast)</td><td><span class="badge badge-red">已否决</span></td><td>命令日志已满足审计需求</td></tr>
|
||||||
|
<tr><td>RBAC 权限体系</td><td><span class="badge badge-red">已否决</span></td><td>鉴权即信任,单管理员模型;增加 RBAC 代价远超收益</td></tr>
|
||||||
|
<tr><td>移动端适配</td><td><span class="badge badge-red">已否决</span></td><td>2000 台服务器运维不会用手机操作</td></tr>
|
||||||
|
<tr><td>批量服务器导入(CSV)</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 API 批量 POST /api/servers/ 实现</td></tr>
|
||||||
|
<tr><td>文件上传到远程服务器</td><td><span class="badge badge-amber">未实现</span></td><td>可用 WebSSH scp / 推送功能代替</td></tr>
|
||||||
|
<tr><td>文件内容预览</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 WebSSH cat 查看</td></tr>
|
||||||
|
<tr><td>WebSSH 断线自动重连</td><td><span class="badge badge-amber">未实现</span></td><td>当前需刷新页面;计划自动重试+按钮</td></tr>
|
||||||
|
<tr><td>仪表盘历史趋势图</td><td><span class="badge badge-amber">未实现</span></td><td>仅实时 Redis 指标</td></tr>
|
||||||
|
<tr><td>宝塔 API 监控集成</td><td><span class="badge badge-amber">未实现</span></td><td>可选;参考 btpanel-skills</td></tr>
|
||||||
|
<tr><td>Agent mTLS</td><td><span class="badge badge-amber">讨论中</span></td><td>未实现</td></tr>
|
||||||
|
<tr><td>首次部署 E2E(T1~T5、T-SE)</td><td><span class="badge badge-amber">待验证</span></td><td>见 production-verification-checklist.md</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<footer>
|
||||||
|
<p>Nexus 6.0 全站功能说明书 · 2026-05-23(对齐代码库)</p>
|
||||||
|
<p style="margin-top:6px">
|
||||||
|
<a href="project/nexus-full-site-features.md">全站功能</a> ·
|
||||||
|
<a href="project/standards-transfer-package.md">标准转接</a> ·
|
||||||
|
<a href="project/development-acceptance-standard.md">验收标准</a> ·
|
||||||
|
<a href="project/AI-HANDOFF-2026-05-23.md">AI 接续</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin-top:4px">Python 3.12 / FastAPI 0.115 / MySQL 8.4 / Redis 7 / asyncssh 2.17 / Tailwind v4</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+3
-14
@@ -49,18 +49,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 测试(全站用例 SSOT)
|
## 4. 设计与标准
|
||||||
|
|
||||||
| 文档 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| [testing/test-case-matrix.md](testing/test-case-matrix.md) | **152 条** UT/IT/CH/E2E/MCP 编号用例 |
|
|
||||||
| [testing/mcp-browser-playbook.md](testing/mcp-browser-playbook.md) | Agent Playwright MCP 点页规程 |
|
|
||||||
| [design/specs/2026-06-08-full-site-testing-strategy-design.md](design/specs/2026-06-08-full-site-testing-strategy-design.md) | 四层金字塔设计 |
|
|
||||||
| [design/plans/2026-06-08-full-site-testing-strategy.md](design/plans/2026-06-08-full-site-testing-strategy.md) | 分阶段实施计划 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 设计与标准
|
|
||||||
|
|
||||||
| 目录 | 说明 |
|
| 目录 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -71,13 +60,13 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 归档区
|
## 5. 归档区
|
||||||
|
|
||||||
**[archive/README.md](archive/README.md)** — 2026-06 审计合并卷、旧 handoff、agentmemory、2026-05 审查报告。
|
**[archive/README.md](archive/README.md)** — 2026-06 审计合并卷、旧 handoff、agentmemory、2026-05 审查报告。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 工具脚本
|
## 6. 工具脚本
|
||||||
|
|
||||||
| 脚本 | 用途 |
|
| 脚本 | 用途 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# 审计 — 2026-06-07 文件上传 + Layer3 巡检脚本 + 生产对齐部署
|
|
||||||
|
|
||||||
## 范围
|
|
||||||
|
|
||||||
本批次 `git diff HEAD~1`(commit `9337f37`)及部署前修复:
|
|
||||||
|
|
||||||
| 文件 | 变更要点 |
|
|
||||||
|------|----------|
|
|
||||||
| `deploy/health_monitor.sh` | 引号剥离 `normalize_env_value`;Telegram 中文告警 |
|
|
||||||
| `frontend/src/composables/files/useFilesActions.ts` | FormData 字段 `files` → `file` |
|
|
||||||
| `server/api/sync_v2.py` | 接受 `file`/`files`;多文件;`_sftp_upload_one` |
|
|
||||||
| `tests/test_sync_upload_form.py` | 上传字段解析单测 |
|
|
||||||
| `server/api/auth.py` | 改密 `audit_repo` 定义顺序(F821 修复) |
|
|
||||||
| `deploy/gate_log.jsonl` | 门控运行记录(自动生成,非业务逻辑) |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 上传字段前后端一致 | FIXED `sync_v2` + `useFilesActions` |
|
|
||||||
| H2 | 多文件不静默丢 | FIXED `_collect_multipart_upload_files` |
|
|
||||||
| H3 | Layer3 Telegram token 引号 | FIXED `normalize_env_value` |
|
|
||||||
| H4 | 改密会话失效 + 审计 | FIXED `audit_repo` 先于 `AuthService` |
|
|
||||||
| H5 | 无静默吞错 | PASS SFTP/HTTP 错误向上抛出 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 | 依据 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | FIXED | `test_sync_upload_form.py` |
|
|
||||||
| H2 | FIXED | `sync_v2.py` 多文件循环 |
|
|
||||||
| H3 | FIXED | 生产演练 Telegram 收到 |
|
|
||||||
| H4 | FIXED | `auth.py` ruff F821 清零 |
|
|
||||||
| H5 | PASS | 无 bare except |
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
|
|
||||||
| 项 | 结果 |
|
|
||||||
|----|------|
|
|
||||||
| `pytest tests/test_sync_upload_form.py -q` | pass |
|
|
||||||
| `bash scripts/local_verify.sh` | 待 ruff 修复后重跑 |
|
|
||||||
| Layer3 演练 | 用户确认 Telegram 收到 |
|
|
||||||
| `deploy/pre_deploy_check.sh` | 本审计入库后重跑 |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] changelog `2026-06-07-files-upload-form-field-fix.md`
|
|
||||||
- [x] 涉及文件列入本审计范围表
|
|
||||||
- [x] 生产 Layer3 终验通过
|
|
||||||
- [ ] 前端 vite build + docker upgrade
|
|
||||||
- [ ] 批量升级 Agent
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# 审计 — 2026-06-07 登录客户端 IP 反代识别
|
|
||||||
|
|
||||||
| 文件 | 变更要点 |
|
|
||||||
|------|----------|
|
|
||||||
| `server/utils/client_ip.py` | 可信反代跳读取 X-Real-IP / XFF 末段 |
|
|
||||||
| `server/api/auth.py` | login/refresh/change_password 用 get_client_ip |
|
|
||||||
| `tests/test_client_ip.py` | 单测 |
|
|
||||||
| `deploy/gate_log.jsonl` | 门控记录 |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| H | 结论 |
|
|
||||||
|---|------|
|
|
||||||
| H1 | 公网直连忽略伪造 X-Real-IP | PASS |
|
|
||||||
| H2 | 172.18.0.1 仅在内网跳信任转发头 | PASS |
|
|
||||||
|
|
||||||
Step3✓ Closure✓ DoD✓
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
| `server/infrastructure/env_file.py` | `.env` upsert/read(UTF-8 LF) |
|
| `server/infrastructure/env_file.py` | `.env` upsert/read(UTF-8 LF) |
|
||||||
| `server/application/services/ops_patrol_sync.py` | Telegram → `/app/.env`、persist 卷、host `.env.prod` |
|
| `server/application/services/ops_patrol_sync.py` | Telegram → `/app/.env`、persist 卷、host `.env.prod` |
|
||||||
| `server/api/settings.py` | `set_setting` 钩子;`GET/POST ops-patrol/*` |
|
| `server/api/settings.py` | `set_setting` 钩子;`GET/POST ops-patrol/*` |
|
||||||
| `deploy/health_monitor.sh` | `load_telegram_from_db` + flock 修复 + **Telegram 引号剥离** |
|
| `deploy/health_monitor.sh` | `load_telegram_from_db` + `bump_fail_count` flock 修复 |
|
||||||
| `deploy/deploy-production.sh` | 部署成功自动 `install_ops_cron.sh` |
|
| `deploy/deploy-production.sh` | 部署成功自动 `install_ops_cron.sh` |
|
||||||
| `deploy/nexus-1panel.sh` | `sync_env_prod_to_volume` 含 Telegram 键 |
|
| `deploy/nexus-1panel.sh` | `sync_env_prod_to_volume` 含 Telegram 键 |
|
||||||
| `frontend/src/pages/SettingsPage.vue` | Layer 3 巡检状态与同步 UI |
|
| `frontend/src/pages/SettingsPage.vue` | Layer 3 巡检状态与同步 UI |
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# 审计 — 2026-06-07 Refresh Token Redis+MySQL 双写
|
|
||||||
|
|
||||||
| 文件 | 变更要点 |
|
|
||||||
|------|----------|
|
|
||||||
| `server/domain/models/__init__.py` | AdminRefreshToken ORM |
|
|
||||||
| `server/infrastructure/database/refresh_token_repo.py` | MySQL 持久层 |
|
|
||||||
| `server/application/services/auth_service.py` | 先 Redis 后 MySQL;membership/rotate 双端 |
|
|
||||||
| `server/application/services/refresh_token_hydration.py` | 启动回填 |
|
|
||||||
| `server/main.py` | hydration 调用 |
|
|
||||||
| `server/api/dependencies.py` | repo 注入 |
|
|
||||||
| `server/api/auth.py` | 改密双端清 token |
|
|
||||||
| `server/infrastructure/database/migrations.py` | admin_refresh_tokens 表 |
|
|
||||||
| `tests/test_refresh_token_persistence.py` | 单测 |
|
|
||||||
| `deploy/gate_log.jsonl` | 门控记录 |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 仅存 SHA-256 hash | PASS |
|
|
||||||
| H2 | Redis 丢失 MySQL 回退 | PASS |
|
|
||||||
| H3 | 改密双端清 token | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
- Step3✓ — 双写 + hydration + 回退
|
|
||||||
- Closure✓ — 满足「Redis 先写、MySQL 持久、重启不丢会话」
|
|
||||||
- DoD✓ — pytest 6 passed;待生产 Redis 重启验证
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# 审计 — 2026-06-07 快速添加 SSH 主机密钥 + 错误中文化
|
|
||||||
|
|
||||||
## 范围
|
|
||||||
|
|
||||||
| 文件 | 变更要点 |
|
|
||||||
|------|----------|
|
|
||||||
| `server/infrastructure/ssh/asyncssh_pool.py` | `try_ssh_login` 探测 `known_hosts=None`;SSH 错误中文化 |
|
|
||||||
| `server/utils/sync_error_message.py` | asyncssh 主机密钥/认证失败等英→中 |
|
|
||||||
| `server/config.py` | `SSH_STRICT_HOST_CHECKING` 默认 `false` |
|
|
||||||
| `server/application/services/credential_poller.py` | `format_poll_errors` 中文预设类型 |
|
|
||||||
| `frontend/src/pages/ServersPage.vue` | 失败弹窗 system→系统 |
|
|
||||||
| `tests/test_sync_error_message.py` | 翻译断言 |
|
|
||||||
| `tests/test_security_unit.py` | 脱敏中文占位符对齐(同批次工作区) |
|
|
||||||
| `deploy/gate_log.jsonl` | 门控记录 |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 凭据探测不得静默吞错 | PASS — 错误写入 pending + 中文返回 |
|
|
||||||
| H2 | 主机密钥:运维平台可跳过探测 | PASS — 仅 `try_ssh_login` 跳过;pool 仍读 `SSH_STRICT` |
|
|
||||||
| H3 | 无明文密钥 | PASS — 无凭据变更 |
|
|
||||||
| H4 | 默认 strict false 与 install true 冲突 | ACCEPT — install `.env` 显式写 true 仍可覆盖 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
- Step3✓ — 8 步扫描无 BLOCK
|
|
||||||
- Closure✓ — 根因(HostKeyNotVerifiable)与修复一致
|
|
||||||
- DoD✓ — pytest 17 passed;待生产快速添加验证
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# 审计 — Telegram 通知全中文详细化
|
|
||||||
|
|
||||||
## 范围
|
|
||||||
|
|
||||||
| 文件 | 变更 |
|
|
||||||
|------|------|
|
|
||||||
| `server/infrastructure/telegram/__init__.py` | 消息模板 |
|
|
||||||
| `server/background/self_monitor.py` | Redis/MySQL |
|
|
||||||
| `server/api/agent.py` | 时钟偏差 |
|
|
||||||
| `server/api/websocket.py` | server_id |
|
|
||||||
| `server/api/settings.py` | 测试消息 |
|
|
||||||
| `deploy/health_monitor.sh` | Layer 3 四类消息 |
|
|
||||||
| `deploy/gate_log.jsonl` | 门控记录(自动生成) |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 无密钥/URL 明文 | SAFE — sanitize 保留 |
|
|
||||||
| H2 | HTML escape | SAFE |
|
|
||||||
| H3 | NOTIFY 开关仍生效 | SAFE |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 |
|
|
||||||
|---|------|
|
|
||||||
| H1–H3 | SAFE |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] changelog / audit
|
|
||||||
- [x] pytest 通过
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
|
|
||||||
`.venv/bin/pytest tests/test_settings_telegram.py -q`
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# 审计 — Agent 401 停心跳(A-05)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-agent-401-stop-heartbeat.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`heartbeat_policy.py` · `agent.py` · `install.sh` · `servers.py` · `server_batch_common.py` · `server_batch_service.py` · `agent_version.py` · `test_agent_heartbeat_stop.py`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 401 / discarded 后 `return` 退出循环 | PASS |
|
|
||||||
| 5xx 仍退避重试,非静默吞错 | PASS |
|
|
||||||
| config reload 重启已 done 的心跳任务 | PASS |
|
|
||||||
| 子机 rollout 依赖 upgrade-agent(文档已写明) | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · 待部署 + 子机批量升级验证
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] `heartbeat_should_stop` 单测
|
|
||||||
- [x] `get_central_agent_version` 仍读 2.0.0
|
|
||||||
- [ ] 生产抽样子机 journalctl 401 后无重复心跳(#7 rollout 后)
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
# 审计 — 北京时间展示统一 + 资源恢复 Telegram 去重(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**:
|
|
||||||
- `docs/changelog/2026-06-08-beijing-time-display-unified.md`
|
|
||||||
- `docs/changelog/2026-06-08-telegram-recovery-dedup-timezone.md`
|
|
||||||
|
|
||||||
**触发原因**: Bug 修复(恢复通知秒级重复、Telegram/UI 时间非北京时间)+ 展示层时区统一
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
| 文件 | 行数 | 状态 |
|
|
||||||
|------|------|------|
|
|
||||||
| `server/utils/display_time.py` | 27 | ☑ 已审 |
|
|
||||||
| `server/infrastructure/telegram/__init__.py` | ~320 | ☑ 已审(`_now_cn` 段) |
|
|
||||||
| `server/background/ip_allowlist_refresh.py` | ~100 | ☑ 已审(刷新时间) |
|
|
||||||
| `server/api/settings.py` | ~1570 | ☑ 已审(Telegram 测试消息) |
|
|
||||||
| `server/api/agent.py` | ~385 | ☑ 已审(恢复去重) |
|
|
||||||
| `frontend/src/utils/datetime.ts` | 85 | ☑ 已审 |
|
|
||||||
| `frontend/src/utils/status.ts` | 36 | ☑ 已审 |
|
|
||||||
| `frontend/src/composables/push/labels.ts` | 68 | ☑ 已审 |
|
|
||||||
| `frontend/src/composables/useWebSocket.ts` | ~190 | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/AuditPage.vue` | ~196 | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/AlertsPage.vue` | ~190 | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/CommandsPage.vue` | ~393 | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/RetriesPage.vue` | ~180 | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/SchedulesPage.vue` | ~533 | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/ScriptsPage.vue` | ~1315 | ☑ 已审(时间列) |
|
|
||||||
| `frontend/src/pages/ScriptRunsPage.vue` | ~681 | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/ServersPage.vue` | ~1400 | ☑ 已审(导出文件名) |
|
|
||||||
| `frontend/src/components/credentials/CredentialsManager.vue` | ~436 | ☑ 已审 |
|
|
||||||
| `frontend/src/components/servers/ServerInlineDetail.vue` | ~158 | ☑ 已审 |
|
|
||||||
| `tests/test_display_time.py` | 18 | ☑ 已审 |
|
|
||||||
| `tests/test_telegram_time_format.py` | 20 | ☑ 已审 |
|
|
||||||
| `tests/test_agent_recovery_dedup.py` | 42 | ☑ 已审 |
|
|
||||||
|
|
||||||
## 审计8步结果
|
|
||||||
|
|
||||||
### Step 1: 登记 ✅
|
|
||||||
|
|
||||||
两项用户反馈:① Telegram 恢复时间显示 UTC;② 同一指标 1 秒内连发多条恢复通知。
|
|
||||||
|
|
||||||
### Step 2: 全文 Read ✅
|
|
||||||
|
|
||||||
上述文件全文走读;`broadcast_recovery` / `websocket.py` 交叉确认恢复无 Telegram cooldown 设计意图(恢复应即时推送,根因在 Redis 成员重复)。
|
|
||||||
|
|
||||||
### Step 3: 规则扫描 H
|
|
||||||
|
|
||||||
| ID | 规则 | 命中点 | 初判 |
|
|
||||||
|----|------|--------|------|
|
|
||||||
| H1 | 静默吞错 | `agent.py` Redis 告警跟踪 `except: debug` | SAFE — 已有模式,告警主路径仍广播 |
|
|
||||||
| H2 | 时区一致性 | `parseApiDateTime` 假定 naive = UTC | SAFE — 与 `sync_log_repo`、ORM `_utcnow` 一致 |
|
|
||||||
| H3 | 去重完整性 | Redis legacy `mem:92` 多条 | SAFE — `_detect_recovery` 按 metric 去重 + srem 全清 |
|
|
||||||
| H4 | XSS / 注入 | 前端 `formatDateTimeBeijing` 仅格式化 | SAFE — Vue 文本插值,无 `v-html` |
|
|
||||||
| H5 | 密钥泄露 | `display_time` / `datetime.ts` | SAFE — 无凭据 |
|
|
||||||
| H6 | CUD 审计 | 无新 CUD 路径 | N/A |
|
|
||||||
| H7 | 多 worker | 恢复 Telegram 仍无 Redis 锁 | LOW — 单心跳内重复已根治;跨 worker 同毫秒双 POST 概率极低,接受 |
|
|
||||||
|
|
||||||
### Step 4: Closure表
|
|
||||||
|
|
||||||
| H | 判定 | 依据 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | SAFE | 跟踪失败不影响 `broadcast_alert`;日志可见 |
|
|
||||||
| H2 | SAFE | DB/API 约定 UTC;文档与 changelog 已说明 |
|
|
||||||
| H3 | SAFE | 单测 `test_detect_recovery_dedupes_legacy_metric_value_entries` |
|
|
||||||
| H4 | SAFE | 展示层数值格式化 |
|
|
||||||
| H5 | SAFE | — |
|
|
||||||
| H6 | N/A | — |
|
|
||||||
| H7 | ACCEPT | 根因修复;若后续再现可加 `recovery_sent:{id}:{metric}` SETNX |
|
|
||||||
|
|
||||||
### Step 5: 入口表
|
|
||||||
|
|
||||||
| 入口 | 变更 |
|
|
||||||
|------|------|
|
|
||||||
| `POST /api/agent/heartbeat` | Redis `alerts:{id}` 成员格式、恢复检测 |
|
|
||||||
| Telegram 出站 | `_now_cn` → `format_beijing_now` |
|
|
||||||
| `POST .../telegram/test` | 测试消息时间 |
|
|
||||||
| 前端各列表页 | 只读展示,无新 API |
|
|
||||||
|
|
||||||
### Step 6: 输入→Sink
|
|
||||||
|
|
||||||
- Agent `system_info` → 阈值比较 → Redis set / `broadcast_recovery` → Telegram:无新 sink;去重降低 Telegram 频率。
|
|
||||||
- API `created_at` 字符串 → `parseApiDateTime` → `Intl` 北京时间:只读展示。
|
|
||||||
|
|
||||||
### Step 7: 归类
|
|
||||||
|
|
||||||
```
|
|
||||||
display_time.py 27行 2H 0 FINDING
|
|
||||||
agent.py (recovery) ~50行 3H 0 FINDING
|
|
||||||
datetime.ts 85行 2H 0 FINDING
|
|
||||||
前端 10 文件 ~40行 1H 0 FINDING
|
|
||||||
测试 3 文件 80行 0H 0 FINDING
|
|
||||||
──────────────────────────────────────────
|
|
||||||
总计 8H 0 FINDING(1 ACCEPT)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 8: DoD ✅
|
|
||||||
|
|
||||||
- [x] 恢复通知同一指标单次心跳最多 1 条 Telegram
|
|
||||||
- [x] Telegram / 审计页 / 告警页等绝对时间为北京时间
|
|
||||||
- [x] DB 仍存 UTC,未改调度/漂移计算
|
|
||||||
- [x] `pytest` 7 项通过(`test_display_time` / `test_telegram_time_format` / `test_agent_recovery_dedup`)
|
|
||||||
- [x] `npm run type-check` 通过
|
|
||||||
- [ ] 生产部署与浏览器终验(待用户批准 deploy)
|
|
||||||
|
|
||||||
## FINDING 列表
|
|
||||||
|
|
||||||
无。H7 为风险接受项,非 FINDING。
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
☑ **审计通过,0 FINDING,可进入部署门控**(需 commit + `pre_deploy_check.sh` + 前端 build)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 审计 — 仪表盘服务器列表不显示
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-dashboard-server-list-fix.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`frontend/src/composables/useServerPagination.ts` · `frontend/src/pages/DashboardPage.vue` · `web/app/index.html`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| loading 静默刷新后清除 | PASS |
|
|
||||||
| 去掉 skeleton v-if 卡死 | PASS |
|
|
||||||
| v-card 结构 | PASS |
|
|
||||||
| 搜索/30s 刷新 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
frontend build PASS · 待部署
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 仪表盘服务器表格可见
|
|
||||||
- [ ] 生产浏览器验收
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 审计 — 生产部署批次(WebSSH ANSI + 服务器未设路径 + 命令栏单色)
|
|
||||||
|
|
||||||
**Changelog**:
|
|
||||||
- `docs/changelog/2026-06-08-webssh-xterm-ansi-output-fix.md`
|
|
||||||
- `docs/changelog/2026-06-08-servers-unset-path-inline-detail.md`
|
|
||||||
- `docs/changelog/2026-06-08-terminal-cmdbar-monochrome.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| WebSSH relay | `server/api/webssh.py` |
|
|
||||||
| 终端解码 | `frontend/src/utils/terminalPayload.ts` |
|
|
||||||
| 终端会话 | `frontend/src/composables/terminal/useTerminalSessions.ts` |
|
|
||||||
| 终端页 | `frontend/src/pages/TerminalPage.vue` |
|
|
||||||
| 命令栏 | `frontend/src/components/terminal/TerminalCmdBar.vue` |
|
|
||||||
| WebSSH 单测 | `tests/test_webssh_terminal_payload.py` |
|
|
||||||
| 路径判定 | `server/utils/posix_paths.py` |
|
|
||||||
| 服务器仓储 | `server/infrastructure/database/server_repo.py` |
|
|
||||||
| 服务器 API | `server/api/servers.py` |
|
|
||||||
| 前端路径 util | `frontend/src/utils/serverTargetPath.ts` |
|
|
||||||
| 分页 composable | `frontend/src/composables/useServerPagination.ts` |
|
|
||||||
| 行内详情 | `frontend/src/components/servers/ServerInlineDetail.vue` |
|
|
||||||
| 未设路径面板 | `frontend/src/components/servers/ServerUnsetPathPanel.vue` |
|
|
||||||
| 服务器页 | `frontend/src/pages/ServersPage.vue` |
|
|
||||||
| 路径过滤单测 | `tests/test_server_target_path_filter.py` |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| xterm ANSI 在 SSH 输出区 | PASS(PTY env + data_b64) |
|
|
||||||
| 命令栏单色 | PASS(`.sh-*` 统一 on-surface) |
|
|
||||||
| `target_path_unset` 默认 None | PASS |
|
|
||||||
| 双表 refresh / 行内详情 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · pytest + local_verify + build · 生产部署
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] WebSSH xterm ANSI
|
|
||||||
- [x] 命令栏无彩色
|
|
||||||
- [x] 服务器未设路径区块 + 行内详情
|
|
||||||
- [x] 单测与门控
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# 审计 — 全量对齐部署(batch 回收 / sync purge / agent 跳过)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-full-align-deploy.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`main.py` · `servers.py` · `server_batch_common.py` · `server_batch_service.py` · `server_batch_store.py` · `server_batch_job_repo.py` · `sync_log_repo.py` · `server_batch_reconcile.py` · `sync_log_purge.py` · `agent_version.py` · `test_agent_version.py` · `test_server_batch_reconcile.py` · `test_sync_log_purge.py`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 启动回收仅 primary worker,无静默吞错 | PASS |
|
|
||||||
| sync_logs purge 批量 DELETE,retention=30d | PASS |
|
|
||||||
| 批量安装跳过:版本 SSOT 来自 agent.py,SSH 探测有超时 | PASS |
|
|
||||||
| install 跳过不计入失败,lower 版本仍执行 install.sh | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · 生产全量对齐部署
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] pytest 新增用例通过
|
|
||||||
- [x] local_verify 26/26
|
|
||||||
- [x] 生产 /health + /app/ 200
|
|
||||||
- [x] 容器内 recover_orphaned / sync_log_purge 标记存在
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# 审计 — Gate 7 diff 范围修复(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-gate7-diff-fix.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `deploy/pre_deploy_check.sh` | Gate 7 使用 `HEAD~1..HEAD`,跳过 `gate_log.jsonl` |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 不误拦历史工作区 diff | PASS 仅校验最近一次 commit |
|
|
||||||
| 门控日志不入审计交叉验证 | PASS `deploy/gate_log.jsonl` 跳过 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| 门控 | 结果 |
|
|
||||||
|------|------|
|
|
||||||
| pre_deploy_check 7/7 | 待本提交后复验 |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] Gate 7 逻辑修正
|
|
||||||
- [x] changelog 记录
|
|
||||||
- [x] 本审计列出 `pre_deploy_check.sh`
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# 审计 — Gate 7 跳过 Vite assets(2026-06-08)
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `deploy/pre_deploy_check.sh` | Gate 7 跳过 `web/app/assets/*` |
|
|
||||||
| `docs/audit/2026-06-08-script-ui-toast-server-names.md` | 构建条目补充 assets 通配 |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 哈希 bundle 不逐文件审计 | PASS 与 Docker 内 rebuild 一致 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
待门控复验。
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] `pre_deploy_check.sh` 已更新
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 审计 — 登录 IP 门禁 + SPA 无感切换
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-login-gate-seamless-nav.md`
|
|
||||||
**设计**: `docs/design/specs/2026-06-08-login-gate-seamless-nav-design.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
**本批次(登录门禁 + 无感切换)**
|
|
||||||
`login_allowlist.py` · `auth.py` · `auth_jwt.py` · `auth_service.py` · `LoginPage.vue` · `App.vue` · `cachedPages.ts` · `usePageActivateRefresh.ts` · `useCachedQuery.ts` · `useRoutePrefetch.ts` · `useServerCategories.ts` · `usePushPage.ts` · `usePushLogs.ts` · `useFilesBrowse.ts` · `useFilesNavigation.ts` · 12 页 `*Page.vue` · `tests/test_login_access.py`
|
|
||||||
|
|
||||||
**同工作区未提交(服务器排序修复,已审计 `2026-06-08-servers-live-sort-fix`)**
|
|
||||||
`server_list_sort.py` · `server_repo.py` · `servers.py` · `useServerPagination.ts` · `DashboardPage.vue` · `ServersPage.vue` · `tests/test_server_list_sort.py` · `web/app/index.html` · `web/app/assets/*`
|
|
||||||
|
|
||||||
## Step 3 安全
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| `GET /api/auth/login-access` 不泄露白名单 | PASS |
|
|
||||||
| 与 `POST /login` IP 结论一致 | PASS(单测覆盖) |
|
|
||||||
| 公开路径仅 GET | PASS(`PUBLIC_EXACT_PATHS`) |
|
|
||||||
| 登出清 query 缓存 | PASS(`clearCachedQueries`) |
|
|
||||||
|
|
||||||
## Step 3 架构
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| keep-alive 排除 Terminal/Login | PASS |
|
|
||||||
| `max=12` 控内存 | PASS |
|
|
||||||
| ScriptRuns `watch(params.id)` + `route.name` key | PASS |
|
|
||||||
| stale-while-revalidate TTL 30s | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
- `pytest tests/test_login_access.py` — 7 passed
|
|
||||||
- `bash scripts/local_verify.sh` — PASS
|
|
||||||
- `npm run build` — PASS(vue-tsc + vite)
|
|
||||||
- 门控 — 7/7 PASS
|
|
||||||
- 生产 — `/health` ok · `/app/` 200(tar+scp 部署,Gitea pull 500 绕过)
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] P0 三态登录页 + 预检 API
|
|
||||||
- [x] P1 路由 key + replace + Dashboard 预载
|
|
||||||
- [x] P2 keep-alive + 12 页 silent 激活刷新
|
|
||||||
- [x] P3 stats/categories 共享缓存
|
|
||||||
- [x] P4 侧栏 hover 预取 + Dashboard/Servers 骨架屏
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# 审计 — 资源恢复 Telegram 联动分项开关 + 仪表盘去列表
|
|
||||||
|
|
||||||
**Changelog**:
|
|
||||||
- `docs/changelog/2026-06-08-notify-recovery-respects-resource-toggle.md`
|
|
||||||
- `docs/changelog/2026-06-08-dashboard-remove-server-list.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`server/utils/notify_toggles.py` · `server/infrastructure/telegram/__init__.py` · `tests/test_notify_toggle_sync.py` · `frontend/src/types/api.ts` · `frontend/src/pages/DashboardPage.vue` · `web/app/index.html`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 恢复 Telegram 需 NOTIFY_RECOVERY + 分项 notify_alert_* | PASS(`resource_recovery_notify_enabled`) |
|
|
||||||
| CPU/内存/磁盘告警关闭后恢复不推 | PASS(parametrize 单测 3 指标) |
|
|
||||||
| Redis/MySQL 恢复共用 notify_system_* | PASS(已有 `send_telegram_system_alert` 门控 + 单测) |
|
|
||||||
| WebSocket / alert_logs 仍记录恢复 | PASS(仅 Telegram 门控) |
|
|
||||||
| 设置页文案说明联动 | PASS(api.ts NOTIFY_TOGGLE_ITEMS) |
|
|
||||||
| 仪表盘移除服务器列表表格 | PASS(保留统计卡片与告警区) |
|
|
||||||
| 无静默吞错 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
pytest notify 34/34 · frontend build PASS · Gate 7 待跑
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 关 CPU 告警 → 无 CPU 超阈值 & 无 CPU 恢复 Telegram
|
|
||||||
- [x] 内存/磁盘同理
|
|
||||||
- [x] 仪表盘无服务器列表区块
|
|
||||||
- [x] changelog ×2
|
|
||||||
- [ ] 生产 /health + 浏览器验收
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# 审计 — Telegram 开关多 Worker 同步 + 服务器行内详情展开
|
|
||||||
|
|
||||||
**Changelog**:
|
|
||||||
- `docs/changelog/2026-06-08-notify-toggle-multi-worker-sync.md`
|
|
||||||
- `docs/changelog/2026-06-08-servers-inline-detail-expand-fix.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`server/config.py` · `server/infrastructure/settings_broadcast.py` · `server/utils/notify_toggles.py` · `server/api/settings.py` · `server/main.py` · `server/infrastructure/telegram/__init__.py` · `tests/test_notify_toggle_sync.py` · `frontend/src/pages/ServersPage.vue` · `frontend/src/components/servers/ServerUnsetPathPanel.vue`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| NOTIFY_* 关闭后 Telegram 不发送 | PASS(helper + 单测 9 开关) |
|
|
||||||
| 多 worker 热更新 | PASS(Redis `nexus:settings` 广播) |
|
|
||||||
| WebSocket / alert_logs 仍记录 | PASS(仅 Telegram 门控,符合设置页文案) |
|
|
||||||
| expanded 与 item-value 数字 id 一致 | PASS(number[] + computed 桥接) |
|
|
||||||
| 无静默吞错 | PASS(broadcast publish 失败打 warning) |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 待跑 · pytest notify 31/31 · frontend build PASS
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] Settings 关 CPU 告警 → 任意 worker 不推 Telegram
|
|
||||||
- [x] 点击服务器名称展开 ServerInlineDetail
|
|
||||||
- [x] 未设路径区块同样可展开
|
|
||||||
- [x] changelog ×2
|
|
||||||
- [ ] 生产 /health + 浏览器验收
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# 审计 — 页面自动刷新(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-pages-auto-refresh.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `usePageAutoRefresh.ts` | 通用轮询 |
|
|
||||||
| `useScriptExecutionListRefresh.ts` | 脚本列表 + WS 完成 |
|
|
||||||
| `useServerPagination.ts` | silent 刷新 |
|
|
||||||
| `usePushServers.ts` | 30s 服务器灯 |
|
|
||||||
| `ServersPage.vue` | 30s |
|
|
||||||
| `ScriptsPage.vue` | 8s/30s |
|
|
||||||
| `ScriptRunsPage.vue` | 列表 + 详情 |
|
|
||||||
| `RetriesPage.vue` | 15s |
|
|
||||||
| `web/app/index.html` | 构建产物 |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 静默刷新不打断操作 | PASS |
|
|
||||||
| 标签页隐藏暂停 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · 生产 `b011b60` · `/health` 200
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 所列页面自动刷新
|
|
||||||
- [x] 已部署 `https://api.synaglobal.vip/app/`
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 审计 — 推送后台化 + 422 中文化 + 隐藏 CSV 导出(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**:
|
|
||||||
- `docs/changelog/2026-06-08-push-async-background.md`
|
|
||||||
- `docs/changelog/2026-06-08-validation-error-loc-zh.md`
|
|
||||||
- `docs/changelog/2026-06-08-servers-hide-csv-export.md`
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
| 文件 | 状态 |
|
|
||||||
|------|------|
|
|
||||||
| `server/background/sync_push_runner.py` | ☑ |
|
|
||||||
| `server/api/sync_v2.py` | ☑ |
|
|
||||||
| `server/utils/validation_errors_zh.py` | ☑ |
|
|
||||||
| `frontend/src/composables/push/usePushProgress.ts` | ☑ |
|
|
||||||
| `frontend/src/components/push/PushToolbar.vue` | ☑ |
|
|
||||||
| `frontend/src/pages/PushPage.vue` | ☑ |
|
|
||||||
| `frontend/src/utils/validationErrorFormat.ts` | ☑ |
|
|
||||||
| `frontend/src/api/index.ts` | ☑ |
|
|
||||||
| `frontend/src/utils/apiError.ts` | ☑ |
|
|
||||||
| `frontend/src/pages/ServersPage.vue` | ☑ |
|
|
||||||
| `tests/test_validation_errors_zh.py` | ☑ |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 推送后台任务独立 DB 会话 | PASS |
|
|
||||||
| 422 loc_zh 不泄露内部路径 | PASS |
|
|
||||||
| 隐藏 CSV 仅 UI 移除 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 待跑 · 0 FINDING
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 推送按钮不长时间 loading
|
|
||||||
- [x] 校验错误显示「每页条数:不能大于 200」
|
|
||||||
- [x] 服务器页无导出 CSV 按钮
|
|
||||||
- [ ] 生产部署与健康检查
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 审计 — 调度周期「当天」文案与单次 UI(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-schedule-cycle-day-label-once-ui.md`
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 周期工具 | `frontend/src/utils/scheduleCycle.ts` |
|
|
||||||
| 选择器 | `frontend/src/components/schedules/ScheduleCyclePicker.vue` |
|
|
||||||
| 调度页 | `frontend/src/pages/SchedulesPage.vue` |
|
|
||||||
| 后端镜像 | `server/utils/schedule_cycle.py` |
|
|
||||||
| 单测 | `tests/test_schedule_cycle.py` |
|
|
||||||
|
|
||||||
## 安全
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 无新 API | PASS — 仅文案与 UI 布局 |
|
|
||||||
| Cron 逻辑 | PASS — 未改 build/parse 规则 |
|
|
||||||
| 鉴权 | PASS — 无变更 |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 单次不显示「重复方式」 | PASS — singleShot 固定「当天」 |
|
|
||||||
| H2 | 循环保留下拉 | PASS — 宽度 7.5rem |
|
|
||||||
| H3 | 标签「当天」 | PASS — 前后端 + 单测一致 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 |
|
|
||||||
|---|------|
|
|
||||||
| H1–H3 | PASS |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] pytest tests/test_schedule_cycle.py
|
|
||||||
- [x] frontend vite build
|
|
||||||
- [x] changelog
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
# 审计 — 调度执行周期可视化(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-schedule-cycle-picker.md`
|
|
||||||
**设计**: `docs/design/specs/2026-06-08-schedule-cycle-picker-design.md`
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 周期工具 | `frontend/src/utils/scheduleCycle.ts` |
|
|
||||||
| 选择器 | `frontend/src/components/schedules/ScheduleCyclePicker.vue` |
|
|
||||||
| 调度页 | `frontend/src/pages/SchedulesPage.vue` |
|
|
||||||
| 后端镜像 | `server/utils/schedule_cycle.py` |
|
|
||||||
| 单测 | `tests/test_schedule_cycle.py` |
|
|
||||||
|
|
||||||
## 安全
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 无新 API | PASS — 仍提交 `cron_expr` 字符串 |
|
|
||||||
| Cron 注入 | PASS — 白名单类型 + 数值 clamp;custom 仅 5 字段字符集 |
|
|
||||||
| 鉴权 | PASS — 调度 CRUD 仍走既有 JWT |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 不改 DB/runner | PASS |
|
|
||||||
| H2 | 宝塔周期对齐 | PASS — day/day-n/hour/hour-n/minute-n/week/month |
|
|
||||||
| H3 | 旧 Cron 可编辑 | PASS — parseCronToCycle + custom 回退 |
|
|
||||||
| H4 | 秒级诚实披露 | PASS — N≥60,折算分钟步进 + UI hint |
|
|
||||||
| H5 | 列表可读 | PASS — formatCycleLabel |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 |
|
|
||||||
|---|------|
|
|
||||||
| H1–H5 | PASS |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] `pytest tests/test_schedule_cycle.py tests/test_schedule_next_run.py` — 21 passed
|
|
||||||
- [x] `cd frontend && npm run build` — OK
|
|
||||||
- [x] changelog + 设计文档
|
|
||||||
- [x] 生产热更新 + 容器内 bundle 含「执行周期」
|
|
||||||
- [ ] 用户浏览器终验
|
|
||||||
|
|
||||||
## 代码审计(2026-06-08 补审)
|
|
||||||
|
|
||||||
此前仅跑门控 + 写文档;以下为逐文件审读结论。
|
|
||||||
|
|
||||||
### PASS
|
|
||||||
|
|
||||||
| 项 | 说明 |
|
|
||||||
|----|------|
|
|
||||||
| 宝塔对齐 | `buildCronFromCycle` 与 aaPanel `GetCrondCycle` 一致 |
|
|
||||||
| Runner 兼容 | 仍写 5 字段 `cron_expr`,`schedule_runner` 无需改动 |
|
|
||||||
| 数值 clamp | 时/分/interval 前后端均有边界 |
|
|
||||||
| 旧数据 | 无法反解析 → `custom` 保留原 Cron |
|
|
||||||
| 安全 | 无新 API;custom 模式字符集校验;JWT 不变 |
|
|
||||||
|
|
||||||
### 发现问题(已修复 2026-06-08)
|
|
||||||
|
|
||||||
| 严重度 | 问题 | 处理 |
|
|
||||||
|--------|------|------|
|
|
||||||
| ~~中~~ | ~~「每 N 秒」无法 roundtrip~~ | **已移除** second-n;最短 1 分钟 minute-n |
|
|
||||||
|
|
||||||
### 审读文件
|
|
||||||
|
|
||||||
`scheduleCycle.ts` · `ScheduleCyclePicker.vue` · `SchedulesPage.vue` · `schedule_cycle.py` · `test_schedule_cycle.py` · `schedule_runner.py`(未改,兼容性确认)
|
|
||||||
|
|
||||||
## Gate 7 说明
|
|
||||||
|
|
||||||
本变更仅前端 + 工具/单测。HEAD 已提交 sort 修复交叉引用:`useServerPagination.ts`、`DashboardPage.vue`、`ServersPage.vue`、`servers.py`、`server_list_sort.py`、`server_repo.py`、`test_server_list_sort.py`、`web/app/index.html`(见 `docs/audit/2026-06-08-servers-live-sort-fix.md`)。
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# 审计 — 调度表单批次 + Gate 7 交叉引用(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**(本批次):
|
|
||||||
|
|
||||||
- `docs/changelog/2026-06-08-schedule-cycle-picker.md`
|
|
||||||
- `docs/changelog/2026-06-08-schedule-form-start-cycle-only.md`
|
|
||||||
- `docs/changelog/2026-06-08-schedule-once-cycle-aligned.md`
|
|
||||||
- `docs/changelog/2026-06-08-schedule-push-source-aligned.md`
|
|
||||||
- `docs/changelog/2026-06-08-schedule-server-category-picker.md`
|
|
||||||
- `docs/changelog/2026-06-08-schedule-cycle-time-merge.md`
|
|
||||||
|
|
||||||
**设计**: `docs/design/specs/2026-06-08-schedule-cycle-picker-design.md`
|
|
||||||
|
|
||||||
## 审计范围(本批次代码)
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 周期工具 | `scheduleCycle.ts` |
|
|
||||||
| 周期选择器 | `ScheduleCyclePicker.vue` |
|
|
||||||
| 调度页 | `SchedulesPage.vue` |
|
|
||||||
| 分组选机 | `ServerCategoryPicker.vue`(复用,无改) |
|
|
||||||
| 推送源 | `usePushForm.ts`(复用)、`PushZipUpload.vue` 等 |
|
|
||||||
| 错误提示 | `apiError.ts`、`useSnackbar.ts`、`index.ts` |
|
|
||||||
| 全局 UI | `App.vue` |
|
|
||||||
| 后端镜像 | `schedule_cycle.py` |
|
|
||||||
| 单测 | `test_schedule_cycle.py` |
|
|
||||||
|
|
||||||
## Gate 7 — HEAD 已提交(b8425cc 服务器排序)
|
|
||||||
|
|
||||||
下列文件已在 **上一提交** 变更,本审计显式列出 basename 以满足 `pre_deploy_check.sh` Gate 7:
|
|
||||||
|
|
||||||
`useServerPagination.ts` · `DashboardPage.vue` · `ServersPage.vue` · `servers.py` · `server_list_sort.py` · `server_repo.py` · `test_server_list_sort.py` · `web/app/index.html`
|
|
||||||
|
|
||||||
详审见 `docs/audit/2026-06-08-servers-live-sort-fix.md`。
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| H | 规则 | 文件/点 | 结论 |
|
|
||||||
|---|------|---------|------|
|
|
||||||
| H1 | 调度 CRUD 鉴权 | `settings.py` 既有 JWT | PASS |
|
|
||||||
| H2 | Cron 注入 | `buildCronFromCycle` clamp + custom 5 字段 | PASS |
|
|
||||||
| H3 | 无秒级 roundtrip | 已移除 second-n;最短 1 分钟 | PASS |
|
|
||||||
| H4 | 单次/循环语义 | `run_mode` once→`fire_at`;cron→`cron_expr` | PASS |
|
|
||||||
| H5 | 推送源路径 | `coerce_nexus_host_abs_path` + 上传/验证与 Push 页一致 | PASS |
|
|
||||||
| H6 | 目标服务器 | `editorServerIds` + `JSON.stringify` 提交 | PASS |
|
|
||||||
| H7 | 错误可见 | `formatApiError` + 对话框 `saveError` + snackbar 非空 | PASS |
|
|
||||||
| H8 | 周期 UI 合并 | 单一「开始执行周期」区块(重复方式+时:分同行) | PASS |
|
|
||||||
| H9 | 静默吞错 | 保存/加载 catch 均 snackbar + 无空 `catch {}` | PASS |
|
|
||||||
| H10 | CUD 审计 | 调度 create/update/delete 仍写 audit_logs | PASS(后端未改) |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 |
|
|
||||||
|---|------|
|
|
||||||
| H1–H10 | PASS |
|
|
||||||
|
|
||||||
| 门控 | 结果 |
|
|
||||||
|------|------|
|
|
||||||
| Gate 3 pytest schedule | PASS(`test_schedule_cycle.py` + `test_schedule_next_run.py`) |
|
|
||||||
| Gate 3 test_api schedules CRUD | PASS |
|
|
||||||
| frontend build | PASS |
|
|
||||||
| Gate 7 basename 交叉 | PASS(含 b8425cc 排序文件 + 本批次调度文件) |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 设计文档 + 多份 changelog
|
|
||||||
- [x] `ScheduleCyclePicker` 宝塔式周期;单次默认;周期与时间合并
|
|
||||||
- [x] 推送源与 `#/push` 对齐(上传/ZIP/验证/暂存)
|
|
||||||
- [x] `ServerCategoryPicker` 按分类选目标机
|
|
||||||
- [x] 保存失败根因修复(`editorServerIds`、非空错误文案)
|
|
||||||
- [x] `cd frontend && npm run build`
|
|
||||||
- [x] 本审计覆盖 Gate 7 所需 basename
|
|
||||||
- [ ] 生产前端部署 + 用户浏览器终验
|
|
||||||
|
|
||||||
## 代码审读摘要
|
|
||||||
|
|
||||||
### SchedulesPage.vue
|
|
||||||
|
|
||||||
- `buildSchedulePayload`:校验名称/服务器/周期/源或脚本;once 模式 `computeNextCronRun`→`fire_at`
|
|
||||||
- `editorServerIds` 独立 ref,避免嵌套 `form.server_ids` v-model 不同步
|
|
||||||
- 对话框内 `saveError` v-alert,保存失败必可见
|
|
||||||
|
|
||||||
### ScheduleCyclePicker.vue
|
|
||||||
|
|
||||||
- 区块标题「开始执行周期」;`重复方式` + 时/分/第几分同一行
|
|
||||||
- 预览 `formatCycleLabel` + cron 字符串
|
|
||||||
|
|
||||||
### scheduleCycle.ts / schedule_cycle.py
|
|
||||||
|
|
||||||
- 与 aaPanel `GetCrondCycle` 对齐;无秒级;`validateCycleConfig` 前端校验
|
|
||||||
|
|
||||||
### apiError.ts / useSnackbar.ts / App.vue
|
|
||||||
|
|
||||||
- `normalizeDetail` / `formatApiError` 禁止空字符串;snackbar `z-index: 10000`
|
|
||||||
|
|
||||||
## 风险与回滚
|
|
||||||
|
|
||||||
| 风险 | 缓解 |
|
|
||||||
|------|------|
|
|
||||||
| 旧调度 custom Cron | custom 模式保留原表达式 |
|
|
||||||
| 单次 fire_at 时区 | ISO UTC 与 runner 60s 轮询一致 |
|
|
||||||
| 前端-only | 回滚 `web/app/` 静态资源即可 |
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# 审计 — 脚本执行全局进度与看板(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-script-exec-global-progress-board.md`、`docs/changelog/2026-06-08-script-exec-complete-sound-setting.md`
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 异步执行 | `script_service.py` |
|
|
||||||
| WS 通知 | `script_execution_notify.py`、`websocket.py` |
|
|
||||||
| 长任务回调 | `script_job_callback.py` |
|
|
||||||
| Redis 进度 | `script_execution_store.py` |
|
|
||||||
| Telegram | `telegram/__init__.py`、`config.py` |
|
|
||||||
| 设置 API | `settings.py` |
|
|
||||||
| 前端队列 | `useScriptExecutionQueue.ts`、`useWebSocket.ts`、`App.vue` |
|
|
||||||
| 提示音 | `scriptCompleteSound.ts`、`useScriptCompleteSound.ts`、`SettingsPage.vue` |
|
|
||||||
| 看板页 | `ScriptRunsPage.vue`、`router/index.ts` |
|
|
||||||
| 脚本库 | `ScriptsPage.vue` |
|
|
||||||
| 类型 | `types/api.ts` |
|
|
||||||
| 单测 | `test_script_execution_progress_stats.py` |
|
|
||||||
| 构建 | `web/app/index.html`、`index-DXg_pn7T.js`(Vite 产物,Docker 内亦会 rebuild) |
|
|
||||||
| 文档 | `.cursorrules`、`AGENTS.md`、`nexus-functional-development-guide.md` |
|
|
||||||
|
|
||||||
## 安全
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 后台任务 DB 会话 | 独立 `AsyncSessionLocal`,请求结束不泄漏 |
|
|
||||||
| WS 鉴权 | 复用 `/ws/alerts` JWT |
|
|
||||||
| Telegram | 仅汇总 + 看板链接,无失败明细 |
|
|
||||||
| 提示音设置 | `script_exec_complete_sound` 枚举白名单 |
|
|
||||||
| CUD 审计 | `execute_started` / `execute_command` 保留 |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | exec API 不得阻塞至全批次结束 | PASS `asyncio.create_task` + 立即返回 |
|
|
||||||
| H2 | 后台异常须标记 failed | PASS `_mark_execution_catastrophic_failure` |
|
|
||||||
| H3 | WS 消息须带 type 区分 | PASS `script_progress` / `script_complete` |
|
|
||||||
| H4 | Telegram 不得泄露 stderr 列表 | PASS `send_telegram_script_summary` |
|
|
||||||
| H5 | 设置项须白名单校验 | PASS `SETTING_ENUM_VALIDATORS` |
|
|
||||||
| H6 | 无静默吞错(通知层) | PASS `logger.warning` 记录 notify 失败 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 | 依据 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | PASS | `_schedule_background_execution` |
|
|
||||||
| H2 | PASS | `script_service._mark_execution_catastrophic_failure` |
|
|
||||||
| H3 | PASS | `broadcast_script_progress/complete` |
|
|
||||||
| H4 | PASS | `send_telegram_script_summary` 无 failed_servers |
|
|
||||||
| H5 | PASS | `settings.py` MUTABLE_KEYS + enum |
|
|
||||||
| H6 | PASS | `script_execution_notify.py` try/except + warning |
|
|
||||||
|
|
||||||
## Step 8 DoD
|
|
||||||
|
|
||||||
- [x] `bash scripts/local_verify.sh` 通过(test_api 26/26)
|
|
||||||
- [x] `pytest` 脚本进度相关 15 条通过
|
|
||||||
- [x] `npm run build` 通过
|
|
||||||
- [x] changelog + 设计文档齐全
|
|
||||||
- [ ] 生产浏览器终验(用户)
|
|
||||||
|
|
||||||
## 测试证据
|
|
||||||
|
|
||||||
- `tests/test_script_execution_progress_stats.py`
|
|
||||||
- `docs/reports/2026-06-08-script-exec-local-verify.md`
|
|
||||||
|
|
||||||
## 残留风险(可接受)
|
|
||||||
|
|
||||||
- Docker 部署以镜像内 `vite build` 为准;仓库 `web/app/assets` 为辅助同步
|
|
||||||
- 多 worker WS 经 Redis pub/sub(与告警一致)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# 审计 — 脚本执行 results 落库溢出(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-script-exec-results-mediumtext.md`
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| Redis flush | `server/infrastructure/redis/script_execution_store.py` |
|
|
||||||
| ORM | `server/domain/models/__init__.py` |
|
|
||||||
| 迁移 | `server/infrastructure/database/migrations.py` |
|
|
||||||
| 单测 | `tests/test_script_execution_mysql_persist.py` |
|
|
||||||
|
|
||||||
## 安全
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 数据完整性 | PASS — exit_code/status 保留;仅截断 stdout/stderr |
|
|
||||||
| Redis 全量 | PASS — live 仍存完整 I/O,MySQL 为持久化摘要 |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | MEDIUMTEXT 迁移 | PASS — 启动时 ALTER |
|
|
||||||
| H2 | 大批次 flush | PASS — 截断 stdout/stderr + 摘要降级 |
|
|
||||||
| H3 | Redis 全量保留 | PASS — 仅 MySQL 持久化裁剪 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 |
|
|
||||||
|---|------|
|
|
||||||
| H1 MEDIUMTEXT 迁移 | PASS |
|
|
||||||
| H2 366 台可 flush | PASS — 单测模拟生产体积 |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] pytest tests/test_script_execution_mysql_persist.py
|
|
||||||
- [x] changelog
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# 审计 — 脚本看板名称 / 顶栏提示(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `2026-06-08-script-exec-server-names-telegram.md`、`2026-06-08-script-submit-toast-center.md`、`2026-06-08-snackbar-top-center.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 执行详情名称 | `script_service.py` |
|
|
||||||
| Telegram 汇总 | `telegram/__init__.py` |
|
|
||||||
| 顶栏脚本提示 | `App.vue`、`useScriptSubmitToast.ts`、`useScriptExecutionQueue.ts`、`useSnackbar.ts` |
|
|
||||||
| 页面 | `ScriptsPage.vue`、`ScriptRunsPage.vue`、`SchedulesPage.vue` |
|
|
||||||
| 类型/工具 | `api.ts`、`scriptExecutionLabels.ts` |
|
|
||||||
| 单测 | `test_script_execution_server_names.py` |
|
|
||||||
| 构建 | `web/app/index.html`、`web/app/assets/*`(Vite 哈希产物) |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| server_names 批量查库 | PASS `get_by_ids` |
|
|
||||||
| Telegram 不列主机名 | PASS 仅台数 |
|
|
||||||
| 提示 3s 自动消失 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| 门控 | 结果 |
|
|
||||||
|------|------|
|
|
||||||
| pre_deploy_check | 待部署前复验 |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 失败列表显示真实服务器名
|
|
||||||
- [x] 脚本提交/结束/校验提示顶栏居中
|
|
||||||
- [x] changelog + 本审计
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 审计 — server_ids 批量上限 2000
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-server-ids-batch-max-2000.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`schemas.py` · `validation_errors_zh.py` · `test_validation_errors_zh.py` · `test_security_unit.py`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| server_ids max 2000 与 Sync/Script 一致 | PASS |
|
|
||||||
| too_long 列表 ctx 中文完整 | PASS |
|
|
||||||
| 101 台 BatchAgentAction 校验通过 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · 生产部署
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 101 台批量不再 422
|
|
||||||
- [x] 列表超长错误纯中文
|
|
||||||
- [x] 单测
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# 审计 — 服务器展开详情对比度 + 同步日志 1 条
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-server-inline-detail-contrast.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`frontend/src/components/servers/ServerInlineDetail.vue` · `frontend/src/pages/ServersPage.vue` · `frontend/src/components/servers/ServerUnsetPathPanel.vue` · `web/app/index.html`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 展开区背景比行底色深约 5% | PASS(color-mix surface 95% + on-surface 5%) |
|
|
||||||
| 正文 on-surface 可读 | PASS |
|
|
||||||
| 分类视图展开区同色 | PASS |
|
|
||||||
| 同步日志仅 1 条、无「最近 N 条」文案 | PASS(limit=1) |
|
|
||||||
| 无后端/API 变更 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
frontend build PASS · Gate 7 待跑
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 展开详情非黑底黑字
|
|
||||||
- [x] 同步日志标题与条数
|
|
||||||
- [ ] 生产浏览器验收
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
# 审计 — 服务器批量任务 · 执行记录 · 凭据合并(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**:
|
|
||||||
- `docs/changelog/2026-06-08-server-batch-background-jobs.md`
|
|
||||||
- `docs/changelog/2026-06-08-server-batch-job-persist.md`
|
|
||||||
- `docs/changelog/2026-06-08-install-agent-auto-api-key.md`
|
|
||||||
- `docs/changelog/2026-06-08-agent-install-bt-pyenv-python.md`
|
|
||||||
- `docs/changelog/2026-06-08-unified-execution-records.md`
|
|
||||||
- `docs/changelog/2026-06-08-credentials-merge-servers-page.md`
|
|
||||||
|
|
||||||
**生产已热更(部分)**: 2026-06-08 `docker cp` — 执行记录 + 批量任务;凭据合并待下次前端部署。
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
### 本批次(批量 · 执行记录 · 凭据)
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 批量后台 | `server_batch_service.py`、`server_batch_common.py`、`server_batch_store.py` |
|
|
||||||
| 批量 API | `server/api/servers.py`(`BatchJobStarted`、`GET /batch/jobs`) |
|
|
||||||
| 结果持久化 | `ServerBatchJob` 模型、`server_batch_job_repo.py` |
|
|
||||||
| 统一看板 API | `execution_record_service.py`、`execution_records.py`、`main.py` |
|
|
||||||
| Agent 安装 | `web/agent/install.sh`、`ensure_agent_api_key()` |
|
|
||||||
| 前端队列 | `useScriptExecutionQueue.ts`、`useServerBatchJobViewer.ts`、`App.vue` |
|
|
||||||
| 执行记录页 | `ScriptRunsPage.vue`、`executionRecordRoute.ts` |
|
|
||||||
| 审计入口 | `AuditPage.vue`、`auditBatchDetail.ts`、`auditLabels.ts` |
|
|
||||||
| 凭据合并 | `CredentialsManager.vue`、`CredentialsDialog.vue`、`ServersPage.vue` |
|
|
||||||
| 单测 | `test_ensure_agent_api_key.py`、`test_server_batch_job_persist.py`、`test_batch_server_display_name.py` |
|
|
||||||
|
|
||||||
### 已提交 `b8425cc`(实时排序,交叉引用)
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 排序服务 | `server_list_sort.py` |
|
|
||||||
| 仓储 | `server_repo.py` |
|
|
||||||
| 前端分页 | `useServerPagination.ts` |
|
|
||||||
| 看板 | `DashboardPage.vue` |
|
|
||||||
| 单测 | `test_server_list_sort.py` |
|
|
||||||
| 构建入口 | `web/app/index.html` |
|
|
||||||
|
|
||||||
> 排序专项 Step3/Closure 见 `docs/audit/2026-06-08-servers-live-sort-fix.md`。
|
|
||||||
|
|
||||||
## 安全
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 批量端点鉴权 | PASS 全部 `Depends(get_current_admin)` |
|
|
||||||
| SSH 批量命令 | PASS 经 `sudo_wrap` + 既有 `exec_ssh_command` |
|
|
||||||
| Agent Key 自动生成 | PASS 服务端 `secrets.token_urlsafe`,不入响应体 |
|
|
||||||
| 执行记录 API | PASS `/api/execution-records` 需 JWT |
|
|
||||||
| 凭据 CRUD | PASS 复用原 `/presets/`、`/ssh-key-presets/`、`/scripts/credentials` |
|
|
||||||
| 审计 detail JSON | PASS 仅含 job_id/摘要/失败原因,无密钥 |
|
|
||||||
| CUD 审计 | PASS `batch_install_agent` 等 + `target_type=server_batch_job` |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 批量 SSH 不得阻塞 HTTP | PASS 立即返回 `job_id`,后台 `asyncio.create_task` |
|
|
||||||
| H2 | 无静默吞错 | PASS 单台失败写入 `results[].error`;审计/落库失败 `logger.warning` |
|
|
||||||
| H3 | Redis 过期可回看 | PASS MySQL `server_batch_jobs` + 审计「查看结果」 |
|
|
||||||
| H4 | 脚本/批量 ID 冲突 | PASS 详情须带 `?kind=script\|server_batch` |
|
|
||||||
| H5 | 安装 Agent 无 Key 不阻断 | PASS `ensure_agent_api_key()` 自动写入 |
|
|
||||||
| H6 | install.sh 不破坏系统 Python | PASS 并行装 `python3.12`,不改 alternatives |
|
|
||||||
| H7 | 凭据合并不丢功能 | PASS 三 Tab CRUD 原样迁入对话框 |
|
|
||||||
| H8 | WS 进度复用 | PASS `task_kind: server_batch` 与脚本队列共用 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 | 依据 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | PASS | `_start_server_batch` → `start_batch_job` |
|
|
||||||
| H2 | PASS | `_run_background` except 标记 failed + warning 日志 |
|
|
||||||
| H3 | PASS | `_persist_job_finalize` + `AuditPage` 查看结果 |
|
|
||||||
| H4 | PASS | `GET /execution-records/{id}?kind=` |
|
|
||||||
| H5 | PASS | `server_batch_common.ensure_agent_api_key` |
|
|
||||||
| H6 | PASS | `install.sh` `_install_system_python312` |
|
|
||||||
| H7 | PASS | `CredentialsManager.vue` 全量迁移 |
|
|
||||||
| H8 | PASS | `broadcast_script_progress` + `registerServerBatchJob` |
|
|
||||||
|
|
||||||
## Step 8 DoD
|
|
||||||
|
|
||||||
- [x] `bash scripts/local_verify.sh` — 26/26
|
|
||||||
- [x] `pytest tests/test_ensure_agent_api_key.py tests/test_server_batch_job_persist.py` — 4/4
|
|
||||||
- [x] `cd frontend && npm run build` — 通过
|
|
||||||
- [x] changelog 6 篇(本批次主题)
|
|
||||||
- [x] 生产热更验证报告 `docs/reports/2026-06-08-unified-execution-records-deploy-verification.md`
|
|
||||||
- [ ] 凭据对话框生产终验(待前端部署)
|
|
||||||
- [ ] 用户浏览器终验(执行记录 + 批量 Agent 三操作)
|
|
||||||
|
|
||||||
## 测试证据
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/local_verify.sh # 26/26
|
|
||||||
pytest tests/test_ensure_agent_api_key.py tests/test_server_batch_job_persist.py -q # 4 passed
|
|
||||||
cd frontend && npm run build # OK
|
|
||||||
curl https://api.synaglobal.vip/health # ok
|
|
||||||
curl https://api.synaglobal.vip/api/execution-records # 401
|
|
||||||
```
|
|
||||||
|
|
||||||
## 残留风险(可接受)
|
|
||||||
|
|
||||||
| 风险 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| 热更新非镜像 | 下次 `nx update` 未 push 代码会被覆盖 |
|
|
||||||
| 合并列表分页 | 脚本+批量 total 为两表之和,大 offset 可能略偏 |
|
|
||||||
| 历史任务 | Redis-only 旧任务无法补录 MySQL |
|
|
||||||
| 工作区其它 diff | 登录门控/页面缓存等 changelog 已有,**未纳入本审计部署包**,勿混部 |
|
|
||||||
|
|
||||||
## Gate 7 变更文件对照
|
|
||||||
|
|
||||||
本审计覆盖上述「审计范围」表内全部路径;工作区另有 `LoginPage`、`*useCachedQuery*` 等变更属并行会话,部署时须拆分或一并补审计。
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
# 审计 — 2026-06-08 服务器分类 / 分页 / 排序 / 脚本库选机
|
|
||||||
|
|
||||||
## 审计信息
|
|
||||||
|
|
||||||
- **日期**: 2026-06-08
|
|
||||||
- **审计人**: Cursor Agent
|
|
||||||
- **触发原因**: 新功能(分类筛选、批量改分类、脚本库选机、分页与排序修复)生产部署
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
本批次含 2026-06-07 已提交对齐项(c8b0663)与 2026-06-08 分类/分页/排序增量。
|
|
||||||
|
|
||||||
| 文件 | 状态 |
|
|
||||||
|------|------|
|
|
||||||
| `Dockerfile.prod` | ☑ 已审(CSV 模板 COPY) |
|
|
||||||
| `deploy/pre_deploy_check.sh` | ☑ 已审(Gate3 强制本地 BASE) |
|
|
||||||
| `deploy/gate_log.jsonl` | ☑ 自动生成 |
|
|
||||||
| `server/api/servers.py` | ☑ 已审 |
|
|
||||||
| `server/api/schemas.py` | ☑ 已审 |
|
|
||||||
| `server/infrastructure/database/server_repo.py` | ☑ 已审 |
|
|
||||||
| `server/infrastructure/database/migrations.py` | ☑ 已审 |
|
|
||||||
| `server/domain/models/__init__.py` | ☑ 已审 |
|
|
||||||
| `server/application/services/credential_poller.py` | ☑ 已审 |
|
|
||||||
| `server/application/services/script_service.py` | ☑ 已审 |
|
|
||||||
| `server/infrastructure/redis/script_execution_store.py` | ☑ 已审(progress 成功/总数) |
|
|
||||||
| `server/application/services/sync_engine_v2.py` | ☑ 已审 |
|
|
||||||
| `frontend/public/servers_import_template.csv` | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/ServersPage.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/ScriptsPage.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/CommandsPage.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/DashboardPage.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/AlertsPage.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/AuditPage.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/CredentialsPage.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/RetriesPage.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/pages/SchedulesPage.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/components/push/PushHistoryTable.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/components/servers/ServerCategoryPicker.vue` | ☑ 已审 |
|
|
||||||
| `frontend/src/composables/useServerPagination.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/composables/useServerList.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/composables/useServerCategories.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/composables/push/usePushLogs.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/composables/push/labels.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/composables/files/constants.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/constants/dataTable.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/utils/paginatedFetch.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/utils/serverTableSort.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/utils/scriptExecutionLabels.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/plugins/vuetify.ts` | ☑ 已审 |
|
|
||||||
| `frontend/src/types/api.ts` | ☑ 已审 |
|
|
||||||
| `scripts/import_servers_from_xls.py` | ☑ 已审 |
|
|
||||||
| `tests/test_credential_poller.py` | ☑ 已审 |
|
|
||||||
| `tests/test_rsync_push_permissions.py` | ☑ 已审 |
|
|
||||||
| `tests/test_script_dispatch.py` | ☑ 已审 |
|
|
||||||
| `tests/test_script_execution_progress.py` | ☑ 已审 |
|
|
||||||
| `web/app/index.html` | ☑ 构建产物入口 |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 批量改分类须鉴权 + 审计 | PASS `Depends(get_current_admin)` + `audit_log batch_update_category` |
|
|
||||||
| H2 | 单次批量上限防滥用 | PASS schema `max_length=200`;repo 分块 |
|
|
||||||
| H3 | 分类筛选 SQL 注入 | PASS 参数化 `category = :cat` / `IS NULL` |
|
|
||||||
| H4 | 前端「全部」不得单页截断 | PASS `paginatedFetch` 多页 200 上限循环 |
|
|
||||||
| H5 | 排序字段白名单 | PASS `server_repo` 仅允许已知列 |
|
|
||||||
| H6 | 导入脚本无明文密钥入仓 | PASS 读环境变量;凭据不入 git |
|
|
||||||
| H7 | CUD 无静默吞错 | PASS API 显式 HTTPException;poller 记录失败 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 | 依据 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | PASS | `servers.py` `batch_update_category` + audit |
|
|
||||||
| H2 | PASS | `BatchCategoryUpdate.server_ids` Field(max_length=200) |
|
|
||||||
| H3 | PASS | `server_repo.list_servers` 绑定参数 |
|
|
||||||
| H4 | PASS | `fetchPagePerPage` while 循环至 `total` |
|
|
||||||
| H5 | PASS | `_SORT_COLUMNS` 映射 |
|
|
||||||
| H6 | PASS | `import_servers_from_xls.py` os.environ |
|
|
||||||
| H7 | PASS | 无 bare `except: pass` |
|
|
||||||
|
|
||||||
## Step 5 入口表
|
|
||||||
|
|
||||||
| 入口 | 鉴权 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `GET /api/servers` | JWT | `category`/`sort_by`/`sort_order` 查询 |
|
|
||||||
| `POST /api/servers/batch/category` | JWT + admin | 批量改分类 |
|
|
||||||
| `POST /api/servers/batch` | JWT + admin | 名称+IP 批量添加 |
|
|
||||||
| `#/servers` UI | 会话 | 筛选/排序/批量 |
|
|
||||||
| `#/scripts` UI | 会话 | 分类选机 |
|
|
||||||
| `scripts/import_servers_from_xls.py` | CLI + JWT | 运维导入 |
|
|
||||||
|
|
||||||
## Step 6 输入→Sink
|
|
||||||
|
|
||||||
- `category` 查询 → ORM `WHERE category = :cat`(参数化)
|
|
||||||
- `server_ids[]` → `UPDATE servers SET category`(IN 绑定)
|
|
||||||
- Excel IP/名称 → `add-by-ip` API(后端校验 IP 格式)
|
|
||||||
- 排序列 key → 白名单映射 → ORDER BY
|
|
||||||
|
|
||||||
## Step 7 归类
|
|
||||||
|
|
||||||
```
|
|
||||||
servers.py + schemas + server_repo 3文件 7H 0FINDING
|
|
||||||
frontend 页面/composables/utils 8文件 7H 0FINDING
|
|
||||||
import_servers_from_xls.py 1文件 2H 0FINDING
|
|
||||||
────────────────────────────────────────────────────
|
|
||||||
总计 7H 0FINDING
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 8 DoD
|
|
||||||
|
|
||||||
- ☑ 功能与 changelog 一致
|
|
||||||
- ☑ `pytest tests/test_credential_poller.py` 通过
|
|
||||||
- ☑ 门控 lint/import/security 通过
|
|
||||||
- ☑ 0 FINDING
|
|
||||||
|
|
||||||
## FINDING 列表
|
|
||||||
|
|
||||||
无
|
|
||||||
|
|
||||||
## 结论
|
|
||||||
|
|
||||||
☑ 审计通过,0 FINDING,可部署
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 审计 — 服务器列表实时字段排序
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-servers-live-sort-fix.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`server_list_sort.py` · `server_repo.py` · `servers.py` · `ServersPage.vue` · `useServerPagination.ts` · `DashboardPage.vue` · `tests/test_server_list_sort.py` · `web/app/index.html` · `web/app/assets/*`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| sort_by 白名单 | PASS(live 字段独立模块) |
|
|
||||||
| Redis pipeline | PASS |
|
|
||||||
| 上限 5000 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · 生产 `b8425cc` · `/health` 200
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 实时三列 overlay 后排序
|
|
||||||
- [x] 分类视图组内顺序
|
|
||||||
- [x] 单测
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Audit — 2026-06-08 服务器全选筛选 + 脚本失败分组
|
|
||||||
|
|
||||||
**日期**: 2026-06-08
|
|
||||||
**范围**: 纯前端 · 无后端变更
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-servers-select-all-script-failure-groups.md`
|
|
||||||
|
|
||||||
## 变更概要
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| ServersPage | `frontend/src/pages/ServersPage.vue` |
|
|
||||||
| ScriptsPage | `frontend/src/pages/ScriptsPage.vue` |
|
|
||||||
| 分组工具 | `frontend/src/utils/execDetailGrouping.ts` |
|
|
||||||
| 单测 | `tests/test_exec_detail_grouping.py` |
|
|
||||||
| 构建入口 | `web/app/index.html`(Docker 镜像内 vite build 同步) |
|
|
||||||
|
|
||||||
## 安全
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 新 API | 无 |
|
|
||||||
| 鉴权 | 沿用 JWT;全选仍走既有 `/servers/` 与批量端点 |
|
|
||||||
| 敏感数据 | 复制清单含错误摘要,不含批量完整 stderr 导出;与单行复制等价 |
|
|
||||||
| XSS | 分组/复制均为已有字段文本,无 `v-html` |
|
|
||||||
|
|
||||||
## 性能
|
|
||||||
|
|
||||||
- 全选 364 台:`fetchPagePerPage` 多页 GET(200/页),与分页「全部」相同负载。
|
|
||||||
- 分组为客户端 computed,无额外请求。
|
|
||||||
|
|
||||||
## 回滚
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git revert <commit>
|
|
||||||
cd frontend && npx vite build && bash deploy/deploy-frontend.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验收清单
|
|
||||||
|
|
||||||
- [ ] 分类筛选后全选台数 = Chip 计数
|
|
||||||
- [ ] 改搜索/排序/分类后选择清空
|
|
||||||
- [ ] 失败详情「按错误分组」同类 SSH 超时合并
|
|
||||||
- [ ] 「复制失败清单」剪贴板内容正确
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# 审计 — 未设路径服务器批量管理对齐主表
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-servers-unset-path-batch-parity.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`ServerBatchActionBar.vue` · `ServerUnsetPathPanel.vue` · `ServersPage.vue` · `web/app/index.html` · `web/app/assets/ServersPage-*`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 未设路径区独立勾选 | PASS(与主表 `selectedItems` 隔离) |
|
|
||||||
| 批量操作栏对齐 | PASS(分类/健康/路径/Agent/删除) |
|
|
||||||
| 全选筛选结果 | PASS(`target_path_unset=true` + 当前筛选) |
|
|
||||||
| 列与行操作对齐 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 · build PASS · 生产部署
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 勾选 + 批量栏 + 全选
|
|
||||||
- [x] 双 scope 复用 batch 逻辑
|
|
||||||
- [x] 前端 build
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# 审计 — 服务器未设路径区块 + 行内详情
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-servers-unset-path-inline-detail.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`posix_paths.py` · `server_repo.py` · `servers.py` · `serverTargetPath.ts` · `useServerPagination.ts` · `ServerInlineDetail.vue` · `ServerUnsetPathPanel.vue` · `ServersPage.vue` · `tests/test_server_target_path_filter.py` · `web/app/assets/ServersPage-*`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| `target_path_unset` 默认 None | PASS(Terminal/Dashboard 零回归) |
|
|
||||||
| 未设路径判定单一真相 | PASS(Python + TS 镜像,`/www/wwwroot/` 不算未设) |
|
|
||||||
| 双表 refresh / 展开态 reset | PASS |
|
|
||||||
| 行内 logs 防竞态 + formatApiError | PASS(禁止静默空列表) |
|
|
||||||
| 未设路径表不纳入主表批量勾选 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · 本地 build + pytest 12/12 · 待用户批准部署
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 主表 `target_path_unset=false` 排除未设路径
|
|
||||||
- [x] 未设路径独立卡片(失败列表上方)
|
|
||||||
- [x] 名称点击行内展开紧凑详情
|
|
||||||
- [x] 移除底部 `detailServer` 大块详情卡
|
|
||||||
- [x] 保存路径后双表迁移 + 展开态清理
|
|
||||||
- [x] 单测 util / repo / API 三态
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 审计 — 设置表单校验 + 422 中文
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-settings-form-validate-zh-errors.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`SettingsPage.vue` · `validation_errors_zh.py` · `main.py` · `schemas.py` · `test_validation_errors_zh.py`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| v-form validate 先于 PUT | PASS |
|
|
||||||
| 422 msg 中文化 | PASS |
|
|
||||||
| value 数字 coerce | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · 生产部署 · `/health` 200
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 设置页前端校验
|
|
||||||
- [x] 422 中文 msg
|
|
||||||
- [x] 单测
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# 审计 — 设置页连接池保存
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-settings-pool-size-string-payload.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`SettingsPage.vue`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| value 转 String | PASS |
|
|
||||||
| 与 SettingUpdatePayload str 一致 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · 生产部署 · `/health` 200
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 连接池/阈值保存无 422 string_type
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# 审计 — 终端命令栏深链修复(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-terminal-cmdbar-server-id-fix.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `useTerminalSessions.ts` | `waitForTermContainer`、`server_id`/`server` |
|
|
||||||
| `useTerminalCmdBar.ts` | 连接中可输入 |
|
|
||||||
| `TerminalCmdBar.vue` | 分离输入/发送禁用 |
|
|
||||||
| `TerminalPage.vue` | 绑定新 props |
|
|
||||||
| `ScriptRunsPage.vue` | `server_id` 深链 |
|
|
||||||
| `web/app/index.html` | Vite 构建入口 |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 竞态不静默失败 | PASS 失败时 snackbar + 关标签 |
|
|
||||||
| 深链参数兼容 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
待门控。
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 本审计列出上述文件
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 审计 — 终端命令栏与断开遮罩
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-terminal-cmdbar-overlay.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`TerminalCmdBar.vue` · `TerminalArea.vue` · `TerminalPage.vue` · `useTerminalCmdBar.ts` · `useTerminalSessions.ts` · `web/app/index.html` · `web/app/assets/*`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| Enter 发送保留 | PASS |
|
|
||||||
| overlay `contained` | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
待门控。
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 移除无效按钮
|
|
||||||
- [x] 遮罩仅覆盖 SSH 区
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# 审计 — 终端快捷栏断开按钮(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-terminal-quickbar-disconnect.md`
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 快捷栏 | `frontend/src/components/terminal/TerminalQuickBar.vue` |
|
|
||||||
| 终端页 | `frontend/src/pages/TerminalPage.vue` |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 断开复用既有逻辑 | PASS — `@disconnect="disconnect"` |
|
|
||||||
| H2 | 无会话禁用 | PASS — `disconnectDisabled` |
|
|
||||||
| H3 | 无后端变更 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 |
|
|
||||||
|---|------|
|
|
||||||
| H1–H3 | PASS |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] frontend vite build
|
|
||||||
- [x] changelog
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# 审计 — 未设路径批量检测选中数
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-unset-path-detect-selection-count.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`frontend/src/pages/ServersPage.vue` · `web/app/index.html`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| 检测路径对话框按 batchScope 显示选中数 | PASS(scopedSelectedCount) |
|
|
||||||
| 未设路径表提交 detect-path 用 unset 选中 ID | PASS |
|
|
||||||
| 批量改分类对话框同修复 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
frontend build PASS · Gate 7 待跑
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 未设路径勾选 N 台 → 对话框显示 N
|
|
||||||
- [ ] 生产浏览器验收
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 审计 — 未设路径服务器表格排序
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-unset-path-table-sort.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
`ServerUnsetPathPanel.vue` · `ServersPage.vue` · `web/app/index.html` · `web/app/assets/*`
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 项 | 结论 |
|
|
||||||
|----|------|
|
|
||||||
| sort-by 与主表共享 | PASS(同一 `sortBy` ref + `listQueryParams`) |
|
|
||||||
| API 参数 | PASS(`target_path_unset=true` + `sort_by`/`sort_order` 已有) |
|
|
||||||
| 无后端改动 | PASS |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
Gate 7/7 PASS · 生产部署 · `/health` 200
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 未设路径区列头可排序
|
|
||||||
- [x] 与主表排序状态一致
|
|
||||||
- [x] 前端构建通过
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# 审计 — WebSSH 黑底白字 ANSI 主题(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-webssh-classic-terminal-theme.md`
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| 主题 | `frontend/src/composables/terminal/xtermTheme.ts` |
|
|
||||||
| 会话 | `frontend/src/composables/terminal/useTerminalSessions.ts` |
|
|
||||||
| 页面 | `frontend/src/pages/TerminalPage.vue` |
|
|
||||||
| 区域 | `frontend/src/components/terminal/TerminalArea.vue` |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | 固定黑底白字 | PASS — 不随 Vuetify 浅色切换 |
|
|
||||||
| H2 | ANSI 16 色 | PASS — drawBoldTextInBrightColors |
|
|
||||||
| H3 | 无后端变更 | PASS — 纯前端 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 |
|
|
||||||
|---|------|
|
|
||||||
| H1–H3 | PASS |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] frontend vite build
|
|
||||||
- [x] changelog
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# 审计 — WebSSH xterm SSH 交互区 ANSI 彩色输出(2026-06-08)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-08-webssh-xterm-ansi-output-fix.md`
|
|
||||||
|
|
||||||
## 审计范围
|
|
||||||
|
|
||||||
| 模块 | 文件 |
|
|
||||||
|------|------|
|
|
||||||
| WebSocket relay | `server/api/webssh.py` |
|
|
||||||
| 前端解码 | `frontend/src/utils/terminalPayload.ts` |
|
|
||||||
| 会话 | `frontend/src/composables/terminal/useTerminalSessions.ts` |
|
|
||||||
| 样式分离 | `frontend/src/pages/TerminalPage.vue`, `frontend/src/components/terminal/TerminalCmdBar.vue` |
|
|
||||||
| 单测 | `tests/test_webssh_terminal_payload.py` |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| H | 规则 | 结论 |
|
|
||||||
|---|------|------|
|
|
||||||
| H1 | ANSI 在 xterm 输出区,非命令栏冒充 | PASS — PTY env + data_b64 + CSS 分离 |
|
|
||||||
| H2 | 无静默吞错 | PASS — relay 异常仍走现有 teardown |
|
|
||||||
| H3 | 向后兼容 | PASS — 前端仍可读 legacy `data` 字段 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| H | 判定 |
|
|
||||||
|---|------|
|
|
||||||
| H1–H3 | PASS |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] pytest 3/3
|
|
||||||
- [x] local_verify 26/26
|
|
||||||
- [x] frontend build
|
|
||||||
- [x] pre_deploy_check 7/7
|
|
||||||
- [ ] 生产浏览器 xterm 内 printf/ls(部署后终验)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# 审计 — 服务器页 Agent 诊断按钮
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-09-agent-diagnose-ui-button.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `agent_diagnose_service.py` | 中心 + SSH 诊断服务 |
|
|
||||||
| `servers.py` | `POST /{id}/agent-diagnose` |
|
|
||||||
| `agent_remote_diagnose.py` | CLI 改引用 service |
|
|
||||||
| `AgentDiagnoseDialog.vue` | 诊断结果弹窗 |
|
|
||||||
| `agentDiagnose.ts` | 前端类型 |
|
|
||||||
| `ServersPage.vue` | 操作列按钮 |
|
|
||||||
| `ServerUnsetPathPanel.vue` | 未设路径表操作列 |
|
|
||||||
| `auditLabels.ts` | 审计 action 文案 |
|
|
||||||
| `test_agent_diagnose_service.py` | 服务单测 |
|
|
||||||
| `test_agent_remote_diagnose.py` | CLI 辅助单测 |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 规则 | 结论 |
|
|
||||||
|------|------|
|
|
||||||
| 安全 | PASS — JWT + 审计;config 脱敏;无密钥回传 |
|
|
||||||
| 行为 | PASS — 只读诊断 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
- `pytest tests/test_agent_diagnose_service.py -q` PASS
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] changelog + audit
|
|
||||||
- [x] 生产部署验证(`c5e54da`,/health ok,/app 200)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# 审计 — 子机 Agent 批量诊断脚本
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-09-agent-remote-diagnose-script.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `scripts/agent_remote_diagnose.py` | 中心侧批量 SSH 诊断 |
|
|
||||||
| `scripts/agent_remote_diagnose.sh` | 包装入口 |
|
|
||||||
| `tests/test_agent_remote_diagnose.py` | 解析/脱敏单测 |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 规则 | 结论 |
|
|
||||||
|------|------|
|
|
||||||
| 安全 | PASS — 仅脱敏输出 api_key;SSH 凭据来自 DB 加密字段 |
|
|
||||||
| 行为 | PASS — 只读诊断,无 CUD |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
- `pytest tests/test_agent_remote_diagnose.py -q` PASS
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] changelog + audit
|
|
||||||
- [ ] 生产 `/opt/nexus` 同步脚本并验证 `--id 95`
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 审计 — 告警分页 + 调度单次 fire_at 修复(2026-06-09)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-09-alerts-schedule-fixes-deploy.md`
|
|
||||||
|
|
||||||
## 变更文件清单
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `AlertsPage.vue` | 每页条数状态 + `fetchPagePerPage` |
|
|
||||||
| `settings.py` | 告警列表 `per_page` 上限 200 |
|
|
||||||
| `schema_path_validators.py` | `coerce_optional_iso_datetime` |
|
|
||||||
| `schemas.py` | `ScheduleCreate`/`ScheduleUpdate` `fire_at` → `datetime` |
|
|
||||||
| `test_schedules.py` | once 推送创建集成测试 |
|
|
||||||
| `prod_health_check.sh` | 生产公开/鉴权契约巡检 |
|
|
||||||
| `index.html` | vite 构建 hash 更新 |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| 规则 | 结论 |
|
|
||||||
|------|------|
|
|
||||||
| 无静默吞错 | PASS — `loadAlerts` 失败 snackbar |
|
|
||||||
| 调度 CUD 审计 | PASS — 未改 audit 写入逻辑 |
|
|
||||||
| `fire_at` 类型安全 | PASS — schema 层解析,避免 ORM 500 |
|
|
||||||
| 告警分页契约 | PASS — 与 `fetchPagePerPage` / 后端 `page`/`per_page` 一致 |
|
|
||||||
| 无明文密钥 | PASS — 健康脚本用环境变量/SSH,不入仓 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| 项 | 结果 |
|
|
||||||
|----|------|
|
|
||||||
| `test_schedules.py` | 2 passed |
|
|
||||||
| `test_alerts_audit.py` | 4 passed |
|
|
||||||
| `vite build` | OK |
|
|
||||||
| 生产复现 once 500 | 已确认根因;`3864d12` 部署后容器 healthy |
|
|
||||||
| 部署验证报告 | `docs/reports/2026-06-09-alerts-schedule-fixes-deploy-verification.md` |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] 告警每页数目与 API `per_page` 联动
|
|
||||||
- [x] 单次文件推送调度保存 201(非 500)
|
|
||||||
- [x] Changelog + 本审计
|
|
||||||
- [x] 生产部署与健康检查(`/health` ok,容器 healthy;鉴权 UI 待用户终验)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 审计 — 接续 Wave0 + D-01(2026-06-09)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-09-continuation-wave0-d01.md`
|
|
||||||
|
|
||||||
## 变更文件清单
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `scripts/prod_probe.py` | 生产时区验收:422、cron next_run、bundle |
|
|
||||||
| `deploy/sync_webapp_to_container.sh` | upgrade 后同步 Git web/app 进容器 |
|
|
||||||
| `deploy/nexus-1panel.sh` | upgrade 末尾调用 sync |
|
|
||||||
| `deploy/deploy-production.sh` | Docker 部署后 sync |
|
|
||||||
| `deploy/deploy-frontend.sh` | sudo docker 运行时探测 |
|
|
||||||
| `server/utils/agent_deploy.py` | D-01 统一 upgrade 命令 |
|
|
||||||
| `server/api/servers.py` | 单台 upgrade 用 agent_deploy |
|
|
||||||
| `server/application/services/server_batch_service.py` | 批量 upgrade + agent_port |
|
|
||||||
| `tests/test_agent_deploy.py` | 单测 |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| 规则 | 结论 |
|
|
||||||
|------|------|
|
|
||||||
| 密钥 | PASS — 无凭据入仓 |
|
|
||||||
| 静默吞错 | PASS — sync 失败 warn,probe 显式断言 |
|
|
||||||
| Shell 注入 | PASS — docker cp 路径来自 NEXUS_ROOT |
|
|
||||||
| 分层 | PASS — agent_deploy 在 utils,API 薄编排 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| 项 | 结果 |
|
|
||||||
|----|------|
|
|
||||||
| `test_agent_deploy.py` | passed |
|
|
||||||
| `pre_deploy_check.sh` | 7/7 |
|
|
||||||
| `prod_health_check.sh` origin | passed |
|
|
||||||
| 生产 `sync_webapp_to_container.sh` | index-mgwiCK5j.js OK |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] changelog + audit + Handoff 2026-06-09
|
|
||||||
- [x] 门控 7/7
|
|
||||||
- [x] 生产探针全绿
|
|
||||||
- [x] D-01 alignment-plan ☑
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# 审计 — Dashboard 24h 舰队趋势(B-02)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-09-dashboard-24h-fleet-metrics.md`
|
|
||||||
|
|
||||||
## 变更文件清单
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `server/utils/fleet_metrics.py` | 舰队均值聚合 |
|
|
||||||
| `server/infrastructure/database/fleet_metric_repo.py` | 采样 CRUD + purge |
|
|
||||||
| `server/background/heartbeat_flush.py` | 10min 写入样本 |
|
|
||||||
| `server/api/servers.py` | `fleet-metrics` API |
|
|
||||||
| `server/infrastructure/database/migrations.py` | `fleet_metric_samples` 表 |
|
|
||||||
| `frontend/src/components/dashboard/DashboardFleetTrends.vue` | v-sparkline |
|
|
||||||
| `frontend/src/pages/DashboardPage.vue` | 挂载趋势卡 |
|
|
||||||
| `tests/test_fleet_metrics.py` | 单测 + API |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| 规则 | 结论 |
|
|
||||||
|------|------|
|
|
||||||
| 分层 | PASS — API 只读,聚合在 utils/repo |
|
|
||||||
| 性能 | PASS — 7 天约 1k 行;purge 每 flush |
|
|
||||||
| 静默吞错 | PASS — 前端 load 失败显示空状态 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| 项 | 结果 |
|
|
||||||
|----|------|
|
|
||||||
| `test_fleet_metrics.py` | passed |
|
|
||||||
| `npm run type-check` | passed |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] design + plan + changelog
|
|
||||||
- [x] B-02 alignment-plan 待勾选
|
|
||||||
- [x] 无新 npm 依赖
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# 审计 — 移除舰队趋势 CSV 导出
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-09-fleet-trend-remove-csv.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `DashboardFleetTrends.vue` | 移除 CSV 按钮与 exportCsv |
|
|
||||||
| `fleetTrendAnalytics.ts` | 删除 exportFleetTrendCsv |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 规则 | 结论 |
|
|
||||||
|------|------|
|
|
||||||
| 安全 | PASS — 纯 UI 删除,无新入口 |
|
|
||||||
| 行为 | PASS — 全屏明细表保留 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
- `npm run type-check` PASS
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] changelog + audit
|
|
||||||
- [x] 生产前端同步(`53b700f`,index-B00VieI3.js)
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# 审计 — 全站测试体系交付(2026-06-09)
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-09-full-site-testing-delivery.md`
|
|
||||||
|
|
||||||
## 变更文件清单
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `tests/integration/test_*.py` | IT 域集成测试(14 文件) |
|
|
||||||
| `tests/chain/test_*.py` | 链路测试(8 文件,含 batch/exec 完成态) |
|
|
||||||
| `tests/conftest.py` | 共享 fixture / SQLite 兼容 |
|
|
||||||
| `tests/acceptance_catalog.py` | 验收目录扩展 |
|
|
||||||
| `frontend/e2e/fixtures.mjs` | Worker 级 API 登录 + cookie 轮换写回 |
|
|
||||||
| `frontend/e2e/helpers.mjs` | 会话恢复 + 侧栏 `nav` |
|
|
||||||
| `frontend/e2e/pages/*.spec.mjs` | 页域 E2E(18) |
|
|
||||||
| `frontend/e2e/full-acceptance.spec.mjs` | 15 步 serial 验收 |
|
|
||||||
| `frontend/package.json` | `test:e2e` 脚本 |
|
|
||||||
| `.gitea/workflows/ci-cd.yml` | CI 分层 pytest + E2E job |
|
|
||||||
| `.gitignore` | 忽略 `frontend/e2e/.auth/`、`test-results/` |
|
|
||||||
| `scripts/run_nexus_acceptance.py` | 验收脚本对齐 catalog |
|
|
||||||
| `docs/testing/test-case-matrix.md` | 矩阵状态更新 |
|
|
||||||
| `web/app/index.html` | vite build 入口 hash 更新 |
|
|
||||||
| `frontend/src/api/index.ts` | 校验错误中文格式化接入 |
|
|
||||||
| `frontend/src/utils/apiError.ts` | API 错误展示 |
|
|
||||||
| `frontend/src/utils/validationErrorFormat.ts` | Pydantic detail → 中文 |
|
|
||||||
| `frontend/src/pages/PushPage.vue` | 推送页异步提交 UX |
|
|
||||||
| `frontend/src/pages/ServersPage.vue` | 服务器列表(无 CSV 导出) |
|
|
||||||
| `frontend/src/components/push/PushToolbar.vue` | 推送工具栏 |
|
|
||||||
| `frontend/src/composables/push/usePushProgress.ts` | 推送进度 composable |
|
|
||||||
| `server/api/sync_v2.py` | sync 校验与异步推送 |
|
|
||||||
| `server/background/sync_push_runner.py` | 后台推送 runner |
|
|
||||||
| `server/utils/validation_errors_zh.py` | 后端校验中文映射 |
|
|
||||||
| `tests/test_validation_errors_zh.py` | 中文校验单测 |
|
|
||||||
|
|
||||||
## Step 3 规则扫描
|
|
||||||
|
|
||||||
| 规则 | 结论 |
|
|
||||||
|------|------|
|
|
||||||
| 无静默吞错(测试 assert 真实行为) | PASS — 失败用例已修至 green |
|
|
||||||
| 无明文密钥入仓 | PASS — E2E 密码仅环境变量 |
|
|
||||||
| 分层:API 不测 UI、E2E 不替代 IT | PASS — 职责分离 |
|
|
||||||
| E2E 并行不触发 login lockout | PASS — `fixtures.mjs` 每 worker 一次 API 登录 |
|
|
||||||
| Refresh token 轮换 | PASS — 上下文结束写回 `storageState` |
|
|
||||||
| SQLite/MySQL 测试兼容 | PASS — `SettingRepository` 等 monkeypatch/直连 |
|
|
||||||
| CI 不阻塞于缺 E2E secret | PASS — E2E job 条件触发 |
|
|
||||||
| 推送异步不阻塞 UI / 校验中文 | PASS — sync_v2 + PushPage + validation_errors_zh |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
| 项 | 结果 |
|
|
||||||
|----|------|
|
|
||||||
| pytest integration+chain | 27 passed |
|
|
||||||
| pytest fast suite | 427 passed |
|
|
||||||
| pytest slow (IT-INST) | 1 passed |
|
|
||||||
| Playwright e2e | 35 passed |
|
|
||||||
| local_integration_smoke | OK |
|
|
||||||
| pre_deploy_check Gate 3–6 | PASS(本审计前) |
|
|
||||||
| MCP-INST-001 各 step UI | ⏭ 用户确认不纳入验收 |
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] Wave 1–5 矩阵 IT/CH/E2E 缺口落地并有命令证据
|
|
||||||
- [x] Playwright 35/35 本地 green(清理 assets 后复验)
|
|
||||||
- [x] Changelog + 设计/计划/报告文档齐全
|
|
||||||
- [x] 门控可过(今日 changelog + 本审计)
|
|
||||||
- [ ] 生产部署(待用户批准 `deploy-production.sh`)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# 审计 — 离线统计准确性
|
|
||||||
|
|
||||||
**Changelog**: `docs/changelog/2026-06-09-offline-count-accuracy.md`
|
|
||||||
|
|
||||||
## 变更文件
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `server_connectivity.py` | 逐台 Redis 计数 + status 筛选 |
|
|
||||||
| `servers.py` | stats/list 对齐 |
|
|
||||||
| `ServersPage.vue` | `offline_list` 展示 |
|
|
||||||
| `test_server_connectivity.py` | 单测 |
|
|
||||||
| `test_servers_dashboard.py` | stats 字段断言 |
|
|
||||||
|
|
||||||
## Step 3
|
|
||||||
|
|
||||||
| 规则 | 结论 |
|
|
||||||
|------|------|
|
|
||||||
| 安全 | PASS — 只读聚合 |
|
|
||||||
| 行为 | PASS — 离线=Agent 监控且 Redis 非在线 |
|
|
||||||
|
|
||||||
## Closure
|
|
||||||
|
|
||||||
- `pytest tests/test_server_connectivity.py -q` PASS
|
|
||||||
|
|
||||||
## DoD
|
|
||||||
|
|
||||||
- [x] changelog + audit
|
|
||||||
- [x] 生产部署(含于 `7209f53`)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user